diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 52b6a64bb..d62757c6a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -118,6 +118,9 @@ Restore container: - .base_deploy_kosko_stage environment: name: prod2 + needs: + - job: Create Azure DB (dev) + dependencies: null variables: # storage copy SOURCE_CONTAINER: "cdtn" @@ -125,8 +128,8 @@ Restore container: DESTINATION_CONTAINER: "cdtn-dev" DESTINATION_SERVER: "dev" # db restore - # BACKUP_DB_NAME: "db_${CI_COMMIR_SHORT_SHA}" - # BACKUP_DB_OWNER: "user_${CI_COMMIR_SHORT_SHA}" - # BACKUP_DB_FILE: "hasura_prod_db.psql.gz" + BACKUP_DB_NAME: "db_${CI_COMMIT_SHORT_SHA}" + BACKUP_DB_OWNER: "user_${CI_COMMIT_SHORT_SHA}" + BACKUP_DB_FILE: "hasura_prod_db.psql.gz" # kosko options KOSKO_GENERATE_ARGS: --env prod restore diff --git a/.k8s/__tests__/__snapshots__/kosko generate --env prod restore.ts.snap b/.k8s/__tests__/__snapshots__/kosko generate --env prod restore.ts.snap index e62817852..c3abe23c0 100644 --- a/.k8s/__tests__/__snapshots__/kosko generate --env prod restore.ts.snap +++ b/.k8s/__tests__/__snapshots__/kosko generate --env prod restore.ts.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kosko generate --prod restore 1`] = ` -"metadata: +"--- +metadata: name: restore-container-8843083e namespace: cdtn-admin-secret spec: @@ -103,5 +104,128 @@ spec: restartPolicy: Never apiVersion: batch/v1 kind: Job +--- +data: + post-restore.sql: | + TRUNCATE TABLE \\"auth\\".\\"users\\" CASCADE; + + WITH admin_row AS ( + INSERT INTO auth.users (email, PASSWORD, name, default_role, active) + VALUES ('codedutravailnumerique@travail.gouv.fr', '$argon2i$v=19$m=4096,t=3,p=1$n9eoWSv+5sCgc7SjB5hLig$iBQ7NzrHHLkJSku/dCetNs+n/JI1CMdkWaoZsUekLU8', 'Administrateur', 'admin', TRUE) + RETURNING + id, default_role) + INSERT INTO auth.user_roles (ROLE, user_id) + SELECT + default_role, + id + FROM + admin_row; + + WITH admin_row AS ( + INSERT INTO auth.users (email, PASSWORD, name, default_role, active) + VALUES ('utilisateur@travail.gouv.fr', '$argon2i$v=19$m=4096,t=3,p=1$PqKPf9cxunVLLtEcINHhWQ$CwHKhk71fc8LGp6BWbcFPzQ2ftOiHa7vUkp1eAqVHSM', 'Utilisateur', 'user', TRUE) + RETURNING + id, default_role) + INSERT INTO auth.user_roles (ROLE, user_id) + SELECT + default_role, + id + FROM + admin_row; +metadata: + name: post-restore-script-configmap-8843083e + namespace: cdtn-admin-secret +apiVersion: v1 +kind: ConfigMap +--- +metadata: + name: restore-db-8843083e + namespace: cdtn-admin-secret +spec: + backoffLimit: 0 + template: + metadata: {} + spec: + containers: + - command: + - sh + - '-c' + - > + + + echo \\"starting restore into $PGHOST/$PGDATABASE\\" + + + [ ! -z $PGDATABASE ] || (echo \\"No PGDATABASE\\"; exit 1) + + [ ! -z $PGHOST ] || (echo \\"No PGHOST\\"; exit 1) + + [ ! -z $PGUSER ] || (echo \\"No PGUSER\\"; exit 1) + + [ ! -z $PGPASSWORD ] || (echo \\"No PGPASSWORD\\"; exit 1) + + [ ! -z $OWNER ] || (echo \\"No OWNER\\"; exit 1) + + + # get latest backup folder + + LATEST=$(ls -1Fr /mnt/data | head -n 1); + + DUMP=\\"/mnt/data/\${LATEST}\${FILE}\\" + + echo \\"Restore \${DUMP} into \${PGDATABASE}\\"; + + + pg_isready; + + + pg_restore --dbname \${PGDATABASE} --clean --if-exists + --no-owner --role \${OWNER} --no-acl --verbose \${DUMP}; + + + psql -v ON_ERROR_STOP=1 \${PGDATABASE} -c \\"ALTER SCHEMA public + owner to \${OWNER};\\" + + + [ -f \\"/mnt/scripts/post-restore.sql\\" ] && psql -v ON_ERROR_STOP=1 + -a < /mnt/scripts/post-restore.sql + env: + - name: PGDATABASE + value: some-database + - name: OWNER + value: some-owner + - name: FILE + value: some-backup.sql.gz + envFrom: + - secretRef: + name: azure-pg-admin-user-dev + image: >- + registry.gitlab.factory.social.gouv.fr/socialgouv/docker/azure-db:2.6.1 + imagePullPolicy: IfNotPresent + name: restore-db + resources: + limits: + cpu: 300m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi + volumeMounts: + - mountPath: /mnt/data + name: backups + - mountPath: /mnt/scripts + name: scripts + restartPolicy: OnFailure + volumes: + - azureFile: + readOnly: true + secretName: azure-cdtnadminprod-volume + shareName: cdtn-admin-backup-restore + name: backups + - configMap: + name: post-restore-script-configmap-8843083e + name: scripts +apiVersion: batch/v1 +kind: Job " `; diff --git a/.k8s/components/restore/db.ts b/.k8s/components/restore/db.ts index e67d4ef0f..007ccfa0b 100644 --- a/.k8s/components/restore/db.ts +++ b/.k8s/components/restore/db.ts @@ -1,8 +1,10 @@ +import fs from "fs"; +import path from "path"; import env from "@kosko/env"; import { ok } from "assert"; import { EnvVar } from "kubernetes-models/v1/EnvVar"; -import { restoreDbJob } from "@socialgouv/kosko-charts/components/azure-pg/restore-db.job"; +import { restoreDbJob } from "../../restore-db.job"; ok(process.env.BACKUP_DB_NAME); ok(process.env.BACKUP_DB_OWNER); @@ -24,6 +26,9 @@ const manifests = restoreDbJob({ value: process.env.BACKUP_DB_FILE, }), ], + postRestoreScript: fs + .readFileSync(path.join(__dirname, "./post-restore.sql")) + .toString(), }); export default manifests; diff --git a/.k8s/components/restore/index.ts b/.k8s/components/restore/index.ts index edb7fc69f..89ef5f12e 100644 --- a/.k8s/components/restore/index.ts +++ b/.k8s/components/restore/index.ts @@ -1,4 +1,4 @@ import containerJob from "./container"; import dbJob from "./db"; -export default [containerJob /*, dbJob*/]; +export default [containerJob, dbJob]; diff --git a/.k8s/components/restore/post-restore.sql b/.k8s/components/restore/post-restore.sql new file mode 100644 index 000000000..b9c190f17 --- /dev/null +++ b/.k8s/components/restore/post-restore.sql @@ -0,0 +1,25 @@ +TRUNCATE TABLE "auth"."users" CASCADE; + +WITH admin_row AS ( +INSERT INTO auth.users (email, PASSWORD, name, default_role, active) + VALUES ('codedutravailnumerique@travail.gouv.fr', '$argon2i$v=19$m=4096,t=3,p=1$n9eoWSv+5sCgc7SjB5hLig$iBQ7NzrHHLkJSku/dCetNs+n/JI1CMdkWaoZsUekLU8', 'Administrateur', 'admin', TRUE) + RETURNING + id, default_role) + INSERT INTO auth.user_roles (ROLE, user_id) + SELECT + default_role, + id + FROM + admin_row; + +WITH admin_row AS ( +INSERT INTO auth.users (email, PASSWORD, name, default_role, active) + VALUES ('utilisateur@travail.gouv.fr', '$argon2i$v=19$m=4096,t=3,p=1$PqKPf9cxunVLLtEcINHhWQ$CwHKhk71fc8LGp6BWbcFPzQ2ftOiHa7vUkp1eAqVHSM', 'Utilisateur', 'user', TRUE) + RETURNING + id, default_role) + INSERT INTO auth.user_roles (ROLE, user_id) + SELECT + default_role, + id + FROM + admin_row; diff --git a/.k8s/restore-db.job.ts b/.k8s/restore-db.job.ts new file mode 100644 index 000000000..a95633b79 --- /dev/null +++ b/.k8s/restore-db.job.ts @@ -0,0 +1,188 @@ +import ok from "assert"; +import { ConfigMap } from "kubernetes-models/_definitions/IoK8sApiCoreV1ConfigMap"; +import { Job } from "kubernetes-models/batch/v1/Job"; +import { EnvFromSource } from "kubernetes-models/v1/EnvFromSource"; +import type { EnvVar } from "kubernetes-models/v1/EnvVar"; + +//import { addInitContainer } from "@socialgouv/kosko-charts/utils/addInitContainer"; +//import { waitForPostgres } from "@socialgouv/kosko-charts/utils/waitForPostgres"; + +import type { IIoK8sApiCoreV1Container } from "kubernetes-models/_definitions/IoK8sApiCoreV1Container"; +import type { Deployment } from "kubernetes-models/apps/v1/Deployment"; +import type { Job as JobType } from "kubernetes-models/batch/v1/Job"; + +//type Manifest = Deployment | JobType; + +// export const addInitContainer = ( +// deployment: Manifest, +// initContainer: IIoK8sApiCoreV1Container +// ): Manifest => { +// if (!deployment.spec?.template) { +// return deployment; +// } + +// deployment.spec.template.spec = deployment.spec.template.spec ?? { +// containers: [], +// initContainers: [], +// }; +// const containers = deployment.spec.template.spec.initContainers ?? []; +// containers.push(initContainer); +// deployment.spec.template.spec.initContainers = containers; + +// return deployment; +// }; + +interface RestoreDbJobArgs { + project: string; + env: EnvVar[]; + envFrom?: EnvFromSource[]; + postRestoreScript?: string; +} + +// renovate: datasource=docker depName=registry.gitlab.factory.social.gouv.fr/socialgouv/docker/azure-db versioning=2.6.1 +const SOCIALGOUV_DOCKER_AZURE_DB = "2.6.1"; + +const restoreScript = ` + +echo "starting restore into $PGHOST/$PGDATABASE" + +[ ! -z $PGDATABASE ] || (echo "No PGDATABASE"; exit 1) +[ ! -z $PGHOST ] || (echo "No PGHOST"; exit 1) +[ ! -z $PGUSER ] || (echo "No PGUSER"; exit 1) +[ ! -z $PGPASSWORD ] || (echo "No PGPASSWORD"; exit 1) +[ ! -z $OWNER ] || (echo "No OWNER"; exit 1) + +# get latest backup folder +LATEST=$(ls -1Fr /mnt/data | head -n 1); +DUMP="/mnt/data/\${LATEST}\${FILE}" +echo "Restore \${DUMP} into \${PGDATABASE}"; + +pg_isready; + +pg_restore \ + --dbname \${PGDATABASE} \ + --clean --if-exists \ + --no-owner \ + --role \${OWNER} \ + --no-acl \ + --verbose \ + \${DUMP}; + +psql -v ON_ERROR_STOP=1 \${PGDATABASE} -c "ALTER SCHEMA public owner to \${OWNER};" + +[ -f "/mnt/scripts/post-restore.sql" ] && psql -v ON_ERROR_STOP=1 -a < /mnt/scripts/post-restore.sql +`; + +const getProjectSecretNamespace = (project: string) => `${project}-secret`; + +const getAzureProdVolumeSecretName = (project: string) => + `azure-${project.replace(/-/g, "")}prod-volume`; + +const getAzureBackupShareName = (project: string) => + `${project}-backup-restore`; + +type ReturnManifest = Job | ConfigMap; + +export const restoreDbJob = ({ + project, + env = [], + envFrom = [], + postRestoreScript, +}: RestoreDbJobArgs): ReturnManifest[] => { + ok(process.env.CI_COMMIT_SHORT_SHA); + const secretNamespace = getProjectSecretNamespace(project); + const azureSecretName = getAzureProdVolumeSecretName(project); + const azureShareName = getAzureBackupShareName(project); + + const manifests = []; + + const jobSpec = { + containers: [ + { + command: ["sh", "-c", restoreScript], + env, + envFrom: [ + new EnvFromSource({ + secretRef: { + name: "azure-pg-admin-user-dev", + }, + }), + ...envFrom, + ], + image: `registry.gitlab.factory.social.gouv.fr/socialgouv/docker/azure-db:${SOCIALGOUV_DOCKER_AZURE_DB}`, + imagePullPolicy: "IfNotPresent", + name: "restore-db", + resources: { + limits: { + cpu: "300m", + memory: "512Mi", + }, + requests: { + cpu: "100m", + memory: "64Mi", + }, + }, + volumeMounts: [ + { + mountPath: "/mnt/data", + name: "backups", + }, + ], + }, + ], + restartPolicy: "OnFailure", + volumes: [ + { + azureFile: { + readOnly: true, + secretName: azureSecretName, + shareName: azureShareName, + }, + name: "backups", + }, + ], + }; + + if (postRestoreScript) { + jobSpec.containers[0].volumeMounts.push({ + mountPath: "/mnt/scripts", + name: "scripts", + }); + jobSpec.volumes.push({ + //@ts-expect-error + configMap: { + name: `post-restore-script-configmap-${process.env.CI_COMMIT_SHORT_SHA}`, + }, + + name: "scripts", + }); + const configMap = new ConfigMap({ + data: { + "post-restore.sql": postRestoreScript, + }, + metadata: { + name: `post-restore-script-configmap-${process.env.CI_COMMIT_SHORT_SHA}`, + namespace: secretNamespace, + }, + }); + manifests.push(configMap); + } + + const job = new Job({ + metadata: { + name: `restore-db-${process.env.CI_COMMIT_SHORT_SHA}`, + namespace: secretNamespace, + }, + spec: { + backoffLimit: 0, + template: { + metadata: {}, + spec: jobSpec, + }, + }, + }); + + manifests.push(job); + + return manifests; +};