From 731357698c9216be2b4cbe8b6c47f1d5bc05cbf4 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 20:43:42 +0100 Subject: [PATCH 01/36] Initial commit --- .envrc | 9 +++ .helmignore | 23 ++++++ Chart.yaml | 6 ++ README.md | 5 ++ devbox.json | 14 ++++ devbox.lock | 53 ++++++++++++++ resources/nginx.conf | 132 ++++++++++++++++++++++++++++++++++ templates/NOTES.txt | 22 ++++++ templates/_helpers.tpl | 62 ++++++++++++++++ templates/configmap.yaml | 7 ++ templates/deployment.yaml | 102 ++++++++++++++++++++++++++ templates/hpa.yaml | 32 +++++++++ templates/service.yaml | 15 ++++ templates/serviceaccount.yaml | 13 ++++ values.yaml | 48 +++++++++++++ 15 files changed, 543 insertions(+) create mode 100755 .envrc create mode 100644 .helmignore create mode 100644 Chart.yaml create mode 100644 README.md create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 resources/nginx.conf create mode 100644 templates/NOTES.txt create mode 100644 templates/_helpers.tpl create mode 100644 templates/configmap.yaml create mode 100644 templates/deployment.yaml create mode 100644 templates/hpa.yaml create mode 100644 templates/service.yaml create mode 100644 templates/serviceaccount.yaml create mode 100644 values.yaml diff --git a/.envrc b/.envrc new file mode 100755 index 0000000..2f05af9 --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..61f173a --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: docker-gcp-private-mirror +description: A Helm chart for Kubernetes +type: application +version: 0.0.1 +appVersion: "0.0.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd22e39 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +[![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/) +![Test Status](https://github.com/sguesdon/docker-gcp-private-mirror/actions/workflows/test.yml/badge.svg) + +# Docker GCP private mirror + diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..58a4b48 --- /dev/null +++ b/devbox.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", + "packages": ["kubernetes-helm@latest"], + "shell": { + "init_hook": [ + "echo 'Welcome to devbox!' > /dev/null" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..04aae70 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,53 @@ +{ + "lockfile_version": "1", + "packages": { + "kubernetes-helm@latest": { + "last_modified": "2024-12-23T21:10:33Z", + "resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#kubernetes-helm", + "source": "devbox-search", + "version": "3.16.4", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ldyw27qinlywajskcgp06b0vxqi3j7sw-kubernetes-helm-3.16.4", + "default": true + } + ], + "store_path": "/nix/store/ldyw27qinlywajskcgp06b0vxqi3j7sw-kubernetes-helm-3.16.4" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lpjy11ccpalk012sbfjxzdgvlbv722a3-kubernetes-helm-3.16.4", + "default": true + } + ], + "store_path": "/nix/store/lpjy11ccpalk012sbfjxzdgvlbv722a3-kubernetes-helm-3.16.4" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/f1141pdh3jzcjx033s0w1h00w21vnlwq-kubernetes-helm-3.16.4", + "default": true + } + ], + "store_path": "/nix/store/f1141pdh3jzcjx033s0w1h00w21vnlwq-kubernetes-helm-3.16.4" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/smn3ia62s9svrh2sy8sasl8sgkjcylsp-kubernetes-helm-3.16.4", + "default": true + } + ], + "store_path": "/nix/store/smn3ia62s9svrh2sy8sasl8sgkjcylsp-kubernetes-helm-3.16.4" + } + } + } + } +} diff --git a/resources/nginx.conf b/resources/nginx.conf new file mode 100644 index 0000000..042bfe1 --- /dev/null +++ b/resources/nginx.conf @@ -0,0 +1,132 @@ +http { + + # Environment variables + env TOKEN_FILE_PATH; + env PROXY_BUFFER_SIZE; + env PROXY_BUFFERS; + env PROXY_BUSY_BUFFERS_SIZE; + env LARGE_CLIENT_HEADER_BUFFERS; + env UPSTREAM_HOST; + env BASE_REWRITE_PATH; + env TOKEN_CACHE_EXPIRATION_SECONDS; + env MAX_AUTH_RETRY_ATTEMPTS; + + lua_shared_dict cache 1m; + + init_by_lua_block { + + -- Function to read an environment variable with a default fallback + local function getenv_with_default(env_var, default) + local value = os.getenv(env_var) + if not value or value == "" then + return default + end + return value + end + + -- Load configurations from environment variables + local token_file_path = getenv_with_default("TOKEN_FILE_PATH", "/config/token") + local token_cache_expiration = tonumber(getenv_with_default("TOKEN_CACHE_EXPIRATION_SECONDS", "3600")) + + -- Function to read the token from the file + function read_token() + local file = io.open(token_file_path, "r") + if not file then + ngx.log(ngx.ERR, "Token file not found: " .. token_file_path) + return nil + end + + local token = file:read("*a") + file:close() + + if not token or token == "" then + ngx.log(ngx.ERR, "Token file is empty or invalid") + return nil + end + + return token + end + + -- Function to get the Authorization header + function get_auth_header(clear_cache) + clear_cache = clear_cache or false + local auth_header = ngx.shared.cache:get("auth_header") + + if not clear_cache and auth_header then + return auth_header + end + + local token = read_token() + if not token then + ngx.log(ngx.ERR, "Failed to read the token file") + return nil + end + + local auth_header = "Bearer " .. token + ngx.shared.cache:set("auth_header", auth_header, token_cache_expiration) + ngx.log(ngx.NOTICE, "Token successfully loaded") + return auth_header + end + + -- Preload the token at startup + get_auth_header() + } + + server { + + server_name docker-mirror; + + proxy_set_header Host $UPSTREAM_HOST; + + proxy_buffer_size $PROXY_BUFFER_SIZE; + proxy_buffers $PROXY_BUFFERS; + proxy_busy_buffers_size $PROXY_BUSY_BUFFERS_SIZE; + large_client_header_buffers $LARGE_CLIENT_HEADER_BUFFERS; + + access_by_lua_block { + local token = get_auth_header() + + if token then + ngx.req.set_header("Authorization", token) + else + ngx.log(ngx.ERR, "Failed to set the authorization token") + end + } + + header_filter_by_lua_block { + local status = ngx.status + local max_auth_retry_attempts = tonumber(os.getenv("MAX_AUTH_RETRY_ATTEMPTS")) or 1 + + if status == 401 or status == 403 then + local retry_attempt = ngx.ctx.retry_attempt or 0 + if retry_attempt < max_auth_retry_attempts then + ngx.log(ngx.NOTICE, "401/403 detected, retrying after refreshing the token") + ngx.ctx.retry_attempt = retry_attempt + 1 + get_auth_header(true) + return ngx.exec(ngx.var.request_uri) + else + ngx.status = ngx.HTTP_FORBIDDEN + ngx.log(ngx.ERR, "Maximum retry limit reached, request failed") + ngx.say("Access denied after multiple token refresh attempts") + ngx.exit(ngx.HTTP_FORBIDDEN) + end + end + } + + # Main base URI + location / { + proxy_pass https://$UPSTREAM_HOST; + } + + # Base URI for /v2/ + location ~ /v2/ { + proxy_pass https://$UPSTREAM_HOST; + + # URL rewriting specific to /v2/ + set $base_rewrite_path $BASE_REWRITE_PATH; + if ($request_uri ~* "^/v2/(.+)$") { + rewrite ^/v2/(.+)$ $base_rewrite_path/$1 break; + } + } + } +} \ No newline at end of file diff --git a/templates/NOTES.txt b/templates/NOTES.txt new file mode 100644 index 0000000..260234b --- /dev/null +++ b/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "docker-gcp-private-mirror.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "docker-gcp-private-mirror.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "docker-gcp-private-mirror.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "docker-gcp-private-mirror.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..89f3f17 --- /dev/null +++ b/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "docker-gcp-private-mirror.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "docker-gcp-private-mirror.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "docker-gcp-private-mirror.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "docker-gcp-private-mirror.labels" -}} +helm.sh/chart: {{ include "docker-gcp-private-mirror.chart" . }} +{{ include "docker-gcp-private-mirror.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "docker-gcp-private-mirror.selectorLabels" -}} +app.kubernetes.io/name: {{ include "docker-gcp-private-mirror.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "docker-gcp-private-mirror.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "docker-gcp-private-mirror.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/templates/configmap.yaml b/templates/configmap.yaml new file mode 100644 index 0000000..4b7aeae --- /dev/null +++ b/templates/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config +data: + nginx.conf: |- +{{ .Files.Get "resources/nginx.conf" | indent 4}} \ No newline at end of file diff --git a/templates/deployment.yaml b/templates/deployment.yaml new file mode 100644 index 0000000..45e3bbc --- /dev/null +++ b/templates/deployment.yaml @@ -0,0 +1,102 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }} + labels: + {{- include "docker-gcp-private-mirror.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "docker-gcp-private-mirror.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "docker-gcp-private-mirror.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "docker-gcp-private-mirror.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: nginx + securityContext: + {{- toYaml .Values.nginx.securityContext | nindent 12 }} + image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.nginx.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + readinessProbe: + httpGet: + path: /v2/ + port: http + livenessProbe: + httpGet: + path: /v2/ + port: http + resources: + {{- toYaml .Values.nginx.resources | nindent 12 }} + volumeMounts: + {{- toYaml .Values.nginx.volumeMounts | nindent 12 }} + - name: shared-config + mountPath: /config + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: cloud-sdk + securityContext: + {{- toYaml .Values.cloudSdk.securityContext | nindent 12 }} + image: "{{ .Values.cloudSdk.image.repository }}:{{ .Values.cloudSdk.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.cloudSdk.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + command: + - '/bin/sh' + - '-c' + - | + while true; do + gcloud auth application-default print-access-token | tr -d '\n\t\r ' > /config/token; + [ $? -ne 0 ] && exit 1 + sleep 300 + done + resources: + {{- toYaml .Values.cloudSdk.resources | nindent 12 }} + volumeMounts: + {{- toYaml .Values.cloudSdk.volumeMounts | nindent 12 }} + - name: shared-config + mountPath: /config + volumes: + {{- toYaml .Values.volumes | nindent 8 }} + - name: nginx-config + configMap: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config + - name: shared-config + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/templates/hpa.yaml b/templates/hpa.yaml new file mode 100644 index 0000000..2c92908 --- /dev/null +++ b/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }} + labels: + {{- include "docker-gcp-private-mirror.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "docker-gcp-private-mirror.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/templates/service.yaml b/templates/service.yaml new file mode 100644 index 0000000..9716d8b --- /dev/null +++ b/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }} + labels: + {{- include "docker-gcp-private-mirror.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "docker-gcp-private-mirror.selectorLabels" . | nindent 4 }} diff --git a/templates/serviceaccount.yaml b/templates/serviceaccount.yaml new file mode 100644 index 0000000..6a7a3bd --- /dev/null +++ b/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "docker-gcp-private-mirror.serviceAccountName" . }} + labels: + {{- include "docker-gcp-private-mirror.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..d42225f --- /dev/null +++ b/values.yaml @@ -0,0 +1,48 @@ +replicaCount: 1 + +nginx: + image: + repository: mirror.gcr.io/nginx + pullPolicy: IfNotPresent + tag: "" + volumeMounts: [] + resources: {} + +cloudSdk: + image: + repository: mirror.gcr.io/google/cloud-sdk + pullPolicy: IfNotPresent + tag: "" + volumeMounts: [] + resources: {} + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} +podSecurityContext: {} +securityContext: {} + +service: + type: ClusterIP + port: 80 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +volumes: [] +nodeSelector: {} +tolerations: [] +affinity: {} From 3b4490b9a81db0b32a35ccac04801a648c280957 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 21:32:09 +0100 Subject: [PATCH 02/36] feat: add test gha --- .github/workflows/integration-tests.yaml | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/integration-tests.yaml diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml new file mode 100644 index 0000000..9060c62 --- /dev/null +++ b/.github/workflows/integration-tests.yaml @@ -0,0 +1,26 @@ +name: Tests +on: + pull_request: + branches: + - main +jobs: + integration-tests: + name: Integration tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.11.0 + + - name: start minikube + uses: medyagh/setup-minikube@latest + + - name: Deploy helm chart + run: | + helm package . --version=0.0.1 + helm install private-gcp-mirror ./docker-gcp-private-mirror-0.0.1.tgz + + - name: Run tests + run: | + devbox run test From e42bf515c79f381c4a2ef6a5892f973a03918067 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 21:37:38 +0100 Subject: [PATCH 03/36] typo: fix typo --- .github/workflows/integration-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 9060c62..68b0c23 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -13,7 +13,7 @@ jobs: - name: Install devbox uses: jetify-com/devbox-install-action@v0.11.0 - - name: start minikube + - name: Start minikube uses: medyagh/setup-minikube@latest - name: Deploy helm chart From 259b1bd676b109d4b013a45512f93c776fba25c0 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 21:37:50 +0100 Subject: [PATCH 04/36] clean: ignore devbox files --- .helmignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.helmignore b/.helmignore index 0e8a0eb..0494518 100644 --- a/.helmignore +++ b/.helmignore @@ -21,3 +21,6 @@ .idea/ *.tmproj .vscode/ +.devbox/ +devbox.json +devbox.lock \ No newline at end of file From 1ff0cb29c2f5cbb1e7426533db11128c74cf009a Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 21:43:25 +0100 Subject: [PATCH 05/36] bugfix: remove ingress note --- templates/NOTES.txt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/templates/NOTES.txt b/templates/NOTES.txt index 260234b..7dda045 100644 --- a/templates/NOTES.txt +++ b/templates/NOTES.txt @@ -1,19 +1,8 @@ 1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} +{{- if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "docker-gcp-private-mirror.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "docker-gcp-private-mirror.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "docker-gcp-private-mirror.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "docker-gcp-private-mirror.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") From 51dfbba344f9346f844dae4797fb50a19db6052a Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 22:50:39 +0100 Subject: [PATCH 06/36] misc: add helm chart icon --- Chart.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Chart.yaml b/Chart.yaml index 61f173a..517501c 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,6 +1,7 @@ apiVersion: v2 name: docker-gcp-private-mirror description: A Helm chart for Kubernetes +icon: data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJ0aW55IiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBvdmVyZmxvdz0idmlzaWJsZSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnID4KCTxyZWN0IHk9IjAiIGZpbGw9Im5vbmUiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIvPgoJPHBvbHlnb24gZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNUM4NURFIiBwb2ludHM9IjYsMi40IDAuOCw1LjQgMC44LDE5LjEgNiwyMi4xIDYsMTkuNSAzLDE3LjggMyw2LjcgNiw1IAkiLz4KCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIwLjgsOCAwLjgsMTYuNSAzLDE3LjggMyw2LjcgCSIvPgoJPHBvbHlnb24gZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMzM2N0Q2IiBwb2ludHM9IjAuOCw4IDMsNy41IDMsNi43IAkiLz4KCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzVDODVERSIgcG9pbnRzPSIxOCwyLjQgMTgsNSAyMSw2LjcgMjEsMTcuOCAxOCwxOS41IDE4LDIyLjEgMjMuMiwxOS4xIDIzLjIsNS40IAkiLz4KCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIyMSwxNy44IDIzLjIsMTYuNSAyMy4yLDggMjEsNi43IAkiLz4KCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIyMSw3LjUgMjMuMiw4IDIxLDYuNyAJIi8+Cgk8cG9seWdvbiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiMzMzY3RDYiIHBvaW50cz0iMjEsMTcuMSAyMSwxNy44IDIzLjIsMTYuNSAJIi8+Cgk8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Ljg2MDAwMCwgNi41MDAwMDApIj4KCQk8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0LjU0MDAwMCwgMC4wMDAwMDApIj4KCQkJPHBvbHlnb24gZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNUM4NURFIiBwb2ludHM9IjAuNiwtMSAtMi40LDAuNiAwLjYsMi4yIDMuNiwwLjYgCQkJIi8+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIxLDUuOSAzLjksNC4zIDMuOSwxLjIgMSwyLjggCQkJIi8+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIwLjMsMi44IC0yLjcsMS4yIC0yLjcsNC4zIDAuMyw1LjkgCQkJIi8+CgkJPC9nPgoJCTxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMDAwMDAwLCA3Ljk3NjE5MCkiPgoJCQk8cG9seWdvbiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiM1Qzg1REUiIHBvaW50cz0iMS43LC0zLjEgLTEuMiwtMS41IDEuNywwLjEgNC41LC0xLjUgCQkJIi8+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIxLjksMy44IDQuOCwyLjIgNC44LC0xIDEuOSwwLjYgCQkJIi8+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSIxLjQsMC42IC0xLjUsLTEgLTEuNSwyLjIgMS40LDMuOCAJCQkiLz4KCQk8L2c+CgkJPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOS4zNjAwMDAsIDcuOTc2MTkwKSI+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzVDODVERSIgcG9pbnRzPSItMC43LC0zLjEgLTMuNiwtMS41IC0wLjcsMC4xIDIuMiwtMS41IAkJCSIvPgoJCQk8cG9seWdvbiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiMzMzY3RDYiIHBvaW50cz0iLTAuNCwzLjggMi41LDIuMiAyLjUsLTEgLTAuNCwwLjYgCQkJIi8+CgkJCTxwb2x5Z29uIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzMzNjdENiIgcG9pbnRzPSItMSwwLjYgLTMuOSwtMSAtMy45LDIuMiAtMSwzLjggCQkJIi8+CgkJPC9nPgoJPC9nPgo8L2c+Cjwvc3ZnPgo= type: application version: 0.0.1 appVersion: "0.0.1" From 4e023f72341c8214fe2f85aafabaaf80ca32de0e Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Fri, 10 Jan 2025 22:50:52 +0100 Subject: [PATCH 07/36] bugfix: fix helm template syntax --- templates/deployment.yaml | 6 ++++++ values.yaml | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 45e3bbc..76e3bbb 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -51,7 +51,9 @@ spec: resources: {{- toYaml .Values.nginx.resources | nindent 12 }} volumeMounts: + {{- if .Values.nginx.volumeMounts }} {{- toYaml .Values.nginx.volumeMounts | nindent 12 }} + {{- end }} - name: shared-config mountPath: /config - name: nginx-config @@ -78,11 +80,15 @@ spec: resources: {{- toYaml .Values.cloudSdk.resources | nindent 12 }} volumeMounts: + {{- if .Values.cloudSdk.volumeMounts }} {{- toYaml .Values.cloudSdk.volumeMounts | nindent 12 }} + {{- end }} - name: shared-config mountPath: /config volumes: + {{- if .Values.volumes }} {{- toYaml .Values.volumes | nindent 8 }} + {{- end }} - name: nginx-config configMap: name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config diff --git a/values.yaml b/values.yaml index d42225f..93a49c0 100644 --- a/values.yaml +++ b/values.yaml @@ -5,16 +5,16 @@ nginx: repository: mirror.gcr.io/nginx pullPolicy: IfNotPresent tag: "" - volumeMounts: [] - resources: {} + volumeMounts: [] + resources: {} cloudSdk: image: repository: mirror.gcr.io/google/cloud-sdk pullPolicy: IfNotPresent tag: "" - volumeMounts: [] - resources: {} + volumeMounts: [] + resources: {} imagePullSecrets: [] nameOverride: "" From 2dac316ed374008c2cabb6507f44258c7c419e37 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 16:15:49 +0100 Subject: [PATCH 08/36] wip: closing tests, fixing many bugs in nginx proxy --- .../{integration-tests.yaml => tests.yaml} | 5 - .gitignore | 1 + devbox.json | 9 +- devbox.lock | 65 + resources/scripts/test.sh | 46 + .helmignore => src/.helmignore | 5 +- Chart.yaml => src/Chart.yaml | 0 src/resources/generate-token-file.sh | 6 + .../resources/nginx.conf.template | 86 +- {templates => src/templates}/NOTES.txt | 0 {templates => src/templates}/_helpers.tpl | 0 src/templates/configmap.yaml | 39 + {templates => src/templates}/deployment.yaml | 30 +- {templates => src/templates}/hpa.yaml | 0 {templates => src/templates}/service.yaml | 0 .../templates}/serviceaccount.yaml | 0 values.yaml => src/values.yaml | 18 +- templates/configmap.yaml | 7 - tests/.gitignore | 1 + tests/Dockerfile | 12 + tests/jest.config.js | 8 + tests/package-lock.json | 5007 +++++++++++++++++ tests/package.json | 22 + tests/resources/config/values.yaml | 17 + tests/resources/pod.yaml | 44 + tests/src/authz.spec.ts | 77 + tests/src/config/config.ts | 9 + tests/src/config/routes.ts | 17 + tests/src/proxify.spec.ts | 68 + tests/src/server/server.ts | 136 + tests/src/server/types/request.type.ts | 7 + .../server/types/supertest-response.type.ts | 3 + tests/tsconfig.json | 12 + 33 files changed, 5686 insertions(+), 71 deletions(-) rename .github/workflows/{integration-tests.yaml => tests.yaml} (71%) create mode 100644 .gitignore create mode 100755 resources/scripts/test.sh rename .helmignore => src/.helmignore (89%) rename Chart.yaml => src/Chart.yaml (100%) create mode 100644 src/resources/generate-token-file.sh rename resources/nginx.conf => src/resources/nginx.conf.template (56%) rename {templates => src/templates}/NOTES.txt (100%) rename {templates => src/templates}/_helpers.tpl (100%) create mode 100644 src/templates/configmap.yaml rename {templates => src/templates}/deployment.yaml (81%) rename {templates => src/templates}/hpa.yaml (100%) rename {templates => src/templates}/service.yaml (100%) rename {templates => src/templates}/serviceaccount.yaml (100%) rename values.yaml => src/values.yaml (61%) delete mode 100644 templates/configmap.yaml create mode 100644 tests/.gitignore create mode 100644 tests/Dockerfile create mode 100644 tests/jest.config.js create mode 100644 tests/package-lock.json create mode 100644 tests/package.json create mode 100644 tests/resources/config/values.yaml create mode 100644 tests/resources/pod.yaml create mode 100644 tests/src/authz.spec.ts create mode 100644 tests/src/config/config.ts create mode 100644 tests/src/config/routes.ts create mode 100644 tests/src/proxify.spec.ts create mode 100644 tests/src/server/server.ts create mode 100644 tests/src/server/types/request.type.ts create mode 100644 tests/src/server/types/supertest-response.type.ts create mode 100644 tests/tsconfig.json diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/tests.yaml similarity index 71% rename from .github/workflows/integration-tests.yaml rename to .github/workflows/tests.yaml index 68b0c23..74cdc21 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,11 +16,6 @@ jobs: - name: Start minikube uses: medyagh/setup-minikube@latest - - name: Deploy helm chart - run: | - helm package . --version=0.0.1 - helm install private-gcp-mirror ./docker-gcp-private-mirror-0.0.1.tgz - - name: Run tests run: | devbox run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e30eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.tgz \ No newline at end of file diff --git a/devbox.json b/devbox.json index 58a4b48..04f674c 100644 --- a/devbox.json +++ b/devbox.json @@ -1,14 +1,15 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", - "packages": ["kubernetes-helm@latest"], + "packages": [ + "kubernetes-helm@latest", + "nodejs@23" + ], "shell": { "init_hook": [ "echo 'Welcome to devbox!' > /dev/null" ], "scripts": { - "test": [ - "echo \"Error: no test specified\" && exit 1" - ] + "test": "./resources/scripts/test.sh" } } } diff --git a/devbox.lock b/devbox.lock index 04aae70..5d8ad1e 100644 --- a/devbox.lock +++ b/devbox.lock @@ -48,6 +48,71 @@ "store_path": "/nix/store/smn3ia62s9svrh2sy8sasl8sgkjcylsp-kubernetes-helm-3.16.4" } } + }, + "nodejs@23": { + "last_modified": "2024-12-23T21:10:33Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#nodejs_23", + "source": "devbox-search", + "version": "23.2.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/h01fs5dy2aykhxfd2nvq2qzn5v88bmj2-nodejs-23.2.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/rpafhg74x82viy5d5ri5llikkd0nv0jy-nodejs-23.2.0-libv8" + } + ], + "store_path": "/nix/store/h01fs5dy2aykhxfd2nvq2qzn5v88bmj2-nodejs-23.2.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/24jjyqsk2bf45a9vc6p24l55v058n0fb-nodejs-23.2.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/lifan3dprdmnkhs6fk537zhs92baqm9x-nodejs-23.2.0-libv8" + } + ], + "store_path": "/nix/store/24jjyqsk2bf45a9vc6p24l55v058n0fb-nodejs-23.2.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/5ckvci4i16whdx5n1vxj14as01kwp1iz-nodejs-23.2.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/13sfp639j9d69n1rjc0a9z3y7xxfwm23-nodejs-23.2.0-libv8" + } + ], + "store_path": "/nix/store/5ckvci4i16whdx5n1vxj14as01kwp1iz-nodejs-23.2.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/cgkpjpl1hspg30dlmv4hhvlhbvik1bn1-nodejs-23.2.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/3mma5xpnfggra9as4l9ibvihx70qm7wc-nodejs-23.2.0-libv8" + } + ], + "store_path": "/nix/store/cgkpjpl1hspg30dlmv4hhvlhbvik1bn1-nodejs-23.2.0" + } + } } } } diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh new file mode 100755 index 0000000..0225ec7 --- /dev/null +++ b/resources/scripts/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +wait_for_pods_to_be_deleted() { + local namespace=$1 + local pod_label_selector=$2 + + echo "Waiting for all pods with label selector '${pod_label_selector}' to be deleted..." + while kubectl get pods -n "${namespace}" -l "${pod_label_selector}" | grep -q 'Running\|Pending'; do + echo "Pods still present. Retrying in 5 seconds..." + sleep 5 + done + echo "All pods with label selector '${pod_label_selector}' have been deleted." +} + +POD_NAME=mirror-test +NAMESPACE=mirror-test +RELEASE_NAME=private-gcp-mirror + +# build and deploy test image +# eval $(minikube docker-env) +docker build -t mirror-test --no-cache ./tests + +kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml +wait_for_pods_to_be_deleted "${NAMESPACE}" "app=${POD_NAME}" + +kubectl apply -f ./tests/resources/pod.yaml +echo "Waiting for the Pod to be ready..." +kubectl wait --for=condition=Ready "pod/${POD_NAME}" --timeout=300s + +helm uninstall "${RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." +wait_for_pods_to_be_deleted "${NAMESPACE}" "app.kubernetes.io/instance=${RELEASE_NAME}" + +# build and deploy helm chart +helm package ./src --version=0.0.1 +helm upgrade --install \ + --namespace "${NAMESPACE}" \ + --create-namespace \ + --values ./tests/resources/config/values.yaml \ + private-gcp-mirror \ + ./docker-gcp-private-mirror-0.0.1.tgz + +echo "Waiting for the Pod to be ready..." +kubectl wait --for=condition=Ready pod/private-gcp-mirror --timeout=300s + +# run tests +kubectl exec -it "pod/${POD_NAME}" -- npm run test diff --git a/.helmignore b/src/.helmignore similarity index 89% rename from .helmignore rename to src/.helmignore index 0494518..691fa13 100644 --- a/.helmignore +++ b/src/.helmignore @@ -20,7 +20,4 @@ .project .idea/ *.tmproj -.vscode/ -.devbox/ -devbox.json -devbox.lock \ No newline at end of file +.vscode/ \ No newline at end of file diff --git a/Chart.yaml b/src/Chart.yaml similarity index 100% rename from Chart.yaml rename to src/Chart.yaml diff --git a/src/resources/generate-token-file.sh b/src/resources/generate-token-file.sh new file mode 100644 index 0000000..9b32972 --- /dev/null +++ b/src/resources/generate-token-file.sh @@ -0,0 +1,6 @@ +#!/bin/sh +while true; do + gcloud auth application-default print-access-token | tr -d '\n\t\r ' > "${TOKEN_FILE_PATH}"; + [ $? -ne 0 ] && exit 1 + sleep 300 +done \ No newline at end of file diff --git a/resources/nginx.conf b/src/resources/nginx.conf.template similarity index 56% rename from resources/nginx.conf rename to src/resources/nginx.conf.template index 042bfe1..8307001 100644 --- a/resources/nginx.conf +++ b/src/resources/nginx.conf.template @@ -1,15 +1,18 @@ -http { +# Variables +# TOKEN_FILE_PATH +# PROXY_BUFFER_SIZE +# PROXY_BUFFERS +# PROXY_BUSY_BUFFERS_SIZE +# LARGE_CLIENT_HEADER_BUFFERS +# UPSTREAM_HOST +# BASE_REWRITE_PATH +# TOKEN_CACHE_EXPIRATION_SECONDS +# MAX_AUTH_RETRY_ATTEMPTS + +events { +} - # Environment variables - env TOKEN_FILE_PATH; - env PROXY_BUFFER_SIZE; - env PROXY_BUFFERS; - env PROXY_BUSY_BUFFERS_SIZE; - env LARGE_CLIENT_HEADER_BUFFERS; - env UPSTREAM_HOST; - env BASE_REWRITE_PATH; - env TOKEN_CACHE_EXPIRATION_SECONDS; - env MAX_AUTH_RETRY_ATTEMPTS; +http { lua_shared_dict cache 1m; @@ -26,7 +29,7 @@ http { -- Load configurations from environment variables local token_file_path = getenv_with_default("TOKEN_FILE_PATH", "/config/token") - local token_cache_expiration = tonumber(getenv_with_default("TOKEN_CACHE_EXPIRATION_SECONDS", "3600")) + local token_cache_expiration = tonumber(getenv_with_default("TOKEN_CACHE_EXPIRATION_SECONDS", "300")) -- Function to read the token from the file function read_token() @@ -76,12 +79,16 @@ http { server_name docker-mirror; - proxy_set_header Host $UPSTREAM_HOST; + proxy_intercept_errors on; + recursive_error_pages on; + error_page 401 = @handle_unauth; + error_page 403 = @handle_unauth; + proxy_set_header Host ${UPSTREAM_HOST}; - proxy_buffer_size $PROXY_BUFFER_SIZE; - proxy_buffers $PROXY_BUFFERS; - proxy_busy_buffers_size $PROXY_BUSY_BUFFERS_SIZE; - large_client_header_buffers $LARGE_CLIENT_HEADER_BUFFERS; + proxy_buffer_size ${PROXY_BUFFER_SIZE}; + proxy_buffers ${PROXY_BUFFERS}; + proxy_busy_buffers_size ${PROXY_BUSY_BUFFERS_SIZE}; + large_client_header_buffers ${LARGE_CLIENT_HEADER_BUFFERS}; access_by_lua_block { local token = get_auth_header() @@ -93,39 +100,46 @@ http { end } - header_filter_by_lua_block { - local status = ngx.status - local max_auth_retry_attempts = tonumber(os.getenv("MAX_AUTH_RETRY_ATTEMPTS")) or 1 - - if status == 401 or status == 403 then - local retry_attempt = ngx.ctx.retry_attempt or 0 - if retry_attempt < max_auth_retry_attempts then - ngx.log(ngx.NOTICE, "401/403 detected, retrying after refreshing the token") - ngx.ctx.retry_attempt = retry_attempt + 1 - get_auth_header(true) - return ngx.exec(ngx.var.request_uri) + location @handle_unauth { + internal; + + rewrite_by_lua_block { + local retry_attempt = tonumber(ngx.req.get_headers()["X-Retry-Attempt"]) or 1 + local max_retry_attempts = tonumber(os.getenv("MAX_AUTH_RETRY_ATTEMPTS")) or 3 + + ngx.log(ngx.ERR, "401/403 detected, retrying after refreshing the token (Attempt: " .. retry_attempt .. "/" .. max_retry_attempts .. ")") + + if retry_attempt < max_retry_attempts then + ngx.req.set_header("X-Retry-Attempt", retry_attempt + 1) + + local token = get_auth_header(true) + if token then + ngx.req.set_header("Authorization", token) + ngx.exec(ngx.var.request_uri) -- Relancer la requête + else + ngx.log(ngx.ERR, "Failed to refresh token on retry attempt #" .. retry_attempt) + end else - ngx.status = ngx.HTTP_FORBIDDEN - ngx.log(ngx.ERR, "Maximum retry limit reached, request failed") + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR ngx.say("Access denied after multiple token refresh attempts") - ngx.exit(ngx.HTTP_FORBIDDEN) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - end + } } # Main base URI location / { - proxy_pass https://$UPSTREAM_HOST; + proxy_pass ${UPSTREAM_PROTOCOL}://${UPSTREAM_HOST}; } # Base URI for /v2/ location ~ /v2/ { - proxy_pass https://$UPSTREAM_HOST; + proxy_pass ${UPSTREAM_PROTOCOL}://${UPSTREAM_HOST}; # URL rewriting specific to /v2/ - set $base_rewrite_path $BASE_REWRITE_PATH; + set $base_rewrite_path ${BASE_REWRITE_PATH}; if ($request_uri ~* "^/v2/(.+)$") { - rewrite ^/v2/(.+)$ $base_rewrite_path/$1 break; + rewrite ^/v2/(.+)$ /v2/$base_rewrite_path/$1 break; } } } diff --git a/templates/NOTES.txt b/src/templates/NOTES.txt similarity index 100% rename from templates/NOTES.txt rename to src/templates/NOTES.txt diff --git a/templates/_helpers.tpl b/src/templates/_helpers.tpl similarity index 100% rename from templates/_helpers.tpl rename to src/templates/_helpers.tpl diff --git a/src/templates/configmap.yaml b/src/templates/configmap.yaml new file mode 100644 index 0000000..2232311 --- /dev/null +++ b/src/templates/configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config +data: + nginx.conf: |- + {{- .Files.Get "resources/nginx.conf.template" | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-env +data: + NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx + TOKEN_FILE_PATH: {{ .Values.nginx.tokenFilePath | quote }} + PROXY_BUFFER_SIZE: {{ .Values.nginx.proxy.bufferSize | quote }} + PROXY_BUFFERS: {{ .Values.nginx.proxy.buffers | quote }} + PROXY_BUSY_BUFFERS_SIZE: {{ .Values.nginx.proxy.busyBuffersSize | quote }} + LARGE_CLIENT_HEADER_BUFFERS: {{ .Values.nginx.proxy.largeClientHeaderBuffers | quote }} + UPSTREAM_HOST: {{ .Values.nginx.proxy.upstreamHost | quote }} + UPSTREAM_PROTOCOL: {{ .Values.nginx.proxy.upstreamProtocol | quote }} + BASE_REWRITE_PATH: {{ .Values.nginx.proxy.rewritePath | quote }} + TOKEN_CACHE_EXPIRATION_SECONDS: {{ .Values.nginx.proxy.tokenCacheExpirationSeconds | quote }} + MAX_AUTH_RETRY_ATTEMPTS: {{ .Values.nginx.proxy.maxAuthRetryAttempts | quote }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-cloud-sdk-script +data: + generate-token-file.sh: |- + {{- .Values.cloudSdk.script | default (.Files.Get "resources/generate-token-file.sh") | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-cloud-sdk-env +data: + TOKEN_FILE_PATH: {{ .Values.nginx.tokenFilePath | quote }} \ No newline at end of file diff --git a/templates/deployment.yaml b/src/templates/deployment.yaml similarity index 81% rename from templates/deployment.yaml rename to src/templates/deployment.yaml index 76e3bbb..149018b 100644 --- a/templates/deployment.yaml +++ b/src/templates/deployment.yaml @@ -31,6 +31,7 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: + # Nginx container - name: nginx securityContext: {{- toYaml .Values.nginx.securityContext | nindent 12 }} @@ -57,26 +58,20 @@ spec: - name: shared-config mountPath: /config - name: nginx-config - mountPath: /etc/nginx/nginx.conf + mountPath: /etc/nginx/templates/nginx.conf.template subPath: nginx.conf + envFrom: + - configMapRef: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-env + # Cloud SDK sidecar - name: cloud-sdk securityContext: {{- toYaml .Values.cloudSdk.securityContext | nindent 12 }} image: "{{ .Values.cloudSdk.image.repository }}:{{ .Values.cloudSdk.image.tag | default "latest" }}" imagePullPolicy: {{ .Values.cloudSdk.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP command: - - '/bin/sh' - - '-c' - - | - while true; do - gcloud auth application-default print-access-token | tr -d '\n\t\r ' > /config/token; - [ $? -ne 0 ] && exit 1 - sleep 300 - done + - /bin/sh + - /tmp/generate-token-file.sh resources: {{- toYaml .Values.cloudSdk.resources | nindent 12 }} volumeMounts: @@ -85,6 +80,12 @@ spec: {{- end }} - name: shared-config mountPath: /config + - name: cloud-sdk-script + mountPath: /tmp/generate-token-file.sh + subPath: generate-token-file.sh + envFrom: + - configMapRef: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-cloud-sdk-env volumes: {{- if .Values.volumes }} {{- toYaml .Values.volumes | nindent 8 }} @@ -92,6 +93,9 @@ spec: - name: nginx-config configMap: name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config + - name: cloud-sdk-script + configMap: + name: {{ include "docker-gcp-private-mirror.fullname" . }}-cloud-sdk-script - name: shared-config emptyDir: {} {{- with .Values.nodeSelector }} diff --git a/templates/hpa.yaml b/src/templates/hpa.yaml similarity index 100% rename from templates/hpa.yaml rename to src/templates/hpa.yaml diff --git a/templates/service.yaml b/src/templates/service.yaml similarity index 100% rename from templates/service.yaml rename to src/templates/service.yaml diff --git a/templates/serviceaccount.yaml b/src/templates/serviceaccount.yaml similarity index 100% rename from templates/serviceaccount.yaml rename to src/templates/serviceaccount.yaml diff --git a/values.yaml b/src/values.yaml similarity index 61% rename from values.yaml rename to src/values.yaml index 93a49c0..8a139c0 100644 --- a/values.yaml +++ b/src/values.yaml @@ -2,19 +2,33 @@ replicaCount: 1 nginx: image: - repository: mirror.gcr.io/nginx + repository: fabiocicerchia/nginx-lua pullPolicy: IfNotPresent tag: "" volumeMounts: [] resources: {} + securityContext: {} + tokenFilePath: /config/token + proxy: + bufferSize: '128k' + buffers: '4 256k' + busyBuffersSize: '256k' + largeClientHeaderBuffers: '16 5120k' + upstreamHost: 'europe-docker.pkg.dev' + upstreamProtocol: 'https' + rewritePath: 'gcp_project/registry_name' + tokenCacheExpirationSeconds: '300' + maxAuthRetryAttempts: '1' cloudSdk: - image: + image: repository: mirror.gcr.io/google/cloud-sdk pullPolicy: IfNotPresent tag: "" volumeMounts: [] resources: {} + securityContext: {} + script: null imagePullSecrets: [] nameOverride: "" diff --git a/templates/configmap.yaml b/templates/configmap.yaml deleted file mode 100644 index 4b7aeae..0000000 --- a/templates/configmap.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "docker-gcp-private-mirror.fullname" . }}-nginx-config -data: - nginx.conf: |- -{{ .Files.Get "resources/nginx.conf" | indent 4}} \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..041bf83 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,12 @@ +FROM node:23-alpine + +WORKDIR /usr/src/app + +COPY package*.json ./ +RUN npm install + +RUN npm install -g ts-node typescript + +COPY . . + +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/tests/jest.config.js b/tests/jest.config.js new file mode 100644 index 0000000..9864325 --- /dev/null +++ b/tests/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; \ No newline at end of file diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 0000000..ee1aeb5 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,5007 @@ +{ + "name": "tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "mock-http-server": "^1.4.5", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.80", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", + "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mock-http-server": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/mock-http-server/-/mock-http-server-1.4.5.tgz", + "integrity": "sha512-7WZx7RJmMQEvTxJTOJt9U6+gLFl0JFaPHLSsAngfLaOgr3UH+ci7PW8049quyXwZGI4mh3W8oAch6w406ccyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "body-parser": "^1.18.1", + "connect": "^3.4.0", + "multiparty": "^4.1.2", + "underscore": "^1.8.3" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/multiparty": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz", + "integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-errors": "~1.8.1", + "safe-buffer": "5.2.1", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/multiparty/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multiparty/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multiparty/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..fd08d09 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,22 @@ +{ + "name": "tests", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "ts-node ./src/server/server.ts", + "test": "jest --runInBand" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "mock-http-server": "^1.4.5", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/tests/resources/config/values.yaml b/tests/resources/config/values.yaml new file mode 100644 index 0000000..43972da --- /dev/null +++ b/tests/resources/config/values.yaml @@ -0,0 +1,17 @@ +fullnameOverride: private-gcp-mirror +cloudSdk: + image: + repository: quay.io/curl/curl + script: |- + #!/bin/sh + while true; do + curl http://mirror-test/token > /config/token; + [ $? -ne 0 ] && exit 1 + sleep 1 + done +nginx: + proxy: + upstreamHost: 'mirror-test' + upstreamProtocol: http + rewritePath: 'test-project/test-registry-name' + maxAuthRetryAttempts: 3 \ No newline at end of file diff --git a/tests/resources/pod.yaml b/tests/resources/pod.yaml new file mode 100644 index 0000000..64dffe9 --- /dev/null +++ b/tests/resources/pod.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Pod +metadata: + name: &name mirror-test + labels: + app: *name +spec: + containers: + - name: *name + image: *name + imagePullPolicy: Never + ports: + - name: http + containerPort: 3000 + protocol: TCP + resources: + requests: + memory: "512Mi" + cpu: "256m" + limits: + memory: "512Mi" + cpu: "256m" + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + livenessProbe: + httpGet: + path: /health + port: http +--- +apiVersion: v1 +kind: Service +metadata: + name: &name mirror-test +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + selector: + app: *name diff --git a/tests/src/authz.spec.ts b/tests/src/authz.spec.ts new file mode 100644 index 0000000..34ccb9e --- /dev/null +++ b/tests/src/authz.spec.ts @@ -0,0 +1,77 @@ +import { mirrorRequest, mockServerRequest } from "./config/config"; +import { + ERROR_IMAGE_NAME_URI, + FORWARDED_ERROR_IMAGE_NAME_URI, + FORWARDED_IMAGE_NAME_URI, + FORWARDED_MISSING_IMAGE_NAME_URI, + IMAGE_NAME_URI, +} from "./config/routes"; +import { Request } from "./server/types/request.type"; +import { SuperTestResponse } from "./server/types/supertest-response.type"; + +describe("authz cache management", () => { + it("should success call after reload bearer cache token in mirror service", async () => { + // update token parsed by sidecar + await mockServerRequest + .put("/token") + .set("Content-Type", "text/plain") + .buffer(true) + .parse((res, cb) => { + let data = Buffer.from(""); + res.on("data", (chunk) => (data = Buffer.concat([data, chunk]))); + res.on("end", () => cb(null, data.toString())); + }) + .send("xyz"); + + // wait sidecar parse new token calling mock server + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // call mirror server + const mirrorResponse = await mirrorRequest.get(IMAGE_NAME_URI); + + expect(mirrorResponse.statusCode).toEqual(200); + + const { body: mockServerRequests }: SuperTestResponse = + await mockServerRequest + .get("/requests") + .query({ query: JSON.stringify({ path: FORWARDED_IMAGE_NAME_URI }) }); + + // first call bearer is invalid and mock server return 401 + expect(mockServerRequests).toHaveLength(2); + expect(mockServerRequests[0]).toMatchObject({ + uri: FORWARDED_IMAGE_NAME_URI, + statusCode: 401, + headers: expect.objectContaining({ + authorization: "Bearer abcd", + }), + }); + + // after cache reload, mirror use updated bearer and mock server return 200 + expect(mockServerRequests[1]).toMatchObject({ + uri: FORWARDED_IMAGE_NAME_URI, + statusCode: 200, + headers: expect.objectContaining({ + authorization: "Bearer xyz", + }), + }); + }); + + it("should failed because unauthorized", async () => { + const mirrorResponse = await mirrorRequest.get(ERROR_IMAGE_NAME_URI); + + expect(mirrorResponse.statusCode).toEqual(500); + + const { body: mockServerRequests }: SuperTestResponse = + await mirrorRequest.get("/requests").query({ + query: JSON.stringify({ path: FORWARDED_ERROR_IMAGE_NAME_URI }), + }); + + expect(mockServerRequests).toHaveLength(3); + for (const mockServerRequest of mockServerRequests) { + expect(mockServerRequest).toMatchObject({ + uri: FORWARDED_ERROR_IMAGE_NAME_URI, + statusCode: 401, + }); + } + }); +}); diff --git a/tests/src/config/config.ts b/tests/src/config/config.ts new file mode 100644 index 0000000..b9a1501 --- /dev/null +++ b/tests/src/config/config.ts @@ -0,0 +1,9 @@ +import request from "supertest"; +import { MIRROR_HOSTNAME, MOCK_SERVER_HOSTNAME } from "./routes"; + +export const mirrorRequest = request(`http://${MIRROR_HOSTNAME}`); +export const mockServerRequest = request(`http://${MOCK_SERVER_HOSTNAME}`); + +beforeEach(() => + request(`http://${MOCK_SERVER_HOSTNAME}`).post("/requests/reset") +); diff --git a/tests/src/config/routes.ts b/tests/src/config/routes.ts new file mode 100644 index 0000000..75304d5 --- /dev/null +++ b/tests/src/config/routes.ts @@ -0,0 +1,17 @@ +export const DOCKER_REGISTRY_VERSION = "v2"; +export const PROJECT_NAME = "test-project"; +export const REGISTRY_NAME = "test-registry-name"; + +export const MIRROR_HOSTNAME = "private-gcp-mirror"; +export const MOCK_SERVER_HOSTNAME = "mirror-test.mirror-test.svc.cluster.local"; + +export const ANY_ROUTE_URI = `/any-route/test-image-name`; + +export const IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/test-image-name`; +export const FORWARDED_IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/${PROJECT_NAME}/${REGISTRY_NAME}/test-image-name`; + +export const ERROR_IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/error-test-image-name`; +export const FORWARDED_ERROR_IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/${PROJECT_NAME}/${REGISTRY_NAME}/error-test-image-name`; + +export const MISSING_IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/missing-test-image-name`; +export const FORWARDED_MISSING_IMAGE_NAME_URI = `/${DOCKER_REGISTRY_VERSION}/${PROJECT_NAME}/${REGISTRY_NAME}/missing-test-image-name`; diff --git a/tests/src/proxify.spec.ts b/tests/src/proxify.spec.ts new file mode 100644 index 0000000..9a25f8d --- /dev/null +++ b/tests/src/proxify.spec.ts @@ -0,0 +1,68 @@ +import { mockServerRequest, mirrorRequest } from "./config/config"; +import { SuperTestResponse } from "./server/types/supertest-response.type"; +import { + ANY_ROUTE_URI, + FORWARDED_IMAGE_NAME_URI, + FORWARDED_MISSING_IMAGE_NAME_URI, + IMAGE_NAME_URI, + MISSING_IMAGE_NAME_URI, +} from "./config/routes"; + +describe("proxify repository requests", () => { + it("should success proxify using /v2/ rewrite", async () => { + const res = await mirrorRequest.get(IMAGE_NAME_URI); + expect(res.statusCode).toEqual(200); + + const { body: mockServerRequests }: SuperTestResponse = + await mockServerRequest.get("/requests").query({ + query: JSON.stringify({ + path: FORWARDED_IMAGE_NAME_URI, + }), + }); + + expect(mockServerRequests).toHaveLength(1); + expect(mockServerRequests[0]).toMatchObject({ + uri: FORWARDED_IMAGE_NAME_URI, + statusCode: 200, + method: "GET", + }); + }); + + it("should sucess proxify using / rewrite", async () => { + const res = await mirrorRequest.get(ANY_ROUTE_URI); + expect(res.statusCode).toEqual(200); + + const { body: mockServerRequests }: SuperTestResponse = + await mockServerRequest.get("/requests").query({ + query: JSON.stringify({ + path: ANY_ROUTE_URI, + }), + }); + + expect(mockServerRequests).toHaveLength(1); + expect(mockServerRequests[0]).toMatchObject({ + method: "GET", + uri: ANY_ROUTE_URI, + statusCode: 200, + }); + }); + + it("should failed because mock server return 404", async () => { + const res = await mirrorRequest.get(MISSING_IMAGE_NAME_URI); + expect(res.statusCode).toEqual(404); + + const { body: mockServerRequests }: SuperTestResponse = + await mockServerRequest.get("/requests").query({ + query: JSON.stringify({ + path: FORWARDED_MISSING_IMAGE_NAME_URI, + }), + }); + + expect(mockServerRequests).toHaveLength(1); + expect(mockServerRequests[0]).toMatchObject({ + method: "GET", + uri: FORWARDED_MISSING_IMAGE_NAME_URI, + statusCode: 404, + }); + }); +}); diff --git a/tests/src/server/server.ts b/tests/src/server/server.ts new file mode 100644 index 0000000..d876ec6 --- /dev/null +++ b/tests/src/server/server.ts @@ -0,0 +1,136 @@ +import { + ANY_ROUTE_URI, + FORWARDED_ERROR_IMAGE_NAME_URI, + FORWARDED_IMAGE_NAME_URI, + FORWARDED_MISSING_IMAGE_NAME_URI, +} from "../config/routes"; + +const ServerMock = require("mock-http-server"); +const server = new ServerMock({ host: "0.0.0.0", port: 3000 }); +let token = "abcd"; + +server.on({ + method: "GET", + path: "/token", + reply: { + status: 200, + header: { "Content-Type": "text/plain; charset=UTF-8" }, + body: () => token, + }, +}); + +server.on({ + method: "PUT", + path: "/token", + reply: { + status: 200, + header: { "Content-Type": "text/plain; charset=UTF-8" }, + body: (request: any) => { + token = request.body; + console.log("updating token with value", token); + return token; + }, + }, +}); + +server.on({ + method: "GET", + path: "/v2/", + reply: { + status: 200, + }, +}); + +server.on({ + method: "GET", + path: "/health", + reply: { + status: 200, + }, +}); + +server.on({ + method: "GET", + path: "/requests", + reply: { + status: 200, + headers: { "content-type": "application/json" }, + body: (request: { query: { query: string } }) => { + return JSON.stringify( + server + .requests(JSON.parse(request.query.query)) + .map((request: any) => ({ + method: request.method, + headers: request.headers, + body: request.body, + uri: request.originalUrl, + statusCode: request.statusCode, + })) + ); + }, + }, +}); + +server.on({ + method: "POST", + path: "/requests/reset", + reply: { + status: 200, + body: () => server.resetRequests(), + }, +}); + +server.on({ + method: "GET", + path: FORWARDED_IMAGE_NAME_URI, + reply: { + status: (request: any) => { + request.statusCode = + request.headers.authorization === `Bearer ${token}` ? 200 : 401; + return request.statusCode; + }, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ result: "ok" }), + }, +}); + +server.on({ + method: "GET", + path: FORWARDED_MISSING_IMAGE_NAME_URI, + reply: { + status: (request: any) => { + request.statusCode = 404; + return request.statusCode; + }, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ result: "ok" }), + }, +}); + +server.on({ + method: "GET", + path: FORWARDED_ERROR_IMAGE_NAME_URI, + reply: { + status: (request: any) => { + request.statusCode = 401; + return request.statusCode; + }, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ result: "ok" }), + }, +}); + +server.on({ + method: "GET", + path: ANY_ROUTE_URI, + reply: { + status: (request: any) => { + request.statusCode = 200; + return request.statusCode; + }, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ result: "ok" }), + }, +}); + +server.start(() => console.log("server started")); diff --git a/tests/src/server/types/request.type.ts b/tests/src/server/types/request.type.ts new file mode 100644 index 0000000..e3f6d3c --- /dev/null +++ b/tests/src/server/types/request.type.ts @@ -0,0 +1,7 @@ +export interface Request { + method: string; + headers: Record; + body: object; + uri: string; + statusCode: number; +} diff --git a/tests/src/server/types/supertest-response.type.ts b/tests/src/server/types/supertest-response.type.ts new file mode 100644 index 0000000..4a9caa4 --- /dev/null +++ b/tests/src/server/types/supertest-response.type.ts @@ -0,0 +1,3 @@ +import { Response } from "supertest"; + +export type SuperTestResponse = Omit & { body: T }; diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..37f2d97 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "./", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file From 030290f35ed807def47398328314473212d3a610 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:04:45 +0100 Subject: [PATCH 09/36] wip: change test deployment strategy --- .github/workflows/tests.yaml | 2 + resources/scripts/test.sh | 31 ++++++--- tests/package.json | 2 +- tests/resources/config/values.yaml | 4 +- tests/resources/pod.yaml | 65 +++++++++++-------- tests/src/authz.spec.ts | 4 +- tests/src/config/routes.ts | 2 +- .../{server/server.ts => fake-registry.ts} | 2 +- tests/src/proxify.spec.ts | 2 +- tests/src/{server => }/types/request.type.ts | 0 .../types/supertest-response.type.ts | 0 11 files changed, 68 insertions(+), 46 deletions(-) rename tests/src/{server/server.ts => fake-registry.ts} (99%) rename tests/src/{server => }/types/request.type.ts (100%) rename tests/src/{server => }/types/supertest-response.type.ts (100%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 74cdc21..7f70e9a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -17,5 +17,7 @@ jobs: uses: medyagh/setup-minikube@latest - name: Run tests + env: + MINIKUBE: true run: | devbox run test diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index 0225ec7..035420f 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -12,23 +12,33 @@ wait_for_pods_to_be_deleted() { echo "All pods with label selector '${pod_label_selector}' have been deleted." } -POD_NAME=mirror-test NAMESPACE=mirror-test -RELEASE_NAME=private-gcp-mirror + +FAKE_REGISTRY_NAME=fake-registry +FAKE_REGISTRY_IMAGE_NAME=fake-registry +FAKE_REGISTRY_SELECTOR="app=${FAKE_REGISTRY_IMAGE_NAME}" +FAKE_REGISTRY_DEPLOYMENT_NAME="${FAKE_REGISTRY_IMAGE_NAME}" + +MIRROR_RELEASE_NAME=private-gcp-mirror +MIRROR_DEPLOYMENT_NAME=private-gcp-mirror # build and deploy test image -# eval $(minikube docker-env) -docker build -t mirror-test --no-cache ./tests +if [ -n "$MINIKUBE" ]; then + echo "setup minikube docker env" + eval $(minikube docker-env) +fi + +docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml -wait_for_pods_to_be_deleted "${NAMESPACE}" "app=${POD_NAME}" +wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" kubectl apply -f ./tests/resources/pod.yaml echo "Waiting for the Pod to be ready..." -kubectl wait --for=condition=Ready "pod/${POD_NAME}" --timeout=300s +kubectl wait --for=condition=available "deployment/${FAKE_REGISTRY_DEPLOYMENT_NAME}" --timeout=300s -helm uninstall "${RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." -wait_for_pods_to_be_deleted "${NAMESPACE}" "app.kubernetes.io/instance=${RELEASE_NAME}" +helm uninstall "${MIRROR_RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." +wait_for_pods_to_be_deleted "${NAMESPACE}" "app.kubernetes.io/instance=${MIRROR_RELEASE_NAME}" # build and deploy helm chart helm package ./src --version=0.0.1 @@ -36,11 +46,12 @@ helm upgrade --install \ --namespace "${NAMESPACE}" \ --create-namespace \ --values ./tests/resources/config/values.yaml \ - private-gcp-mirror \ + "$MIRROR_RELEASE_NAME" \ ./docker-gcp-private-mirror-0.0.1.tgz echo "Waiting for the Pod to be ready..." -kubectl wait --for=condition=Ready pod/private-gcp-mirror --timeout=300s +kubectl wait --for=condition=available "deployment/${MIRROR_DEPLOYMENT_NAME}" --timeout=300s # run tests +POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") kubectl exec -it "pod/${POD_NAME}" -- npm run test diff --git a/tests/package.json b/tests/package.json index fd08d09..d0fbd50 100644 --- a/tests/package.json +++ b/tests/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "start": "ts-node ./src/server/server.ts", + "start": "ts-node ./src/fake-registry.ts", "test": "jest --runInBand" }, "author": "", diff --git a/tests/resources/config/values.yaml b/tests/resources/config/values.yaml index 43972da..b690ada 100644 --- a/tests/resources/config/values.yaml +++ b/tests/resources/config/values.yaml @@ -5,13 +5,13 @@ cloudSdk: script: |- #!/bin/sh while true; do - curl http://mirror-test/token > /config/token; + curl http://fake-registry/token > /config/token; [ $? -ne 0 ] && exit 1 sleep 1 done nginx: proxy: - upstreamHost: 'mirror-test' + upstreamHost: 'fake-registry' upstreamProtocol: http rewritePath: 'test-project/test-registry-name' maxAuthRetryAttempts: 3 \ No newline at end of file diff --git a/tests/resources/pod.yaml b/tests/resources/pod.yaml index 64dffe9..f079ab9 100644 --- a/tests/resources/pod.yaml +++ b/tests/resources/pod.yaml @@ -1,39 +1,48 @@ -apiVersion: v1 -kind: Pod +apiVersion: apps/v1 +kind: Deployment metadata: - name: &name mirror-test + name: &name fake-registry labels: app: *name spec: - containers: - - name: *name - image: *name - imagePullPolicy: Never - ports: - - name: http - containerPort: 3000 - protocol: TCP - resources: - requests: - memory: "512Mi" - cpu: "256m" - limits: - memory: "512Mi" - cpu: "256m" - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 30 - livenessProbe: - httpGet: - path: /health - port: http + replicas: 1 + selector: + matchLabels: + app: *name + template: + metadata: + labels: + app: *name + spec: + containers: + - name: *name + image: *name + imagePullPolicy: Never + ports: + - name: http + containerPort: 3000 + protocol: TCP + resources: + requests: + memory: "512Mi" + cpu: "256m" + limits: + memory: "512Mi" + cpu: "256m" + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + livenessProbe: + httpGet: + path: /health + port: http --- apiVersion: v1 kind: Service metadata: - name: &name mirror-test + name: &name fake-registry spec: type: ClusterIP ports: diff --git a/tests/src/authz.spec.ts b/tests/src/authz.spec.ts index 34ccb9e..b7206d0 100644 --- a/tests/src/authz.spec.ts +++ b/tests/src/authz.spec.ts @@ -6,8 +6,8 @@ import { FORWARDED_MISSING_IMAGE_NAME_URI, IMAGE_NAME_URI, } from "./config/routes"; -import { Request } from "./server/types/request.type"; -import { SuperTestResponse } from "./server/types/supertest-response.type"; +import { Request } from "./types/request.type"; +import { SuperTestResponse } from "./types/supertest-response.type"; describe("authz cache management", () => { it("should success call after reload bearer cache token in mirror service", async () => { diff --git a/tests/src/config/routes.ts b/tests/src/config/routes.ts index 75304d5..45fdb4c 100644 --- a/tests/src/config/routes.ts +++ b/tests/src/config/routes.ts @@ -3,7 +3,7 @@ export const PROJECT_NAME = "test-project"; export const REGISTRY_NAME = "test-registry-name"; export const MIRROR_HOSTNAME = "private-gcp-mirror"; -export const MOCK_SERVER_HOSTNAME = "mirror-test.mirror-test.svc.cluster.local"; +export const MOCK_SERVER_HOSTNAME = "fake-registry"; export const ANY_ROUTE_URI = `/any-route/test-image-name`; diff --git a/tests/src/server/server.ts b/tests/src/fake-registry.ts similarity index 99% rename from tests/src/server/server.ts rename to tests/src/fake-registry.ts index d876ec6..d56d67d 100644 --- a/tests/src/server/server.ts +++ b/tests/src/fake-registry.ts @@ -3,7 +3,7 @@ import { FORWARDED_ERROR_IMAGE_NAME_URI, FORWARDED_IMAGE_NAME_URI, FORWARDED_MISSING_IMAGE_NAME_URI, -} from "../config/routes"; +} from "./config/routes"; const ServerMock = require("mock-http-server"); const server = new ServerMock({ host: "0.0.0.0", port: 3000 }); diff --git a/tests/src/proxify.spec.ts b/tests/src/proxify.spec.ts index 9a25f8d..8f68bf1 100644 --- a/tests/src/proxify.spec.ts +++ b/tests/src/proxify.spec.ts @@ -1,5 +1,5 @@ import { mockServerRequest, mirrorRequest } from "./config/config"; -import { SuperTestResponse } from "./server/types/supertest-response.type"; +import { SuperTestResponse } from "./types/supertest-response.type"; import { ANY_ROUTE_URI, FORWARDED_IMAGE_NAME_URI, diff --git a/tests/src/server/types/request.type.ts b/tests/src/types/request.type.ts similarity index 100% rename from tests/src/server/types/request.type.ts rename to tests/src/types/request.type.ts diff --git a/tests/src/server/types/supertest-response.type.ts b/tests/src/types/supertest-response.type.ts similarity index 100% rename from tests/src/server/types/supertest-response.type.ts rename to tests/src/types/supertest-response.type.ts From c6dbdfc345c746167bc6eaaf2aa25d577ebc4c3d Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:10:29 +0100 Subject: [PATCH 10/36] chore: add debug --- resources/scripts/test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index 035420f..a3794b7 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -52,6 +52,8 @@ helm upgrade --install \ echo "Waiting for the Pod to be ready..." kubectl wait --for=condition=available "deployment/${MIRROR_DEPLOYMENT_NAME}" --timeout=300s +kubectl get pods -A + # run tests POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") kubectl exec -it "pod/${POD_NAME}" -- npm run test From e91275f8565e0b555a20987f6193e8bed7e32c24 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:16:46 +0100 Subject: [PATCH 11/36] bugfix: add namespace in script --- resources/scripts/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index a3794b7..a593719 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -33,7 +33,7 @@ docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" -kubectl apply -f ./tests/resources/pod.yaml +kubectl apply -f ./tests/resources/pod.yaml -n "$NAMESPACE" echo "Waiting for the Pod to be ready..." kubectl wait --for=condition=available "deployment/${FAKE_REGISTRY_DEPLOYMENT_NAME}" --timeout=300s From 3d226704b37bfe8654ed3262217da7d80611c7cc Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:21:27 +0100 Subject: [PATCH 12/36] bugfix: create namespace before install --- resources/scripts/test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index a593719..24c863e 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -30,7 +30,9 @@ fi docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests -kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml +kubectl create namespace "${NAMESPACE}" + +kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml -n "$NAMESPACE" wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" kubectl apply -f ./tests/resources/pod.yaml -n "$NAMESPACE" From b376e1828626bb738d8b04f4045a9026811874dd Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:26:08 +0100 Subject: [PATCH 13/36] bugfix: use valid namespace in all k8s commands --- resources/scripts/test.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index 24c863e..c8b79f0 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -32,12 +32,13 @@ docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests kubectl create namespace "${NAMESPACE}" + kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml -n "$NAMESPACE" wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" kubectl apply -f ./tests/resources/pod.yaml -n "$NAMESPACE" echo "Waiting for the Pod to be ready..." -kubectl wait --for=condition=available "deployment/${FAKE_REGISTRY_DEPLOYMENT_NAME}" --timeout=300s +kubectl wait --for=condition=available "deployment/${FAKE_REGISTRY_DEPLOYMENT_NAME}" -n "$NAMESPACE" --timeout=300s helm uninstall "${MIRROR_RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." wait_for_pods_to_be_deleted "${NAMESPACE}" "app.kubernetes.io/instance=${MIRROR_RELEASE_NAME}" @@ -52,10 +53,10 @@ helm upgrade --install \ ./docker-gcp-private-mirror-0.0.1.tgz echo "Waiting for the Pod to be ready..." -kubectl wait --for=condition=available "deployment/${MIRROR_DEPLOYMENT_NAME}" --timeout=300s +kubectl wait -n "$NAMESPACE" --timeout=300s --for=condition=available "deployment/${MIRROR_DEPLOYMENT_NAME}" kubectl get pods -A # run tests -POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") +POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") kubectl exec -it "pod/${POD_NAME}" -- npm run test From 9c1512f57852a4b297c2f9342d48fb8debf8bdb3 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 17:35:32 +0100 Subject: [PATCH 14/36] bugfix: use valid namespace in all k8s commands --- resources/scripts/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index c8b79f0..ff73fa8 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -31,7 +31,7 @@ fi docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests kubectl create namespace "${NAMESPACE}" - +kubens "${NAMESPACE}" kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml -n "$NAMESPACE" wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" @@ -59,4 +59,4 @@ kubectl get pods -A # run tests POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") -kubectl exec -it "pod/${POD_NAME}" -- npm run test +kubectl exec -n "$NAMESPACE" -it "pod/${POD_NAME}" -- npm run test From 9402a580989e24599456568fae8b14db95e7b1aa Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 20:00:07 +0100 Subject: [PATCH 15/36] wip: clean script, add linter step --- .github/workflows/tests.yaml | 13 ++ devbox.json | 6 +- devbox.lock | 64 ++++++++ resources/scripts/common.sh | 13 ++ resources/scripts/lint.sh | 7 + resources/scripts/test.sh | 141 +++++++++++------- ...pod.yaml => fake-registry-deployment.yaml} | 0 7 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 resources/scripts/common.sh create mode 100755 resources/scripts/lint.sh rename tests/resources/{pod.yaml => fake-registry-deployment.yaml} (100%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7f70e9a..7879689 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,3 +21,16 @@ jobs: MINIKUBE: true run: | devbox run test + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.11.0 + + - name: Run helm lint + run: | + devbox run lint diff --git a/devbox.json b/devbox.json index 04f674c..a00faa8 100644 --- a/devbox.json +++ b/devbox.json @@ -2,14 +2,16 @@ "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", "packages": [ "kubernetes-helm@latest", - "nodejs@23" + "nodejs@23", + "yq@latest" ], "shell": { "init_hook": [ "echo 'Welcome to devbox!' > /dev/null" ], "scripts": { - "test": "./resources/scripts/test.sh" + "test": "./resources/scripts/test.sh", + "lint": "./resources/scripts/lint.sh" } } } diff --git a/devbox.lock b/devbox.lock index 5d8ad1e..5e7e13e 100644 --- a/devbox.lock +++ b/devbox.lock @@ -113,6 +113,70 @@ "store_path": "/nix/store/cgkpjpl1hspg30dlmv4hhvlhbvik1bn1-nodejs-23.2.0" } } + }, + "yq@latest": { + "last_modified": "2024-12-23T21:10:33Z", + "resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#yq", + "source": "devbox-search", + "version": "3.4.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/cgv2hhnlw4bd0jdj8jvvlcjb9avz1857-python3.12-yq-3.4.3", + "default": true + }, + { + "name": "dist", + "path": "/nix/store/470qg7pi2kycj4lnmy9v0jgarnvm3zk2-python3.12-yq-3.4.3-dist" + } + ], + "store_path": "/nix/store/cgv2hhnlw4bd0jdj8jvvlcjb9avz1857-python3.12-yq-3.4.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xl3sjpmqqi81ibfkynlxw9v3dmg769f7-python3.12-yq-3.4.3", + "default": true + }, + { + "name": "dist", + "path": "/nix/store/lmnvwnhbspljq4kqn9vrr5i0q8693k0z-python3.12-yq-3.4.3-dist" + } + ], + "store_path": "/nix/store/xl3sjpmqqi81ibfkynlxw9v3dmg769f7-python3.12-yq-3.4.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gi4lccm6b46ga339l801fwyzshhifh0j-python3.12-yq-3.4.3", + "default": true + }, + { + "name": "dist", + "path": "/nix/store/3ywlcss71g4s454kl3q2kipri85k4a16-python3.12-yq-3.4.3-dist" + } + ], + "store_path": "/nix/store/gi4lccm6b46ga339l801fwyzshhifh0j-python3.12-yq-3.4.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/04fz3jg4dbfizdlpzqkpvp3j4p6nwg75-python3.12-yq-3.4.3", + "default": true + }, + { + "name": "dist", + "path": "/nix/store/gpgds87xhyd4jpr048vdirbqpz06wk2d-python3.12-yq-3.4.3-dist" + } + ], + "store_path": "/nix/store/04fz3jg4dbfizdlpzqkpvp3j4p6nwg75-python3.12-yq-3.4.3" + } + } } } } diff --git a/resources/scripts/common.sh b/resources/scripts/common.sh new file mode 100644 index 0000000..f9dd924 --- /dev/null +++ b/resources/scripts/common.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +package_mirror_helm_chart() { + VERSION=$1 + + echo "--- package helm chart" + helm package ./src --version="${VERSION}" +} + +lint_mirror_helm_chart() { + echo "--- lint helm chart" + helm lint ./src +} \ No newline at end of file diff --git a/resources/scripts/lint.sh b/resources/scripts/lint.sh new file mode 100755 index 0000000..b5e33c3 --- /dev/null +++ b/resources/scripts/lint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + +source "${SCRIPT_DIR}/common.sh" + +lint_mirror_helm_chart \ No newline at end of file diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index ff73fa8..09d336a 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -1,62 +1,99 @@ #!/bin/bash +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + +source "${SCRIPT_DIR}/common.sh" + +NAMESPACE=mirror-test +FAKE_REGISTRY_IMAGE_NAME=fake-registry +FAKE_REGISTRY_MANIFEST_FILENAME=./tests/resources/fake-registry-deployment.yaml +MIRROR_RELEASE_NAME=private-gcp-mirror + wait_for_pods_to_be_deleted() { - local namespace=$1 - local pod_label_selector=$2 + local POD_LABEL_SELECTOR=$1 - echo "Waiting for all pods with label selector '${pod_label_selector}' to be deleted..." - while kubectl get pods -n "${namespace}" -l "${pod_label_selector}" | grep -q 'Running\|Pending'; do - echo "Pods still present. Retrying in 5 seconds..." + while kubectl get pods -l "${POD_LABEL_SELECTOR}" | grep -q 'Running\|Pending\|Terminating'; do sleep 5 done - echo "All pods with label selector '${pod_label_selector}' have been deleted." } -NAMESPACE=mirror-test +get_fake_registry_deployment_selector() { + MANIFEST_FILENAME=$1 + DEPLOYMENT_SELECTOR=$(yq -r 'select(.kind == "Deployment") | .spec.selector.matchLabels | to_entries | map(.key + "=" + .value) | join(",")' "${MANIFEST_FILENAME}") + echo "${DEPLOYMENT_SELECTOR}" +} -FAKE_REGISTRY_NAME=fake-registry -FAKE_REGISTRY_IMAGE_NAME=fake-registry -FAKE_REGISTRY_SELECTOR="app=${FAKE_REGISTRY_IMAGE_NAME}" -FAKE_REGISTRY_DEPLOYMENT_NAME="${FAKE_REGISTRY_IMAGE_NAME}" +build_clean_and_deploy_fake_registry_deployment() { + IMAGE_NAME=$1 + MANIFEST_FILENAME=$2 + DEPLOYMENT_SELECTOR=$(get_fake_registry_deployment_selector "${MANIFEST_FILENAME}") + DEPLOYMENT_NAME=$(yq -r 'select(.kind == "Deployment") | .metadata.name' "${MANIFEST_FILENAME}") -MIRROR_RELEASE_NAME=private-gcp-mirror -MIRROR_DEPLOYMENT_NAME=private-gcp-mirror - -# build and deploy test image -if [ -n "$MINIKUBE" ]; then - echo "setup minikube docker env" - eval $(minikube docker-env) -fi - -docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests - -kubectl create namespace "${NAMESPACE}" -kubens "${NAMESPACE}" - -kubectl delete --ignore-not-found=true -f ./tests/resources/pod.yaml -n "$NAMESPACE" -wait_for_pods_to_be_deleted "${NAMESPACE}" "${FAKE_REGISTRY_SELECTOR}" - -kubectl apply -f ./tests/resources/pod.yaml -n "$NAMESPACE" -echo "Waiting for the Pod to be ready..." -kubectl wait --for=condition=available "deployment/${FAKE_REGISTRY_DEPLOYMENT_NAME}" -n "$NAMESPACE" --timeout=300s - -helm uninstall "${MIRROR_RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." -wait_for_pods_to_be_deleted "${NAMESPACE}" "app.kubernetes.io/instance=${MIRROR_RELEASE_NAME}" - -# build and deploy helm chart -helm package ./src --version=0.0.1 -helm upgrade --install \ - --namespace "${NAMESPACE}" \ - --create-namespace \ - --values ./tests/resources/config/values.yaml \ - "$MIRROR_RELEASE_NAME" \ - ./docker-gcp-private-mirror-0.0.1.tgz - -echo "Waiting for the Pod to be ready..." -kubectl wait -n "$NAMESPACE" --timeout=300s --for=condition=available "deployment/${MIRROR_DEPLOYMENT_NAME}" - -kubectl get pods -A - -# run tests -POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" -l "${FAKE_REGISTRY_SELECTOR}") -kubectl exec -n "$NAMESPACE" -it "pod/${POD_NAME}" -- npm run test + echo "--- build fake registry, tests image" + docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests + + echo "--- cleaning fake registry deployment" + kubectl delete --ignore-not-found=true -f "${MANIFEST_FILENAME}" + wait_for_pods_to_be_deleted "${DEPLOYMENT_SELECTOR}" + + echo "--- deploy fake registry deployment" + kubectl apply -f "${MANIFEST_FILENAME}" + kubectl wait --timeout=300s --for=condition=available "deployment/${DEPLOYMENT_NAME}" +} + +package_clean_and_deploy_mirror_helm_chart() { + RELEASE_NAME=$1 + NAMESPACE=$2 + PACKAGE_VERSION="0.0.1" + + package_mirror_helm_chart "${PACKAGE_VERSION}" + + echo "--- uninstall helm deployment" + helm uninstall "${RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." + wait_for_pods_to_be_deleted "app.kubernetes.io/instance=${RELEASE_NAME}" + + echo "--- deploy helm chart" + helm upgrade --install \ + --namespace "${NAMESPACE}" \ + --values ./tests/resources/config/values.yaml \ + "$RELEASE_NAME" \ + ./docker-gcp-private-mirror-0.0.1.tgz + kubectl wait --timeout=300s --for=condition=available "deployment/${RELEASE_NAME}" +} + +configure_env() { + + if [ -n "$MINIKUBE" ]; then + echo "--- setup minikube docker env" + eval $(minikube docker-env) + fi + + echo "--- create namespace and set current" + kubectl create namespace "${NAMESPACE}" + kubectl config set-context --current --namespace="${NAMESPACE}" +} + +run_tests_in_fake_registry() { + MANIFEST_FILENAME=$1 + NAMESPACE=$2 + + echo "--- running tests in fake registry container" + DEPLOYMENT_SELECTOR=$(get_fake_registry_deployment_selector "${MANIFEST_FILENAME}") + POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" -l "${DEPLOYMENT_SELECTOR}") + kubectl exec -n "$NAMESPACE" -it "pod/${POD_NAME}" -- npm run test +} + +main() { + + configure_env + + build_clean_and_deploy_fake_registry_deployment "${FAKE_REGISTRY_IMAGE_NAME}" "${FAKE_REGISTRY_MANIFEST_FILENAME}" + package_clean_and_deploy_mirror_helm_chart "${MIRROR_RELEASE_NAME}" "${NAMESPACE}" + + echo "--- pods summary" + kubectl get pods + + run_tests_in_fake_registry "${FAKE_REGISTRY_MANIFEST_FILENAME}" "${NAMESPACE}" +} + +main diff --git a/tests/resources/pod.yaml b/tests/resources/fake-registry-deployment.yaml similarity index 100% rename from tests/resources/pod.yaml rename to tests/resources/fake-registry-deployment.yaml From 9b313dec7092747d2428a24d8c1144afac275699 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 20:07:34 +0100 Subject: [PATCH 16/36] fix badge --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index bd22e39..0073636 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/) -![Test Status](https://github.com/sguesdon/docker-gcp-private-mirror/actions/workflows/test.yml/badge.svg) +![Test Status](https://github.com/sguesdon/docker-gcp-private-mirror/actions/workflows/tests.yaml/badge.svg?branch=main) # Docker GCP private mirror - From 6a63d881fb0c7d712130da12fb7ea35f10066ae7 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 21:37:50 +0100 Subject: [PATCH 17/36] wip: create doc, add deployment release pipeline --- .github/workflows/deploy.yaml | 40 +++++++++++++++++++++++++++++++++++ README.md | 34 +++++++++++++++++++++++++++++ devbox.json | 6 ++++-- resources/scripts/publish.sh | 9 ++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy.yaml create mode 100644 resources/scripts/publish.sh diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..2a520b4 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,40 @@ +name: Deploy +on: + push: + tags: + - '*' +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.11.0 + + - name: Check tests passed + uses: actions/github-script@v7 + with: + script: | + const { data: workflows } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: 'main', + status: 'success', + }); + const recentRun = workflows.workflow_runs.find(run => run.head_commit.id === process.env.GITHUB_SHA); + if (!recentRun) { + core.setFailed('Tests did not pass successfully for this commit.'); + } + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Publish helm chart + run: | + helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} + devbox script publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} \ No newline at end of file diff --git a/README.md b/README.md index 0073636..c668d61 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,37 @@ ![Test Status](https://github.com/sguesdon/docker-gcp-private-mirror/actions/workflows/tests.yaml/badge.svg?branch=main) # Docker GCP private mirror + +This project was created with the aim of using an image mirror from Artifact Registry in a Kubernetes cluster (GKE) within Google Cloud Platform. It addresses several issues, the first being the ability to call a mirror that contains a URL with a URI, not just a hostname (it is possible that Containerd now supports this, but that has not always been the case). It also leverages Workload Identity to automatically add an authorization header to all requests sent to the Artifact Registry image mirror. + +## Installation + +Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. + +```sh + +``` + +## Customization + +## Requirements to run tests locally + +To quickly run the project, you need to use Devbox and direnv. Otherwise, I encourage you to install it: [Install DevBox](https://www.jetify.com/docs/devbox/installing_devbox/) [Install direnv](https://www.jetify.com/docs/devbox/ide_configuration/direnv/) + +You will need Kubernetes locally to run the tests. Currently, the tests have already been successfully executed on the Kubernetes provided by Docker Desktop and on Minikube. + +Before running your tests, you must ensure that `kubectl` is properly configured to connect to your local cluster. + +## Running tests + +```sh +# If you are using Minikube, you will need to set the following variable: MINIKUBE=true +devbox run test +``` + +## Lint helm chart + +```sh +# If you are using Minikube, you will need to set the following variable: MINIKUBE=true +devbox run lint +``` diff --git a/devbox.json b/devbox.json index a00faa8..0a659f1 100644 --- a/devbox.json +++ b/devbox.json @@ -7,11 +7,13 @@ ], "shell": { "init_hook": [ - "echo 'Welcome to devbox!' > /dev/null" + "echo 'Welcome to devbox!' > /dev/null", + "cd ./tests && npm install" ], "scripts": { "test": "./resources/scripts/test.sh", - "lint": "./resources/scripts/lint.sh" + "lint": "./resources/scripts/lint.sh", + "publish": "./resources/scripts/publish.sh \"$@\"" } } } diff --git a/resources/scripts/publish.sh b/resources/scripts/publish.sh new file mode 100644 index 0000000..1b5056c --- /dev/null +++ b/resources/scripts/publish.sh @@ -0,0 +1,9 @@ +#!/bin/bash +VERSION=$2 +REPOSITORY=$1 +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + +source "${SCRIPT_DIR}/common.sh" + +package_mirror_helm_chart "${VERSION}" +helm push "docker-gcp-private-mirror-${VERSION}.tgz" "${REPOSITORY}" \ No newline at end of file From 86440c9766b1712f1b21a4076d8968fa6c02f95e Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 21:41:41 +0100 Subject: [PATCH 18/36] hotfix: subprocess to fix cd effect on init --- devbox.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index 0a659f1..5c3e552 100644 --- a/devbox.json +++ b/devbox.json @@ -8,7 +8,7 @@ "shell": { "init_hook": [ "echo 'Welcome to devbox!' > /dev/null", - "cd ./tests && npm install" + "(cd ./tests && npm install)" ], "scripts": { "test": "./resources/scripts/test.sh", From ae13ee9fc83436f6ea1f9a9ed5a2ff0485ef37e9 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 21:46:31 +0100 Subject: [PATCH 19/36] feat: add job to update dockerhub description --- .github/workflows/deploy.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 2a520b4..40e8ace 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -37,4 +37,16 @@ jobs: - name: Publish helm chart run: | helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} - devbox script publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} \ No newline at end of file + devbox script publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} + + update_docker_description: + name: Update docker hub description + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Update docker hub description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} From 47fbdb06188fcd7d31aff99f7f3fc372d002fca9 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 21:57:52 +0100 Subject: [PATCH 20/36] bugfix: remove logic --- .github/workflows/deploy.yaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 40e8ace..e0d4f84 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -13,21 +13,6 @@ jobs: - name: Install devbox uses: jetify-com/devbox-install-action@v0.11.0 - - name: Check tests passed - uses: actions/github-script@v7 - with: - script: | - const { data: workflows } = await github.rest.actions.listWorkflowRunsForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - branch: 'main', - status: 'success', - }); - const recentRun = workflows.workflow_runs.find(run => run.head_commit.id === process.env.GITHUB_SHA); - if (!recentRun) { - core.setFailed('Tests did not pass successfully for this commit.'); - } - - name: Log in to Docker Hub uses: docker/login-action@v3 with: From 35f057aa762696ceb19d0885040ecede73d4daee Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:02:11 +0100 Subject: [PATCH 21/36] bugfix: use only helm registry login with password --- .github/workflows/deploy.yaml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e0d4f84..a7ca480 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -13,25 +13,7 @@ jobs: - name: Install devbox uses: jetify-com/devbox-install-action@v0.11.0 - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Publish helm chart run: | - helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} + helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} devbox script publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} - - update_docker_description: - name: Update docker hub description - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Update docker hub description - uses: peter-evans/dockerhub-description@v4 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} From 175b4e18d7100a4ea4f35a83b5a04278cf8963e7 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:06:54 +0100 Subject: [PATCH 22/36] bugfix: fix db command, use password-stdin --- .github/workflows/deploy.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index a7ca480..11b700c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -15,5 +15,5 @@ jobs: - name: Publish helm chart run: | - helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - devbox script publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} + echo "${{ secrets.DOCKER_PASSWORD }}" | helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + devbox run publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} From 271389f197a17712bade1a72d52a929df0867dfa Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:10:16 +0100 Subject: [PATCH 23/36] hotfix: allow execute publish script --- resources/scripts/publish.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 resources/scripts/publish.sh diff --git a/resources/scripts/publish.sh b/resources/scripts/publish.sh old mode 100644 new mode 100755 From b9669a1a47e879b7565c68896e94045c5759b007 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:15:02 +0100 Subject: [PATCH 24/36] hotfix: fix oci url --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 11b700c..a46b0fd 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -16,4 +16,4 @@ jobs: - name: Publish helm chart run: | echo "${{ secrets.DOCKER_PASSWORD }}" | helm registry login registry-1.docker.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - devbox run publish oci://registry-1.docker.io/docker-gcp-private-mirror ${{ github.ref_name }} + devbox run publish oci://registry-1.docker.io/sguesdon ${{ github.ref_name }} From 336ff4268b50b7ba839e8226a767705744f657d7 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:43:37 +0100 Subject: [PATCH 25/36] doc: update doc --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c668d61..1676fff 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,48 @@ This project was created with the aim of using an image mirror from Artifact Reg Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. +### Command line + ```sh +helm install gcp-mirror oci://registry-1.docker.io/sguesdon/docker-gcp-private-mirror --version 0.0.1 +``` + +### Helm chart dependency + +```yaml +# Chart.yaml +# [...] +dependencies: + - name: docker-gcp-private-mirror + alias: gcp-mirror + version: 0.0.1 + repository: oci://registry-1.docker.io/sguesdon +# [...] +``` + +## Required values +```yaml +fullnameOverride: gcp-mirror +nginx: + proxy: + # Depends on the location of your Artifact Registry repository. + upstreamHost: "europe-docker.pkg.dev" + # This is the missing URI in the mirror configuration to access the repository. + # It is composed of the Google project ID and the name of the Artifact Registry repository. + rewritePath: "gcp_project/registry_name" +serviceAccount: + name: "gcp-mirror" + annotations: + # Properly link the Kubernetes service account with the Google service account so that the sidecar can generate the tokens. + iam.gke.io/gcp-service-account: my-gcp-sa@my-sa-project-id.iam.gserviceaccount.com ``` -## Customization +## All values + +Other configurations are available, including settings related to the NGINX cache. The behavior of the sidecar responsible for retrieving the Google token can also be modified. + +[values.yaml](src/values.yaml) ## Requirements to run tests locally From 3cf8948a1c2ce3fe7b949c56daee80990be9a184 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:44:16 +0100 Subject: [PATCH 26/36] chore: add dsstore to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9e30eb9..ee27b40 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.tgz \ No newline at end of file +*.tgz +.DS_Store \ No newline at end of file From 60b6c3d2f2e3fed9f58df25bae9699b324326119 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Mon, 13 Jan 2025 22:48:08 +0100 Subject: [PATCH 27/36] doc: update doc --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1676fff..e486f0b 100644 --- a/README.md +++ b/README.md @@ -54,22 +54,22 @@ Other configurations are available, including settings related to the NGINX cach ## Requirements to run tests locally -To quickly run the project, you need to use Devbox and direnv. Otherwise, I encourage you to install it: [Install DevBox](https://www.jetify.com/docs/devbox/installing_devbox/) [Install direnv](https://www.jetify.com/docs/devbox/ide_configuration/direnv/) +To quickly run the project, you need to use [DevBox](https://www.jetify.com/docs/devbox/installing_devbox/) and [direnv](https://www.jetify.com/docs/devbox/ide_configuration/direnv/). I encourage you to install it. You will need Kubernetes locally to run the tests. Currently, the tests have already been successfully executed on the Kubernetes provided by Docker Desktop and on Minikube. -Before running your tests, you must ensure that `kubectl` is properly configured to connect to your local cluster. +> Before running your tests, you must ensure that `kubectl` is properly configured to connect to your local cluster. ## Running tests +> If you are using Minikube, you will need to set the following variable: MINIKUBE=true + ```sh -# If you are using Minikube, you will need to set the following variable: MINIKUBE=true devbox run test ``` ## Lint helm chart ```sh -# If you are using Minikube, you will need to set the following variable: MINIKUBE=true devbox run lint ``` From 4ed4e24d6755e502991ed16191628037b5b4b671 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 00:42:27 +0100 Subject: [PATCH 28/36] fact: change test approch to simplify scripts --- .prettierrc.json | 10 +++ README.md | 1 - devbox.json | 3 +- resources/scripts/test.sh | 83 +++++-------------- src/values.yaml | 19 +++-- tests/{ => fake-registry}/.gitignore | 0 tests/{ => fake-registry}/Dockerfile | 0 tests/{ => fake-registry}/jest.config.js | 0 tests/{ => fake-registry}/package-lock.json | 0 tests/{ => fake-registry}/package.json | 0 tests/{ => fake-registry}/src/authz.spec.ts | 0 .../{ => fake-registry}/src/config/config.ts | 0 .../{ => fake-registry}/src/config/routes.ts | 2 +- .../{ => fake-registry}/src/fake-registry.ts | 0 tests/{ => fake-registry}/src/proxify.spec.ts | 0 .../src/types/request.type.ts | 0 .../src/types/supertest-response.type.ts | 0 tests/{ => fake-registry}/tsconfig.json | 0 tests/helm-chart/.gitignore | 2 + tests/helm-chart/.helmignore | 23 +++++ tests/helm-chart/Chart.yaml | 10 +++ .../templates/deployment.yaml} | 29 +------ tests/helm-chart/templates/service.yaml | 12 +++ tests/helm-chart/values.yaml | 18 ++++ tests/resources/config/values.yaml | 17 ---- 25 files changed, 118 insertions(+), 111 deletions(-) create mode 100644 .prettierrc.json rename tests/{ => fake-registry}/.gitignore (100%) rename tests/{ => fake-registry}/Dockerfile (100%) rename tests/{ => fake-registry}/jest.config.js (100%) rename tests/{ => fake-registry}/package-lock.json (100%) rename tests/{ => fake-registry}/package.json (100%) rename tests/{ => fake-registry}/src/authz.spec.ts (100%) rename tests/{ => fake-registry}/src/config/config.ts (100%) rename tests/{ => fake-registry}/src/config/routes.ts (94%) rename tests/{ => fake-registry}/src/fake-registry.ts (100%) rename tests/{ => fake-registry}/src/proxify.spec.ts (100%) rename tests/{ => fake-registry}/src/types/request.type.ts (100%) rename tests/{ => fake-registry}/src/types/supertest-response.type.ts (100%) rename tests/{ => fake-registry}/tsconfig.json (100%) create mode 100644 tests/helm-chart/.gitignore create mode 100644 tests/helm-chart/.helmignore create mode 100644 tests/helm-chart/Chart.yaml rename tests/{resources/fake-registry-deployment.yaml => helm-chart/templates/deployment.yaml} (50%) create mode 100644 tests/helm-chart/templates/service.yaml create mode 100644 tests/helm-chart/values.yaml delete mode 100644 tests/resources/config/values.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..1366709 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": "**/*.yaml", + "options": { + "bracketSpacing": false + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index e486f0b..0ff57fb 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ nginx: # It is composed of the Google project ID and the name of the Artifact Registry repository. rewritePath: "gcp_project/registry_name" serviceAccount: - name: "gcp-mirror" annotations: # Properly link the Kubernetes service account with the Google service account so that the sidecar can generate the tokens. iam.gke.io/gcp-service-account: my-gcp-sa@my-sa-project-id.iam.gserviceaccount.com diff --git a/devbox.json b/devbox.json index 5c3e552..cf3ea7b 100644 --- a/devbox.json +++ b/devbox.json @@ -8,7 +8,8 @@ "shell": { "init_hook": [ "echo 'Welcome to devbox!' > /dev/null", - "(cd ./tests && npm install)" + "(cd ./tests/fake-registry && npm install)", + "(cd ./tests/helm-chart && helm dependency update)" ], "scripts": { "test": "./resources/scripts/test.sh", diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index 09d336a..de65058 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -9,56 +9,31 @@ FAKE_REGISTRY_IMAGE_NAME=fake-registry FAKE_REGISTRY_MANIFEST_FILENAME=./tests/resources/fake-registry-deployment.yaml MIRROR_RELEASE_NAME=private-gcp-mirror -wait_for_pods_to_be_deleted() { - local POD_LABEL_SELECTOR=$1 - - while kubectl get pods -l "${POD_LABEL_SELECTOR}" | grep -q 'Running\|Pending\|Terminating'; do - sleep 5 - done -} - -get_fake_registry_deployment_selector() { - MANIFEST_FILENAME=$1 - DEPLOYMENT_SELECTOR=$(yq -r 'select(.kind == "Deployment") | .spec.selector.matchLabels | to_entries | map(.key + "=" + .value) | join(",")' "${MANIFEST_FILENAME}") - echo "${DEPLOYMENT_SELECTOR}" -} - -build_clean_and_deploy_fake_registry_deployment() { - IMAGE_NAME=$1 - MANIFEST_FILENAME=$2 - DEPLOYMENT_SELECTOR=$(get_fake_registry_deployment_selector "${MANIFEST_FILENAME}") - DEPLOYMENT_NAME=$(yq -r 'select(.kind == "Deployment") | .metadata.name' "${MANIFEST_FILENAME}") +build_fake_registry_image() { + DIR=$1 + IMAGE_NAME=$2 echo "--- build fake registry, tests image" - docker build -t "${FAKE_REGISTRY_IMAGE_NAME}" --no-cache ./tests - - echo "--- cleaning fake registry deployment" - kubectl delete --ignore-not-found=true -f "${MANIFEST_FILENAME}" - wait_for_pods_to_be_deleted "${DEPLOYMENT_SELECTOR}" - - echo "--- deploy fake registry deployment" - kubectl apply -f "${MANIFEST_FILENAME}" - kubectl wait --timeout=300s --for=condition=available "deployment/${DEPLOYMENT_NAME}" + docker build -t "${IMAGE_NAME}" --no-cache "${DIR}" } -package_clean_and_deploy_mirror_helm_chart() { +clean_and_deploy_test_helm_chart() { RELEASE_NAME=$1 - NAMESPACE=$2 - PACKAGE_VERSION="0.0.1" - package_mirror_helm_chart "${PACKAGE_VERSION}" - - echo "--- uninstall helm deployment" - helm uninstall "${RELEASE_NAME}" --namespace "${NAMESPACE}" || echo "Helm release not found. Skipping uninstall." - wait_for_pods_to_be_deleted "app.kubernetes.io/instance=${RELEASE_NAME}" - - echo "--- deploy helm chart" - helm upgrade --install \ - --namespace "${NAMESPACE}" \ - --values ./tests/resources/config/values.yaml \ - "$RELEASE_NAME" \ - ./docker-gcp-private-mirror-0.0.1.tgz - kubectl wait --timeout=300s --for=condition=available "deployment/${RELEASE_NAME}" + echo "--- uninstall test helm chart" + helm uninstall "${RELEASE_NAME}" || echo "Helm release not found. Skipping uninstall." + kubectl wait --timeout=300s --for=delete --all "pod" + + echo "--- deploy test helm chart" + helm upgrade --install "$RELEASE_NAME" ./tests/helm-chart + kubectl wait --timeout=300s --for=condition=available --all "deployment" +} + +run_tests_in_fake_registry() { + + echo "--- running tests in fake registry container" + POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "app=fake-registry") + kubectl exec -it "pod/${POD_NAME}" -- npm run test } configure_env() { @@ -73,27 +48,13 @@ configure_env() { kubectl config set-context --current --namespace="${NAMESPACE}" } -run_tests_in_fake_registry() { - MANIFEST_FILENAME=$1 - NAMESPACE=$2 - - echo "--- running tests in fake registry container" - DEPLOYMENT_SELECTOR=$(get_fake_registry_deployment_selector "${MANIFEST_FILENAME}") - POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers -o custom-columns=":metadata.name" -l "${DEPLOYMENT_SELECTOR}") - kubectl exec -n "$NAMESPACE" -it "pod/${POD_NAME}" -- npm run test -} - main() { - configure_env - build_clean_and_deploy_fake_registry_deployment "${FAKE_REGISTRY_IMAGE_NAME}" "${FAKE_REGISTRY_MANIFEST_FILENAME}" - package_clean_and_deploy_mirror_helm_chart "${MIRROR_RELEASE_NAME}" "${NAMESPACE}" - - echo "--- pods summary" - kubectl get pods + build_fake_registry_image "./tests/fake-registry" "fake-registry" + clean_and_deploy_test_helm_chart "fake-registry" - run_tests_in_fake_registry "${FAKE_REGISTRY_MANIFEST_FILENAME}" "${NAMESPACE}" + run_tests_in_fake_registry } main diff --git a/src/values.yaml b/src/values.yaml index 8a139c0..4dbbcb5 100644 --- a/src/values.yaml +++ b/src/values.yaml @@ -10,10 +10,10 @@ nginx: securityContext: {} tokenFilePath: /config/token proxy: - bufferSize: '128k' - buffers: '4 256k' - busyBuffersSize: '256k' - largeClientHeaderBuffers: '16 5120k' + bufferSize: '256k' + buffers: '8 256k' + busyBuffersSize: '512k' + largeClientHeaderBuffers: '32 5120k' upstreamHost: 'europe-docker.pkg.dev' upstreamProtocol: 'https' rewritePath: 'gcp_project/registry_name' @@ -29,6 +29,13 @@ cloudSdk: resources: {} securityContext: {} script: null + # script: |- + # #!/bin/sh + # while true; do + # gcloud auth application-default print-access-token | tr -d '\n\t\r ' > "${TOKEN_FILE_PATH}"; + # [ $? -ne 0 ] && exit 1 + # sleep 300 + # done imagePullSecrets: [] nameOverride: "" @@ -36,9 +43,11 @@ fullnameOverride: "" serviceAccount: create: true + name: "" automount: true annotations: {} - name: "" + # annotations: + # iam.gke.io/gcp-service-account: my-gcp-sa@my-sa-project-id.iam.gserviceaccount.com podAnnotations: {} podLabels: {} diff --git a/tests/.gitignore b/tests/fake-registry/.gitignore similarity index 100% rename from tests/.gitignore rename to tests/fake-registry/.gitignore diff --git a/tests/Dockerfile b/tests/fake-registry/Dockerfile similarity index 100% rename from tests/Dockerfile rename to tests/fake-registry/Dockerfile diff --git a/tests/jest.config.js b/tests/fake-registry/jest.config.js similarity index 100% rename from tests/jest.config.js rename to tests/fake-registry/jest.config.js diff --git a/tests/package-lock.json b/tests/fake-registry/package-lock.json similarity index 100% rename from tests/package-lock.json rename to tests/fake-registry/package-lock.json diff --git a/tests/package.json b/tests/fake-registry/package.json similarity index 100% rename from tests/package.json rename to tests/fake-registry/package.json diff --git a/tests/src/authz.spec.ts b/tests/fake-registry/src/authz.spec.ts similarity index 100% rename from tests/src/authz.spec.ts rename to tests/fake-registry/src/authz.spec.ts diff --git a/tests/src/config/config.ts b/tests/fake-registry/src/config/config.ts similarity index 100% rename from tests/src/config/config.ts rename to tests/fake-registry/src/config/config.ts diff --git a/tests/src/config/routes.ts b/tests/fake-registry/src/config/routes.ts similarity index 94% rename from tests/src/config/routes.ts rename to tests/fake-registry/src/config/routes.ts index 45fdb4c..2629f94 100644 --- a/tests/src/config/routes.ts +++ b/tests/fake-registry/src/config/routes.ts @@ -2,7 +2,7 @@ export const DOCKER_REGISTRY_VERSION = "v2"; export const PROJECT_NAME = "test-project"; export const REGISTRY_NAME = "test-registry-name"; -export const MIRROR_HOSTNAME = "private-gcp-mirror"; +export const MIRROR_HOSTNAME = "gcp-mirror"; export const MOCK_SERVER_HOSTNAME = "fake-registry"; export const ANY_ROUTE_URI = `/any-route/test-image-name`; diff --git a/tests/src/fake-registry.ts b/tests/fake-registry/src/fake-registry.ts similarity index 100% rename from tests/src/fake-registry.ts rename to tests/fake-registry/src/fake-registry.ts diff --git a/tests/src/proxify.spec.ts b/tests/fake-registry/src/proxify.spec.ts similarity index 100% rename from tests/src/proxify.spec.ts rename to tests/fake-registry/src/proxify.spec.ts diff --git a/tests/src/types/request.type.ts b/tests/fake-registry/src/types/request.type.ts similarity index 100% rename from tests/src/types/request.type.ts rename to tests/fake-registry/src/types/request.type.ts diff --git a/tests/src/types/supertest-response.type.ts b/tests/fake-registry/src/types/supertest-response.type.ts similarity index 100% rename from tests/src/types/supertest-response.type.ts rename to tests/fake-registry/src/types/supertest-response.type.ts diff --git a/tests/tsconfig.json b/tests/fake-registry/tsconfig.json similarity index 100% rename from tests/tsconfig.json rename to tests/fake-registry/tsconfig.json diff --git a/tests/helm-chart/.gitignore b/tests/helm-chart/.gitignore new file mode 100644 index 0000000..1654d0d --- /dev/null +++ b/tests/helm-chart/.gitignore @@ -0,0 +1,2 @@ +charts/ +Chart.lock \ No newline at end of file diff --git a/tests/helm-chart/.helmignore b/tests/helm-chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/tests/helm-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/tests/helm-chart/Chart.yaml b/tests/helm-chart/Chart.yaml new file mode 100644 index 0000000..73f01ab --- /dev/null +++ b/tests/helm-chart/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: helm-chart +type: application +version: 0.0.1 +appVersion: "0.0.1" +dependencies: + - name: docker-gcp-private-mirror + alias: private-gcp-mirror + version: 0.0.1 + repository: file://../../../src \ No newline at end of file diff --git a/tests/resources/fake-registry-deployment.yaml b/tests/helm-chart/templates/deployment.yaml similarity index 50% rename from tests/resources/fake-registry-deployment.yaml rename to tests/helm-chart/templates/deployment.yaml index f079ab9..323c08e 100644 --- a/tests/resources/fake-registry-deployment.yaml +++ b/tests/helm-chart/templates/deployment.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: &name fake-registry + name: &name {{.Release.Name}} labels: app: *name spec: @@ -19,35 +19,14 @@ spec: image: *name imagePullPolicy: Never ports: - - name: http - containerPort: 3000 - protocol: TCP - resources: - requests: - memory: "512Mi" - cpu: "256m" - limits: - memory: "512Mi" - cpu: "256m" + - name: http + containerPort: 3000 + protocol: TCP readinessProbe: httpGet: path: /health port: http - initialDelaySeconds: 30 livenessProbe: httpGet: path: /health port: http ---- -apiVersion: v1 -kind: Service -metadata: - name: &name fake-registry -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: http - protocol: TCP - selector: - app: *name diff --git a/tests/helm-chart/templates/service.yaml b/tests/helm-chart/templates/service.yaml new file mode 100644 index 0000000..b264faa --- /dev/null +++ b/tests/helm-chart/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: &name {{.Release.Name}} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + selector: + app: *name diff --git a/tests/helm-chart/values.yaml b/tests/helm-chart/values.yaml new file mode 100644 index 0000000..1e892e5 --- /dev/null +++ b/tests/helm-chart/values.yaml @@ -0,0 +1,18 @@ +private-gcp-mirror: + fullnameOverride: gcp-mirror + cloudSdk: + image: + repository: quay.io/curl/curl + script: |- + #!/bin/sh + while true; do + curl http://fake-registry/token > /config/token; + [ $? -ne 0 ] && exit 1 + sleep 1 + done + nginx: + proxy: + upstreamHost: 'fake-registry' + upstreamProtocol: http + rewritePath: 'test-project/test-registry-name' + maxAuthRetryAttempts: 3 \ No newline at end of file diff --git a/tests/resources/config/values.yaml b/tests/resources/config/values.yaml deleted file mode 100644 index b690ada..0000000 --- a/tests/resources/config/values.yaml +++ /dev/null @@ -1,17 +0,0 @@ -fullnameOverride: private-gcp-mirror -cloudSdk: - image: - repository: quay.io/curl/curl - script: |- - #!/bin/sh - while true; do - curl http://fake-registry/token > /config/token; - [ $? -ne 0 ] && exit 1 - sleep 1 - done -nginx: - proxy: - upstreamHost: 'fake-registry' - upstreamProtocol: http - rewritePath: 'test-project/test-registry-name' - maxAuthRetryAttempts: 3 \ No newline at end of file From cf8fe6c79b9a0fc02fb936cfbae857389b033500 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 01:31:16 +0100 Subject: [PATCH 29/36] bugfix: configure probe to prevent proxy restart --- resources/scripts/test.sh | 20 +++++++++++--------- src/templates/deployment.yaml | 14 ++++++-------- src/values.yaml | 10 ++++++++++ tests/helm-chart/Chart.yaml | 2 +- tests/helm-chart/values.yaml | 3 +++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index de65058..00b90a1 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -5,33 +5,35 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" source "${SCRIPT_DIR}/common.sh" NAMESPACE=mirror-test + FAKE_REGISTRY_IMAGE_NAME=fake-registry FAKE_REGISTRY_MANIFEST_FILENAME=./tests/resources/fake-registry-deployment.yaml + MIRROR_RELEASE_NAME=private-gcp-mirror build_fake_registry_image() { DIR=$1 IMAGE_NAME=$2 - echo "--- build fake registry, tests image" + echo "--- Build fake registry, tests image" docker build -t "${IMAGE_NAME}" --no-cache "${DIR}" } clean_and_deploy_test_helm_chart() { RELEASE_NAME=$1 - echo "--- uninstall test helm chart" - helm uninstall "${RELEASE_NAME}" || echo "Helm release not found. Skipping uninstall." + echo "--- Uninstall test helm chart" + helm uninstall "${RELEASE_NAME}" || echo "Skipping uninstall" kubectl wait --timeout=300s --for=delete --all "pod" - echo "--- deploy test helm chart" - helm upgrade --install "$RELEASE_NAME" ./tests/helm-chart + echo "--- Deploy test helm chart" + helm upgrade --dependency-update --install "$RELEASE_NAME" ./tests/helm-chart kubectl wait --timeout=300s --for=condition=available --all "deployment" } run_tests_in_fake_registry() { - echo "--- running tests in fake registry container" + echo "--- Running tests in fake registry container" POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "app=fake-registry") kubectl exec -it "pod/${POD_NAME}" -- npm run test } @@ -39,12 +41,12 @@ run_tests_in_fake_registry() { configure_env() { if [ -n "$MINIKUBE" ]; then - echo "--- setup minikube docker env" + echo "--- Setup minikube docker env" eval $(minikube docker-env) fi - echo "--- create namespace and set current" - kubectl create namespace "${NAMESPACE}" + echo "--- Create namespace and set current" + kubectl create namespace "${NAMESPACE}" || echo "namespace already exist" kubectl config set-context --current --namespace="${NAMESPACE}" } diff --git a/src/templates/deployment.yaml b/src/templates/deployment.yaml index 149018b..ef71fb5 100644 --- a/src/templates/deployment.yaml +++ b/src/templates/deployment.yaml @@ -41,14 +41,9 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP - readinessProbe: - httpGet: - path: /v2/ - port: http - livenessProbe: - httpGet: - path: /v2/ - port: http + {{- if .Values.nginx.probes }} + {{- toYaml .Values.nginx.probes | nindent 10 }} + {{- end }} resources: {{- toYaml .Values.nginx.resources | nindent 12 }} volumeMounts: @@ -69,6 +64,9 @@ spec: {{- toYaml .Values.cloudSdk.securityContext | nindent 12 }} image: "{{ .Values.cloudSdk.image.repository }}:{{ .Values.cloudSdk.image.tag | default "latest" }}" imagePullPolicy: {{ .Values.cloudSdk.image.pullPolicy }} + {{- if .Values.cloudSdk.probes }} + {{- toYaml .Values.cloudSdk.probes | nindent 10 }} + {{- end }} command: - /bin/sh - /tmp/generate-token-file.sh diff --git a/src/values.yaml b/src/values.yaml index 4dbbcb5..8f8d483 100644 --- a/src/values.yaml +++ b/src/values.yaml @@ -9,6 +9,15 @@ nginx: resources: {} securityContext: {} tokenFilePath: /config/token + probes: + readinessProbe: + httpGet: + path: /v2/ + port: http + livenessProbe: + httpGet: + path: /v2/ + port: http proxy: bufferSize: '256k' buffers: '8 256k' @@ -28,6 +37,7 @@ cloudSdk: volumeMounts: [] resources: {} securityContext: {} + probes: null script: null # script: |- # #!/bin/sh diff --git a/tests/helm-chart/Chart.yaml b/tests/helm-chart/Chart.yaml index 73f01ab..e1bc20a 100644 --- a/tests/helm-chart/Chart.yaml +++ b/tests/helm-chart/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - name: docker-gcp-private-mirror alias: private-gcp-mirror version: 0.0.1 - repository: file://../../../src \ No newline at end of file + repository: file://../../src \ No newline at end of file diff --git a/tests/helm-chart/values.yaml b/tests/helm-chart/values.yaml index 1e892e5..9b82470 100644 --- a/tests/helm-chart/values.yaml +++ b/tests/helm-chart/values.yaml @@ -11,6 +11,9 @@ private-gcp-mirror: sleep 1 done nginx: + probes: + readinessProbe: + initialDelaySeconds: 30 proxy: upstreamHost: 'fake-registry' upstreamProtocol: http From 60f3eafa5a37f08aa26b419c749e898187692a5b Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 10:46:38 +0100 Subject: [PATCH 30/36] clean: clean test script --- resources/scripts/test.sh | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index 00b90a1..d0eebbe 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -5,22 +5,21 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" source "${SCRIPT_DIR}/common.sh" NAMESPACE=mirror-test - +FAKE_REGISTRY_APP_NAME=fake-registry FAKE_REGISTRY_IMAGE_NAME=fake-registry -FAKE_REGISTRY_MANIFEST_FILENAME=./tests/resources/fake-registry-deployment.yaml - -MIRROR_RELEASE_NAME=private-gcp-mirror +TEST_HELM_CHART_RELEASE_NAME=fake-registry +TEST_HELM_CHART_DIR=./tests/fake-registry build_fake_registry_image() { - DIR=$1 - IMAGE_NAME=$2 + local DIR=$1 + local IMAGE_NAME=$2 echo "--- Build fake registry, tests image" docker build -t "${IMAGE_NAME}" --no-cache "${DIR}" } clean_and_deploy_test_helm_chart() { - RELEASE_NAME=$1 + local RELEASE_NAME=$1 echo "--- Uninstall test helm chart" helm uninstall "${RELEASE_NAME}" || echo "Skipping uninstall" @@ -32,13 +31,15 @@ clean_and_deploy_test_helm_chart() { } run_tests_in_fake_registry() { + local APP_SELECTOR=$1 echo "--- Running tests in fake registry container" - POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "app=fake-registry") + POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" -l "app=${APP_SELECTOR}") kubectl exec -it "pod/${POD_NAME}" -- npm run test } configure_env() { + local TARGET_NAMESPACE=$1 if [ -n "$MINIKUBE" ]; then echo "--- Setup minikube docker env" @@ -46,17 +47,15 @@ configure_env() { fi echo "--- Create namespace and set current" - kubectl create namespace "${NAMESPACE}" || echo "namespace already exist" - kubectl config set-context --current --namespace="${NAMESPACE}" + kubectl create namespace "${TARGET_NAMESPACE}" || echo "namespace already exist" + kubectl config set-context --current --namespace="${TARGET_NAMESPACE}" } main() { - configure_env - - build_fake_registry_image "./tests/fake-registry" "fake-registry" - clean_and_deploy_test_helm_chart "fake-registry" - - run_tests_in_fake_registry + configure_env "${NAMESPACE}" + build_fake_registry_image "${TEST_HELM_CHART_DIR}" "${FAKE_REGISTRY_IMAGE_NAME}" + clean_and_deploy_test_helm_chart "${TEST_HELM_CHART_RELEASE_NAME}" + run_tests_in_fake_registry "${FAKE_REGISTRY_APP_NAME}" } main From adeb0ebb0b5de5f77ce34f09d983606c7ed90ea3 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 16:51:53 +0100 Subject: [PATCH 31/36] feature: add tofu project to deploy partial arch on gcp --- README.md | 20 +++++++++ devbox.json | 3 +- devbox.lock | 48 ++++++++++++++++++++ tests/tofu/.gitignore | 4 ++ tests/tofu/.terraform.lock.hcl | 53 ++++++++++++++++++++++ tests/tofu/data.tf | 6 +++ tests/tofu/main.tf | 68 +++++++++++++++++++++++++++++ tests/tofu/providers.tf | 18 ++++++++ tests/tofu/terraform.tfvars.example | 8 ++++ tests/tofu/variables.tf | 23 ++++++++++ 10 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 tests/tofu/.gitignore create mode 100644 tests/tofu/.terraform.lock.hcl create mode 100644 tests/tofu/data.tf create mode 100644 tests/tofu/main.tf create mode 100644 tests/tofu/providers.tf create mode 100644 tests/tofu/terraform.tfvars.example create mode 100644 tests/tofu/variables.tf diff --git a/README.md b/README.md index 0ff57fb..9f464aa 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,26 @@ You will need Kubernetes locally to run the tests. Currently, the tests have alr > Before running your tests, you must ensure that `kubectl` is properly configured to connect to your local cluster. +## Quick Deployment for Testing + +If you want to quickly test the solution, you can do so using the Opentofu project located in the [following folder](tests/tofu). +However, you will need to have gcloud properly configured and an active GKE cluster with Workload Identity enabled. + +Before deploying the solution, make sure to fill in the minimum configurations in the `terraform.tfvars` file. The `terraform.tfvars.example` file contains the required information. + +```sh +cd tests/tofu +tofu init +tofu apply +``` + +After installation, you can quickly test the solution using the following commands: + +```sh +kubectl run dind --rm -it --image=docker:dind --privileged -- --insecure-registry docker-mirror --registry-mirror http://docker-mirror +kubectl exec -it dind -- docker pull redis:latest +``` + ## Running tests > If you are using Minikube, you will need to set the following variable: MINIKUBE=true diff --git a/devbox.json b/devbox.json index cf3ea7b..083175e 100644 --- a/devbox.json +++ b/devbox.json @@ -3,7 +3,8 @@ "packages": [ "kubernetes-helm@latest", "nodejs@23", - "yq@latest" + "yq@latest", + "opentofu@1.8.8" ], "shell": { "init_hook": [ diff --git a/devbox.lock b/devbox.lock index 5e7e13e..1c2583f 100644 --- a/devbox.lock +++ b/devbox.lock @@ -114,6 +114,54 @@ } } }, + "opentofu@1.8.8": { + "last_modified": "2024-12-27T03:08:00Z", + "resolved": "github:NixOS/nixpkgs/7cc0bff31a3a705d3ac4fdceb030a17239412210#opentofu", + "source": "devbox-search", + "version": "1.8.8", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/v4rwnr98r1cfahfzaarffn45kkkk33kz-opentofu-1.8.8", + "default": true + } + ], + "store_path": "/nix/store/v4rwnr98r1cfahfzaarffn45kkkk33kz-opentofu-1.8.8" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/02py2xznm9yajjipm5hjrmwshfq5j3z7-opentofu-1.8.8", + "default": true + } + ], + "store_path": "/nix/store/02py2xznm9yajjipm5hjrmwshfq5j3z7-opentofu-1.8.8" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zrn1girp29g45508i5kwbbwbjya25sc8-opentofu-1.8.8", + "default": true + } + ], + "store_path": "/nix/store/zrn1girp29g45508i5kwbbwbjya25sc8-opentofu-1.8.8" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zzf1nd9yx5fhl3dl7jmaj3lbfshiijv1-opentofu-1.8.8", + "default": true + } + ], + "store_path": "/nix/store/zzf1nd9yx5fhl3dl7jmaj3lbfshiijv1-opentofu-1.8.8" + } + } + }, "yq@latest": { "last_modified": "2024-12-23T21:10:33Z", "resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#yq", diff --git a/tests/tofu/.gitignore b/tests/tofu/.gitignore new file mode 100644 index 0000000..962a54d --- /dev/null +++ b/tests/tofu/.gitignore @@ -0,0 +1,4 @@ +.terraform/ +terraform.tfvars +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/tests/tofu/.terraform.lock.hcl b/tests/tofu/.terraform.lock.hcl new file mode 100644 index 0000000..ff7c700 --- /dev/null +++ b/tests/tofu/.terraform.lock.hcl @@ -0,0 +1,53 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/google" { + version = "6.16.0" + hashes = [ + "h1:AK7x4FojXo4sXzUvsYav7II/qUDqyxjuR9Di6XIxcKk=", + "zh:20aa38d6b62e56ae40401b41ad3d6028e8864a69648b867153380c665a2354b0", + "zh:425886b016178f99077033fde72b0bb302b699449eba4d1968e6b738c560c00c", + "zh:484dc0630e32e8bba04298df58b7ea90f7166842b171b3bf81876e18d04ded97", + "zh:7380930403b0b5874a29bba0e5670ff715fdb18b4ae9b55bdf93af6e4167421e", + "zh:7c5bc18bcff9244b1234d5e21497a92312cceec31c279fda118e01eee5be9b3b", + "zh:804a4466224741bfbcbe5e3cd45b8a15033930dc5d30e5432517dc0fbf9adb6d", + "zh:82abf9ba1fe7c91af173c0aff37b47f4a4f5220d7405801d79e03deb63ffc1b4", + "zh:b1f82ab5a775dbde31550e6ac6af5acbea737b3779ffbd0f07b7e268f2acd7d3", + "zh:cc816edeeb9ee2ed12fe9aa07c26a3504f85ed457e27b6aae714b9456eb7404c", + "zh:e5d9732c86badf550aac54902e473009e076aa0c6aea937ada51609aa1402dac", + ] +} + +provider "registry.opentofu.org/hashicorp/helm" { + version = "2.17.0" + hashes = [ + "h1:ShIag7wqd5Rs+zYpVMpjAh+T0ozr4XGYfSTKWqceQBY=", + "zh:02690815e35131a42cb9851f63a3369c216af30ad093d05b39001d43da04b56b", + "zh:27a62f12b29926387f4d71aeeee9f7ffa0ccb81a1b6066ee895716ad050d1b7a", + "zh:2d0a5babfa73604b3fefc9dab9c87f91c77fce756c2e32b294e9f1290aed26c0", + "zh:3976400ceba6dda4636e1d297e3097e1831de5628afa534a166de98a70d1dcbe", + "zh:54440ef14f342b41d75c1aded7487bfcc3f76322b75894235b47b7e89ac4bfa4", + "zh:6512e2ab9f2fa31cbb90d9249647b5c5798f62eb1215ec44da2cdaa24e38ad25", + "zh:795f327ca0b8c5368af0ed03d5d4f6da7260692b4b3ca0bd004ed542e683464d", + "zh:ba659e1d94f224bc3f1fd34cbb9d2663e3a8e734108e5a58eb49eda84b140978", + "zh:c5c8575c4458835c2acbc3d1ed5570589b14baa2525d8fbd04295c097caf41eb", + "zh:e0877a5dac3de138e61eefa26b2f5a13305a17259779465899880f70e11314e0", + ] +} + +provider "registry.opentofu.org/hashicorp/kubernetes" { + version = "2.35.1" + hashes = [ + "h1:3kVDveERi2qDyLY8zCz/rGmxlGiCAUyJEQR/QiZEIsA=", + "zh:0a569918d9e81755bdacb2380e70ed304c442e957a029984cbcd9ec88e5d3635", + "zh:1d4d1241cf51d7d4a036c774add1384bb1ba9ca16146334d17c730e1b41ad3e0", + "zh:243219f415f5d8caf32a4e6b6bf596c11cf7db5501ccb4ae77cc0b084bb5d108", + "zh:2f3a33cba73918adc6f580c76b252881f22beb75277df8ca26a01eb5411348f9", + "zh:3b5247f69e72d1e94ac965fa570f448436cedb278f3f29836f6a345aa1bbd5b6", + "zh:4206bca7bf30708e235535af50529565b14f30262dc43142153a1774ee5086af", + "zh:490c80454b8808bb937498aea98e4076a74887446b05feb6e200015613b5e065", + "zh:5e39824289f7b29711681bce98fbb6c27ed221b071a8c78fd0de7f6c2dae4371", + "zh:a7bf7892217bdb0464664f62485d89d014874b0dfb564e99c364fc6dd20c6a3b", + "zh:e8251170bad1c3e2d9c22d0f4dae7239f1a364f05732f7dff5c8e4ec76a95c5a", + ] +} diff --git a/tests/tofu/data.tf b/tests/tofu/data.tf new file mode 100644 index 0000000..98a7c62 --- /dev/null +++ b/tests/tofu/data.tf @@ -0,0 +1,6 @@ +data "google_client_config" "this" {} + +data "google_container_cluster" "this" { + name = var.gke.cluster_name + location = var.gke.location +} diff --git a/tests/tofu/main.tf b/tests/tofu/main.tf new file mode 100644 index 0000000..0f56d3d --- /dev/null +++ b/tests/tofu/main.tf @@ -0,0 +1,68 @@ +resource "google_artifact_registry_repository" "this" { + repository_id = var.mirror.name + format = "DOCKER" + description = "Mirror repository for dockerhub images" + location = "europe" + mode = "REMOTE_REPOSITORY" + remote_repository_config { + description = "docker hub" + docker_repository { + public_repository = "DOCKER_HUB" + } + } +} + +resource "google_service_account" "this" { + account_id = var.mirror.name + display_name = "wisa for ${var.mirror.name}" +} + +resource "google_artifact_registry_repository_iam_binding" "this" { + repository = google_artifact_registry_repository.this.name + location = google_artifact_registry_repository.this.location + role = "roles/artifactregistry.reader" + members = ["serviceAccount:${google_service_account.this.email}"] +} + +resource "google_service_account_iam_binding" "this" { + service_account_id = google_service_account.this.name + role = "roles/iam.workloadIdentityUser" + members = ["serviceAccount:${var.google.project_id}.svc.id.goog[${var.gke.namespace}/${var.mirror.name}]"] +} + +resource "kubernetes_namespace" "this" { + metadata { + name = var.mirror.name + } +} + +resource "helm_release" "this" { + name = var.mirror.name + namespace = var.mirror.name + + chart = "docker-gcp-private-mirror" + repository = "oci://registry-1.docker.io/sguesdon" + version = "0.0.1" + + set { + name = "fullnameOverride" + value = var.mirror.name + } + + set { + name = "nginx.proxy.upstreamHost" + value = "${google_artifact_registry_repository.this.location}-docker.pkg.dev" + } + + set { + name = "nginx.proxy.rewritePath" + value = "${google_artifact_registry_repository.this.project}/${google_artifact_registry_repository.this.repository_id}" + } + + set { + name = "serviceAccount.annotations.iam\\.gke\\.io\\/gcp-service-account" + value = google_service_account.this.email + } + + depends_on = [ kubernetes_namespace.this ] +} diff --git a/tests/tofu/providers.tf b/tests/tofu/providers.tf new file mode 100644 index 0000000..c0d2fa2 --- /dev/null +++ b/tests/tofu/providers.tf @@ -0,0 +1,18 @@ +provider "google" { + project = var.google.project_id + region = var.google.region +} + +provider "kubernetes" { + host = data.google_container_cluster.this.endpoint + token = data.google_client_config.this.access_token + cluster_ca_certificate = base64decode(data.google_container_cluster.this.master_auth.0.cluster_ca_certificate) +} + +provider "helm" { + kubernetes { + host = data.google_container_cluster.this.endpoint + token = data.google_client_config.this.access_token + cluster_ca_certificate = base64decode(data.google_container_cluster.this.master_auth.0.cluster_ca_certificate) + } +} \ No newline at end of file diff --git a/tests/tofu/terraform.tfvars.example b/tests/tofu/terraform.tfvars.example new file mode 100644 index 0000000..1430c9d --- /dev/null +++ b/tests/tofu/terraform.tfvars.example @@ -0,0 +1,8 @@ +google = { + project_id = "xxxx" +} + +gke = { + cluster_name = "xxxx" + location = "yyyy" +} \ No newline at end of file diff --git a/tests/tofu/variables.tf b/tests/tofu/variables.tf new file mode 100644 index 0000000..f8a14ec --- /dev/null +++ b/tests/tofu/variables.tf @@ -0,0 +1,23 @@ +variable "google" { + type = object({ + project_id = string + region = optional(string, "eu-west9") + }) + description = "google provider configuration" +} + +variable "mirror" { + type = object({ + name = optional(string, "docker-mirror") + }) + default = {} +} + +variable "gke" { + type = object({ + location = string + cluster_name = string + namespace = optional(string, "docker-mirror") + }) + description = "target cluster configuration" +} \ No newline at end of file From 073b9f9b9d917abf9e558188ddcf47f417dec3e8 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 21:01:25 +0100 Subject: [PATCH 32/36] clean: clean files --- .gitignore | 2 +- .prettierrc.json | 18 +++++++++--------- README.md | 2 +- devbox.json | 2 +- resources/scripts/common.sh | 6 +++--- resources/scripts/lint.sh | 2 +- resources/scripts/publish.sh | 3 ++- resources/scripts/test.sh | 2 +- src/.helmignore | 2 +- tests/fake-registry/.gitignore | 2 +- tests/fake-registry/jest.config.js | 10 +++++----- tests/fake-registry/tsconfig.json | 22 +++++++++++----------- 12 files changed, 37 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index ee27b40..e1c155d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ *.tgz -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.prettierrc.json b/.prettierrc.json index 1366709..5cc37c2 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,10 +1,10 @@ { - "overrides": [ - { - "files": "**/*.yaml", - "options": { - "bracketSpacing": false - } - } - ] -} \ No newline at end of file + "overrides": [ + { + "files": "**/*.yaml", + "options": { + "bracketSpacing": false + } + } + ] +} diff --git a/README.md b/README.md index 9f464aa..c0029db 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This project was created with the aim of using an image mirror from Artifact Reg ## Installation -Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. +Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. An (Opentofu example)[tests/tofu] is available in the tests folder without the GKE cluster deployment.. ### Command line diff --git a/devbox.json b/devbox.json index 083175e..9eb3811 100644 --- a/devbox.json +++ b/devbox.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", "packages": [ "kubernetes-helm@latest", "nodejs@23", diff --git a/resources/scripts/common.sh b/resources/scripts/common.sh index f9dd924..9527fe1 100644 --- a/resources/scripts/common.sh +++ b/resources/scripts/common.sh @@ -8,6 +8,6 @@ package_mirror_helm_chart() { } lint_mirror_helm_chart() { - echo "--- lint helm chart" - helm lint ./src -} \ No newline at end of file + echo "--- lint helm chart" + helm lint ./src +} diff --git a/resources/scripts/lint.sh b/resources/scripts/lint.sh index b5e33c3..4388d08 100755 --- a/resources/scripts/lint.sh +++ b/resources/scripts/lint.sh @@ -4,4 +4,4 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" source "${SCRIPT_DIR}/common.sh" -lint_mirror_helm_chart \ No newline at end of file +lint_mirror_helm_chart diff --git a/resources/scripts/publish.sh b/resources/scripts/publish.sh index 1b5056c..b512f63 100755 --- a/resources/scripts/publish.sh +++ b/resources/scripts/publish.sh @@ -1,4 +1,5 @@ #!/bin/bash + VERSION=$2 REPOSITORY=$1 SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" @@ -6,4 +7,4 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" source "${SCRIPT_DIR}/common.sh" package_mirror_helm_chart "${VERSION}" -helm push "docker-gcp-private-mirror-${VERSION}.tgz" "${REPOSITORY}" \ No newline at end of file +helm push "docker-gcp-private-mirror-${VERSION}.tgz" "${REPOSITORY}" diff --git a/resources/scripts/test.sh b/resources/scripts/test.sh index d0eebbe..5203bd8 100755 --- a/resources/scripts/test.sh +++ b/resources/scripts/test.sh @@ -20,7 +20,7 @@ build_fake_registry_image() { clean_and_deploy_test_helm_chart() { local RELEASE_NAME=$1 - + echo "--- Uninstall test helm chart" helm uninstall "${RELEASE_NAME}" || echo "Skipping uninstall" kubectl wait --timeout=300s --for=delete --all "pod" diff --git a/src/.helmignore b/src/.helmignore index 691fa13..0e8a0eb 100644 --- a/src/.helmignore +++ b/src/.helmignore @@ -20,4 +20,4 @@ .project .idea/ *.tmproj -.vscode/ \ No newline at end of file +.vscode/ diff --git a/tests/fake-registry/.gitignore b/tests/fake-registry/.gitignore index 40b878d..c2658d7 100644 --- a/tests/fake-registry/.gitignore +++ b/tests/fake-registry/.gitignore @@ -1 +1 @@ -node_modules/ \ No newline at end of file +node_modules/ diff --git a/tests/fake-registry/jest.config.js b/tests/fake-registry/jest.config.js index 9864325..1d7759c 100644 --- a/tests/fake-registry/jest.config.js +++ b/tests/fake-registry/jest.config.js @@ -1,8 +1,8 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', + preset: "ts-jest", + testEnvironment: "node", transform: { - '^.+\\.tsx?$': 'ts-jest', + "^.+\\.tsx?$": "ts-jest", }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], -}; \ No newline at end of file + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], +}; diff --git a/tests/fake-registry/tsconfig.json b/tests/fake-registry/tsconfig.json index 37f2d97..b14b416 100644 --- a/tests/fake-registry/tsconfig.json +++ b/tests/fake-registry/tsconfig.json @@ -1,12 +1,12 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "CommonJS", - "rootDir": "./", - "outDir": "./dist", - "strict": true, - "esModuleInterop": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} \ No newline at end of file + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "./", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} From a997967e25d79d3c6da881ff398acb927964790e Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Tue, 14 Jan 2025 21:05:07 +0100 Subject: [PATCH 33/36] clean: eof --- src/resources/generate-token-file.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/generate-token-file.sh b/src/resources/generate-token-file.sh index 9b32972..098c7e1 100644 --- a/src/resources/generate-token-file.sh +++ b/src/resources/generate-token-file.sh @@ -1,6 +1,6 @@ #!/bin/sh while true; do - gcloud auth application-default print-access-token | tr -d '\n\t\r ' > "${TOKEN_FILE_PATH}"; + gcloud auth application-default print-access-token | tr -d '\n\t\r ' >"${TOKEN_FILE_PATH}" [ $? -ne 0 ] && exit 1 sleep 300 -done \ No newline at end of file +done From 86a72f9c9e3436462c4e2df6de1d38eecc005134 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Wed, 15 Jan 2025 10:55:42 +0100 Subject: [PATCH 34/36] clean: clean project --- .gitignore | 1 + .prettierrc.json | 10 ------- README.md | 50 +++++++++++++++---------------- src/resources/nginx.conf.template | 2 +- src/templates/configmap.yaml | 2 +- tests/fake-registry/Dockerfile | 2 +- tests/helm-chart/.gitignore | 2 +- tests/helm-chart/Chart.yaml | 2 +- tests/helm-chart/values.yaml | 6 ++-- tests/tofu/.gitignore | 2 +- tests/tofu/main.tf | 14 ++++----- tests/tofu/providers.tf | 2 +- tests/tofu/variables.tf | 8 ++--- 13 files changed, 47 insertions(+), 56 deletions(-) delete mode 100644 .prettierrc.json diff --git a/.gitignore b/.gitignore index e1c155d..5bfa28f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.tgz .DS_Store +.vscode/ diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 5cc37c2..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "overrides": [ - { - "files": "**/*.yaml", - "options": { - "bracketSpacing": false - } - } - ] -} diff --git a/README.md b/README.md index c0029db..316ee9a 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,26 @@ This project was created with the aim of using an image mirror from Artifact Registry in a Kubernetes cluster (GKE) within Google Cloud Platform. It addresses several issues, the first being the ability to call a mirror that contains a URL with a URI, not just a hostname (it is possible that Containerd now supports this, but that has not always been the case). It also leverages Workload Identity to automatically add an authorization header to all requests sent to the Artifact Registry image mirror. -## Installation +## Deployment using helm -Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. An (Opentofu example)[tests/tofu] is available in the tests folder without the GKE cluster deployment.. +Before proceeding with the installation, you must have deployed an Artifact Registry repository, have a cluster with Workload Identity enabled, and possess a GCP user with permissions to read from your repository. An (Opentofu example)[tests/tofu] is available in the tests folder without the GKE cluster deployment. + +### Minimum Configuration + +```yaml +fullnameOverride: gcp-mirror +nginx: + proxy: + # Depends on the location of your Artifact Registry repository. + upstreamHost: "europe-docker.pkg.dev" + # This is the missing URI in the mirror configuration to access the repository. + # It is composed of the Google project ID and the name of the Artifact Registry repository. + rewritePath: "gcp_project/registry_name" +serviceAccount: + annotations: + # Properly link the Kubernetes service account with the Google service account so that the sidecar can generate the tokens. + iam.gke.io/gcp-service-account: my-gcp-sa@my-sa-project-id.iam.gserviceaccount.com +``` ### Command line @@ -15,7 +32,7 @@ Before proceeding with the installation, you must have deployed an Artifact Regi helm install gcp-mirror oci://registry-1.docker.io/sguesdon/docker-gcp-private-mirror --version 0.0.1 ``` -### Helm chart dependency +### Using the Helm chart as a Helm dependency ```yaml # Chart.yaml @@ -28,28 +45,9 @@ dependencies: # [...] ``` -## Required values - -```yaml -fullnameOverride: gcp-mirror -nginx: - proxy: - # Depends on the location of your Artifact Registry repository. - upstreamHost: "europe-docker.pkg.dev" - # This is the missing URI in the mirror configuration to access the repository. - # It is composed of the Google project ID and the name of the Artifact Registry repository. - rewritePath: "gcp_project/registry_name" -serviceAccount: - annotations: - # Properly link the Kubernetes service account with the Google service account so that the sidecar can generate the tokens. - iam.gke.io/gcp-service-account: my-gcp-sa@my-sa-project-id.iam.gserviceaccount.com -``` - ## All values -Other configurations are available, including settings related to the NGINX cache. The behavior of the sidecar responsible for retrieving the Google token can also be modified. - -[values.yaml](src/values.yaml) +Other configurations are available, including settings related to the NGINX cache. The behavior of the sidecar responsible for retrieving the Google token can also be modified. All the values are available [here](src/values.yaml). ## Requirements to run tests locally @@ -64,7 +62,7 @@ You will need Kubernetes locally to run the tests. Currently, the tests have alr If you want to quickly test the solution, you can do so using the Opentofu project located in the [following folder](tests/tofu). However, you will need to have gcloud properly configured and an active GKE cluster with Workload Identity enabled. -Before deploying the solution, make sure to fill in the minimum configurations in the `terraform.tfvars` file. The `terraform.tfvars.example` file contains the required information. +Before deploying the solution, make sure to fill in the minimum configurations in the `terraform.tfvars` file. The `terraform.tfvars.example` file contains the minimum required information. ```sh cd tests/tofu @@ -81,7 +79,9 @@ kubectl exec -it dind -- docker pull redis:latest ## Running tests -> If you are using Minikube, you will need to set the following variable: MINIKUBE=true +Before running the tests, ensure that you have a functional Kubernetes cluster in your development environment. + +> If you are using Minikube, you will need to set the following variable: `MINIKUBE=true` ```sh devbox run test diff --git a/src/resources/nginx.conf.template b/src/resources/nginx.conf.template index 8307001..cbd58b7 100644 --- a/src/resources/nginx.conf.template +++ b/src/resources/nginx.conf.template @@ -143,4 +143,4 @@ http { } } } -} \ No newline at end of file +} diff --git a/src/templates/configmap.yaml b/src/templates/configmap.yaml index 2232311..1a3dba4 100644 --- a/src/templates/configmap.yaml +++ b/src/templates/configmap.yaml @@ -36,4 +36,4 @@ kind: ConfigMap metadata: name: {{ include "docker-gcp-private-mirror.fullname" . }}-cloud-sdk-env data: - TOKEN_FILE_PATH: {{ .Values.nginx.tokenFilePath | quote }} \ No newline at end of file + TOKEN_FILE_PATH: {{ .Values.nginx.tokenFilePath | quote }} diff --git a/tests/fake-registry/Dockerfile b/tests/fake-registry/Dockerfile index 041bf83..b6ef056 100644 --- a/tests/fake-registry/Dockerfile +++ b/tests/fake-registry/Dockerfile @@ -9,4 +9,4 @@ RUN npm install -g ts-node typescript COPY . . -CMD ["npm", "run", "start"] \ No newline at end of file +CMD ["npm", "run", "start"] diff --git a/tests/helm-chart/.gitignore b/tests/helm-chart/.gitignore index 1654d0d..f791801 100644 --- a/tests/helm-chart/.gitignore +++ b/tests/helm-chart/.gitignore @@ -1,2 +1,2 @@ charts/ -Chart.lock \ No newline at end of file +Chart.lock diff --git a/tests/helm-chart/Chart.yaml b/tests/helm-chart/Chart.yaml index e1bc20a..e316948 100644 --- a/tests/helm-chart/Chart.yaml +++ b/tests/helm-chart/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - name: docker-gcp-private-mirror alias: private-gcp-mirror version: 0.0.1 - repository: file://../../src \ No newline at end of file + repository: file://../../src diff --git a/tests/helm-chart/values.yaml b/tests/helm-chart/values.yaml index 9b82470..48c1bae 100644 --- a/tests/helm-chart/values.yaml +++ b/tests/helm-chart/values.yaml @@ -15,7 +15,7 @@ private-gcp-mirror: readinessProbe: initialDelaySeconds: 30 proxy: - upstreamHost: 'fake-registry' + upstreamHost: "fake-registry" upstreamProtocol: http - rewritePath: 'test-project/test-registry-name' - maxAuthRetryAttempts: 3 \ No newline at end of file + rewritePath: "test-project/test-registry-name" + maxAuthRetryAttempts: 3 diff --git a/tests/tofu/.gitignore b/tests/tofu/.gitignore index 962a54d..ac2da8f 100644 --- a/tests/tofu/.gitignore +++ b/tests/tofu/.gitignore @@ -1,4 +1,4 @@ .terraform/ terraform.tfvars terraform.tfstate -terraform.tfstate.backup \ No newline at end of file +terraform.tfstate.backup diff --git a/tests/tofu/main.tf b/tests/tofu/main.tf index 0f56d3d..7c596b9 100644 --- a/tests/tofu/main.tf +++ b/tests/tofu/main.tf @@ -18,10 +18,10 @@ resource "google_service_account" "this" { } resource "google_artifact_registry_repository_iam_binding" "this" { - repository = google_artifact_registry_repository.this.name - location = google_artifact_registry_repository.this.location - role = "roles/artifactregistry.reader" - members = ["serviceAccount:${google_service_account.this.email}"] + repository = google_artifact_registry_repository.this.name + location = google_artifact_registry_repository.this.location + role = "roles/artifactregistry.reader" + members = ["serviceAccount:${google_service_account.this.email}"] } resource "google_service_account_iam_binding" "this" { @@ -37,8 +37,8 @@ resource "kubernetes_namespace" "this" { } resource "helm_release" "this" { - name = var.mirror.name - namespace = var.mirror.name + name = var.mirror.name + namespace = var.mirror.name chart = "docker-gcp-private-mirror" repository = "oci://registry-1.docker.io/sguesdon" @@ -64,5 +64,5 @@ resource "helm_release" "this" { value = google_service_account.this.email } - depends_on = [ kubernetes_namespace.this ] + depends_on = [kubernetes_namespace.this] } diff --git a/tests/tofu/providers.tf b/tests/tofu/providers.tf index c0d2fa2..61379a6 100644 --- a/tests/tofu/providers.tf +++ b/tests/tofu/providers.tf @@ -15,4 +15,4 @@ provider "helm" { token = data.google_client_config.this.access_token cluster_ca_certificate = base64decode(data.google_container_cluster.this.master_auth.0.cluster_ca_certificate) } -} \ No newline at end of file +} diff --git a/tests/tofu/variables.tf b/tests/tofu/variables.tf index f8a14ec..3840e49 100644 --- a/tests/tofu/variables.tf +++ b/tests/tofu/variables.tf @@ -1,7 +1,7 @@ variable "google" { type = object({ project_id = string - region = optional(string, "eu-west9") + region = optional(string, "eu-west9") }) description = "google provider configuration" } @@ -15,9 +15,9 @@ variable "mirror" { variable "gke" { type = object({ - location = string + location = string cluster_name = string - namespace = optional(string, "docker-mirror") + namespace = optional(string, "docker-mirror") }) description = "target cluster configuration" -} \ No newline at end of file +} From 929cadabbe65ecf0def902f9b531b1dd3de41996 Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Wed, 15 Jan 2025 11:00:46 +0100 Subject: [PATCH 35/36] clean: clean tfvar example --- tests/tofu/terraform.tfvars.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tofu/terraform.tfvars.example b/tests/tofu/terraform.tfvars.example index 1430c9d..ff861f6 100644 --- a/tests/tofu/terraform.tfvars.example +++ b/tests/tofu/terraform.tfvars.example @@ -4,5 +4,5 @@ google = { gke = { cluster_name = "xxxx" - location = "yyyy" -} \ No newline at end of file + location = "yyyy" +} From 24b97e1861ba912d7dd5ea223503f55e97455eba Mon Sep 17 00:00:00 2001 From: Sebastian Guesdon Date: Wed, 15 Jan 2025 11:02:31 +0100 Subject: [PATCH 36/36] feat: add test to main push --- .github/workflows/tests.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7879689..021082a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -3,6 +3,9 @@ on: pull_request: branches: - main + push: + branches: + - main jobs: integration-tests: name: Integration tests @@ -12,7 +15,7 @@ jobs: - name: Install devbox uses: jetify-com/devbox-install-action@v0.11.0 - + - name: Start minikube uses: medyagh/setup-minikube@latest @@ -21,7 +24,7 @@ jobs: MINIKUBE: true run: | devbox run test - + lint: name: Lint runs-on: ubuntu-latest @@ -30,7 +33,7 @@ jobs: - name: Install devbox uses: jetify-com/devbox-install-action@v0.11.0 - + - name: Run helm lint run: | devbox run lint