├── .dockerignore ├── .env ├── .github ├── actions │ ├── mirror │ │ └── action.yml │ └── setup-terraform │ │ └── action.yml └── workflows │ ├── jobs.yml │ ├── local-offline-installer │ └── main.tf │ ├── local-online-installer │ └── main.tf │ ├── remote-offline-installer │ └── main.tf │ └── remote-online-installer │ └── main.tf ├── .gitignore ├── Dockerfile ├── Dockerfile.online ├── LICENSE ├── Makefile ├── README.md ├── ansible-runner ├── context │ ├── Containerfile │ ├── _build │ │ └── requirements.yml │ └── app │ │ ├── env │ │ └── settings │ │ └── project │ │ ├── ansible.cfg │ │ ├── install_mirror_appliance.yml │ │ ├── roles │ │ └── mirror_appliance │ │ │ ├── defaults │ │ │ └── main.yml │ │ │ ├── meta │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ ├── autodetect-image-archive.yaml │ │ │ ├── autodetect-sqlite-archive.yaml │ │ │ ├── cleanup-postgres.yaml │ │ │ ├── create-init-user.yaml │ │ │ ├── expand-vars.yaml │ │ │ ├── install-deps.yaml │ │ │ ├── install-pod-service.yaml │ │ │ ├── install-quay-service.yaml │ │ │ ├── install-redis-service.yaml │ │ │ ├── main.yaml │ │ │ ├── migrate.yaml │ │ │ ├── secret-vars.yaml │ │ │ ├── set-selinux-rules.yaml │ │ │ ├── uninstall.yaml │ │ │ ├── upgrade-config-vars.yaml │ │ │ ├── upgrade-pod-service.yaml │ │ │ ├── upgrade-quay-service.yaml │ │ │ ├── upgrade-redis-service.yaml │ │ │ ├── upgrade.yaml │ │ │ └── wait-for-quay.yaml │ │ │ └── templates │ │ │ ├── config.yaml.j2 │ │ │ ├── pg_dump.sh │ │ │ ├── pg_to_sqlite.sh │ │ │ ├── pod.service.j2 │ │ │ ├── quay.service.j2 │ │ │ ├── redis.service.j2 │ │ │ └── req.j2 │ │ ├── uninstall_mirror_appliance.yml │ │ └── upgrade_mirror_appliance.yml ├── execution-environment.yml ├── requirements.yml └── setup.cfg ├── cmd ├── install.go ├── root.go ├── uninstall.go ├── upgrade.go └── utils.go ├── go.mod ├── go.sum ├── main.go └── test ├── Makefile └── Vagrantfile /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | Dockerfile 3 | Dockerfile.online 4 | Makefile 5 | README.md 6 | test 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | EE_IMAGE=quay.io/quay/mirror-registry-ee:latest 2 | EE_BASE_IMAGE=registry.redhat.io/ansible-automation-platform-24/ee-minimal-rhel8:1.0.0-683 3 | EE_BUILDER_IMAGE=registry.redhat.io/ansible-automation-platform-24/ansible-builder-rhel8:3.0.1-55 4 | QUAY_IMAGE=registry.redhat.io/quay/quay-rhel8:v3.12.8 5 | REDIS_IMAGE=registry.redhat.io/rhel8/redis-6:1-190 6 | PAUSE_IMAGE=registry.access.redhat.com/ubi8/pause:8.10-5 7 | SQLITE_IMAGE=quay.io/projectquay/sqlite-cli:latest 8 | -------------------------------------------------------------------------------- /.github/actions/mirror/action.yml: -------------------------------------------------------------------------------- 1 | name: "Mirror to Quay" 2 | description: "Mirrors Openshift Catalog to Quay" 3 | inputs: 4 | quay-hostname: # path 5 | description: "The hostname of Quay to mirror to" 6 | required: true 7 | pull-secret: 8 | description: "The pull secret passed to OC" 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Create pull secret 14 | run: | 15 | echo "$PULL_SECRET" > /tmp/pull-secret.json; chmod 777 /tmp/pull-secret.json 16 | echo "{\"auths\": {\"${{ inputs.quay-hostname }}\": {\"auth\": \"$(echo -n init:password | base64 -w0)\", \"email\":\"init@quay.io\"}}}" > /tmp/mirror-secret.json; chmod 777 /tmp/mirror-secret.json 17 | jq -s '.[0] * .[1]' /tmp/pull-secret.json /tmp/mirror-secret.json > /tmp/merged-secret.json; chmod 777 /tmp/merged-secret.json; cat /tmp/merged-secret.json 18 | shell: bash 19 | env: 20 | PULL_SECRET: ${{ inputs.pull-secret }} 21 | 22 | - name: Mirror OCP Images 23 | run: | 24 | oc adm release mirror -a ${LOCAL_SECRET_JSON} \ 25 | --from=quay.io/${PRODUCT_REPO}/${RELEASE_NAME}:${OCP_RELEASE}-${ARCHITECTURE} \ 26 | --to=${{ inputs.quay-hostname }}/${LOCAL_REPOSITORY} \ 27 | --to-release-image=${{ inputs.quay-hostname }}/${LOCAL_REPOSITORY}:${OCP_RELEASE}-${ARCHITECTURE} \ 28 | --insecure 29 | shell: bash 30 | env: 31 | OCP_RELEASE: 4.5.4 32 | LOCAL_REPOSITORY: "ocp-install/openshift4" 33 | PRODUCT_REPO: "openshift-release-dev" 34 | LOCAL_SECRET_JSON: "/tmp/merged-secret.json" 35 | RELEASE_NAME: "ocp-release" 36 | ARCHITECTURE: "x86_64" 37 | -------------------------------------------------------------------------------- /.github/actions/setup-terraform/action.yml: -------------------------------------------------------------------------------- 1 | name: "Start Terraform VM" 2 | description: "Starts a terraform VM in a terraform context" 3 | inputs: 4 | terraform-context: # path 5 | description: "The folder which holds the terraform context for this VM" 6 | required: true 7 | default: "~" 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Terraform Format 12 | id: fmt 13 | run: terraform fmt -check 14 | shell: bash 15 | working-directory: ${{ inputs.terraform-context }} 16 | 17 | - name: Terraform Init 18 | id: init 19 | run: terraform init 20 | shell: bash 21 | working-directory: ${{ inputs.terraform-context }} 22 | 23 | - name: Terraform Plan 24 | id: plan 25 | run: terraform plan 26 | shell: bash 27 | working-directory: ${{ inputs.terraform-context }} 28 | 29 | - name: Terraform Apply 30 | run: terraform apply --auto-approve 31 | shell: bash 32 | working-directory: ${{ inputs.terraform-context }} 33 | 34 | - name: Get IP Address 35 | run: | 36 | export IP=$(terraform output ip) 37 | echo "VM Started on ${IP}" 38 | shell: bash 39 | working-directory: ${{ inputs.terraform-context }} 40 | 41 | - name: Add hostname to /etc/hosts 42 | run: echo "$(terraform output --raw ip) quay" | sudo tee -a /etc/hosts; sudo cat /etc/hosts 43 | shell: bash 44 | working-directory: ${{ inputs.terraform-context }} 45 | -------------------------------------------------------------------------------- /.github/workflows/jobs.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | pull_request_target: 8 | types: [labeled] 9 | branches: 10 | - main 11 | - mirror-registry-* 12 | # Allows us to run release manually from the Actions tab 13 | workflow_dispatch: 14 | inputs: 15 | version: 16 | description: 'Version to release (tag name)' 17 | required: true 18 | 19 | concurrency: 20 | group: limit-to-one 21 | 22 | jobs: 23 | build-install-zip: 24 | name: "Build Installer" 25 | runs-on: ubuntu-latest 26 | if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ok-to-test') || github.event.inputs.version 27 | strategy: 28 | matrix: 29 | installer-type: ["online", "offline"] 30 | steps: 31 | # Checkout source branch for testing 32 | - name: Checkout PR 33 | uses: actions/checkout@v4 34 | if: contains(github.event.pull_request.labels.*.name, 'ok-to-test') 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha }} 37 | 38 | # Checkout target branch for release build 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | ref: refs/tags/${{ github.event.inputs.version }} 43 | if: github.event.inputs.version 44 | 45 | - name: Set version from tag/branch 46 | run: | 47 | echo "RELEASE_VERSION=$GITHUB_REF_NAME" >>"$GITHUB_ENV" 48 | if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ok-to-test') 49 | 50 | - name: Set version from input 51 | run: | 52 | echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >>"$GITHUB_ENV" 53 | if: github.event.inputs.version 54 | 55 | - name: Set up Go 1.x 56 | uses: actions/setup-go@v4 57 | with: 58 | go-version: ^1.21 59 | id: go 60 | 61 | - name: Get dependencies 62 | run: | 63 | go install -v ./... 64 | if [ -f Gopkg.toml ]; then 65 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 66 | dep ensure 67 | fi 68 | 69 | - name: Log into docker for registry.redhat.io 70 | uses: docker/login-action@v2 71 | with: 72 | registry: registry.redhat.io 73 | username: ${{ secrets.REGISTRY_USERNAME }} 74 | password: ${{ secrets.REGISTRY_PASSWORD }} 75 | 76 | - name: Build tarfile 77 | run: CLIENT=docker make build-${{ matrix.installer-type }}-zip 78 | 79 | - name: Upload tarfile 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: mirror-registry-${{ matrix.installer-type }}-installer 83 | path: mirror-registry.tar.gz 84 | retention-days: 1 85 | 86 | test-remote-install: 87 | name: "Remote Install" 88 | needs: ["build-install-zip"] 89 | runs-on: ubuntu-latest 90 | if: contains(github.event.pull_request.labels.*.name, 'ok-to-test') # Skip on release 91 | strategy: 92 | fail-fast: false 93 | matrix: 94 | installer-type: ["online", "offline"] 95 | env: 96 | GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} 97 | TF_VAR_SSH_PUBLIC_KEY: ${{ secrets.TF_VAR_SSH_PUBLIC_KEY }} 98 | steps: 99 | - name: Checkout 100 | uses: actions/checkout@v3 101 | 102 | - name: Install oc 103 | uses: redhat-actions/oc-installer@v1 104 | with: 105 | oc_version: "4.6" 106 | 107 | - name: Setup Terraform 108 | uses: hashicorp/setup-terraform@v1 109 | with: 110 | # terraform_version: 0.13.0: 111 | cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} 112 | terraform_wrapper: false 113 | 114 | - name: Install SSH Key 115 | uses: webfactory/ssh-agent@v0.5.2 116 | with: 117 | ssh-private-key: ${{ secrets.TF_VAR_SSH_PRIVATE_KEY }} 118 | 119 | - name: Write SSH Key id_rsa 120 | run: | 121 | echo "$SSH_KEY" > /home/runner/.ssh/id_rsa 122 | chmod 600 ~/.ssh/id_rsa 123 | env: 124 | SSH_KEY: ${{ secrets.TF_VAR_SSH_PRIVATE_KEY }} 125 | 126 | - name: Write SSH Key staging.key 127 | run: | 128 | mkdir -p ~/.ssh/ 129 | echo "$SSH_KEY" > ~/.ssh/staging.key 130 | chmod 600 ~/.ssh/staging.key 131 | cat >>~/.ssh/config < /home/runner/.ssh/id_rsa 262 | chmod 600 ~/.ssh/id_rsa 263 | env: 264 | SSH_KEY: ${{ secrets.TF_VAR_SSH_PRIVATE_KEY }} 265 | 266 | - name: Write SSH Key staging.key 267 | run: | 268 | mkdir -p ~/.ssh/ 269 | echo "$SSH_KEY" > ~/.ssh/staging.key 270 | chmod 600 ~/.ssh/staging.key 271 | cat >>~/.ssh/config <:8443. 33 | --quayRoot -r The folder where quay persistent quay config data is saved. This defaults to $HOME/quay-install. 34 | --quayStorage The folder where quay persistent storage data is saved. This defaults to a Podman named volume 'quay-storage'. Root is required to uninstall. 35 | --sqliteStorage The folder where quay sqlite db data is saved. This defaults to a Podman named volume 'sqlite-storage'. Root is required to uninstall. 36 | --ssh-key -k The path of your ssh identity key. This defaults to ~/.ssh/quay_installer. 37 | --sslCert The path to the SSL certificate Quay should use. 38 | --sslCheckSkip Whether or not to check the certificate hostname against the SERVER_HOSTNAME in config.yaml. 39 | --sslKey The path to the SSL key. 40 | --targetHostname -H The hostname of the target you wish to install Quay to. This defaults to $HOST. 41 | --targetUsername -u The user on the target host which will be used for SSH. This defaults to $USER 42 | --verbose -v Show debug logs and ansible playbook outputs 43 | --no-color -c Force disabling colored output 44 | ``` 45 | 46 | **Note**: Installing mirror registry will enable `systemd` user services to run without the target user session being active. 47 | 48 | **Note**: You may need to modify the value for `--quayHostname` in case the public DNS name of your system is different from its local hostname. 49 | 50 | **Note** If you do not supply `--sslCert` and `--sslKey`, these will be autogenerated and made available on that target host under the `{quayRoot}/quay-rootCA` directory. 51 | 52 | ### Installing on a Remote Host 53 | 54 | You can provide your ssh private key to the installer CLI with the `--ssh-key` flag. 55 | 56 | To install Quay on a remote host, run the following command: 57 | 58 | ```console 59 | $ ./mirror-registry install -v --targetHostname some.remote.host.com --targetUsername someuser --quayRoot /home/someuser/quay-install -k ~/.ssh/my_ssh_key --quayHostname some.remote.host.com 60 | ``` 61 | 62 | **Note**: `--quayRoot` is currently required for remote install since the default installation directory is based on the users home directory 63 | 64 | Behind the scenes, Ansible is using `ssh -i ~/.ssh/my_ssh_key someuser@some.remote.host.com` as the target to run its playbooks. 65 | 66 | ### What does the installer do? 67 | 68 | This command will make the following changes to your machine 69 | 70 | - Generate trusted SSH keys, if not supplied, in case the deployment target is the local host (required since the installer is ansible-based) 71 | - Pulls Quay and Redis images from `registry.redhat.io` (if using online installer) 72 | - Sets up systemd files on host machine to ensure that container runtimes are persistent 73 | - Creates the folder defined by `--quayRoot` (default: `$HOME/quay-install`) contains install files, local storage, and config bundle. 74 | - Installs Quay and creates an initial user called `init` with an auto-generated password 75 | - Access credentials are printed at the end of the install routine 76 | 77 | ## Access Quay 78 | 79 | Once installed, the Quay console will be accessible at `https://:8443`. **Refer to the output of the install process to retrieve user name and password.** 80 | 81 | You can then log into the registry using the provided credentials, for example: 82 | 83 | ```console 84 | $ podman login -u init -p --tls-verify=false quay:8443 85 | ``` 86 | 87 | After logging in, you can run commands such as: 88 | 89 | ```console 90 | $ podman pull docker.io/library/busybox:latest 91 | $ podman tag docker.io/library/busybox:latest quay:8443/init/busybox:latest 92 | $ podman push quay:8443/init/busybox:latest --tls-verify=false 93 | $ podman pull quay:8443/init/busybox:latest --tls-verify=false 94 | ``` 95 | 96 | Prior to pushing quay:8443/init/busybox, you must create the repository "busybox" in the Quay console. In future versions of mirror registry this will be created automatically. 97 | 98 | ## Upgrade 99 | To upgrade Quay from localhost, run the following command: 100 | 101 | ```console 102 | $ sudo ./mirror-registry upgrade -v 103 | ``` 104 | 105 | To upgrade Quay from a remote host, run the following command: 106 | 107 | ```console 108 | $ ./mirror-registry upgrade -v --targetHostname some.remote.host.com --targetUsername someuser -k ~/.ssh/my_ssh_key 109 | ``` 110 | 111 | **Note**: If Quay has been installed with `--quayHostname` or `--quayRoot` the same options need to be specified at upgrade. The upgrade process does not currently detect previous installations or configurations. 112 | 113 | ## Uninstall 114 | To uninstall Quay from localhost, run the following command: 115 | 116 | ```console 117 | $ sudo ./mirror-registry uninstall -v 118 | ``` 119 | 120 | To uninstall Quay from a remote host, run the following command: 121 | 122 | ```console 123 | $ ./mirror-registry uninstall -v --targetHostname some.remote.host.com --targetUsername someuser -k ~/.ssh/my_ssh_key 124 | ``` 125 | 126 | **Note**: If Quay has been installed with `--quayRoot` the same option needs to be specified at uninstall. 127 | 128 | ## Local DNS resolution 129 | 130 | In case the target host does not have a resolvable DNS record, you can rely on the default host name called `quay` and add the following line to your host machine's `/etc/hosts` file: 131 | 132 | ``` 133 | quay 134 | ``` 135 | 136 | ## Generate SSH Keys 137 | 138 | In order to run the installation playbooks, you must have password-less SSH access in place. Local installations will automatically generate the SSH keys for you. 139 | 140 | **Note** Passwordless ssh from `root` account is blocked by default on OpenSSH. 141 | 142 | To generate your own SSH keys to install on a remote host, run the following commands. 143 | 144 | ```console 145 | $ ssh-keygen 146 | $ ssh-add 147 | $ ssh-copy-id 148 | ``` 149 | ## Compile your own installer 150 | 151 | To compile the `mirror-registry.tar.gz` for distribution you need only `podman` and `make` installed. 152 | 153 | **NOTE:** The build process pulls images from registry.redhat.io, you may need to run `sudo podman login registry.redhat.io` before starting the build. 154 | 155 | You can build the installer running the following command: 156 | 157 | ```console 158 | $ git clone https://github.com/quay/mirror-registry.git 159 | $ cd mirror-registry 160 | $ make build-online-zip # OR make build-offline-zip 161 | ``` 162 | 163 | This will generate a `mirror-registry.tar.gz` which contains the `mirror-registry` binary, the `image-archive.tar` and the `execution-environment.tar` (if using offline installer). These archives contain all images required to set up Quay. 164 | 165 | Once generated, you may untar this file on your desired host machine for installation. You may use the following command: 166 | 167 | ```console 168 | mkdir mirror-registry 169 | tar -xzvf mirror-registry.tar.gz -C mirror-registry 170 | ``` 171 | 172 | **NOTE** This command may take some time to complete depending on host resources. 173 | -------------------------------------------------------------------------------- /ansible-runner/context/Containerfile: -------------------------------------------------------------------------------- 1 | ARG EE_BASE_IMAGE=registry.redhat.io/ansible-automation-platform-25/ee-minimal-rhel8:1.0.0-842 2 | ARG EE_BUILDER_IMAGE=registry.redhat.io/ansible-automation-platform-25/ansible-builder-rhel8:3.1.0-225 3 | 4 | FROM $EE_BASE_IMAGE as galaxy 5 | ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS= 6 | USER root 7 | 8 | ADD _build /build 9 | WORKDIR /build 10 | 11 | RUN ansible-galaxy role install -r requirements.yml --roles-path /usr/share/ansible/roles 12 | RUN ansible-galaxy collection install $ANSIBLE_GALAXY_CLI_COLLECTION_OPTS -r requirements.yml --collections-path /usr/share/ansible/collections 13 | 14 | FROM $EE_BUILDER_IMAGE as builder 15 | 16 | COPY --from=galaxy /usr/share/ansible /usr/share/ansible 17 | 18 | ADD _build/requirements.txt requirements.txt 19 | RUN ansible-builder introspect --sanitize --user-pip=requirements.txt --write-bindep=/tmp/src/bindep.txt --write-pip=/tmp/src/requirements.txt 20 | RUN assemble 21 | 22 | FROM $EE_BASE_IMAGE 23 | USER root 24 | 25 | COPY --from=galaxy /usr/share/ansible /usr/share/ansible 26 | 27 | COPY --from=builder /output/ /output/ 28 | RUN /output/install-from-bindep && rm -rf /output/wheels 29 | COPY app /runner 30 | -------------------------------------------------------------------------------- /ansible-runner/context/_build/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - ansible.posix 4 | - containers.podman 5 | - community.general 6 | -------------------------------------------------------------------------------- /ansible-runner/context/app/env/settings: -------------------------------------------------------------------------------- 1 | suppress_ansible_output: False -------------------------------------------------------------------------------- /ansible-runner/context/app/project/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | interpreter_python=auto_silent 3 | deprecation_warnings=False 4 | host_key_checking = False -------------------------------------------------------------------------------- /ansible-runner/context/app/project/install_mirror_appliance.yml: -------------------------------------------------------------------------------- 1 | - name: "Install Mirror Appliance" 2 | gather_facts: yes 3 | hosts: all 4 | tags: 5 | - quay 6 | roles: 7 | - mirror_appliance 8 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | systemd_unit_dir: "{{ '/etc/systemd/system' if ansible_user_uid == 0 else '$HOME/.config/systemd/user' }}" 3 | systemd_scope: "{{ 'system' if ansible_user_uid == 0 else 'user' }}" 4 | auto_approve: "false" 5 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | allow_duplicates: false 3 | 4 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/autodetect-image-archive.yaml: -------------------------------------------------------------------------------- 1 | - name: Checking for Image Archive 2 | local_action: stat path=/runner/image-archive.tar 3 | register: p 4 | 5 | - name: Create install directory for image-archive.tar dest 6 | ansible.builtin.file: 7 | path: "{{ quay_root }}" 8 | state: directory 9 | recurse: yes 10 | when: p.stat.exists 11 | 12 | - name: Copy Images if /runner/image-archive.tar exists 13 | copy: 14 | src: /runner/image-archive.tar 15 | dest: "{{ quay_root }}/image-archive.tar" 16 | when: p.stat.exists and local_install == "false" 17 | 18 | - name: Unpack Images if /runner/image-archive.tar exists 19 | command: "tar -xvf {{ quay_root }}/image-archive.tar -C {{ quay_root }}/" 20 | when: p.stat.exists and local_install == "false" 21 | 22 | - name: Loading Pause if pause.tar exists 23 | shell: 24 | cmd: podman image import --change 'ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' --change 'ENV container=oci' --change 'ENTRYPOINT=["sleep"]' --change 'CMD ["infinity"]' - {{ pause_image }} < {{ quay_root }}/pause.tar 25 | when: p.stat.exists and local_install == "false" 26 | 27 | - name: Loading Redis if redis.tar exists 28 | shell: 29 | cmd: podman image import --change 'ENV PATH=/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' --change 'ENV container=oci' --change 'ENV STI_SCRIPTS_URL=image:///usr/libexec/s2i' --change 'ENV STI_SCRIPTS_PATH=/usr/libexec/s2i' --change 'ENV APP_ROOT=/opt/app-root' --change 'ENV HOME=/var/lib/redis' --change 'ENV PLATFORM=el8' --change 'ENV REDIS_VERSION=6' --change 'ENV CONTAINER_SCRIPTS_PATH=/usr/share/container-scripts/redis' --change 'ENV REDIS_PREFIX=/usr' --change 'ENV REDIS_CONF=/etc/redis.conf' --change 'ENTRYPOINT=["container-entrypoint"]' --change 'USER=1001' --change 'WORKDIR=/opt/app-root/src' --change 'EXPOSE=6379' --change 'VOLUME=/var/lib/redis/data' --change 'CMD ["run-redis"]' - {{ redis_image }} < {{ quay_root }}/redis.tar 30 | when: p.stat.exists and local_install == "false" 31 | 32 | - name: Loading Quay if quay.tar exists 33 | shell: 34 | cmd: podman image import --change 'ENV container=oci' --change 'ENV PATH=/app/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' --change 'ENV PYTHONUNBUFFERED=1' --change 'ENV PYTHONIOENCODING=UTF-8' --change 'ENV LC_ALL=C.UTF-8' --change 'ENV LANG=C.UTF-8' --change 'ENV QUAYDIR=/quay-registry' --change 'ENV QUAYCONF=/quay-registry/conf' --change 'ENV QUAYRUN=/quay-registry/conf' --change 'ENV QUAYPATH=/quay-registry' --change 'ENV PYTHONUSERBASE=/app' --change 'ENV PYTHONPATH=/quay-registry' --change 'ENV TZ=UTC' --change 'ENV RED_HAT_QUAY=true' --change 'ENTRYPOINT=["dumb-init","--","/quay-registry/quay-entrypoint.sh"]' --change 'WORKDIR=/quay-registry' --change 'EXPOSE=7443' --change 'EXPOSE=8080' --change 'EXPOSE=8443' --change 'VOLUME=/conf/stack' --change 'VOLUME=/datastorage' --change 'VOLUME=/sqlite' --change 'VOLUME=/tmp' --change 'VOLUME=/var/log' --change 'USER=1001' --change 'CMD ["registry"]' - {{ quay_image }} < {{ quay_root }}/quay.tar 35 | when: p.stat.exists and local_install == "false" 36 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/autodetect-sqlite-archive.yaml: -------------------------------------------------------------------------------- 1 | - name: Checking for sqlite3.tar archive 2 | local_action: stat path=/runner/sqlite3.tar 3 | register: s 4 | 5 | - name: Copy Image if /runner/sqlite3.tar exists 6 | copy: 7 | src: /runner/sqlite3.tar 8 | dest: "{{ quay_root }}/sqlite3.tar" 9 | when: s.stat.exists and local_install == "false" 10 | 11 | - name: Load sqlite image if sqlite3.tar exists 12 | shell: 13 | cmd: podman image import --change 'ENV PATH=/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' --change 'ENV container=oci' --change 'ENTRYPOINT=["/usr/bin/sqlite3"]' - {{ sqlite_image }} < {{ quay_root }}/sqlite3.tar 14 | when: s.stat.exists and local_install == "false" 15 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/cleanup-postgres.yaml: -------------------------------------------------------------------------------- 1 | - name: Stop Postgres service 2 | systemd: 3 | name: quay-postgres.service 4 | enabled: no 5 | daemon_reload: yes 6 | state: stopped 7 | force: yes 8 | scope: "{{ systemd_scope }}" 9 | 10 | - name: Delete Postgres Storage named volume 11 | containers.podman.podman_volume: 12 | state: absent 13 | name: pg-storage 14 | when: auto_approve|bool == true and pg_storage == "pg-storage" 15 | 16 | - name: Delete Postgres Password Secret 17 | containers.podman.podman_secret: 18 | state: absent 19 | name: pgdb_pass 20 | 21 | - name: Delete necessary directory for Postgres persistent data 22 | ansible.builtin.file: 23 | path: "{{ pg_storage }}" 24 | state: absent 25 | become: yes 26 | when: auto_approve|bool == true and pg_storage.startswith('/') 27 | 28 | - name: Cleanup quay-postgres systemd unit file 29 | file: 30 | state: absent 31 | path: "{{ systemd_unit_dir }}/quay-postgres.service" 32 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/create-init-user.yaml: -------------------------------------------------------------------------------- 1 | - name: Creating init user at endpoint https://{{ quay_hostname }}/api/v1/user/initialize 2 | uri: 3 | url: "https://{{ quay_hostname}}/api/v1/user/initialize" 4 | method: POST 5 | validate_certs: no 6 | return_content: yes 7 | body_format: json 8 | body: '{ "username": "{{ init_user }}", "password": "{{ init_password }}", "email": "init@quay.io", "access_token": "true" }' 9 | register: result 10 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/expand-vars.yaml: -------------------------------------------------------------------------------- 1 | - name: Expand quay_root 2 | shell: 'echo {{ quay_root }}' 3 | register: expanded_quay_root_output 4 | 5 | - name: Expand quay_storage 6 | shell: 'echo {{ quay_storage }}' 7 | register: expanded_quay_storage_output 8 | 9 | - name: Expand sqlite_storage 10 | shell: 'echo {{ sqlite_storage }}' 11 | register: expanded_sqlite_storage_output 12 | 13 | - name: Set expanded variables 14 | set_fact: 15 | expanded_sqlite_storage: "{{ expanded_sqlite_storage_output.stdout }}" 16 | expanded_quay_root: "{{ expanded_quay_root_output.stdout }}" 17 | expanded_quay_storage: "{{ expanded_quay_storage_output.stdout }}" 18 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/install-deps.yaml: -------------------------------------------------------------------------------- 1 | - name: Create user service directory 2 | file: 3 | path: "{{ systemd_unit_dir }}" 4 | state: directory 5 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/install-pod-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Copy Quay Pod systemd service file 2 | template: 3 | src: ../templates/pod.service.j2 4 | dest: "{{ systemd_unit_dir }}/quay-pod.service" 5 | 6 | - name: Check if pod pause image is loaded 7 | command: podman inspect --type=image {{ pause_image }} 8 | register: r 9 | ignore_errors: yes 10 | 11 | - name: Pull Infra image 12 | containers.podman.podman_image: 13 | name: "{{ pause_image }}" 14 | when: r.rc != 0 15 | retries: 5 16 | delay: 5 17 | 18 | - name: Start Quay Pod service 19 | systemd: 20 | name: quay-pod.service 21 | enabled: yes 22 | daemon_reload: yes 23 | state: restarted 24 | scope: "{{ systemd_scope }}" 25 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/install-quay-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Create necessary directory for Quay local storage 2 | ansible.builtin.file: 3 | mode: 0775 4 | path: "{{ quay_storage }}" 5 | state: directory 6 | recurse: yes 7 | when: "quay_storage.startswith('/')" 8 | 9 | - name: Set permissions on local storage directory 10 | ansible.posix.acl: 11 | path: "{{ quay_storage }}" 12 | entity: 1001 13 | etype: user 14 | permissions: wx 15 | state: present 16 | when: "quay_storage.startswith('/')" 17 | 18 | - name: Create necessary directory for sqlite storage 19 | ansible.builtin.file: 20 | mode: 0775 21 | path: "{{ sqlite_storage }}" 22 | state: directory 23 | recurse: yes 24 | when: "sqlite_storage.startswith('/')" 25 | 26 | - name: Set permissions on sqlite storage directory 27 | ansible.posix.acl: 28 | path: "{{ sqlite_storage }}" 29 | entity: 1001 30 | etype: user 31 | permissions: wx 32 | state: present 33 | when: "sqlite_storage.startswith('/')" 34 | 35 | - name: Create necessary directory for Quay config bundle 36 | ansible.builtin.file: 37 | path: "{{ quay_root }}/quay-config" 38 | mode: 0750 39 | state: directory 40 | recurse: yes 41 | 42 | - name: Copy Quay config.yaml file 43 | template: 44 | src: ../templates/config.yaml.j2 45 | dest: "{{ quay_root }}/quay-config/config.yaml" 46 | mode: 0750 47 | 48 | - name: Check if SSL Cert exists 49 | stat: 50 | path: /runner/certs/quay.cert 51 | delegate_to: localhost 52 | register: ssl_cert 53 | 54 | - name: Check if SSL Key exists 55 | stat: 56 | path: /runner/certs/quay.key 57 | delegate_to: localhost 58 | register: ssl_key 59 | 60 | - name: Create SSL Certs 61 | block: 62 | - name: Create necessary directory for Quay rootCA files 63 | ansible.builtin.file: 64 | path: "{{ quay_root }}/quay-rootCA" 65 | mode: 0750 66 | state: directory 67 | recurse: yes 68 | 69 | - name: Create OpenSSL Config 70 | template: 71 | src: ../templates/req.j2 72 | dest: "{{ quay_root }}/quay-config/openssl.cnf" 73 | 74 | - name: Create root CA key 75 | command: "openssl genrsa -out {{ quay_root }}/quay-rootCA/rootCA.key 2048" 76 | 77 | - name: Create root CA pem 78 | command: "openssl req -x509 -new -config {{ quay_root }}/quay-config/openssl.cnf -nodes -key {{ quay_root }}/quay-rootCA/rootCA.key -sha256 -days 1024 -out {{ quay_root }}/quay-rootCA/rootCA.pem -addext basicConstraints=critical,CA:TRUE,pathlen:1" 79 | 80 | - name: Create ssl key 81 | command: "openssl genrsa -out {{ quay_root }}/quay-config/ssl.key 2048" 82 | 83 | - name: Create CSR 84 | command: "openssl req -new -key {{ quay_root }}/quay-config/ssl.key -out {{ quay_root }}/quay-config/ssl.csr -subj \"/CN=quay-enterprise\" -config {{ quay_root }}/quay-config/openssl.cnf" 85 | 86 | - name: Create self-signed cert 87 | command: "openssl x509 -req -in {{ quay_root }}/quay-config/ssl.csr -CA {{ quay_root }}/quay-rootCA/rootCA.pem -CAkey {{ quay_root }}/quay-rootCA/rootCA.key -CAcreateserial -out {{ quay_root }}/quay-config/ssl.cert -days 356 -extensions v3_req -extfile {{ quay_root }}/quay-config/openssl.cnf" 88 | 89 | - name: Create chain cert 90 | ansible.builtin.shell: cat {{ quay_root }}/quay-config/ssl.cert {{ quay_root }}/quay-rootCA/rootCA.pem > {{ quay_root }}/quay-config/chain.cert 91 | 92 | - name: Replace ssl cert with chain cert 93 | command: mv --force {{ quay_root }}/quay-config/chain.cert {{ quay_root }}/quay-config/ssl.cert 94 | when: (ssl_cert.stat.exists == False) and (ssl_key.stat.exists == False) 95 | 96 | - name: Copy SSL Certs 97 | block: 98 | - name: Copy SSL certificate 99 | copy: 100 | src: /runner/certs/quay.cert 101 | dest: "{{ quay_root }}/quay-config/ssl.cert" 102 | 103 | - name: Copy SSL key 104 | copy: 105 | src: /runner/certs/quay.key 106 | dest: "{{ quay_root }}/quay-config/ssl.key" 107 | when: (ssl_cert.stat.exists == True) and (ssl_key.stat.exists == True) 108 | 109 | - name: Set certificate permissions 110 | block: 111 | - name: Set permissions for key 112 | ansible.builtin.file: 113 | path: "{{ quay_root }}/quay-config/ssl.key" 114 | mode: u=rw,g=r,o=r 115 | 116 | - name: Set permissions for cert 117 | ansible.builtin.file: 118 | path: "{{ quay_root }}/quay-config/ssl.cert" 119 | mode: u=rw,g=r,o=r 120 | 121 | - name: Copy Quay systemd service file 122 | template: 123 | src: ../templates/quay.service.j2 124 | dest: "{{ systemd_unit_dir }}/quay-app.service" 125 | 126 | - name: Check if Quay image is loaded 127 | command: podman inspect --type=image {{ quay_image }} 128 | register: q 129 | ignore_errors: yes 130 | 131 | - name: Pull Quay image 132 | containers.podman.podman_image: 133 | name: "{{ quay_image }}" 134 | when: q.rc != 0 135 | retries: 5 136 | delay: 5 137 | 138 | - name: Create Quay Storage named volume 139 | containers.podman.podman_volume: 140 | state: present 141 | name: "{{ quay_storage }}" 142 | when: "not quay_storage.startswith('/')" 143 | 144 | - name: Create Sqlite Storage named volume 145 | containers.podman.podman_volume: 146 | state: present 147 | name: "{{ sqlite_storage }}" 148 | when: "not sqlite_storage.startswith('/')" 149 | 150 | - name: Start Quay service 151 | systemd: 152 | name: quay-app.service 153 | enabled: yes 154 | daemon_reload: yes 155 | state: restarted 156 | scope: "{{ systemd_scope }}" 157 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/install-redis-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Copy Redis systemd service file 2 | template: 3 | src: ../templates/redis.service.j2 4 | dest: "{{ systemd_unit_dir }}/quay-redis.service" 5 | 6 | - name: Check if Redis image is loaded 7 | command: podman inspect --type=image {{ redis_image }} 8 | register: r 9 | ignore_errors: yes 10 | 11 | - name: Pull Redis image 12 | containers.podman.podman_image: 13 | name: "{{ redis_image }}" 14 | when: r.rc != 0 15 | retries: 5 16 | delay: 5 17 | 18 | - name: Create Redis Password Secret 19 | containers.podman.podman_secret: 20 | state: present 21 | name: redis_pass 22 | data: "{{ redis_password }}" 23 | skip_existing: true 24 | 25 | - name: Start Redis service 26 | systemd: 27 | name: quay-redis.service 28 | enabled: yes 29 | daemon_reload: yes 30 | state: restarted 31 | scope: "{{ systemd_scope }}" 32 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Expand variables 2 | include_tasks: expand-vars.yaml 3 | 4 | - name: Create secret vars 5 | include_tasks: secret-vars.yaml 6 | 7 | - name: Install Dependencies 8 | include_tasks: install-deps.yaml 9 | 10 | - name: Set SELinux Rules 11 | include_tasks: set-selinux-rules.yaml 12 | 13 | - name: Autodetect Image Archive 14 | include_tasks: autodetect-image-archive.yaml 15 | 16 | - name: Install Quay Pod Service 17 | include_tasks: install-pod-service.yaml 18 | 19 | - name: Install Redis Service 20 | include_tasks: install-redis-service.yaml 21 | 22 | - name: Install Quay Service 23 | include_tasks: install-quay-service.yaml 24 | 25 | - name: Wait for Quay 26 | include_tasks: wait-for-quay.yaml 27 | 28 | - name: Create init user 29 | include_tasks: create-init-user.yaml 30 | 31 | - name: Enable lingering for systemd user processes 32 | command: "loginctl enable-linger" 33 | when: ansible_user_uid != 0 34 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/migrate.yaml: -------------------------------------------------------------------------------- 1 | - name: Check if sqlite cli image is loaded 2 | command: podman inspect --type=image {{ sqlite_image }} 3 | register: sqlite_cli 4 | ignore_errors: yes 5 | 6 | - name: Fail if sqlite cli image is not found 7 | fail: 8 | msg: "The SQLite CLI image '{{ sqlite_image }}' is not loaded." 9 | when: sqlite_cli.rc != 0 10 | 11 | - name: Create Sqlite storage named volume 12 | containers.podman.podman_volume: 13 | state: present 14 | name: "{{ sqlite_storage }}" 15 | when: "not sqlite_storage.startswith('/')" 16 | 17 | - name: Create necessary directory for storing quay postgres db snapshot 18 | ansible.builtin.file: 19 | path: "{{ expanded_quay_root }}/quay-postgres-backup" 20 | mode: 0750 21 | state: directory 22 | recurse: yes 23 | 24 | - name: Verify sqlite3 cli binary is available as entrypoint command from the container 25 | command: > 26 | timeout 20 podman run --name sqlite-cli {{ sqlite_image }} --version 27 | register: sqlite_version 28 | ignore_errors: yes 29 | 30 | - name: Fail if sqlite cli binary is not found 31 | fail: 32 | msg: "sqlite cli is not available via the container, cannot proceed to migrate" 33 | when: sqlite_version.rc != 0 or sqlite_version.stdout.split(' ')[0] | regex_search('(\\d+\\.\\d+\\.\\d+)') is not defined 34 | 35 | - name: Remove the sqlite-cli container (cleanup) 36 | command: > 37 | podman rm -f sqlite-cli 38 | register: remove_sqlite_cli 39 | ignore_errors: yes 40 | changed_when: remove_sqlite_cli.rc == 0 41 | 42 | - name: Copy pg_dump.sh bash script to host machine 43 | template: 44 | src: ../templates/pg_dump.sh 45 | dest: "{{ expanded_quay_root }}/quay-postgres-backup/pg_dump.sh" 46 | mode: '0755' 47 | 48 | - name: Run pg_dump in parallel to fetch postgres data as a .sql file 49 | command: /bin/bash "{{ expanded_quay_root }}/quay-postgres-backup/pg_dump.sh" 50 | register: pg_dump_output 51 | 52 | - name: Copy the generated .sql file from postgres container to ansible host machine 53 | command: podman cp quay-postgres:/tmp/pg_data_dump.sql "{{ expanded_quay_root }}/quay-postgres-backup/" 54 | 55 | - name: Display pg_dump output 56 | debug: 57 | var: pg_dump_output.stdout 58 | 59 | - name: Copy postgres-to-sqlite bash script to host machine 60 | template: 61 | src: ../templates/pg_to_sqlite.sh 62 | dest: "{{ expanded_quay_root }}/quay-postgres-backup/pg_to_sqlite.sh" 63 | mode: '0755' 64 | 65 | - name: Convert PostgreSQL data-only dump to SQLite-compatible SQL format 66 | ansible.builtin.shell: | 67 | {{ expanded_quay_root }}/quay-postgres-backup/pg_to_sqlite.sh "{{ input_file }}" "{{ output_file }}" 2>&1 68 | args: 69 | executable: /bin/bash 70 | register: conversion_result 71 | failed_when: 72 | - conversion_result.rc != 0 73 | vars: 74 | input_file: "{{ expanded_quay_root }}/quay-postgres-backup/pg_data_dump.sql" 75 | output_file: "{{ expanded_quay_root }}/quay-postgres-backup/transformed_pgdata.sql" 76 | 77 | - name: Stop Quay service 78 | systemd: 79 | name: quay-app.service 80 | enabled: no 81 | daemon_reload: yes 82 | state: stopped 83 | force: yes 84 | scope: "{{ systemd_scope }}" 85 | 86 | - name: Update DB_URI in config.yaml to sqlite file 87 | replace: 88 | path: "{{ expanded_quay_root }}/quay-config/config.yaml" 89 | regexp: '^DB_URI: postgresql://.*$' 90 | replace: 'DB_URI: sqlite:////sqlite/quay_sqlite.db' 91 | register: db_uri_update 92 | 93 | - name: Ensure DB_URI was updated successfully 94 | assert: 95 | that: 96 | - db_uri_update.changed 97 | fail_msg: "Failed to update DB_URI in quay's config" 98 | success_msg: "DB_URI has been updated successfully" 99 | 100 | - name: Copy Quay systemd service file with migrate command 101 | template: 102 | src: ../templates/quay.service.j2 103 | dest: "{{ systemd_unit_dir }}/quay-migrate.service" 104 | vars: 105 | quay_cmd: "migrate head" 106 | 107 | # This starts quay with sqlite db and runs the alembic migration 108 | - name: Start Quay service 109 | systemd: 110 | name: quay-migrate.service 111 | enabled: yes 112 | daemon_reload: yes 113 | scope: "{{ systemd_scope }}" 114 | state: started 115 | register: quay_service 116 | 117 | - name: Add wait to ensure quay runs alembic migration and is available 118 | wait_for: 119 | timeout: 30 120 | 121 | - name: Create temporary data-only container to copy contents of sqlite_storage volume 122 | containers.podman.podman_container: 123 | name: quay-copy 124 | image: "{{ pause_image }}" 125 | state: present 126 | volumes: 127 | - "{{ sqlite_storage }}:/data:Z" 128 | 129 | - name: Copy data from container to host 130 | command: podman cp quay-copy:/data {{ expanded_quay_root }}/quay-postgres-backup/ 131 | 132 | - name: Run sqlite3 command inside container to take quay's sqlite database schema dump 133 | command: > 134 | podman run --name sqlite-cli --rm 135 | -v {{ expanded_quay_root }}/quay-postgres-backup/:/backup:Z 136 | {{ sqlite_image }} /backup/quay_sqlite.db .schema 137 | register: sqlite_schema_result 138 | ignore_errors: yes 139 | 140 | - name: Fail if sqlite3 command failed 141 | fail: 142 | msg: "sqlite3 command failed. Output was: {{ sqlite_schema_result.stdout }}" 143 | when: sqlite_schema_result.rc != 0 144 | 145 | - name: Back up sqlite schema data to host machine 146 | copy: 147 | content: "{{ sqlite_schema_result.stdout }}" 148 | dest: "{{ expanded_quay_root }}/quay-postgres-backup/sqlite_schema_dump.sql" 149 | when: sqlite_schema_result.rc == 0 150 | 151 | - name: Remove the quay_sqlite.db file 152 | file: 153 | path: "{{ expanded_quay_root }}/quay-postgres-backup/quay_sqlite.db" 154 | state: absent 155 | 156 | - name: Stop Quay migrate service 157 | systemd: 158 | name: quay-migrate.service 159 | enabled: no 160 | daemon_reload: yes 161 | state: stopped 162 | force: yes 163 | scope: "{{ systemd_scope }}" 164 | 165 | - name: Cleanup quay-migrate systemd unit file 166 | file: 167 | state: absent 168 | path: "{{ systemd_unit_dir }}/quay-migrate.service" 169 | 170 | - name: Concatenate sqlite schema .sql and transformed postgres data into single merged_sqlite.sql 171 | shell: cat "{{ expanded_quay_root }}/quay-postgres-backup/sqlite_schema_dump.sql" "{{ expanded_quay_root }}/quay-postgres-backup/transformed_pgdata.sql" > "{{ expanded_quay_root }}/quay-postgres-backup/merged_sqlite.sql" 172 | args: 173 | executable: /bin/bash 174 | 175 | - name: Apply merged_sqlite.sql into a new quay_sqlite.db file 176 | shell: cat "{{ expanded_quay_root }}/quay-postgres-backup/merged_sqlite.sql" | podman run -i --rm --name sqlite-cli -v {{ expanded_quay_root }}/quay-postgres-backup:/backup:Z {{ sqlite_image }} /backup/quay_sqlite.db 177 | args: 178 | executable: /bin/bash 179 | 180 | - name: Change permissions of quay_sqlite.db file 181 | file: 182 | path: "{{ expanded_quay_root }}/quay-postgres-backup/quay_sqlite.db" 183 | mode: '0664' 184 | 185 | - name: Copy quay_sqlite.db file to sqlite-storage volume 186 | command: podman cp {{ expanded_quay_root }}/quay-postgres-backup/quay_sqlite.db quay-copy:/data/ 187 | 188 | - name: Delete temporary container 189 | containers.podman.podman_container: 190 | name: quay-copy 191 | state: absent 192 | 193 | - name: Copy Quay systemd service file to run quay without migration 194 | template: 195 | src: ../templates/quay.service.j2 196 | dest: "{{ systemd_unit_dir }}/quay-app.service" 197 | vars: 198 | quay_cmd: "registry-nomigrate" 199 | 200 | - name: Start Quay service 201 | systemd: 202 | name: quay-app.service 203 | enabled: yes 204 | daemon_reload: yes 205 | state: restarted 206 | scope: "{{ systemd_scope }}" 207 | 208 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/secret-vars.yaml: -------------------------------------------------------------------------------- 1 | - name: Generate secrets for Quay config.yaml 2 | set_fact: 3 | secret_key: "{{ lookup('community.general.random_string', length=48, base64=True) }}" 4 | database_secret_key: "{{ lookup('community.general.random_string', length=48, base64=True) }}" 5 | redis_password: "{{ lookup('community.general.random_string', length=24, special=False) }}" 6 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/set-selinux-rules.yaml: -------------------------------------------------------------------------------- 1 | - name: Set container_manage_cgroup flag on and keep it persistent across reboots 2 | when: ansible_facts['distribution'] == "Red Hat Enterprise Linux" 3 | ansible.posix.seboolean: 4 | name: container_manage_cgroup 5 | state: yes 6 | persistent: yes 7 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/uninstall.yaml: -------------------------------------------------------------------------------- 1 | - name: Stop Quay service 2 | systemd: 3 | name: quay-app.service 4 | enabled: no 5 | daemon_reload: yes 6 | state: stopped 7 | force: yes 8 | scope: "{{ systemd_scope }}" 9 | 10 | - name: Stop Redis service 11 | systemd: 12 | name: quay-redis.service 13 | enabled: no 14 | daemon_reload: yes 15 | state: stopped 16 | force: yes 17 | scope: "{{ systemd_scope }}" 18 | 19 | - name: Stop Quay Pod service 20 | systemd: 21 | name: quay-pod.service 22 | enabled: no 23 | daemon_reload: yes 24 | state: stopped 25 | force: yes 26 | scope: "{{ systemd_scope }}" 27 | 28 | - name: Delete pod 29 | containers.podman.podman_pod: 30 | name: quay-pod 31 | state: absent 32 | 33 | - name: Delete Quay Storage named volume 34 | containers.podman.podman_volume: 35 | state: absent 36 | name: quay-storage 37 | when: auto_approve|bool == true and quay_storage == "quay-storage" 38 | 39 | - name: Delete Sqlite Storage named volume 40 | containers.podman.podman_volume: 41 | state: absent 42 | name: sqlite-storage 43 | when: auto_approve|bool == true and sqlite_storage == "sqlite-storage" 44 | 45 | - name: Delete Redis Password Secret 46 | containers.podman.podman_secret: 47 | state: absent 48 | name: redis_pass 49 | 50 | - name: Delete necessary directory for Quay local storage 51 | ansible.builtin.file: 52 | path: "{{ quay_storage }}" 53 | state: absent 54 | become: yes 55 | when: auto_approve|bool == true and quay_storage.startswith('/') 56 | 57 | - name: Delete necessary directory for Sqlite storage data 58 | ansible.builtin.file: 59 | path: "{{ sqlite_storage }}" 60 | state: absent 61 | become: yes 62 | when: auto_approve|bool == true and sqlite_storage.startswith('/') 63 | 64 | - name: Delete Install Directory 65 | file: 66 | state: absent 67 | path: "{{ quay_root }}" 68 | when: auto_approve|bool == true 69 | 70 | - name: Cleanup systemd unit files 71 | file: 72 | state: absent 73 | path: "{{ systemd_unit_dir }}/{{ item }}" 74 | loop: 75 | - quay-pod.service 76 | - quay-redis.service 77 | - quay-app.service 78 | 79 | - name: Just force systemd to reread configs (2.4 and above) 80 | ansible.builtin.systemd: 81 | daemon_reload: yes 82 | scope: "{{ systemd_scope }}" 83 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/upgrade-config-vars.yaml: -------------------------------------------------------------------------------- 1 | - name: Look up quay_root, set it to /etc/quay-install if not found. 2 | ansible.builtin.set_fact: 3 | quay_root: "{{ quay_root | default('/etc/quay-install') }}" 4 | 5 | - name: Include vars of the config.yaml into the 'quay_config_file' variable. 6 | ansible.builtin.slurp: 7 | src: "{{ quay_root }}/quay-config/config.yaml" 8 | register: remote_yaml_file 9 | 10 | - name: Parse the remote YAML file and set as a fact 11 | ansible.builtin.set_fact: 12 | quay_config_file: "{{ remote_yaml_file['content'] | b64decode | from_yaml }}" 13 | 14 | - name: Set facts for the existing redis secrets only if they are a string and not a jinja2 variable in the config.yaml. 15 | ansible.builtin.set_fact: 16 | REDIS_PASSWORD : "{{ quay_config_file['USER_EVENTS_REDIS']['password'] }}" 17 | when: quay_config_file['DATABASE_SECRET_KEY'] is string and quay_config_file['USER_EVENTS_REDIS']['password'] is string 18 | 19 | - name: Check if quay-postgres container is running 20 | command: podman ps -q -f name=quay-postgres 21 | register: postgres_container_status 22 | changed_when: false 23 | 24 | - name: Set facts for existing postgres secrets only if they are a string and not a jinja2 variable in the config.yaml. 25 | ansible.builtin.set_fact: 26 | PGDB_PASSWORD : "{{ quay_config_file['DB_URI'].split('@')[0].split(':')[2] }}" 27 | when: postgres_container_status.stdout != "" and quay_config_file['DATABASE_SECRET_KEY'] is string and quay_config_file['DB_URI'] is string 28 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/upgrade-pod-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Copy Quay Pod systemd service file 2 | template: 3 | src: ../templates/pod.service.j2 4 | dest: "{{ systemd_unit_dir }}/quay-pod.service" 5 | 6 | - name: Check if pod pause image is loaded 7 | command: podman inspect --type=image {{ pause_image }} 8 | register: r 9 | ignore_errors: yes 10 | 11 | - name: Pull Infra image 12 | containers.podman.podman_image: 13 | name: "{{ pause_image }}" 14 | when: r.rc != 0 15 | retries: 5 16 | delay: 5 17 | 18 | - name: Start Quay Pod service 19 | systemd: 20 | name: quay-pod.service 21 | enabled: yes 22 | daemon_reload: yes 23 | state: restarted 24 | scope: "{{ systemd_scope }}" 25 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/upgrade-quay-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Copy Quay systemd service file 2 | template: 3 | src: ../templates/quay.service.j2 4 | dest: "{{ systemd_unit_dir }}/quay-app.service" 5 | vars: 6 | quay_cmd: "registry" 7 | 8 | - name: Check if Quay image is loaded 9 | command: podman inspect --type=image {{ quay_image }} 10 | register: q 11 | ignore_errors: yes 12 | 13 | - name: Pull Quay image 14 | containers.podman.podman_image: 15 | name: "{{ quay_image }}" 16 | when: q.rc != 0 17 | retries: 5 18 | delay: 5 19 | 20 | - name: Check if the SQLite storage directory exists 21 | stat: 22 | path: "{{ sqlite_storage }}" 23 | register: sqlite_storage_stat 24 | when: sqlite_storage.startswith('/') 25 | 26 | - name: Create necessary directory for sqlite storage 27 | ansible.builtin.file: 28 | path: "{{ sqlite_storage }}" 29 | state: directory 30 | recurse: yes 31 | when: sqlite_storage.startswith('/') and not sqlite_storage_stat.stat.exists 32 | 33 | - name: Set permissions on sqlite storage directory 34 | ansible.posix.acl: 35 | path: "{{ sqlite_storage }}" 36 | entity: 1001 37 | etype: user 38 | permissions: wx 39 | state: present 40 | when: sqlite_storage.startswith('/') and not sqlite_storage_stat.stat.exists 41 | 42 | - name: Start Quay service 43 | systemd: 44 | name: quay-app.service 45 | enabled: yes 46 | daemon_reload: yes 47 | state: restarted 48 | scope: "{{ systemd_scope }}" 49 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/upgrade-redis-service.yaml: -------------------------------------------------------------------------------- 1 | - name: Copy Redis systemd service file 2 | template: 3 | src: ../templates/redis.service.j2 4 | dest: "{{ systemd_unit_dir }}/quay-redis.service" 5 | 6 | - name: Check if Redis image is loaded 7 | command: podman inspect --type=image {{ redis_image }} 8 | register: r 9 | ignore_errors: yes 10 | 11 | - name: Pull Redis image 12 | containers.podman.podman_image: 13 | name: "{{ redis_image }}" 14 | when: r.rc != 0 15 | retries: 5 16 | delay: 5 17 | 18 | - name: Create Redis Password Secret 19 | containers.podman.podman_secret: 20 | state: present 21 | name: redis_pass 22 | data: "{{ REDIS_PASSWORD }}" 23 | skip_existing: true 24 | 25 | - name: Start Redis service 26 | systemd: 27 | name: quay-redis.service 28 | enabled: yes 29 | daemon_reload: yes 30 | state: restarted 31 | scope: "{{ systemd_scope }}" 32 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/upgrade.yaml: -------------------------------------------------------------------------------- 1 | - name: Expand variables 2 | include_tasks: expand-vars.yaml 3 | 4 | - name: Install Dependencies 5 | include_tasks: install-deps.yaml 6 | 7 | - name: Set SELinux Rules 8 | include_tasks: set-selinux-rules.yaml 9 | 10 | - name: Autodetect Image Archive 11 | include_tasks: autodetect-image-archive.yaml 12 | 13 | - name: Autodetect existing Secrets in config.yaml 14 | include_tasks: upgrade-config-vars.yaml 15 | 16 | - name: Upgrade Quay Pod Service 17 | include_tasks: upgrade-pod-service.yaml 18 | 19 | - name: Upgrade Redis Service 20 | include_tasks: upgrade-redis-service.yaml 21 | 22 | - name: Upgrade Quay Service 23 | include_tasks: upgrade-quay-service.yaml 24 | 25 | - name: Wait for Quay 26 | include_tasks: wait-for-quay.yaml 27 | 28 | - name: Check if quay-postgres container is running 29 | command: podman ps -q -f name=quay-postgres 30 | register: postgres_container_status 31 | changed_when: false 32 | 33 | - name: Autodetect Sqlite Archive 34 | include_tasks: autodetect-sqlite-archive.yaml 35 | when: postgres_container_status.stdout != "" 36 | 37 | - name: Migrate postgres db to sqlite for Quay 38 | include_tasks: migrate.yaml 39 | when: postgres_container_status.stdout != "" 40 | 41 | - name: Wait for Quay 42 | include_tasks: wait-for-quay.yaml 43 | 44 | - name: Clean up old postgres service 45 | include_tasks: cleanup-postgres.yaml 46 | when: postgres_container_status.stdout != "" 47 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/tasks/wait-for-quay.yaml: -------------------------------------------------------------------------------- 1 | - name: Wait for Quay to become alive and handle failure 2 | block: 3 | - name: Waiting up to 3 minutes for Quay to become alive at https://{{ quay_hostname }}/health/instance 4 | uri: 5 | url: "https://{{ quay_hostname }}/health/instance" 6 | method: GET 7 | validate_certs: no 8 | register: result 9 | until: result.status == 200 10 | retries: 10 11 | delay: 30 12 | rescue: 13 | - name: Print debug logs for quay-app container on failure 14 | command: podman logs quay-app 15 | register: quay_logs 16 | ignore_errors: yes 17 | changed_when: false 18 | 19 | - name: Display logs if the container exists 20 | when: quay_logs.rc == 0 and quay_logs.stdout != "" 21 | debug: 22 | msg: "{{ quay_logs.stdout_lines }}" 23 | 24 | - name: Show quay-app.service status 25 | command: systemctl --{{ systemd_scope }} status quay-app.service 26 | register: systemctl_status 27 | ignore_errors: yes 28 | 29 | - name: Fail the playbook since Quay failed to startup 30 | fail: 31 | msg: "Quay did not become alive, see systemctl status output: {{ systemctl_status.stdout_lines }}" 32 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/config.yaml.j2: -------------------------------------------------------------------------------- 1 | AUTHENTICATION_TYPE: Database 2 | BUILDLOGS_REDIS: 3 | host: localhost 4 | password: {{ redis_password }} 5 | port: 6379 6 | DATABASE_SECRET_KEY: {{ database_secret_key }} 7 | DB_URI: sqlite:////sqlite/quay_sqlite.db 8 | DEFAULT_TAG_EXPIRATION: 2w 9 | DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: [] 10 | DISTRIBUTED_STORAGE_PREFERENCE: 11 | - default 12 | DISTRIBUTED_STORAGE_CONFIG: 13 | default: 14 | - LocalStorage 15 | - storage_path: /datastorage 16 | ENTERPRISE_LOGO_URL: /static/img/quay-horizontal-color.svg 17 | FEATURE_ACI_CONVERSION: false 18 | FEATURE_ANONYMOUS_ACCESS: true 19 | FEATURE_APP_REGISTRY: false 20 | FEATURE_APP_SPECIFIC_TOKENS: true 21 | FEATURE_BUILD_SUPPORT: false 22 | FEATURE_CHANGE_TAG_EXPIRATION: true 23 | FEATURE_DIRECT_LOGIN: true 24 | FEATURE_PARTIAL_USER_AUTOCOMPLETE: true 25 | FEATURE_REPO_MIRROR: true 26 | FEATURE_MAILING: false 27 | FEATURE_REQUIRE_TEAM_INVITE: true 28 | FEATURE_RESTRICTED_V1_PUSH: true 29 | FEATURE_SECURITY_NOTIFICATIONS: true 30 | FEATURE_SECURITY_SCANNER: false 31 | FEATURE_USERNAME_CONFIRMATION: true 32 | FEATURE_USER_CREATION: true 33 | FEATURE_USER_LOG_ACCESS: true 34 | GITHUB_LOGIN_CONFIG: {} 35 | GITHUB_TRIGGER_CONFIG: {} 36 | GITLAB_TRIGGER_KIND: {} 37 | LOGS_MODEL: database 38 | LOGS_MODEL_CONFIG: {} 39 | LOG_ARCHIVE_LOCATION: default 40 | PREFERRED_URL_SCHEME: https 41 | REGISTRY_TITLE: Red Hat Quay 42 | REGISTRY_TITLE_SHORT: Red Hat Quay 43 | REPO_MIRROR_SERVER_HOSTNAME: null 44 | REPO_MIRROR_TLS_VERIFY: false 45 | SECRET_KEY: {{ secret_key }} 46 | SECURITY_SCANNER_ISSUER_NAME: security_scanner 47 | SERVER_HOSTNAME: {{ quay_hostname }} 48 | SETUP_COMPLETE: true 49 | SUPER_USERS: 50 | - admin 51 | TAG_EXPIRATION_OPTIONS: 52 | - 0s 53 | - 1d 54 | - 1w 55 | - 2w 56 | - 4w 57 | TEAM_RESYNC_STALE_TIME: 60m 58 | TESTING: false 59 | USERFILES_LOCATION: default 60 | USERFILES_PATH: userfiles/ 61 | USER_EVENTS_REDIS: 62 | host: localhost 63 | password: {{ redis_password }} 64 | port: 6379 65 | USE_CDN: false 66 | FEATURE_USER_INITIALIZE: true 67 | CREATE_NAMESPACE_ON_PUSH: true 68 | 69 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/pg_dump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CPU_COUNT=$(nproc 2>/dev/null || echo 1) 4 | CPU_COUNT=$(( CPU_COUNT > 0 ? CPU_COUNT : 1)) 5 | echo "CPU COUNT: ${CPU_COUNT}" 6 | 7 | # clean up postgres tmp/pgdump_dir if previously created 8 | podman exec quay-postgres rm -rf /tmp/pgdump_dir 9 | 10 | # Take pg_dump of postgres container in parallel and store it in tmp/pgdump_dir 11 | podman exec -it quay-postgres \ 12 | pg_dump -j "$CPU_COUNT" -Z 0 --no-sync --format=directory \ 13 | --data-only --column-inserts --no-owner --no-privileges \ 14 | --disable-triggers -U user -d quay -f /tmp/pgdump_dir 15 | 16 | # Convert the .dat dump files to a single .sql file using pg_restore 17 | podman exec -i quay-postgres \ 18 | pg_restore -Fd /tmp/pgdump_dir -f /tmp/pg_data_dump.sql -j "$CPU_COUNT" 19 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/pg_to_sqlite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # error tracing 5 | trap 'echo "ERROR at line $LINENO: Command failed - $BASH_COMMAND" >&2; exit 1' ERR 6 | 7 | #function that takes postgres data-only dump as "input_file" arg and converts it into a sqlite compatible .sql file 8 | pg_to_sqlite() { 9 | local input_file="$1" 10 | local output_file="$2" 11 | 12 | # Validate input file 13 | [[ ! -f "$input_file" ]] && { echo "Error: Input file '$input_file' not found (line $LINENO)" >&2; exit 1; } 14 | [[ ! -s "$input_file" ]] && { echo "Error: Input file '$input_file' is empty (line $LINENO)" >&2; exit 1; } 15 | 16 | { 17 | echo "BEGIN TRANSACTION;" 18 | 19 | # Process conversion 20 | sed -E " 21 | s/'true'/1/g; 22 | s/'false'/0/g; 23 | /SET\s+\w+\s*=\s*[^;]+;/d; 24 | /^\s*--.*$/d; 25 | /ALTER TABLE .*? DISABLE TRIGGER ALL;/d; 26 | /ALTER TABLE .*? ENABLE TRIGGER ALL;/d; 27 | s/SELECT pg_catalog\.set_config\('search_path', '', false\);//g; 28 | /SET SESSION AUTHORIZATION DEFAULT;/d; 29 | /SET client_encoding = '\''UTF8'\'';/d; 30 | /^pg_dump:.*$/d; 31 | s/SELECT pg_catalog\.setval\('public\..*?', [0-9]+, (true|false)\);//g; 32 | s/INSERT INTO public\./INSERT INTO /g; 33 | /^\s*$/d; 34 | " "$input_file" 35 | 36 | echo "COMMIT;" 37 | } > "$output_file" 38 | 39 | # Validate output file 40 | if [[ ! -f "$output_file" ]]; then 41 | echo "Error: Output file '$output_file' was not created (line $LINENO)" >&2 42 | exit 1 43 | elif [[ ! -s "$output_file" ]]; then 44 | echo "Error: Output file '$output_file' is empty (line $LINENO)" >&2 45 | exit 1 46 | fi 47 | } 48 | 49 | pg_to_sqlite "$1" "$2" 50 | echo "Success: Created valid output file '$2'" 51 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/pod.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Infra Container for Quay 3 | Wants=network.target 4 | After=network-online.target 5 | Before=quay-redis.service 6 | 7 | [Service] 8 | Type=simple 9 | RemainAfterExit=yes 10 | TimeoutStartSec=5m 11 | ExecStartPre=-/bin/rm -f %t/%n-pid %t/%n-pod-id 12 | ExecStart=/usr/bin/podman pod create \ 13 | --name quay-pod \ 14 | --infra-image {{ pause_image }} \ 15 | --publish {{ quay_hostname.split(":")[1] if (":" in quay_hostname) else "8443" }}:8443 \ 16 | --pod-id-file %t/%n-pod-id \ 17 | --replace 18 | ExecStop=-/usr/bin/podman pod stop --ignore --pod-id-file %t/%n-pod-id -t 10 19 | ExecStopPost=-/usr/bin/podman pod rm --ignore -f --pod-id-file %t/%n-pod-id 20 | PIDFile=%t/%n-pid 21 | KillMode=none 22 | Restart=always 23 | RestartSec=30 24 | 25 | [Install] 26 | WantedBy=multi-user.target default.target 27 | 28 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/quay.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Quay Container 3 | Wants=network.target 4 | After=network-online.target quay-pod.service quay-redis.service 5 | Requires=quay-pod.service quay-redis.service 6 | 7 | [Service] 8 | Type=simple 9 | TimeoutStartSec=5m 10 | Environment=PODMAN_SYSTEMD_UNIT=%n 11 | ExecStartPre=-/bin/rm -f %t/%n-pid %t/%n-cid 12 | ExecStart=/usr/bin/podman run \ 13 | --name quay-app \ 14 | -v {{ expanded_quay_root }}/quay-config:/quay-registry/conf/stack:Z \ 15 | -v {{ expanded_sqlite_storage }}:/sqlite:Z \ 16 | -v {{ expanded_quay_storage }}:/datastorage:Z \ 17 | --image-volume=ignore \ 18 | --pod=quay-pod \ 19 | --conmon-pidfile %t/%n-pid \ 20 | --cidfile %t/%n-cid \ 21 | --cgroups=no-conmon \ 22 | --log-driver=journald \ 23 | --replace \ 24 | -e WORKER_COUNT_UNSUPPORTED_MINIMUM=1 \ 25 | -e WORKER_COUNT=1 \ 26 | {{ quay_image }} {{ quay_cmd }} 27 | 28 | ExecStop=-/usr/bin/podman stop --ignore --cidfile %t/%n-cid -t 10 29 | ExecStopPost=-/bin/sh -c 'if [ "$EXIT_STATUS" -eq 0 ]; then /usr/bin/podman rm --ignore -f --cidfile %t/%n-cid; fi' 30 | PIDFile=%t/%n-pid 31 | KillMode=none 32 | Restart=always 33 | RestartSec=30 34 | 35 | [Install] 36 | WantedBy=multi-user.target default.target 37 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/redis.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Redis Podman Container for Quay 3 | Wants=network.target 4 | After=network-online.target quay-pod.service 5 | Requires=quay-pod.service 6 | 7 | [Service] 8 | Type=simple 9 | TimeoutStartSec=5m 10 | ExecStartPre=-/bin/rm -f %t/%n-pid %t/%n-cid 11 | ExecStart=/usr/bin/podman run \ 12 | --name quay-redis \ 13 | --pod=quay-pod \ 14 | --conmon-pidfile %t/%n-pid \ 15 | --cidfile %t/%n-cid \ 16 | --cgroups=no-conmon \ 17 | --image-volume=ignore \ 18 | --secret=redis_pass,type=env,target=REDIS_PASSWORD \ 19 | --replace \ 20 | {{ redis_image }} 21 | 22 | ExecStop=-/usr/bin/podman stop --ignore --cidfile %t/%n-cid -t 10 23 | ExecStopPost=-/usr/bin/podman rm --ignore -f --cidfile %t/%n-cid 24 | PIDFile=%t/%n-pid 25 | KillMode=none 26 | Restart=always 27 | RestartSec=30 28 | 29 | [Install] 30 | WantedBy=multi-user.target default.target 31 | 32 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/roles/mirror_appliance/templates/req.j2: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 4096 3 | default_md = sha256 4 | distinguished_name = req_distinguished_name 5 | x509_extensions = v3_req 6 | prompt = no 7 | [req_distinguished_name] 8 | C = US 9 | ST = VA 10 | L = New York 11 | O = Quay 12 | OU = Division 13 | CN = {{ quay_hostname.split(":")[0] if (":" in quay_hostname) else quay_hostname }} 14 | [v3_req] 15 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyCertSign 16 | extendedKeyUsage = serverAuth 17 | subjectAltName = @alt_names 18 | [alt_names] 19 | DNS = {{ quay_hostname.split(":")[0] if (":" in quay_hostname) else quay_hostname }} -------------------------------------------------------------------------------- /ansible-runner/context/app/project/uninstall_mirror_appliance.yml: -------------------------------------------------------------------------------- 1 | - name: "Uninstall Mirror Appliance" 2 | gather_facts: yes 3 | hosts: all 4 | tags: 5 | - quay 6 | tasks: 7 | - name: uninstall_mirror_appliance 8 | import_role: 9 | name: mirror_appliance 10 | tasks_from: uninstall 11 | -------------------------------------------------------------------------------- /ansible-runner/context/app/project/upgrade_mirror_appliance.yml: -------------------------------------------------------------------------------- 1 | - name: "Upgrade Mirror Appliance" 2 | gather_facts: yes 3 | hosts: all 4 | tags: 5 | - quay 6 | tasks: 7 | - name: upgrade_mirror_appliance 8 | import_role: 9 | name: mirror_appliance 10 | tasks_from: upgrade 11 | -------------------------------------------------------------------------------- /ansible-runner/execution-environment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | 4 | build_arg_defaults: 5 | EE_BASE_IMAGE: registry.redhat.io/ansible-automation-platform-25/ee-minimal-rhel8:1.0.0-842 6 | 7 | dependencies: 8 | galaxy: requirements.yml 9 | 10 | additional_build_steps: 11 | append: 12 | - COPY app /runner 13 | -------------------------------------------------------------------------------- /ansible-runner/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - ansible.posix 4 | - containers.podman 5 | - community.general 6 | -------------------------------------------------------------------------------- /ansible-runner/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = mirror-registry 3 | version = 1.0.0 -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | 13 | _ "github.com/lib/pq" // pg driver 14 | "github.com/sethvargo/go-password/password" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // These variables are set at build time via ldflags 19 | var eeImage string 20 | var pauseImage string 21 | var quayImage string 22 | var redisImage string 23 | 24 | // imageArchivePath is the optional location of the OCI image archive containing required install images 25 | var imageArchivePath string 26 | 27 | // executableDir is the optional location of the OCI image archive containing unpacked required install images 28 | var executableDir string 29 | 30 | // sshKey is the optional location of the SSH key you would like to use to connect to your host. 31 | var sshKey string 32 | 33 | // sslCert is the path to the SSL certitificate 34 | var sslCert string 35 | 36 | // sslKey is the path to the SSL key 37 | var sslKey string 38 | 39 | // sslCheckSkip holds whether or not to check the SSL certificate 40 | var sslCheckSkip bool 41 | 42 | // targetHostname is the hostname of the server you wish to install Quay on 43 | var targetHostname string 44 | 45 | // targetUsername is the name of the user on the target host to connect with SSH 46 | var targetUsername string 47 | 48 | // initUser is the initial username. 49 | var initUser string 50 | 51 | // initPassword is the password of the initial user. 52 | var initPassword string 53 | 54 | // quayHostname is the value to set SERVER_HOSTNAME in the Quay config.yaml 55 | var quayHostname string 56 | 57 | // askBecomePass holds whether or not to ask for password during SSH connection 58 | var askBecomePass bool 59 | 60 | // quayRoot is the directory where all the quay config data is stored 61 | var quayRoot string 62 | 63 | // quayStorage is the directory where all the Quay data is stored 64 | var quayStorage string 65 | 66 | // sqliteStorage is the directory where all the Quay sqlite data is stored 67 | var sqliteStorage string 68 | 69 | // additionalArgs are arguments that you would like to append to the end of the ansible-playbook call (used mostly for development) 70 | var additionalArgs string 71 | 72 | // command to run when starting quay container 73 | var quayCmd string 74 | 75 | // installCmd represents the install command 76 | var installCmd = &cobra.Command{ 77 | Use: "install", 78 | Short: "Install Quay and its required dependencies.", 79 | Run: func(cmd *cobra.Command, args []string) { 80 | install() 81 | }, 82 | } 83 | 84 | func init() { 85 | 86 | // Add install command 87 | rootCmd.AddCommand(installCmd) 88 | 89 | installCmd.Flags().StringVarP(&targetHostname, "targetHostname", "H", getFQDN(), "The hostname of the target you wish to install Quay to. This defaults to $HOST") 90 | installCmd.Flags().StringVarP(&targetUsername, "targetUsername", "u", os.Getenv("USER"), "The user on the target host which will be used for SSH. This defaults to $USER") 91 | installCmd.Flags().StringVarP(&sshKey, "ssh-key", "k", os.Getenv("HOME")+"/.ssh/quay_installer", "The path of your ssh identity key. This defaults to ~/.ssh/quay_installer") 92 | 93 | installCmd.Flags().StringVarP(&sslCert, "sslCert", "", "", "The path to the SSL certificate Quay should use") 94 | installCmd.Flags().StringVarP(&sslKey, "sslKey", "", "", "The path to the SSL key Quay should use") 95 | installCmd.Flags().BoolVarP(&sslCheckSkip, "sslCheckSkip", "", false, "Whether or not to check the certificate hostname against the SERVER_HOSTNAME in config.yaml.") 96 | 97 | installCmd.Flags().StringVarP(&initUser, "initUser", "", "init", "The username of the initial user. This defaults to init.") 98 | installCmd.Flags().StringVarP(&initPassword, "initPassword", "", "", "The password of the initial user. If not specified, this will be randomly generated.") 99 | installCmd.Flags().StringVarP(&quayHostname, "quayHostname", "", "", "The value to set SERVER_HOSTNAME in the Quay config.yaml. This defaults to :8443") 100 | 101 | installCmd.Flags().StringVarP(&imageArchivePath, "image-archive", "i", "", "An archive containing images") 102 | installCmd.Flags().BoolVarP(&askBecomePass, "askBecomePass", "", false, "Whether or not to ask for sudo password during SSH connection.") 103 | installCmd.Flags().StringVarP(&quayRoot, "quayRoot", "r", "~/quay-install", "The folder where quay persistent data are saved. This defaults to ~/quay-install") 104 | installCmd.Flags().StringVarP(&quayStorage, "quayStorage", "", "quay-storage", "The folder where quay persistent storage data is saved. This defaults to a Podman named volume 'quay-storage'. Root is required to uninstall.") 105 | installCmd.Flags().StringVarP(&sqliteStorage, "sqliteStorage", "", "sqlite-storage", "The folder where quay sqlite data is saved. This defaults to a Podman named volume 'sqlite-storage'. Root is required to uninstall.") 106 | installCmd.Flags().StringVarP(&additionalArgs, "additionalArgs", "", "", "Additional arguments you would like to append to the ansible-playbook call. Used mostly for development.") 107 | 108 | } 109 | 110 | func install() { 111 | 112 | var err error 113 | log.Printf("Install has begun") 114 | 115 | log.Debug("Ansible Execution Environment Image: " + eeImage) 116 | log.Debug("Pause Image: " + pauseImage) 117 | log.Debug("Quay Image: " + quayImage) 118 | log.Debug("Redis Image: " + redisImage) 119 | 120 | // Load execution environment 121 | err = loadExecutionEnvironment() 122 | check(err) 123 | 124 | // Set quayHostname if not already set 125 | if quayHostname == "" { 126 | quayHostname = targetHostname + ":8443" 127 | } 128 | 129 | // Load the SSL certificate and the key 130 | err = loadCerts(sslCert, sslKey, strings.Split(quayHostname, ":")[0], sslCheckSkip) 131 | check(err) 132 | 133 | // Check that SSH key is present, and generate if not 134 | err = loadSSHKeys() 135 | check(err) 136 | 137 | // Handle Image Archive Defaulting 138 | var imageArchiveMountFlag string 139 | if imageArchivePath == "" { 140 | executableDir, err := os.Executable() 141 | check(err) 142 | defaultArchivePath := path.Join(path.Dir(executableDir), "image-archive.tar") 143 | if pathExists(defaultArchivePath) { 144 | imageArchivePath = defaultArchivePath 145 | } 146 | } else { 147 | if !pathExists(imageArchivePath) { 148 | check(errors.New("Could not find image-archive.tar at " + imageArchivePath)) 149 | } 150 | } 151 | 152 | if imageArchivePath != "" { 153 | imageArchiveMountFlag = fmt.Sprintf("-v %s:/runner/image-archive.tar", imageArchivePath) 154 | log.Info("Found image archive at " + imageArchivePath) 155 | if isLocalInstall() { 156 | log.Printf("Unpacking image archive from %s", imageArchivePath) 157 | cmd := exec.Command("tar", "-xvf", imageArchivePath) 158 | if verbose { 159 | cmd.Stderr = os.Stderr 160 | cmd.Stdout = os.Stdout 161 | } 162 | err = cmd.Run() 163 | check(err) 164 | 165 | // Load Pause image 166 | pauseArchivePath := path.Join(path.Dir(executableDir), "pause.tar") 167 | log.Printf("Loading pause image archive from %s", pauseArchivePath) 168 | statement := getImageMetadata("pause", pauseImage, pauseArchivePath) 169 | pauseImport := exec.Command("/bin/bash", "-c", statement) 170 | if verbose { 171 | pauseImport.Stderr = os.Stderr 172 | pauseImport.Stdout = os.Stdout 173 | } 174 | log.Debug("Importing Pause with command: ", pauseImport) 175 | err = pauseImport.Run() 176 | check(err) 177 | 178 | // Load Redis image 179 | redisArchivePath := path.Join(path.Dir(executableDir), "redis.tar") 180 | log.Printf("Loading redis image archive from %s", redisArchivePath) 181 | statement = getImageMetadata("redis", redisImage, redisArchivePath) 182 | redisImport := exec.Command("/bin/bash", "-c", statement) 183 | if verbose { 184 | redisImport.Stderr = os.Stderr 185 | redisImport.Stdout = os.Stdout 186 | } 187 | log.Debug("Importing Redis with command: ", redisImport) 188 | err = redisImport.Run() 189 | check(err) 190 | 191 | // Load Quay image 192 | quayArchivePath := path.Join(path.Dir(executableDir), "quay.tar") 193 | log.Printf("Loading Quay image archive from %s", quayArchivePath) 194 | statement = getImageMetadata("quay", quayImage, quayArchivePath) 195 | quayImport := exec.Command("/bin/bash", "-c", statement) 196 | if verbose { 197 | quayImport.Stderr = os.Stderr 198 | quayImport.Stdout = os.Stdout 199 | } 200 | log.Debug("Importing Quay with command: ", quayImport) 201 | err = quayImport.Run() 202 | check(err) 203 | } 204 | log.Infof("Attempting to set SELinux rules on image archive") 205 | cmd := exec.Command("chcon", "-Rt", "svirt_sandbox_file_t", imageArchivePath) 206 | if verbose { 207 | cmd.Stderr = os.Stderr 208 | cmd.Stdout = os.Stdout 209 | } 210 | if err := cmd.Run(); err != nil { 211 | log.Warn("Could not set SELinux rule. If your system does not have SELinux enabled, you may ignore this.") 212 | } 213 | } 214 | 215 | // Generate password if none provided 216 | if initPassword == "" { 217 | initPassword, err = password.Generate(32, 10, 0, false, false) 218 | check(err) 219 | } 220 | 221 | // Set quayHostname if not already set 222 | if quayHostname == "" { 223 | quayHostname = targetHostname + ":8443" 224 | } 225 | 226 | // Add port if not present 227 | if !strings.Contains(quayHostname, ":") { 228 | quayHostname = quayHostname + ":8443" 229 | } 230 | 231 | // Set askBecomePass flag if true 232 | var askBecomePassFlag string 233 | if askBecomePass { 234 | askBecomePassFlag = "-K" 235 | } 236 | 237 | // Set the SSL flag if cert and key are defined 238 | var sslCertKeyFlag string 239 | if sslCert != "" && sslKey != "" { 240 | sslCertAbs, err := filepath.Abs(sslCert) 241 | if err != nil { 242 | check(errors.New("Unable to get absolute path of " + sslCert)) 243 | } 244 | sslKeyAbs, err := filepath.Abs(sslKey) 245 | if err != nil { 246 | check(errors.New("Unable to get absolute path of " + sslKey)) 247 | } 248 | sslCertKeyFlag = fmt.Sprintf(" -v %s:/runner/certs/quay.cert:Z -v %s:/runner/certs/quay.key:Z", sslCertAbs, sslKeyAbs) 249 | } 250 | 251 | quayCmd = "registry" 252 | 253 | // Run playbook 254 | log.Printf("Running install playbook. This may take some time. To see playbook output run the installer with -v (verbose) flag.") 255 | quayVersion := strings.Split(quayImage, ":")[1] 256 | podmanCmd := fmt.Sprintf(`podman run `+ 257 | `--rm --interactive --tty `+ 258 | `--workdir /runner/project `+ 259 | `--net host `+ 260 | imageArchiveMountFlag+ // optional image archive flag 261 | sslCertKeyFlag+ // optional ssl cert/key flag 262 | ` -v %s:/runner/env/ssh_key `+ 263 | `-e RUNNER_OMIT_EVENTS=False `+ 264 | `-e RUNNER_ONLY_FAILED_EVENTS=False `+ 265 | `-e ANSIBLE_HOST_KEY_CHECKING=False `+ 266 | `-e ANSIBLE_CONFIG=/runner/project/ansible.cfg `+ 267 | fmt.Sprintf("-e ANSIBLE_NOCOLOR=%t ", noColor)+ 268 | `--quiet `+ 269 | `--name ansible_runner_instance `+ 270 | fmt.Sprintf("%s ", eeImage)+ 271 | `ansible-playbook -i %s@%s, --private-key /runner/env/ssh_key -e "init_user=%s init_password=%s quay_image=%s quay_version=%s redis_image=%s pause_image=%s quay_hostname=%s local_install=%s quay_root=%s quay_storage=%s sqlite_storage=%s quay_cmd=%s" install_mirror_appliance.yml %s %s`, 272 | sshKey, targetUsername, targetHostname, initUser, initPassword, quayImage, quayVersion, redisImage, pauseImage, quayHostname, strconv.FormatBool(isLocalInstall()), quayRoot, quayStorage, sqliteStorage, quayCmd, askBecomePassFlag, additionalArgs) 273 | 274 | log.Debug("Running command: " + podmanCmd) 275 | cmd := exec.Command("bash", "-c", podmanCmd) 276 | cmd.Stderr = os.Stderr 277 | cmd.Stdout = os.Stdout 278 | cmd.Stdin = os.Stdin 279 | err = cmd.Run() 280 | check(err) 281 | 282 | log.Printf("Quay installed successfully, config data is stored in %s", quayRoot) 283 | log.Printf("Quay is available at %s with credentials (%s, %s)", "https://"+quayHostname, initUser, initPassword) 284 | } 285 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // Create logger 12 | var log = &logrus.Logger{ 13 | Out: os.Stdout, 14 | Level: logrus.InfoLevel, 15 | } 16 | 17 | // verbose is the optional command that will display INFO logs 18 | var verbose bool 19 | 20 | // noColor is the optional flag for controlling ANSI sequence output 21 | var noColor bool 22 | 23 | // version is an optional command that will display the current release version 24 | var releaseVersion string 25 | 26 | func init() { 27 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Display verbose logs") 28 | rootCmd.PersistentFlags().BoolVarP(&noColor, "no-color", "c", false, "Control colored output") 29 | } 30 | 31 | var ( 32 | rootCmd = &cobra.Command{ 33 | Use: "mirror-registry", 34 | Version: releaseVersion, 35 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 36 | if verbose { 37 | log.SetLevel(logrus.DebugLevel) 38 | } else { 39 | log.SetLevel(logrus.InfoLevel) 40 | } 41 | }, 42 | } 43 | ) 44 | 45 | // Execute executes the root command. 46 | func Execute() error { 47 | log.SetFormatter(&logrus.TextFormatter{ 48 | DisableColors: noColor, 49 | TimestampFormat: "2006-01-02 15:04:05", 50 | FullTimestamp: true, 51 | }) 52 | fmt.Println(` 53 | __ __ 54 | / \ / \ ______ _ _ __ __ __ 55 | / /\ / /\ \ / __ \ | | | | / \ \ \ / / 56 | / / / / \ \ | | | | | | | | / /\ \ \ / 57 | \ \ \ \ / / | |__| | | |__| | / ____ \ | | 58 | \ \/ \ \/ / \_ ___/ \____/ /_/ \_\ |_| 59 | \__/ \__/ \ \__ 60 | \___\ by Red Hat 61 | Build, Store, and Distribute your Containers 62 | `) 63 | return rootCmd.Execute() 64 | } 65 | -------------------------------------------------------------------------------- /cmd/uninstall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // autoApprove controls whether or not to prompt user 13 | var autoApprove bool 14 | 15 | // uninstallCmd represents the uninstall command 16 | var uninstallCmd = &cobra.Command{ 17 | Use: "uninstall", 18 | Short: "uninstall will remove all Quay dependencies.", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | uninstall() 21 | }, 22 | } 23 | 24 | func init() { 25 | 26 | // Add install command 27 | rootCmd.AddCommand(uninstallCmd) 28 | 29 | uninstallCmd.Flags().StringVarP(&sshKey, "ssh-key", "k", os.Getenv("HOME")+"/.ssh/quay_installer", "The path of your ssh identity key. This defaults to ~/.ssh/quay_installer") 30 | uninstallCmd.Flags().StringVarP(&targetHostname, "targetHostname", "H", "localhost", "The hostname of the target you wish to install Quay to. This defaults to localhost") 31 | uninstallCmd.Flags().StringVarP(&targetUsername, "targetUsername", "u", os.Getenv("USER"), "The user you wish to ssh into your remote with. This defaults to the current username") 32 | uninstallCmd.Flags().BoolVarP(&askBecomePass, "askBecomePass", "", false, "Whether or not to ask for sudo password during SSH connection.") 33 | uninstallCmd.Flags().StringVarP(&quayRoot, "quayRoot", "r", "~/quay-install", "The folder where quay persistent data are saved. This defaults to ~/quay-install") 34 | uninstallCmd.Flags().StringVarP(&quayStorage, "quayStorage", "", "quay-storage", "The folder where quay persistent storage data is saved. This defaults to a Podman named volume 'quay-storage'. Root is required to uninstall.") 35 | uninstallCmd.Flags().StringVarP(&sqliteStorage, "sqliteStorage", "", "sqlite-storage", "The folder where quay sqlite data is saved. This defaults to a Podman named volume 'sqlite-storage'. Root is required to uninstall.") 36 | uninstallCmd.Flags().StringVarP(&additionalArgs, "additionalArgs", "", "", "Additional arguments you would like to append to the ansible-playbook call. Used mostly for development.") 37 | uninstallCmd.Flags().BoolVarP(&autoApprove, "autoApprove", "", false, "Skips interactive approval") 38 | } 39 | 40 | func uninstall() { 41 | 42 | var err error 43 | log.Printf("Uninstall has begun") 44 | 45 | if !autoApprove { 46 | question := fmt.Sprintf("Are you sure want to delete quayRoot directory %s and all storage data? [y/n]", quayRoot) 47 | fmt.Println(question) 48 | autoApprove = getApproval(question) 49 | if !autoApprove { 50 | log.Info("Skipping deletion of quayRoot.") 51 | } 52 | } 53 | 54 | // Load execution environment 55 | err = loadExecutionEnvironment() 56 | check(err) 57 | 58 | err = loadSSHKeys() 59 | check(err) 60 | 61 | // Set askBecomePass flag if true 62 | var askBecomePassFlag string 63 | if askBecomePass { 64 | askBecomePassFlag = "-K" 65 | } 66 | 67 | log.Printf("Running uninstall playbook. This may take some time. To see playbook output run the installer with -v (verbose) flag.") 68 | podmanCmd := fmt.Sprintf(`podman run `+ 69 | `--rm --interactive --tty `+ 70 | `--workdir /runner/project `+ 71 | `--net host `+ 72 | ` -v %s:/runner/env/ssh_key `+ 73 | `-e RUNNER_OMIT_EVENTS=False `+ 74 | `-e RUNNER_ONLY_FAILED_EVENTS=False `+ 75 | `-e ANSIBLE_HOST_KEY_CHECKING=False `+ 76 | `-e ANSIBLE_CONFIG=/runner/project/ansible.cfg `+ 77 | fmt.Sprintf("-e ANSIBLE_NOCOLOR=%t ", noColor)+ 78 | `--quiet `+ 79 | `--name ansible_runner_instance `+ 80 | fmt.Sprintf("%s ", eeImage)+ 81 | `ansible-playbook -i %s@%s, --private-key /runner/env/ssh_key uninstall_mirror_appliance.yml -e "quay_root=%s quay_storage=%s sqlite_storage=%s auto_approve=%t" %s %s`, 82 | sshKey, targetUsername, strings.Split(targetHostname, ":")[0], quayRoot, quayStorage, sqliteStorage, autoApprove, askBecomePassFlag, additionalArgs) 83 | 84 | log.Debug("Running command: " + podmanCmd) 85 | cmd := exec.Command("bash", "-c", podmanCmd) 86 | if verbose { 87 | cmd.Stderr = os.Stderr 88 | cmd.Stdout = os.Stdout 89 | } 90 | cmd.Stdin = os.Stdin 91 | err = cmd.Run() 92 | check(err) 93 | 94 | log.Printf("Quay uninstalled successfully") 95 | } 96 | -------------------------------------------------------------------------------- /cmd/upgrade.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "strconv" 10 | "strings" 11 | 12 | _ "github.com/lib/pq" // pg driver 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // upgradeCmd represents the upgrade command 17 | var upgradeCmd = &cobra.Command{ 18 | Use: "upgrade", 19 | Short: "Upgrade all mirror registry images.", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | upgrade() 22 | }, 23 | } 24 | 25 | func init() { 26 | 27 | // Add upgrade command 28 | rootCmd.AddCommand(upgradeCmd) 29 | 30 | upgradeCmd.Flags().StringVarP(&targetHostname, "targetHostname", "H", getFQDN(), "The hostname of the target you wish to install Quay to. This defaults to $HOST") 31 | upgradeCmd.Flags().StringVarP(&targetUsername, "targetUsername", "u", os.Getenv("USER"), "The user on the target host which will be used for SSH. This defaults to $USER") 32 | upgradeCmd.Flags().StringVarP(&sshKey, "ssh-key", "k", os.Getenv("HOME")+"/.ssh/quay_installer", "The path of your ssh identity key. This defaults to ~/.ssh/quay_installer") 33 | 34 | upgradeCmd.Flags().StringVarP(&quayHostname, "quayHostname", "", "", "The value to set SERVER_HOSTNAME in the Quay config.yaml. This defaults to :8443") 35 | 36 | upgradeCmd.Flags().StringVarP(&imageArchivePath, "image-archive", "i", "", "An archive containing images") 37 | upgradeCmd.Flags().BoolVarP(&askBecomePass, "askBecomePass", "", false, "Whether or not to ask for sudo password during SSH connection.") 38 | upgradeCmd.Flags().StringVarP(&quayRoot, "quayRoot", "r", "~/quay-install", "The folder where quay persistent data are saved. This defaults to ~/quay-install") 39 | upgradeCmd.Flags().StringVarP(&quayStorage, "quayStorage", "", "quay-storage", "The folder where quay persistent storage data is saved. This defaults to a Podman named volume 'quay-storage'. Root is required to uninstall.") 40 | upgradeCmd.Flags().StringVarP(&sqliteStorage, "sqliteStorage", "", "sqlite-storage", "The folder where quay sqlite data is saved. This defaults to a Podman named volume 'sqlite-storage'. Root is required to uninstall.") 41 | upgradeCmd.Flags().StringVarP(&additionalArgs, "additionalArgs", "", "", "Additional arguments you would like to append to the ansible-playbook call. Used mostly for development.") 42 | 43 | } 44 | 45 | func upgrade() { 46 | 47 | var err error 48 | log.Printf("Upgrade has begun") 49 | 50 | log.Debug("Ansible Execution Environment Image: " + eeImage) 51 | log.Debug("Pause Image: " + pauseImage) 52 | log.Debug("Quay Image: " + quayImage) 53 | log.Debug("Redis Image: " + redisImage) 54 | 55 | // Load execution environment 56 | err = loadExecutionEnvironment() 57 | check(err) 58 | 59 | // Set quayHostname if not already set 60 | if quayHostname == "" { 61 | quayHostname = targetHostname + ":8443" 62 | } 63 | 64 | // Check that SSH key is present, and generate if not 65 | err = loadSSHKeys() 66 | check(err) 67 | 68 | var sqliteArchiveMountFlag string 69 | // Load sqlite cli binary required for migrating from postgres to sqlite 70 | sqliteArchiveMountFlag, err = loadSqliteCli() 71 | check(err) 72 | 73 | // Handle Image Archive Defaulting 74 | var imageArchiveMountFlag string 75 | if imageArchivePath == "" { 76 | executableDir, err := os.Executable() 77 | check(err) 78 | defaultArchivePath := path.Join(path.Dir(executableDir), "image-archive.tar") 79 | if pathExists(defaultArchivePath) { 80 | imageArchivePath = defaultArchivePath 81 | } 82 | } else { 83 | if !pathExists(imageArchivePath) { 84 | check(errors.New("Could not find image-archive.tar at " + imageArchivePath)) 85 | } 86 | } 87 | 88 | if imageArchivePath != "" { 89 | imageArchiveMountFlag = fmt.Sprintf("-v %s:/runner/image-archive.tar", imageArchivePath) 90 | log.Info("Found image archive at " + imageArchivePath) 91 | if isLocalInstall() { 92 | log.Printf("Unpacking image archive from %s", imageArchivePath) 93 | cmd := exec.Command("tar", "-xvf", imageArchivePath) 94 | if verbose { 95 | cmd.Stderr = os.Stderr 96 | cmd.Stdout = os.Stdout 97 | } 98 | err = cmd.Run() 99 | check(err) 100 | 101 | // Load Pause image 102 | pauseArchivePath := path.Join(path.Dir(executableDir), "pause.tar") 103 | log.Printf("Loading pause image archive from %s", pauseArchivePath) 104 | statement := getImageMetadata("pause", pauseImage, pauseArchivePath) 105 | pauseImport := exec.Command("/bin/bash", "-c", statement) 106 | if verbose { 107 | pauseImport.Stderr = os.Stderr 108 | pauseImport.Stdout = os.Stdout 109 | } 110 | log.Debug("Importing Pause with command: ", pauseImport) 111 | err = pauseImport.Run() 112 | check(err) 113 | 114 | // Load Redis image 115 | redisArchivePath := path.Join(path.Dir(executableDir), "redis.tar") 116 | log.Printf("Loading redis image archive from %s", redisArchivePath) 117 | statement = getImageMetadata("redis", redisImage, redisArchivePath) 118 | redisImport := exec.Command("/bin/bash", "-c", statement) 119 | if verbose { 120 | redisImport.Stderr = os.Stderr 121 | redisImport.Stdout = os.Stdout 122 | } 123 | log.Debug("Importing Redis with command: ", redisImport) 124 | err = redisImport.Run() 125 | check(err) 126 | 127 | // Load Quay image 128 | quayArchivePath := path.Join(path.Dir(executableDir), "quay.tar") 129 | log.Printf("Loading Quay image archive from %s", quayArchivePath) 130 | statement = getImageMetadata("quay", quayImage, quayArchivePath) 131 | quayImport := exec.Command("/bin/bash", "-c", statement) 132 | if verbose { 133 | quayImport.Stderr = os.Stderr 134 | quayImport.Stdout = os.Stdout 135 | } 136 | log.Debug("Importing Quay with command: ", quayImport) 137 | err = quayImport.Run() 138 | check(err) 139 | } 140 | log.Infof("Attempting to set SELinux rules on image archive") 141 | cmd := exec.Command("chcon", "-Rt", "svirt_sandbox_file_t", imageArchivePath) 142 | if verbose { 143 | cmd.Stderr = os.Stderr 144 | cmd.Stdout = os.Stdout 145 | } 146 | if err := cmd.Run(); err != nil { 147 | log.Warn("Could not set SELinux rule. If your system does not have SELinux enabled, you may ignore this.") 148 | } 149 | } 150 | 151 | // Set quayHostname if not already set 152 | if quayHostname == "" { 153 | quayHostname = targetHostname + ":8443" 154 | } 155 | 156 | // Add port if not present 157 | if !strings.Contains(quayHostname, ":") { 158 | quayHostname = quayHostname + ":8443" 159 | } 160 | 161 | // Set askBecomePass flag if true 162 | var askBecomePassFlag string 163 | if askBecomePass { 164 | askBecomePassFlag = "-K" 165 | } 166 | 167 | // Run playbook 168 | log.Printf("Running upgrade playbook. This may take some time. To see playbook output run the installer with -v (verbose) flag.") 169 | quayVersion := strings.Split(quayImage, ":")[1] 170 | podmanCmd := fmt.Sprintf(`podman run `+ 171 | `--rm --interactive --tty `+ 172 | `--workdir /runner/project `+ 173 | `--net host `+ 174 | imageArchiveMountFlag+ // optional image archive flag 175 | sqliteArchiveMountFlag+ 176 | ` -v %s:/runner/env/ssh_key `+ 177 | `-e RUNNER_OMIT_EVENTS=False `+ 178 | `-e RUNNER_ONLY_FAILED_EVENTS=False `+ 179 | `-e ANSIBLE_HOST_KEY_CHECKING=False `+ 180 | `-e ANSIBLE_CONFIG=/runner/project/ansible.cfg `+ 181 | fmt.Sprintf("-e ANSIBLE_NOCOLOR=%t ", noColor)+ 182 | `--quiet `+ 183 | `--name ansible_runner_instance `+ 184 | fmt.Sprintf("%s ", eeImage)+ 185 | `ansible-playbook -i %s@%s, --private-key /runner/env/ssh_key -e "quay_image=%s quay_version=%s redis_image=%s sqlite_image=%s pause_image=%s quay_hostname=%s local_install=%s quay_root=%s quay_storage=%s sqlite_storage=%s" upgrade_mirror_appliance.yml %s %s`, 186 | sshKey, targetUsername, targetHostname, quayImage, quayVersion, redisImage, sqliteImage, pauseImage, quayHostname, strconv.FormatBool(isLocalInstall()), quayRoot, quayStorage, sqliteStorage, askBecomePassFlag, additionalArgs) 187 | 188 | log.Debug("Running command: " + podmanCmd) 189 | cmd := exec.Command("bash", "-c", podmanCmd) 190 | cmd.Stderr = os.Stderr 191 | cmd.Stdout = os.Stdout 192 | cmd.Stdin = os.Stdin 193 | err = cmd.Run() 194 | check(err) 195 | 196 | log.Printf("Quay upgraded successfully") 197 | } 198 | -------------------------------------------------------------------------------- /cmd/utils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "strings" 13 | ) 14 | 15 | // This variable is set at build time via ldflags 16 | var sqliteImage string 17 | 18 | func loadExecutionEnvironment() error { 19 | 20 | // Ensure execution environment is present 21 | executableDir, err := os.Executable() 22 | if err != nil { 23 | return err 24 | } 25 | executionEnvironmentPath := path.Join(path.Dir(executableDir), "execution-environment.tar") 26 | if !pathExists(executionEnvironmentPath) { 27 | return errors.New("Could not find execution-environment.tar at " + executionEnvironmentPath) 28 | } 29 | log.Info("Found execution environment at " + executionEnvironmentPath) 30 | 31 | // Load execution environment into podman 32 | log.Printf("Loading execution environment from execution-environment.tar") 33 | statement := getImageMetadata("ansible", eeImage, executionEnvironmentPath) 34 | cmd := exec.Command("/bin/bash", "-c", statement) 35 | if verbose { 36 | cmd.Stderr = os.Stderr 37 | cmd.Stdout = os.Stdout 38 | } 39 | log.Debug("Importing execution environment with command: ", cmd) 40 | 41 | err = cmd.Run() 42 | if err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | func isLocalInstall() bool { 49 | if targetHostname == "localhost" || targetHostname == getFQDN() && targetUsername == os.Getenv("USER") { 50 | log.Infof("Detected an installation to localhost") 51 | return true 52 | } 53 | return false 54 | } 55 | 56 | func loadSSHKeys() error { 57 | if sshKey == os.Getenv("HOME")+"/.ssh/quay_installer" && isLocalInstall() { 58 | if pathExists(sshKey) { 59 | log.Info("Found SSH key at " + sshKey) 60 | } else { 61 | log.Info("Did not find SSH key in default location. Attempting to set up SSH keys.") 62 | if err := setupLocalSSH(); err != nil { 63 | return err 64 | } 65 | log.Info("Successfully set up SSH keys") 66 | } 67 | } else { 68 | if !pathExists(sshKey) { 69 | return errors.New("Could not find ssh key at " + sshKey) 70 | } else { 71 | log.Info("Found SSH key at " + sshKey) 72 | } 73 | } 74 | setSELinux(sshKey) 75 | 76 | return nil 77 | } 78 | 79 | func setupLocalSSH() error { 80 | 81 | log.Infof("Generating SSH Key") 82 | cmd := exec.Command("bash", "-c", "ssh-keygen -b 2048 -t rsa -N '' -f ~/.ssh/quay_installer") 83 | if verbose { 84 | cmd.Stderr = os.Stderr 85 | cmd.Stdout = os.Stdout 86 | } 87 | 88 | if err := cmd.Run(); err != nil { 89 | return err 90 | } 91 | log.Infof("Generated SSH Key at " + os.Getenv("HOME") + "/.ssh/quay_installer") 92 | 93 | keyFile, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/quay_installer.pub") 94 | if err != nil { 95 | return err 96 | } 97 | 98 | log.Infof("Adding key to ~/.ssh/authorized_keys") 99 | cmd = exec.Command("bash", "-c", "umask 066 && /bin/echo \""+string(keyFile)+"\" >> ~/.ssh/authorized_keys") 100 | if verbose { 101 | cmd.Stderr = os.Stderr 102 | cmd.Stdout = os.Stdout 103 | } 104 | if err := cmd.Run(); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func loadCerts(certFile, keyFile, hostname string, skipCheck bool) error { 112 | if certFile != "" && keyFile != "" { 113 | log.Info("Loading SSL certificate file " + certFile) 114 | log.Info("Loading SSL key file " + keyFile) 115 | if !skipCheck { 116 | certKey, err := tls.LoadX509KeyPair(certFile, keyFile) 117 | if err != nil { 118 | log.Errorf("Failed loading certificate and key file: %s", err.Error()) 119 | return err 120 | } 121 | 122 | cert, err := x509.ParseCertificate(certKey.Certificate[0]) 123 | if err != nil { 124 | log.Errorf("Failed parsing certificate file: %s", err.Error()) 125 | return err 126 | } 127 | 128 | roots := x509.NewCertPool() 129 | // Allow self-signed certificate and do not check the issuer 130 | roots.AddCert(cert) 131 | 132 | opts := x509.VerifyOptions{ 133 | DNSName: hostname, 134 | Roots: roots, 135 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, 136 | } 137 | 138 | _, err = cert.Verify(opts) 139 | if err != nil { 140 | log.Errorf("Failed verifying certificate: %s", err.Error()) 141 | return err 142 | } 143 | log.Info("SSL certificate check succeeded") 144 | } 145 | 146 | if pathExists(certFile) { 147 | setSELinux(certFile) 148 | } else { 149 | return errors.New("Certificate file not found: " + certFile) 150 | } 151 | 152 | if pathExists(keyFile) { 153 | setSELinux(keyFile) 154 | } else { 155 | return errors.New("Certificate key file not found: " + keyFile) 156 | } 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func setSELinux(path string) { 163 | log.Infof("Attempting to set SELinux rules on " + path) 164 | cmd := exec.Command("chcon", "-Rt", "svirt_sandbox_file_t", path) 165 | if verbose { 166 | cmd.Stderr = os.Stderr 167 | cmd.Stdout = os.Stdout 168 | } 169 | if err := cmd.Run(); err != nil { 170 | log.Warn("Could not set SELinux rule. If your system does not have SELinux enabled, you may ignore this.") 171 | } 172 | } 173 | 174 | func pathExists(path string) bool { 175 | _, err := os.Stat(path) 176 | return !os.IsNotExist(err) 177 | } 178 | 179 | func check(err error) { 180 | if err != nil { 181 | log.Errorf("An error occurred: %s", err.Error()) 182 | os.Exit(1) 183 | } 184 | } 185 | 186 | func loadSqliteCli() (string, error) { 187 | // Ensure execution environment is present 188 | executableDir, err := os.Executable() 189 | if err != nil { 190 | return "", err 191 | } 192 | sqliteArchivePath := path.Join(path.Dir(executableDir), "sqlite3.tar") 193 | if !pathExists(sqliteArchivePath) { 194 | return "", errors.New("Could not find sqlite3.tar at " + sqliteArchivePath) 195 | } 196 | log.Info("Found sqlite3 cli binary at " + sqliteArchivePath) 197 | 198 | sqliteArchiveMountFlag := fmt.Sprintf(" -v %s:/runner/sqlite3.tar", sqliteArchivePath) 199 | 200 | if isLocalInstall() { 201 | // Load sqlite3 as a podman image 202 | log.Printf("Loading sqlite3 cli binary from sqlite3.tar") 203 | statement := getImageMetadata("sqlite", sqliteImage, sqliteArchivePath) 204 | sqliteImportCmd := exec.Command("/bin/bash", "-c", statement) 205 | if verbose { 206 | sqliteImportCmd.Stderr = os.Stderr 207 | sqliteImportCmd.Stdout = os.Stdout 208 | } 209 | log.Debug("Importing sqlite3 cli binary with command: ", sqliteImportCmd) 210 | err = sqliteImportCmd.Run() 211 | if err != nil { 212 | return "", err 213 | } 214 | } 215 | log.Infof("Attempting to set SELinux rules on sqlite archive") 216 | cmd := exec.Command("chcon", "-Rt", "svirt_sandbox_file_t", sqliteArchivePath) 217 | if verbose { 218 | cmd.Stderr = os.Stderr 219 | cmd.Stdout = os.Stdout 220 | } 221 | if err := cmd.Run(); err != nil { 222 | log.Warn("Could not set SELinux rule. If your system does not have SELinux enabled, you may ignore this.") 223 | } 224 | return sqliteArchiveMountFlag, nil 225 | } 226 | 227 | // getImageMetadata provides the metadata needed for a corresponding image 228 | func getImageMetadata(app, imageName, archivePath string) string { 229 | var statement string 230 | 231 | switch app { 232 | case "pause": 233 | statement = `/usr/bin/podman image import \ 234 | --change 'ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 235 | --change 'ENV container=oci' \ 236 | --change 'ENTRYPOINT=["sleep"]' \ 237 | --change 'CMD=["infinity"]' \ 238 | - ` + imageName + ` < ` + archivePath 239 | case "sqlite": 240 | statement = `/usr/bin/podman image import \ 241 | --change 'ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 242 | --change 'ENV container=oci' \ 243 | --change 'ENTRYPOINT=["/usr/bin/sqlite3"]' \ 244 | - ` + imageName + ` < ` + archivePath 245 | case "ansible": 246 | statement = `/usr/bin/podman image import \ 247 | --change 'ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 248 | --change 'ENV HOME=/home/runner' \ 249 | --change 'ENV container=oci' \ 250 | --change 'ENTRYPOINT=["entrypoint"]' \ 251 | --change 'WORKDIR=/runner' \ 252 | --change 'EXPOSE=6379' \ 253 | --change 'VOLUME=/runner' \ 254 | --change 'CMD ["ansible-runner", "run", "/runner"]' \ 255 | - ` + imageName + ` < ` + archivePath 256 | case "redis": 257 | statement = `/usr/bin/podman image import \ 258 | --change 'ENV PATH=/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 259 | --change 'ENV container=oci' \ 260 | --change 'ENV STI_SCRIPTS_URL=image:///usr/libexec/s2i' \ 261 | --change 'ENV STI_SCRIPTS_PATH=/usr/libexec/s2i' \ 262 | --change 'ENV APP_ROOT=/opt/app-root' \ 263 | --change 'ENV HOME=/var/lib/redis' \ 264 | --change 'ENV PLATFORM=el8' \ 265 | --change 'ENV REDIS_VERSION=6' \ 266 | --change 'ENV CONTAINER_SCRIPTS_PATH=/usr/share/container-scripts/redis' \ 267 | --change 'ENV REDIS_PREFIX=/usr' \ 268 | --change 'ENV REDIS_CONF=/etc/redis.conf' \ 269 | --change 'ENTRYPOINT=["container-entrypoint"]' \ 270 | --change 'USER=1001' \ 271 | --change 'WORKDIR=/opt/app-root/src' \ 272 | --change 'EXPOSE=6379' \ 273 | --change 'VOLUME=/var/lib/redis/data' \ 274 | --change 'CMD ["run-redis"]' \ 275 | - ` + imageName + ` < ` + archivePath 276 | case "quay": 277 | // quay.io 278 | statement = `/usr/bin/podman image import \ 279 | --change 'ENV container=oci' \ 280 | --change 'ENV PATH=/app/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 281 | --change 'ENV PYTHONUNBUFFERED=1' \ 282 | --change 'ENV PYTHONIOENCODING=UTF-8' \ 283 | --change 'ENV LC_ALL=C.UTF-8' \ 284 | --change 'ENV LANG=C.UTF-8' \ 285 | --change 'ENV QUAYDIR=/quay-registry' \ 286 | --change 'ENV QUAYCONF=/quay-registry/conf' \ 287 | --change 'ENV QUAYRUN=/quay-registry/conf' \ 288 | --change 'ENV QUAYPATH=/quay-registry' \ 289 | --change 'ENV PYTHONUSERBASE=/app' \ 290 | --change 'ENV PYTHONPATH=/quay-registry' \ 291 | --change 'ENV TZ=UTC' \ 292 | --change 'ENV RED_HAT_QUAY=true' \ 293 | --change 'ENTRYPOINT=["dumb-init","--","/quay-registry/quay-entrypoint.sh"]' \ 294 | --change 'WORKDIR=/quay-registry' \ 295 | --change 'EXPOSE=7443' \ 296 | --change 'EXPOSE=8080' \ 297 | --change 'EXPOSE=8443' \ 298 | --change 'VOLUME=/conf/stack' \ 299 | --change 'VOLUME=/datastorage' \ 300 | --change 'VOLUME=/sqlite' \ 301 | --change 'VOLUME=/tmp' \ 302 | --change 'VOLUME=/var/log' \ 303 | --change 'USER=1001' \ 304 | --change 'CMD ["registry"]' \ 305 | - ` + imageName + ` < ` + archivePath 306 | } 307 | 308 | return statement 309 | } 310 | 311 | // checkInput validates user input against available options 312 | func getApproval(question string) bool { 313 | var response string 314 | _, err := fmt.Scanln(&response) 315 | if err != nil { 316 | log.Fatal(err) 317 | } 318 | 319 | switch strings.ToLower(response) { 320 | case "y": 321 | return true 322 | case "n": 323 | return false 324 | default: 325 | fmt.Println("Invalid input.", question) 326 | return getApproval(question) 327 | } 328 | } 329 | 330 | func getFQDN() string { 331 | fqdn, err := exec.Command("hostname", "-f").Output() 332 | if err != nil { 333 | errorMessage := "Failed to automatically acquire host FQDN, please set manually with --targetHostname. " 334 | log.Fatal(errorMessage, err) 335 | } 336 | 337 | return strings.TrimSuffix(string(fqdn), "\n") 338 | } 339 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quay/mirror-registry 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/lib/pq v1.10.0 7 | github.com/sethvargo/go-password v0.2.0 8 | github.com/sirupsen/logrus v1.8.1 9 | github.com/spf13/cobra v1.1.3 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/spf13/pflag v1.0.5 // indirect 15 | github.com/stretchr/testify v1.7.0 // indirect 16 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 17 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 19 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 20 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 21 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 23 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 24 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 25 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 26 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 28 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 29 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 30 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 31 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 32 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 38 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 39 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 40 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 41 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 42 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 43 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 44 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 45 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 46 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 47 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 48 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 49 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 50 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 51 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 52 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 53 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 54 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 55 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 56 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 57 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 58 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 59 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 60 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 61 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 62 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 63 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 64 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 65 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 66 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 67 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 68 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 69 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 70 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 71 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 72 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 73 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 74 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 75 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 76 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 77 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 78 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 79 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 80 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 81 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 82 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 83 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 84 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 85 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 86 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 87 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 88 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 89 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 90 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 91 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 92 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 93 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 94 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 95 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 96 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 97 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 98 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 99 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 100 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 101 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 102 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 103 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 104 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 105 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 106 | github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= 107 | github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 108 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 109 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 110 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 111 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 112 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 113 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 114 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 115 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 116 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 117 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 118 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 119 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 120 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 121 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 122 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 123 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 124 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 125 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 126 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 127 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 128 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 129 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 130 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 131 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 132 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 133 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 134 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 135 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 136 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 137 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 138 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 139 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 140 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 141 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 142 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 143 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 144 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 145 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 146 | github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= 147 | github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= 148 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 149 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 150 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 151 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 152 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 153 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 154 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 155 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 156 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 157 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 158 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= 159 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 160 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 161 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 162 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 163 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 164 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 165 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 166 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 167 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 168 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 169 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 170 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 171 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 172 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 173 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 174 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 175 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 176 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 177 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 178 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 179 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 180 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 181 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 182 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 183 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 184 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 185 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 186 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 187 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 188 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 189 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 190 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 191 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 192 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 193 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 194 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 195 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 196 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 197 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 198 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 199 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 200 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 201 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 202 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 203 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 204 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 205 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 206 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 207 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 208 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 209 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 213 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 214 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 215 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 216 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 217 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 218 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 219 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 220 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 226 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 227 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 232 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 240 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 242 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 243 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 244 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 245 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 246 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 247 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 248 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 249 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 250 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 251 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 252 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 253 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 254 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 255 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 256 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 257 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 258 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 259 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 260 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 261 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 262 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 263 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 264 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 265 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 266 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 267 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 268 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 269 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 270 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 271 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 272 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 273 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 274 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 275 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 276 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 277 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 278 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 279 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 280 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 281 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 282 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 283 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 284 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 285 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 286 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 288 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 289 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 290 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 291 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 292 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 293 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 294 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 295 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 296 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 297 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 298 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 299 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 300 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 301 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 302 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 303 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quay/mirror-registry/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # TREEISH is the hash of the current repository content including uncommitted 2 | # changes, but not untracked files. 3 | ifndef TREEISH 4 | export TREEISH := $(shell tmpdir=$$(mktemp -d); export GIT_INDEX_FILE="$$tmpdir/index"; trap 'rm -rf $$tmpdir' EXIT; cp "$$(git rev-parse --git-dir)/index" "$$GIT_INDEX_FILE" && git add -u && git write-tree) 5 | endif 6 | 7 | # VERSION is the version of mirror-registry that should be installed. By 8 | # default a build from the current repository is used. 9 | VERSION := dev-$(TREEISH) 10 | 11 | # OLD_VERSION is the version of mirror-registry that upgrade should be tested 12 | # from. 13 | OLD_VERSION := 1.2.9 14 | 15 | all: test-install test-sudo-install test-sudo-upgrade 16 | 17 | # mirror-registry archive from the current repository. 18 | mirror-registry-dev-$(TREEISH).tar.gz: 19 | # Building $@ 20 | $(MAKE) -C .. build-offline-zip 21 | mv ../mirror-registry.tar.gz $@ 22 | 23 | # released mirror-registry archive. 24 | mirror-registry-%.tar.gz: 25 | wget -O $@ https://developers.redhat.com/content-gateway/file/pub/openshift-v4/clients/mirror-registry/$*/mirror-registry.tar.gz 26 | 27 | # inside-vagrant runs a command inside the Vagrant VM. 28 | # 29 | # The virtual machine will be starter before the command is run and stopped 30 | # afterwards. 31 | # 32 | # Example: 33 | # $(call inside-vagrant,vagrant ssh -c 'uname -a') 34 | define inside-vagrant 35 | $(MAKE) start-vagrant && (set -x; $(1)); ret=$$?; $(MAKE) stop-vagrant && exit $$ret 36 | endef 37 | 38 | start-vagrant: 39 | vagrant up 40 | 41 | stop-vagrant: 42 | ifneq ($(DEBUG), 1) 43 | vagrant destroy -f 44 | endif 45 | 46 | # vagrant-unpack uploads and unpacks the mirror-registry archive into the 47 | # Vagrant VM. 48 | vagrant-unpack: mirror-registry-$(VERSION).tar.gz 49 | vagrant upload mirror-registry-$(VERSION).tar.gz 50 | vagrant ssh -c "tar -vxf mirror-registry-$(VERSION).tar.gz" 51 | 52 | vagrant-install: vagrant-unpack 53 | vagrant ssh -c "./mirror-registry install -v --initPassword password --quayHostname localhost" 54 | vagrant ssh -c "podman login -u init -p password localhost:8443 --tls-verify=false" 55 | 56 | vagrant-sudo-install: vagrant-unpack 57 | vagrant ssh -c "sudo ./mirror-registry install -v --initPassword password --quayHostname localhost" 58 | vagrant ssh -c "podman login -u init -p password localhost:8443 --tls-verify=false" 59 | 60 | vagrant-sudo-upgrade: 61 | $(MAKE) vagrant-sudo-install VERSION=$(OLD_VERSION) 62 | $(MAKE) vagrant-unpack 63 | vagrant ssh -c "sudo ./mirror-registry upgrade -v --quayHostname localhost" 64 | vagrant ssh -c "podman login -u init -p password localhost:8443 --tls-verify=false" 65 | 66 | # test-install is an end-to-end test that installs mirror-registry. 67 | # Version can be specified with VERSION. 68 | # Use DEBUG=1 to prevent the Vagrant VM from being destroyed after the test. 69 | # 70 | # This target has an explicit dependency on the archive target so that the 71 | # archive is built/downloaded before the virtual machine is started. 72 | test-install: mirror-registry-$(VERSION).tar.gz 73 | $(call inside-vagrant,$(MAKE) vagrant-install) 74 | @echo "$@: OK" 75 | 76 | test-sudo-install: mirror-registry-$(VERSION).tar.gz 77 | $(call inside-vagrant,$(MAKE) vagrant-sudo-install) 78 | @echo "$@: OK" 79 | 80 | test-sudo-upgrade: mirror-registry-$(OLD_VERSION).tar.gz mirror-registry-$(VERSION).tar.gz 81 | $(call inside-vagrant,$(MAKE) vagrant-sudo-upgrade) 82 | @echo "$@: OK" 83 | -------------------------------------------------------------------------------- /test/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "generic/fedora37" 6 | config.vm.network "forwarded_port", guest: 8080, host: 8080 7 | config.vm.network "forwarded_port", guest: 8443, host: 8443 8 | config.vm.provider "virtualbox" do |vb| 9 | vb.memory = "3072" 10 | end 11 | config.vm.provision "shell", inline: <<-SHELL 12 | dnf install -y \ 13 | acl \ 14 | openssl \ 15 | podman 16 | SHELL 17 | end 18 | --------------------------------------------------------------------------------