diff --git a/.github/workflows/deploy.template.yml b/.github/workflows/deploy.template.yml index 8f572e8..64f46f4 100644 --- a/.github/workflows/deploy.template.yml +++ b/.github/workflows/deploy.template.yml @@ -38,7 +38,6 @@ jobs: - name: Install and configure poetry run: | pipx install poetry - poetry config virtualenvs.create false - if: github.event.inputs.cluster == 'prod' run: | @@ -51,19 +50,40 @@ jobs: - name: Install package run: poetry install - - name: Install jq for fingerprinter - run: sudo apt-get -y install jq - - name: Update env with promotion version that was provided if: github.event.inputs.version run: echo "target_version=${{ github.event.inputs.version }}" >> $GITHUB_ENV - name: Update env with promotion version if not provided if: '! env.target_version' + shell: bash run: | - source ./scripts/globals.sh - target_version=$(get_promotion_version ${{ github.event.inputs.cluster }}) - echo "target_version=${target_version}" >> $GITHUB_ENV + # $1 will be package name, $2 will be current version + set $(poetry version) + + case "${{ github.event.inputs.cluster }}" in + dev) + version="$2" + echo "Would set version to '$version' (from poetry version)" + ;; + eval) + # substitute '_' for '-' in APP_NAME + url="https://${1//_/-}.iamdev.s.uw.edu/status" + version=$(curl --silent $url | python -c "import json, sys; print(json.load(sys.stdin)['version'])") + echo "After consulting dev the eval version will be ${version}" + ;; + prod) + # substitute '_' for '-' in APP_NAME + url="https://${1//_/-}.iameval.s.uw.edu/status" + version=$(curl --silent $url | python -c "import json, sys; print(json.load(sys.stdin)['version'])") + echo "After consulting eval the prod version will be ${version}" + ;; + *) + echo "Invalid cluster! Pick one of dev|eval|prod" + exit 1 + ;; + esac + echo "target_version=${version}" >> $GITHUB_ENV - name: Auth to Google Cloud # important! this 'auth' is referenced as `steps.auth` on the next job @@ -80,10 +100,14 @@ jobs: run: |- echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://us-docker.pkg.dev - - name: Deploy version ${{ env.target_version }} + - name: Tag version ${{ env.target_version }} for ${{ github.event.inputs.cluster }} id: deploy run: | - echo "::notice::Deploying appid version ${{ env.target_version }} to ${{ github.event.inputs.cluster }}" - ./scripts/build.sh \ - --deploy ${{ inputs.cluster }} \ - -dversion ${{ env.target_version }} + # timestamp and deploy_tag are not DRY - see also release-on-push-to-main.yaml + timestamp=$(date --utc +%Y.%m.%d.%H.%M.%S) + deploy_tag="deploy-${{ github.event.inputs.cluster }}.${timestamp}.v${{ env.target_version }}" + echo "::notice::Deploying appid version ${{ env.target_version }} to ${{ github.event.inputs.cluster }} as ${deploy_tag}" + # this will create a new tag (deploy_tag) on an existing tag (env.target_version) + docker buildx imagetools create \ + --tag us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}:${deploy_tag} \ + us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}:${{ env.target_version }} diff --git a/.github/workflows/finalize-template.yaml b/.github/workflows/finalize-template.yaml index e626546..6bd3030 100644 --- a/.github/workflows/finalize-template.yaml +++ b/.github/workflows/finalize-template.yaml @@ -3,7 +3,7 @@ name: Finalize Template on: workflow_dispatch: inputs: - app-name: + app_name: required: false description: > This is the name of your app; if left blank, it will match the diff --git a/.github/workflows/pull-request.template.yml b/.github/workflows/pull-request.template.yml index c10fcfd..a189976 100644 --- a/.github/workflows/pull-request.template.yml +++ b/.github/workflows/pull-request.template.yml @@ -71,7 +71,7 @@ jobs: file: ./Dockerfile push: true target: dependencies - tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}.dependencies:${{ env.pr_tag }} + tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}.dependencies:${{ env.pr_tag }} secret-files: | "gcloud_auth_credentials=${{ steps.auth.outputs.credentials_file_path }}" @@ -82,7 +82,7 @@ jobs: file: ./Dockerfile push: true target: app - tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}.app:${{ env.pr_tag }} + tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}.app:${{ env.pr_tag }} - name: Build and push Docker image (tests) uses: docker/build-push-action@v5 @@ -91,11 +91,11 @@ jobs: file: ./Dockerfile push: true target: tests - tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}.tests:${{ env.pr_tag }} + tags: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}.tests:${{ env.pr_tag }} - uses: mshick/add-pr-comment@v2 env: - image: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}.app:${{ env.pr_tag }} + image: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}.app:${{ env.pr_tag }} with: repo-token: ${{ secrets.GITHUB_TOKEN }} allow-repeats: false diff --git a/.github/workflows/release-on-push-to-main.template.yaml b/.github/workflows/release-on-push-to-main.template.yaml index 32ba63a..7926409 100644 --- a/.github/workflows/release-on-push-to-main.template.yaml +++ b/.github/workflows/release-on-push-to-main.template.yaml @@ -56,17 +56,14 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v5 - env: - DEPLOYMENT_ID: deploy-dev.${{ steps.get-version.outputs.timestamp }}.v${{ steps.get-version.outputs.version }} with: - build-args: DEPLOYMENT_ID=${{ env.DEPLOYMENT_ID }} context: . file: ./Dockerfile push: true target: app tags: | - us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}:${{ steps.get-version.outputs.version }} - us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name}:${{ env.DEPLOYMENT_ID }} + us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}:${{ steps.get-version.outputs.version }} + us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}:deploy-dev.${{ steps.get-version.outputs.timestamp }}.v${{ steps.get-version.outputs.version }} + us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen}:latest secret-files: | "gcloud_auth_credentials=${{ steps.auth.outputs.credentials_file_path }}" - diff --git a/Dockerfile b/Dockerfile.template similarity index 85% rename from Dockerfile rename to Dockerfile.template index d8cd1b4..92bc25c 100644 --- a/Dockerfile +++ b/Dockerfile.template @@ -13,14 +13,13 @@ RUN --mount=type=secret,id=gcloud_auth_credentials \ poetry install --only main --no-root --no-interaction FROM dependencies AS app - -ARG DEPLOYMENT_ID -ARG APP_MODULE=example_app +# If you change your app directory to e.g., use src/ you MUST +# change the APP_MODULE here to match OR supply a --build-arg +ARG APP_MODULE=${template:app_name_underscore} ARG FLASK_PORT=5000 ENV FLASK_ENV=development \ PYTHONPATH=${APP_MODULE} \ - FLASK_APP=${APP_MODULE}.app \ - DEPLOYMENT_ID=${DEPLOYMENT_ID} + FLASK_APP=${APP_MODULE}.app EXPOSE ${FLASK_PORT} COPY ${APP_MODULE}/ ./${APP_MODULE} # install root package now that we've copied it diff --git a/README.md b/README.md index 6b06ca1..dc59d20 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ click the button above that says "Use this template" You will be asked to name your new repository. To take full advantage of the automation and kubernetes capabilities, you should create your new -repository under the "UWIT-IAM" namespace; this gives you access to +repository under the "UWIT-IAM" namespace; this gives you access to secrets that are consumable in your Github actions. Once you have done that, follow the instructions for @@ -22,12 +22,12 @@ Once you have done that, follow the instructions for If you are reading this, and you are not in the `flask-example` template repository, then you haven't yet finalized your template! -To finalize the template, visit your repository on GitHub, +To finalize the template, visit your repository on GitHub, then click on "Actions." Click the "Finalize Template" workflow, and click "Run this workflow." -A pull request will be generated for you to review and merge! This message will +A pull request will be generated for you to review and merge! This message will self-destruct after running that workflow. ## Updating the template @@ -37,15 +37,15 @@ This very basic templating engine does not allow for conditional logic. To use an argument name inside a document, make sure the document is named `. template.`, the final file name will be `.`. -You can use any supported argument with the format: `${template:}`; all -values are treated as strings. +You can use any supported argument with the format: `${template:}`; all +values are treated as strings. Functionally: ``` # foo.template.yaml -- policy-name: ${template:app_name}-policy +- policy-name: ${template:app_name_hyphen}-policy ``` becomes: @@ -60,8 +60,8 @@ becomes: ### About templating templates... Please note that `.template.` files are interpolated before any other templating -engine does anything; you may freely nest the `${template:}` syntax inside -other strings that other templating engines might use. +engine does anything; you may freely nest the `${template:}` syntax inside +other strings that other templating engines might use. ## Supported Values diff --git a/example_app/app.template.py b/example_app/app.template.py index b2e3118..735bb5c 100644 --- a/example_app/app.template.py +++ b/example_app/app.template.py @@ -1,4 +1,3 @@ -import os import importlib.metadata as importlib_metadata # Python ^3.9 change from flask import Flask, jsonify @@ -9,7 +8,7 @@ @app.route("/", methods=("GET",)) def index(): print("Hello there") - return 'OK', 200 + return "OK", 200 APP_VERSION = None @@ -19,13 +18,15 @@ def index(): def status(): global APP_VERSION if APP_VERSION is None: - APP_VERSION = importlib_metadata.version("${template:app_name}") + try: + APP_VERSION = importlib_metadata.version("${template:app_name_underscore}") + except importlib_metadata.PackageNotFoundError: + "Something went wrong locating the package that is this application. Was it installed via poetry?" - deployment_id = os.environ.get("DEPLOYMENT_ID") - status = 200 if deployment_id else 503 + status = 200 if APP_VERSION else 503 - return jsonify({"deployment_id": deployment_id, "version": APP_VERSION}), status + return jsonify({"version": APP_VERSION}), status if __name__ == "__main__": - app.run(host='0.0.0.0', port=5000) + app.run(host="0.0.0.0", port=5000) diff --git a/fingerprints.template.yaml b/fingerprints.template.yaml deleted file mode 100644 index 72ca842..0000000 --- a/fingerprints.template.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This is the build configuration used by uw-it-build-fingerprinter. -# For more information, refer to the docs at: -# https://github.com/uwit-iam/fingerprinter - -release-target: app -docker: - repository: us-docker.pkg.dev/uwit-mci-iam/containers - app-name: ${template:app_name} - -targets: - dependencies: - include-paths: - - poetry.lock - - Dockerfile - app: - depends-on: [dependencies] - include-paths: - - example_app - - tests: - depends-on: [app] - include-paths: - - test diff --git a/kubernetes-config/README.template.md b/kubernetes-config/README.template.md index 638d422..51facdc 100644 --- a/kubernetes-config/README.template.md +++ b/kubernetes-config/README.template.md @@ -1,13 +1,13 @@ # Template Kubernetes Configuration -The basic kubernetes configuration provided here will subscribe you -to the [basic-web-service helm chart]. +The basic kubernetes configuration provided here will subscribe you +to the [basic-web-service helm chart]. After finalizing your template, you should copy the files in this directory into the [gcp-k8] repository, in the `dev/${template:app_name}` directory. Unless you change the values generated for you, your app will -expect to run at `https://${template:app_name}.iamdev.s.uw.edu`. +expect to run at `https://${template:app_name_hyphen}.iamdev.s.uw.edu`. [gcp-k8]: https://github.com/uwit-iam/gcp-k8 diff --git a/kubernetes-config/automation.template.yaml b/kubernetes-config/automation.template.yaml index f301141..fc9ceb9 100644 --- a/kubernetes-config/automation.template.yaml +++ b/kubernetes-config/automation.template.yaml @@ -4,11 +4,11 @@ apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImagePolicy metadata: - name: ${template:app_name}-policy + name: ${template:app_name_hyphen}-policy namespace: default # must be 'default' in MCI, even if app itself is not in default spec: imageRepositoryRef: - name: ${template:app_name}-gar + name: ${template:app_name_hyphen}-gar filterTags: pattern: '^deploy-dev.(?P[0-9\.]+)\.v.+$' extract: '$ts' @@ -19,10 +19,10 @@ spec: apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository metadata: - name: ${template:app_name}-gar + name: ${template:app_name_hyphen}-gar namespace: default # must be 'default' in MCI, even if app itself is not in default spec: - image: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name} + image: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen} interval: 2m0s secretRef: name: flux-mci-registry-credential diff --git a/kubernetes-config/helm-release.template.yaml b/kubernetes-config/helm-release.template.yaml index 83d6e9c..658ae2a 100644 --- a/kubernetes-config/helm-release.template.yaml +++ b/kubernetes-config/helm-release.template.yaml @@ -9,12 +9,12 @@ apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: - name: ${template:app_name} + name: ${template:app_name_hyphen} namespace: default spec: values: app: - name: ${template:app_name} + name: ${template:app_name_hyphen} clusterDomain: iamdev.s.uw.edu replicaCount: 1 ports: @@ -45,8 +45,8 @@ spec: memory: "64M" cpu: "100m" image: - name: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name} - tag: latest # {"$imagepolicy": "default:${template:app_name}-policy:tag"} + name: us-docker.pkg.dev/uwit-mci-iam/containers/${template:app_name_hyphen} + tag: latest # {"$imagepolicy": "default:${template:app_name_hyphen}-policy:tag"} chart: spec: diff --git a/pyproject.template.toml b/pyproject.template.toml index bce5cde..e803ea4 100644 --- a/pyproject.template.toml +++ b/pyproject.template.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "${template:app_name}" +name = "${template:app_name_underscore}" version = "0.1.0" description = "" authors = [] @@ -23,3 +23,7 @@ priority = "explicit" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.black] +# coordinated with setup.cfg max-line-length +line-length = 119 diff --git a/scripts/finalize-template.py b/scripts/finalize-template.py index 9383655..7cad5da 100644 --- a/scripts/finalize-template.py +++ b/scripts/finalize-template.py @@ -11,16 +11,14 @@ def get_parser(): "Fills out argument templates, then destroys, saves the new files," "and destroys all template files, including this script." ) - parser.add_argument('--app-name', required=True) - parser.add_argument('--maintainer', required=True) - parser.add_argument('--keepalive', required=False, default=False, action='store_true', dest='meta_keepalive') + parser.add_argument("--app-name", required=True) + parser.add_argument("--maintainer", required=True) + parser.add_argument("--keepalive", required=False, default=False, action="store_true", dest="meta_keepalive") return parser def get_template_files(): - return [ - str(path.joinpath()) for path in list(Path('.').rglob('*.template.*')) - ] + return [str(path.joinpath()) for path in list(Path(".").rglob("*.template*"))] def finalize_template_file(path: str, args: Dict[str, Any]): @@ -28,26 +26,25 @@ def finalize_template_file(path: str, args: Dict[str, Any]): template = f.read() for key, val in args.items(): - template = template.replace( - f'${{template:{key}}}', str(val) - ) + template = template.replace(f"${{template:{key}}}", str(val)) - new_file_name = path.replace('.template', '') - with open(new_file_name, 'w') as f: + new_file_name = path.replace(".template", "") + with open(new_file_name, "w") as f: f.write(template) - logging.info( - f"Generated {new_file_name} from {path}" - ) + logging.info(f"Generated {new_file_name} from {path}") return new_file_name def main(): args = get_parser().parse_args() - values = { - k: v for k, v in vars(args).items() - if not k.startswith('meta_') - } + values = {k: v for k, v in vars(args).items() if not k.startswith("meta_")} + # add explicit values for only-hypen and only-underscore variants of the app name + # this is important because + # 1) Python package names cannot contain hyphens when importing ('django-utils' illegal; must be 'django_utils') + # 2) subdomains cannot contain underscores and we seed our application subdomains with the application name + values["app_name_hyphen"] = values["app_name"].replace("_", "-") + values["app_name_underscore"] = values["app_name"].replace("-", "_") logging.basicConfig(level=logging.INFO) logging.getLogger(__name__).setLevel(logging.INFO) @@ -58,16 +55,16 @@ def main(): for t in templates: finalize_template_file(t, values) if not args.meta_keepalive: - logging.info(f'Deleting template {t}') + logging.info(f"Deleting template {t}") os.remove(t) - os.system('poetry update') + os.system("poetry update") - logging.info('Deleting myself, too. My purpose is fulfilled. Byeeeeeeeeee!') + logging.info("Deleting myself, too. My purpose is fulfilled. Byeeeeeeeeee!") os.remove(__file__) - safe_app_name = args.app_name.replace("-", "_") - shutil.move("example_app", safe_app_name) + # this rename tied to Dockerfile.template + shutil.move("example_app", values["app_name_underscore"]) if __name__ == "__main__": diff --git a/scripts/globals.template.sh b/scripts/globals.template.sh deleted file mode 100644 index 6bd0893..0000000 --- a/scripts/globals.template.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -export APP_NAME="${template:app_name}" - -function get_instance_status { - local stage="$1" - # substitute '_' for '-' in APP_NAME - local url="https://${APP_NAME//_/-}.iam${stage}.s.uw.edu/status" - curl -sk "$url" || >&2 echo "Failed to check $url" -} - -function get_poetry_version { - grep 'version =' pyproject.toml | cut -f2 -d\" | head -n1 -} - - -function get_promotion_version { - # provides default values for the application version to deploy - # dev gets current version of app (presumed newest due to CI process) - # otherwise promotes dev => eval or eval => prod, depending on target stage - local target=$1 - case $target in - dev) - get_poetry_version - ;; - eval) - get_instance_status dev | jq -r .version - ;; - prod) - get_instance_status eval | jq -r .version - ;; - esac -} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fa31184 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +# coordinated with pyproject.toml MAX_LINE_LENGTH +max-line-length = 119 diff --git a/tests/conftest.template.py b/tests/conftest.template.py index f366329..5860b4a 100644 --- a/tests/conftest.template.py +++ b/tests/conftest.template.py @@ -1,5 +1,5 @@ import pytest -from ${template:app_name}.app import app as flask_app +from ${template:app_name_underscore}.app import app as flask_app @pytest.fixture