diff --git a/helm/datareplicate/Chart.yaml b/helm/datareplicate/Chart.yaml index 5db972724..f3313fd59 100644 --- a/helm/datareplicate/Chart.yaml +++ b/helm/datareplicate/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.12 +version: 0.1.13 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm/datareplicate/README.md b/helm/datareplicate/README.md index 960a459ec..3ea02ce0f 100644 --- a/helm/datareplicate/README.md +++ b/helm/datareplicate/README.md @@ -1,6 +1,6 @@ # datareplicate -![Version: 0.1.12](https://img.shields.io/badge/Version-0.1.12-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.13](https://img.shields.io/badge/Version-0.1.13-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 datareplicate diff --git a/helm/datareplicate/templates/aws-batch-replication-job.yaml b/helm/datareplicate/templates/aws-batch-replication-job.yaml index ea8b3d630..f6d634f6c 100644 --- a/helm/datareplicate/templates/aws-batch-replication-job.yaml +++ b/helm/datareplicate/templates/aws-batch-replication-job.yaml @@ -72,7 +72,7 @@ spec: volumeMounts: - name: cred-volume mountPath: "/root/.aws/credentials" - subPath: dcf-aws-fence-bot-secret + subPath: credentials - name: "setting-volume" mountPath: "/secrets/dcf_dataservice_settings.py" subPath: "dcf-dataservice-settings-secrets" @@ -80,7 +80,6 @@ spec: args: - -c - | - mkdir ~/.aws echo """ [default] region: $REGION diff --git a/helm/gen3-workflow/Chart.yaml b/helm/gen3-workflow/Chart.yaml index f962a6979..21f9075f8 100644 --- a/helm/gen3-workflow/Chart.yaml +++ b/helm/gen3-workflow/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.7 +version: 0.1.8 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,7 +24,7 @@ appVersion: "master" dependencies: - name: common - version: 0.1.28 + version: 0.1.29 repository: file://../common - name: funnel # NOTE: @@ -34,5 +34,5 @@ dependencies: # # ArgoCD relies on this checked-in .tgz reference — if it's missing, # Funnel will not be deployed as a dependency. - version: 0.1.58 + version: 0.1.71 repository: "https://ohsu-comp-bio.github.io/helm-charts" diff --git a/helm/gen3-workflow/README.md b/helm/gen3-workflow/README.md index 075edfdb1..a1c600005 100644 --- a/helm/gen3-workflow/README.md +++ b/helm/gen3-workflow/README.md @@ -1,6 +1,6 @@ # gen3-workflow -![Version: 0.1.7](https://img.shields.io/badge/Version-0.1.7-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.8](https://img.shields.io/badge/Version-0.1.8-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for Kubernetes @@ -8,8 +8,8 @@ A Helm chart for Kubernetes | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.28 | -| https://ohsu-comp-bio.github.io/helm-charts | funnel | 0.1.58 | +| file://../common | common | 0.1.29 | +| https://ohsu-comp-bio.github.io/helm-charts | funnel | 0.1.71 | ## Values @@ -47,14 +47,8 @@ A Helm chart for Kubernetes | externalSecrets.gen3workflowG3auto | string | `""` | Will override the name of the aws secrets manager secret. Default is "gen3workflow-g3auto" | | extraLabels | map | `{"dbgen3workflow":"yes","netnolimit":"yes","public":"yes"}` | Will completely override the extraLabels defined in the common chart's _label_setup.tpl | | fullnameOverride | string | `""` | Override the full name of the chart, which is used as the name of resources created by the chart | -| funnel.Kubernetes.ExecutorTemplate | string | `"# Task Executor\napiVersion: batch/v1\nkind: Job\nmetadata:\n name: {{.TaskId}}-{{.JobId}}\n namespace: {{.JobsNamespace}}\n labels:\n app: funnel-executor\n job-name: {{.TaskId}}-{{.JobId}}\nspec:\n backoffLimit: 1\n completions: 1\n template:\n spec:\n restartPolicy: OnFailure\n serviceAccountName: funnel-sa-{{.Namespace}}\n containers:\n - name: funnel-worker-{{.TaskId}}\n image: {{.Image}}\n imagePullPolicy: Always\n command: [\"/bin/sh\", \"-c\"]\n args: {{.Command}}\n workingDir: {{.Workdir}}\n resources:\n requests:\n cpu: {{if ne .Cpus 0 -}}{{.Cpus}}{{ else }}{{\"100m\"}}{{end}}\n memory: '{{if ne .RamGb 0.0 -}}{{printf \"%.0fG\" .RamGb}}{{else}}{{\"4G\"}}{{end}}'\n ephemeral-storage: '{{if ne .DiskGb 0.0 -}}{{printf \"%.0fG\" .DiskGb}}{{else}}{{\"2G\"}}{{end}}'\n\n volumeMounts:\n ### DO NOT CHANGE THIS\n {{- if .NeedsPVC }}\n {{range $idx, $item := .Volumes}}\n - name: funnel-storage-{{$.TaskId}}\n mountPath: {{$item.ContainerPath}}\n subPath: {{$.TaskId}}{{$item.ContainerPath}}\n {{end}}\n {{- end }}\n\n volumes:\n {{- if .NeedsPVC }}\n - name: funnel-storage-{{.TaskId}}\n persistentVolumeClaim:\n claimName: funnel-worker-pvc-{{.TaskId}}\n {{- end }}\n"` | | -| funnel.Plugins.Params.OidcClientId | string | `""` | | -| funnel.Plugins.Params.OidcClientSecret | string | `""` | | -| funnel.Plugins.Params.OidcTokenUrl | string | `"https://{{ .Values.gen3WorkflowConfig.hostname }}/user"` | OIDC token URL for the Funnel service to use for authentication. Replace {{ .Values.gen3WorkflowConfig.hostname }} with the actual hostname where gen3-workflow is deployed. | -| funnel.Plugins.Params.S3Url | string | `"gen3-workflow-service.{{ .Release.Namespace }}.svc.cluster.local"` | | -| funnel.Plugins.Path | string | `"plugin-binaries/auth-plugin"` | | -| funnel.image | map | `{"initContainers":[{"command":["cp","/app/build/plugins/authorizer","/opt/funnel/plugin-binaries/auth-plugin"],"image":"quay.io/cdis/funnel-gen3-plugin","name":"plugin","pullPolicy":"Always","tag":"main-gen3","volumeMounts":[{"mountPath":"/opt/funnel/plugin-binaries","name":"plugin-volume"}]},{"args":["-c","echo \"Priting FUNNEL_OIDC_CLIENT_ID: $FUNNEL_OIDC_CLIENT_ID\"\n\necho \"Patching values...\"\n\n# Assuming we don't have any other occurence of OidcClientId in the config file\nsed -E \"s|(OidcClientId:).*|\\1 ${FUNNEL_OIDC_CLIENT_ID}|\" /etc/config/funnel.conf \\\n| sed -E \"s|(OidcClientSecret:).*|\\1 ${FUNNEL_OIDC_CLIENT_SECRET}|\" > /tmp/funnel-patched.conf\n\nif [[ ! -s /tmp/funnel-patched.conf ]]; then\n echo \"ERROR: Patched config is empty. Aborting.\"\n exit 1\nfi\n"],"command":["/bin/bash"],"env":[{"name":"FUNNEL_OIDC_CLIENT_ID","valueFrom":{"secretKeyRef":{"key":"client_id","name":"funnel-oidc-client","optional":false}}},{"name":"FUNNEL_OIDC_CLIENT_SECRET","valueFrom":{"secretKeyRef":{"key":"client_secret","name":"funnel-oidc-client","optional":false}}}],"image":"quay.io/cdis/awshelper","name":"secrets-updater","tag":"master","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"},{"mountPath":"/etc/config/funnel.conf","name":"funnel-config-volume","subPath":"funnel-server.yaml"}]}],"pullPolicy":"Always","repository":"quay.io/ohsu-comp-bio/funnel"}` | Configuration for the Funnel container image. | -| funnel.image.initContainers | map | `[{"command":["cp","/app/build/plugins/authorizer","/opt/funnel/plugin-binaries/auth-plugin"],"image":"quay.io/cdis/funnel-gen3-plugin","name":"plugin","pullPolicy":"Always","tag":"main-gen3","volumeMounts":[{"mountPath":"/opt/funnel/plugin-binaries","name":"plugin-volume"}]},{"args":["-c","echo \"Priting FUNNEL_OIDC_CLIENT_ID: $FUNNEL_OIDC_CLIENT_ID\"\n\necho \"Patching values...\"\n\n# Assuming we don't have any other occurence of OidcClientId in the config file\nsed -E \"s|(OidcClientId:).*|\\1 ${FUNNEL_OIDC_CLIENT_ID}|\" /etc/config/funnel.conf \\\n| sed -E \"s|(OidcClientSecret:).*|\\1 ${FUNNEL_OIDC_CLIENT_SECRET}|\" > /tmp/funnel-patched.conf\n\nif [[ ! -s /tmp/funnel-patched.conf ]]; then\n echo \"ERROR: Patched config is empty. Aborting.\"\n exit 1\nfi\n"],"command":["/bin/bash"],"env":[{"name":"FUNNEL_OIDC_CLIENT_ID","valueFrom":{"secretKeyRef":{"key":"client_id","name":"funnel-oidc-client","optional":false}}},{"name":"FUNNEL_OIDC_CLIENT_SECRET","valueFrom":{"secretKeyRef":{"key":"client_secret","name":"funnel-oidc-client","optional":false}}}],"image":"quay.io/cdis/awshelper","name":"secrets-updater","tag":"master","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"},{"mountPath":"/etc/config/funnel.conf","name":"funnel-config-volume","subPath":"funnel-server.yaml"}]}]` | Configuration for the Funnel init container. | +| funnel.image | map | `{"initContainers":[{"command":["cp","/app/build/plugins/authorizer","/opt/funnel/plugin-binaries/auth-plugin"],"image":"quay.io/cdis/funnel-gen3-plugin","name":"plugin","pullPolicy":"Always","tag":"main-gen3","volumeMounts":[{"mountPath":"/opt/funnel/plugin-binaries","name":"plugin-volume"}]},{"args":["while [ $(curl -sw '%{http_code}' http://fence-service -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for fence...'; done; curl -s 'http://fence-service/.well-known/openid-configuration' > /tmp/openid-config.json"],"command":["/bin/sh","-c"],"image":"curlimages/curl","imagePullPolicy":"IfNotPresent","name":"fence-url-fetcher","tag":"latest","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"}]},{"args":["-c","echo \"Priting FUNNEL_OIDC_CLIENT_ID: $FUNNEL_OIDC_CLIENT_ID\"\n\nnamespace=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)\nexport JOBS_NAMESPACE=gen3-$namespace-workflow-pods\nexport S3_URL=gen3-workflow-service.$namespace.svc.cluster.local\nexport OIDC_TOKEN_URL=$(cat /tmp/openid-config.json | jq -r \".issuer\")\n\necho \"Patching values...\"\n\nyq -y '.Kubernetes.JobsNamespace = env.JOBS_NAMESPACE | .Plugins = {\n \"Path\": \"plugin-binaries/auth-plugin\",\n \"Params\": {\n \"OidcClientId\": env.FUNNEL_OIDC_CLIENT_ID,\n \"OidcClientSecret\": env.FUNNEL_OIDC_CLIENT_SECRET,\n \"OidcTokenUrl\": env.OIDC_TOKEN_URL,\n \"S3Url\": env.S3_URL\n }\n}' /etc/config/funnel.conf > /tmp/funnel-patched.conf\n\nif [[ ! -s /tmp/funnel-patched.conf ]]; then\n echo \"ERROR: Patched config is empty. Aborting.\"\n exit 1\nfi\n"],"command":["/bin/bash"],"env":[{"name":"FUNNEL_OIDC_CLIENT_ID","valueFrom":{"secretKeyRef":{"key":"client_id","name":"funnel-oidc-client","optional":false}}},{"name":"FUNNEL_OIDC_CLIENT_SECRET","valueFrom":{"secretKeyRef":{"key":"client_secret","name":"funnel-oidc-client","optional":false}}}],"image":"quay.io/cdis/awshelper","name":"secrets-updater","tag":"master","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"},{"mountPath":"/etc/config/funnel.conf","name":"funnel-config-volume","subPath":"funnel-server.yaml"}]}],"pullPolicy":"Always","repository":"quay.io/ohsu-comp-bio/funnel"}` | Configuration for the Funnel container image. | +| funnel.image.initContainers | map | `[{"command":["cp","/app/build/plugins/authorizer","/opt/funnel/plugin-binaries/auth-plugin"],"image":"quay.io/cdis/funnel-gen3-plugin","name":"plugin","pullPolicy":"Always","tag":"main-gen3","volumeMounts":[{"mountPath":"/opt/funnel/plugin-binaries","name":"plugin-volume"}]},{"args":["while [ $(curl -sw '%{http_code}' http://fence-service -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for fence...'; done; curl -s 'http://fence-service/.well-known/openid-configuration' > /tmp/openid-config.json"],"command":["/bin/sh","-c"],"image":"curlimages/curl","imagePullPolicy":"IfNotPresent","name":"fence-url-fetcher","tag":"latest","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"}]},{"args":["-c","echo \"Priting FUNNEL_OIDC_CLIENT_ID: $FUNNEL_OIDC_CLIENT_ID\"\n\nnamespace=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)\nexport JOBS_NAMESPACE=gen3-$namespace-workflow-pods\nexport S3_URL=gen3-workflow-service.$namespace.svc.cluster.local\nexport OIDC_TOKEN_URL=$(cat /tmp/openid-config.json | jq -r \".issuer\")\n\necho \"Patching values...\"\n\nyq -y '.Kubernetes.JobsNamespace = env.JOBS_NAMESPACE | .Plugins = {\n \"Path\": \"plugin-binaries/auth-plugin\",\n \"Params\": {\n \"OidcClientId\": env.FUNNEL_OIDC_CLIENT_ID,\n \"OidcClientSecret\": env.FUNNEL_OIDC_CLIENT_SECRET,\n \"OidcTokenUrl\": env.OIDC_TOKEN_URL,\n \"S3Url\": env.S3_URL\n }\n}' /etc/config/funnel.conf > /tmp/funnel-patched.conf\n\nif [[ ! -s /tmp/funnel-patched.conf ]]; then\n echo \"ERROR: Patched config is empty. Aborting.\"\n exit 1\nfi\n"],"command":["/bin/bash"],"env":[{"name":"FUNNEL_OIDC_CLIENT_ID","valueFrom":{"secretKeyRef":{"key":"client_id","name":"funnel-oidc-client","optional":false}}},{"name":"FUNNEL_OIDC_CLIENT_SECRET","valueFrom":{"secretKeyRef":{"key":"client_secret","name":"funnel-oidc-client","optional":false}}}],"image":"quay.io/cdis/awshelper","name":"secrets-updater","tag":"master","volumeMounts":[{"mountPath":"/tmp","name":"funnel-patched-config-volume"},{"mountPath":"/etc/config/funnel.conf","name":"funnel-config-volume","subPath":"funnel-server.yaml"}]}]` | Configuration for the Funnel init container. | | funnel.image.initContainers[0].command | list | `["cp","/app/build/plugins/authorizer","/opt/funnel/plugin-binaries/auth-plugin"]` | Arguments to pass to the init container. | | funnel.image.initContainers[0].image | string | `"quay.io/cdis/funnel-gen3-plugin"` | The Docker image repository for the Funnel init/plugin container. | | funnel.image.initContainers[0].pullPolicy | string | `"Always"` | When to pull the image. This value should be "Always" to ensure the latest image is used. | @@ -66,6 +60,8 @@ A Helm chart for Kubernetes | funnel.mongodb.readinessProbe.initialDelaySeconds | int | `20` | | | funnel.mongodb.readinessProbe.periodSeconds | int | `10` | | | funnel.mongodb.readinessProbe.timeoutSeconds | int | `10` | | +| funnel.resources.requests.ephemeral_storage | string | `"2Gi"` | | +| funnel.resources.requests.memory | string | `"2Gi"` | | | funnel.volumeMounts[0].mountPath | string | `"/etc/config/funnel-server.yaml"` | | | funnel.volumeMounts[0].name | string | `"funnel-patched-config-volume"` | | | funnel.volumeMounts[0].subPath | string | `"funnel-patched.conf"` | | diff --git a/helm/gen3-workflow/charts/funnel-0.1.58.tgz b/helm/gen3-workflow/charts/funnel-0.1.58.tgz deleted file mode 100644 index e67b9d0d7..000000000 Binary files a/helm/gen3-workflow/charts/funnel-0.1.58.tgz and /dev/null differ diff --git a/helm/gen3-workflow/charts/funnel-0.1.71.tgz b/helm/gen3-workflow/charts/funnel-0.1.71.tgz new file mode 100644 index 000000000..af5429d52 Binary files /dev/null and b/helm/gen3-workflow/charts/funnel-0.1.71.tgz differ diff --git a/helm/gen3-workflow/templates/crossplane.yaml b/helm/gen3-workflow/templates/crossplane.yaml index abb04947d..6de4ab75d 100644 --- a/helm/gen3-workflow/templates/crossplane.yaml +++ b/helm/gen3-workflow/templates/crossplane.yaml @@ -79,7 +79,8 @@ spec: "iam:PutRolePolicy", "iam:GetRole", "iam:GetRolePolicy", - "iam:TagRole" + "iam:TagRole", + "iam:UpdateAssumeRolePolicy" ], "Resource": [ "arn:aws:iam::*:role/gen3wf-*", diff --git a/helm/gen3-workflow/templates/jobs-namespace.yaml b/helm/gen3-workflow/templates/jobs-namespace.yaml index 2203b134a..d1f5f9fa7 100644 --- a/helm/gen3-workflow/templates/jobs-namespace.yaml +++ b/helm/gen3-workflow/templates/jobs-namespace.yaml @@ -1,8 +1,10 @@ -{{- if and .Values.funnel.Kubernetes.JobsNamespace (ne .Values.funnel.Kubernetes.JobsNamespace .Release.Namespace) -}} +{{- $jobsNamespace := default (printf "gen3-%s-workflow-pods" .Release.Namespace) .Values.funnel.Kubernetes.JobsNamespace }} + +{{- if ne $jobsNamespace .Release.Namespace }} apiVersion: v1 kind: Namespace metadata: - name: {{ .Values.funnel.Kubernetes.JobsNamespace | quote }} + name: {{ $jobsNamespace | quote }} labels: - app.kubernetes.io/name: {{ .Values.funnel.Kubernetes.JobsNamespace | quote }} + app.kubernetes.io/name: {{ $jobsNamespace | quote }} {{- end }} diff --git a/helm/gen3-workflow/templates/netpolicy.yaml b/helm/gen3-workflow/templates/netpolicy.yaml index 1978641e0..9e539c387 100644 --- a/helm/gen3-workflow/templates/netpolicy.yaml +++ b/helm/gen3-workflow/templates/netpolicy.yaml @@ -11,7 +11,7 @@ {{ if .Values.global.netPolicy.enabled }} -{{ $jobsNamespace := .Values.funnel.Kubernetes.JobsNamespace | default .Release.Namespace }} +{{- $jobsNamespace := default (printf "gen3-%s-workflow-pods" .Release.Namespace) .Values.funnel.Kubernetes.JobsNamespace }} --- # Funnel needs both ingress and egress to/from gen3-workflow and funnel-mongodb diff --git a/helm/gen3-workflow/templates/secrets.yaml b/helm/gen3-workflow/templates/secrets.yaml index f9cfbcc1c..f8fb0a0f6 100644 --- a/helm/gen3-workflow/templates/secrets.yaml +++ b/helm/gen3-workflow/templates/secrets.yaml @@ -45,7 +45,7 @@ stringData: # EKS CLUSTER # ################# - WORKER_PODS_NAMESPACE: {{ .Values.funnel.Kubernetes.JobsNamespace | default .Release.Namespace }} + WORKER_PODS_NAMESPACE: {{ .Values.funnel.Kubernetes.JobsNamespace | default (printf "gen3-%s-workflow-pods" .Release.Namespace) }} EKS_CLUSTER_NAME: {{ .Values.global.clusterName }} EKS_CLUSTER_REGION: {{ .Values.global.aws.region }} {{- end }} diff --git a/helm/gen3-workflow/values.yaml b/helm/gen3-workflow/values.yaml index dc5e8c5c4..ca37e3ac2 100644 --- a/helm/gen3-workflow/values.yaml +++ b/helm/gen3-workflow/values.yaml @@ -378,6 +378,16 @@ funnel: volumeMounts: - name: plugin-volume mountPath: /opt/funnel/plugin-binaries + - name: fence-url-fetcher + image: curlimages/curl + tag: latest + imagePullPolicy: IfNotPresent + command: ["/bin/sh", "-c"] + args: + - "while [ $(curl -sw '%{http_code}' http://fence-service -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for fence...'; done; curl -s 'http://fence-service/.well-known/openid-configuration' > /tmp/openid-config.json" + volumeMounts: + - name: funnel-patched-config-volume + mountPath: /tmp - name: secrets-updater image: quay.io/cdis/awshelper tag: master @@ -406,11 +416,22 @@ funnel: - | echo "Priting FUNNEL_OIDC_CLIENT_ID: $FUNNEL_OIDC_CLIENT_ID" + namespace=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + export JOBS_NAMESPACE=gen3-$namespace-workflow-pods + export S3_URL=gen3-workflow-service.$namespace.svc.cluster.local + export OIDC_TOKEN_URL=$(cat /tmp/openid-config.json | jq -r ".issuer") + echo "Patching values..." - # Assuming we don't have any other occurence of OidcClientId in the config file - sed -E "s|(OidcClientId:).*|\1 ${FUNNEL_OIDC_CLIENT_ID}|" /etc/config/funnel.conf \ - | sed -E "s|(OidcClientSecret:).*|\1 ${FUNNEL_OIDC_CLIENT_SECRET}|" > /tmp/funnel-patched.conf + yq -y '.Kubernetes.JobsNamespace = env.JOBS_NAMESPACE | .Plugins = { + "Path": "plugin-binaries/auth-plugin", + "Params": { + "OidcClientId": env.FUNNEL_OIDC_CLIENT_ID, + "OidcClientSecret": env.FUNNEL_OIDC_CLIENT_SECRET, + "OidcTokenUrl": env.OIDC_TOKEN_URL, + "S3Url": env.S3_URL + } + }' /etc/config/funnel.conf > /tmp/funnel-patched.conf if [[ ! -s /tmp/funnel-patched.conf ]]; then echo "ERROR: Patched config is empty. Aborting." @@ -455,53 +476,11 @@ funnel: - name: plugin-volume mountPath: /opt/funnel/plugin-binaries - Kubernetes: - ExecutorTemplate: | - # Task Executor - apiVersion: batch/v1 - kind: Job - metadata: - name: {{.TaskId}}-{{.JobId}} - namespace: {{.JobsNamespace}} - labels: - app: funnel-executor - job-name: {{.TaskId}}-{{.JobId}} - spec: - backoffLimit: 1 - completions: 1 - template: - spec: - restartPolicy: OnFailure - serviceAccountName: funnel-sa-{{.Namespace}} - containers: - - name: funnel-worker-{{.TaskId}} - image: {{.Image}} - imagePullPolicy: Always - command: ["/bin/sh", "-c"] - args: {{.Command}} - workingDir: {{.Workdir}} - resources: - requests: - cpu: {{if ne .Cpus 0 -}}{{.Cpus}}{{ else }}{{"100m"}}{{end}} - memory: '{{if ne .RamGb 0.0 -}}{{printf "%.0fG" .RamGb}}{{else}}{{"4G"}}{{end}}' - ephemeral-storage: '{{if ne .DiskGb 0.0 -}}{{printf "%.0fG" .DiskGb}}{{else}}{{"2G"}}{{end}}' - - volumeMounts: - ### DO NOT CHANGE THIS - {{- if .NeedsPVC }} - {{range $idx, $item := .Volumes}} - - name: funnel-storage-{{$.TaskId}} - mountPath: {{$item.ContainerPath}} - subPath: {{$.TaskId}}{{$item.ContainerPath}} - {{end}} - {{- end }} - - volumes: - {{- if .NeedsPVC }} - - name: funnel-storage-{{.TaskId}} - persistentVolumeClaim: - claimName: funnel-worker-pvc-{{.TaskId}} - {{- end }} + resources: + requests: + memory: "2Gi" + ephemeral_storage: "2Gi" + mongodb: # This overrides the default mongodb image used by Funnel which doesn't support ARM architecture, # uncomment this if you're running it on an ARM chipset machine @@ -516,16 +495,6 @@ funnel: periodSeconds: 10 failureThreshold: 10 - Plugins: - Path: plugin-binaries/auth-plugin - Params: - OidcClientId: - OidcClientSecret: - # Replace {{ .Release.Namespace }} with the actual namespace where gen3-workflow is deployed - S3Url: gen3-workflow-service.{{ .Release.Namespace }}.svc.cluster.local - # -- (string) OIDC token URL for the Funnel service to use for authentication. Replace {{ .Values.gen3WorkflowConfig.hostname }} with the actual hostname where gen3-workflow is deployed. - OidcTokenUrl: https://{{ .Values.gen3WorkflowConfig.hostname }}/user - karpenter: nodeclass.yaml: | apiVersion: karpenter.k8s.aws/v1 diff --git a/helm/gen3/Chart.yaml b/helm/gen3/Chart.yaml index 079015239..a5aedd524 100644 --- a/helm/gen3/Chart.yaml +++ b/helm/gen3/Chart.yaml @@ -44,7 +44,7 @@ dependencies: repository: file://../dashboard condition: dashboard.enabled - name: datareplicate - version: 0.1.12 + version: 0.1.13 repository: "file://../datareplicate" condition: datareplicate.enabled - name: embedding-management-service @@ -68,7 +68,7 @@ dependencies: repository: "file://../gen3-user-data-library" condition: gen3-user-data-library.enabled - name: gen3-workflow - version: 0.1.7 + version: 0.1.8 repository: "file://../gen3-workflow" condition: gen3-workflow.enabled - name: guppy @@ -104,7 +104,7 @@ dependencies: repository: "file://../requestor" condition: requestor.enabled - name: revproxy - version: 0.1.49 + version: 0.1.50 repository: "file://../revproxy" condition: revproxy.enabled - name: sheepdog @@ -177,7 +177,7 @@ type: application # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.115 +version: 0.2.118 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm/gen3/README.md b/helm/gen3/README.md index 44d913d7d..5620873c6 100644 --- a/helm/gen3/README.md +++ b/helm/gen3/README.md @@ -1,6 +1,6 @@ # gen3 -![Version: 0.2.115](https://img.shields.io/badge/Version-0.2.115-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.2.118](https://img.shields.io/badge/Version-0.2.118-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) Helm chart to deploy Gen3 Data Commons @@ -28,7 +28,7 @@ Helm chart to deploy Gen3 Data Commons | file://../cohort-middleware | cohort-middleware | 0.1.16 | | file://../common | common | 0.1.29 | | file://../dashboard | dashboard | 0.1.13 | -| file://../datareplicate | datareplicate | 0.1.12 | +| file://../datareplicate | datareplicate | 0.1.13 | | file://../dicom-server | dicom-server | 0.1.23 | | file://../embedding-management-service | embedding-management-service | 0.1.1 | | file://../etl | etl | 0.1.19 | @@ -37,7 +37,7 @@ Helm chart to deploy Gen3 Data Commons | file://../gen3-analysis | gen3-analysis | 0.1.4 | | file://../gen3-network-policies | gen3-network-policies | 0.1.3 | | file://../gen3-user-data-library | gen3-user-data-library | 0.1.9 | -| file://../gen3-workflow | gen3-workflow | 0.1.7 | +| file://../gen3-workflow | gen3-workflow | 0.1.8 | | file://../guppy | guppy | 0.1.30 | | file://../hatchery | hatchery | 0.1.61 | | file://../indexd | indexd | 0.1.37 | @@ -49,7 +49,7 @@ Helm chart to deploy Gen3 Data Commons | file://../peregrine | peregrine | 0.1.36 | | file://../portal | portal | 0.1.50 | | file://../requestor | requestor | 0.1.28 | -| file://../revproxy | revproxy | 0.1.49 | +| file://../revproxy | revproxy | 0.1.50 | | file://../sheepdog | sheepdog | 0.1.36 | | file://../sower | sower | 0.1.39 | | file://../ssjdispatcher | ssjdispatcher | 0.1.38 | diff --git a/helm/revproxy/Chart.yaml b/helm/revproxy/Chart.yaml index 358d44cbd..51cb1a41f 100644 --- a/helm/revproxy/Chart.yaml +++ b/helm/revproxy/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.49 +version: 0.1.50 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -25,5 +25,5 @@ appVersion: "master" dependencies: - name: common - version: 0.1.28 + version: 0.1.29 repository: file://../common diff --git a/helm/revproxy/README.md b/helm/revproxy/README.md index 227d9c686..e5f503049 100644 --- a/helm/revproxy/README.md +++ b/helm/revproxy/README.md @@ -1,6 +1,6 @@ # revproxy -![Version: 0.1.49](https://img.shields.io/badge/Version-0.1.49-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.50](https://img.shields.io/badge/Version-0.1.50-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 revproxy @@ -8,7 +8,7 @@ A Helm chart for gen3 revproxy | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.28 | +| file://../common | common | 0.1.29 | ## Values @@ -18,6 +18,7 @@ A Helm chart for gen3 revproxy | autoscaling | object | `{}` | | | commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | | criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| enableRobotsTxt | bool | `false` | Whether to enable robots.txt generation and serving. | | extraServices | map | `nil` | Configuration to add any extra service endpoints outside of gen3 to be served by revproxy | | fullnameOverride | string | `""` | Override the full name of the deployment. | | global.autoscaling.averageCPUValue | string | `"500m"` | | diff --git a/helm/revproxy/gen3.nginx.conf/robots/robots-txt.conf b/helm/revproxy/gen3.nginx.conf/robots/robots-txt.conf new file mode 100644 index 000000000..9ea7ad24d --- /dev/null +++ b/helm/revproxy/gen3.nginx.conf/robots/robots-txt.conf @@ -0,0 +1,4 @@ + location /robots.txt { + default_type text/plain; + return 200 "User-agent: *\nDisallow: /\n"; + } diff --git a/helm/revproxy/nginxPrivate/helpers.js b/helm/revproxy/nginxPrivate/helpers.js new file mode 100644 index 000000000..9dcb8d524 --- /dev/null +++ b/helm/revproxy/nginxPrivate/helpers.js @@ -0,0 +1,283 @@ +/** + * This is a helper script used in the reverse proxy + * Note that this is not technically javascript, but nginscript (or njs) + * See here for info: + * - http://nginx.org/en/docs/njs/ + * - https://www.nginx.com/blog/introduction-nginscript/ + */ + +/** global supporting atob polyfill below */ +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; //pragma: allowlist secret +// default threshold for assigning a service to production +// e.g. weight of 0 would mean all services are assigned to production +var DEFAULT_WEIGHT = 0; + +/** + * base64 decode polyfill from + * https://github.com/davidchambers/Base64.js/blob/master/base64.js + */ +function atob(input) { + var str = String(input).replace(/[=]+$/, ''); // #31: ExtendScript bad parse of /= + if (str.length % 4 == 1) { + return input; + } + for ( + // initialize result and counters + var bc = 0, bs, buffer, idx = 0, output = ''; + // get next character + buffer = str.charAt(idx++); + // character found in table? initialize bit storage and add its ascii value; + ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, + // and if not first of each 4 characters, + // convert the first 8 bits to one ascii character + bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 + ) { + // try to find character in table (0-63, not found => -1) + buffer = chars.indexOf(buffer); + } + return output; +} + +/** + * nginscript helper for parsing user out of JWT tokens. + * We appear to have access to the 'access_token' variable + * defined in nginx.conf when this function runs via 'js_set'. + * see https://www.nginx.com/blog/introduction-nginscript/ + * + * @param {*} req + * @param {*} res + */ +function userid(req, res) { + var token = req.variables["access_token"]; + var user = "uid:null,unknown@unknown"; + + if (token) { + // note - raw token is secret, so do not expose in userid + var raw = atob((token.split('.')[1] || "").replace('-', '+').replace('_', '/')); + if (raw) { + try { + var data = JSON.parse(raw); + if (data) { + if (data.context && data.context.user && data.context.user.name) { + user = "uid:" + data.sub + "," + data.context.user.name; + } + } + } catch (err) {} + } + } + return user; +} + +/** + * returns absolute value of a number + */ +function MathAbs(x) { + x = +x; + return (x > 0) ? x : 0 - x; +} + +/** + * util for hashing a string into given range + * Source: http://pmav.eu/stuff/javascript-hashing-functions/source.html + * + * @param s - string to hash + */ +function simpleHash(s) { + var i, hash = 0; + for (i = 0; i < s.length; i++) { + hash += (s[i].charCodeAt() * (i+1)); + } + // mod 100 b/c we want a percentage range (ie 0-99) + return MathAbs(hash) % 100; +} + +/** + * Returns a release (string) depending on the given + * values provided + * + * @param hash_res - an integer to compare to service_weight + * @param service_weight - integer threshold for assigning release as 'production' + * @param default_weight - if service_weight is undefined, compare hash to this value + * @returns {string} - release + */ +function selectRelease(hash_res, w) { + // determine release by comparing hash val to service weight + if (hash_res < parseInt(w)) { + return 'canary'; + } + return 'production'; +} + +function getWeight(service, weights) { + if (typeof weights[service] === 'undefined') { + return weights['default']; + } + return weights[service]; +} + +function releasesObjToString(releases) { + var res = ''; + for (var service in releases) { + if (releases.hasOwnProperty(service)) { + res = res + service + '.' + releases[service] + '&'; + } + } + return res; +} + +/** + * Checks cookie (dev_canaries or service_releases) + * for service release versions and assigns + * release versions for services not in the cookie based + * on hash value and the percent weight of the canary. + * If the weight for a service is 0, it ignores the cookie + * and sets the release to production. + * + * @param req - nginx request object + * @return a string of service assignments. E.g: + * "fence.canary&sheepdog.production&" + */ +function getServiceReleases(req) { + // + // client cookie containing releases + // developer override can force canary even when canary has + // been deployed for general users by setting the canary weights to zero + // + var devOverride= !!req.variables['cookie_dev_canaries']; + var release_cookie = req.variables['cookie_dev_canaries'] || req.variables['cookie_service_releases'] || ''; + // services to assign to a service (edit this if adding a new canary service) + var services = ['fence', 'fenceshib', 'sheepdog', 'indexd', 'peregrine']; + // weights for services - if given a default weight, use it; else use the default weight from this file + var canary_weights = JSON.parse(req.variables['canary_percent_json']); + if (typeof canary_weights['default'] === 'undefined') { + canary_weights['default'] = DEFAULT_WEIGHT + } else { + canary_weights['default'] = parseInt(canary_weights['default']) + } + // the string to be hashed + var hash_str = ['app', req.variables['realip'], req.variables['http_user_agent'], req.variables['date_gmt']].join(); + var hash_res = -1; + + // for each service: + // if it's weight == 0, ignore the cookie and set release to production + // else if it's in the cookie, use that release + // else select one by hashing and comparing to weight + var updated_releases = {}; + for (var i=0; i < services.length; i++) { + var service = services[i]; + var parsed_release = release_cookie.match(service+'\.(production|canary)'); + if ((!devOverride) && getWeight(service, canary_weights) === 0) { + updated_releases[service] = 'production'; + } else if (!parsed_release) { + // if we haven't yet generated a hash value, do that now + if (hash_res < 0) { + hash_res = simpleHash(hash_str); + } + updated_releases[service] = selectRelease(hash_res, getWeight(service, canary_weights)); + } else { + // append the matched values from the cookie + updated_releases[service] = parsed_release[1]; + } + } + + return releasesObjToString(updated_releases); +} + +/** + * Controls the value of Access-Control-Allow-Credentials by environment variable + * ORIGINS_ALLOW_CREDENTIALS. + * + * ORIGINS_ALLOW_CREDENTIALS is supposed to be a list of origins in JSON string. Only + * requests with origins in this list are allowed to send credentials like cookies to + * this website. See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Requests_with_credentials + * + * In most cases, credentials shouldn't be sent cross-site to mitigate CSRF attack risks. + * This is useful when Gen3 is deployed as an SSO and centralized service in a cross-site + * manner. The NDEF for example, serves two sub-commons at sub1.example.com and + * sub2.example.com with a centralized commons at example.com running Fence, Indexd and + * Arborist. When logged in at example.com, requests sent to both sub1 and sub2 are + * allowed to carry the same authentication cookie, therefore extra login is not needed + * for sub1 or sub2. + * + * @param req - nginx request object + * @returns {string} value used in Access-Control-Allow-Credentials header, empty string + * to not include this header + */ +function isCredentialsAllowed(req) { + if (!!req.variables['http_origin']) { + var origins = JSON.parse(req.variables['origins_allow_credentials'] || '[]') || []; + for (var i = 0; i < origins.length; i++) { + // cannot use === to compare byte strings, whose "typeof" is also confusingly "string" + if (origins[i].fromUTF8().toLowerCase().trim() === + req.variables['http_origin'].fromUTF8().toLowerCase().trim()) { + return 'true'; + } + } + } + return ''; +} + +/** + * Test whether the given ipAddrStr is in the global blackListStr. + * Currently does not support CIDR format - just list of IP's + * + * @param {string} ipAddrStr + * @param {string} blackListStr comma separated black list - defaults to globalBlackListStr (see below) + * @return {boolean} true if ipAddrStr is in the black list + */ +function isOnBlackList(ipAddrStr, blackListStr) { + return blackListStr.includes(ipAddrStr); +} + +/** + * Call via nginx.conf js_set after setting the blackListStr and + * ipAddrStr variables via set: + * + * set blackListStr="whatever" + * set ipAddrStr="whatever" + * js_set blackListCheck checkBlackList + * + * Note: kube-setup-revproxy generates gen3-blacklist.conf - which + * gets sucked into the nginx.conf config + * + * @param {Request} req + * @param {Response} res + * @return "ok" or "block" - fail to "ok" in ambiguous situation + */ +function checkBlackList(req,res) { + var ipAddrStr = req.variables["ip_addr_str"]; + var blackListStr = req.variables["black_list_str"]; + + if (ipAddrStr && blackListStr && isOnBlackList(ipAddrStr, blackListStr)) { + return "block"; + } + return "ok"; // + "-" + ipAddrStr + "-" + blackListStr; +} + + +/** + * Handle the js_content callout from /workspace-authorize. + * Basically - redirect to a subdomain /wts/authorize endpoint + * based on the state=SUBDOMAIN-... query parameter with + * some guards to stop attacks. + * + * @param {*} req + * @param {*} res + */ +function gen3_workspace_authorize_handler(req) { + var subdomain = ''; + var query = req.variables["args"] || ""; + var matchGroups = null; + + if (matchGroups = query.match(/(^state=|&state=)(\w+)-/)) { + subdomain = matchGroups[2]; + var location = "https://" + subdomain + "." + req.variables["host"] + + "/wts/oauth2/authorize?" + query; + req.return(302, location); + } else { + req.headersOut["Content-Type"] = "application/json" + req.return(400, '{ "status": "redirect failed validation" }'); + } +} + +export default {userid, isCredentialsAllowed}; diff --git a/helm/revproxy/nginxPrivate/nginx.conf b/helm/revproxy/nginxPrivate/nginx.conf new file mode 100644 index 000000000..989b0affc --- /dev/null +++ b/helm/revproxy/nginxPrivate/nginx.conf @@ -0,0 +1,348 @@ +user nginx; +worker_processes 4; +pid /var/run/nginx.pid; + +load_module modules/ngx_http_js_module.so; +load_module modules/ngx_http_perl_module.so; + +## +# Preserve environment variables +# Note: to use the variable in blocks below, you must use +# perl to set the variable. eg: +# perl_set $my_var 'sub { return $ENV{"MY_ENVIRONMENT_VAIRABLE"}; }'; +## +env POD_NAMESPACE; +env CANARY_PERCENT_JSON; +env COOKIE_DOMAIN; +env ORIGINS_ALLOW_CREDENTIALS; +env DES_NAMESPACE; +env MAINTENANCE_MODE; +env INDEXD_AUTHZ; +env MDS_AUTHZ; +env FRONTEND_ROOT; +env DOCUMENT_URL; + +events { + worker_connections 768; +} + +http { + ## + # Basic Settings + ## + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + port_in_redirect off; + server_tokens off; + + # For websockets + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + + map $proxy_protocol_addr $initialip { + "" $http_x_forwarded_for; + default $proxy_protocol_addr; + } + + map $initialip $realip { + "" $remote_addr; #if this header missing set remote_addr as real ip + default $initialip; + } + +# Log filtering for health checks +map $http_user_agent $loggable { + default 1; + "ELB-HealthChecker/2.0" 0; + ~^Uptime-Kuma 0; + ~^kube-probe 0; + ~GoogleStackdriverMonitoring 0; +} + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # ## + # # Note - nginscript js_set, etc get processed + # # on demand: https://www.nginx.com/blog/introduction-nginscript/ + # # # + js_import helpers.js; + js_set $userid helpers.userid; + + + perl_set $document_url_env 'sub { return $ENV{"DOCUMENT_URL"} || ""; }'; + + # see portal-conf + perl_set $maintenance_mode_env 'sub { return $ENV{"MAINTENANCE_MODE"} || "undefined"; }'; + + # Setup root path frontend service + perl_set $frontend_root_service 'sub { return $ENV{"FRONTEND_ROOT"} eq "gen3ff" ? "gen3ff" : "portal"; }'; + + + ## + # Logging Settings + ## + log_format json '{"gen3log": "nginx", ' + '"date_access": "$time_iso8601", ' + '"user_id": "$userid", ' + '"request_id": "$request_id", ' + '"session_id": "$session_id", ' + '"visitor_id": "$visitor_id", ' + '"network_client_ip": "$realip", ' + '"network_bytes_write": $body_bytes_sent, ' + '"response_secs": $request_time, ' + '"http_status_code": $status, ' + '"http_request": "$request_uri", ' + '"http_verb": "$request_method", ' + '"http_referer": "$http_referer", ' + '"http_useragent": "$http_user_agent", ' + '"http_upstream": "$upstream", ' + '"proxy_service": "$proxy_service", ' + '"message": "$request" }'; + + access_log /dev/stdout json if=$loggable; + + + ## + # Gzip Settings + ## + gzip on; + gzip_disable "msie6"; + gzip_proxied any; + gzip_types + text/css + text/javascript + text/xml + text/plain + application/javascript + application/x-javascript + application/json; + + # ## + # # Namespace + # ## + perl_set $namespace 'sub { return $ENV{"POD_NAMESPACE"}; }'; + + # ## + # # Fence Namespace + # ## + # # For using fence, indexd, etc from a different namespace within the same k8 cluster - + # # support data ecosystem feature ... + # ## + perl_set $des_domain 'sub { return $ENV{"DES_NAMESPACE"} ? qq{.$ENV{"DES_NAMESPACE"}.svc.cluster.local} : qq{.$ENV{"POD_NAMESPACE"}.svc.cluster.local}; }'; + + # ## + # # CORS Credential White List + # ## + perl_set $origins_allow_credentials 'sub { return $ENV{"ORIGINS_ALLOW_CREDENTIALS"}; }'; + js_set $credentials_allowed helpers.isCredentialsAllowed; + + # ## For multi-domain deployments + perl_set $csrf_cookie_domain 'sub { return $ENV{"COOKIE_DOMAIN"} ? qq{;domain=$ENV{"COOKIE_DOMAIN"}} : ""; }'; + + # # indexd password for admin endpoint + perl_set $indexd_b64 'sub { $_ = $ENV{"INDEXD_AUTHZ"}; chomp; return "$_"; }'; + # # metadata service password for admin endpoint + perl_set $mds_b64 'sub { $_ = $ENV{"MDS_AUTHZ"}; chomp; return "$_"; }'; + + + server { + listen 6567; + + root /var/www/metrics; + + location /aggregated_metrics { + types {} + default_type text/plain; + try_files $uri $uri/ /metrics.txt; + autoindex on; + access_log off; + } + } + + server { + listen 80; + + server_tokens off; + proxy_hide_header server; + proxy_hide_header X-Powered-By; + add_header "X-Frame-Options" "SAMEORIGIN" always; + add_header "X-Content-Type-Options" "nosniff" always; + add_header "X-Xss-Protection" "1; mode=block" always; + add_header "X-Robots-Tag" "noindex, nofollow" always; + + if ($http_x_forwarded_proto = "http") { return 301 https://$host$request_uri; } + # + # Strict-Transport-Security only applys for https traffic - set after testing protocol + # + add_header "Strict-Transport-Security" "max-age=63072000; includeSubdomains;" always; + + # + # From https://enable-cors.org/server_nginx.html + # This overrides the individual services + # + set $allow_origin "*"; + if ($http_origin = "https://$host") { + set $allow_origin "$http_origin"; + } + + proxy_hide_header Access-Control-Allow-Origin; # Remove existing header + add_header "Access-Control-Allow-Origin" "$allow_origin" always; + add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, DELETE, PUT" always; + add_header "Access-Control-Allow-Credentials" "$credentials_allowed" always; + add_header "Access-Control-Allow-Headers" "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Cookie,X-CSRF-Token" always; + add_header "Access-Control-Expose-Headers" "Content-Length,Content-Range" always; + + + + # update service release cookie + # add_header Set-Cookie "service_releases=${service_releases};Path=/;Max-Age=600;HttpOnly;Secure;SameSite=Lax" always; + + if ($request_method = 'OPTIONS') { + return 204; + } + + # + # DNS resolver required to resolve dynamic hostnames, btw - kubedns may not support ipv6 + # see https://www.nginx.com/blog/dns-service-discovery-nginx-plus/ + # https://distinctplace.com/2017/04/19/nginx-resolver-explained/ + # + resolver kube-dns.kube-system.svc.cluster.local ipv6=off; + + set $access_token ""; + set $csrf_check "ok-tokenauth"; + + # + # Note: add_header blocks are inheritted iff the current block does not call add_header: + # http://nginx.org/en/docs/http/ngx_http_headers_module.html + # + set $csrf_token "$request_id$request_length$request_time$time_iso8601"; + if ($cookie_csrftoken) { + set $csrf_token "$cookie_csrftoken"; + } + add_header Set-Cookie "csrftoken=$csrf_token$csrf_cookie_domain;Path=/;Secure;SameSite=Lax"; + + # visitor and session tracking for analytics - + # https://developers.google.com/analytics/devguides/collection/analyticsjs/cookies-user-id + # + # Simple session tracking - expire the session if not active for 20 minutes + set $session_id "$request_id"; + if ($cookie_session) { + set $session_id "$cookie_session"; + } + add_header Set-Cookie "session=$session_id;Path=/;Max-Age=1200;HttpOnly;Secure;SameSite=Lax"; + # Simple visitor tracking - immortal + set $visitor_id "$request_id"; + if ($cookie_visitor) { + set $visitor_id "$cookie_visitor"; + } + add_header Set-Cookie "visitor=$visitor_id;Path=/;Max-Age=36000000;HttpOnly;Secure;SameSite=Lax"; + + if ($cookie_access_token) { + set $access_token "bearer $cookie_access_token"; + # cookie auth requires csrf check + set $csrf_check "fail"; + } + if ($http_authorization) { + # Authorization header is present - prefer that token over cookie token + set $access_token "$http_authorization"; + } + + # + # initialize proxy_service and upstream used as key in logs to + # unspecified values - + # individual service locations should override to "peregrine", ... + # + set $proxy_service "noproxy"; + + # + # Note - need to repeat this line in location blocks that call proxy_set_header, + # as nginx proxy module inherits proxy_set_header if and only if current level does + # not set headers ... http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header + # + proxy_set_header Authorization "$access_token"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For "$realip"; + proxy_set_header X-UserId "$userid"; + # Can propagate this request id through downstream microservice requests for tracing + proxy_set_header X-ReqId "$request_id"; + proxy_set_header X-SessionId "$session_id"; + proxy_set_header X-VisitorId "$visitor_id"; + proxy_intercept_errors on; + + # + # Accomodate large jwt token headers + # * http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffer_size + # * https://ma.ttias.be/nginx-proxy-upstream-sent-big-header-reading-response-header-upstream/ + # + proxy_buffer_size 16k; + proxy_buffers 8 16k; + proxy_busy_buffers_size 32k; + client_body_buffer_size 16k; + proxy_read_timeout 400; + proxy_send_timeout 400; + proxy_connect_timeout 400; + + # + # also incoming from client: + # * https://fullvalence.com/2016/07/05/cookie-size-in-nginx/ + # * https://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_buffer_size + large_client_header_buffers 4 64k; + client_header_buffer_size 4k; + + # + # CSRF check + # This block requires a csrftoken for all POST requests. + # + if ($cookie_csrftoken = $http_x_csrf_token) { + # this will fail further below if cookie_csrftoken is empty + set $csrf_check "ok-$cookie_csrftoken"; + } + if ($request_method != "POST") { + set $csrf_check "ok-$request_method"; + } + if ($cookie_access_token = "") { + # do this again here b/c empty cookie_csrftoken == empty http_x_csrf_token - ugh + set $csrf_check "ok-tokenauth"; + } + + error_page 500 501 502 503 504 @5xx; + + location @5xx { + internal; + return 500 "{ \"error\": \"service failure - try again later\"}"; + } + + location = /_status { + default_type application/json; + set $upstream http://localhost; + access_log off; + return 200 "{ \"message\": \"Feelin good!\", \"csrf\": \"$csrf_token\" }\n"; + } + + include /etc/nginx/gen3.conf/*.conf; + if ($document_url_env != "") { + include /etc/nginx/gen3.conf/documentation-site/*.conf; + } + + location @errorworkspace { + # if ($frontend_root_service = "gen3ff") { + # return 302 https://$host/portal/no-workspace-access; + # } + return 302 https://$host/no-workspace-access; + } + + location /canary { + add_header Content-Type text/html; + return 200 'You are running the Helm version of this commons'; + } + } +} diff --git a/helm/revproxy/templates/configMaps.yaml b/helm/revproxy/templates/configMaps.yaml index eb0d5655e..3d3703f21 100644 --- a/helm/revproxy/templates/configMaps.yaml +++ b/helm/revproxy/templates/configMaps.yaml @@ -18,12 +18,25 @@ data: {{ "portal-service.conf" }}: | {{- .Files.Get "gen3.nginx.conf/portal-as-root/portal-service.conf" | nindent 4}} {{- end }} +{{- if .Values.enableRobotsTxt }} + {{ "robots-txt.conf" }}: | + {{- .Files.Get "gen3.nginx.conf/robots/robots-txt.conf" | nindent 4}} +{{- end }} {{- range .Values.extraServices }} {{ printf "%s-service.conf" .name }}: | location {{ .path }}/ { + {{- if .csrfCheck -}} if ($csrf_check !~ ^ok-\S.+$) { return 403 "failed csrf check"; } + {{- end }} + {{- if and .authzPolicy .authzService -}} + set $authz_resource "/{{ .authzPolicy }}"; + set $authz_method "access"; + set $authz_service "{{ .authzService }}"; + # be careful - sub-request runs in same context as this request + auth_request /gen3-authz; + {{- end }} set $proxy_service "{{ .name }}"; set $upstream http://{{ .serviceName }}$des_domain; @@ -38,7 +51,14 @@ kind: ConfigMap metadata: name: revproxy-nginx-conf data: +{{- if .Values.enableRobotsTxt }} +{{- range $path, $bytes := .Files.Glob "nginxPrivate/*" }} + {{ ($a := split "/" $path)._1 }}: | + {{- $bytes | toString | nindent 4 }} +{{- end}} +{{- else }} {{- range $path, $bytes := .Files.Glob "nginx/*" }} {{ ($a := split "/" $path)._1 }}: | {{- $bytes | toString | nindent 4 }} {{- end}} +{{- end }} diff --git a/helm/revproxy/values.yaml b/helm/revproxy/values.yaml index 2bd6f5307..82649dd2f 100644 --- a/helm/revproxy/values.yaml +++ b/helm/revproxy/values.yaml @@ -263,3 +263,9 @@ extraServices: # - name: "protein-paint" # path: /protein-paint # serviceName: protein-paint +# authzPolicy: "protein-paint" +# authzService: "protein-paint" +# csrfCheck: true + +# -- (bool) Whether to enable robots.txt generation and serving. +enableRobotsTxt: false