diff --git a/.github/integration/scripts/charts/values.yaml b/.github/integration/scripts/charts/values.yaml index 9b2303a08..6f4524ac1 100644 --- a/.github/integration/scripts/charts/values.yaml +++ b/.github/integration/scripts/charts/values.yaml @@ -3,6 +3,7 @@ global: ingress: deploy: false hostName: + api: pipeline-sda-svc-api auth: pipeline-sda-svc-auth download: pipeline-sda-svc-download s3Inbox: pipeline-sda-svc-inbox @@ -12,6 +13,13 @@ global: enabled: false issuer: "" clusterIssuer: "cert-issuer" + api: + adminsFileSecret: + adminUsers: + - dummy@example.com + - requester@demo.org + jwtPubKeyName: jwt.pub + jwtSecret: jwk archive: storageType: s3 s3AccessKey: PLACEHOLDER_VALUE @@ -102,6 +110,9 @@ global: port: "8080" password: "pass" user: "user" +api: + replicaCount: 1 + resources: null auth: replicaCount: 1 resources: null diff --git a/.github/workflows/build_pr_container.yaml b/.github/workflows/build_pr_container.yaml index 4da4e3374..d1654f17a 100644 --- a/.github/workflows/build_pr_container.yaml +++ b/.github/workflows/build_pr_container.yaml @@ -285,7 +285,7 @@ jobs: - name: Check deployment run: | sleep 30 - for n in auth download finalize inbox ingest mapper reencrypt sync syncapi verify; do + for n in api auth download finalize inbox ingest mapper reencrypt sync syncapi verify; do if [ ${{matrix.storage}} == "posix" ] && [ "$n" == "auth" ] || [ "$n" == "sync" ] || [ "$n" == "syncapi" ]; then continue fi @@ -300,7 +300,7 @@ jobs: run: | kubectl get pods sleep 1 - for svc in auth finalize inbox ingest mapper reencrypt sync syncapi verify; do + for svc in api auth finalize inbox ingest mapper reencrypt sync syncapi verify; do echo "## describe $svc" && kubectl describe pod -l role="$svc" sleep 1 echo "## logs $svc" && kubectl logs -l role="$svc" diff --git a/charts/sda-svc/Chart.yaml b/charts/sda-svc/Chart.yaml index b9cf4f2a0..850091bf9 100644 --- a/charts/sda-svc/Chart.yaml +++ b/charts/sda-svc/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: sda-svc -version: 0.27.7 +version: 0.28.0 appVersion: v0.3.114 kubeVersion: '>= 1.26.0' description: Components for Sensitive Data Archive (SDA) installation diff --git a/charts/sda-svc/README.md b/charts/sda-svc/README.md index f20c4ec25..1cdc7c89b 100644 --- a/charts/sda-svc/README.md +++ b/charts/sda-svc/README.md @@ -76,6 +76,10 @@ Parameter | Description | Default `global.backupArchive.volumePath` | Path to the mounted `posix` volume. |`/backup` `global.backupArchive.nfsServer` | URL or IP address to a NFS server. |`""` `global.backupArchive.nfsPath` | Path on the NFS server for the backup archive. |`""` +`global.api.adminFileSecret` | A secret holding a JSON file named `admin.json` containg a list of identifiers |`` +`global.api.adminUsers` | A list of identifiers of the users with admin privileges |`` +`global.api.jwtPubKeyName` | Public key used to verify the JWT. |`` +`global.api.jwtSecret` | The name of the secret holding the JWT public key |`` `global.auth.jwtAlg` | Key type to sign the JWT, available options are RS265 & ES256, Must match the key type |`"ES256"` `global.auth.jwtKey` | Private key used to sign the JWT. |`""` `global.auth.jwtPub` | Public key ues to verify the JWT. |`""` @@ -171,6 +175,10 @@ If no shared credentials for the message broker and database are used these shou Parameter | Description | Default --------- | ----------- | ------- +`credentials.api.dbUser` | Database user for api | `""` +`credentials.api.dbPassword` | Database password for api | `""` +`credentials.api.mqUser` | Broker user for api | `""` +`credentials.api.mqPassword` | Broker password for api | `""` `credentials.doa.dbUser` | Database user for doa | `""` `credentials.doa.dbPassword` | Database password for doa| `""` `credentials.download.dbUser` | Database user for download | `""` @@ -206,6 +214,13 @@ Parameter | Description | Default Parameter | Description | Default --------- | ----------- | ------- +`api.replicaCount` | Desired number of replicas | `2` +`api.annotations` | Specific annotation for the auth pod | `{}` +`api.resources.requests.memory` | Memory request for container. |`128Mi` +`api.resources.requests.cpu` | CPU request for container. |`100m` +`api.resources.limits.memory` | Memory limit for container. |`256Mi` +`api.resources.limits.cpu` | CPU limit for container. |`250m` +`api.tls.secretName` | Secret holding the application TLS certificates |`` `auth.replicaCount` | desired number of replicas | `2` `auth.annotations` | Specific annotation for the auth pod | `{}` `auth.resources.requests.memory` | Memory request for container. |`128Mi` diff --git a/charts/sda-svc/templates/_helpers.yaml b/charts/sda-svc/templates/_helpers.yaml index 8042638ae..c25468dff 100644 --- a/charts/sda-svc/templates/_helpers.yaml +++ b/charts/sda-svc/templates/_helpers.yaml @@ -133,6 +133,20 @@ Create chart name and version as used by the chart label. {{ end }} {{- end -}} +{{/**/}} +{{- define "dbUserAPI" -}} +{{- ternary .Values.global.db.user .Values.credentials.api.dbUser (empty .Values.credentials.api.dbUser) -}} +{{- end -}} +{{- define "dbPassAPI" -}} +{{- ternary .Values.global.db.password .Values.credentials.api.dbPassword (empty .Values.credentials.api.dbPassword) -}} +{{- end -}} +{{- define "mqUserAPI" -}} +{{- ternary .Values.global.broker.username .Values.credentials.api.mqUser (empty .Values.credentials.api.mqUser) -}} +{{- end -}} +{{- define "mqPassAPI" -}} +{{- ternary .Values.global.broker.password .Values.credentials.api.mqPassword (empty .Values.credentials.api.mqPassword) -}} +{{- end -}} + {{/**/}} {{- define "dbUserSync" -}} {{- ternary .Values.global.db.user .Values.credentials.sync.dbUser (empty .Values.credentials.sync.dbUser) -}} diff --git a/charts/sda-svc/templates/api-certificate.yaml b/charts/sda-svc/templates/api-certificate.yaml new file mode 100644 index 000000000..1c8305ea8 --- /dev/null +++ b/charts/sda-svc/templates/api-certificate.yaml @@ -0,0 +1,39 @@ +{{- if .Values.global.tls.enabled }} +{{- if or .Values.global.tls.clusterIssuer .Values.global.tls.issuer }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ template "sda.fullname" . }}-api-certs +spec: + # Secret names are always required. + secretName: {{ template "sda.fullname" . }}-api-certs + + duration: 2160h # 90d + + # The use of the common name field has been deprecated since 2000 and is + # discouraged from being used. However, it is still needed for TLS based authentication for Postgres and other services. + commonName: {{ template "sda.fullname" . }}-api + isCA: false + privateKey: + algorithm: ECDSA + size: 384 + usages: + - client auth + - server auth + # At least one of a DNS Name, URI, or IP address is required. + dnsNames: + - {{ template "sda.fullname" . }}-api + - {{ template "sda.fullname" . }}-api.{{ .Release.Namespace }}.svc + ipAddresses: + - 127.0.0.1 + # Issuer references are always required. + issuerRef: + name: {{ template "TLSissuer" . }} + # We can reference ClusterIssuers by changing the kind here. + # The default value is Issuer (i.e. a locally namespaced Issuer) + kind: {{ ternary "Issuer" "ClusterIssuer" (empty .Values.global.tls.clusterIssuer )}} + # This is optional since cert-manager will default to this value however + # if you are using an external issuer, change this to that issuer group. + group: cert-manager.io +{{- end -}} +{{- end -}} diff --git a/charts/sda-svc/templates/api-deploy.yaml b/charts/sda-svc/templates/api-deploy.yaml new file mode 100644 index 000000000..6bf9a67e1 --- /dev/null +++ b/charts/sda-svc/templates/api-deploy.yaml @@ -0,0 +1,248 @@ +{{- if or (or (eq "all" .Values.global.deploymentType) (eq "external" .Values.global.deploymentType) ) (not .Values.global.deploymentType) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "sda.fullname" . }}-api + labels: + role: api + app: {{ template "sda.fullname" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: {{ template "sda.fullname" . }}-api + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.api.replicaCount }} + revisionHistoryLimit: {{ default "3" .Values.global.revisionHistory }} + selector: + matchLabels: + app: {{ template "sda.fullname" . }}-api + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "sda.fullname" . }}-api + role: api + release: {{ .Release.Name }} + annotations: + {{- if not .Values.global.vaultSecrets }} + checksum/secret: {{ include (print $.Template.BasePath "/api-secrets.yaml") . | sha256sum }} + {{- end }} +{{- if .Values.global.podAnnotations }} +{{- toYaml .Values.global.podAnnotations | nindent 8 -}} +{{- end }} +{{- if .Values.api.annotations }} +{{- toYaml .Values.api.annotations | nindent 8 -}} +{{- end }} + spec: + topologySpreadConstraints: + - maxSkew: 1 + whenUnsatisfiable: DoNotSchedule + topologyKey: kubernetes.io/hostname + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - {{ template "sda.fullname" . }}-api + topologyKey: kubernetes.io/hostname + {{- if .Values.global.rbacEnabled}} + serviceAccountName: {{ .Release.Name }} + {{- end }} + securityContext: + runAsUser: 65534 + runAsGroup: 65534 + fsGroup: 65534 + containers: + - name: api + image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + runAsNonRoot: true + seccompProfile: + type: "RuntimeDefault" +{{- if .Values.global.extraSecurityContext }} +{{- toYaml .Values.global.extraSecurityContext | nindent 10 -}} +{{- end }} + command: ["sda-api"] + env: +{{- if not .Values.global.vaultSecrets }} + - name: API_ADMINFILE + value: {{ template "secretsPath" . }}/admins.json + {{- if .Values.global.tls.enabled }} + - name: API_SERVERCERT + value: {{ template "tlsPath" . }}/tls.crt + - name: API_SERVERKEY + value: {{ template "tlsPath" . }}/tls.key + {{- end }} + - name: BROKER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "sda.fullname" . }}-api + key: mqPassword + - name: BROKER_USER + valueFrom: + secretKeyRef: + name: {{ template "sda.fullname" . }}-api + key: mqUser + {{- if .Values.global.tls.enabled }} + - name: BROKER_CACERT + value: {{ template "tlsPath" . }}/ca.crt + {{- if .Values.global.broker.verifyPeer }} + - name: BROKER_CLIENTCERT + value: {{ template "tlsPath" . }}/tls.crt + - name: BROKER_CLIENTKEY + value: {{ template "tlsPath" . }}/tls.key + {{- end }} + - name: BROKER_SSL + value: {{ .Values.global.tls.enabled | quote }} + - name: BROKER_VERIFYPEER + value: {{ .Values.global.broker.verifyPeer | quote }} + {{- end }} + - name: BROKER_EXCHANGE + value: {{ default "sda" .Values.global.broker.exchange }} + - name: BROKER_HOST + value: {{ required "A valid MQ host is required" .Values.global.broker.host | quote }} + - name: BROKER_PORT + value: {{ .Values.global.broker.port | quote }} + - name: BROKER_PREFETCHCOUNT + value: {{ .Values.global.broker.prefetchCount | quote }} + - name: BROKER_VHOST + value: {{ .Values.global.broker.vhost | quote }} + - name: BROKER_SERVERNAME + value: {{ .Values.global.broker.host | quote }} + {{- if .Values.global.tls.enabled }} + - name: DB_CACERT + value: {{ include "tlsPath" . }}/ca.crt + {{- if ne "verify-none" .Values.global.db.sslMode }} + - name: DB_CLIENTCERT + value: {{ include "tlsPath" . }}/tls.crt + - name: DB_CLIENTKEY + value: {{ include "tlsPath" . }}/tls.key + {{- end }} + - name: DB_SSLMODE + value: {{ .Values.global.db.sslMode | quote }} + {{- else }} + - name: DB_SSLMODE + value: "disable" + {{- end }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "sda.fullname" . }}-api + key: dbPassword + - name: DB_USER + valueFrom: + secretKeyRef: + name: {{ template "sda.fullname" . }}-api + key: dbUser + - name: DB_DATABASE + value: {{ default "lega" .Values.global.db.name | quote }} + - name: DB_HOST + value: {{ required "A valid DB host is required" .Values.global.db.host | quote }} + - name: DB_PORT + value: {{ .Values.global.db.port | quote }} + {{- if .Values.global.log.format }} + - name: LOG_FORMAT + value: {{ .Values.global.log.format | quote }} + {{- end }} + {{- if .Values.global.log.level }} + - name: LOG_LEVEL + value: {{ .Values.global.log.level | quote }} + {{- end }} + {{- if .Values.global.api.jwtPubKeyName }} + - name: SERVER_JWTPUBKEYPATH + value: {{ include "jwtPath" . }} + {{- else }} + - name: SERVER_JWTPUBKEYURL + value: {{ required "A oidc provider is required" .Values.global.oidc.provider }}{{ .Values.global.oidc.jwkPath }} + {{- end }} +{{- else }} + - name: SERVER_CONFFILE + value: {{ include "confFile" . }} +{{- end }} + ports: + - name: api + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + port: api + path: /ready + scheme: {{ ternary "HTTPS" "HTTP" ( .Values.global.tls.enabled ) }} + httpHeaders: + - name: Host + value: {{ .Values.global.ingress.hostName.api }} + initialDelaySeconds: 20 + periodSeconds: 10 + readinessProbe: + httpGet: + port: api + path: /ready + scheme: {{ ternary "HTTPS" "HTTP" ( .Values.global.tls.enabled) }} + httpHeaders: + - name: Host + value: {{ .Values.global.ingress.hostName.api }} + initialDelaySeconds: 20 + periodSeconds: 10 + resources: +{{ toYaml .Values.api.resources | trim | indent 10 }} + volumeMounts: + {{- if .Values.global.api.jwtPubKeyName }} + - name: jwt + mountPath: {{ include "jwtPath" . }} + {{- end }} + {{- if not .Values.global.vaultSecrets }} + - name: admins + mountPath: {{ template "secretsPath" . }} + {{- end }} + {{- if .Values.global.tls.enabled }} + - name: tls + mountPath: {{ template "tlsPath" . }} + {{- end }} + volumes: + {{- if not .Values.global.vaultSecrets }} + - name: admins + projected: + sources: + - secret: + {{- if .Values.global.api.adminsFileSecret }} + name: {{ .Values.global.api.adminsFileSecret }} + items: + - key: admins.json + path: admins.json + {{- else }} + name: {{ template "sda.fullname" . }}-api-admins + items: + - key: admins.json + path: admins.json + {{- end }} + {{- if .Values.global.api.jwtPubKeyName }} + - name: jwt + projected: + sources: + - secret: + name: {{ .Values.global.api.jwtSecret }} + items: + - key: {{ .Values.global.api.jwtPubKeyName }} + path: {{ .Values.global.api.jwtPubKeyName }} + {{- end }} + {{- end }} + {{- if and (not .Values.global.pkiService) .Values.global.tls.enabled }} + - name: tls + {{- if or .Values.global.tls.clusterIssuer .Values.global.tls.issuer }} + secret: + defaultMode: 0440 + secretName: {{ template "sda.fullname" . }}-api-certs + {{- else }} + secret: + defaultMode: 0440 + secretName: {{ required "An certificate issuer or a TLS secret name is required for api" .Values.api.tls.secretName }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/sda-svc/templates/api-ingress.yaml b/charts/sda-svc/templates/api-ingress.yaml new file mode 100644 index 000000000..a5f29d7ac --- /dev/null +++ b/charts/sda-svc/templates/api-ingress.yaml @@ -0,0 +1,49 @@ +{{- if (or (or (eq "all" .Values.global.deploymentType) (eq "external" .Values.global.deploymentType)) (not .Values.global.deploymentType)) }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ template "sda.fullname" . }}-api-ingress + labels: + app: {{ template "sda.fullname" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.global.ingress.labels }} +{{ toYaml .Values.global.ingress.labels | indent 4 }} +{{- end }} + annotations: + {{- if eq "nginx" .Values.global.ingress.ingressClassName }} + nginx.ingress.kubernetes.io/rewrite-target: "/" + nginx.ingress.kubernetes.io/backend-protocol: "{{ ternary "HTTPS" "HTTP" .Values.global.tls.enabled }}" + nginx.ingress.kubernetes.io/affinity: "cookie" + {{- end }} + {{- if .Values.global.ingress.clusterIssuer }} + cert-manager.io/cluster-issuer: {{ .Values.global.ingress.clusterIssuer | quote }} + {{- else if .Values.global.ingress.issuer }} + cert-manager.io/issuer: {{ .Values.global.ingress.issuer | quote }} + {{- end }} +{{- if .Values.global.ingress.annotations }} +{{ toYaml .Values.global.ingress.annotations | indent 4 }} +{{- end }} +spec: +{{- if .Values.global.ingress.ingressClassName }} + ingressClassName: {{ .Values.global.ingress.ingressClassName }} +{{- end }} + rules: + - host: {{ required "An ingress hostname is required!" .Values.global.ingress.hostName.api }} + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: {{ template "sda.fullname" . }}-api + port: + number: 8080 +{{- if .Values.global.tls.enabled }} + tls: + - hosts: + - {{ required "An ingress hostname is required!" .Values.global.ingress.hostName.api }} + secretName: {{ if .Values.global.ingress.secretNames.api }}{{ .Values.global.ingress.secretNames.api }}{{- else }}"{{ template "sda.fullname" . }}-ingress-api"{{- end }} +{{- end }} +{{- end }} diff --git a/charts/sda-svc/templates/api-secrets.yaml b/charts/sda-svc/templates/api-secrets.yaml new file mode 100644 index 000000000..a2a1d387a --- /dev/null +++ b/charts/sda-svc/templates/api-secrets.yaml @@ -0,0 +1,25 @@ +{{- if or (or (eq "all" .Values.global.deploymentType) (eq "external" .Values.global.deploymentType) ) (not .Values.global.deploymentType)}} +{{- if not .Values.global.vaultSecrets }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "sda.fullname" . }}-api +type: Opaque +data: + dbPassword: {{ required "DB password is required" (include "dbPassAPI" .) | b64enc }} + dbUser: {{ required "DB user is required" (include "dbUserAPI" .) | b64enc }} + mqPassword: {{ required "MQ password is required" (include "mqPassAPI" .) | b64enc }} + mqUser: {{ required "MQ user is required" (include "mqUserAPI" .) | b64enc }} +--- +{{- if not .Values.global.api.adminsFileSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "sda.fullname" . }}-api-admins +type: Opaque +data: + admins.json: {{ .Values.global.api.adminUsers | toJson | b64enc | quote }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/sda-svc/templates/api-service.yaml b/charts/sda-svc/templates/api-service.yaml new file mode 100644 index 000000000..6e3baf723 --- /dev/null +++ b/charts/sda-svc/templates/api-service.yaml @@ -0,0 +1,16 @@ +{{- if or (or (eq "all" .Values.global.deploymentType) (eq "external" .Values.global.deploymentType) ) (not .Values.global.deploymentType) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "sda.fullname" . }}-api + labels: + app: {{ template "sda.fullname" . }}-api +spec: + ports: + - name: api + port: 8080 + targetPort: api + protocol: TCP + selector: + app: {{ template "sda.fullname" . }}-api +{{- end }} diff --git a/charts/sda-svc/values.yaml b/charts/sda-svc/values.yaml index 214749c88..4fa92756a 100644 --- a/charts/sda-svc/values.yaml +++ b/charts/sda-svc/values.yaml @@ -34,6 +34,7 @@ global: deploy: true ingressClassName: "nginx" hostName: + api: "" auth: "" doa: "" download: "" @@ -106,6 +107,11 @@ global: vaultSecrets: false # global configurations + api: + adminsFileSecret: "" + adminUsers: + jwtPubKeyName: + jwtSecret: archive: storageType: "" # s3 or posix # The six lines below is only used with S3 backend @@ -289,6 +295,12 @@ global: ################################## # service specific credentials credentials: + api: + dbUser: "" + dbPassword: "" + mqUser: "" + mqPassword: "" + doa: dbUser: "" dbPassword: "" @@ -346,6 +358,23 @@ credentials: ################################## # Service specific settings +api: + name: api + replicaCount: 2 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "250m" +# Extra annotations to attach to the service pods +# This should be a multi-line string mapping directly to the a map of +# the annotations to apply to the service pods + annotations: {} + tls: + secretName: "" + auth: name: auth replicaCount: 2 diff --git a/sda/internal/config/config.go b/sda/internal/config/config.go index 860c2a7a6..dc326fa75 100644 --- a/sda/internal/config/config.go +++ b/sda/internal/config/config.go @@ -208,7 +208,6 @@ func NewConfig(app string) (*Config, error) { "broker.port", "broker.user", "broker.password", - "broker.routingkey", "db.host", "db.port", "db.user",