diff --git a/helm-charts/images/gcp-filestore-backups/Dockerfile b/helm-charts/images/gcp-filestore-backups/Dockerfile deleted file mode 100644 index 19caf21952..0000000000 --- a/helm-charts/images/gcp-filestore-backups/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM python:3.12.4 - -USER root - -# Install gcloud CLI -RUN apt-get update -RUN apt-get install apt-transport-https ca-certificates gnupg curl -y -RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \ - && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg \ - && apt-get update -y && apt-get install google-cloud-sdk -y - -COPY requirements.txt /tmp/ -RUN pip install -r /tmp/requirements.txt - -RUN mkdir -p /app/.config/gcloud -RUN chown 1000:1000 /app/.config/gcloud - -COPY gcp-filestore-backups.py /app/ -WORKDIR /app - -ENV CLOUDSDK_CONFIG=/app/.config/gcloud diff --git a/helm-charts/images/gcp-filestore-backups/gcp-filestore-backups.py b/helm-charts/images/gcp-filestore-backups/gcp-filestore-backups.py deleted file mode 100644 index 34d8aeae1a..0000000000 --- a/helm-charts/images/gcp-filestore-backups/gcp-filestore-backups.py +++ /dev/null @@ -1,264 +0,0 @@ -import argparse -import json -import subprocess -import time -from datetime import datetime, timedelta - -import jmespath - - -def extract_region_from_zone(zone: str): - """ - Parse a GCP zone (e.g. us-central1-b) to return a region (e.g. us-central1) - """ - return "-".join(zone.split("-")[:2]) - - -def get_existing_backups( - project: str, region: str, filestore_name: str, filestore_share_name: str -): - """List existing backups of a share on a filestore using the gcloud CLI. - We filter the backups based on: - - GCP project - - GCP region - - Filestore name - - Filestore share name - - Args: - project (str): The GCP project the filestore is located in - region (str): The region the filestore is located in, e.g., us-central1 - filestore_name (str): The name of the filestore instance - filestore_share_name (str): The name of the share on the filestore instance - - Returns: - list(dict): A JSON-like object, where each dict-entry in the list describes - an existing backup of the filestore - """ - # Get all existing backups in the selected project and region - backups = subprocess.check_output( - [ - "gcloud", - "filestore", - "backups", - "list", - "--format=json", - f"--project={project}", - f"--region={region}", - ], - text=True, - ) - backups = json.loads(backups) - - # Filter returned backups by filestore and share names - backups = jmespath.search( - f"[?sourceFileShare == '{filestore_share_name}' && contains(sourceInstance, '{filestore_name}')]", - backups, - ) - - # Parse `createTime` property into a datetime object for comparison - backups = [ - { - k: ( - datetime.strptime(v.split(".")[0], "%Y-%m-%dT%H:%M:%S") - if k == "createTime" - else v - ) - for k, v in backup.items() - } - for backup in backups - ] - - return backups - - -def filter_backups_into_recent_and_old( - backups: list, retention_days: int, day_freq: int = 1 -): - """Filter the list of backups into two groups: - - Recently created backups that were created within our backup window, - defined by day_freq - - Out of date back ups that are older than our retention window, defined by - retention days - - Args: - backups (list(dict)): A JSON-like object defining the existing backups - for the filestore and share we care about - retention_days (int): The number of days above which a backup is considered - to be out of date - day_freq (int, optional): The time period in days for which we create a - backup. Defaults to 1 (ie. daily backups). NOTE: The frequency at - which we make backups is not yet configurable on the command line, - but could be if required. - - Returns: - recent_backups (list(dict)): A JSON-like object containing all existing - backups with a `createTime` within our backup window - old_backups (list(dict)): A JSON-like object containing all existing - backups with a `createTime` older than our retention window - """ - # Generate a list of filestore backups that are younger than our backup window - recent_backups = [ - backup - for backup in backups - if datetime.now() - backup["createTime"] < timedelta(days=day_freq) - ] - - # Generate a list of filestore backups that are older than our set retention period - old_backups = [ - backup - for backup in backups - if datetime.now() - backup["createTime"] > timedelta(days=retention_days) - ] - if len(old_backups) > 0: - print( - f"Filestore backups older than {retention_days} days have been found. They will be deleted." - ) - - return recent_backups, old_backups - - -def create_backup_if_necessary( - backups: list, - filestore_name: str, - filestore_share_name: str, - project: str, - region: str, - zone: str, -): - """If no recent backups have been found, create a new backup using the gcloud CLI - - Args: - backups (list(dict)): A JSON-like object containing details of recently - created backups - filestore_name (str): The name of the Filestore instance to backup - filestore_share_name (str): The name of the share on the Filestore to backup - project (str): The GCP project within which to create a backup - region (str): The GCP region to create the backup in, e.g. us-central1 - zone (str): The GCP zone to create the backup in, e.g. us-central1-b - """ - if len(backups) == 0: - print( - f"There have been no recent backups of the filestore for project {project}. Creating a backup now..." - ) - - subprocess.check_call( - [ - "gcloud", - "filestore", - "backups", - "create", - f"{filestore_name}-{filestore_share_name}-backup-{datetime.now().strftime('%Y-%m-%d')}", - f"--file-share={filestore_share_name}", - f"--instance={filestore_name}", - f"--instance-zone={zone}", - f"--region={region}", - # This operation can take a long time to complete and will only take - # longer as filestores grow, hence we use the `--async` flag to - # return immediately, without waiting for the operation in progress - # to complete. Given that we only expect to be creating a backup - # once a day, this feels safe enough to try for now. - # The `gcloud filestore backups list` command is instantaneously - # populated with new backups, even if they are not done creating. - # So we don't have to worry about the async flag not taking those - # into account. - "--async", - ] - ) - else: - print("Recent backup found.") - - -def delete_old_backups(backups: list, region: str): - """If out of date backups exist, delete them using the gcloud CLI - - Args: - backups (list(dict)): A JSON-like object containing out of date backups - region (str): The GCP region the backups exist in, e.g. us-central1 - """ - if len(backups) > 0: - for backup in backups: - subprocess.check_call( - [ - "gcloud", - "filestore", - "backups", - "delete", - backup["name"].split("/")[-1], - f"--region={region}", - "--quiet", # Otherwise we are prompted to confirm deletion - ] - ) - else: - print("No outdated backups found.") - - -def main(args): - region = extract_region_from_zone(args.zone) - - for filestore_name in args.filestore_names: - filestore_backups = get_existing_backups( - args.project, region, filestore_name, args.filestore_share_name - ) - recent_filestore_backups, old_filestore_backups = ( - filter_backups_into_recent_and_old(filestore_backups, args.retention_days) - ) - create_backup_if_necessary( - recent_filestore_backups, - filestore_name, - args.filestore_share_name, - args.project, - region, - args.zone, - ) - delete_old_backups(old_filestore_backups, region) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="""Uses the gcloud CLI to check for existing backups of a GCP - Filestore, creates a new backup if necessary, and deletes outdated backups - """ - ) - - parser.add_argument( - "filestore_names", - nargs="+", - help="The name of one or more GCP Filestores to backup", - ) - parser.add_argument( - "project", - type=str, - help="The GCP project the Filestore belongs to", - ) - parser.add_argument( - "zone", - type=str, - help="The GCP zone the Filestore is deployed in, e.g. us-central1-b", - ) - - # NOTE: We assume that the share name will be homes on all GCP filestores - # right now, which is a safe assumption given that this is not configurable - # in our terraform code: - # - # https://github.com/2i2c-org/infrastructure/blob/HEAD/terraform/gcp/storage.tf - # - # We should change this if that value becomes configurable. - # - parser.add_argument( - "--filestore-share-name", - type=str, - default="homes", - help="The name of the share on the Filestore to backup", - ) - parser.add_argument( - "--retention-days", - type=int, - default=5, - help="The number of days to store backups for", - ) - - args = parser.parse_args() - - while True: - main(args) - time.sleep(600) # 60 seconds * 10 for 10 minutes sleep period diff --git a/helm-charts/images/gcp-filestore-backups/requirements.txt b/helm-charts/images/gcp-filestore-backups/requirements.txt deleted file mode 100644 index 45c1e038e5..0000000000 --- a/helm-charts/images/gcp-filestore-backups/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -jmespath diff --git a/helm-charts/support/Chart.yaml b/helm-charts/support/Chart.yaml index 4c3f8b3708..ff90105df0 100644 --- a/helm-charts/support/Chart.yaml +++ b/helm-charts/support/Chart.yaml @@ -52,3 +52,10 @@ dependencies: version: "0.0.1-set.by.chartpress" repository: "file://../aws-ce-grafana-backend" condition: aws-ce-grafana-backend.enabled + + # gcpFilestoreBackups runs regular backups of GCP Filestore Instances hosting + # home directories within the cloud project. + - name: gcpFilestoreBackups + version: "0.0.1-0.dev.git.53.h8b9558e" + repository: https://2i2c.org/gcp-filestore-backups + condition: gcpFilestoreBackups.enabled diff --git a/helm-charts/support/templates/gcp-filestore-backups/deployment.yaml b/helm-charts/support/templates/gcp-filestore-backups/deployment.yaml deleted file mode 100644 index a958ec06d9..0000000000 --- a/helm-charts/support/templates/gcp-filestore-backups/deployment.yaml +++ /dev/null @@ -1,40 +0,0 @@ -{{- if .Values.gcpFilestoreBackups.enabled -}} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Release.Name }}-gcp-filestore-backups -spec: - replicas: 1 - strategy: - type: "Recreate" - selector: - matchLabels: - app: gcp-filestore-backups - template: - metadata: - labels: - app: gcp-filestore-backups - spec: - serviceAccountName: gcp-filestore-backups-sa - automountServiceAccountToken: false - containers: - - name: {{ .Release.Name }}-gcp-filestore-backups - image: '{{ .Values.gcpFilestoreBackups.image }}' - command: - - python - - gcp-filestore-backups.py - args: - {{- range .Values.gcpFilestoreBackups.filestoreNames | required "gcpFilestoreBackups.filestoreNames is required with gcpFilestoreBackups.enabled set to true" }} - - '{{ . }}' - {{- end }} - - '{{ .Values.gcpFilestoreBackups.project | required "gcpFilestoreBackups.project is required with gcpFilestoreBackups.enabled set to true" }}' - - '{{ .Values.gcpFilestoreBackups.zone | required "gcpFilestoreBackups.zone is required with gcpFilestoreBackups.enabled set to true" }}' - securityContext: - runAsUser: 1000 - allowPrivilegeEscalation: False - # The image used for gcp-filestore-backups uses gcloud, which wants to - # write a log file. Without setting readOnlyRootFilesystem = False, - # gcloud will not have permissions to write it's log file and will - # fail and crash the pod. - readOnlyRootFilesystem: False -{{- end -}} diff --git a/helm-charts/support/templates/gcp-filestore-backups/serviceaccount.yaml b/helm-charts/support/templates/gcp-filestore-backups/serviceaccount.yaml deleted file mode 100644 index 8b819b7437..0000000000 --- a/helm-charts/support/templates/gcp-filestore-backups/serviceaccount.yaml +++ /dev/null @@ -1,7 +0,0 @@ -{{ if .Values.gcpFilestoreBackups.enabled -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: gcp-filestore-backups-sa - annotations: {{ .Values.gcpFilestoreBackups.annotations | toJson }} -{{- end }} diff --git a/helm-charts/support/values.schema.yaml b/helm-charts/support/values.schema.yaml index 4f140aadf5..c59d52e40a 100644 --- a/helm-charts/support/values.schema.yaml +++ b/helm-charts/support/values.schema.yaml @@ -173,54 +173,11 @@ properties: type: string description: | Name of the StorageClass to create + # Enables https://github.com/2i2c-org/gcp-filestore-backups to regularly backup + # GCP Filestore instances gcpFilestoreBackups: type: object - additionalProperties: false - required: - - enabled - - image - # Require options to be set *only* if gcpFilestoreBackups is enabled - if: - properties: - enabled: - const: true - then: - required: - - filestoreNames - - project - - zone - - annotations - properties: - enabled: - type: boolean - description: | - Enable automatic daily backups of GCP Filestores - image: - type: string - description: | - The image name and tag to use for the gcp-filestore-backups pod. - Will be set by chartpress. - filestoreNames: - type: array - description: | - The name of one or more GCP Filestores to backup as a list - project: - type: string - description: | - The GCP project the Filestore and backups are stored in - zone: - type: string - description: | - The GCP zone the Filestore and backups are stored in, e.g., us-central1-b - annotations: - type: object - additionalProperties: true - description: | - Dictionary of annotations that can be applied to the service account. - - When used with GKE and Workload Identity, you need to set the - annotation with the key "iam.gke.io/gcp-service-account" to the email - address of the Google Service Account whose credentials it should have. + additionalProperties: true global: type: object additionalProperties: true diff --git a/helm-charts/support/values.yaml b/helm-charts/support/values.yaml index c4805512b8..d0e7c01884 100644 --- a/helm-charts/support/values.yaml +++ b/helm-charts/support/values.yaml @@ -522,7 +522,6 @@ prometheusStorageClass: # Setup a deployment that will periodically backup the Filestore contents gcpFilestoreBackups: enabled: false - image: "quay.io/2i2c/gcp-filestore-backups:0.0.1-0.dev.git.9908.hcc20334f" # A placeholder as global values that can be referenced from the same location # of any chart should be possible to provide, but aren't necessarily provided or