├── src ├── version.txt ├── templates │ ├── TrustedHosts │ ├── opendmarc.conf │ ├── master.cf │ ├── opendkim.conf │ ├── dovecot.conf │ └── main.cf ├── secrets │ ├── users.txt │ ├── fullchain.pem │ └── privkey.pem └── docker-entrypoint.sh ├── requirements.txt ├── pytest.ini ├── requirements-dev.txt ├── .github ├── lineage.yml ├── CODEOWNERS ├── labeler.yml ├── dependabot.yml ├── workflows │ ├── _repo-metadata.yml │ ├── label-prs.yml │ ├── sync-labels.yml │ ├── dependency-review.yml │ ├── update-dockerhub-description.yml │ ├── codeql-analysis.yml │ └── build.yml └── labels.yml ├── trivy.yml ├── requirements-test.txt ├── .prettierignore ├── tag.sh ├── .gitignore ├── .isort.cfg ├── .bandit.yml ├── .ansible-lint ├── compose.yml ├── .flake8 ├── .mdl_config.yaml ├── tests ├── conftest.py └── container_test.py ├── Dockerfile ├── .yamllint ├── bump-version ├── LICENSE ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── README.md └── setup-env /src/version.txt: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | wheel 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose -ra 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | --requirement requirements-test.txt 2 | ipython 3 | pipenv 4 | -------------------------------------------------------------------------------- /src/templates/TrustedHosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 2 | localhost 3 | ${PRIMARY_DOMAIN} 4 | ${RELAY_IP} 5 | -------------------------------------------------------------------------------- /.github/lineage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | lineage: 3 | skeleton: 4 | remote-url: https://github.com/cisagov/skeleton-docker.git 5 | version: "1" 6 | -------------------------------------------------------------------------------- /trivy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Fail if something is flagged 3 | exit-code: 1 4 | # Only flag critical and high vulnerabilities 5 | severity: 6 | - CRITICAL 7 | - HIGH 8 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | --requirement requirements.txt 2 | pre-commit 3 | pytest 4 | python-on-whales 5 | # The bump-version script requires at least version 3 of semver. 6 | semver>=3 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Already being linted by pretty-format-json 2 | *.lock 3 | *.json 4 | # Already being linted by mdl 5 | *.md 6 | # Already being linted by yamllint 7 | *.yaml 8 | *.yml 9 | -------------------------------------------------------------------------------- /tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -o pipefail 6 | 7 | version=$(./bump-version show) 8 | 9 | git tag "v$version" && git push --tags 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file specifies intentionally untracked files that Git should ignore. 2 | # Files already tracked by Git are not affected. 3 | # See: https://git-scm.com/docs/gitignore 4 | 5 | ## Docker ## 6 | Dockerfile-x 7 | 8 | ## Python ## 9 | __pycache__ 10 | .mypy_cache 11 | .pytest_cache 12 | .python-version 13 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | combine_star=true 3 | force_sort_within_sections=true 4 | 5 | import_heading_stdlib=Standard Python Libraries 6 | import_heading_thirdparty=Third-Party Libraries 7 | import_heading_firstparty=cisagov Libraries 8 | 9 | # Run isort under the black profile to align with our other Python linting 10 | profile=black 11 | -------------------------------------------------------------------------------- /src/templates/opendmarc.conf: -------------------------------------------------------------------------------- 1 | AuthservID ${PRIMARY_DOMAIN} 2 | PidFile /var/run/opendmarc/opendmarc.pid 3 | RejectFailures false 4 | Syslog true 5 | TrustedAuthservIDs ${PRIMARY_DOMAIN} 6 | Socket inet:54321@localhost 7 | UMask 0002 8 | UserID opendmarc:opendmarc 9 | IgnoreHosts /etc/opendmarc/ignore.hosts 10 | HistoryFile /var/run/opendmarc/opendmarc.dat 11 | -------------------------------------------------------------------------------- /src/secrets/users.txt: -------------------------------------------------------------------------------- 1 | # Define the users to be created at container startup. 2 | # If is omitted for a user it will be generated and logged at startup 3 | # username 4 | 5 | # The mailarchive user is mandatory since all mail is BCC'd to this user. 6 | mailarchive foobar 7 | 8 | # define other users below as needed 9 | testsender1 lemmy is god 10 | testsender2 11 | -------------------------------------------------------------------------------- /.bandit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Configuration file for the Bandit python security scanner 3 | # https://bandit.readthedocs.io/en/latest/config.html 4 | 5 | # Tests are first included by `tests`, and then excluded by `skips`. 6 | # If `tests` is empty, all tests are considered included. 7 | 8 | tests: 9 | # - B101 10 | # - B102 11 | 12 | skips: 13 | - B101 # skip "assert used" check since assertions are required in pytests 14 | -------------------------------------------------------------------------------- /src/templates/master.cf: -------------------------------------------------------------------------------- 1 | submission inet n - - - - smtpd 2 | -o syslog_name=postfix/submission 3 | -o smtpd_tls_wrappermode=no 4 | -o smtpd_tls_security_level=may 5 | -o smtpd_sasl_auth_enable=yes 6 | -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject 7 | -o milter_macro_daemon_name=ORIGINATING 8 | -o smtpd_sasl_type=dovecot 9 | -o smtpd_sasl_path=private/auth 10 | -------------------------------------------------------------------------------- /src/templates/opendkim.conf: -------------------------------------------------------------------------------- 1 | domain * 2 | AutoRestart Yes 3 | AutoRestartRate 10/1h 4 | Umask 0002 5 | Syslog Yes 6 | SyslogSuccess Yes 7 | LogWhy Yes 8 | Canonicalization relaxed/simple 9 | ExternalIgnoreList refile:/etc/opendkim/TrustedHosts 10 | InternalHosts refile:/etc/opendkim/TrustedHosts 11 | KeyFile /etc/opendkim/keys/${PRIMARY_DOMAIN}/mail.private 12 | Selector mail 13 | Mode sv 14 | PidFile /var/run/opendkim/opendkim.pid 15 | SignatureAlgorithm rsa-sha256 16 | UserID opendkim:opendkim 17 | Socket inet:12301@localhost 18 | -------------------------------------------------------------------------------- /src/templates/dovecot.conf: -------------------------------------------------------------------------------- 1 | auth_mechanisms = plain login 2 | disable_plaintext_auth = no 3 | mail_privileged_group = mail 4 | mail_location = mbox:~/mail:INBOX=/var/mail/%u 5 | userdb { 6 | driver = passwd 7 | } 8 | passdb { 9 | args = %s 10 | driver = pam 11 | } 12 | protocols = " imap" 13 | protocol imap { 14 | mail_plugins = " autocreate" 15 | } 16 | plugin { 17 | autocreate = Trash 18 | autocreate2 = Sent 19 | autosubscribe = Trash 20 | autosubscribe2 = Sent 21 | } 22 | service imap-login { 23 | inet_listener imap { 24 | port = 0 25 | } 26 | inet_listener imaps { 27 | port = 993 28 | } 29 | } 30 | service auth { 31 | unix_listener /var/spool/postfix/private/auth { 32 | group = postfix 33 | mode = 0660 34 | user = postfix 35 | } 36 | } 37 | ssl=required 38 | ssl_cert = while non-official 2 | # images are in the form /. 3 | FROM docker.io/library/debian:bullseye-slim 4 | 5 | 6 | ### 7 | # For a list of pre-defined annotation keys and value types see: 8 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 9 | # 10 | # Note: Additional labels are added by the build workflow. 11 | ### 12 | LABEL org.opencontainers.image.authors="vm-fusion-dev-group@trio.dhs.gov" 13 | LABEL org.opencontainers.image.vendor="Cybersecurity and Infrastructure Security Agency" 14 | 15 | ### 16 | # This Docker container does not use an unprivileged user because it 17 | # must be able to modify postfix and opendkim config files and 18 | # therefore must run as root. 19 | ### 20 | 21 | ### 22 | # Install everything we need 23 | ### 24 | RUN apt-get update --quiet --quiet \ 25 | && DEBIAN_FRONTEND=noninteractive apt-get install --quiet --quiet --yes \ 26 | --no-install-recommends --no-install-suggests \ 27 | ca-certificates=20210119 \ 28 | diceware=0.9.6-1 \ 29 | dovecot-imapd=1:2.3.13+dfsg1-2+deb11u2 \ 30 | dovecot-lmtpd=1:2.3.13+dfsg1-2+deb11u2 \ 31 | gettext-base=0.21-4 \ 32 | mailutils=1:3.10-3+b1 \ 33 | opendkim=2.11.0~beta2-4+deb11u1 \ 34 | opendkim-tools=2.11.0~beta2-4+deb11u1 \ 35 | opendmarc=1.4.0~beta1+dfsg-6+deb11u1 \ 36 | postfix=3.5.25-0+deb11u1 \ 37 | procmail=3.22-26+deb11u1 \ 38 | sasl2-bin=2.1.27+dfsg-2.1+deb11u1 \ 39 | && apt-get --quiet --quiet clean \ 40 | && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* 41 | 42 | ### 43 | # Create a mailarchive user 44 | ### 45 | RUN adduser mailarchive --quiet --disabled-password \ 46 | --shell /usr/sbin/nologin --gecos "Mail Archive" 47 | 48 | ### 49 | # Setup entrypoint 50 | ### 51 | USER root 52 | WORKDIR /root 53 | 54 | # Make backups of configurations. These are modified at startup. 55 | RUN mv /etc/default/opendkim /etc/default/opendkim.orig 56 | RUN mv /etc/default/opendmarc /etc/default/opendmarc.orig 57 | RUN mv /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.orig 58 | RUN mv /etc/postfix/master.cf /etc/postfix/master.cf.orig 59 | 60 | COPY src/templates templates/ 61 | COPY src/docker-entrypoint.sh src/version.txt ./ 62 | 63 | ### 64 | # Prepare to run 65 | ### 66 | VOLUME ["/var/log", "/var/spool/postfix"] 67 | EXPOSE 25/TCP 587/TCP 993/TCP 68 | ENTRYPOINT ["./docker-entrypoint.sh"] 69 | CMD ["postfix", "-v", "start-fg"] 70 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | # Do not allow non-empty flow mappings 7 | forbid: non-empty 8 | # Allow up to one space inside braces. This is required for Ansible compatibility. 9 | max-spaces-inside: 1 10 | 11 | brackets: 12 | # Do not allow non-empty flow sequences 13 | forbid: non-empty 14 | 15 | comments: 16 | # Ensure that inline comments have at least one space before the preceding content. 17 | # This is required for Ansible compatibility. 18 | min-spaces-from-content: 1 19 | 20 | # yamllint does not like it when you comment out different parts of 21 | # dictionaries in a list. You can see 22 | # https://github.com/adrienverge/yamllint/issues/384 for some examples of 23 | # this behavior. 24 | comments-indentation: disable 25 | 26 | indentation: 27 | # Ensure that block sequences inside of a mapping are indented 28 | indent-sequences: true 29 | # Enforce a specific number of spaces 30 | spaces: 2 31 | 32 | # yamllint does not allow inline mappings that exceed the line length by 33 | # default. There are many scenarios where the inline mapping may be a key, 34 | # hash, or other long value that would exceed the line length but cannot 35 | # reasonably be broken across lines. 36 | line-length: 37 | # This rule implies the allow-non-breakable-words rule 38 | allow-non-breakable-inline-mappings: true 39 | # Allows a 10% overage from the default limit of 80 40 | max: 88 41 | 42 | # Using anything other than strings to express octal values can lead to unexpected 43 | # and potentially unsafe behavior. Ansible strongly recommends against such practices 44 | # and these rules are needed for Ansible compatibility. Please see the following for 45 | # more information: 46 | # https://ansible.readthedocs.io/projects/lint/rules/risky-octal/ 47 | octal-values: 48 | # Do not allow explicit octal values (those beginning with a leading 0o). 49 | forbid-explicit-octal: true 50 | # Do not allow implicit octal values (those beginning with a leading 0). 51 | forbid-implicit-octal: true 52 | 53 | quoted-strings: 54 | # Allow disallowed quotes (single quotes) for strings that contain allowed quotes 55 | # (double quotes). 56 | allow-quoted-quotes: true 57 | # Apply these rules to keys in mappings as well 58 | check-keys: true 59 | # We prefer double quotes for strings when they are needed 60 | quote-type: double 61 | # Only require quotes when they are necessary for proper processing 62 | required: only-when-needed 63 | -------------------------------------------------------------------------------- /.github/workflows/_repo-metadata.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Provide repository metadata 3 | 4 | on: # yamllint disable-line rule:truthy 5 | workflow_call: 6 | outputs: 7 | image-name: 8 | description: The name of the Docker image. 9 | value: ${{ jobs.output-repo-metadata.outputs.image-name }} 10 | image-platforms: 11 | description: The supported platforms for the Docker image. 12 | value: ${{ jobs.output-repo-metadata.outputs.image-platforms }} 13 | 14 | jobs: 15 | output-repo-metadata: 16 | name: Generate outputs for repository metadata 17 | outputs: 18 | image-name: ${{ steps.set-outputs.outputs.image-name }} 19 | image-platforms: ${{ steps.set-outputs.outputs.image-platforms }} 20 | permissions: {} 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Set outputs for repository metadata 24 | id: set-outputs 25 | run: | 26 | # Standard Python Libraries 27 | import json 28 | import os 29 | import sys 30 | from typing import Any, TypedDict 31 | 32 | 33 | class GhaOutput(TypedDict): 34 | 35 | description: str 36 | name: str 37 | value: Any 38 | 39 | 40 | # Every output in this list must be configured as an output for the workflow. 41 | gha_outputs: list[GhaOutput] = [ 42 | { 43 | "description": "The name of the Docker image.", 44 | "name": "image-name", 45 | "value": "cisagov/postfix", 46 | }, 47 | { 48 | "description": "The supported platforms for the Docker image.", 49 | "name": "image-platforms", 50 | "value": [ 51 | # The platforms disabled below are not available for the current 52 | # base image (debian:bullseye-slim). Please see #60 for more 53 | # information. 54 | "linux/386", 55 | "linux/amd64", 56 | # "linux/arm/v6", 57 | "linux/arm/v7", 58 | "linux/arm64", 59 | # "linux/ppc64le", 60 | # "linux/riscv64", 61 | # "linux/s390x", 62 | ], 63 | }, 64 | ] 65 | 66 | if os.getenv("GITHUB_OUTPUT") is None: 67 | print( 68 | "GITHUB_OUTPUT is not set. " 69 | "This script is intended to be run in a GitHub Actions environment." 70 | ) 71 | sys.exit(1) 72 | 73 | with open(os.environ["GITHUB_OUTPUT"], "a") as gh_output: 74 | for output in gha_outputs: 75 | if any(isinstance(output["value"], t) for t in [list, dict]): 76 | output["value"] = json.dumps(output["value"]) 77 | gh_output.write(f"{output['name']}={output['value']}\n") 78 | shell: python3 {0} 79 | -------------------------------------------------------------------------------- /src/secrets/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC9yUW9DpYCRBen 3 | A2SyOOiOW8AqSpOccny8dozQLfKtKb5ow8fT+XMtO5Om/l5pIFRTOLl/9WMEWyUf 4 | uQAZy3ibUwlR/FFh0/i5leqd+SIzJPzHvP6TpJsPbZTpfFrvJX6HCRuE0MU6ZwJZ 5 | rCCsRG9+RJ3Kn+X3i75NaDm/8u+aSawVGLFP5Tl2X+C9J1dsC+N2uf7H8hFUwWsQ 6 | NOblA0IevHWVNB/iCIWXtVTzbQdCI1PdjckPu/4kcs4NTJhkADpumDm9hgxuHF96 7 | Bm78LsORPf9m2lFcQ+/WZH6ShLuc8C1mYny3voZH95QDmtgu/U/prgq9R/T+5E8Y 8 | dBmyPnuVVGrJHdMOLftoPLC9x1MFBkd7183Jhh7uDqxOgrNj9n4rBW4j4pxqZUfp 9 | AsuEc5nsgUh0Hk+tIIwseUNE/bYZZXmVsWPE/70f/Y8HlCSgG49Li9X8Y3EyUaWd 10 | sKhUN1SW7KmeyXSl2lp7fo+IU+WQDd3MnU5w63zTeNETcccGDgyr2s7nU1B/HHcu 11 | yco9W9qNUKFwSJ7VQQf4NAsRVyJEEin0mGjhIyH3FL1VcVFdeucQqR8s5fCR3dXL 12 | i6m/JCvGkQyaPVIdKwWonjHPW6jlqSmDDMHkjXih76Q4r+ws/4HgavJanvxwBJBD 13 | /SV1u85bEHGbs5VI9AHoXq8dUo0TGwIDAQABAoICAQCRaDhKVXaRXeJRT8RC2F81 14 | Uw60WFcoMn9nVd0lU07vZWBBnF7qBeE88rx54cIsAV0aNgfKBhRLLhoPaAqvuLk7 15 | KC+n5Q3lSiby6e3MAyk0zk3uKttR+3fiJi9FhMWXHL8Ibu3qoJm72Vhvo/WUhwp1 16 | T9UlfcUQGL1BSW2Vp2f0aiWyNC0F7bZM/8CMrCvK2ID6Yh7WypyEt3xz+lQ9enWa 17 | XwInwrv6zlSsm33u08YP4klLImq952ccPempPtozJAmg2njCwIWdh5ePQoaeKKYm 18 | Db4062gSrOqA9JYVZCTqZQoju6majhsL4KBC8sxXlDU58OLBivQmpn4DWlClxEGi 19 | IbY/FIE6WEhOrdoGPzIjAcC3OYYTasIMBDLdA0tODmtv9Nvst30IGZc4Pm/QIJOk 20 | EGJo4hqWbxiy4gisWxHwYeQ9/EEwrrc3FP94VscVkT8x0i22w5WMLtcrnCGpwzMg 21 | E10+9v4ZUZ7cu9V+IeWQUkeuP3xhumI7RIDVRHpGC6TfEk/Q2gNdsPL2E8ng2Ytx 22 | KMI3Pj5FuYi7enIR9AWdBVmVc2u7nzJMF/ODAwY6GmqHxni7PD97cnYwCy7Gxp/S 23 | DZqiiD32RHwUwBm0AgdLhftkgqyTN/qo/Bhmj9ieO2CkuAvTYoXG0VMzxCb9wBG/ 24 | 7BJSGcbwtTJOJGK7LvrDAQKCAQEA6Q45teOKcmOSw5ne2cXzXuaXZ0OOCkjJ2ens 25 | M89YmKXDVEZRbGoHVtftInUpr0H2UJ/N268Ogfzw62enZ40WIGwNALvp9PkLvdT0 26 | 6LD/4MhcgZGQ5WDwqfqwkOanHdw9HJb752yEJ+3OG+fojmKkOs6OoQk1Ypxv5+5K 27 | OuG/qtiKKpSLbG/nKAbPsPObArBxyfH9pV5F2E6vy38lYoDTURlA2BXHPoXu9M4c 28 | /K2BMmO5zvGu5VOpAtnag5CWUwVvnX9DKDYs+k+exErluEj+U8GbKNQUTE+1p6fT 29 | j4KKNVZBgnavOST3Xm/i4qVbccF/CwUc387HPdK5FU6kn3evewKCAQEA0HiEAytq 30 | jzlBBHm892tojRzvpQa65fT7khsxETLhABvqeWZ2h9lE8TJTLC46N4cG1MC/hnWB 31 | Q7XzKd7jAeht41Lp0mlDWv6eqKN4VyXSpAYzATcEO739eja7WNTgkYB91eDSyT+K 32 | DVaElaXMjw/uX9tBnqaVyEe8JDqHw9E3Gl0MLWi89ztYptaWvKjt0+QqENBc6o+G 33 | K/qzO+B4o9AyjyYkUYVA87tRrDk746LA5DbkpLQKPmQ3lb1hvVysJOnEdRabu5ly 34 | mC0HR9n2UwcU98Op/EX3D4MuCUoFB/HQNMXq7oRMg+AcfsG0/ENcbiY6o0yRhxHu 35 | ACgcjTi/QKAI4QKCAQBbgzB6EZ0diafpkpQFI0uLKjStYcN2mlpYbRhIx9RcLErk 36 | 3q++SGwVV7hP3X2+ycH0qqtk5fpmZHIdnZgIe0gC9yqr7R3TCa/onKSGcmonU8Wv 37 | Qv+IcmZN+Jg4bbmVahO9FDRaDSxfmWtjXc7dijI+vTkYVstVq2PtyI3xTQ+8AEdQ 38 | rP+KVu6HsxT+wMlPZwVnbNRSiRAX/d3dpFGDul4/7BCgSPzxuhm4mu6a8W5X4Pzn 39 | G9O3TQCClBTPsIi2lN3dFEnEknFa4MTRAy/tCwyCyvUoNQ67YFlOOgJCydmHVBVp 40 | Kz1mzPMta/XFVXTw2DAQnbNW1pU523K9wSG3VIHdAoIBACJTZbE76dzRWZJKFUJM 41 | DjgGBrOOiyGoF/Azx/2D+iZRcmcw5t1xefeZCLbimbVg51AKuL6EBJfIktRXHdvH 42 | kKh4k4WQzYVjHW65E+yNjsRxPN67V1ga7Wy9LFXxH1T16kJYNXzrmGif0U7usOLx 43 | hZeE+6YK2ejTXvg8JvSoM0GFBqdHcq3muK8n8EP6MMbN79s648G/hiEhs3dte4/F 44 | jT2i0yIVJd+7/TO1bNYLi2VIYJd6CaHCUKC4QSqz4qhlUXLSGSxnlMXXzDYZfoSn 45 | St2M+yVNw+Nq/x6KcI+hUl4OJKPHZu3j7e01Kf7LfKGqa8dNqTyrSBwAfssGB/+1 46 | GiECggEAJD0KWTfJrSbgCkMfp1fNkwNExW2+neB+MI1eIR1sWsu8rz1a5d/NIdQq 47 | pkoJp4FQUgRFEK+CzPWbKBDOxDVwpZ5o84JzxAEc78tL8/QIYwbtw5ZOiHNZ+wS6 48 | OYk6weY7rro7PwzqsTXcGdg/yxtphwguveSQM8y6McqBNZKqlN2fvXY8a4KZtt8O 49 | RXBwpsqYulHpMGPh2MsMJBGEEII7Y2WKZG41oU1SGb5J2tBdGixW0buQnr6qwBgL 50 | Ie8VV5kgbei97WK1lwvosn3HetBYSEE0GWMvjx93yoeozV8L/IF1rf7xss2BSqzF 51 | UjgsHxWMDJWcER8NHXkE5DQORLtKCA== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /src/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2016 3 | 4 | set -e 5 | #set -x 6 | 7 | function generate_configs() { 8 | # configure postfix 9 | echo "Generating postfix configurations for ${PRIMARY_DOMAIN}" 10 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/main.cf > /etc/postfix/main.cf 11 | cp /etc/postfix/master.cf.orig /etc/postfix/master.cf 12 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/master.cf >> /etc/postfix/master.cf 13 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/opendkim.conf > /etc/opendkim.conf 14 | 15 | # configure opendkim 16 | echo "Generating opendkim configurations for ${PRIMARY_DOMAIN}" 17 | mkdir -p "/etc/opendkim/keys/${PRIMARY_DOMAIN}" 18 | opendkim-genkey --verbose --bits=1024 --selector=mail --directory="/etc/opendkim/keys/${PRIMARY_DOMAIN}" 19 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/TrustedHosts > /etc/opendkim/TrustedHosts 20 | cp /etc/default/opendkim.orig /etc/default/opendkim 21 | echo 'SOCKET="inet:12301"' >> /etc/default/opendkim 22 | chown -R opendkim:opendkim /etc/opendkim 23 | 24 | # configure opendmarc 25 | echo "Generating opendmarc configurations for ${PRIMARY_DOMAIN}" 26 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/opendmarc.conf > /etc/opendmarc.conf 27 | mkdir -p "/etc/opendmarc/" 28 | echo "localhost" > /etc/opendmarc/ignore.hosts 29 | chown -R opendmarc:opendmarc /etc/opendmarc 30 | cp /etc/default/opendmarc.orig /etc/default/opendmarc 31 | echo 'SOCKET="inet:54321"' >> /etc/default/opendmarc 32 | 33 | # configure dovecot 34 | echo "Generating dovecot configurations for ${PRIMARY_DOMAIN}" 35 | envsubst '\$PRIMARY_DOMAIN \$RELAY_IP' < templates/dovecot.conf > /etc/dovecot/dovecot.conf 36 | 37 | # create a file marking the configuration as completed for this domain 38 | echo "All configurations generated for ${PRIMARY_DOMAIN}" 39 | } 40 | 41 | function generate_users() { 42 | echo "Generating users and passwords:" 43 | echo "--------------------------------------------" 44 | while IFS=" " read -r username password || [ -n "$username" ]; do 45 | if [ -z "$password" ]; then 46 | password=$(diceware -d-) 47 | echo -e "$username\t$password" 48 | else 49 | echo -e "$username\t" 50 | fi 51 | adduser "$username" --quiet --disabled-password --shell /usr/sbin/nologin --gecos "" --force-badname || true 52 | echo "$username:$password" | chpasswd || true 53 | done 54 | echo "--------------------------------------------" 55 | } 56 | 57 | if [ "$1" = 'postfix' ]; then 58 | echo "Starting mail server with:" 59 | echo " PRIMARY_DOMAIN=${PRIMARY_DOMAIN}" 60 | echo " RELAY_IP=${RELAY_IP}" 61 | 62 | # check to see if the configuration was completed for this domain 63 | if [[ ! -f conf_gen_done.txt ]] || [[ $(< conf_gen_done.txt) != "${PRIMARY_DOMAIN}" ]]; then 64 | generate_configs 65 | echo "${PRIMARY_DOMAIN}" > conf_gen_done.txt 66 | else 67 | echo "Configurations already generated for ${PRIMARY_DOMAIN}, preserving." 68 | fi 69 | 70 | # generate the users from the secrets 71 | grep -v '^#\|^$' /run/secrets/users.txt | generate_users 72 | 73 | # postfix needs fresh copies of files in its chroot jail 74 | cp /etc/{hosts,localtime,nsswitch.conf,resolv.conf,services} /var/spool/postfix/etc/ 75 | 76 | echo "DKIM DNS entry:" 77 | echo "--------------------------------------------" 78 | cat "/etc/opendkim/keys/${PRIMARY_DOMAIN}/mail.txt" 79 | echo "--------------------------------------------" 80 | 81 | opendmarc 82 | opendkim 83 | dovecot 84 | exec "$@" 85 | fi 86 | 87 | exec "$@" 88 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Rather than breaking up descriptions into multiline strings we disable that 3 | # specific rule in yamllint for this file. 4 | # yamllint disable rule:line-length 5 | - color: f15a53 6 | description: Pull requests that update Ansible code 7 | name: ansible 8 | - color: eb6420 9 | description: This issue or pull request is awaiting the outcome of another issue or pull request 10 | name: blocked 11 | - color: "000000" 12 | description: This issue or pull request involves changes to existing functionality 13 | name: breaking change 14 | - color: d73a4a 15 | description: This issue or pull request addresses broken functionality 16 | name: bug 17 | - color: 07648d 18 | description: This issue will be advertised on code.gov's Open Tasks page (https://code.gov/open-tasks) 19 | name: code.gov 20 | - color: 0366d6 21 | description: Pull requests that update a dependency file 22 | name: dependencies 23 | - color: 2497ed 24 | description: Pull requests that update Docker code 25 | name: docker 26 | - color: 5319e7 27 | description: This issue or pull request improves or adds to documentation 28 | name: documentation 29 | - color: cfd3d7 30 | description: This issue or pull request already exists or is covered in another issue or pull request 31 | name: duplicate 32 | - color: b005bc 33 | description: A high-level objective issue encompassing multiple issues instead of a specific unit of work 34 | name: epic 35 | - color: "000000" 36 | description: Pull requests that update GitHub Actions code 37 | name: github-actions 38 | - color: 0e8a16 39 | description: This issue or pull request is well-defined and good for newcomers 40 | name: good first issue 41 | - color: ff7518 42 | description: Pull request that should count toward Hacktoberfest participation 43 | name: hacktoberfest-accepted 44 | - color: a2eeef 45 | description: This issue or pull request will add or improve functionality, maintainability, or ease of use 46 | name: improvement 47 | - color: fef2c0 48 | description: This issue or pull request is not applicable, incorrect, or obsolete 49 | name: invalid 50 | - color: f1d642 51 | description: Pull requests that update JavaScript code 52 | name: javascript 53 | - color: ce099a 54 | description: This pull request is ready to merge during the next Lineage Kraken release 55 | name: kraken 🐙 56 | - color: a4fc5d 57 | description: This issue or pull request requires further information 58 | name: need info 59 | - color: fcdb45 60 | description: This pull request is awaiting an action or decision to move forward 61 | name: on hold 62 | - color: 02a8ef 63 | description: Pull requests that update Packer code 64 | name: packer 65 | - color: 3772a4 66 | description: Pull requests that update Python code 67 | name: python 68 | - color: ef476c 69 | description: This issue is a request for information or needs discussion 70 | name: question 71 | - color: d73a4a 72 | description: This issue or pull request addresses a security issue 73 | name: security 74 | - color: 7b42bc 75 | description: Pull requests that update Terraform code 76 | name: terraform 77 | - color: 00008b 78 | description: This issue or pull request adds or otherwise modifies test code 79 | name: test 80 | - color: 2b6ebf 81 | description: Pull requests that update TypeScript code 82 | name: typescript 83 | - color: 1d76db 84 | description: This issue or pull request pulls in upstream updates 85 | name: upstream update 86 | - color: d4c5f9 87 | description: This issue or pull request increments the version number 88 | name: version bump 89 | - color: ffffff 90 | description: This issue will not be incorporated 91 | name: wontfix 92 | -------------------------------------------------------------------------------- /.github/workflows/label-prs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Label pull requests 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: 6 | types: 7 | - edited 8 | - opened 9 | - synchronize 10 | 11 | # Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, 12 | # nounset, errexit, and pipefail. The `-x` will print all commands as they are 13 | # run. Please see the GitHub Actions documentation for more information: 14 | # https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs 15 | defaults: 16 | run: 17 | shell: bash -Eueo pipefail -x {0} 18 | 19 | jobs: 20 | diagnostics: 21 | name: Run diagnostics 22 | # This job does not need any permissions 23 | permissions: {} 24 | runs-on: ubuntu-latest 25 | steps: 26 | # Note that a duplicate of this step must be added at the top of 27 | # each job. 28 | - name: Apply standard cisagov job preamble 29 | uses: cisagov/action-job-preamble@v1 30 | with: 31 | check_github_status: "true" 32 | # This functionality is poorly implemented and has been 33 | # causing problems due to the MITM implementation hogging or 34 | # leaking memory. As a result we disable it by default. If 35 | # you want to temporarily enable it, simply set 36 | # monitor_permissions equal to "true". 37 | # 38 | # TODO: Re-enable this functionality when practical. See 39 | # cisagov/skeleton-generic#207 for more details. 40 | monitor_permissions: "false" 41 | output_workflow_context: "true" 42 | # Use a variable to specify the permissions monitoring 43 | # configuration. By default this will yield the 44 | # configuration stored in the cisagov organization-level 45 | # variable, but if you want to use a different configuration 46 | # then simply: 47 | # 1. Create a repository-level variable with the name 48 | # ACTIONS_PERMISSIONS_CONFIG. 49 | # 2. Set this new variable's value to the configuration you 50 | # want to use for this repository. 51 | # 52 | # Note in particular that changing the permissions 53 | # monitoring configuration *does not* require you to modify 54 | # this workflow. 55 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 56 | label: 57 | needs: 58 | - diagnostics 59 | permissions: 60 | # Permissions required by actions/labeler 61 | contents: read 62 | issues: write 63 | pull-requests: write 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Apply standard cisagov job preamble 67 | uses: cisagov/action-job-preamble@v1 68 | with: 69 | # This functionality is poorly implemented and has been 70 | # causing problems due to the MITM implementation hogging or 71 | # leaking memory. As a result we disable it by default. If 72 | # you want to temporarily enable it, simply set 73 | # monitor_permissions equal to "true". 74 | # 75 | # TODO: Re-enable this functionality when practical. See 76 | # cisagov/skeleton-generic#207 for more details. 77 | monitor_permissions: "false" 78 | # Use a variable to specify the permissions monitoring 79 | # configuration. By default this will yield the 80 | # configuration stored in the cisagov organization-level 81 | # variable, but if you want to use a different configuration 82 | # then simply: 83 | # 1. Create a repository-level variable with the name 84 | # ACTIONS_PERMISSIONS_CONFIG. 85 | # 2. Set this new variable's value to the configuration you 86 | # want to use for this repository. 87 | # 88 | # Note in particular that changing the permissions 89 | # monitoring configuration *does not* require you to modify 90 | # this workflow. 91 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 92 | - name: Apply suitable labels to a pull request 93 | uses: actions/labeler@v6 94 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: sync-labels 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | paths: 7 | - .github/labels.yml 8 | - .github/workflows/sync-labels.yml 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | diagnostics: 16 | name: Run diagnostics 17 | # This job does not need any permissions 18 | permissions: {} 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Note that a duplicate of this step must be added at the top of 22 | # each job. 23 | - name: Apply standard cisagov job preamble 24 | uses: cisagov/action-job-preamble@v1 25 | with: 26 | check_github_status: "true" 27 | # This functionality is poorly implemented and has been 28 | # causing problems due to the MITM implementation hogging or 29 | # leaking memory. As a result we disable it by default. If 30 | # you want to temporarily enable it, simply set 31 | # monitor_permissions equal to "true". 32 | # 33 | # TODO: Re-enable this functionality when practical. See 34 | # cisagov/skeleton-generic#207 for more details. 35 | monitor_permissions: "false" 36 | output_workflow_context: "true" 37 | # Use a variable to specify the permissions monitoring 38 | # configuration. By default this will yield the 39 | # configuration stored in the cisagov organization-level 40 | # variable, but if you want to use a different configuration 41 | # then simply: 42 | # 1. Create a repository-level variable with the name 43 | # ACTIONS_PERMISSIONS_CONFIG. 44 | # 2. Set this new variable's value to the configuration you 45 | # want to use for this repository. 46 | # 47 | # Note in particular that changing the permissions 48 | # monitoring configuration *does not* require you to modify 49 | # this workflow. 50 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 51 | labeler: 52 | needs: 53 | - diagnostics 54 | permissions: 55 | # actions/checkout needs this to fetch code 56 | contents: read 57 | # crazy-max/ghaction-github-labeler needs this to manage repository labels 58 | issues: write 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Apply standard cisagov job preamble 62 | uses: cisagov/action-job-preamble@v1 63 | with: 64 | # This functionality is poorly implemented and has been 65 | # causing problems due to the MITM implementation hogging or 66 | # leaking memory. As a result we disable it by default. If 67 | # you want to temporarily enable it, simply set 68 | # monitor_permissions equal to "true". 69 | # 70 | # TODO: Re-enable this functionality when practical. See 71 | # cisagov/skeleton-generic#207 for more details. 72 | monitor_permissions: "false" 73 | # Use a variable to specify the permissions monitoring 74 | # configuration. By default this will yield the 75 | # configuration stored in the cisagov organization-level 76 | # variable, but if you want to use a different configuration 77 | # then simply: 78 | # 1. Create a repository-level variable with the name 79 | # ACTIONS_PERMISSIONS_CONFIG. 80 | # 2. Set this new variable's value to the configuration you 81 | # want to use for this repository. 82 | # 83 | # Note in particular that changing the permissions 84 | # monitoring configuration *does not* require you to modify 85 | # this workflow. 86 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 87 | - uses: actions/checkout@v5 88 | - name: Sync repository labels 89 | if: success() 90 | uses: crazy-max/ghaction-github-labeler@v5 91 | with: 92 | # This is a hideous ternary equivalent so we only do a dry run unless 93 | # this workflow is triggered by the develop branch. 94 | dry-run: ${{ github.ref_name == 'develop' && 'false' || 'true' }} 95 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependency review 3 | 4 | on: # yamllint disable-line rule:truthy 5 | merge_group: 6 | types: 7 | - checks_requested 8 | pull_request: 9 | 10 | # Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, 11 | # nounset, errexit, and pipefail. The `-x` will print all commands as they are 12 | # run. Please see the GitHub Actions documentation for more information: 13 | # https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs 14 | defaults: 15 | run: 16 | shell: bash -Eueo pipefail -x {0} 17 | 18 | jobs: 19 | diagnostics: 20 | name: Run diagnostics 21 | # This job does not need any permissions 22 | permissions: {} 23 | runs-on: ubuntu-latest 24 | steps: 25 | # Note that a duplicate of this step must be added at the top of 26 | # each job. 27 | - name: Apply standard cisagov job preamble 28 | uses: cisagov/action-job-preamble@v1 29 | with: 30 | check_github_status: "true" 31 | # This functionality is poorly implemented and has been 32 | # causing problems due to the MITM implementation hogging or 33 | # leaking memory. As a result we disable it by default. If 34 | # you want to temporarily enable it, simply set 35 | # monitor_permissions equal to "true". 36 | # 37 | # TODO: Re-enable this functionality when practical. See 38 | # cisagov/skeleton-generic#207 for more details. 39 | monitor_permissions: "false" 40 | output_workflow_context: "true" 41 | # Use a variable to specify the permissions monitoring 42 | # configuration. By default this will yield the 43 | # configuration stored in the cisagov organization-level 44 | # variable, but if you want to use a different configuration 45 | # then simply: 46 | # 1. Create a repository-level variable with the name 47 | # ACTIONS_PERMISSIONS_CONFIG. 48 | # 2. Set this new variable's value to the configuration you 49 | # want to use for this repository. 50 | # 51 | # Note in particular that changing the permissions 52 | # monitoring configuration *does not* require you to modify 53 | # this workflow. 54 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 55 | dependency-review: 56 | name: Dependency review 57 | needs: 58 | - diagnostics 59 | permissions: 60 | # actions/checkout needs this to fetch code 61 | contents: read 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Apply standard cisagov job preamble 65 | uses: cisagov/action-job-preamble@v1 66 | with: 67 | # This functionality is poorly implemented and has been 68 | # causing problems due to the MITM implementation hogging or 69 | # leaking memory. As a result we disable it by default. If 70 | # you want to temporarily enable it, simply set 71 | # monitor_permissions equal to "true". 72 | # 73 | # TODO: Re-enable this functionality when practical. See 74 | # cisagov/skeleton-generic#207 for more details. 75 | monitor_permissions: "false" 76 | # Use a variable to specify the permissions monitoring 77 | # configuration. By default this will yield the 78 | # configuration stored in the cisagov organization-level 79 | # variable, but if you want to use a different configuration 80 | # then simply: 81 | # 1. Create a repository-level variable with the name 82 | # ACTIONS_PERMISSIONS_CONFIG. 83 | # 2. Set this new variable's value to the configuration you 84 | # want to use for this repository. 85 | # 86 | # Note in particular that changing the permissions 87 | # monitoring configuration *does not* require you to modify 88 | # this workflow. 89 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 90 | - id: checkout-repo 91 | name: Checkout the repository 92 | uses: actions/checkout@v5 93 | - id: dependency-review 94 | name: Review dependency changes for vulnerabilities and license changes 95 | uses: actions/dependency-review-action@v4 96 | -------------------------------------------------------------------------------- /.github/workflows/update-dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update the Docker Hub description 3 | 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | branches: 8 | - develop 9 | paths: 10 | - README.md 11 | workflow_dispatch: 12 | 13 | jobs: 14 | diagnostics: 15 | name: Run diagnostics 16 | # This job does not need any permissions 17 | permissions: {} 18 | runs-on: ubuntu-latest 19 | steps: 20 | # Note that a duplicate of this step must be added at the top of 21 | # each job. 22 | - name: Apply standard cisagov job preamble 23 | uses: cisagov/action-job-preamble@v1 24 | with: 25 | check_github_status: "true" 26 | # This functionality is poorly implemented and has been 27 | # causing problems due to the MITM implementation hogging or 28 | # leaking memory. As a result we disable it by default. If 29 | # you want to temporarily enable it, simply set 30 | # monitor_permissions equal to "true". 31 | # 32 | # TODO: Re-enable this functionality when practical. See 33 | # cisagov/skeleton-generic#207 for more details. 34 | monitor_permissions: "false" 35 | output_workflow_context: "true" 36 | # Use a variable to specify the permissions monitoring 37 | # configuration. By default this will yield the 38 | # configuration stored in the cisagov organization-level 39 | # variable, but if you want to use a different configuration 40 | # then simply: 41 | # 1. Create a repository-level variable with the name 42 | # ACTIONS_PERMISSIONS_CONFIG. 43 | # 2. Set this new variable's value to the configuration you 44 | # want to use for this repository. 45 | # 46 | # Note in particular that changing the permissions 47 | # monitoring configuration *does not* require you to modify 48 | # this workflow. 49 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 50 | repo-metadata: 51 | name: Gather repository metadata 52 | needs: 53 | - diagnostics 54 | permissions: 55 | # actions/checkout needs this to fetch code 56 | contents: read 57 | uses: ./.github/workflows/_repo-metadata.yml 58 | update: 59 | name: Update the description for the image on Docker Hub 60 | needs: 61 | - diagnostics 62 | - repo-metadata 63 | permissions: 64 | # actions/checkout needs this to fetch code 65 | contents: read 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Apply standard cisagov job preamble 69 | uses: cisagov/action-job-preamble@v1 70 | with: 71 | # This functionality is poorly implemented and has been 72 | # causing problems due to the MITM implementation hogging or 73 | # leaking memory. As a result we disable it by default. If 74 | # you want to temporarily enable it, simply set 75 | # monitor_permissions equal to "true". 76 | # 77 | # TODO: Re-enable this functionality when practical. See 78 | # cisagov/skeleton-docker#224 for more details. 79 | monitor_permissions: "false" 80 | # Use a variable to specify the permissions monitoring 81 | # configuration. By default this will yield the 82 | # configuration stored in the cisagov organization-level 83 | # variable, but if you want to use a different configuration 84 | # then simply: 85 | # 1. Create a repository-level variable with the name 86 | # ACTIONS_PERMISSIONS_CONFIG. 87 | # 2. Set this new variable's value to the configuration you 88 | # want to use for this repository. 89 | # 90 | # Note in particular that changing the permissions 91 | # monitoring configuration *does not* require you to modify 92 | # this workflow. 93 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 94 | - name: Checkout the repository 95 | uses: actions/checkout@v5 96 | - name: Update the Docker Hub description 97 | uses: peter-evans/dockerhub-description@v5 98 | with: 99 | password: ${{ secrets.DOCKER_PASSWORD }} 100 | readme-filepath: README.md 101 | repository: ${{ needs.repo-metadata.outputs.image-name }} 102 | short-description: ${{ github.event.repository.description }} 103 | username: ${{ secrets.DOCKER_USERNAME }} 104 | -------------------------------------------------------------------------------- /bump-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # bump-version [--push] [--label LABEL] (major | minor | patch | prerelease | build | finalize | show) 4 | # bump-version --list-files 5 | 6 | set -o nounset 7 | set -o errexit 8 | set -o pipefail 9 | 10 | # Stores the canonical version for the project. 11 | VERSION_FILE=src/version.txt 12 | # Files that should be updated with the new version. 13 | VERSION_FILES=("$VERSION_FILE" README.md) 14 | 15 | USAGE=$( 16 | cat << END_OF_LINE 17 | Update the version of the project. 18 | 19 | Usage: 20 | ${0##*/} [--push] [--label LABEL] (major | minor | patch | prerelease | build | finalize | show) 21 | ${0##*/} --list-files 22 | ${0##*/} (-h | --help) 23 | 24 | Options: 25 | -h | --help Show this message. 26 | --push Perform a \`git push\` after updating the version. 27 | --label LABEL Specify the label to use when updating the build or prerelease version. 28 | --list-files List the files that will be updated when the version is bumped. 29 | END_OF_LINE 30 | ) 31 | 32 | old_version=$(< "$VERSION_FILE") 33 | # Comment out periods so they are interpreted as periods and don't 34 | # just match any character 35 | old_version_regex=${old_version//\./\\\.} 36 | new_version="$old_version" 37 | 38 | bump_part="" 39 | label="" 40 | commit_prefix="Bump" 41 | with_push=false 42 | commands_with_label=("build" "prerelease") 43 | commands_with_prerelease=("major" "minor" "patch") 44 | with_prerelease=false 45 | 46 | ####################################### 47 | # Display an error message, the help information, and exit with a non-zero status. 48 | # Arguments: 49 | # Error message. 50 | ####################################### 51 | function invalid_option() { 52 | echo "$1" 53 | echo "$USAGE" 54 | exit 1 55 | } 56 | 57 | ####################################### 58 | # Bump the version using the provided command. 59 | # Arguments: 60 | # The version to bump. 61 | # The command to bump the version. 62 | # Returns: 63 | # The new version. 64 | ####################################### 65 | function bump_version() { 66 | local temp_version 67 | temp_version=$(python -c "import semver; print(semver.parse_version_info('$1').${2})") 68 | echo "$temp_version" 69 | } 70 | 71 | if [ $# -eq 0 ]; then 72 | echo "$USAGE" 73 | exit 1 74 | else 75 | while [ $# -gt 0 ]; do 76 | case $1 in 77 | --push) 78 | if [ "$with_push" = true ]; then 79 | invalid_option "Push has already been set." 80 | fi 81 | 82 | with_push=true 83 | shift 84 | ;; 85 | --label) 86 | if [ -n "$label" ]; then 87 | invalid_option "Label has already been set." 88 | fi 89 | 90 | label="$2" 91 | shift 2 92 | ;; 93 | build | finalize | major | minor | patch) 94 | if [ -n "$bump_part" ]; then 95 | invalid_option "Only one version part should be bumped at a time." 96 | fi 97 | 98 | bump_part="$1" 99 | shift 100 | ;; 101 | prerelease) 102 | with_prerelease=true 103 | shift 104 | ;; 105 | show) 106 | echo "$old_version" 107 | exit 0 108 | ;; 109 | -h | --help) 110 | echo "$USAGE" 111 | exit 0 112 | ;; 113 | --list-files) 114 | printf '%s\n' "${VERSION_FILES[@]}" 115 | exit 0 116 | ;; 117 | *) 118 | invalid_option "Invalid option: $1" 119 | ;; 120 | esac 121 | done 122 | fi 123 | 124 | if [ -n "$label" ] && [ "$with_prerelease" = false ] && [[ ! " ${commands_with_label[*]} " =~ [[:space:]]${bump_part}[[:space:]] ]]; then 125 | invalid_option "Setting the label is only allowed for the following commands: ${commands_with_label[*]}" 126 | fi 127 | 128 | if [ "$with_prerelease" = true ] && [ -n "$bump_part" ] && [[ ! " ${commands_with_prerelease[*]} " =~ [[:space:]]${bump_part}[[:space:]] ]]; then 129 | invalid_option "Changing the prerelease is only allowed in conjunction with the following commands: ${commands_with_prerelease[*]}" 130 | fi 131 | 132 | label_option="" 133 | if [ -n "$label" ]; then 134 | label_option="token='$label'" 135 | fi 136 | 137 | if [ -n "$bump_part" ]; then 138 | if [ "$bump_part" = "finalize" ]; then 139 | commit_prefix="Finalize" 140 | bump_command="finalize_version()" 141 | elif [ "$bump_part" = "build" ]; then 142 | bump_command="bump_${bump_part}($label_option)" 143 | else 144 | bump_command="bump_${bump_part}()" 145 | fi 146 | new_version=$(bump_version "$old_version" "$bump_command") 147 | echo Changing version from "$old_version" to "$new_version" 148 | fi 149 | 150 | if [ "$with_prerelease" = true ]; then 151 | bump_command="bump_prerelease($label_option)" 152 | temp_version=$(bump_version "$new_version" "$bump_command") 153 | echo Changing version from "$new_version" to "$temp_version" 154 | new_version="$temp_version" 155 | fi 156 | 157 | tmp_file=/tmp/version.$$ 158 | for version_file in "${VERSION_FILES[@]}"; do 159 | if [ ! -f "$version_file" ]; then 160 | echo Missing expected file: "$version_file" 161 | exit 1 162 | fi 163 | sed "s/$old_version_regex/$new_version/" "$version_file" > $tmp_file 164 | mv $tmp_file "$version_file" 165 | done 166 | 167 | git add "${VERSION_FILES[@]}" 168 | git commit --message "$commit_prefix version from $old_version to $new_version" 169 | 170 | if [ "$with_push" = true ]; then 171 | git push 172 | fi 173 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For most projects, this workflow file will not need changing; you simply need 3 | # to commit it to your repository. 4 | # 5 | # You may wish to alter this file to override the set of languages analyzed, 6 | # or to provide custom queries or build logic. 7 | name: CodeQL 8 | 9 | # The use of on here as a key is part of the GitHub actions syntax. 10 | # yamllint disable-line rule:truthy 11 | on: 12 | merge_group: 13 | types: 14 | - checks_requested 15 | pull_request: 16 | # The branches here must be a subset of the ones in the push key 17 | branches: 18 | - develop 19 | push: 20 | # Dependabot-triggered push events have read-only access, but uploading code 21 | # scanning requires write access. 22 | branches-ignore: 23 | - dependabot/** 24 | schedule: 25 | - cron: 0 21 * * 6 26 | 27 | jobs: 28 | diagnostics: 29 | name: Run diagnostics 30 | # This job does not need any permissions 31 | permissions: {} 32 | runs-on: ubuntu-latest 33 | steps: 34 | # Note that a duplicate of this step must be added at the top of 35 | # each job. 36 | - name: Apply standard cisagov job preamble 37 | uses: cisagov/action-job-preamble@v1 38 | with: 39 | check_github_status: "true" 40 | # This functionality is poorly implemented and has been 41 | # causing problems due to the MITM implementation hogging or 42 | # leaking memory. As a result we disable it by default. If 43 | # you want to temporarily enable it, simply set 44 | # monitor_permissions equal to "true". 45 | # 46 | # TODO: Re-enable this functionality when practical. See 47 | # cisagov/skeleton-generic#207 for more details. 48 | monitor_permissions: "false" 49 | output_workflow_context: "true" 50 | # Use a variable to specify the permissions monitoring 51 | # configuration. By default this will yield the 52 | # configuration stored in the cisagov organization-level 53 | # variable, but if you want to use a different configuration 54 | # then simply: 55 | # 1. Create a repository-level variable with the name 56 | # ACTIONS_PERMISSIONS_CONFIG. 57 | # 2. Set this new variable's value to the configuration you 58 | # want to use for this repository. 59 | # 60 | # Note in particular that changing the permissions 61 | # monitoring configuration *does not* require you to modify 62 | # this workflow. 63 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 64 | analyze: 65 | name: Analyze 66 | needs: 67 | - diagnostics 68 | runs-on: ubuntu-latest 69 | permissions: 70 | # actions/checkout needs this to fetch code 71 | contents: read 72 | # required for all workflows 73 | security-events: write 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | # Override automatic language detection by changing the below 78 | # list 79 | # 80 | # Supported options are actions, c-cpp, csharp, go, 81 | # java-kotlin, javascript-typescript, python, ruby, and swift. 82 | language: 83 | - actions 84 | - python 85 | # Learn more... 86 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 87 | 88 | steps: 89 | - name: Apply standard cisagov job preamble 90 | uses: cisagov/action-job-preamble@v1 91 | with: 92 | # This functionality is poorly implemented and has been 93 | # causing problems due to the MITM implementation hogging or 94 | # leaking memory. As a result we disable it by default. If 95 | # you want to temporarily enable it, simply set 96 | # monitor_permissions equal to "true". 97 | # 98 | # TODO: Re-enable this functionality when practical. See 99 | # cisagov/skeleton-generic#207 for more details. 100 | monitor_permissions: "false" 101 | # Use a variable to specify the permissions monitoring 102 | # configuration. By default this will yield the 103 | # configuration stored in the cisagov organization-level 104 | # variable, but if you want to use a different configuration 105 | # then simply: 106 | # 1. Create a repository-level variable with the name 107 | # ACTIONS_PERMISSIONS_CONFIG. 108 | # 2. Set this new variable's value to the configuration you 109 | # want to use for this repository. 110 | # 111 | # Note in particular that changing the permissions 112 | # monitoring configuration *does not* require you to modify 113 | # this workflow. 114 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 115 | 116 | - name: Checkout repository 117 | uses: actions/checkout@v5 118 | 119 | # Initializes the CodeQL tools for scanning. 120 | - name: Initialize CodeQL 121 | uses: github/codeql-action/init@v3 122 | with: 123 | languages: ${{ matrix.language }} 124 | 125 | # Autobuild attempts to build any compiled languages (C/C++, C#, or 126 | # Java). If this step fails, then you should remove it and run the build 127 | # manually (see below). 128 | - name: Autobuild 129 | uses: github/codeql-action/autobuild@v3 130 | 131 | # ℹ️ Command-line programs to run using the OS shell. 132 | # 📚 https://git.io/JvXDl 133 | 134 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 135 | # three lines and modify them (or add more) to build your code if your 136 | # project uses a compiled language 137 | 138 | # - run: | 139 | # make bootstrap 140 | # make release 141 | 142 | - name: Perform CodeQL Analysis 143 | uses: github/codeql-action/analyze@v3 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome # 2 | 3 | We're so glad you're thinking about contributing to this open source 4 | project! If you're unsure or afraid of anything, just ask or submit 5 | the issue or pull request anyway. The worst that can happen is that 6 | you'll be politely asked to change something. We appreciate any sort 7 | of contribution, and don't want a wall of rules to get in the way of 8 | that. 9 | 10 | Before contributing, we encourage you to read our CONTRIBUTING policy 11 | (you are here), our [LICENSE](LICENSE), and our [README](README.md), 12 | all of which should be in this repository. 13 | 14 | ## Issues ## 15 | 16 | If you want to report a bug or request a new feature, the most direct 17 | method is to [create an 18 | issue](https://github.com/cisagov/postfix-docker/issues) in this 19 | repository. We recommend that you first search through existing 20 | issues (both open and closed) to check if your particular issue has 21 | already been reported. If it has then you might want to add a comment 22 | to the existing issue. If it hasn't then feel free to create a new 23 | one. 24 | 25 | ## Pull requests ## 26 | 27 | If you choose to [submit a pull 28 | request](https://github.com/cisagov/postfix-docker/pulls), you will 29 | notice that our continuous integration (CI) system runs a fairly 30 | extensive set of linters and syntax checkers. Your pull request may 31 | fail these checks, and that's OK. If you want you can stop there and 32 | wait for us to make the necessary corrections to ensure your code 33 | passes the CI checks. 34 | 35 | If you want to make the changes yourself, or if you want to become a 36 | regular contributor, then you will want to set up 37 | [pre-commit](https://pre-commit.com/) on your local machine. Once you 38 | do that, the CI checks will run locally before you even write your 39 | commit message. This speeds up your development cycle considerably. 40 | 41 | ### Setting up pre-commit ### 42 | 43 | There are a few ways to do this, but we prefer to use 44 | [`pyenv`](https://github.com/pyenv/pyenv) and 45 | [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) to 46 | create and manage a Python virtual environment specific to this 47 | project. 48 | 49 | We recommend using the `setup-env` script located in this repository, 50 | as it automates the entire environment configuration process. The 51 | dependencies required to run this script are 52 | [GNU `getopt`](https://github.com/util-linux/util-linux/blob/master/misc-utils/getopt.1.adoc), 53 | [`pyenv`](https://github.com/pyenv/pyenv), and [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv). 54 | If these tools are already configured on your system, you can simply run the 55 | following command: 56 | 57 | ```console 58 | ./setup-env 59 | ``` 60 | 61 | Otherwise, follow the steps below to manually configure your 62 | environment. 63 | 64 | #### Installing and using GNU `getopt`, `pyenv`, and `pyenv-virtualenv` #### 65 | 66 | On macOS, we recommend installing [brew](https://brew.sh/). Then 67 | installation is as simple as `brew install gnu-getopt pyenv pyenv-virtualenv` and 68 | adding this to your profile: 69 | 70 | ```bash 71 | # GNU getopt must be explicitly added to the path since it is 72 | # keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean) 73 | export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH" 74 | 75 | # Setup pyenv 76 | export PYENV_ROOT="$HOME/.pyenv" 77 | export PATH="$PYENV_ROOT/bin:$PATH" 78 | eval "$(pyenv init --path)" 79 | eval "$(pyenv init -)" 80 | eval "$(pyenv virtualenv-init -)" 81 | ``` 82 | 83 | For Linux, Windows Subsystem for Linux (WSL), or macOS (if you 84 | don't want to use `brew`) you can use 85 | [pyenv/pyenv-installer](https://github.com/pyenv/pyenv-installer) to 86 | install the necessary tools. Before running this ensure that you have 87 | installed the prerequisites for your platform according to the 88 | [`pyenv` wiki 89 | page](https://github.com/pyenv/pyenv/wiki/common-build-problems). 90 | GNU `getopt` is included in most Linux distributions as part of the 91 | [`util-linux`](https://github.com/util-linux/util-linux) package. 92 | 93 | On WSL you should treat your platform as whatever Linux distribution 94 | you've chosen to install. 95 | 96 | Once you have installed `pyenv` you will need to add the following 97 | lines to your `.bash_profile` (or `.profile`): 98 | 99 | ```bash 100 | export PYENV_ROOT="$HOME/.pyenv" 101 | export PATH="$PYENV_ROOT/bin:$PATH" 102 | eval "$(pyenv init --path)" 103 | ``` 104 | 105 | and then add the following lines to your `.bashrc`: 106 | 107 | ```bash 108 | eval "$(pyenv init -)" 109 | eval "$(pyenv virtualenv-init -)" 110 | ``` 111 | 112 | If you want more information about setting up `pyenv` once installed, please run 113 | 114 | ```console 115 | pyenv init 116 | ``` 117 | 118 | and 119 | 120 | ```console 121 | pyenv virtualenv-init 122 | ``` 123 | 124 | for the current configuration instructions. 125 | 126 | If you are using a shell other than `bash` you should follow the 127 | instructions that the `pyenv-installer` script outputs. 128 | 129 | You will need to reload your shell for these changes to take effect so 130 | you can begin to use `pyenv`. 131 | 132 | For a list of Python versions that are already installed and ready to 133 | use with `pyenv`, use the command `pyenv versions`. To see a list of 134 | the Python versions available to be installed and used with `pyenv` 135 | use the command `pyenv install --list`. You can read more about 136 | the [many things that `pyenv` can do](https://github.com/pyenv/pyenv/blob/master/COMMANDS.md). 137 | See the [usage information](https://github.com/pyenv/pyenv-virtualenv#usage) 138 | for the additional capabilities that pyenv-virtualenv adds to the `pyenv` 139 | command. 140 | 141 | #### Creating the Python virtual environment #### 142 | 143 | Once `pyenv` and `pyenv-virtualenv` are installed on your system, you 144 | can create and configure the Python virtual environment with these 145 | commands: 146 | 147 | ```console 148 | cd postfix-docker 149 | pyenv virtualenv postfix-docker 150 | pyenv local postfix-docker 151 | pip install --requirement requirements-dev.txt 152 | ``` 153 | 154 | #### Installing the pre-commit hook #### 155 | 156 | Now setting up pre-commit is as simple as: 157 | 158 | ```console 159 | pre-commit install 160 | ``` 161 | 162 | At this point the pre-commit checks will run against any files that 163 | you attempt to commit. If you want to run the checks against the 164 | entire repo, just execute `pre-commit run --all-files`. 165 | 166 | ## Public domain ## 167 | 168 | This project is in the public domain within the United States, and 169 | copyright and related rights in the work worldwide are waived through 170 | the [CC0 1.0 Universal public domain 171 | dedication](https://creativecommons.org/publicdomain/zero/1.0/). 172 | 173 | All contributions to this project will be released under the CC0 174 | dedication. By submitting a pull request, you are agreeing to comply 175 | with this waiver of copyright interest. 176 | -------------------------------------------------------------------------------- /tests/container_test.py: -------------------------------------------------------------------------------- 1 | """Tests for postfix container.""" 2 | 3 | # Standard Python Libraries 4 | from email.message import EmailMessage 5 | from imaplib import IMAP4_SSL 6 | import os 7 | import smtplib 8 | import time 9 | 10 | # Third-Party Libraries 11 | import pytest 12 | 13 | ARCHIVE_PW = "foobar" 14 | ARCHIVE_USER = "mailarchive" 15 | DOMAIN = "example.com" 16 | IMAP_PORT = 1993 17 | MESSAGE = """ 18 | This is a test message sent during the unit tests. 19 | """ 20 | READY_MESSAGE = "daemon started" 21 | RELEASE_TAG = os.getenv("RELEASE_TAG") 22 | TEST_SEND_PW = "lemmy is god" 23 | TEST_SEND_USER = "testsender1" 24 | VERSION_FILE = "src/version.txt" 25 | 26 | 27 | def test_container_count(dockerc): 28 | """Verify the test composition and container.""" 29 | # all parameter allows non-running containers in results 30 | assert ( 31 | len(dockerc.compose.ps(all=True)) == 1 32 | ), "Wrong number of containers were started." 33 | 34 | 35 | def test_wait_for_ready(main_container): 36 | """Wait for container to be ready.""" 37 | TIMEOUT = 10 38 | for i in range(TIMEOUT): 39 | if READY_MESSAGE in main_container.logs(): 40 | break 41 | time.sleep(1) 42 | else: 43 | raise Exception( 44 | f"Container does not seem ready. " 45 | f'Expected "{READY_MESSAGE}" in the log within {TIMEOUT} seconds.' 46 | ) 47 | 48 | 49 | @pytest.mark.parametrize("port", [1025, 1587]) 50 | @pytest.mark.parametrize("to_user", [ARCHIVE_USER, TEST_SEND_USER]) 51 | def test_sending_mail(port, to_user): 52 | """Send an email message to the server.""" 53 | msg = EmailMessage() 54 | msg.set_content(MESSAGE) 55 | msg["Subject"] = f"Test Message on port {port}" 56 | msg["From"] = f"test@{DOMAIN}" 57 | msg["To"] = f"{to_user}@{DOMAIN}" 58 | with smtplib.SMTP("localhost", port=port) as s: 59 | s.send_message(msg) 60 | 61 | 62 | @pytest.mark.parametrize( 63 | "username,password", 64 | [ 65 | (ARCHIVE_USER, ARCHIVE_PW), 66 | (TEST_SEND_USER, TEST_SEND_PW), 67 | pytest.param(ARCHIVE_USER, TEST_SEND_PW, marks=pytest.mark.xfail), 68 | pytest.param("your_mom", "so_fat", marks=pytest.mark.xfail), 69 | ], 70 | ) 71 | def test_imap_login(username, password): 72 | """Test logging in to the IMAP server.""" 73 | with IMAP4_SSL("localhost", IMAP_PORT) as m: 74 | m.login(username, password) 75 | 76 | 77 | # Note that "username" is changed to "user" in this function to work around 78 | # a CodeQL failure for "Clear-text logging of sensitive information". :( 79 | @pytest.mark.parametrize( 80 | "user,password", [(ARCHIVE_USER, ARCHIVE_PW), (TEST_SEND_USER, TEST_SEND_PW)] 81 | ) 82 | def test_imap_messages_exist(user, password): 83 | """Test test existence of our test messages.""" 84 | with IMAP4_SSL("localhost", IMAP_PORT) as m: 85 | m.login(user, password) 86 | typ, data = m.select() 87 | assert typ == "OK", f"Select did not return OK status for {user}" 88 | message_count = int(data[0]) 89 | print(f"{user} inbox message count: {message_count}") 90 | assert message_count > 0, f"Expected message in the {user} inbox" 91 | 92 | 93 | @pytest.mark.parametrize( 94 | "username,password", [(ARCHIVE_USER, ARCHIVE_PW), (TEST_SEND_USER, TEST_SEND_PW)] 95 | ) 96 | def test_imap_reading(username, password): 97 | """Test receiving message from the IMAP server.""" 98 | with IMAP4_SSL("localhost", IMAP_PORT) as m: 99 | m.login(username, password) 100 | typ, data = m.select() 101 | assert typ == "OK", "Select did not return OK status" 102 | message_count = int(data[0]) 103 | print(f"inbox message count: {message_count}") 104 | typ, data = m.search(None, "ALL") 105 | assert typ == "OK", "Search did not return OK status" 106 | message_numbers = data[0].split() 107 | for num in message_numbers: 108 | typ, data = m.fetch(num, "(RFC822)") 109 | assert typ == "OK", f"Fetch of message {num} did not return OK status" 110 | print("-" * 40) 111 | print(f"Message: {num}") 112 | print(data[0][1].decode("utf-8")) 113 | # mark messag as deleted 114 | typ, data = m.store(num, "+FLAGS", "\\Deleted") 115 | assert ( 116 | typ == "OK" 117 | ), f"Storing '\\deleted' flag on message {num} did not return OK status" 118 | # expunge all deleted messages 119 | typ, data = m.expunge() 120 | assert typ == "OK", "Expunge did not return OK status" 121 | 122 | 123 | @pytest.mark.parametrize( 124 | "username,password", [(ARCHIVE_USER, ARCHIVE_PW), (TEST_SEND_USER, TEST_SEND_PW)] 125 | ) 126 | def test_imap_delete_all(username, password): 127 | """Test deleting messages from the IMAP server.""" 128 | with IMAP4_SSL("localhost", IMAP_PORT) as m: 129 | m.login(username, password) 130 | typ, data = m.select() 131 | assert typ == "OK", "Select did not return OK status" 132 | typ, data = m.search(None, "ALL") 133 | assert typ == "OK", "Search did not return OK status" 134 | message_numbers = data[0].split() 135 | for num in message_numbers: 136 | # mark messag as deleted 137 | typ, data = m.store(num, "+FLAGS", "\\Deleted") 138 | assert ( 139 | typ == "OK" 140 | ), f"Storing '\\deleted' flag on message {num} did not return OK status" 141 | # expunge all deleted messages 142 | typ, data = m.expunge() 143 | assert typ == "OK", "Expunge did not return OK status" 144 | 145 | 146 | @pytest.mark.parametrize( 147 | "username,password", [(ARCHIVE_USER, ARCHIVE_PW), (TEST_SEND_USER, TEST_SEND_PW)] 148 | ) 149 | def test_imap_messages_cleared(username, password): 150 | """Test that all messages were expunged.""" 151 | with IMAP4_SSL("localhost", IMAP_PORT) as m: 152 | m.login(username, password) 153 | typ, data = m.select() 154 | assert typ == "OK", "Select did not return OK status" 155 | message_count = int(data[0]) 156 | print(f"inbox message count: {message_count}") 157 | assert message_count == 0, "Expected the inbox to be empty" 158 | 159 | 160 | @pytest.mark.skipif( 161 | RELEASE_TAG in [None, ""], reason="this is not a release (RELEASE_TAG not set)" 162 | ) 163 | def test_release_version(project_version): 164 | """Verify that release tag version agrees with the module version.""" 165 | assert ( 166 | RELEASE_TAG == f"v{project_version}" 167 | ), "RELEASE_TAG does not match the project version" 168 | 169 | 170 | @pytest.mark.skipif( 171 | RELEASE_TAG in [None, ""], reason="this is not a release (RELEASE_TAG not set)" 172 | ) 173 | def test_container_version_label_matches(project_version, main_container): 174 | """Verify the container version label is the correct version.""" 175 | assert ( 176 | main_container.config.labels["org.opencontainers.image.version"] 177 | == project_version 178 | ), "Dockerfile version label does not match project version" 179 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | # Do not commit changes from running pre-commit for pull requests. 4 | autofix_prs: false 5 | # Autoupdate hooks weekly (this is the default). 6 | autoupdate_schedule: weekly 7 | 8 | default_language_version: 9 | # force all unspecified python hooks to run python3 10 | python: python3 11 | 12 | repos: 13 | # Check the pre-commit configuration 14 | - repo: meta 15 | hooks: 16 | - id: check-useless-excludes 17 | 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v6.0.0 20 | hooks: 21 | - id: check-case-conflict 22 | - id: check-executables-have-shebangs 23 | - id: check-json 24 | - id: check-merge-conflict 25 | - id: check-shebang-scripts-are-executable 26 | - id: check-symlinks 27 | - id: check-toml 28 | - id: check-vcs-permalinks 29 | - id: check-xml 30 | - id: debug-statements 31 | - id: destroyed-symlinks 32 | - id: detect-aws-credentials 33 | args: 34 | - --allow-missing-credentials 35 | - id: detect-private-key 36 | # Ignore the fake private key in this repo 37 | exclude: src/secrets/privkey.pem 38 | - id: end-of-file-fixer 39 | - id: mixed-line-ending 40 | args: 41 | - --fix=lf 42 | - id: pretty-format-json 43 | args: 44 | - --autofix 45 | - id: requirements-txt-fixer 46 | - id: trailing-whitespace 47 | 48 | # Text file hooks 49 | - repo: https://github.com/igorshubovych/markdownlint-cli 50 | rev: v0.45.0 51 | hooks: 52 | - id: markdownlint 53 | args: 54 | - --config=.mdl_config.yaml 55 | - repo: https://github.com/rbubley/mirrors-prettier 56 | rev: v3.6.2 57 | hooks: 58 | - id: prettier 59 | - repo: https://github.com/adrienverge/yamllint 60 | rev: v1.37.1 61 | hooks: 62 | - id: yamllint 63 | args: 64 | - --strict 65 | 66 | # GitHub Actions hooks 67 | - repo: https://github.com/python-jsonschema/check-jsonschema 68 | rev: 0.33.3 69 | hooks: 70 | - id: check-github-actions 71 | - id: check-github-workflows 72 | 73 | # pre-commit hooks 74 | - repo: https://github.com/pre-commit/pre-commit 75 | rev: v4.3.0 76 | hooks: 77 | - id: validate_manifest 78 | 79 | # Go hooks 80 | - repo: https://github.com/TekWizely/pre-commit-golang 81 | rev: v1.0.0-rc.2 82 | hooks: 83 | # Go Build 84 | - id: go-build-repo-mod 85 | # Style Checkers 86 | - id: go-critic 87 | # goimports 88 | - id: go-imports-repo 89 | args: 90 | # Write changes to files 91 | - -w 92 | # Go Mod Tidy 93 | - id: go-mod-tidy-repo 94 | # GoSec 95 | - id: go-sec-repo-mod 96 | # StaticCheck 97 | - id: go-staticcheck-repo-mod 98 | # Go Test 99 | - id: go-test-repo-mod 100 | # Go Vet 101 | - id: go-vet-repo-mod 102 | # Nix hooks 103 | - repo: https://github.com/nix-community/nixpkgs-fmt 104 | rev: v1.3.0 105 | hooks: 106 | - id: nixpkgs-fmt 107 | 108 | # Shell script hooks 109 | - repo: https://github.com/scop/pre-commit-shfmt 110 | rev: v3.12.0-2 111 | hooks: 112 | - id: shfmt 113 | args: 114 | # List files that will be formatted 115 | - --list 116 | # Write result to file instead of stdout 117 | - --write 118 | # Indent by two spaces 119 | - --indent 120 | - "2" 121 | # Binary operators may start a line 122 | - --binary-next-line 123 | # Switch cases are indented 124 | - --case-indent 125 | # Redirect operators are followed by a space 126 | - --space-redirects 127 | - repo: https://github.com/shellcheck-py/shellcheck-py 128 | rev: v0.11.0.1 129 | hooks: 130 | - id: shellcheck 131 | 132 | # Python hooks 133 | # Run bandit on the "tests" tree with a configuration 134 | - repo: https://github.com/PyCQA/bandit 135 | rev: 1.8.6 136 | hooks: 137 | - id: bandit 138 | name: bandit (tests tree) 139 | files: tests 140 | args: 141 | - --config=.bandit.yml 142 | # Run bandit on everything except the "tests" tree 143 | - repo: https://github.com/PyCQA/bandit 144 | rev: 1.8.6 145 | hooks: 146 | - id: bandit 147 | name: bandit (everything else) 148 | exclude: tests 149 | - repo: https://github.com/psf/black-pre-commit-mirror 150 | rev: 25.1.0 151 | hooks: 152 | - id: black 153 | - repo: https://github.com/PyCQA/flake8 154 | rev: 7.3.0 155 | hooks: 156 | - id: flake8 157 | additional_dependencies: 158 | - flake8-docstrings==1.7.0 159 | - repo: https://github.com/PyCQA/isort 160 | rev: 6.0.1 161 | hooks: 162 | - id: isort 163 | - repo: https://github.com/pre-commit/mirrors-mypy 164 | rev: v1.18.1 165 | hooks: 166 | - id: mypy 167 | - repo: https://github.com/pypa/pip-audit 168 | rev: v2.9.0 169 | hooks: 170 | - id: pip-audit 171 | args: 172 | # Add any pip requirements files to scan 173 | - --requirement 174 | - requirements-dev.txt 175 | - --requirement 176 | - requirements-test.txt 177 | - --requirement 178 | - requirements.txt 179 | - repo: https://github.com/asottile/pyupgrade 180 | rev: v3.20.0 181 | hooks: 182 | - id: pyupgrade 183 | 184 | # Ansible hooks 185 | - repo: https://github.com/ansible/ansible-lint 186 | rev: v25.9.0 187 | hooks: 188 | - id: ansible-lint 189 | additional_dependencies: 190 | # On its own ansible-lint does not pull in ansible, only 191 | # ansible-core. Therefore, if an Ansible module lives in 192 | # ansible instead of ansible-core, the linter will complain 193 | # that the module is unknown. In these cases it is 194 | # necessary to add the ansible package itself as an 195 | # additional dependency, with the same pinning as is done in 196 | # requirements-test.txt of cisagov/skeleton-ansible-role. 197 | # 198 | # Version 10 is required because the pip-audit pre-commit 199 | # hook identifies a vulnerability in ansible-core 2.16.13, 200 | # but all versions of ansible 9 have a dependency on 201 | # ~=2.16.X. 202 | # 203 | # It is also a good idea to go ahead and upgrade to version 204 | # 10 since version 9 is going EOL at the end of November: 205 | # https://endoflife.date/ansible 206 | # - ansible>=10,<11 207 | # ansible-core 2.16.3 through 2.16.6 suffer from the bug 208 | # discussed in ansible/ansible#82702, which breaks any 209 | # symlinked files in vars, tasks, etc. for any Ansible role 210 | # installed via ansible-galaxy. Hence we never want to 211 | # install those versions. 212 | # 213 | # Note that the pip-audit pre-commit hook identifies a 214 | # vulnerability in ansible-core 2.16.13. The pin of 215 | # ansible-core to >=2.17 effectively also pins ansible to 216 | # >=10. 217 | # 218 | # It is also a good idea to go ahead and upgrade to 219 | # ansible-core 2.17 since security support for ansible-core 220 | # 2.16 ends this month: 221 | # https://docs.ansible.com/ansible/devel/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix 222 | # 223 | # Note that any changes made to this dependency must also be 224 | # made in requirements.txt in cisagov/skeleton-packer and 225 | # requirements-test.txt in cisagov/skeleton-ansible-role. 226 | - ansible-core>=2.17 227 | 228 | # Terraform hooks 229 | - repo: https://github.com/antonbabenko/pre-commit-terraform 230 | rev: v1.100.0 231 | hooks: 232 | - id: terraform_fmt 233 | - id: terraform_validate 234 | 235 | # Docker hooks 236 | - repo: https://github.com/IamTheFij/docker-pre-commit 237 | rev: v3.0.1 238 | hooks: 239 | - id: docker-compose-check 240 | 241 | # Packer hooks 242 | - repo: https://github.com/cisagov/pre-commit-packer 243 | rev: v0.3.1 244 | hooks: 245 | - id: packer_fmt 246 | - id: packer_validate 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postfix-docker 📮🐳 # 2 | 3 | [![GitHub Build Status](https://github.com/cisagov/postfix-docker/workflows/build/badge.svg)](https://github.com/cisagov/postfix-docker/actions/workflows/build.yml) 4 | [![CodeQL](https://github.com/cisagov/postfix-docker/workflows/CodeQL/badge.svg)](https://github.com/cisagov/postfix-docker/actions/workflows/codeql-analysis.yml) 5 | 6 | ## Docker Image ## 7 | 8 | [![Docker Pulls](https://img.shields.io/docker/pulls/cisagov/postfix)](https://hub.docker.com/r/cisagov/postfix) 9 | [![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/cisagov/postfix)](https://hub.docker.com/r/cisagov/postfix) 10 | [![Platforms](https://img.shields.io/badge/platforms-386%20%7C%20amd64%20%7C%20arm%2Fv7%20%7C%20arm64-blue)](https://hub.docker.com/r/cisagov/postfix/tags) 11 | 12 | Creates a Docker container with an installation of the 13 | [postfix](http://postfix.org) MTA. Additionally it has an IMAP 14 | server ([dovecot](https://dovecot.org)) for accessing the archives 15 | of sent email. All email is BCC'd to the `mailarchive` account. 16 | 17 | ## Running ## 18 | 19 | ### Running with Docker ### 20 | 21 | To run the `cisagov/postfix` image via Docker: 22 | 23 | ```console 24 | docker run cisagov/postfix:0.2.0 25 | ``` 26 | 27 | ### Running with Docker Compose ### 28 | 29 | 1. Create a `compose.yml` file similar to the one below to use [Docker Compose](https://docs.docker.com/compose/) 30 | or use the [sample `compose.yml`](compose.yml) provided with 31 | this repository. 32 | 33 | ```yaml 34 | --- 35 | name: postfix-docker 36 | 37 | services: 38 | postfix: 39 | build: 40 | context: . 41 | dockerfile: Dockerfile 42 | image: cisagov/postfix 43 | init: true 44 | restart: always 45 | environment: 46 | - PRIMARY_DOMAIN=example.com 47 | - RELAY_IP=172.16.202.1/32 48 | networks: 49 | front: 50 | ipv4_address: 172.16.202.2 51 | ports: 52 | - target: "25" 53 | published: "1025" 54 | protocol: tcp 55 | mode: host 56 | - target: "587" 57 | published: "1587" 58 | protocol: tcp 59 | mode: host 60 | - target: "993" 61 | published: "1993" 62 | protocol: tcp 63 | mode: host 64 | 65 | networks: 66 | front: 67 | driver: bridge 68 | ipam: 69 | driver: default 70 | config: 71 | - subnet: 172.16.202.0/24 72 | ``` 73 | 74 | 1. Start the container and detach: 75 | 76 | ```console 77 | docker compose up --detach 78 | ``` 79 | 80 | ## Using secrets with your container ## 81 | 82 | This container also supports passing sensitive values via [Docker 83 | secrets](https://docs.docker.com/engine/swarm/secrets/). Passing sensitive 84 | values like your credentials can be more secure using secrets than using 85 | environment variables. See the 86 | [secrets](#secrets) section below for a table of all supported secret files. 87 | 88 | 1. To use secrets, populate the following files in the `src/secrets` directory: 89 | 90 | - `fullchain.pem` 91 | - `privkey.pem` 92 | - `users.txt` 93 | 94 | 1. Then add the secrets to your `compose.yml` file: 95 | 96 | ```yaml 97 | --- 98 | name: postfix-docker 99 | 100 | secrets: 101 | fullchain_pem: 102 | file: ./src/secrets/fullchain.pem 103 | privkey_pem: 104 | file: ./src/secrets/privkey.pem 105 | users_txt: 106 | file: ./src/secrets/users.txt 107 | 108 | services: 109 | postfix: 110 | build: 111 | context: . 112 | dockerfile: Dockerfile 113 | image: cisagov/postfix 114 | init: true 115 | restart: always 116 | environment: 117 | - PRIMARY_DOMAIN=example.com 118 | - RELAY_IP=172.16.202.1/32 119 | networks: 120 | front: 121 | ipv4_address: 172.16.202.2 122 | ports: 123 | - target: "25" 124 | published: "1025" 125 | protocol: tcp 126 | mode: host 127 | - target: "587" 128 | published: "1587" 129 | protocol: tcp 130 | mode: host 131 | - target: "993" 132 | published: "1993" 133 | protocol: tcp 134 | mode: host 135 | secrets: 136 | - source: fullchain_pem 137 | target: fullchain.pem 138 | - source: privkey_pem 139 | target: privkey.pem 140 | - source: users_txt 141 | target: users.txt 142 | 143 | networks: 144 | front: 145 | driver: bridge 146 | ipam: 147 | driver: default 148 | config: 149 | - subnet: 172.16.202.0/24 150 | ``` 151 | 152 | ## Updating your container ## 153 | 154 | ### Docker Compose ### 155 | 156 | 1. Pull the new image from Docker Hub: 157 | 158 | ```console 159 | docker compose pull 160 | ``` 161 | 162 | 1. Recreate the running container by following the [previous instructions](#running-with-docker-compose): 163 | 164 | ```console 165 | docker compose up --detach 166 | ``` 167 | 168 | ### Docker ### 169 | 170 | 1. Stop the running container: 171 | 172 | ```console 173 | docker stop 174 | ``` 175 | 176 | 1. Pull the new image: 177 | 178 | ```console 179 | docker pull cisagov/postfix:0.2.0 180 | ``` 181 | 182 | 1. Recreate and run the container by following the [previous instructions](#running-with-docker). 183 | 184 | ## Image tags ## 185 | 186 | The images of this container are tagged with [semantic 187 | versions](https://semver.org) of the underlying Postfix project that they 188 | containerize. It is recommended that most users use a version tag (e.g. 189 | `:0.2.0`). 190 | 191 | | Image:tag | Description | 192 | |-----------|-------------| 193 | |`cisagov/postfix:0.2.0`| An exact release version. | 194 | |`cisagov/postfix:0.2`| The most recent release matching the major and minor version numbers. | 195 | |`cisagov/postfix:0`| The most recent release matching the major version number. | 196 | |`cisagov/postfix:edge` | The most recent image built from a merge into the `develop` branch of this repository. | 197 | |`cisagov/postfix:nightly` | A nightly build of the `develop` branch of this repository. | 198 | |`cisagov/postfix:latest`| The most recent release image pushed to a container registry. Pulling an image using the `:latest` tag [should be avoided.](https://vsupalov.com/docker-latest-tag/) | 199 | 200 | See the [tags tab](https://hub.docker.com/r/cisagov/postfix/tags) on Docker 201 | Hub for a list of all the supported tags. 202 | 203 | ## Volumes ## 204 | 205 | | Mount point | Purpose | 206 | |-------------|---------| 207 | | `/var/log` | System logs | 208 | | `/var/spool/postfix` | Mail queues | 209 | 210 | ## Ports ## 211 | 212 | The following ports are exposed by this container: 213 | 214 | | Port | Purpose | 215 | |------|----------------| 216 | | 25 | SMTP relay | 217 | | 587 | Mail submission | 218 | | 993 | IMAPS | 219 | 220 | The sample [Docker composition](compose.yml) publishes the 221 | exposed ports at 1025, 1587, and 1993, respectively. 222 | 223 | ## Environment variables ## 224 | 225 | ### Required ### 226 | 227 | | Name | Purpose | 228 | |-------|---------| 229 | | `PRIMARY_DOMAIN` | The primary domain of the mail server. | 230 | 231 | ### Optional ### 232 | 233 | | Name | Purpose | Default | 234 | |-------|---------|---------| 235 | | `RELAY_IP` | An IP address that is allowed to relay mail without authentication. | `null` | 236 | 237 | ## Secrets ## 238 | 239 | | Filename | Purpose | 240 | |--------------|---------| 241 | | `fullchain.pem` | Public key for the Postfix server. | 242 | | `privkey.pem` | Private key for the Postfix server. | 243 | | `users.txt` | Mail account credentials to create at startup. | 244 | 245 | ## Building from source ## 246 | 247 | Build the image locally using this git repository as the [build context](https://docs.docker.com/engine/reference/commandline/build/#git-repositories): 248 | 249 | ```console 250 | docker build \ 251 | --tag cisagov/postfix:0.2.0 \ 252 | https://github.com/cisagov/postfix-docker.git#develop 253 | ``` 254 | 255 | ## Cross-platform builds ## 256 | 257 | To create images that are compatible with other platforms, you can use the 258 | [`buildx`](https://docs.docker.com/buildx/working-with-buildx/) feature of 259 | Docker: 260 | 261 | 1. Copy the project to your machine using the `Code` button above 262 | or the command line: 263 | 264 | ```console 265 | git clone https://github.com/cisagov/postfix-docker.git 266 | cd postfix-docker 267 | ``` 268 | 269 | 1. Create the `Dockerfile-x` file with `buildx` platform support: 270 | 271 | ```console 272 | ./buildx-dockerfile.sh 273 | ``` 274 | 275 | 1. Build the image using `buildx`: 276 | 277 | ```console 278 | docker buildx build \ 279 | --file Dockerfile-x \ 280 | --platform linux/amd64 \ 281 | --output type=docker \ 282 | --tag cisagov/postfix:0.2.0 . 283 | ``` 284 | 285 | ## Contributing ## 286 | 287 | We welcome contributions! Please see [`CONTRIBUTING.md`](CONTRIBUTING.md) for 288 | details. 289 | 290 | ## License ## 291 | 292 | This project is in the worldwide [public domain](LICENSE.md). 293 | 294 | This project is in the public domain within the United States, and 295 | copyright and related rights in the work worldwide are waived through 296 | the [CC0 1.0 Universal public domain 297 | dedication](https://creativecommons.org/publicdomain/zero/1.0/). 298 | 299 | All contributions to this project will be released under the CC0 300 | dedication. By submitting a pull request, you are agreeing to comply 301 | with this waiver of copyright interest. 302 | -------------------------------------------------------------------------------- /setup-env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -o pipefail 6 | 7 | USAGE=$( 8 | cat << 'END_OF_LINE' 9 | Configure a development environment for this repository. 10 | 11 | It does the following: 12 | - Allows the user to specify the Python version to use for the virtual environment. 13 | - Allows the user to specify a name for the virtual environment. 14 | - Verifies pyenv and pyenv-virtualenv are installed. 15 | - Creates the Python virtual environment. 16 | - Configures the activation of the virtual enviroment for the repo directory. 17 | - Installs the requirements needed for development. 18 | - Installs git pre-commit hooks. 19 | - Configures git remotes for upstream "lineage" repositories. 20 | 21 | Usage: 22 | setup-env [--venv-name venv_name] [--python-version python_version] 23 | setup-env (-h | --help) 24 | 25 | Options: 26 | -f | --force Delete virtual enviroment if it already exists. 27 | -h | --help Show this message. 28 | -i | --install-hooks Install hook environments for all environments in the 29 | pre-commit config file. 30 | -l | --list-versions List available Python versions and select one interactively. 31 | -v | --venv-name Specify the name of the virtual environment. 32 | -p | --python-version Specify the Python version for the virtual environment. 33 | 34 | END_OF_LINE 35 | ) 36 | 37 | # Display pyenv's installed Python versions 38 | python_versions() { 39 | pyenv versions --bare --skip-aliases --skip-envs 40 | } 41 | 42 | check_python_version() { 43 | local version=$1 44 | 45 | # This is a valid regex for semantically correct Python version strings. 46 | # For more information see here: 47 | # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 48 | # Break down the regex into readable parts major.minor.patch 49 | local major="0|[1-9]\d*" 50 | local minor="0|[1-9]\d*" 51 | local patch="0|[1-9]\d*" 52 | 53 | # Splitting the prerelease part for readability 54 | # Start of the prerelease 55 | local prerelease="(?:-" 56 | # Numeric or alphanumeric identifiers 57 | local prerelease+="(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" 58 | # Additional dot-separated identifiers 59 | local prerelease+="(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*" 60 | # End of the prerelease, making it optional 61 | local prerelease+=")?" 62 | # Optional build metadata 63 | local build="(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" 64 | 65 | # Final regex composed of parts 66 | local regex="^($major)\.($minor)\.($patch)$prerelease$build$" 67 | 68 | # This checks if the Python version does not match the regex pattern specified in $regex, 69 | # using Perl for regex matching. If the pattern is not found, then prompt the user with 70 | # the invalid version message. 71 | if ! echo "$version" | perl -ne "exit(!/$regex/)"; then 72 | echo "Invalid version of Python: Python follows semantic versioning," \ 73 | "so any version string that is not a valid semantic version is an" \ 74 | "invalid version of Python." 75 | exit 1 76 | # Else if the Python version isn't installed then notify the user. 77 | # grep -E is used for searching through text lines that match the 78 | # specific version. 79 | elif ! python_versions | grep -E "^${version}$" > /dev/null; then 80 | echo "Error: Python version $version is not installed." 81 | echo "Installed Python versions are:" 82 | python_versions 83 | exit 1 84 | else 85 | echo "Using Python version $version" 86 | fi 87 | } 88 | 89 | # Flag to force deletion and creation of virtual environment 90 | FORCE=0 91 | 92 | # Initialize the other flags 93 | INSTALL_HOOKS=0 94 | LIST_VERSIONS=0 95 | PYTHON_VERSION="" 96 | VENV_NAME="" 97 | 98 | # Define long options 99 | LONGOPTS="force,help,install-hooks,list-versions,python-version:,venv-name:" 100 | 101 | # Define short options for getopt 102 | SHORTOPTS="fhilp:v:" 103 | 104 | # Check for GNU getopt by testing for long option support. GNU getopt supports 105 | # the "--test" option and will return exit code 4 while POSIX/BSD getopt does 106 | # not and will return exit code 0. 107 | if getopt --test > /dev/null 2>&1; then 108 | cat << 'END_OF_LINE' 109 | 110 | Please note, this script requires GNU getopt due to its enhanced 111 | functionality and compatibility with certain script features that 112 | are not supported by the POSIX getopt found in some systems, particularly 113 | those with a non-GNU version of getopt. This distinction is crucial 114 | as a system might have a non-GNU version of getopt installed by default, 115 | which could lead to unexpected behavior. 116 | 117 | On macOS, we recommend installing brew (https://brew.sh/). Then installation 118 | is as simple as `brew install gnu-getopt` and adding this to your 119 | profile: 120 | 121 | export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH" 122 | 123 | GNU getopt must be explicitly added to the PATH since it 124 | is keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean). 125 | 126 | END_OF_LINE 127 | exit 1 128 | fi 129 | 130 | # Check to see if pyenv is installed 131 | if [ -z "$(command -v pyenv)" ] || { [ -z "$(command -v pyenv-virtualenv)" ] && [ ! -f "$(pyenv root)/plugins/pyenv-virtualenv/bin/pyenv-virtualenv" ]; }; then 132 | echo "pyenv and pyenv-virtualenv are required." 133 | if [[ "$OSTYPE" == "darwin"* ]]; then 134 | cat << 'END_OF_LINE' 135 | 136 | On macOS, we recommend installing brew, https://brew.sh/. Then installation 137 | is as simple as `brew install pyenv pyenv-virtualenv` and adding this to your 138 | profile: 139 | 140 | eval "$(pyenv init -)" 141 | eval "$(pyenv virtualenv-init -)" 142 | 143 | END_OF_LINE 144 | 145 | fi 146 | cat << 'END_OF_LINE' 147 | For Linux, Windows Subsystem for Linux (WSL), or macOS (if you don't want 148 | to use "brew") you can use https://github.com/pyenv/pyenv-installer to install 149 | the necessary tools. Before running this ensure that you have installed the 150 | prerequisites for your platform according to the pyenv wiki page, 151 | https://github.com/pyenv/pyenv/wiki/common-build-problems. 152 | 153 | On WSL you should treat your platform as whatever Linux distribution you've 154 | chosen to install. 155 | 156 | Once you have installed "pyenv" you will need to add the following lines to 157 | your ".bashrc": 158 | 159 | export PATH="$PATH:$HOME/.pyenv/bin" 160 | eval "$(pyenv init -)" 161 | eval "$(pyenv virtualenv-init -)" 162 | END_OF_LINE 163 | exit 1 164 | fi 165 | 166 | # Use GNU getopt to parse options 167 | if ! PARSED=$(getopt --options $SHORTOPTS --longoptions $LONGOPTS --name "$0" -- "$@"); then 168 | echo "Error parsing options" 169 | exit 1 170 | fi 171 | eval set -- "$PARSED" 172 | 173 | while true; do 174 | case "$1" in 175 | -f | --force) 176 | FORCE=1 177 | shift 178 | ;; 179 | -h | --help) 180 | echo "$USAGE" 181 | exit 0 182 | ;; 183 | -i | --install-hooks) 184 | INSTALL_HOOKS=1 185 | shift 186 | ;; 187 | -l | --list-versions) 188 | LIST_VERSIONS=1 189 | shift 190 | ;; 191 | -p | --python-version) 192 | PYTHON_VERSION="$2" 193 | shift 2 194 | # Check the Python version being passed in. 195 | check_python_version "$PYTHON_VERSION" 196 | ;; 197 | -v | --venv-name) 198 | VENV_NAME="$2" 199 | shift 2 200 | ;; 201 | --) 202 | shift 203 | break 204 | ;; 205 | *) 206 | # Unreachable due to GNU getopt handling all options 207 | echo "Programming error" 208 | exit 64 209 | ;; 210 | esac 211 | done 212 | 213 | # Determine the virtual environment name 214 | if [ -n "$VENV_NAME" ]; then 215 | # Use the user-provided environment name 216 | env_name="$VENV_NAME" 217 | else 218 | # Set the environment name to the last part of the working directory. 219 | env_name=${PWD##*/} 220 | fi 221 | 222 | # List Python versions and select one interactively. 223 | if [ $LIST_VERSIONS -ne 0 ]; then 224 | echo Available Python versions: 225 | python_versions 226 | # Read the user's desired Python version. 227 | # -r: treat backslashes as literal, -p: display prompt before input. 228 | read -r -p "Enter the desired Python version: " PYTHON_VERSION 229 | # Check the Python version being passed in. 230 | check_python_version "$PYTHON_VERSION" 231 | fi 232 | 233 | # Remove any lingering local configuration. 234 | if [ $FORCE -ne 0 ]; then 235 | rm -f .python-version 236 | pyenv virtualenv-delete --force "${env_name}" || true 237 | elif [[ -f .python-version ]]; then 238 | cat << 'END_OF_LINE' 239 | An existing .python-version file was found. Either remove this file yourself 240 | or re-run with the --force option to have it deleted along with the associated 241 | virtual environment. 242 | 243 | rm .python-version 244 | 245 | END_OF_LINE 246 | exit 1 247 | fi 248 | 249 | # Create a new virtual environment for this project 250 | # 251 | # If $PYTHON_VERSION is undefined then the current pyenv Python version will be used. 252 | # 253 | # We can't quote ${PYTHON_VERSION:=} below since if the variable is 254 | # undefined then we want nothing to appear; this is the reason for the 255 | # "shellcheck disable" line below. 256 | # 257 | # shellcheck disable=SC2086 258 | if ! pyenv virtualenv ${PYTHON_VERSION:=} "${env_name}"; then 259 | cat << END_OF_LINE 260 | An existing virtual environment named $env_name was found. Either delete this 261 | environment yourself or re-run with the --force option to have it deleted. 262 | 263 | pyenv virtualenv-delete ${env_name} 264 | 265 | END_OF_LINE 266 | exit 1 267 | fi 268 | 269 | # Set the local application-specific Python version(s) by writing the 270 | # version name to a file named `.python-version'. 271 | pyenv local "${env_name}" 272 | 273 | # Upgrade pip and friends 274 | python3 -m pip install --upgrade pip setuptools wheel 275 | 276 | # Find a requirements file (if possible) and install 277 | for req_file in "requirements-dev.txt" "requirements-test.txt" "requirements.txt"; do 278 | if [[ -f $req_file ]]; then 279 | pip install --requirement $req_file 280 | break 281 | fi 282 | done 283 | 284 | # Install git pre-commit hooks now or later. 285 | pre-commit install ${INSTALL_HOOKS:+"--install-hooks"} 286 | 287 | # Setup git remotes from lineage configuration 288 | # This could fail if the remotes are already setup, but that is ok. 289 | set +o errexit 290 | 291 | eval "$( 292 | python3 << 'END_OF_LINE' 293 | from pathlib import Path 294 | import yaml 295 | import sys 296 | 297 | LINEAGE_CONFIG = Path(".github/lineage.yml") 298 | 299 | if not LINEAGE_CONFIG.exists(): 300 | print("No lineage configuration found.", file=sys.stderr) 301 | sys.exit(0) 302 | 303 | with LINEAGE_CONFIG.open("r") as f: 304 | lineage = yaml.safe_load(stream=f) 305 | 306 | if lineage["version"] == "1": 307 | for parent_name, v in lineage["lineage"].items(): 308 | remote_url = v["remote-url"] 309 | print(f"git remote add {parent_name} {remote_url};") 310 | print(f"git remote set-url --push {parent_name} no_push;") 311 | else: 312 | print(f'Unsupported lineage version: {lineage["version"]}', file=sys.stderr) 313 | END_OF_LINE 314 | )" 315 | 316 | # Qapla' 317 | echo "Success!" 318 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | 4 | on: # yamllint disable-line rule:truthy 5 | merge_group: 6 | types: 7 | - checks_requested 8 | pull_request: 9 | push: 10 | branches: 11 | - "**" 12 | tags: 13 | - v*.*.* 14 | repository_dispatch: 15 | # Respond to rebuild requests. See: https://github.com/cisagov/action-apb/ 16 | types: 17 | - apb 18 | schedule: 19 | - cron: 0 10 * * * # everyday at 10am 20 | workflow_dispatch: 21 | inputs: 22 | image-tag: 23 | default: dispatch 24 | description: Tag to apply to pushed images 25 | required: true 26 | remote-shell: 27 | default: "false" 28 | description: Debug with remote shell 29 | required: true 30 | 31 | # Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, 32 | # nounset, errexit, and pipefail. The `-x` will print all commands as they are 33 | # run. Please see the GitHub Actions documentation for more information: 34 | # https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs 35 | defaults: 36 | run: 37 | shell: bash -Eueo pipefail -x {0} 38 | 39 | env: 40 | PIP_CACHE_DIR: ~/.cache/pip 41 | PRE_COMMIT_CACHE_DIR: ~/.cache/pre-commit 42 | RUN_TMATE: ${{ secrets.RUN_TMATE }} 43 | TERRAFORM_DOCS_REPO_BRANCH_NAME: improvement/support_atx_closed_markdown_headers 44 | TERRAFORM_DOCS_REPO_DEPTH: 1 45 | TERRAFORM_DOCS_REPO_URL: https://github.com/mcdonnnj/terraform-docs.git 46 | 47 | jobs: 48 | diagnostics: 49 | name: Run diagnostics 50 | # This job does not need any permissions 51 | permissions: {} 52 | runs-on: ubuntu-latest 53 | steps: 54 | # Note that a duplicate of this step must be added at the top of 55 | # each job. 56 | - name: Apply standard cisagov job preamble 57 | uses: cisagov/action-job-preamble@v1 58 | with: 59 | check_github_status: "true" 60 | # This functionality is poorly implemented and has been 61 | # causing problems due to the MITM implementation hogging or 62 | # leaking memory. As a result we disable it by default. If 63 | # you want to temporarily enable it, simply set 64 | # monitor_permissions equal to "true". 65 | # 66 | # TODO: Re-enable this functionality when practical. See 67 | # cisagov/skeleton-generic#207 for more details. 68 | monitor_permissions: "false" 69 | output_workflow_context: "true" 70 | # Use a variable to specify the permissions monitoring 71 | # configuration. By default this will yield the 72 | # configuration stored in the cisagov organization-level 73 | # variable, but if you want to use a different configuration 74 | # then simply: 75 | # 1. Create a repository-level variable with the name 76 | # ACTIONS_PERMISSIONS_CONFIG. 77 | # 2. Set this new variable's value to the configuration you 78 | # want to use for this repository. 79 | # 80 | # Note in particular that changing the permissions 81 | # monitoring configuration *does not* require you to modify 82 | # this workflow. 83 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 84 | lint: 85 | # Checks out the source and runs pre-commit hooks. Detects coding errors 86 | # and style deviations. 87 | name: Lint sources 88 | needs: 89 | - diagnostics 90 | permissions: 91 | # actions/checkout needs this to fetch code 92 | contents: read 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Apply standard cisagov job preamble 96 | uses: cisagov/action-job-preamble@v1 97 | with: 98 | # This functionality is poorly implemented and has been 99 | # causing problems due to the MITM implementation hogging or 100 | # leaking memory. As a result we disable it by default. If 101 | # you want to temporarily enable it, simply set 102 | # monitor_permissions equal to "true". 103 | # 104 | # TODO: Re-enable this functionality when practical. See 105 | # cisagov/skeleton-generic#207 for more details. 106 | monitor_permissions: "false" 107 | # Use a variable to specify the permissions monitoring 108 | # configuration. By default this will yield the 109 | # configuration stored in the cisagov organization-level 110 | # variable, but if you want to use a different configuration 111 | # then simply: 112 | # 1. Create a repository-level variable with the name 113 | # ACTIONS_PERMISSIONS_CONFIG. 114 | # 2. Set this new variable's value to the configuration you 115 | # want to use for this repository. 116 | # 117 | # Note in particular that changing the permissions 118 | # monitoring configuration *does not* require you to modify 119 | # this workflow. 120 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 121 | - id: setup-env 122 | uses: cisagov/setup-env-github-action@v1 123 | - uses: actions/checkout@v5 124 | - id: setup-python 125 | uses: actions/setup-python@v6 126 | with: 127 | python-version: ${{ steps.setup-env.outputs.python-version }} 128 | # We need the Go version and Go cache location for the actions/cache step, 129 | # so the Go installation must happen before that. 130 | - id: setup-go 131 | uses: actions/setup-go@v6 132 | with: 133 | # There is no expectation for actual Go code so we disable caching as 134 | # it relies on the existence of a go.sum file. 135 | cache: false 136 | go-version: ${{ steps.setup-env.outputs.go-version }} 137 | - id: go-cache 138 | name: Lookup Go cache directory 139 | run: | 140 | echo "dir=$(go env GOCACHE)" >> $GITHUB_OUTPUT 141 | - uses: actions/cache@v4 142 | env: 143 | BASE_CACHE_KEY: ${{ github.job }}-${{ runner.os }}-\ 144 | py${{ steps.setup-python.outputs.python-version }}-\ 145 | go${{ steps.setup-go.outputs.go-version }}-\ 146 | packer${{ steps.setup-env.outputs.packer-version }}-\ 147 | tf${{ steps.setup-env.outputs.terraform-version }}- 148 | with: 149 | key: ${{ env.BASE_CACHE_KEY }}\ 150 | ${{ hashFiles('**/requirements-test.txt') }}-\ 151 | ${{ hashFiles('**/requirements.txt') }}-\ 152 | ${{ hashFiles('**/.pre-commit-config.yaml') }} 153 | # Note that the .terraform directory IS NOT included in the 154 | # cache because if we were caching, then we would need to use 155 | # the `-upgrade=true` option. This option blindly pulls down the 156 | # latest modules and providers instead of checking to see if an 157 | # update is required. That behavior defeats the benefits of caching. 158 | # so there is no point in doing it for the .terraform directory. 159 | path: | 160 | ${{ env.PIP_CACHE_DIR }} 161 | ${{ env.PRE_COMMIT_CACHE_DIR }} 162 | ${{ steps.go-cache.outputs.dir }} 163 | restore-keys: | 164 | ${{ env.BASE_CACHE_KEY }} 165 | - uses: hashicorp/setup-packer@v3 166 | with: 167 | version: ${{ steps.setup-env.outputs.packer-version }} 168 | - uses: hashicorp/setup-terraform@v3 169 | with: 170 | terraform_version: ${{ steps.setup-env.outputs.terraform-version }} 171 | - name: Install go-critic 172 | env: 173 | PACKAGE_URL: github.com/go-critic/go-critic/cmd/gocritic 174 | PACKAGE_VERSION: ${{ steps.setup-env.outputs.go-critic-version }} 175 | run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} 176 | - name: Install goimports 177 | env: 178 | PACKAGE_URL: golang.org/x/tools/cmd/goimports 179 | PACKAGE_VERSION: ${{ steps.setup-env.outputs.goimports-version }} 180 | run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} 181 | - name: Install gosec 182 | env: 183 | PACKAGE_URL: github.com/securego/gosec/v2/cmd/gosec 184 | PACKAGE_VERSION: ${{ steps.setup-env.outputs.gosec-version }} 185 | run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} 186 | - name: Install staticcheck 187 | env: 188 | PACKAGE_URL: honnef.co/go/tools/cmd/staticcheck 189 | PACKAGE_VERSION: ${{ steps.setup-env.outputs.staticcheck-version }} 190 | run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} 191 | # TODO: https://github.com/cisagov/skeleton-generic/issues/165 192 | # We are temporarily using @mcdonnnj's forked branch of terraform-docs 193 | # until his PR: https://github.com/terraform-docs/terraform-docs/pull/745 194 | # is approved. This temporary fix will allow for ATX header support when 195 | # terraform-docs is run during linting. 196 | - name: Clone ATX headers branch from terraform-docs fork 197 | run: | 198 | git clone \ 199 | --branch $TERRAFORM_DOCS_REPO_BRANCH_NAME \ 200 | --depth $TERRAFORM_DOCS_REPO_DEPTH \ 201 | --single-branch \ 202 | $TERRAFORM_DOCS_REPO_URL /tmp/terraform-docs 203 | - name: Build and install terraform-docs binary 204 | run: | 205 | go build \ 206 | -C /tmp/terraform-docs \ 207 | -o $(go env GOPATH)/bin/terraform-docs 208 | - name: Install dependencies 209 | run: | 210 | python -m pip install --upgrade pip setuptools wheel 211 | pip install --upgrade --requirement requirements-test.txt 212 | - name: Set up pre-commit hook environments 213 | run: pre-commit install-hooks 214 | - name: Run pre-commit on all files 215 | run: pre-commit run --all-files 216 | - name: Setup tmate debug session 217 | uses: mxschmitt/action-tmate@v3 218 | if: env.RUN_TMATE 219 | repo-metadata: 220 | name: Gather repository metadata 221 | needs: 222 | - diagnostics 223 | permissions: 224 | # actions/checkout needs this to fetch code 225 | contents: read 226 | uses: ./.github/workflows/_repo-metadata.yml 227 | prepare: 228 | # Generate Docker image metadata using the docker/metadata-action GitHub Action. 229 | name: Prepare build variables 230 | needs: 231 | - diagnostics 232 | - repo-metadata 233 | outputs: 234 | labels: ${{ steps.generate-metadata.outputs.labels }} 235 | tags: ${{ steps.generate-metadata.outputs.tags }} 236 | permissions: 237 | # actions/checkout needs this to fetch code 238 | contents: read 239 | runs-on: ubuntu-latest 240 | steps: 241 | - name: Apply standard cisagov job preamble 242 | uses: cisagov/action-job-preamble@v1 243 | with: 244 | # This functionality is poorly implemented and has been 245 | # causing problems due to the MITM implementation hogging or 246 | # leaking memory. As a result we disable it by default. If 247 | # you want to temporarily enable it, simply set 248 | # monitor_permissions equal to "true". 249 | # 250 | # TODO: Re-enable this functionality when practical. See 251 | # cisagov/skeleton-docker#224 for more details. 252 | monitor_permissions: "false" 253 | # Use a variable to specify the permissions monitoring 254 | # configuration. By default this will yield the 255 | # configuration stored in the cisagov organization-level 256 | # variable, but if you want to use a different configuration 257 | # then simply: 258 | # 1. Create a repository-level variable with the name 259 | # ACTIONS_PERMISSIONS_CONFIG. 260 | # 2. Set this new variable's value to the configuration you 261 | # want to use for this repository. 262 | # 263 | # Note in particular that changing the permissions 264 | # monitoring configuration *does not* require you to modify 265 | # this workflow. 266 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 267 | - uses: actions/checkout@v5 268 | - id: generate-metadata 269 | name: Generate Docker image metadata 270 | uses: docker/metadata-action@v5 271 | with: 272 | images: | 273 | ${{ needs.repo-metadata.outputs.image-name }} 274 | ghcr.io/${{ needs.repo-metadata.outputs.image-name }} 275 | tags: | 276 | type=edge 277 | type=raw,event=workflow_dispatch,value=${{ github.event.inputs.image-tag }} 278 | type=ref,event=branch 279 | type=ref,event=pr 280 | type=ref,event=tag 281 | type=schedule 282 | type=semver,pattern={{major}} 283 | type=semver,pattern={{major}}.{{minor}} 284 | type=semver,pattern={{version}} 285 | type=sha 286 | - name: Setup tmate debug session 287 | uses: mxschmitt/action-tmate@v3 288 | if: github.event.inputs.remote-shell == 'true' || env.RUN_TMATE 289 | build: 290 | # Builds a single test image for the native platform. This image is saved 291 | # as an artifact and loaded by the test job. 292 | name: Build test image 293 | needs: 294 | - diagnostics 295 | - repo-metadata 296 | - prepare 297 | permissions: 298 | # actions/checkout needs this to fetch code 299 | contents: read 300 | runs-on: ubuntu-latest 301 | steps: 302 | - name: Apply standard cisagov job preamble 303 | uses: cisagov/action-job-preamble@v1 304 | with: 305 | # This functionality is poorly implemented and has been 306 | # causing problems due to the MITM implementation hogging or 307 | # leaking memory. As a result we disable it by default. If 308 | # you want to temporarily enable it, simply set 309 | # monitor_permissions equal to "true". 310 | # 311 | # TODO: Re-enable this functionality when practical. See 312 | # cisagov/skeleton-docker#224 for more details. 313 | monitor_permissions: "false" 314 | # Use a variable to specify the permissions monitoring 315 | # configuration. By default this will yield the 316 | # configuration stored in the cisagov organization-level 317 | # variable, but if you want to use a different configuration 318 | # then simply: 319 | # 1. Create a repository-level variable with the name 320 | # ACTIONS_PERMISSIONS_CONFIG. 321 | # 2. Set this new variable's value to the configuration you 322 | # want to use for this repository. 323 | # 324 | # Note in particular that changing the permissions 325 | # monitoring configuration *does not* require you to modify 326 | # this workflow. 327 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 328 | - name: Set up QEMU 329 | uses: docker/setup-qemu-action@v3 330 | - name: Set up Docker Buildx 331 | uses: docker/setup-buildx-action@v3 332 | - name: Create dist directory 333 | run: mkdir -p dist 334 | - name: Build image 335 | id: docker_build 336 | uses: docker/build-push-action@v6 337 | with: 338 | cache-from: type=gha 339 | # We use the max mode to cache all layers which includes ones from 340 | # intermediate steps. This will provide us the potential for more cache hits 341 | # and thus better build times. It is also the suggested setting per the 342 | # documentation: 343 | # https://docs.docker.com/build/ci/github-actions/cache/#cache-backend-api 344 | cache-to: type=gha,mode=max 345 | # For a list of pre-defined annotation keys and value types see: 346 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 347 | labels: ${{ needs.prepare.outputs.labels }} 348 | outputs: type=docker,dest=dist/image.tar 349 | # Uncomment the following option if you are building an image for use 350 | # on Google Cloud Run or AWS Lambda. The current default image output 351 | # is unable to run on either. Please see the following issue for more 352 | # information: https://github.com/docker/buildx/issues/1533 353 | # provenance: false 354 | tags: ${{ needs.repo-metadata.outputs.image-name }}:latest # not to be pushed 355 | - name: Compress image 356 | run: gzip dist/image.tar 357 | - name: Upload artifacts 358 | uses: actions/upload-artifact@v4 359 | with: 360 | name: dist 361 | path: dist 362 | - name: Setup tmate debug session 363 | uses: mxschmitt/action-tmate@v3 364 | if: env.RUN_TMATE 365 | scan: 366 | name: Scan the image for vulnerabilities 367 | needs: 368 | - diagnostics 369 | - repo-metadata 370 | - build 371 | permissions: 372 | # actions/checkout needs this to fetch code 373 | contents: read 374 | runs-on: ubuntu-latest 375 | steps: 376 | - name: Apply standard cisagov job preamble 377 | uses: cisagov/action-job-preamble@v1 378 | with: 379 | # This functionality is poorly implemented and has been 380 | # causing problems due to the MITM implementation hogging or 381 | # leaking memory. As a result we disable it by default. If 382 | # you want to temporarily enable it, simply set 383 | # monitor_permissions equal to "true". 384 | # 385 | # TODO: Re-enable this functionality when practical. See 386 | # cisagov/skeleton-docker#224 for more details. 387 | monitor_permissions: "false" 388 | # Use a variable to specify the permissions monitoring 389 | # configuration. By default this will yield the 390 | # configuration stored in the cisagov organization-level 391 | # variable, but if you want to use a different configuration 392 | # then simply: 393 | # 1. Create a repository-level variable with the name 394 | # ACTIONS_PERMISSIONS_CONFIG. 395 | # 2. Set this new variable's value to the configuration you 396 | # want to use for this repository. 397 | # 398 | # Note in particular that changing the permissions 399 | # monitoring configuration *does not* require you to modify 400 | # this workflow. 401 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 402 | - name: Download Docker image artifact 403 | uses: actions/download-artifact@v5 404 | with: 405 | name: dist 406 | path: dist 407 | - name: Load Docker image 408 | run: docker load < dist/image.tar.gz 409 | - name: Run Trivy vulnerability scanner 410 | uses: aquasecurity/trivy-action@0.33.1 411 | with: 412 | image-ref: ${{ needs.repo-metadata.outputs.image-name }}:latest 413 | test: 414 | # Executes tests on the single-platform image created in the "build" job. 415 | name: Test image 416 | needs: 417 | - diagnostics 418 | - build 419 | permissions: 420 | # actions/checkout needs this to fetch code 421 | contents: read 422 | runs-on: ubuntu-latest 423 | steps: 424 | - name: Apply standard cisagov job preamble 425 | uses: cisagov/action-job-preamble@v1 426 | with: 427 | # This functionality is poorly implemented and has been 428 | # causing problems due to the MITM implementation hogging or 429 | # leaking memory. As a result we disable it by default. If 430 | # you want to temporarily enable it, simply set 431 | # monitor_permissions equal to "true". 432 | # 433 | # TODO: Re-enable this functionality when practical. See 434 | # cisagov/skeleton-docker#224 for more details. 435 | monitor_permissions: "false" 436 | # Use a variable to specify the permissions monitoring 437 | # configuration. By default this will yield the 438 | # configuration stored in the cisagov organization-level 439 | # variable, but if you want to use a different configuration 440 | # then simply: 441 | # 1. Create a repository-level variable with the name 442 | # ACTIONS_PERMISSIONS_CONFIG. 443 | # 2. Set this new variable's value to the configuration you 444 | # want to use for this repository. 445 | # 446 | # Note in particular that changing the permissions 447 | # monitoring configuration *does not* require you to modify 448 | # this workflow. 449 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 450 | - id: setup-env 451 | uses: cisagov/setup-env-github-action@v1 452 | - uses: actions/checkout@v5 453 | - id: setup-python 454 | uses: actions/setup-python@v6 455 | with: 456 | python-version: ${{ steps.setup-env.outputs.python-version }} 457 | - name: Cache testing environments 458 | uses: actions/cache@v4 459 | env: 460 | BASE_CACHE_KEY: ${{ github.job }}-${{ runner.os }}-\ 461 | py${{ steps.setup-python.outputs.python-version }}- 462 | with: 463 | key: ${{ env.BASE_CACHE_KEY }}\ 464 | ${{ hashFiles('**/requirements-test.txt') }}-\ 465 | ${{ hashFiles('**/requirements.txt') }} 466 | path: ${{ env.PIP_CACHE_DIR }} 467 | restore-keys: | 468 | ${{ env.BASE_CACHE_KEY }} 469 | - name: Install dependencies 470 | run: | 471 | python -m pip install --upgrade pip setuptools wheel 472 | pip install --upgrade --requirement requirements-test.txt 473 | - name: Download Docker image artifact 474 | uses: actions/download-artifact@v5 475 | with: 476 | name: dist 477 | path: dist 478 | - name: Load Docker image 479 | run: docker load < dist/image.tar.gz 480 | - name: Run tests 481 | env: 482 | RELEASE_TAG: ${{ github.event.release.tag_name }} 483 | run: pytest --runslow 484 | - name: Setup tmate debug session 485 | uses: mxschmitt/action-tmate@v3 486 | if: env.RUN_TMATE 487 | build-push-all: 488 | # Builds the final set of images for each of the platforms specified in the 489 | # "platforms" input for the docker/build-push-action Action. These images 490 | # are tagged with the Docker tags calculated in the "prepare" job and 491 | # pushed to Docker Hub and the GitHub Container Registry. The contents of 492 | # README.md are pushed as the image's description to Docker Hub. This job 493 | # is skipped when the triggering event is a pull request. 494 | if: github.event_name != 'pull_request' 495 | name: Build and push all platforms 496 | needs: 497 | - diagnostics 498 | - lint 499 | - repo-metadata 500 | - prepare 501 | - scan 502 | - test 503 | permissions: 504 | # actions/checkout needs this to fetch code 505 | contents: read 506 | # When Dependabot creates a PR it requires this permission in 507 | # order to push Docker images to ghcr.io. 508 | packages: write 509 | runs-on: ubuntu-latest 510 | steps: 511 | - name: Apply standard cisagov job preamble 512 | uses: cisagov/action-job-preamble@v1 513 | with: 514 | # This functionality is poorly implemented and has been 515 | # causing problems due to the MITM implementation hogging or 516 | # leaking memory. As a result we disable it by default. If 517 | # you want to temporarily enable it, simply set 518 | # monitor_permissions equal to "true". 519 | # 520 | # TODO: Re-enable this functionality when practical. See 521 | # cisagov/skeleton-docker#224 for more details. 522 | monitor_permissions: "false" 523 | # Use a variable to specify the permissions monitoring 524 | # configuration. By default this will yield the 525 | # configuration stored in the cisagov organization-level 526 | # variable, but if you want to use a different configuration 527 | # then simply: 528 | # 1. Create a repository-level variable with the name 529 | # ACTIONS_PERMISSIONS_CONFIG. 530 | # 2. Set this new variable's value to the configuration you 531 | # want to use for this repository. 532 | # 533 | # Note in particular that changing the permissions 534 | # monitoring configuration *does not* require you to modify 535 | # this workflow. 536 | permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} 537 | - name: Login to Docker Hub 538 | uses: docker/login-action@v3 539 | with: 540 | password: ${{ secrets.DOCKER_PASSWORD }} 541 | username: ${{ secrets.DOCKER_USERNAME }} 542 | - name: Login to GitHub Container Registry 543 | uses: docker/login-action@v3 544 | with: 545 | password: ${{ secrets.GITHUB_TOKEN }} 546 | registry: ghcr.io 547 | username: ${{ github.actor }} 548 | - name: Set up QEMU 549 | uses: docker/setup-qemu-action@v3 550 | - name: Set up Docker Buildx 551 | uses: docker/setup-buildx-action@v3 552 | # We only build to ensure that the image layers are cached to push later. This is 553 | # because if the build takes over 10 minutes the token acquired to push to the 554 | # GitHub Container Registry will have expired. This results in errors like: 555 | # 556 | # Signature not valid in the specified time frame: 557 | # Start [Tue, 08 Jul 2025 06:05:02 GMT] - Expiry [Tue, 08 Jul 2025 06:15:07 GMT] 558 | # - Current [Tue, 08 Jul 2025 06:16:10 GMT] 559 | # 560 | # Please see https://github.com/docker/build-push-action/issues/1371 for more 561 | # information. 562 | - name: Build platform images 563 | id: docker_build 564 | uses: docker/build-push-action@v6 565 | with: 566 | cache-from: type=gha 567 | # We use the max mode to cache all layers which includes ones from 568 | # intermediate steps. This will provide us the potential for more cache hits 569 | # and thus better build times. It is also the suggested setting per the 570 | # documentation: 571 | # https://docs.docker.com/build/ci/github-actions/cache/#cache-backend-api 572 | cache-to: type=gha,mode=max 573 | # For a list of pre-defined annotation keys and value types see: 574 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 575 | labels: ${{ needs.prepare.outputs.labels }} 576 | platforms: ${{ join(fromJSON(needs.repo-metadata.outputs.image-platforms)) }} 577 | # Uncomment the following option if you are building an image for use 578 | # on Google Cloud Run or AWS Lambda. The current default image output 579 | # is unable to run on either. Please see the following issue for more 580 | # information: https://github.com/docker/buildx/issues/1533 581 | # provenance: false 582 | tags: ${{ needs.prepare.outputs.tags }} 583 | # Now that the image layers should be available from the cache we can push to the 584 | # registries. 585 | - name: Push platform images to registries 586 | id: docker_push 587 | uses: docker/build-push-action@v6 588 | with: 589 | cache-from: type=gha 590 | # We use the max mode to cache all layers which includes ones from 591 | # intermediate steps. This will provide us the potential for more cache hits 592 | # and thus better build times. It is also the suggested setting per the 593 | # documentation: 594 | # https://docs.docker.com/build/ci/github-actions/cache/#cache-backend-api 595 | cache-to: type=gha,mode=max 596 | # For a list of pre-defined annotation keys and value types see: 597 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 598 | labels: ${{ needs.prepare.outputs.labels }} 599 | platforms: ${{ join(fromJSON(needs.repo-metadata.outputs.image-platforms)) }} 600 | # Uncomment the following option if you are building an image for use 601 | # on Google Cloud Run or AWS Lambda. The current default image output 602 | # is unable to run on either. Please see the following issue for more 603 | # information: https://github.com/docker/buildx/issues/1533 604 | # provenance: false 605 | push: true 606 | tags: ${{ needs.prepare.outputs.tags }} 607 | - name: Setup tmate debug session 608 | uses: mxschmitt/action-tmate@v3 609 | if: env.RUN_TMATE 610 | --------------------------------------------------------------------------------