├── .fossa.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── fossa.yml │ ├── pulumi-aws-container-tests.yml │ └── pulumi-aws-tests.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .python-version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── aws_write_creds.sh ├── destroy.sh ├── destroy_kube.sh ├── kubernetes-extras.sh ├── setup_venv.sh ├── start.sh ├── start_kube.sh ├── test-forward.sh ├── test.py └── test_runner.sh ├── config ├── .gitignore └── pulumi │ ├── Pulumi.stackname.yaml.example │ └── README.md ├── docker ├── Dockerfile.debian ├── Dockerfile.ubuntu ├── README.md └── build_dev_docker_image.sh ├── docs ├── DIAG-NGINX-ModernAppsRefArch-NGINX-MARA-1-0-blog-1024x800.png ├── NGINX-MARA-icon.png ├── accessing_mgmt_tools.md ├── getting_started.md └── status-and-issues.md ├── extras ├── README.md ├── jenkins │ ├── AWS │ │ └── Jenkinsfile │ ├── DigitalOcean │ │ └── Jenkinsfile │ ├── K3S │ │ └── Jenkinsfile │ ├── Linode │ │ └── Jenkinsfile │ ├── MicroK8s │ │ └── Jenkinsfile │ ├── Minikube │ │ └── Jenkinsfile │ └── README.md └── jwt.token └── pulumi └── python ├── Pipfile ├── Pipfile.lock ├── README.md ├── automation ├── DESIGN.md ├── colorize.py ├── env_config_parser.py ├── headers.py ├── main.py ├── providers │ ├── aws.py │ ├── base_provider.py │ ├── do.py │ ├── linode.py │ ├── pulumi_project.py │ └── update_kubeconfig.py └── stack_config_parser.py ├── config ├── Pulumi.yaml └── README.md ├── infrastructure ├── README.md ├── aws │ ├── .dockerignore │ ├── ecr │ │ ├── Pulumi.yaml │ │ └── __main__.py │ ├── eks │ │ ├── Pulumi.yaml │ │ ├── __main__.py │ │ └── iam.py │ └── vpc │ │ ├── Pulumi.yaml │ │ └── __main__.py ├── digitalocean │ ├── container-registry-credentials │ │ ├── Pulumi.yaml │ │ └── __main__.py │ ├── container-registry │ │ ├── Pulumi.yaml │ │ └── __main__.py │ ├── dns-record │ │ ├── Pulumi.yaml │ │ └── __main__.py │ └── domk8s │ │ ├── Pulumi.yaml │ │ └── __main__.py ├── kubeconfig │ ├── Pulumi.yaml │ └── __main__.py └── linode │ ├── container-registry-credentials │ ├── Pulumi.yaml │ └── __main__.py │ ├── harbor-configuration │ ├── Pulumi.yaml │ └── __main__.py │ ├── harbor │ ├── Pulumi.yaml │ └── __main__.py │ └── lke │ ├── Pulumi.yaml │ └── __main__.py ├── kubernetes ├── README.md ├── applications │ └── sirius │ │ ├── Pulumi.yaml │ │ ├── __main__.py │ │ ├── cert │ │ └── self-sign.yaml │ │ └── verify.py ├── certmgr │ ├── Pulumi.yaml │ ├── __main__.py │ └── manifests │ │ └── cert-manager.crds.yaml ├── logagent │ ├── Pulumi.yaml │ └── __main__.py ├── logstore │ ├── Pulumi.yaml │ └── __main__.py ├── nginx │ ├── ingress-controller-namespace │ │ ├── Pulumi.yaml │ │ └── __main__.py │ ├── ingress-controller-repo-only │ │ ├── Pulumi.yaml │ │ ├── __main__.py │ │ └── manifests │ │ │ └── .gitkeep │ └── ingress-controller │ │ ├── Pulumi.yaml │ │ └── __main__.py ├── observability │ ├── Pulumi.yaml │ ├── __main__.py │ ├── otel-objects │ │ ├── README.md │ │ ├── otel-collector.yaml │ │ ├── otel-collector.yaml.basic │ │ ├── otel-collector.yaml.basic-debug │ │ ├── otel-collector.yaml.full │ │ ├── otel-collector.yaml.lightstep │ │ └── otel-collector.yaml.with-prom │ └── otel-operator │ │ ├── README.md │ │ └── opentelemetry-operator.yaml ├── prometheus │ ├── Pulumi.yaml │ ├── __main__.py │ ├── extras │ │ ├── README.md │ │ └── kube-proxy.yaml │ └── manifests │ │ └── nginx-service-mon.yaml └── secrets │ ├── .gitignore │ ├── Pulumi.yaml │ └── __main__.py ├── requirements.txt ├── runner ├── tools ├── README.md ├── common │ ├── Pulumi.yaml │ └── config │ │ └── .gitkeep ├── metallb │ ├── Pulumi.yaml │ ├── __main__.py │ └── manifests │ │ └── metallb.yaml └── nfsvolumes │ ├── Pulumi.yaml │ └── __main__.py └── utility ├── kic-image-build ├── Pulumi.yaml ├── __main__.py ├── ingress_controller_image.py ├── ingress_controller_image_base_provider.py ├── ingress_controller_image_builder_args.py ├── ingress_controller_image_builder_provider.py ├── ingress_controller_image_puller_args.py ├── ingress_controller_image_puller_provider.py ├── ingress_controller_source_archive_url.py ├── nginx_plus_args.py ├── test_ingress_controller_image_base_provider.py └── test_ingress_controller_image_builder_provider.py ├── kic-image-push ├── Pulumi.yaml ├── __main__.py ├── registries │ ├── aws.py │ ├── base_registry.py │ ├── do.py │ └── lke.py └── repository_push.py └── kic-pulumi-utils ├── kic_util ├── __init__.py ├── archive_download.py ├── docker_image_name.py ├── external_process.py ├── pulumi_config.py ├── test_archive_download.py ├── test_docker_image_name.py ├── test_pulumi_config.py ├── test_url_type.py └── url_type.py └── setup.py /.fossa.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | cli: 3 | server: https://app.fossa.com 4 | fetcher: custom 5 | project: git@github.com:nginxinc/kic-reference-architectures.git 6 | analyze: 7 | modules: 8 | - name: pulumi/python 9 | type: pip 10 | path: pulumi/python 11 | target: pulumi/python 12 | options: 13 | strategy: pip 14 | requirements: config/pulumi/requirements.txt 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Deploy x to '...' using some.yaml 13 | 2. View logs on '....' 14 | 3. See error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Your environment** 20 | * Version of the repo - a specific commit or tag 21 | * Version of KIC 22 | * Version of infrastructure tooling (e.g. Pulumi) 23 | * Version of executing environment (e.g. Python, node) 24 | * OS and distribution 25 | * Details about containerization or virtualization environment 26 | 27 | **Additional context** 28 | Add any other context about the problem here. Any log files you want to share. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Proposed changes 2 | Describe the use case and detail of the change. If this PR addresses an issue 3 | on GitHub, make sure to include a link to that issue here in this description 4 | (not in the title of the PR). 5 | 6 | ### Checklist 7 | Before creating a PR, run through this checklist and mark each as complete. 8 | 9 | - [ ] I have written my commit messages in the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format. 10 | - [ ] I have read the [CONTRIBUTING](/CONTRIBUTING.md) doc 11 | - [ ] I have added tests (when possible) that prove my fix is effective or that my feature works 12 | - [ ] I have checked that all unit tests pass after adding my changes 13 | - [ ] I have updated necessary documentation 14 | - [ ] I have rebased my branch onto master 15 | - [ ] I will ensure my PR is targeting the master branch and pulling from my branch from my own fork 16 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: License Scanning 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Run FOSSA scan and upload build data 16 | uses: fossa-contrib/fossa-action@v1 17 | with: 18 | fossa-api-key: ${{ secrets.FOSSA_API_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/pulumi-aws-container-tests.yml: -------------------------------------------------------------------------------- 1 | name: Pulumi AWS Container Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | debian-container-tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Build and Test Debian Image 15 | working-directory: 16 | run: bash docker/build_dev_docker_image.sh debian -------------------------------------------------------------------------------- /.github/workflows/pulumi-aws-tests.yml: -------------------------------------------------------------------------------- 1 | name: Pulumi AWS Tests 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | ubuntu-tests: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v2 13 | - uses: syphar/restore-virtualenv@v1 14 | id: pulumi-aws-tests-cache-virtualenv 15 | with: 16 | requirement_files: pulumi/python/Pipfile.lock 17 | 18 | - uses: syphar/restore-pip-download-cache@v1 19 | if: steps.pulumi-aws-tests-cache-virtualenv.outputs.cache-hit != 'true' 20 | 21 | - run: ./setup_venv.sh 22 | working-directory: bin 23 | 24 | - name: Test 25 | working-directory: bin 26 | run: ./test_runner.sh "/home/runner/work/kic-reference-architectures/kic-reference-architectures" 27 | 28 | macos-tests: 29 | runs-on: macos-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-python@v2 33 | - uses: syphar/restore-virtualenv@v1 34 | id: pulumi-aws-tests-cache-virtualenv 35 | with: 36 | requirement_files: pulumi/python/Pipfile.lock 37 | 38 | - uses: syphar/restore-pip-download-cache@v1 39 | if: steps.pulumi-aws-tests-cache-virtualenv.outputs.cache-hit != 'true' 40 | 41 | - run: ./setup_venv.sh 42 | working-directory: bin 43 | 44 | - name: Test 45 | working-directory: bin 46 | run: ./test_runner.sh "/Users/runner/work/kic-reference-architectures/kic-reference-architectures" 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pulumi/python/kubernetes/applications/sirius/src"] 2 | path = pulumi/python/kubernetes/applications/sirius/src 3 | url = https://github.com/nginxinc/bank-of-sirius.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | - id: check-yaml 8 | args: [--allow-multiple-documents] 9 | - id: check-added-large-files 10 | - id: check-merge-conflict 11 | - id: detect-private-key 12 | - id: trailing-whitespace 13 | - id: mixed-line-ending 14 | - id: end-of-file-fixer 15 | - id: debug-statements 16 | - id: check-merge-conflict 17 | - id: check-ast 18 | 19 | - repo: https://github.com/pre-commit/mirrors-autopep8 20 | rev: v1.7.0 21 | hooks: 22 | - id: autopep8 23 | 24 | - repo: https://github.com/asottile/dead 25 | rev: v1.5.0 26 | hooks: 27 | - id: dead 28 | 29 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 30 | rev: 3.0.0 31 | hooks: 32 | - id: shellcheck 33 | - id: shfmt 34 | - id: markdownlint 35 | 36 | - repo: https://github.com/PyCQA/flake8 37 | rev: 5.0.4 38 | hooks: 39 | - id: flake8 40 | 41 | - repo: https://github.com/zricethezav/gitleaks 42 | rev: v8.11.0 43 | hooks: 44 | - id: gitleaks 45 | 46 | - repo: https://github.com/Yelp/detect-secrets 47 | rev: v1.3.0 48 | hooks: 49 | - id: detect-secrets 50 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.12 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | The following is a set of guidelines for contributing. We really appreciate 4 | that you are considering contributing! 5 | 6 | ## Table Of Contents 7 | 8 | [Ask a Question](#ask-a-question) 9 | 10 | [Contributing](#contributing) 11 | 12 | [Style Guides](#style-guides) 13 | 14 | * [Git Style Guide](#git-style-guide) 15 | * [Go Style Guide](#go-style-guide) 16 | 17 | [Code of Conduct](https://github.com/nginxinc/nginx-wrapper/blob/master/CODE_OF_CONDUCT.md) 18 | 19 | ## Ask a Question 20 | 21 | Please open an Issue on GitHub with the label `question`. 22 | 23 | ## Contributing 24 | 25 | ### Report a Bug 26 | 27 | To report a bug, open an issue on GitHub with the label `bug` using the 28 | available bug report issue template. Please ensure the issue has not already 29 | been reported. 30 | 31 | ### Suggest an Enhancement 32 | 33 | To suggest an enhancement, please create an issue on GitHub with the label 34 | `enhancement` using the available feature issue template. 35 | 36 | ### Open a Pull Request 37 | 38 | * Fork the repo, create a branch, submit a PR when your changes are tested and 39 | ready for review. 40 | * Fill in [our pull request template](/.github/PULL_REQUEST_TEMPLATE.md) 41 | 42 | Note: if you’d like to implement a new feature, please consider creating a 43 | feature request issue first to start a discussion about the feature. 44 | 45 | ## Style Guides 46 | 47 | ### Git Style Guide 48 | 49 | * Keep a clean, concise and meaningful git commit history on your branch, 50 | rebasing locally and squashing before submitting a PR 51 | * Use the 52 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format 53 | when writing a commit message, so that changelogs can be automatically 54 | generated 55 | * Follow the guidelines of writing a good commit message as described 56 | [here](https://chris.beams.io/posts/git-commit/) and summarised in the next 57 | few points 58 | * In the subject line, use the present tense 59 | ("Add feature" not "Added feature") 60 | * In the subject line, use the imperative mood ("Move cursor to..." not 61 | "Moves cursor to...") 62 | * Limit the subject line to 72 characters or less 63 | * Reference issues and pull requests liberally after the subject line 64 | * Add more detailed description in the body of the git message ( 65 | `git commit -a` to give you more space and time in your text editor to 66 | write a good message instead of `git commit -am`) 67 | 68 | ### Code Style Guide 69 | 70 | * Python code should conform to the 71 | [PEP-8 style guidelines](https://www.python.org/dev/peps/pep-0008/) 72 | whenever possible. 73 | * Where feasible, include unit tests. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX Modern Reference Architectures 2 | 3 | ## Current Test Status 4 | 5 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B5618%2Fgit%40github.com%3Anginxinc%2Fkic-reference-architectures.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B5618%2Fgit%40github.com%3Anginxinc%2Fkic-reference-architectures.git?ref=badge_shield) 6 | ![AWS Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_aws_prod&subject=AWS) 7 | ![DO Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_do_prod&subject=DigitalOcean) 8 | ![LKE Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_lke_prod&subject=Linode) 9 | ![K3s Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_k3s_prod&subject=K3s) 10 | ![MicroK8s Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_mk8s_prod&subject=MicroK8s) 11 | ![Minikube Status](https://jenkins.mantawang.com/buildStatus/icon?job=mara_minikube_prod&subject=Minikube) 12 | 13 | ![MARA Project](./docs/NGINX-MARA-icon.png) 14 | 15 | This repository has the basics for a common way to deploy and manage modern 16 | apps. Over time, we'll build more example architectures using different 17 | deployment models and options – including other clouds – and you’ll be able 18 | to find those here. 19 | 20 | ## Nomenclature 21 | 22 | Internally, we refer to this project as MARA for Modern Application Reference 23 | Architecture. The current repository name reflects the humble origins of this 24 | project, as it was started with the purpose of allowing users to build custom 25 | versions of the NGINX Ingress Controller in Kubernetes. This went so well that 26 | we expanded it to the project you're currently viewing. 27 | 28 | ## Modern App Architectures 29 | 30 | We define modern app architectures as those driven by four characteristics: 31 | *scalability*, *portability*, *resiliency*, and *agility*. While many different 32 | aspects of a modern architecture exist, these are fundamental. 33 | 34 | * **Scalability** – Quickly and seamlessly scale up or down to accommodate 35 | spikes or reductions in demand, anywhere in the world. 36 | 37 | * **Portability** – Easy to deploy on multiple types of devices and 38 | infrastructures, on public clouds, and on premises. 39 | 40 | * **Resiliency** – Can fail over to newly spun‑up clusters or virtual 41 | environments in different availability regions, clouds, or data centers. 42 | 43 | * **Agility** – Ability to update through automated CI/CD pipelines with higher 44 | code velocity and more frequent code pushes. 45 | 46 | This diagram is an example of what we mean by a **modern app architecture**: 47 | ![Modern Apps Architecture Example Diagram](docs/DIAG-NGINX-ModernAppsRefArch-NGINX-MARA-1-0-blog-1024x800.png) 48 | 49 | To satisfy the four key characteristics, many modern app architectures employ: 50 | 51 | * Platform agnosticism 52 | * Prioritization of OSS 53 | * Everything defined by code 54 | * CI/CD automation 55 | * Security-minded development 56 | * Containerized builds 57 | * Distributed storage 58 | 59 | ## What's Being Built 60 | 61 | For details on the current state of this project, please see the 62 | [readme](pulumi/python/README.md) in the [`pulumi/python`](pulumi/python) 63 | subdirectory. This project is under active development, and the current work is 64 | using [Pulumi](https://www.pulumi.com/) with Python. Additionally, please see 65 | [Status and Issues](docs/status-and-issues.md) for the project's up-to-date 66 | build status and known issues. 67 | 68 | Subdirectories contained within the root directory separate reference 69 | architectures by infrastructure deployment tooling with additional 70 | subdirectories as needed. For example, Pulumi allows the use of multiple 71 | languages for deployment. As we decided to use Python in our first build, there 72 | is a `python` subdirectory under the `pulumi` directory. 73 | 74 | This project was started to provide a complete, stealable, easy to deploy, and 75 | standalone example of how a modern app architecture can be built. It was driven 76 | by the necessity to be flexible and not require a long list of dependencies to 77 | get started. It needs to provide examples of tooling used to build this sort of 78 | architecture in the real world. Most importantly, it needs to work. Hopefully 79 | this provides a ‘jumping off’ point for someone to build their own 80 | infrastructure. 81 | 82 | ## Deployment Tools 83 | 84 | ### Pulumi 85 | 86 | [Pulumi](https://www.pulumi.com/) is a modern Infrastructure as Code (IaC) tool 87 | that allows you to write code (node, Python, Go, etc.) that defines cloud 88 | infrastructure. Within the [`pulumi`](pulumi) folder are examples of the pulumi 89 | being used to stand up MARA. 90 | 91 | ## Contribution 92 | 93 | We welcome pull requests and issues! 94 | 95 | Please refer to the [Contributing Guidelines](CONTRIBUTING.md) when doing a PR. 96 | 97 | ## License 98 | 99 | All code in this repository is licensed under the 100 | [Apache License v2 license](LICENSE). 101 | 102 | Open source license notices for all projects in this repository can be 103 | found 104 | [here](https://app.fossa.com/reports/92595e16-c0b8-4c68-8c76-59696b6ac219). 105 | 106 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B5618%2Fgit%40github.com%3Anginxinc%2Fkic-reference-architectures.git.svg?type=large)](https://app.fossa.com/projects/custom%2B5618%2Fgit%40github.com%3Anginxinc%2Fkic-reference-architectures.git?ref=badge_large) 107 | -------------------------------------------------------------------------------- /bin/aws_write_creds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit # abort on nonzero exit status 3 | set -o pipefail # don't hide errors within pipes 4 | 5 | # 6 | # This script is temporary until we rewrite the AWS deployment following 7 | # 81 and #82. # We look into the environment and if we see environment 8 | # variables for the AWS # authentication process we move them into a 9 | # credentials file. This is primarily being # done at this time to support 10 | # Jenkins using env vars for creds 11 | # 12 | 13 | aws_auth_vars=(AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN) 14 | 15 | missing_auth_vars=() 16 | for i in "${aws_auth_vars[@]}"; do 17 | test -n "${!i:+y}" || missing_vars+=("$i") 18 | done 19 | 20 | if [ ${#missing_auth_vars[@]} -ne 0 ]; then 21 | echo "Did not find values for:" 22 | printf ' %q\n' "${missing_vars[@]}" 23 | echo "Will assume they are in credentials file or not needed" 24 | else 25 | echo "Creating credentials file" 26 | # Create the directory.... 27 | mkdir -p ~/.aws 28 | CREDS=~/.aws/credentials 29 | echo "[default]" >$CREDS 30 | echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >>$CREDS 31 | echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >>$CREDS 32 | # This is if we have non-temp credentials... 33 | if [[ -z "${AWS_SESSION_TOKEN+x}" ]]; then 34 | echo "Variable AWS_SESSION_TOKEN was unset; not adding to credentials" 35 | else 36 | echo "aws_session_token=$AWS_SESSION_TOKEN" >>$CREDS 37 | fi 38 | 39 | fi 40 | -------------------------------------------------------------------------------- /bin/destroy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | # Don't pollute console output with upgrade notifications 8 | export PULUMI_SKIP_UPDATE_CHECK=true 9 | # Run Pulumi non-interactively 10 | export PULUMI_SKIP_CONFIRMATIONS=true 11 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 12 | 13 | # 14 | # Check to see if the venv has been installed, since this is only going to be 15 | # used to start pulumi/python based projects. 16 | # 17 | if ! command -v "${script_dir}/../pulumi/python/venv/bin/python" >/dev/null; then 18 | echo "NOTICE! Unable to find the venv directory. This is required for the pulumi/python deployment process." 19 | echo "Please run ./setup_venv.sh from this directory to install the required virtual environment." 20 | echo " " 21 | exit 1 22 | else 23 | echo "Adding to [${script_dir}/venv/bin] to PATH" 24 | export PATH="${script_dir}/../pulumi/python/venv/bin:$PATH" 25 | fi 26 | 27 | if ! command -v pulumi >/dev/null; then 28 | if [ -x "${script_dir}/../pulumi/python/venv/bin/pulumi" ]; then 29 | echo "Adding to [${script_dir}/venv/bin] to PATH" 30 | export PATH="${script_dir}/../pulumi/python/venv/bin:$PATH" 31 | 32 | if ! command -v pulumi >/dev/null; then 33 | echo >&2 "Pulumi must be installed to continue" 34 | exit 1 35 | fi 36 | else 37 | echo >&2 "Pulumi must be installed to continue" 38 | exit 1 39 | fi 40 | fi 41 | 42 | if ! command -v python3 >/dev/null; then 43 | echo >&2 "Python 3 must be installed to continue" 44 | exit 1 45 | fi 46 | 47 | # Check to see if the user is logged into Pulumi 48 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 49 | pulumi login 50 | 51 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 52 | echo >&2 "Unable to login to Pulumi - exiting" 53 | exit 2 54 | fi 55 | fi 56 | 57 | echo " " 58 | echo "Notice! This shell script will only destroy kubeconfig based deployments; if you have deployed to AWS, " 59 | echo "DigitalOcean, or Linode you will need to use the ./pulumi/python/runner script instead." 60 | echo " " 61 | 62 | # Sleep so we are seen... 63 | sleep 5 64 | 65 | source "${script_dir}/../config/pulumi/environment" 66 | echo "Configuring all Pulumi projects to use the stack: ${PULUMI_STACK}" 67 | 68 | # 69 | # Determine what destroy script we need to run 70 | # 71 | if pulumi config get kubernetes:infra_type -C "${script_dir}"/../pulumi/python/config >/dev/null 2>&1; then 72 | INFRA="$(pulumi config get kubernetes:infra_type -C ${script_dir}/../pulumi/python/config)" 73 | if [ "$INFRA" == 'AWS' ]; then 74 | echo "This script no longer works with AWS deployments; please use ./pulumi/python/runner instead" 75 | exec ${script_dir}/../pulumi/python/runner 76 | exit 0 77 | elif [ "$INFRA" == 'kubeconfig' ]; then 78 | echo "Destroying a kubeconfig based stack; if this is not right please type ctrl-c to abort this script." 79 | sleep 5 80 | "${script_dir}"/destroy_kube.sh 81 | exit 0 82 | elif [ "$INFRA" == 'DO' ]; then 83 | echo "This script no longer works with DigitalOcean deployments; please use ./pulumi/python/runner instead" 84 | exec "${script_dir}"/../pulumi/python/runner 85 | sleep 5 86 | "${script_dir}"/destroy_do.sh 87 | exit 0 88 | elif [ "$INFRA" == 'LKE' ]; then 89 | echo "This script no longer works with Linode deployments; please use ./pulumi/python/runner instead" 90 | exec "${script_dir}"/../pulumi/python/runner 91 | sleep 5 92 | "${script_dir}"/destroy_lke.sh 93 | exit 0 94 | else 95 | print "No infrastructure set in config file; aborting!" 96 | exit 1 97 | fi 98 | else 99 | print "No infrastructure set in config file; aborting!" 100 | exit 2 101 | fi 102 | -------------------------------------------------------------------------------- /bin/destroy_kube.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | # Don't pollute console output with upgrade notifications 8 | export PULUMI_SKIP_UPDATE_CHECK=true 9 | # Run Pulumi non-interactively 10 | export PULUMI_SKIP_CONFIRMATIONS=true 11 | 12 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 13 | 14 | if ! command -v pulumi >/dev/null; then 15 | if [ -x "${script_dir}/venv/bin/pulumi" ]; then 16 | echo "Adding to [${script_dir}/venv/bin] to PATH" 17 | export PATH="${script_dir}/venv/bin:$PATH" 18 | 19 | if ! command -v pulumi >/dev/null; then 20 | echo >&2 "Pulumi must be installed to continue" 21 | exit 1 22 | fi 23 | else 24 | echo >&2 "Pulumi must be installed to continue" 25 | exit 1 26 | fi 27 | fi 28 | 29 | if ! command -v python3 >/dev/null; then 30 | echo >&2 "Python 3 must be installed to continue" 31 | exit 1 32 | fi 33 | 34 | if ! command -v node >/dev/null; then 35 | if [ -x "${script_dir}/venv/bin/pulumi" ]; then 36 | echo "Adding to [${script_dir}/venv/bin] to PATH" 37 | export PATH="${script_dir}/venv/bin:$PATH" 38 | 39 | if ! command -v node >/dev/null; then 40 | echo >&2 "NodeJS must be installed to continue" 41 | exit 1 42 | fi 43 | else 44 | echo >&2 "NodeJS must be installed to continue" 45 | exit 1 46 | fi 47 | fi 48 | 49 | # Check to see if the user is logged into Pulumi 50 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 51 | pulumi login 52 | 53 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 54 | echo >&2 "Unable to login to Pulumi - exiting" 55 | exit 2 56 | fi 57 | fi 58 | 59 | source "${script_dir}/../config/pulumi/environment" 60 | echo "Configuring all Pulumi projects to use the stack: ${PULUMI_STACK}" 61 | 62 | APPLICATIONS=(sirius) 63 | KUBERNETES=(secrets observability logagent logstore certmgr prometheus) 64 | NGINX=(kubernetes/nginx/ingress-controller-repo-only) 65 | INFRA=(kubeconfig digitalocean/domk8s) 66 | 67 | # 68 | # This is a temporary process until we complete the directory reorg and move the start/stop 69 | # process into more solid code. 70 | # 71 | 72 | # Destroy the application(s) 73 | for project_dir in "${APPLICATIONS[@]}"; do 74 | echo "$project_dir" 75 | if [ -f "${script_dir}/../pulumi/python/kubernetes/applications/${project_dir}/Pulumi.yaml" ]; then 76 | pulumi_args="--cwd ${script_dir}/../pulumi/python/kubernetes/applications/${project_dir} --emoji --stack ${PULUMI_STACK}" 77 | pulumi $pulumi_args destroy 78 | else 79 | echo >&2 "Not destroying - Pulumi.yaml not found in directory: ${script_dir}/../pulumi/python/kubernetes/applications/${project_dir}" 80 | fi 81 | done 82 | 83 | # Destroy other K8 resources 84 | for project_dir in "${KUBERNETES[@]}"; do 85 | echo "$project_dir" 86 | if [ -f "${script_dir}/../pulumi/python/kubernetes/${project_dir}/Pulumi.yaml" ]; then 87 | pulumi_args="--cwd ${script_dir}/../pulumi/python/kubernetes/${project_dir} --emoji --stack ${PULUMI_STACK}" 88 | pulumi $pulumi_args destroy 89 | else 90 | echo >&2 "Not destroying - Pulumi.yaml not found in directory: ${script_dir}/../pulumi/python/kubernetes/${project_dir}" 91 | fi 92 | done 93 | 94 | # TODO: figure out a more elegant way to do the CRD removal for prometheus #83 95 | # This is a hack for now to remove the CRD's for prometheus-kube-stack 96 | # See https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md#uninstall-chart 97 | kubectl delete crd alertmanagerconfigs.monitoring.coreos.com >/dev/null 2>&1 98 | kubectl delete crd alertmanagers.monitoring.coreos.com >/dev/null 2>&1 99 | kubectl delete crd podmonitors.monitoring.coreos.com >/dev/null 2>&1 100 | kubectl delete crd probes.monitoring.coreos.com >/dev/null 2>&1 101 | kubectl delete crd prometheuses.monitoring.coreos.com >/dev/null 2>&1 102 | kubectl delete crd prometheusrules.monitoring.coreos.com >/dev/null 2>&1 103 | kubectl delete crd servicemonitors.monitoring.coreos.com >/dev/null 2>&1 104 | kubectl delete crd thanosrulers.monitoring.coreos.com >/dev/null 2>&1 105 | 106 | # Destroy NGINX components 107 | for project_dir in "${NGINX[@]}"; do 108 | echo "$project_dir" 109 | if [ -f "${script_dir}/../pulumi/python/${project_dir}/Pulumi.yaml" ]; then 110 | pulumi_args="--cwd ${script_dir}/../pulumi/python/${project_dir} --emoji --stack ${PULUMI_STACK}" 111 | pulumi $pulumi_args destroy 112 | else 113 | echo >&2 "Not destroying - Pulumi.yaml not found in directory: ${script_dir}/../pulumi/python/${project_dir}" 114 | fi 115 | done 116 | 117 | # Clean up the kubeconfig project 118 | for project_dir in "${INFRA[@]}"; do 119 | echo "$project_dir" 120 | if [ -f "${script_dir}/../pulumi/python/infrastructure/${project_dir}/Pulumi.yaml" ]; then 121 | pulumi_args="--cwd ${script_dir}/../pulumi/python/infrastructure/${project_dir} --emoji --stack ${PULUMI_STACK}" 122 | pulumi $pulumi_args destroy 123 | else 124 | echo >&2 "Not destroying - Pulumi.yaml not found in directory: ${script_dir}/../pulumi/python/infrastructure/${project_dir}" 125 | fi 126 | done 127 | -------------------------------------------------------------------------------- /bin/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | # Don't pollute console output with upgrade notifications 8 | export PULUMI_SKIP_UPDATE_CHECK=true 9 | # Run Pulumi non-interactively 10 | export PULUMI_SKIP_CONFIRMATIONS=true 11 | 12 | # Unset virtual environment if defined.... 13 | unset VIRTUAL_ENV 14 | 15 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 16 | 17 | # Check to see if the venv has been installed, since this is only going to be used to start pulumi/python based 18 | # projects. 19 | # 20 | if ! command -v "${script_dir}/../pulumi/python/venv/bin/python" >/dev/null; then 21 | echo "NOTICE! Unable to find the venv directory. This is required for the pulumi/python deployment process." 22 | echo "Please run ./setup_venv.sh from this directory to install the required virtual environment." 23 | echo " " 24 | exit 1 25 | else 26 | echo "Adding to [${script_dir}/venv/bin] to PATH" 27 | export PATH="${script_dir}/../pulumi/python/venv/bin:$PATH" 28 | fi 29 | 30 | if ! command -v pulumi >/dev/null; then 31 | if [ -x "${script_dir}/../pulumi/python/venv/bin/pulumi" ]; then 32 | echo "Adding to [${script_dir}/venv/bin] to PATH" 33 | export PATH="${script_dir}/../pulumi/python/venv/bin:$PATH" 34 | if ! command -v pulumi >/dev/null; then 35 | echo >&2 "Pulumi must be installed to continue" 36 | exit 1 37 | fi 38 | else 39 | echo >&2 "Pulumi must be installed to continue" 40 | exit 1 41 | fi 42 | fi 43 | 44 | if ! command -v python3 >/dev/null; then 45 | echo >&2 "Python 3 must be installed to continue" 46 | exit 1 47 | fi 48 | 49 | # Check to see if the user is logged into Pulumi 50 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 51 | pulumi login 52 | 53 | if ! pulumi whoami --non-interactive >/dev/null 2>&1; then 54 | echo >&2 "Unable to login to Pulumi - exiting" 55 | exit 2 56 | fi 57 | fi 58 | 59 | echo " " 60 | echo "NOTICE! This shell script is maintained for compatibility for the kubeconfig only deployment and will be" 61 | echo "deprecated once the kubeconfig deployments are fully integrated with the automation api." 62 | echo " " 63 | echo "If you are deploying AWS, DigitalOcean, or Linode based stacks you will need to use the runner script." 64 | echo " " 65 | echo "Please read the documentation for more details." 66 | echo " " 67 | # Sleep so we are seen... 68 | sleep 5 69 | 70 | if [ -s "${script_dir}/../config/pulumi/environment" ] && grep --quiet '^PULUMI_STACK=.*' "${script_dir}/../config/pulumi/environment"; then 71 | source "${script_dir}"/../config/pulumi/environment 72 | echo "Environment data found for stack: ${PULUMI_STACK}" 73 | while true; do 74 | read -r -e -p "Environment file exists and is not empty. Answer yes to use, no to delete. " yn 75 | case $yn in 76 | [Yy]*) # We have an environment file, and they want to keep it.... 77 | if pulumi config get kubernetes:infra_type -C "${script_dir}"/../pulumi/python/config >/dev/null 2>&1; then 78 | INFRA=$(pulumi config get kubernetes:infra_type -C "${script_dir}"/../pulumi/python/config) 79 | if [ "$INFRA" == 'AWS' ]; then 80 | echo "This script no longer works with AWS deployments; please use ./pulumi/python/runner instead" 81 | exec "${script_dir}"/../pulumi/python/runner 82 | exit 0 83 | elif [ "$INFRA" == 'kubeconfig' ]; then 84 | exec "${script_dir}"/start_kube.sh 85 | exit 0 86 | elif [ "$INFRA" == 'DO' ]; then 87 | echo "This script no longer works with DigitalOcean deployments; please use ./pulumi/python/runner instead" 88 | exec "${script_dir}"/../pulumi/python/runner 89 | exit 0 90 | elif [ "$INFRA" == 'LKE' ]; then 91 | echo "This script no longer works with Linode deployments; please use ./pulumi/python/runner instead" 92 | exec "${script_dir}"/../pulumi/python/runner 93 | exit 0 94 | else 95 | echo "Corrupt or non-existent configuration file, please restart and delete and reconfigure." 96 | exit 1 97 | fi 98 | else 99 | echo "Corrupt or non-existent configuration file, please restart and delete and reconfigure." 100 | exit 1 101 | fi 102 | break 103 | ;; 104 | [Nn]*) # They want to remove and reconfigure 105 | rm -f "${script_dir}"/../config/pulumi/environment 106 | break 107 | ;; 108 | *) echo "Please answer yes or no." ;; 109 | esac 110 | done 111 | fi 112 | 113 | while true; do 114 | read -e -r -p "Type a for AWS, d for Digital Ocean, k for kubeconfig, l for Linode? " infra 115 | case "$infra" in 116 | [Aa]*) 117 | echo "This script no longer works with AWS deployments; please use ./pulumi/python/runner instead" 118 | exec "${script_dir}"/../pulumi/python/runner 119 | exit 0 120 | break 121 | ;; 122 | [Kk]*) 123 | echo "Calling kubeconfig startup script" 124 | exec "${script_dir}"/start_kube.sh 125 | exit 0 126 | break 127 | ;; 128 | [Dd]*) 129 | echo "This script no longer works with DigitalOcean deployments; please use ./pulumi/python/runner instead" 130 | exec "${script_dir}"/../pulumi/python/runner 131 | exit 0 132 | break 133 | ;; 134 | [Ll]*) 135 | echo "This script no longer works with Linode deployments; please use ./pulumi/python/runner instead" 136 | exec "${script_dir}"/../pulumi/python/runner 137 | exit 0 138 | break 139 | ;; 140 | *) echo "Please answer a, d, k, or l." ;; 141 | esac 142 | done 143 | -------------------------------------------------------------------------------- /bin/test-forward.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This is a simple shell script that sets up port forwards locally for 4 | # the various benchmarking/monitoring tooling that is part of the 5 | # deployment. This should be run on the same machine as your web browser, 6 | # then you will be able to connect to the localhost ports to get to the 7 | # services. 8 | # 9 | # This script is designed to clean itself up once a Ctrl-C is issued. 10 | # 11 | 12 | PID01="$(mktemp)" 13 | PID02="$(mktemp)" 14 | PID03="$(mktemp)" 15 | PID04="$(mktemp)" 16 | PID05="$(mktemp)" 17 | 18 | # this function is called when Ctrl-C is sent 19 | function trap_ctrlc() { 20 | # perform cleanup here 21 | echo "Ctrl-C caught...performing clean up" 22 | 23 | echo "Doing cleanup" 24 | 25 | echo "Kill forwards" 26 | kill $(cat "$PID01") 27 | kill $(cat "$PID02") 28 | kill $(cat "$PID03") 29 | kill $(cat "$PID04") 30 | kill $(cat "$PID05") 31 | 32 | echo "Remove temp files" 33 | rm "$PID01" 34 | rm "$PID02" 35 | rm "$PID03" 36 | rm "$PID04" 37 | rm "$PID05" 38 | 39 | # exit shell script with error code 2 40 | # if omitted, shell script will continue execution 41 | exit 2 42 | } 43 | 44 | # initialise trap to call trap_ctrlc function 45 | # when signal 2 (SIGINT) is received 46 | trap "trap_ctrlc" 2 47 | 48 | ## Kibana Tunnel 49 | kubectl port-forward service/elastic-kibana --namespace logstore 5601:5601 & 50 | echo $! >"$PID01" 51 | 52 | ## Grafana Tunnel 53 | kubectl port-forward service/prometheus-grafana --namespace prometheus 3000:80 & 54 | echo $! >"$PID02" 55 | 56 | ## Loadgenerator Tunnel 57 | kubectl port-forward service/loadgenerator --namespace bos 8089:8089 & 58 | echo $! >"$PID03" 59 | 60 | ## Prometheus Tunnel 61 | kubectl port-forward service/prometheus-kube-prometheus-prometheus --namespace prometheus 9090:9090 & 62 | echo $! >"$PID04" 63 | 64 | ## Elasticsearch Tunnel 65 | kubectl port-forward service/elastic-coordinating-only --namespace logstore 9200:9200 & 66 | echo $! >"$PID05" 67 | 68 | ## Legend 69 | echo "Connections Details" 70 | echo "====================================" 71 | echo "Kibana: http://localhost:5601" 72 | echo "Grafana: http://localhost:3000" 73 | echo "Locust: http://localhost:8089" 74 | echo "Prometheus: http://localhost:9090" 75 | echo "Elasticsearch: http://localhost:9200" 76 | echo "====================================" 77 | echo "" 78 | echo "Issue Ctrl-C to Exit" 79 | ## Wait... 80 | wait 81 | -------------------------------------------------------------------------------- /bin/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import platform 5 | import sys 6 | import unittest 7 | import pathlib 8 | import collections 9 | from typing import List 10 | 11 | IGNORE_DIRS = ['.pyenv', 'venv', 'config', 'kic-pulumi-utils'] 12 | SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 13 | TEST_FILE_PATTERN = 'test_*.py' 14 | 15 | TestsInDir = collections.namedtuple( 16 | typename='TestsInDir', field_names=['directory', 'loader']) 17 | RunDirectories = collections.namedtuple( 18 | typename='RunDirectories', field_names=['start_dir', 'top_level_dir']) 19 | 20 | test_dirs: List[TestsInDir] = [] 21 | 22 | 23 | def find_testable_dirs(dir_name: pathlib.Path) -> List[pathlib.Path]: 24 | def is_main_file(filename: str) -> bool: 25 | return filename == '__main__.py' or filename == 'main.py' 26 | 27 | test_dirs = [] 28 | contains_main_file = False 29 | 30 | for item in os.listdir(dir_name): 31 | name = str(item) 32 | path = pathlib.Path(dir_name, name) 33 | if path.is_dir() and name != '__pycache__': 34 | test_dirs.extend(find_testable_dirs(path.absolute())) 35 | # If there is a main file we consider it a top level project where tests would 36 | # live under it 37 | elif path.is_file() and is_main_file(name) and not contains_main_file: 38 | contains_main_file = True 39 | test_dirs.append(pathlib.Path(dir_name)) 40 | break 41 | 42 | return test_dirs 43 | 44 | 45 | def find_kic_util_path(): 46 | py_ver = f'{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}' 47 | virtual_env = os.environ.get('VIRTUAL_ENV') 48 | venv_path = virtual_env if virtual_env else 'venv' 49 | 50 | venv_start_dir = f'{venv_path}/lib/python{py_ver}/site-packages/kic_util' 51 | 52 | if not os.path.isdir(venv_start_dir): 53 | raise NotADirectoryError(venv_start_dir) 54 | 55 | # Load in utilities module specifically from venv path 56 | kic_util_loader = unittest.defaultTestLoader.discover( 57 | start_dir=venv_start_dir, 58 | top_level_dir=venv_start_dir 59 | ) 60 | 61 | return TestsInDir(venv_start_dir, kic_util_loader) 62 | 63 | 64 | # We explicitly test the kic util package separately because it needs to live 65 | # under venv when tested. By default, we do no traverse into the venv directory. 66 | test_dirs.append(find_kic_util_path()) 67 | pulumi_python_dir = os.path.join(SCRIPT_DIR, '..', 'pulumi', 'python') 68 | 69 | for item in os.listdir(pulumi_python_dir): 70 | directory = pathlib.Path(pulumi_python_dir, item) 71 | if not directory.is_dir() or item in IGNORE_DIRS: 72 | continue 73 | 74 | directory = pathlib.Path(pulumi_python_dir, item) 75 | for test_dir in find_testable_dirs(directory): 76 | start_dir = str(os.path.realpath(test_dir)) 77 | loader = unittest.defaultTestLoader.discover( 78 | start_dir=start_dir, 79 | top_level_dir=start_dir, 80 | pattern=TEST_FILE_PATTERN) 81 | test_dirs.append(TestsInDir(start_dir, loader)) 82 | 83 | successful = True 84 | 85 | for test_dir in test_dirs: 86 | runner = unittest.TextTestRunner(verbosity=2) 87 | print(f'## Running Tests for: {test_dir.directory}', file=sys.stderr) 88 | result = runner.run(test_dir.loader) 89 | if not result.wasSuccessful(): 90 | successful = False 91 | 92 | if not successful: 93 | sys.exit(1) 94 | -------------------------------------------------------------------------------- /bin/test_runner.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o pipefail # don't hide errors within pipes 5 | 6 | # 7 | # Because of GH actions and Docker and the different ways they work, we need to make sure we 8 | # define the environment of our tests properly. 9 | # 10 | # This change allows us to pass an argument to the command which is then used as the ROOT of the 11 | # repository when running our tests. Without an argument we default to the home directory (which works 12 | # for docker but not GH actions 13 | # 14 | 15 | if [ -z "$1" ]; then 16 | source ~/pulumi/python/venv/bin/activate 17 | ~/pulumi/python/venv/bin/python3 ~/bin/test.py 18 | else 19 | source "$1/pulumi/python/venv/bin/activate" 20 | $1/pulumi/python/venv/bin/python3 $1/bin/test.py 21 | fi 22 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | environment 2 | *.yaml -------------------------------------------------------------------------------- /config/pulumi/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/config/pulumi` 4 | 5 | ## Purpose 6 | 7 | This directory contains the yaml configuration files used for the pulumi 8 | installation. 9 | 10 | ## Key Files 11 | 12 | * [`Pulumi.stackname.yaml.example`](./Pulumi.stackname.yaml.example) Contains 13 | the list of variables that this installation understands. 14 | * [`environmenet`](./environment) Created at runtime; this file contains details 15 | about the environment including the stack name, and the ASW profile and region 16 | (if deploying in AWS). 17 | * `Pulumi.YOURSTACK.yaml` Contains the list of variables associated with the 18 | stack with the name YOURSTACK. This configuration will be created at the first 19 | run for the named stack, but it can be created in advance with an editor. 20 | 21 | ## Notes 22 | 23 | Many of the variables have defaults that are enforced through the Pulumi code 24 | for each project, however there are certain variables that are required. When 25 | the process reaches one of these variables and it is not set the process will 26 | abort with an error message. 27 | -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG ARCH=amd64 2 | 3 | FROM $ARCH/docker:latest AS docker 4 | 5 | FROM $ARCH/debian:bullseye-slim 6 | ARG DEBIAN_FRONTEND=noninteractive 7 | ARG UID 8 | ARG GID 9 | ARG DOCKER_GID=999 10 | COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker 11 | 12 | RUN set -eux; \ 13 | groupadd --gid $DOCKER_GID docker; \ 14 | groupadd --gid $GID runner; \ 15 | mkdir -p /pulumi/projects; \ 16 | useradd --home-dir /pulumi/projects/kic-reference-architectures \ 17 | --groups docker --uid $UID --gid $GID --shell /bin/bash --create-home runner 18 | 19 | # Copy the source code into the container... 20 | COPY --chown=runner:runner . /pulumi/projects/kic-reference-architectures 21 | 22 | RUN set -eux; \ 23 | apt-get update -qq; \ 24 | apt-get install --no-install-recommends -qqq --yes \ 25 | gcc \ 26 | python3-venv \ 27 | ca-certificates \ 28 | git \ 29 | libbz2-dev \ 30 | libffi-dev \ 31 | libreadline-dev \ 32 | libsqlite3-dev \ 33 | libssl-dev \ 34 | make \ 35 | nano \ 36 | vim \ 37 | wget \ 38 | zlib1g-dev; \ 39 | su --group runner runner --login --command '/pulumi/projects/kic-reference-architectures/bin/setup_venv.sh'; \ 40 | echo 'source /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/activate' >> /pulumi/projects/kic-reference-architectures/.bashrc; \ 41 | apt-get purge --yes \ 42 | gcc \ 43 | libbz2-dev \ 44 | libffi-dev \ 45 | libreadline-dev \ 46 | libsqlite3-dev \ 47 | libssl-dev \ 48 | zlib1g-dev; \ 49 | apt-get purge --yes --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ 50 | rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* /usr/share/man/* /root/.cache \ 51 | /pulumi/projects/kic-reference-architectures/.cache \ 52 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-dotnet \ 53 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-go \ 54 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-nodejs; \ 55 | find -type d -name __pycache__ -exec rm --force --recursive '{}' \; 2> /dev/null || true 56 | 57 | USER runner 58 | WORKDIR /pulumi/projects/kic-reference-architectures 59 | 60 | CMD ["/bin/bash", "--login"] 61 | -------------------------------------------------------------------------------- /docker/Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | ARG ARCH=amd64 2 | 3 | FROM $ARCH/docker:latest AS docker 4 | 5 | FROM $ARCH/ubuntu:focal 6 | ARG DEBIAN_FRONTEND=noninteractive 7 | ARG UID 8 | ARG GID 9 | ARG DOCKER_GID=999 10 | COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker 11 | 12 | RUN set -eux; \ 13 | groupadd --gid $DOCKER_GID docker; \ 14 | groupadd --gid $GID runner; \ 15 | mkdir -p /pulumi/projects; \ 16 | useradd --home-dir /pulumi/projects/kic-reference-architectures \ 17 | --groups docker --uid $UID --gid $GID --shell /bin/bash --create-home runner 18 | 19 | COPY --chown=runner:runner . /pulumi/projects/kic-reference-architectures 20 | 21 | RUN set -eux; \ 22 | apt-get update -qq; \ 23 | apt-get install --no-install-recommends -qqq --yes \ 24 | gcc \ 25 | ca-certificates \ 26 | git \ 27 | libbz2-dev \ 28 | libffi-dev \ 29 | libreadline-dev \ 30 | libsqlite3-dev \ 31 | libssl-dev \ 32 | make \ 33 | nano \ 34 | vim \ 35 | wget \ 36 | zlib1g-dev; \ 37 | su --group runner runner --login --command '/pulumi/projects/kic-reference-architectures/bin/setup_venv.sh'; \ 38 | echo 'source /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/activate' >> /pulumi/projects/kic-reference-architectures/.bashrc; \ 39 | apt-get purge --yes \ 40 | gcc \ 41 | libbz2-dev \ 42 | libffi-dev \ 43 | libreadline-dev \ 44 | libsqlite3-dev \ 45 | libssl-dev \ 46 | zlib1g-dev; \ 47 | apt-get purge --yes --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ 48 | rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* /usr/share/man/* /root/.cache \ 49 | /pulumi/projects/kic-reference-architectures/.cache \ 50 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-dotnet \ 51 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-go \ 52 | /pulumi/projects/kic-reference-architectures/pulumi/python/venv/bin/pulumi-language-nodejs; \ 53 | find -type d -name __pycache__ -exec rm --force --recursive '{}' \; 2> /dev/null || true 54 | 55 | #USER runner 56 | WORKDIR /pulumi/projects/kic-reference-architectures 57 | 58 | CMD ["/bin/bash", "--login"] 59 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/docker` 4 | 5 | ## Purpose 6 | 7 | This directory contains the necessary code to create a docker image that can 8 | then be used to deploy MARA. Each docker image created is self-sufficient with 9 | all necessary tools installed. In order to fully understand how to use these 10 | images, please see the [Getting Started](../docs/getting_started.md) guide. 11 | 12 | ## Key Files 13 | 14 | * [`build_dev_docker_image.sh`](./build_dev_docker_image.sh) Controlling script 15 | for docker build process. 16 | 17 | ## Notes 18 | 19 | Please be sure to read the instructions. 20 | -------------------------------------------------------------------------------- /docker/build_dev_docker_image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 8 | 9 | DEFAULT_UID=1000 10 | DEFAULT_GID=1000 11 | 12 | # CentOS already has a Group Id (GID) assigned of 999, so we use 996 the GID that gets 13 | # auto-assigned when you install docker onto a fresh CentOS install. 14 | if [ "${1}" == "centos" ]; then 15 | DEFAULT_DOCKER_GID=996 16 | # Otherwise, we use 999 the GID that gets auto-assigned when you install 17 | # docker onto a fresh Debian install. 18 | else 19 | DEFAULT_DOCKER_GID=999 20 | fi 21 | 22 | # Choose a docker GID based on the owner of the Docker socket or the existing Docker group. 23 | if [ -S "/var/run/docker.sock" ]; then 24 | DOCKER_GID="$(stat --printf="%g" /var/run/docker.sock 2>/dev/null || echo ${DEFAULT_DOCKER_GID})" 25 | elif command -v getent >/dev/null; then 26 | DOCKER_GID="$(getent group docker | cut --delimiter=: --field=3)" 27 | else 28 | DOCKER_GID=$DEFAULT_DOCKER_GID 29 | fi 30 | 31 | # If we chose a GID that conflicts with a known gid on CentOS, we use the default 32 | # instead. 33 | if [ "${1}" == "centos" ]; then 34 | if [[ $DOCKER_GID -gt 996 ]] && [[ $DOCKER_GID -lt 1000 ]]; then 35 | DOCKER_GID=$DEFAULT_DOCKER_GID 36 | fi 37 | fi 38 | 39 | # If we have the id command, then we use it to get the current user's uid and gid. 40 | # This helps when we mount directories into a Docker image. It isn't strictly 41 | # necessary, but it removes a headache when using the image in a development 42 | # workflow. 43 | if command -v id >/dev/null; then 44 | CURRENT_USER_UID="$(id -u || echo ${DEFAULT_UID})" 45 | CURRENT_USER_GID="${CURRENT_USER_UID}" 46 | 47 | # Reject superuser UIDs 48 | if [ "$CURRENT_USER_UID" -eq 0 ]; then 49 | DOCKER_USER_UID=$DEFAULT_UID 50 | else 51 | DOCKER_USER_UID=$CURRENT_USER_UID 52 | fi 53 | 54 | # Reject superuser GIDs 55 | if [ "$CURRENT_USER_GID" -eq 0 ]; then 56 | DOCKER_USER_GID=$DEFAULT_GID 57 | else 58 | DOCKER_USER_GID=$CURRENT_USER_GID 59 | fi 60 | # If we don't have an id command, we just use the defaults. 61 | else 62 | DOCKER_USER_UID=$DEFAULT_UID 63 | DOCKER_USER_GID=$DEFAULT_GID 64 | fi 65 | 66 | # Attempt to build the container with the same architecture as the host. 67 | ARCH="" 68 | case $(uname -m) in 69 | i386) ARCH="386" ;; 70 | i686) ARCH="386" ;; 71 | x86_64) ARCH="amd64" ;; 72 | aarch64) ARCH="arm64v8" ;; 73 | arm) dpkg --print-architecture | grep -q "arm64" && ARCH="arm64v8" || ARCH="arm" ;; 74 | *) 75 | echo >&2 "Unable to determine system architecture." 76 | exit 1 77 | ;; 78 | esac 79 | echo "Building container image with [${ARCH}] system architecture]" 80 | 81 | # Squash our image if we are running in experimental mode 82 | if [ "$(docker version -f '{{.Server.Experimental}}')" == 'true' ]; then 83 | echo "Enabling squash mode for container image" 84 | additional_docker_opts="--squash" 85 | else 86 | additional_docker_opts="" 87 | fi 88 | 89 | echo "User id for [runner] user in container ${DOCKER_USER_UID}" 90 | echo "Group id for [runner] group in container ${DOCKER_USER_GID}" 91 | echo "Group id for [docker] group in container ${DOCKER_GID}" 92 | 93 | docker build ${additional_docker_opts} \ 94 | --build-arg ARCH="${ARCH}" \ 95 | --build-arg UID="${DOCKER_USER_UID}" \ 96 | --build-arg GID="${DOCKER_USER_GID}" \ 97 | --build-arg DOCKER_GID="${DOCKER_GID}" \ 98 | -t "kic-ref-arch-pulumi-aws:${1}" \ 99 | -f "${script_dir}/Dockerfile.${1}" \ 100 | "${script_dir}/.." 101 | 102 | # Run unit tests 103 | docker run --interactive --tty --rm "kic-ref-arch-pulumi-aws:${1}" bin/test_runner.sh 104 | -------------------------------------------------------------------------------- /docs/DIAG-NGINX-ModernAppsRefArch-NGINX-MARA-1-0-blog-1024x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/docs/DIAG-NGINX-ModernAppsRefArch-NGINX-MARA-1-0-blog-1024x800.png -------------------------------------------------------------------------------- /docs/NGINX-MARA-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/docs/NGINX-MARA-icon.png -------------------------------------------------------------------------------- /docs/accessing_mgmt_tools.md: -------------------------------------------------------------------------------- 1 | # Accessing the Management Tools in MARA 2 | 3 | Currently, the management tool suite in MARA consists of: 4 | 5 | - [Prometheus](https://prometheus.io/) 6 | - [Grafana](https://grafana.com) 7 | - [Locust](https://locust.io) 8 | - [Elasticsearch](https://elastic.co) 9 | - [Kibana](https://www.elastic.co/kibana/) 10 | 11 | Each of these tools provides an interface that can be reached through an 12 | endpoint exposed by the tool. For security reasons these tools are not exposed 13 | to the internet, which means you will need to use some form of port forwarding 14 | to access them. 15 | 16 | ## Running MARA on your Local Workstation 17 | 18 | If you are running MARA on your local workstation, you can use the 19 | [`test-forward.sh`](../bin/test-forward.sh) script to use 20 | [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) to forward the ports 21 | on your behalf. These ports are all forwarded to the corresponding port on 22 | localhost as shown below: 23 | 24 | ```txt 25 | Connections Details 26 | ==================================== 27 | Kibana: http://localhost:5601 28 | Grafana: http://localhost:3000 29 | Locust: http://localhost:8089 30 | Prometheus: http://localhost:9090 31 | Elasticsearch: http://localhost:9200 32 | ==================================== 33 | 34 | Issue Ctrl-C to Exit 35 | ``` 36 | 37 | Issuing a Ctrl-C will cause the ports to close. 38 | 39 | ## Running MARA Somewhere Else 40 | 41 | In the event you are running MARA somewhere else - in the cloud, on a different 42 | server, in a VM on your laptop, etc. you will need to go through an additional 43 | step. Note that this is just one way of accomplishing this, and depending on 44 | your environment you may want or need to do this differently. 45 | 46 | The easiest thing is to install `kubectl` on the system you want to access the 47 | MARA tooling from and then copy over the `kubeconfig` from your MARA deployment 48 | system. This will then allow you to copy over the `test-forward.sh` script and 49 | use that to build the tunnels locally. 50 | 51 | ## Edge Cases 52 | 53 | There are definitely cases where these solutions will not work. Please see the 54 | "More Information" section below, and if you have one of these cases and 55 | discover a solution please open a PR so that we can add to this section. 56 | 57 | ## More Information 58 | 59 | To learn more about Kubernetes port-forwarding, please see 60 | [this article](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/) 61 | -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/extras` 4 | 5 | ## Purpose 6 | 7 | This directory is for files that, although important, don't have a clearly 8 | defined home. Files from this directory will most likely be moved as the 9 | project matures. 10 | 11 | ## Key Files 12 | 13 | * [`jwt.token`](./jwt.token) This file contains the JWT required to pull 14 | the NGINX IC from the NGINX, Inc registry. See 15 | [this webpage](https://docs.nginx.com/nginx-ingress-controller/installation/using-the-jwt-token-docker-secret) 16 | for details and examples. 17 | * [`jenkins`](./jenkins) This directory contains sample jenkinsfiles. Note 18 | that these are not guaranteed to be production ready. These files are named 19 | according to the specific type of build they manage; for example, AWS, K3S, 20 | MicroK8s, and DO (Digital Ocean). 21 | 22 | ## Notes 23 | 24 | None. 25 | -------------------------------------------------------------------------------- /extras/jenkins/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/extras/jenkins` 4 | 5 | ## Purpose 6 | 7 | This directory contains several subdirectories, each of which contains a 8 | [Jenkinsfile](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/). These are 9 | designed to be used by the [Jenkins](https://www.jenkins.io/) CI system to run 10 | deployments of the MARA project. These can be used as-is from the repository 11 | using the ability of Jenkins to pull its pipeline configuration from SCM, as 12 | described in 13 | [this article](https://www.jenkins.io/doc/book/pipeline/getting-started/#defining-a-pipeline-in-scm) 14 | 15 | Please note that these should be considered to be in a "draft" status, and 16 | should be reviewed and modified if you plan on using them. As always, pull 17 | requests, issues, and comments are welcome. 18 | 19 | ## Key Files 20 | 21 | - [`AWS`](./AWS) This directory contains the [`Jenkinsfile`](./AWS/Jenkinsfile) 22 | to deploy to AWS. Please see the file for additional information regarding the 23 | configuration. 24 | - [`DigitalOcean`](./DigitalOcean) This directory contains the 25 | [`Jenkinsfile`](./DigitalOcean/Jenkinsfile) to deploy to Digital Ocean. Please 26 | see the file for additional information regarding the configuration. 27 | - [`K3S`](./K3S) This directory contains the [`Jenkinsfile`](./AWS/Jenkinsfile) 28 | to deploy to K3S. Please see the file for additional information regarding the 29 | configuration. 30 | - [`MicroK8s`](./MicroK8s) This directory contains the 31 | [`Jenkinsfile`](./AWS/MicroK8s) to deploy to MicroK8s. Please see the file for 32 | additional information regarding the configuration. 33 | 34 | ## Notes 35 | 36 | None. 37 | -------------------------------------------------------------------------------- /extras/jwt.token: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/extras/jwt.token -------------------------------------------------------------------------------- /pulumi/python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | awscli = "~=1.25.35" 8 | grpcio = "==1.43.0" 9 | fart = "~=0.1.5" 10 | lolcat = "~=1.4" 11 | passlib = "~=1.7.4" 12 | pulumi-aws = ">=4.39.0" 13 | pulumi-docker = "==3.1.0" 14 | pulumi-eks = ">=0.41.2" 15 | pulumi-kubernetes = "==3.20.1" 16 | pycryptodome = "~=3.14.0" 17 | requests = "~=2.27.1" 18 | setuptools-git-versioning = "==1.9.2" 19 | yamlreader = "==3.0.4" 20 | pulumi-digitalocean = "==4.12.0" 21 | pulumi-linode = "==3.7.1" 22 | linode-cli = "~=5.17.2" 23 | pulumi = "~=3.36.0" 24 | PyYAML = "~=5.4.1" 25 | nodeenv = "~=1.6.0" 26 | 27 | [dev-packages] 28 | wheel = "~=0.37.1" 29 | nodeenv = "~=1.6.0" 30 | 31 | [requires] 32 | python_version = "3.9" 33 | -------------------------------------------------------------------------------- /pulumi/python/automation/colorize.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file provides two functions println_nocolor and println_color - println_color will be redirected to 3 | println_nocolor if the execution environment does not support color output. If the environment does support 4 | color output, then the string specified for println_color will be rendered in rainbow colors using the lolcat 5 | library. 6 | """ 7 | 8 | import collections 9 | import os 10 | import random 11 | import sys 12 | import typing 13 | from importlib.machinery import SourceFileLoader 14 | 15 | 16 | def println_nocolor(text: str, output: typing.TextIO = sys.stdout): 17 | """Prints a new line to the console without using color 18 | :param text: text to print 19 | :param output: output destination 20 | """ 21 | print(text, file=output) 22 | 23 | 24 | if os.environ.get('NO_COLOR'): 25 | PRINTLN_FUNC = println_nocolor 26 | else: 27 | lolcat_fields = ['animate', 'duration', 'force', 'freq', 'mode', 'speed', 'spread', 'os'] 28 | LolCatOptions = collections.namedtuple('LolCatOptions', lolcat_fields) 29 | 30 | # Unfortunately, we do the below hack to load the lolcat code because it was not written 31 | # such that it could be easily consumable as a library, for it was a stand-alone executable. 32 | if os.environ.get('VIRTUAL_ENV'): 33 | venv = os.environ.get('VIRTUAL_ENV') 34 | lolcat_path = os.path.sep.join([venv, 'bin', 'lolcat']) 35 | if os.path.exists(lolcat_path): 36 | loader = SourceFileLoader('lolcat', lolcat_path) 37 | lolcat = loader.load_module() 38 | 39 | if lolcat: 40 | options = LolCatOptions(animate=False, 41 | duration=12, 42 | freq=0.1, 43 | os=random.randint(0, 256), 44 | mode=lolcat.detect_mode(), 45 | speed=-1.0, 46 | spread=0.5, 47 | force=False) 48 | 49 | def println_color(text: str, output: typing.TextIO = sys.stdout): 50 | """Prints a new line to the console using rainbow colors 51 | :param text: text to print 52 | :param output: output destination 53 | """ 54 | colorizer = lolcat.LolCat(mode=options.mode, output=output) 55 | colorizer.println_plain(text, options) 56 | output.write('\x1b[0m') 57 | output.flush() 58 | 59 | PRINTLN_FUNC = println_color 60 | else: 61 | PRINTLN_FUNC = println_nocolor 62 | 63 | -------------------------------------------------------------------------------- /pulumi/python/automation/env_config_parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines a data structure containing the environment variables that have been written to a file 3 | (`config/pulumi/environment`). The values stored there are used to specify the environment when executing 4 | operations using the Pulumi Automation API. 5 | """ 6 | 7 | import os 8 | from typing import Optional, Mapping 9 | from configparser import ConfigParser 10 | 11 | import stack_config_parser 12 | 13 | # Directory in which script is located 14 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 15 | # Default path to the MARA environment file 16 | DEFAULT_PATH = os.path.abspath(os.path.sep.join([SCRIPT_DIR, '..', '..', '..', 'config', 'pulumi', 'environment'])) 17 | 18 | # Default environment variables set for all Pulumi executions invoked by the Automation API 19 | DEFAULT_ENV_VARS = { 20 | 'PULUMI_SKIP_UPDATE_CHECK': 'true' 21 | } 22 | 23 | 24 | class EnvConfig(dict): 25 | """Object containing environment variables used when executing operations with the Pulumi Automation API""" 26 | 27 | _stack_config: Optional[stack_config_parser.PulumiStackConfig] = None 28 | config_path: Optional[str] = None 29 | 30 | def __init__(self, 31 | env_vars: Mapping[str, str], 32 | file_vars: Mapping[str, str], 33 | stack_config: Optional[stack_config_parser.PulumiStackConfig] = None, 34 | config_path: Optional[str] = None) -> None: 35 | super().__init__() 36 | self.update(DEFAULT_ENV_VARS) 37 | self.update(env_vars) 38 | self.update(file_vars) 39 | self._stack_config = stack_config 40 | self.config_path = config_path 41 | 42 | def stack_name(self) -> str: 43 | """Returns the stack name used in the environment""" 44 | return self.get('PULUMI_STACK') 45 | 46 | def no_color(self) -> bool: 47 | """Returns a flag if color in the console is supported""" 48 | return self.get('NO_COLOR') is not None 49 | 50 | def pulumi_color_settings(self): 51 | """Returns a string indicating if console colors should be auto-detected or just disabled""" 52 | if self.no_color(): 53 | return 'never' 54 | else: 55 | return 'auto' 56 | 57 | 58 | def read(config_file_path: str = DEFAULT_PATH) -> EnvConfig: 59 | """Reads the contents of the specified file path into a new instance of `EnvConfig`. 60 | :param config_file_path: path to environment variable file 61 | :return: new instance of EnvConfig 62 | """ 63 | config_parser = ConfigParser() 64 | config_parser.optionxform = lambda option: option 65 | 66 | with open(config_file_path, 'r') as f: 67 | # The Python configparser library is used to parse the file because it supports the KEY=VALUE syntax of the 68 | # environment file. However, there is one exception; it requires the presence of a [main] section using the 69 | # ini format style. In order avoid having to add a "[main]" string to the environment file, we spoof the 70 | # presence of that section with this line below. It just prepends the string "[main]" before the contents of 71 | # the environment file. 72 | content = f'[main]{os.linesep}{f.read()}' 73 | 74 | config_parser.read_string(content) 75 | 76 | return EnvConfig(env_vars=os.environ, file_vars=config_parser['main'], config_path=config_file_path) 77 | -------------------------------------------------------------------------------- /pulumi/python/automation/headers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines the functions needed to render headers that are displayed before each Pulumi project is executed. 3 | These headers provide a useful visual distinction between each step taken to set up an environment. 4 | """ 5 | import logging 6 | 7 | import colorize 8 | import env_config_parser 9 | from fart import fart 10 | 11 | LOG = logging.getLogger('runner') 12 | FART_FONT = fart.load_font('standard') 13 | banner_type = 'fabulous' 14 | 15 | 16 | def render_header(text: str, env_config: env_config_parser.EnvConfig): 17 | """Renders the given text to a header displayed in the console - this header could be large ascii art 18 | :param text: header text to render 19 | :param env_config: reference to environment configuration 20 | """ 21 | global banner_type 22 | 23 | if banner_type == 'fabulous': 24 | header = fart.render_fart(text=text, font=FART_FONT) 25 | if not env_config.no_color(): 26 | colorize.PRINTLN_FUNC(header) 27 | elif banner_type == 'log': 28 | LOG.info('[%s] started', text) 29 | else: 30 | print(f'* {text}') 31 | -------------------------------------------------------------------------------- /pulumi/python/automation/providers/pulumi_project.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains classes related to modeling Pulumi projects as discrete directories that 3 | are invoked individually in sequence by the Pulumi Automation API. 4 | """ 5 | 6 | import os.path 7 | from typing import Optional, Callable, Mapping, List, MutableMapping 8 | import yaml 9 | from pulumi import automation as auto 10 | 11 | # Directory in which script is located 12 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | class PulumiConfigException(Exception): 16 | """Generic exception thrown when Pulumi configuration errors are encountered""" 17 | pass 18 | 19 | 20 | class SecretConfigKey: 21 | """ 22 | Class representing a secret that the user will be prompted to enter and subsequently stored in the Pulumi 23 | secrets store. 24 | """ 25 | key_name: str 26 | prompt: str 27 | default: Optional[str] 28 | 29 | def __init__(self, key_name: str, prompt: str, default: Optional[str] = None) -> None: 30 | super().__init__() 31 | self.key_name = key_name 32 | self.prompt = prompt 33 | self.default = default 34 | 35 | 36 | class PulumiProject: 37 | """ 38 | Class representing a Pulumi project that is associated with a directory and containing properties regarding the 39 | secrets used, description and the operation to run when it is successfully stood up. 40 | """ 41 | path: str 42 | description: str 43 | config_keys_with_secrets: List[SecretConfigKey] 44 | on_success: Optional[Callable] = None 45 | _config_data: Optional[Mapping[str, str]] = None 46 | 47 | def __init__(self, 48 | path: str, 49 | description: str, 50 | config_keys_with_secrets: Optional[List[SecretConfigKey]] = None, 51 | on_success: Optional[Callable] = None) -> None: 52 | super().__init__() 53 | self.path = path 54 | self.description = description 55 | self.config_keys_with_secrets = config_keys_with_secrets or [] 56 | self.on_success = on_success 57 | 58 | def abspath(self) -> str: 59 | relative_path = os.path.sep.join([SCRIPT_DIR, '..', '..', self.path]) 60 | return os.path.abspath(relative_path) 61 | 62 | def config(self) -> Mapping[str, str]: 63 | if not self._config_data: 64 | config_path = os.path.sep.join([self.abspath(), 'Pulumi.yaml']) 65 | with open(config_path, 'r') as f: 66 | self._config_data = yaml.safe_load(f) 67 | 68 | return self._config_data 69 | 70 | def name(self) -> str: 71 | config_data = self.config() 72 | 73 | if 'name' not in config_data.keys(): 74 | raise PulumiConfigException('Pulumi configuration did not contain required "name" key') 75 | 76 | return config_data['name'] 77 | 78 | 79 | class PulumiProjectEventParams: 80 | """Object containing the state passed to an on_success event after the successful stand up of a Pulumi project.""" 81 | stack_outputs: MutableMapping[str, auto._output.OutputValue] 82 | config: MutableMapping[str, auto._config.ConfigValue] 83 | env_config: Mapping[str, str] 84 | 85 | def __init__(self, 86 | stack_outputs: MutableMapping[str, auto._output.OutputValue], 87 | config: MutableMapping[str, auto._config.ConfigValue], 88 | env_config: Mapping[str, str]) -> None: 89 | self.stack_outputs = stack_outputs 90 | self.config = config 91 | self.env_config = env_config 92 | -------------------------------------------------------------------------------- /pulumi/python/automation/stack_config_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Optional, MutableMapping 4 | 5 | from pulumi.automation import ConfigValue 6 | 7 | import yaml 8 | 9 | # Directory in which script is located 10 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 11 | # Default path to the directory containing the global MARA Pulumi stack configuration file 12 | DEFAULT_DIR_PATH = os.path.abspath(os.path.sep.join([SCRIPT_DIR, '..', '..', '..', 'config', 'pulumi'])) 13 | 14 | 15 | class EmptyConfigurationException(RuntimeError): 16 | filename: str 17 | 18 | def __init__(self, filename: str, *args: object) -> None: 19 | super().__init__(*args) 20 | self.filename = filename 21 | 22 | 23 | class PulumiStackConfig(dict): 24 | """Object containing the configuration parameters used by Pulumi to stand up projects. When this file is loaded by 25 | Pulumi within the context of a project execution, it is *not* loaded into this object. This object is used only by 26 | the MARA runner for the Pulumi Automation API.""" 27 | 28 | config_path: Optional[str] = None 29 | 30 | def to_pulumi_config_value(self) -> MutableMapping[str, ConfigValue]: 31 | if 'config' not in self: 32 | return {} 33 | 34 | config = self.get('config') 35 | 36 | pulumi_config = {} 37 | for key, val in config.items(): 38 | if type(val) in [str, int, float]: 39 | pulumi_config[key] = ConfigValue(value=val) 40 | elif type(val) is dict and 'secure' in val: 41 | pulumi_config[key] = ConfigValue(value=val['secure'], secret=True) 42 | else: 43 | json_val = json.dumps(val) 44 | pulumi_config[key] = ConfigValue(value=json_val) 45 | 46 | return pulumi_config 47 | 48 | 49 | def _stack_config_path(stack_name: str) -> str: 50 | """Path to the stack configuration file on the file system""" 51 | return os.path.sep.join([DEFAULT_DIR_PATH, f'Pulumi.{stack_name}.yaml']) 52 | 53 | 54 | def _read(config_file_path: str) -> PulumiStackConfig: 55 | """Reads the "stack configuration file from the specified path, parses it, and loads it into the PulumiStackConfig 56 | data structure.""" 57 | 58 | # Return empty config for empty config files 59 | if os.path.getsize(config_file_path) == 0: 60 | raise EmptyConfigurationException(filename=config_file_path) 61 | 62 | with open(config_file_path, 'r') as f: 63 | stack_config = PulumiStackConfig() 64 | stack_config.config_path = config_file_path 65 | stack_config.update(yaml.safe_load(f)) 66 | return stack_config 67 | 68 | 69 | def read(stack_name: str) -> PulumiStackConfig: 70 | """Generate the configuration file path based on the stack name, reads the "stack configuration file, parse it, 71 | and load it into the PulumiStackConfig data structure. 72 | 73 | :param stack_name: stack name to read configuration for 74 | :return: new instance of PulumiStackConfig 75 | """ 76 | stack_config_path = _stack_config_path(stack_name) 77 | return _read(stack_config_path) 78 | -------------------------------------------------------------------------------- /pulumi/python/config/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: common-config 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../venv 6 | config: ../../../config/pulumi 7 | description: Common Configuration Project -------------------------------------------------------------------------------- /pulumi/python/config/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/pulumi/python/config` 4 | 5 | ## Purpose 6 | 7 | This directory is used for configuration management in Pulumi. In previous 8 | versions of this project, the `vpc` directory was used to manage writes to the 9 | configuration file. This is required because you can only run the `pulumi config` 10 | command if you have a `Pulumi.yaml` somewhere in your directory or above that 11 | allows you to use the Pulumi tooling. 12 | 13 | Why not use each stack directory as its own configuration? Using different 14 | directories will result in failures encrypting/decrypting the values in the 15 | main configuration file if different stacks are used. This is a stopgap 16 | workaround that will be obsoleted at such time that Pulumi provides 17 | nested/included configuration files. This is also the reason why we have created 18 | the `secrets` project. 19 | 20 | ## Key Files 21 | 22 | * [`Pulumi.yaml`](./Pulumi.yaml) This file tells the `pulumi` command where to 23 | * find its virtual environment and its configuration. 24 | 25 | ## Notes 26 | 27 | Once Pulumi adds nested configuration files to the product we should be able to 28 | remove this work-around. 29 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/python/pulumi/infrastructure` 4 | 5 | ## Purpose 6 | 7 | Holds all infrastructure related files. 8 | 9 | ## Key Files 10 | 11 | * [`aws`](./aws) Files to stand up a K8 cluster in AWS using VPC, EKS, and ECR. 12 | * [`digitalocean`](./digitalocean) Files to stand up a K8 cluster in 13 | DigitalOcean using DO Managed K8s. 14 | * [`linode`](./linode) Files to stand up a K8 cluster in Linode using Linode 15 | Kubernetes Engine. 16 | * [`kubeconfig`](./kubeconfig) Files to allow users to connect to any kubernetes 17 | installation that can be specified via a `kubeconfig` file. 18 | 19 | ## Notes 20 | 21 | The `kubeconfig` project is intended to serve as a shim between infrastructure 22 | providers and the rest of the project. For example, even if you use the AWS 23 | logic you will still use the logic inside the `kubeconfig` stack as part of the 24 | process. Additional infrastructures added will need to follow this pattern. 25 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | build_dev_docker_image.sh 3 | Dockerfile 4 | venv 5 | __pycache__ -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/ecr/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: aws-ecr 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Sets up ECR repository for ingress controller images 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/ecr/__main__.py: -------------------------------------------------------------------------------- 1 | import pulumi 2 | from pulumi_aws import ecr 3 | 4 | stack_name = pulumi.get_stack() 5 | project_name = pulumi.get_project() 6 | 7 | # Build a new ECR instance for storing KIC Docker images 8 | ecr_repo = ecr.Repository(name=f'ingress-controller-{stack_name}', 9 | resource_name=f'nginx-ingress-repository-{stack_name}', 10 | image_tag_mutability="MUTABLE", 11 | force_delete=True, 12 | tags={"Project": project_name, "Stack": stack_name}) 13 | 14 | pulumi.export('repository_url', ecr_repo.repository_url) 15 | pulumi.export('registry_id', ecr_repo.registry_id) 16 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/eks/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: aws-eks 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new EKS cluster using existing VPC 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/eks/__main__.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os 3 | 4 | import pulumi 5 | import pulumi_aws as aws 6 | import pulumi_eks as eks 7 | 8 | import iam 9 | from kic_util import pulumi_config 10 | 11 | VPCDefinition = collections.namedtuple('VPCDefinition', ['vpc_id', 'public_subnet_ids', 'private_subnet_ids']) 12 | 13 | 14 | def pulumi_vpc_project_name(): 15 | script_dir = os.path.dirname(os.path.abspath(__file__)) 16 | vpc_project_path = os.path.join(script_dir, '..', 'vpc') 17 | return pulumi_config.get_pulumi_project_name(vpc_project_path) 18 | 19 | 20 | def retrieve_vpc_and_subnets(vpc) -> VPCDefinition: 21 | pulumi.log.info(f"vpc id: {vpc['id']}") 22 | 23 | _public_subnet_ids = aws.ec2.get_subnet_ids(vpc_id=vpc['id'], 24 | tags={"Project": "aws-vpc", 25 | "Stack": stack_name, 26 | "kubernetes.io/role/elb": "1"}).ids 27 | pulumi.log.info(f"public subnets: {_public_subnet_ids}") 28 | 29 | _private_subnet_ids = aws.ec2.get_subnet_ids(vpc_id=vpc['id'], 30 | tags={"Project": "aws-vpc", 31 | "Stack": stack_name, 32 | "kubernetes.io/role/internal-elb": "1"}).ids 33 | pulumi.log.info(f"public subnets: {_private_subnet_ids}") 34 | 35 | return VPCDefinition(vpc_id=vpc['id'], public_subnet_ids=_public_subnet_ids, private_subnet_ids=_private_subnet_ids) 36 | 37 | 38 | config = pulumi.Config("eks") 39 | k8s_version = config.get('k8s_version') if config.get('k8s_version') else '1.21' 40 | instance_type = config.get('instance_type') if config.get('instance_type') else 't2.large' 41 | min_size = config.get_int('min_size') if config.get('min_size') else 3 42 | max_size = config.get_int('max_size') if config.get('max_size') else 12 43 | desired_capacity = config.get_int('desired_capacity') if config.get('desired_capacity') else 3 44 | 45 | stack_name = pulumi.get_stack() 46 | project_name = pulumi.get_project() 47 | vpc_project_name = pulumi_vpc_project_name() 48 | pulumi_user = pulumi_config.get_pulumi_user() 49 | aws_config = pulumi.Config("aws") 50 | aws_profile = aws_config.get("profile") 51 | 52 | provider_credential_opts = {} 53 | if aws_profile is not None: 54 | pulumi.log.info(f"aws {aws_profile} profile") 55 | provider_credential_opts["profileName"] = aws_profile 56 | 57 | stack_ref_id = f"{pulumi_user}/{vpc_project_name}/{stack_name}" 58 | stack_ref = pulumi.StackReference(stack_ref_id) 59 | vpc_definition: pulumi.Output[VPCDefinition] = stack_ref.get_output('vpc').apply(retrieve_vpc_and_subnets) 60 | 61 | instance_profile = aws.iam.InstanceProfile( 62 | resource_name=f'node-group-profile-{project_name}-{stack_name}', 63 | role=iam.ec2_role 64 | ) 65 | 66 | # 67 | # We were initially using a "ClusterNodeGroupOptionsArg" construct here, but that was sporadically failing when 68 | # the process would run. This has been raised as an issue both in this project, and an issue in the Pulumi EKS 69 | # project. 70 | # 71 | # See https://github.com/nginxinc/kic-reference-architectures/issues/72 for details and discussion on the current 72 | # workaround being used here. 73 | # 74 | 75 | cluster_args = eks.ClusterArgs( 76 | min_size=min_size, 77 | max_size=max_size, 78 | desired_capacity=desired_capacity, 79 | instance_type=instance_type, 80 | vpc_id=vpc_definition.vpc_id, 81 | public_subnet_ids=vpc_definition.public_subnet_ids, 82 | private_subnet_ids=vpc_definition.private_subnet_ids, 83 | service_role=iam.eks_role, 84 | create_oidc_provider=False, 85 | version=k8s_version, 86 | provider_credential_opts=provider_credential_opts, 87 | tags={"Project": project_name, "Stack": stack_name} 88 | ) 89 | 90 | # Create an EKS cluster with the default configuration. 91 | cluster = eks.Cluster(resource_name=f"{project_name}-{stack_name}", 92 | args=cluster_args) 93 | 94 | # Export the clusters' kubeconfig 95 | pulumi.export("cluster_name", cluster.eks_cluster.name) 96 | pulumi.export("kubeconfig", cluster.kubeconfig) 97 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/eks/iam.py: -------------------------------------------------------------------------------- 1 | from pulumi_aws import iam 2 | import json 3 | 4 | # EKS Cluster Role 5 | 6 | eks_role = iam.Role( 7 | 'eks-iam-role', 8 | assume_role_policy=json.dumps({ 9 | 'Version': '2012-10-17', 10 | 'Statement': [ 11 | { 12 | 'Action': 'sts:AssumeRole', 13 | 'Principal': { 14 | 'Service': 'eks.amazonaws.com' 15 | }, 16 | 'Effect': 'Allow', 17 | 'Sid': '' 18 | } 19 | ], 20 | }), 21 | ) 22 | 23 | iam.RolePolicyAttachment( 24 | 'eks-service-policy-attachment', 25 | role=eks_role.id, 26 | policy_arn='arn:aws:iam::aws:policy/AmazonEKSServicePolicy', 27 | ) 28 | 29 | iam.RolePolicyAttachment( 30 | 'eks-cluster-policy-attachment', 31 | role=eks_role.id, 32 | policy_arn='arn:aws:iam::aws:policy/AmazonEKSClusterPolicy', 33 | ) 34 | 35 | ## Ec2 NodeGroup Role 36 | 37 | ec2_role = iam.Role( 38 | 'ec2-nodegroup-iam-role', 39 | assume_role_policy=json.dumps({ 40 | 'Version': '2012-10-17', 41 | 'Statement': [ 42 | { 43 | 'Action': 'sts:AssumeRole', 44 | 'Principal': { 45 | 'Service': 'ec2.amazonaws.com' 46 | }, 47 | 'Effect': 'Allow', 48 | 'Sid': '' 49 | } 50 | ], 51 | }), 52 | ) 53 | 54 | iam.RolePolicyAttachment( 55 | 'eks-workernode-policy-attachment', 56 | role=ec2_role.id, 57 | policy_arn='arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', 58 | ) 59 | 60 | iam.RolePolicyAttachment( 61 | 'eks-cni-policy-attachment', 62 | role=ec2_role.id, 63 | policy_arn='arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', 64 | ) 65 | 66 | iam.RolePolicyAttachment( 67 | 'ec2-container-ro-policy-attachment', 68 | role=ec2_role.id, 69 | policy_arn='arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', 70 | ) 71 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/aws/vpc/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: aws-vpc 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: VPC standup 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/container-registry-credentials/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: container-registry-credentials 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Adds container registry login credentials to the k8s cluster 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/container-registry-credentials/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | from pulumi import StackReference 5 | from pulumi_digitalocean import ContainerRegistryDockerCredentials 6 | from kic_util import pulumi_config 7 | import pulumi_kubernetes as k8s 8 | from pulumi_kubernetes.core.v1 import Secret, SecretInitArgs 9 | 10 | 11 | stack_name = pulumi.get_stack() 12 | project_name = pulumi.get_project() 13 | pulumi_user = pulumi_config.get_pulumi_user() 14 | script_dir = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | def project_name_from_same_parent(directory: str): 18 | project_path = os.path.join(script_dir, '..', directory) 19 | return pulumi_config.get_pulumi_project_name(project_path) 20 | 21 | 22 | def project_name_of_namespace_project(): 23 | project_path = os.path.join(script_dir, '..', '..', '..', 'kubernetes', 'nginx', 'ingress-controller-namespace') 24 | return pulumi_config.get_pulumi_project_name(project_path) 25 | 26 | 27 | k8_project_name = project_name_from_same_parent('domk8s') 28 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 29 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 30 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 31 | 32 | container_registry_stack_ref_id = f"{pulumi_user}/{project_name_from_same_parent('container-registry')}/{stack_name}" 33 | cr_stack_ref = StackReference(container_registry_stack_ref_id) 34 | container_registry_output = cr_stack_ref.require_output('container_registry') 35 | registry_name_output = cr_stack_ref.require_output('container_registry_name') 36 | 37 | namespace_stack_ref_id = f"{pulumi_user}/{project_name_of_namespace_project()}/{stack_name}" 38 | ns_stack_ref = StackReference(namespace_stack_ref_id) 39 | namespace_name_output = ns_stack_ref.require_output('ingress_namespace_name') 40 | 41 | fifty_years_in_seconds = 1_576_800_000 42 | registry_credentials = ContainerRegistryDockerCredentials(resource_name='do_k8s_docker_credentials', 43 | expiry_seconds=fifty_years_in_seconds, 44 | registry_name=registry_name_output, 45 | write=False) 46 | docker_credentials = registry_credentials.docker_credentials 47 | 48 | k8s_provider = k8s.Provider(resource_name='kubernetes', kubeconfig=kubeconfig) 49 | 50 | secret = Secret(resource_name='ingress-controller-registry-secret', 51 | args=SecretInitArgs(string_data={'.dockerconfigjson': docker_credentials}, 52 | type='kubernetes.io/dockerconfigjson', 53 | metadata={'namespace': namespace_name_output, 54 | 'name': 'ingress-controller-registry'}), 55 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 56 | 57 | pulumi.export('ingress-controller-registry-secret', secret) 58 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/container-registry/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: container-registry 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new Digital Ocean Container Registry 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/container-registry/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_digitalocean as docean 5 | 6 | from kic_util import external_process 7 | 8 | config = pulumi.Config('docean') 9 | # valid values: starter, basic, professional 10 | subscription_tier = config.get('container_registry_subscription_tier') 11 | if not subscription_tier: 12 | subscription_tier = 'starter' 13 | region = config.get('region') 14 | if not region: 15 | region = 'sfo3' 16 | 17 | 18 | def token(): 19 | if config.get('token'): 20 | return config.get('token') 21 | if config.get_secret('token'): 22 | return config.get_secret('token') 23 | if 'DIGITALOCEAN_TOKEN' in os.environ: 24 | return os.environ['DIGITALOCEAN_TOKEN'] 25 | raise 'No valid token for Digital Ocean found' 26 | 27 | 28 | stack_name = pulumi.get_stack() 29 | 30 | # Digital Ocean allows only a single container registry per user. This means that we need to use doctl 31 | # to check to see if a registry already exists, and if so use it. We must do this using an external 32 | # command because Pulumi does not support the model of checking to see if a resource created outside of 33 | # Pulumi already exists and thereby forking logic. 34 | registry_name_query_cmd = f'doctl --access-token {token()} registry get --format Name --no-header --output text' 35 | registry_name, err = external_process.run(cmd=registry_name_query_cmd, suppress_error=True) 36 | registry_name = registry_name.strip() 37 | if not err and registry_name and not registry_name.startswith('shared-global-container-registry-'): 38 | pulumi.log.info(f'Using already existing global Digital Ocean container registry: {registry_name}') 39 | container_registry = docean.ContainerRegistry.get(registry_name, id=registry_name) 40 | else: 41 | pulumi.log.info('Creating new global Digital Ocean container registry') 42 | container_registry = docean.ContainerRegistry('shared-global-container-registry', 43 | subscription_tier_slug=subscription_tier, 44 | region=region) 45 | 46 | pulumi.export('container_registry_id', container_registry.id) 47 | pulumi.export('container_registry_name', container_registry.name) 48 | pulumi.export('container_registry', container_registry) 49 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/dns-record/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: dns-record 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new DNS record for Ingress Controller 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/dns-record/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | from pulumi import StackReference 5 | import pulumi_digitalocean as docean 6 | 7 | from kic_util import pulumi_config 8 | 9 | stack_name = pulumi.get_stack() 10 | project_name = pulumi.get_project() 11 | pulumi_user = pulumi_config.get_pulumi_user() 12 | script_dir = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | def project_name_of_ingress_controller_project(): 16 | project_path = os.path.join(script_dir, '..', '..', '..', 'kubernetes', 'nginx', 'ingress-controller') 17 | return pulumi_config.get_pulumi_project_name(project_path) 18 | 19 | 20 | def extract_ip_address(lb_ingress): 21 | return lb_ingress['load_balancer']['ingress'][0]['ip'] 22 | 23 | 24 | namespace_stack_ref_id = f"{pulumi_user}/{project_name_of_ingress_controller_project()}/{stack_name}" 25 | ns_stack_ref = StackReference(namespace_stack_ref_id) 26 | ip = ns_stack_ref.require_output('lb_ingress').apply(extract_ip_address) 27 | 28 | config = pulumi.Config('kic-helm') 29 | fqdn = config.require('fqdn') 30 | 31 | # 32 | # Split our hostname off the domain name to build the DNS records 33 | # 34 | hostname, domainname = fqdn.split('.',1) 35 | 36 | ingress_domain = docean.Domain.get(resource_name='ingress-domain', id=domainname, name=domainname) 37 | ingress_a_record = docean.DnsRecord(resource_name='ingress-a-record', 38 | name=hostname, 39 | domain=ingress_domain.id, 40 | type="A", 41 | ttl=1800, 42 | value=ip) 43 | 44 | pulumi.export('ingress_domain', ingress_domain) 45 | pulumi.export('ingress_a_record', ingress_a_record) 46 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/domk8s/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: do-k8s 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new Digital Ocean K8 cluster 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/digitalocean/domk8s/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | from pulumi_digitalocean import KubernetesCluster, KubernetesClusterNodePoolArgs 5 | 6 | from kic_util import pulumi_config 7 | # Configuration details for the K8 cluster 8 | config = pulumi.Config('docean') 9 | instance_size = config.get('instance_size') 10 | if not instance_size: 11 | instance_size = 's-4vcpu-8gb' 12 | region = config.get('region') 13 | if not region: 14 | region = 'sfo3' 15 | node_count = config.get_int('node_count') 16 | if not node_count: 17 | node_count = 3 18 | k8s_version = config.get('k8s_version') 19 | if not k8s_version: 20 | k8s_version = '1.22.8-do.1' 21 | 22 | stack_name = pulumi.get_stack() 23 | project_name = pulumi.get_project() 24 | pulumi_user = pulumi_config.get_pulumi_user() 25 | 26 | 27 | def container_registry_project_name(): 28 | script_dir = os.path.dirname(os.path.abspath(__file__)) 29 | project_path = os.path.join(script_dir, '..', 'container-registry') 30 | return pulumi_config.get_pulumi_project_name(project_path) 31 | 32 | 33 | # Derive our names for the cluster and the pool 34 | resource_name = f'do-{stack_name}-cluster' 35 | pool_name = f'do-{stack_name}-pool' 36 | 37 | # Create a digital ocean cluster 38 | cluster = KubernetesCluster(resource_name=resource_name, 39 | region=region, 40 | version=k8s_version, 41 | node_pool=KubernetesClusterNodePoolArgs( 42 | name=pool_name, 43 | size=instance_size, 44 | node_count=node_count 45 | )) 46 | 47 | kubeconfig = cluster.kube_configs[0].raw_config 48 | 49 | # Export the clusters' kubeconfig 50 | pulumi.export("cluster_name", cluster.name) 51 | pulumi.export("cluster_id", cluster.id) 52 | pulumi.export("kubeconfig", pulumi.Output.unsecret(kubeconfig)) 53 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/kubeconfig/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: kubeconfig 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Use an existing kubeconfig for deployment 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/kubeconfig/__main__.py: -------------------------------------------------------------------------------- 1 | import pulumi 2 | import os 3 | import base64 4 | from kic_util import pulumi_config 5 | 6 | config = pulumi.Config('kubernetes') 7 | 8 | 9 | # Determine directory path 10 | def project_name_from_project_dir(dirname1: str, dirname2: str): 11 | script_dir = os.path.dirname(os.path.abspath(__file__)) 12 | project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', dirname1, dirname2) 13 | return pulumi_config.get_pulumi_project_name(project_path) 14 | 15 | 16 | def get_kubeconfig(): 17 | decoded = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(base64.b64decode(c), 'utf-8')) 18 | kubeconfig = pulumi.Output.secret(decoded) 19 | return kubeconfig 20 | 21 | # 22 | # There are several paths currently available; if the user has requested that we stand up MARA on AWS 23 | # we pursue one route, if they have selected DO we pursue another, a third for Linode, and then a fourth 24 | # for standard kubeconfig. 25 | # 26 | # The difference is in where the information about the cluster is pulled from; if AWS is chosen the 27 | # data is pulled from the ../aws/eks directory. If Kubeconfig is chosen the information is pulled from 28 | # the configuration file under /config/pulumi. 29 | # 30 | # In both cases, this project is used to reference the kubernetes cluster. That is, this project exports 31 | # the variables used for cluster connection regardless of where it pulls them from (AWS project or kubeconfig). 32 | 33 | 34 | infra_type = config.require('infra_type') 35 | if infra_type == 'AWS': 36 | stack_name = pulumi.get_stack() 37 | project_name = pulumi.get_project() 38 | pulumi_user = pulumi_config.get_pulumi_user() 39 | k8_project_name = project_name_from_project_dir('aws', 'eks') 40 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 41 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 42 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 43 | cluster_name = k8_stack_ref.require_output('cluster_name').apply(lambda c: str(c)) 44 | # 45 | # Export the clusters' kubeconfig 46 | # 47 | pulumi.export("cluster_name", cluster_name) 48 | pulumi.export("kubeconfig", kubeconfig) 49 | 50 | elif infra_type == 'DO': 51 | stack_name = pulumi.get_stack() 52 | project_name = pulumi.get_project() 53 | pulumi_user = pulumi_config.get_pulumi_user() 54 | k8_project_name = project_name_from_project_dir('digitalocean', 'domk8s') 55 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 56 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 57 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 58 | cluster_name = k8_stack_ref.require_output('cluster_name').apply(lambda c: str(c)) 59 | cluster_id = k8_stack_ref.require_output('cluster_id').apply(lambda c: str(c)) 60 | # 61 | # Export the clusters' kubeconfig 62 | # 63 | pulumi.export("cluster_name", cluster_name) 64 | pulumi.export("kubeconfig", kubeconfig) 65 | pulumi.export("cluster_id", cluster_id) 66 | elif infra_type == 'LKE': 67 | stack_name = pulumi.get_stack() 68 | project_name = pulumi.get_project() 69 | pulumi_user = pulumi_config.get_pulumi_user() 70 | k8_project_name = project_name_from_project_dir('linode', 'lke') 71 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 72 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 73 | cluster_name = k8_stack_ref.require_output('cluster_name').apply(lambda c: str(c)) 74 | cluster_id = k8_stack_ref.require_output('cluster_id').apply(lambda c: str(c)) 75 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(base64.b64decode(c), 'utf-8')) 76 | # 77 | # Export the clusters' kubeconfig 78 | # 79 | pulumi.export("cluster_name", cluster_name) 80 | pulumi.export("kubeconfig", pulumi.Output.unsecret(kubeconfig)) 81 | pulumi.export("cluster_id", cluster_id) 82 | else: 83 | # 84 | # Get the cluster name and the config 85 | # 86 | cluster_name = config.require('cluster_name') 87 | kubeconfig = config.require('kubeconfig') 88 | # 89 | # Export the clusters' kubeconfig 90 | # 91 | pulumi.export("cluster_name", cluster_name) 92 | pulumi.export("kubeconfig", kubeconfig) 93 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/container-registry-credentials/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: container-registry-credentials 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Adds container registry login credentials to the k8s cluster 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/container-registry-credentials/__main__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import base64 4 | from typing import List 5 | 6 | import pulumi 7 | from pulumi import StackReference 8 | from kic_util import pulumi_config 9 | import pulumi_kubernetes as k8s 10 | from pulumi_kubernetes.core.v1 import Secret, SecretInitArgs 11 | 12 | 13 | stack_name = pulumi.get_stack() 14 | project_name = pulumi.get_project() 15 | pulumi_user = pulumi_config.get_pulumi_user() 16 | script_dir = os.path.dirname(os.path.abspath(__file__)) 17 | 18 | 19 | def project_name_from_kubeconfig(): 20 | project_path = os.path.join(script_dir, '..', '..', 'kubeconfig') 21 | return pulumi_config.get_pulumi_project_name(project_path) 22 | 23 | 24 | def project_name_from_same_parent(directory: str): 25 | project_path = os.path.join(script_dir, '..', directory) 26 | return pulumi_config.get_pulumi_project_name(project_path) 27 | 28 | 29 | def project_name_of_namespace_project(): 30 | project_path = os.path.join(script_dir, '..', '..', '..', 'kubernetes', 'nginx', 'ingress-controller-namespace') 31 | return pulumi_config.get_pulumi_project_name(project_path) 32 | 33 | 34 | k8_project_name = project_name_from_kubeconfig() 35 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 36 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 37 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 38 | 39 | container_registry_stack_ref_id = f"{pulumi_user}/{project_name_from_same_parent('harbor')}/{stack_name}" 40 | harbor_stack_ref = StackReference(container_registry_stack_ref_id) 41 | harbor_hostname_output = harbor_stack_ref.require_output('harbor_hostname') 42 | harbor_user_output = harbor_stack_ref.require_output('harbor_user') 43 | harbor_password_output = harbor_stack_ref.require_output('harbor_password') 44 | 45 | namespace_stack_ref_id = f"{pulumi_user}/{project_name_of_namespace_project()}/{stack_name}" 46 | ns_stack_ref = StackReference(namespace_stack_ref_id) 47 | namespace_name_output = ns_stack_ref.require_output('ingress_namespace_name') 48 | 49 | 50 | def build_docker_credentials(params: List[str]): 51 | registry_host = params[0] 52 | username = params[1] 53 | password = params[2] 54 | auth_string = f'{username}:{password}' 55 | auth_base64 = str(base64.encodebytes(auth_string.encode('ascii')), 'ascii') 56 | 57 | data = { 58 | 'auths': { 59 | registry_host: { 60 | 'auth': auth_base64 61 | } 62 | } 63 | } 64 | 65 | return json.dumps(data) 66 | 67 | 68 | docker_credentials = pulumi.Output.all(harbor_hostname_output, 69 | harbor_user_output, 70 | harbor_password_output).apply(build_docker_credentials) 71 | 72 | k8s_provider = k8s.Provider(resource_name='kubernetes', kubeconfig=kubeconfig) 73 | 74 | secret = Secret(resource_name='ingress-controller-registry-secret', 75 | args=SecretInitArgs(string_data={'.dockerconfigjson': docker_credentials}, 76 | type='kubernetes.io/dockerconfigjson', 77 | metadata={'namespace': namespace_name_output, 78 | 'name': 'ingress-controller-registry'}), 79 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 80 | 81 | pulumi.export('ingress-controller-registry-secret', secret) 82 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/harbor-configuration/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: harbor-configuration 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Configures Harbor Container Registry 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/harbor-configuration/__main__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import urllib.request 4 | import urllib.error 5 | import os 6 | import time 7 | from typing import List 8 | 9 | import pulumi 10 | from kic_util import pulumi_config 11 | 12 | stack_name = pulumi.get_stack() 13 | project_name = pulumi.get_project() 14 | pulumi_user = pulumi_config.get_pulumi_user() 15 | 16 | 17 | def project_name_from_harbor_dir(): 18 | script_dir = os.path.dirname(os.path.abspath(__file__)) 19 | project_path = os.path.join(script_dir, '..', 'harbor') 20 | return pulumi_config.get_pulumi_project_name(project_path) 21 | 22 | 23 | harbor_project_name = project_name_from_harbor_dir() 24 | stack_ref_id = f"{pulumi_user}/{harbor_project_name}/{stack_name}" 25 | stack_ref = pulumi.StackReference(stack_ref_id) 26 | harbor_hostname_output = stack_ref.require_output('harbor_hostname') 27 | harbor_user_output = stack_ref.require_output('harbor_user') 28 | harbor_password_output = stack_ref.require_output('harbor_password') 29 | 30 | 31 | def configure_harbor(params: List[str]) -> bool: 32 | hostname = params[0] 33 | user = params[1] 34 | password = params[2] 35 | base_url = f'https://{hostname}/api/v2.0' 36 | base64creds = str(base64.b64encode(f'{user}:{password}'.encode('ascii')), 'ascii') 37 | max_retries = 12 38 | retries = 0 39 | timeout = 1000 40 | 41 | def is_harbor_is_up() -> bool: 42 | url = f'{base_url}/health' 43 | request = urllib.request.Request(url=url, method='GET') 44 | request.add_header(key='Authorization', val=f'Basic {base64creds}') 45 | 46 | try: 47 | with urllib.request.urlopen(url=request, timeout=timeout) as context: 48 | if context.getcode() != 200: 49 | return False 50 | 51 | health_check = json.load(context) 52 | components = health_check['components'] 53 | for component in components: 54 | if component['status'] != 'healthy': 55 | pulumi.log.info(f"Harbor component [{component['name']}] is not healthy") 56 | return False 57 | 58 | return True 59 | except urllib.error.URLError as e: 60 | # Don't retry for name resolution failures 61 | if e.errno == -3: 62 | raise e 63 | 64 | pulumi.log.info(f'Unable to connect to Harbor [try {retries+1} of {max_retries}]: {e}') 65 | return False 66 | 67 | def modify_default_project_registry(): 68 | url = f'{base_url}/projects/library/metadatas/public' 69 | request = urllib.request.Request(url=url, method='PUT') 70 | request.add_header(key='Authorization', val=f'Basic {base64creds}') 71 | request.add_header(key='Content-Type', val='application/json') 72 | body = { 73 | 'public': 'false' 74 | } 75 | body_json = json.dumps(body) 76 | request.data = body_json.encode('utf-8') 77 | urllib.request.urlopen(url=request, timeout=timeout) 78 | 79 | while not is_harbor_is_up(): 80 | retries += 1 81 | timeout = 1000 * retries 82 | time.sleep(timeout) 83 | 84 | if retries >= max_retries: 85 | raise f'Harbor has not come up after {retries} retries' 86 | 87 | pulumi.log.info('Harbor is up, modifying default registry') 88 | modify_default_project_registry() 89 | 90 | return True 91 | 92 | 93 | harbor_is_alive = pulumi.Output.all(harbor_hostname_output, harbor_user_output, harbor_password_output)\ 94 | .apply(configure_harbor) 95 | 96 | pulumi.export('harbor_is_alive', harbor_is_alive) 97 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/harbor/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: harbor 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new Harbor Container Registry 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/lke/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: lke 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates new Linode LKE cluster 8 | -------------------------------------------------------------------------------- /pulumi/python/infrastructure/linode/lke/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pulumi 3 | import pulumi_linode as linode 4 | 5 | # Configuration details for the K8 cluster 6 | config = pulumi.Config('linode') 7 | 8 | api_token = config.get('token') or \ 9 | config.get_secret('token') or \ 10 | os.getenv('LINODE_TOKEN') or \ 11 | os.getenv('LINODE_CLI_TOKEN') 12 | 13 | # For whatever reason, the Linode provider does not pickup the token from the 14 | # stack configuration nor from the environment variables, so we do that work 15 | # here. 16 | provider = linode.Provider(resource_name='linode_provider', token=api_token) 17 | 18 | instance_type = config.require('instance_type') 19 | region = config.require('region') 20 | node_count = config.require_int('node_count') 21 | k8s_version = config.require('k8s_version') 22 | k8s_ha = config.require_bool('k8s_ha') 23 | 24 | stack = pulumi.get_stack() 25 | resource_name = f'lke-{stack}-cluster' 26 | 27 | # Create a linode cluster 28 | cluster = linode.LkeCluster(resource_name=resource_name, 29 | k8s_version=k8s_version, 30 | control_plane=linode.LkeClusterControlPlaneArgs( 31 | high_availability=k8s_ha), 32 | label=f'MARA [{stack}]', 33 | pools=[linode.LkeClusterPoolArgs( 34 | count=node_count, 35 | type=instance_type, 36 | )], 37 | region=region, 38 | tags=["mara"], 39 | opts=pulumi.ResourceOptions(provider=provider)) 40 | 41 | # Export the clusters' kubeconfig 42 | pulumi.export("cluster_name", resource_name) 43 | pulumi.export("cluster_id", cluster.id) 44 | pulumi.export("kubeconfig", cluster.kubeconfig) 45 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/pulumi/python/kubernetes` 4 | 5 | ## Purpose 6 | 7 | All kubernetes deployments are stored in this directory; all of these stacks 8 | will use the [`infrastructure/kubeconfig`](../infrastructure/kubeconfig) stack as 9 | a source of information about the kubernetes installation that is being used. 10 | 11 | ## Key Files 12 | 13 | * [`nginx`](./nginx) NGINX related components; Ingress Controller, Service 14 | Mesh, App Protect, etc. Each in a separate 15 | directory. 16 | * [`applications`](./applications) Applications; each in it's own directory. 17 | 18 | ## Notes 19 | 20 | None. 21 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/applications/sirius/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: sirius-deploy 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates the Bank of Sirius App 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/applications/sirius/cert/self-sign.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1 3 | kind: ClusterIssuer 4 | metadata: 5 | name: selfsigned-issuer 6 | spec: 7 | selfSigned: { } 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/applications/sirius/verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | import sys 5 | import json 6 | 7 | import urllib3 8 | 9 | stdin_json = json.load(sys.stdin) 10 | if 'application_url' not in stdin_json: 11 | raise ValueError( 12 | "Missing expected key 'application_url' in STDIN json data") 13 | 14 | url = f"{stdin_json['application_url']}/login" 15 | 16 | payload = 'username=testuser&password=password' 17 | headers = { 18 | 'Content-Type': 'application/x-www-form-urlencoded' 19 | } 20 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 21 | response = requests.request( 22 | "POST", url, headers=headers, data=payload, verify=False) 23 | response_code = response.status_code 24 | 25 | if response_code != 200: 26 | print( 27 | f'Application failed health check [url={url},response_code={response_code}', file=sys.stderr) 28 | sys.exit(1) 29 | else: 30 | print('Application passed health check', file=sys.stderr) 31 | print(stdin_json['application_url']) 32 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/certmgr/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: certmgr 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Sets up cert-manager.io 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/certmgr/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_kubernetes as k8s 5 | from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs 6 | from pulumi_kubernetes.yaml import ConfigFile 7 | 8 | from kic_util import pulumi_config 9 | 10 | 11 | def project_name_from_project_dir(dirname: str): 12 | script_dir = os.path.dirname(os.path.abspath(__file__)) 13 | project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', dirname) 14 | return pulumi_config.get_pulumi_project_name(project_path) 15 | 16 | 17 | def add_namespace(obj): 18 | obj['metadata']['namespace'] = 'cert-manager' 19 | 20 | 21 | stack_name = pulumi.get_stack() 22 | project_name = pulumi.get_project() 23 | pulumi_user = pulumi_config.get_pulumi_user() 24 | 25 | k8_project_name = project_name_from_project_dir('kubeconfig') 26 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 27 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 28 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 29 | 30 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', 31 | kubeconfig=kubeconfig) 32 | 33 | ns = k8s.core.v1.Namespace(resource_name='cert-manager', 34 | metadata={'name': 'cert-manager'}, 35 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 36 | 37 | config = pulumi.Config('certmgr') 38 | chart_name = config.get('chart_name') 39 | if not chart_name: 40 | chart_name = 'cert-manager' 41 | chart_version = config.get('chart_version') 42 | if not chart_version: 43 | chart_version = 'v1.10.0' 44 | helm_repo_name = config.get('certmgr_helm_repo_name') 45 | if not helm_repo_name: 46 | helm_repo_name = 'jetstack' 47 | 48 | helm_repo_url = config.get('certmgr_helm_repo_url') 49 | if not helm_repo_url: 50 | helm_repo_url = 'https://charts.jetstack.io' 51 | 52 | # 53 | # Allow the user to set timeout per helm chart; otherwise 54 | # we default to 5 minutes. 55 | # 56 | helm_timeout = config.get_int('helm_timeout') 57 | if not helm_timeout: 58 | helm_timeout = 300 59 | 60 | certmgr_release_args = ReleaseArgs( 61 | chart=chart_name, 62 | repository_opts=RepositoryOptsArgs( 63 | repo=helm_repo_url 64 | ), 65 | version=chart_version, 66 | namespace=ns.metadata.name, 67 | values={ 68 | "installCRDs": "True" 69 | }, 70 | # Configure the timeout value. 71 | timeout=helm_timeout, 72 | # By default Release resource will wait till all created resources 73 | # are available. Set this to true to skip waiting on resources being 74 | # available. 75 | skip_await=False, 76 | # If we fail, clean up 77 | cleanup_on_fail=True, 78 | # Provide a name for our release 79 | name="certmgr", 80 | # Lint the chart before installing 81 | lint=True, 82 | # Force update if required 83 | force_update=True) 84 | 85 | certmgr_release = Release("certmgr", args=certmgr_release_args, opts=pulumi.ResourceOptions(depends_on=ns)) 86 | 87 | status = certmgr_release.status 88 | pulumi.export("certmgr_status", status) 89 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/logagent/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: logagent 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Deploys a log agent 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/logagent/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pulumi import Output 3 | 4 | import pulumi 5 | import pulumi_kubernetes as k8s 6 | from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs 7 | 8 | from kic_util import pulumi_config 9 | 10 | config = pulumi.Config('logagent') 11 | chart_name = config.get('chart_name') 12 | if not chart_name: 13 | chart_name = 'filebeat' 14 | chart_version = config.get('chart_version') 15 | if not chart_version: 16 | chart_version = '7.17.3' 17 | helm_repo_name = config.get('helm_repo_name') 18 | if not helm_repo_name: 19 | helm_repo_name = 'elastic' 20 | helm_repo_url = config.get('helm_repo_url') 21 | if not helm_repo_url: 22 | helm_repo_url = 'https://helm.elastic.co' 23 | 24 | # 25 | # Allow the user to set timeout per helm chart; otherwise 26 | # we default to 5 minutes. 27 | # 28 | helm_timeout = config.get_int('helm_timeout') 29 | if not helm_timeout: 30 | helm_timeout = 300 31 | 32 | 33 | def project_name_from_project_dir(dirname: str): 34 | script_dir = os.path.dirname(os.path.abspath(__file__)) 35 | project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', dirname) 36 | return pulumi_config.get_pulumi_project_name(project_path) 37 | 38 | 39 | def pulumi_logstore_project_name(): 40 | script_dir = os.path.dirname(os.path.abspath(__file__)) 41 | logstore_project_path = os.path.join(script_dir, '..', 'logstore') 42 | return pulumi_config.get_pulumi_project_name(logstore_project_path) 43 | 44 | 45 | stack_name = pulumi.get_stack() 46 | project_name = pulumi.get_project() 47 | pulumi_user = pulumi_config.get_pulumi_user() 48 | 49 | k8_project_name = project_name_from_project_dir('kubeconfig') 50 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 51 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 52 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 53 | 54 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', 55 | kubeconfig=kubeconfig) 56 | 57 | ns = k8s.core.v1.Namespace(resource_name='logagent', 58 | metadata={'name': 'logagent'}, 59 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 60 | 61 | # Logic to extract the FQDN of logstore 62 | logstore_project_name = pulumi_logstore_project_name() 63 | logstore_stack_ref_id = f"{pulumi_user}/{logstore_project_name}/{stack_name}" 64 | logstore_stack_ref = pulumi.StackReference(logstore_stack_ref_id) 65 | elastic_hostname = logstore_stack_ref.get_output('elastic_hostname') 66 | kibana_hostname = logstore_stack_ref.get_output('kibana_hostname') 67 | 68 | filebeat_yaml = Output.concat("setup.kibana.host: 'http://", kibana_hostname, 69 | ":5601'\nsetup.dashboards.enabled: true\nfilebeat.autodiscover:\n", 70 | " providers:\n - type: kubernetes\n hints.enabled: true\n", 71 | " hints.default_config:\n type: container\n paths:\n", 72 | " - /var/lib/docker/containers/${data.kubernetes.container.id}/*.log\noutput.elasticsearch:\n", 73 | " host: '${NODE_NAME}'\n hosts: '", elastic_hostname, ":9200'\n") 74 | 75 | filebeat_release_args = ReleaseArgs( 76 | chart=chart_name, 77 | repository_opts=RepositoryOptsArgs( 78 | repo=helm_repo_url 79 | ), 80 | version=chart_version, 81 | namespace=ns.metadata.name, 82 | 83 | # Values from Chart's parameters specified hierarchically, 84 | values={ 85 | "daemonset": { 86 | "enabled": True, 87 | "filebeatConfig": { 88 | "filebeat.yml": filebeat_yaml 89 | } 90 | } 91 | }, 92 | # User configurable timeout 93 | timeout=helm_timeout, 94 | # By default Release resource will wait till all created resources 95 | # are available. Set this to true to skip waiting on resources being 96 | # available. 97 | skip_await=False, 98 | # If we fail, clean up 99 | cleanup_on_fail=True, 100 | # Provide a name for our release 101 | name="filebeat", 102 | # Lint the chart before installing 103 | lint=True, 104 | # Force update if required 105 | force_update=True) 106 | filebeat_release = Release("filebeat", args=filebeat_release_args) 107 | 108 | status = filebeat_release.status 109 | pulumi.export("logagent_status", status) 110 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/logstore/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: logstore 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Deploys a logging store 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/logstore/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_kubernetes as k8s 5 | from pulumi import Output 6 | from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs 7 | 8 | from kic_util import pulumi_config 9 | 10 | config = pulumi.Config('logstore') 11 | chart_name = config.get('chart_name') 12 | if not chart_name: 13 | chart_name = 'elasticsearch' 14 | chart_version = config.get('chart_version') 15 | if not chart_version: 16 | chart_version = '19.4.4' 17 | helm_repo_name = config.get('helm_repo_name') 18 | if not helm_repo_name: 19 | helm_repo_name = 'bitnami' 20 | helm_repo_url = config.get('helm_repo_url') 21 | if not helm_repo_url: 22 | helm_repo_url = 'https://charts.bitnami.com/bitnami' 23 | 24 | # 25 | # Allow the user to set timeout per helm chart; otherwise 26 | # we default to 5 minutes. 27 | # 28 | helm_timeout = config.get_int('helm_timeout') 29 | if not helm_timeout: 30 | helm_timeout = 300 31 | 32 | # 33 | # Define the default replicas for the Elastic components. If not set we default to one copy of each - master, ingest, 34 | # data, and coordinating. This is ideal for smaller installations - K3S, Microk8s, minikube, etc. However, it may fall 35 | # over when running with a high volume of logs. 36 | # 37 | master_replicas = config.get('master_replicas') 38 | if not master_replicas: 39 | master_replicas = 1 40 | 41 | ingest_replicas = config.get('ingest_replicas') 42 | if not ingest_replicas: 43 | ingest_replicas = 1 44 | 45 | data_replicas = config.get('data_replicas') 46 | if not data_replicas: 47 | data_replicas = 1 48 | 49 | coordinating_replicas = config.get('coordinating_replicas') 50 | if not coordinating_replicas: 51 | coordinating_replicas = 1 52 | 53 | 54 | def project_name_from_project_dir(dirname: str): 55 | script_dir = os.path.dirname(os.path.abspath(__file__)) 56 | project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', dirname) 57 | return pulumi_config.get_pulumi_project_name(project_path) 58 | 59 | 60 | stack_name = pulumi.get_stack() 61 | project_name = pulumi.get_project() 62 | pulumi_user = pulumi_config.get_pulumi_user() 63 | 64 | k8_project_name = project_name_from_project_dir('kubeconfig') 65 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 66 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 67 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 68 | 69 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', 70 | kubeconfig=kubeconfig) 71 | 72 | ns = k8s.core.v1.Namespace(resource_name='logstore', 73 | metadata={'name': 'logstore'}, 74 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 75 | 76 | elastic_release_args = ReleaseArgs( 77 | chart=chart_name, 78 | repository_opts=RepositoryOptsArgs( 79 | repo=helm_repo_url 80 | ), 81 | version=chart_version, 82 | namespace=ns.metadata.name, 83 | 84 | # Values from Chart's parameters specified hierarchically, 85 | values={ 86 | "master": { 87 | "replicas": master_replicas, 88 | "resources": { 89 | "requests": {}, 90 | "limits": {} 91 | }, 92 | }, 93 | "coordinating": { 94 | "replicas": coordinating_replicas 95 | }, 96 | "data": { 97 | "replicas": data_replicas, 98 | "resources": { 99 | "requests": {}, 100 | "limits": {} 101 | }, 102 | }, 103 | "global": { 104 | "kibanaEnabled": True 105 | }, 106 | "ingest": { 107 | "enabled": True, 108 | "replicas": ingest_replicas, 109 | "resources": { 110 | "requests": {}, 111 | "limits": {} 112 | }, 113 | } 114 | }, 115 | # User configurable timeout 116 | timeout=helm_timeout, 117 | # By default, Release resource will wait till all created resources 118 | # are available. Set this to true to skip waiting on resources being 119 | # available. 120 | skip_await=False, 121 | # If we fail, clean up 122 | cleanup_on_fail=True, 123 | # Provide a name for our release 124 | name="elastic", 125 | # Lint the chart before installing 126 | lint=True, 127 | # Force update if required 128 | force_update=True) 129 | 130 | elastic_release = Release("elastic", args=elastic_release_args) 131 | 132 | elastic_rname = elastic_release.status.name 133 | 134 | elastic_fqdn = Output.concat(elastic_rname, "-elasticsearch.logstore.svc.cluster.local") 135 | kibana_fqdn = Output.concat(elastic_rname, "-kibana.logstore.svc.cluster.local") 136 | 137 | pulumi.export('elastic_hostname', pulumi.Output.unsecret(elastic_fqdn)) 138 | pulumi.export('kibana_hostname', pulumi.Output.unsecret(kibana_fqdn)) 139 | 140 | # Print out our status 141 | estatus = elastic_release.status 142 | pulumi.export("logstat_status", estatus) 143 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/nginx/ingress-controller-namespace/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: ingress-controller-namespace 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Creates the NGINX Kubernetes Ingress Controller Namespace 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/nginx/ingress-controller-namespace/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_kubernetes as k8s 5 | 6 | from kic_util import pulumi_config 7 | 8 | 9 | def infrastructure_project_name_from_project_dir(dirname: str): 10 | script_dir = os.path.dirname(os.path.abspath(__file__)) 11 | project_path = os.path.join(script_dir, '..', '..', '..', 'infrastructure', dirname) 12 | return pulumi_config.get_pulumi_project_name(project_path) 13 | 14 | 15 | stack_name = pulumi.get_stack() 16 | project_name = pulumi.get_project() 17 | pulumi_user = pulumi_config.get_pulumi_user() 18 | 19 | k8_project_name = infrastructure_project_name_from_project_dir('kubeconfig') 20 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 21 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 22 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 23 | cluster_name = k8_stack_ref.require_output('cluster_name').apply(lambda c: str(c)) 24 | 25 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', 26 | kubeconfig=kubeconfig) 27 | 28 | namespace_name = 'nginx-ingress' 29 | 30 | ns = k8s.core.v1.Namespace(resource_name='nginx-ingress', 31 | metadata={'name': namespace_name, 32 | 'labels': { 33 | 'prometheus': 'scrape'} 34 | }, 35 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 36 | 37 | pulumi.export('ingress_namespace', ns) 38 | pulumi.export('ingress_namespace_name', namespace_name) 39 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/nginx/ingress-controller-repo-only/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: ingress-controller 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Sets up NGINX Kubernetes Ingress Controller using Helm -------------------------------------------------------------------------------- /pulumi/python/kubernetes/nginx/ingress-controller-repo-only/manifests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/pulumi/python/kubernetes/nginx/ingress-controller-repo-only/manifests/.gitkeep -------------------------------------------------------------------------------- /pulumi/python/kubernetes/nginx/ingress-controller/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: ingress-controller 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../../venv 6 | config: ../../../../../config/pulumi 7 | description: Sets up NGINX Kubernetes Ingress Controller using Helm 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: observability 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Deploys OTEL 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_kubernetes as k8s 5 | from pulumi_kubernetes.yaml import ConfigGroup 6 | 7 | from kic_util import pulumi_config 8 | 9 | 10 | # Removes the status field from the Nginx Ingress Helm Chart, so that i#t is 11 | # compatible with the Pulumi Chart implementation. 12 | def remove_status_field(obj): 13 | if obj['kind'] == 'CustomResourceDefinition' and 'status' in obj: 14 | del obj['status'] 15 | 16 | 17 | def pulumi_k8_project_name(): 18 | script_dir = os.path.dirname(os.path.abspath(__file__)) 19 | eks_project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', 'kubeconfig') 20 | return pulumi_config.get_pulumi_project_name(eks_project_path) 21 | 22 | 23 | def otel_operator_location(): 24 | script_dir = os.path.dirname(os.path.abspath(__file__)) 25 | otel_operator_path = os.path.join(script_dir, 'otel-operator', '*.yaml') 26 | return otel_operator_path 27 | 28 | 29 | def otel_deployment_location(): 30 | script_dir = os.path.dirname(os.path.abspath(__file__)) 31 | otel_deployment_path = os.path.join(script_dir, 'otel-objects', '*.yaml') 32 | return otel_deployment_path 33 | 34 | 35 | def add_namespace(obj): 36 | obj['metadata']['namespace'] = 'observability' 37 | 38 | 39 | stack_name = pulumi.get_stack() 40 | project_name = pulumi.get_project() 41 | k8_project_name = pulumi_k8_project_name() 42 | pulumi_user = pulumi_config.get_pulumi_user() 43 | 44 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 45 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 46 | kubeconfig = k8_stack_ref.get_output('kubeconfig').apply(lambda c: str(c)) 47 | k8_stack_ref.get_output('cluster_name').apply( 48 | lambda s: pulumi.log.info(f'Cluster name: {s}')) 49 | 50 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', kubeconfig=kubeconfig) 51 | 52 | # Create the namespace 53 | ns = k8s.core.v1.Namespace(resource_name='observability', 54 | metadata={'name': 'observability'}, 55 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 56 | 57 | # Config Manifests: OTEL operator 58 | otel_operator = otel_operator_location() 59 | 60 | otel_op = ConfigGroup( 61 | 'otel-op', 62 | files=[otel_operator], 63 | transformations=[remove_status_field], # Need to review w/ operator 64 | opts=pulumi.ResourceOptions(depends_on=[ns]) 65 | ) 66 | 67 | # Config Manifests: OTEL components 68 | otel_deployment = otel_deployment_location() 69 | 70 | otel_dep = ConfigGroup( 71 | 'otel-dep', 72 | files=[otel_deployment], 73 | transformations=[add_namespace, remove_status_field], # Need to review w/ operator 74 | opts=pulumi.ResourceOptions(depends_on=[ns, otel_op]) 75 | ) 76 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/README.md: -------------------------------------------------------------------------------- 1 | # Sample Configurations 2 | 3 | This directory contains a number of sample configurations that can be used with 4 | the 5 | [OTEL kubernetes operator](https://github.com/open-telemetry/opentelemetry-operator) 6 | that is installed as part of the MARA project. 7 | 8 | Each configuration currently uses the `simplest` deployment, which uses an 9 | in-memory store for data being processed. This is obviously not suited to a 10 | production deployment, but it is intended to illustrate the steps required to 11 | work with the OTEL deployment. 12 | 13 | ## Commonality 14 | 15 | ### Listening Ports 16 | 17 | Each of the sample files is configured to listen on the 18 | [OTLP protocol](https://opentelemetry.io/docs/reference/specification/protocol/otlp/) 19 | . The listen ports configured are: 20 | 21 | * grpc on port 9978 22 | * http on port 9979 23 | 24 | ### Logging 25 | 26 | All the examples log to the container's stdout. However, the basic configuration 27 | is configured to only show the condensed version of the traces being received. 28 | In order to see the full traces, you need to set the logging level to 29 | `DEBUG`. The basic-debug object is configured to do this automatically. 30 | 31 | ## Configurations 32 | 33 | ### `otel-collector.yaml.basic` 34 | 35 | This is the default collector that only listens and logs summary spans to the 36 | container's stdout. 37 | 38 | ### `otel-collector.yaml.full` 39 | 40 | This is a more complex variant that contains multiple receivers, processors, 41 | and exporters. Please see the file for details. 42 | 43 | ### `otel-collector.yaml.lightstep` 44 | 45 | This configuration file deploys lightstep as an ingester. Please note you will 46 | need to have a [lightstep](https://lightstep.com/) account to use this option, 47 | and you will need to add your lightstep access token to the file in the field 48 | noted. 49 | 50 | ## Usage 51 | 52 | By default, the `otel-collector.yaml.basic` configuration is copied into the 53 | live `otel-collector.yaml`. The logic for this project runs all files ending in 54 | `.yaml` as part of the configuration, so you simply need to either rename your 55 | chosen file to `otel-collector.yaml` or add ensuring only the files you want to 56 | use have the `.yaml` extension. 57 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | endpoint: 0.0.0.0:9978 13 | http: 14 | endpoint: 0.0.0.0:9979 15 | 16 | processors: 17 | batch: 18 | 19 | exporters: 20 | logging: 21 | logLevel: 22 | 23 | service: 24 | pipelines: 25 | traces: 26 | receivers: [otlp] 27 | processors: [batch] 28 | exporters: [logging] 29 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml.basic: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | endpoint: 0.0.0.0:9978 13 | http: 14 | endpoint: 0.0.0.0:9979 15 | 16 | processors: 17 | batch: 18 | 19 | exporters: 20 | logging: 21 | logLevel: 22 | 23 | service: 24 | pipelines: 25 | traces: 26 | receivers: [otlp] 27 | processors: [batch] 28 | exporters: [logging] 29 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml.basic-debug: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | endpoint: 0.0.0.0:9978 13 | http: 14 | endpoint: 0.0.0.0:9979 15 | 16 | processors: 17 | batch: 18 | 19 | exporters: 20 | logging: 21 | logLevel: debug 22 | 23 | service: 24 | pipelines: 25 | traces: 26 | receivers: [otlp] 27 | processors: [batch] 28 | exporters: [logging] 29 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml.full: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | extensions: 9 | health_check: 10 | pprof: 11 | endpoint: 0.0.0.0:1777 12 | zpages: 13 | endpoint: 0.0.0.0:55679 14 | 15 | receivers: 16 | otlp: 17 | protocols: 18 | grpc: 19 | endpoint: 0.0.0.0:9978 20 | http: 21 | endpoint: 0.0.0.0:9979 22 | opencensus: 23 | jaeger: 24 | protocols: 25 | grpc: 26 | thrift_binary: 27 | thrift_compact: 28 | thrift_http: 29 | zipkin: 30 | 31 | # Collect own metrics 32 | prometheus: 33 | config: 34 | scrape_configs: 35 | - job_name: 'otel-collector' 36 | scrape_interval: 120s 37 | static_configs: 38 | - targets: [ '0.0.0.0:8080'] 39 | metrics_path: '/z/prometheus' 40 | 41 | processors: 42 | batch: 43 | 44 | exporters: 45 | prometheus: 46 | endpoint: "0.0.0.0:8889" 47 | 48 | logging: 49 | logLevel: debug 50 | 51 | jaeger: 52 | endpoint: "0.0.0.0:14250" 53 | 54 | otlp: 55 | endpoint: ingest.lightstep.com:443 56 | headers: {"lightstep-access-token":""} 57 | 58 | service: 59 | pipelines: 60 | traces: 61 | receivers: [otlp, opencensus, jaeger, zipkin] 62 | processors: [batch] 63 | exporters: [logging, jaeger, otlp] 64 | metrics: 65 | receivers: [otlp, opencensus, prometheus] 66 | processors: [batch] 67 | exporters: [logging] 68 | 69 | extensions: [health_check, pprof, zpages] 70 | 71 | 72 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml.lightstep: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | endpoint: 0.0.0.0:9978 13 | http: 14 | endpoint: 0.0.0.0:9979 15 | 16 | exporters: 17 | logging: 18 | otlp: 19 | endpoint: ingest.lightstep.com:443 20 | headers: 21 | "lightstep-access-token":"YOURTOKEN" 22 | 23 | processors: 24 | batch: 25 | 26 | service: 27 | pipelines: 28 | traces: 29 | receivers: [otlp] 30 | processors: [batch] 31 | exporters: [logging, otlp] 32 | metrics: 33 | receivers: [otlp] 34 | processors: [batch] 35 | exporters: [logging, otlp] -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-objects/otel-collector.yaml.with-prom: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: OpenTelemetryCollector 3 | metadata: 4 | name: simplest 5 | namespace: observability 6 | spec: 7 | config: | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | endpoint: 0.0.0.0:9978 13 | http: 14 | endpoint: 0.0.0.0:9979 15 | # Collect Prometheus Metrics 16 | prometheus: 17 | config: 18 | scrape_configs: 19 | - job_name: 'federate' 20 | scrape_interval: 15s 21 | 22 | honor_labels: false 23 | metrics_path: '/federate' 24 | 25 | params: 26 | 'match[]': 27 | - '{job=~".+"}' 28 | static_configs: 29 | - targets: 30 | - 'prometheus-kube-prometheus-prometheus.prometheus:9090' 31 | exporters: 32 | otlp: 33 | endpoint: https://ingest.lightstep.com:443 34 | headers: {"lightstep-service-name":"my-service","lightstep-access-token":"XXXX"} 35 | processors: 36 | batch: 37 | service: 38 | pipelines: 39 | traces: 40 | receivers: [otlp] 41 | processors: [batch] 42 | exporters: [otlp] 43 | metrics: 44 | receivers: [otlp,prometheus] 45 | processors: [batch] 46 | exporters: [otlp] 47 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/observability/otel-operator/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/pulumi/python/kubernetes/observablity/otel-operator` 4 | 5 | ## Purpose 6 | 7 | Deploys the OpenTelemetry Operator via a YAML manifest. 8 | 9 | ## Key Files 10 | 11 | * [`opentelemetry-operator.yaml`](./opentelemetry-operator.yaml) This file is 12 | used by the Pulumi code in the directory above to deploy the OTEL operator. 13 | Note that this file is pulled from the 14 | [OpenTelemetry Operator](https://opentelemetry.io/docs/k8s-operator/) install 15 | documentation. It is included as a static resource in order to manage the 16 | version within MARA. 17 | 18 | ## Notes 19 | 20 | The OTEL operator had dependencies on [cert-manager](../../certmgr) 21 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/prometheus/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: prometheus 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Deploys Prometheus 8 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/prometheus/extras/README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | This directory contains a manifest that can be used to change the metrics 4 | bind port for the kube-proxy from 127.0.0.1 to 0.0.0.0 in order to allow the 5 | metrics to be scraped by the prometheus service. 6 | 7 | This is not being automatically applied, since it is changing the bind address 8 | that is being used for the metrics port. That said, this should be secure 9 | since it is internal to the installation and the connection is done via HTTPS. 10 | 11 | However, please see this 12 | 13 | [github issue](https://github.com/prometheus-community/helm-charts/issues/977) 14 | for the full discussion of why this is required. 15 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/prometheus/extras/kube-proxy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This version of the kube proxy configuration is required to change the 3 | # bind address for metrics from 127.0.0.1 to 0.0.0.0. See: 4 | # https://github.com/prometheus-community/helm-charts/issues/977 5 | # for details as why this is required. 6 | # 7 | # Note this is a hack, and as such should be tested with any version 8 | # changes to Kubernetes. You have been warned. 9 | # 10 | apiVersion: v1 11 | data: 12 | config: |- 13 | apiVersion: kubeproxy.config.k8s.io/v1alpha1 14 | bindAddress: 0.0.0.0 15 | clientConnection: 16 | acceptContentTypes: "" 17 | burst: 10 18 | contentType: application/vnd.kubernetes.protobuf 19 | kubeconfig: /var/lib/kube-proxy/kubeconfig 20 | qps: 5 21 | clusterCIDR: "" 22 | configSyncPeriod: 15m0s 23 | conntrack: 24 | maxPerCore: 32768 25 | min: 131072 26 | tcpCloseWaitTimeout: 1h0m0s 27 | tcpEstablishedTimeout: 24h0m0s 28 | enableProfiling: false 29 | healthzBindAddress: 0.0.0.0:10256 30 | hostnameOverride: "" 31 | iptables: 32 | masqueradeAll: false 33 | masqueradeBit: 14 34 | minSyncPeriod: 0s 35 | syncPeriod: 30s 36 | ipvs: 37 | excludeCIDRs: null 38 | minSyncPeriod: 0s 39 | scheduler: "" 40 | syncPeriod: 30s 41 | kind: KubeProxyConfiguration 42 | metricsBindAddress: 0.0.0.0:10249 43 | mode: "iptables" 44 | nodePortAddresses: null 45 | oomScoreAdj: -998 46 | portRange: "" 47 | udpIdleTimeout: 250ms 48 | kind: ConfigMap 49 | metadata: 50 | annotations: 51 | labels: 52 | eks.amazonaws.com/component: kube-proxy 53 | k8s-app: kube-proxy 54 | name: kube-proxy-config 55 | namespace: kube-system 56 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/prometheus/manifests/nginx-service-mon.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | # needs to match above matchLabels 7 | prometheus: nginx-monitor 8 | name: nginx-monitor 9 | namespace: nginx-ingress 10 | spec: 11 | endpoints: 12 | - interval: 15s 13 | port: prometheus 14 | scheme: http 15 | jobLabel: app.kubernetes.io/name 16 | selector: 17 | matchLabels: 18 | app: kic-nginx-ingress 19 | namespaceSelector: 20 | any: true 21 | 22 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | Pulumi.*.yaml -------------------------------------------------------------------------------- /pulumi/python/kubernetes/secrets/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: secrets 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | description: Adds Kubernetes Secrets 7 | -------------------------------------------------------------------------------- /pulumi/python/kubernetes/secrets/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pulumi 4 | import pulumi_kubernetes as k8s 5 | from pulumi_kubernetes.core.v1 import Secret, SecretInitArgs 6 | 7 | from kic_util import pulumi_config 8 | 9 | script_dir = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def project_name_from_project_dir(dirname: str): 13 | global script_dir 14 | project_path = os.path.join(script_dir, '..', '..', '..', 'python', 'infrastructure', dirname) 15 | return pulumi_config.get_pulumi_project_name(project_path) 16 | 17 | 18 | stack_name = pulumi.get_stack() 19 | project_name = pulumi.get_project() 20 | pulumi_user = pulumi_config.get_pulumi_user() 21 | 22 | k8_project_name = project_name_from_project_dir('kubeconfig') 23 | k8_stack_ref_id = f"{pulumi_user}/{k8_project_name}/{stack_name}" 24 | k8_stack_ref = pulumi.StackReference(k8_stack_ref_id) 25 | kubeconfig = k8_stack_ref.require_output('kubeconfig').apply(lambda c: str(c)) 26 | 27 | k8s_provider = k8s.Provider(resource_name='kubernetes', kubeconfig=kubeconfig) 28 | keys = pulumi.runtime.get_config_secret_keys_env() 29 | 30 | config_secrets = {} 31 | for key in keys: 32 | bag_name, config_key = key.split(':') 33 | config_bag = pulumi.config.Config(bag_name) 34 | if bag_name not in config_secrets.keys(): 35 | config_secrets[bag_name] = {} 36 | 37 | config_secrets[bag_name][config_key] = pulumi.Output.unsecret(config_bag.require_secret(config_key)) 38 | 39 | secrets_output = {} 40 | for k, v in config_secrets.items(): 41 | resource_name = f'pulumi-secret-{k}' 42 | secret = Secret(resource_name=resource_name, 43 | args=SecretInitArgs(string_data=v), 44 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 45 | secrets_output[k] = secret.id 46 | 47 | pulumi.export('pulumi_secrets', secrets_output) 48 | -------------------------------------------------------------------------------- /pulumi/python/requirements.txt: -------------------------------------------------------------------------------- 1 | awscli~=1.25.35 2 | grpcio==1.43.0 3 | fart~=0.1.5 4 | lolcat~=1.4 5 | nodeenv~=1.6.0 6 | passlib~=1.7.4 7 | pulumi-aws>=4.39.0 8 | pulumi-docker==3.1.0 9 | pulumi-eks>=0.41.2 10 | pulumi-kubernetes==3.20.1 11 | pycryptodome~=3.14.0 12 | PyYAML~=5.4.1 13 | requests~=2.27.1 14 | setuptools==62.1.0 15 | setuptools-git-versioning==1.9.2 16 | wheel==0.37.1 17 | yamlreader==3.0.4 18 | pulumi-digitalocean==4.12.0 19 | pulumi-linode==3.7.1 20 | linode-cli~=5.17.2 21 | pulumi~=3.36.0 -------------------------------------------------------------------------------- /pulumi/python/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exit status 4 | set -o pipefail # don't hide errors within pipes 5 | 6 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 7 | PYENV_ROOT="${script_dir}/.pyenv" 8 | 9 | if [ -d "${PYENV_ROOT}" ]; then 10 | PATH="${PATH}:${PYENV_ROOT}/bin" 11 | eval "$(pyenv init --path)" 12 | eval "$(pyenv init -)" 13 | fi 14 | 15 | if [ -d "${script_dir}/venv" ]; then 16 | source "${script_dir}/venv/bin/activate" 17 | else 18 | >&2 echo "Python virtual environment not found at path: ${script_dir}/venv" 19 | >&2 echo "Have you run setup_venv.sh to initialize the environment?" 20 | fi 21 | 22 | exec "$script_dir/automation/main.py" $@ -------------------------------------------------------------------------------- /pulumi/python/tools/README.md: -------------------------------------------------------------------------------- 1 | # Directory 2 | 3 | `/pulumi/python/tools` 4 | 5 | ## _Deprecation Notice_ 6 | 7 | These tools are no longer supported by the MARA team and will be removed in a 8 | future release. They *should* work correctly, but this is not guaranteed. Any 9 | use is at your own risk. 10 | 11 | ## Purpose 12 | 13 | This directory holds common tools that *may* be required by kubernetes 14 | installations that do not meet the minimum requirements of MARA. 15 | 16 | These tools address two main areas: 17 | 18 | * Ability to create persistent volumes. 19 | * Ability to obtain an external egress IP. 20 | 21 | Note that these tools are not specifically endorsed by the creators of MARA, and 22 | you should do your own determination of the best way to provide these 23 | capabilities. Many kubernetes distributions have recommended approaches to 24 | solving these problems. 25 | 26 | To use these tools you will need to run the 27 | [kubernetes-extras.sh](../../../bin/kubernetes-extras.sh) script from the 28 | main `bin` directory. This will walk you through the process of setting up 29 | these tools. 30 | 31 | ## Key Files 32 | 33 | * [`common`](./common) Common directory to hold the pulumi configuration file. 34 | * [`metallb`](./metallb) Install directory for the `metallb` package. 35 | * [`nfsvolumes`](./nfsvolumes) Install directory for the `nfsvolumes` package. 36 | 37 | ## Notes 38 | 39 | Please read the comments inside the installation script, as there are some 40 | important caveats. 41 | -------------------------------------------------------------------------------- /pulumi/python/tools/common/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: tools-common 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ./config 7 | description: Common configuration directory 8 | -------------------------------------------------------------------------------- /pulumi/python/tools/common/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/pulumi/python/tools/common/config/.gitkeep -------------------------------------------------------------------------------- /pulumi/python/tools/metallb/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: metallb 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../common/config 7 | description: Deploys Metallb 8 | -------------------------------------------------------------------------------- /pulumi/python/tools/metallb/__main__.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | from base64 import b64encode 3 | import pulumi 4 | import ipaddress 5 | import os 6 | import pulumi_kubernetes as k8s 7 | from pulumi_kubernetes.yaml import ConfigFile 8 | from kic_util import pulumi_config 9 | 10 | # We need the kubeconfig and cluster name. 11 | config = pulumi.Config('kubernetes') 12 | cluster_name = config.require('cluster_name') 13 | context_name = config.require('context_name') 14 | kubeconfig = config.require('kubeconfig') 15 | 16 | 17 | # Function to add namespace 18 | def add_namespace(obj): 19 | obj['metadata']['namespace'] = 'metallb-system' 20 | 21 | 22 | def pulumi_kube_project_name(): 23 | script_dir = os.path.dirname(os.path.abspath(__file__)) 24 | kube_project_path = os.path.join(script_dir, '..', 'common') 25 | return pulumi_config.get_pulumi_project_name(kube_project_path) 26 | 27 | 28 | # Where are our manifests? 29 | def k8_manifest_location(): 30 | script_dir = os.path.dirname(os.path.abspath(__file__)) 31 | k8_manifest_path = os.path.join(script_dir, 'manifests', 'metallb.yaml') 32 | return k8_manifest_path 33 | 34 | 35 | stack_name = pulumi.get_stack() 36 | project_name = pulumi.get_project() 37 | kube_project_name = pulumi_kube_project_name() 38 | pulumi_user = pulumi_config.get_pulumi_user() 39 | 40 | kube_stack_ref_id = f"{pulumi_user}/{kube_project_name}/{stack_name}" 41 | kube_stack_ref = pulumi.StackReference(kube_stack_ref_id) 42 | 43 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', kubeconfig=kubeconfig) 44 | 45 | # Create the namespace for metallb 46 | ns = k8s.core.v1.Namespace(resource_name='metallb-system', 47 | metadata={'name': 'metallb-system'}, 48 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 49 | 50 | config = pulumi.Config('metallb') 51 | thecidr = config.require('thecidr') 52 | 53 | thenet = ipaddress.IPv4Network(thecidr, strict=False) 54 | therange = str(thenet[0]) + "-" + str(thenet[-1]) 55 | 56 | k8_manifest = k8_manifest_location() 57 | 58 | metallb = ConfigFile( 59 | "metallb", 60 | transformations=[add_namespace], 61 | opts=pulumi.ResourceOptions(depends_on=[ns]), 62 | file=k8_manifest) 63 | 64 | # Generate a secret 65 | secretkey = b64encode(token_bytes(128)).decode() 66 | 67 | # Create a secret in K8 68 | metallb_system_memberlist_secret = k8s.core.v1.Secret("metallb_systemMemberlistSecret", 69 | api_version="v1", 70 | data={ 71 | "secretkey": secretkey 72 | }, 73 | kind="Secret", 74 | metadata=k8s.meta.v1.ObjectMetaArgs( 75 | name="memberlist", 76 | namespace="metallb-system", 77 | ), 78 | opts=pulumi.ResourceOptions(depends_on=[ns]) 79 | ) 80 | 81 | # Create a config map 82 | metallb_system_config_config_map = k8s.core.v1.ConfigMap("metallb_systemConfigConfigMap", 83 | api_version="v1", 84 | kind="ConfigMap", 85 | metadata=k8s.meta.v1.ObjectMetaArgs( 86 | namespace="metallb-system", 87 | name="config", 88 | ), 89 | opts=pulumi.ResourceOptions(depends_on=[ns]), 90 | data={ 91 | "config": """address-pools: 92 | - name: default 93 | protocol: layer2 94 | addresses: 95 | - """ + therange, 96 | } 97 | ) 98 | -------------------------------------------------------------------------------- /pulumi/python/tools/nfsvolumes/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: nfsvolumes 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../common/config 7 | description: Deploys NFS PV Client 8 | -------------------------------------------------------------------------------- /pulumi/python/tools/nfsvolumes/__main__.py: -------------------------------------------------------------------------------- 1 | import pulumi 2 | import os 3 | import pulumi_kubernetes as k8s 4 | from kic_util import pulumi_config 5 | from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs 6 | 7 | # We need the kubeconfig and cluster name. 8 | config = pulumi.Config('kubernetes') 9 | cluster_name = config.require('cluster_name') 10 | context_name = config.require('context_name') 11 | kubeconfig = config.require('kubeconfig') 12 | 13 | 14 | # Function to add namespace 15 | def add_namespace(obj): 16 | obj['metadata']['namespace'] = 'nfsvols' 17 | 18 | 19 | def pulumi_kube_project_name(): 20 | script_dir = os.path.dirname(os.path.abspath(__file__)) 21 | kube_project_path = os.path.join(script_dir, '..', 'common') 22 | return pulumi_config.get_pulumi_project_name(kube_project_path) 23 | 24 | 25 | stack_name = pulumi.get_stack() 26 | project_name = pulumi.get_project() 27 | kube_project_name = pulumi_kube_project_name() 28 | pulumi_user = pulumi_config.get_pulumi_user() 29 | 30 | kube_stack_ref_id = f"{pulumi_user}/{kube_project_name}/{stack_name}" 31 | kube_stack_ref = pulumi.StackReference(kube_stack_ref_id) 32 | 33 | k8s_provider = k8s.Provider(resource_name=f'ingress-controller', kubeconfig=kubeconfig) 34 | 35 | ns = k8s.core.v1.Namespace(resource_name='nfsvols', 36 | metadata={'name': 'nfsvols'}, 37 | opts=pulumi.ResourceOptions(provider=k8s_provider)) 38 | 39 | config = pulumi.Config('nfsvols') 40 | chart_name = config.get('chart_name') 41 | if not chart_name: 42 | chart_name = 'nfs-subdir-external-provisioner' 43 | chart_version = config.get('chart_version') 44 | if not chart_version: 45 | chart_version = '4.0.14' 46 | helm_repo_name = config.get('helm_repo_name') 47 | if not helm_repo_name: 48 | helm_repo_name = 'nfs-subdir-external-provisioner' 49 | helm_repo_url = config.get('helm_repo_url') 50 | if not helm_repo_url: 51 | helm_repo_url = 'https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner' 52 | nfsserver = config.require('nfsserver') 53 | nfspath = config.require('nfspath') 54 | nfsopts = '{nolock,nfsvers=3}' 55 | 56 | # 57 | # Allow the user to set timeout per helm chart; otherwise 58 | # we default to 5 minutes. 59 | # 60 | helm_timeout = config.get_int('helm_timeout') 61 | if not helm_timeout: 62 | helm_timeout = 300 63 | 64 | nfsvols_release_args = ReleaseArgs( 65 | chart=chart_name, 66 | repository_opts=RepositoryOptsArgs( 67 | repo=helm_repo_url 68 | ), 69 | version=chart_version, 70 | namespace=ns.metadata.name, 71 | 72 | # Values from Chart's parameters specified hierarchically, 73 | values={ 74 | "storageClass": { 75 | "defaultClass": True 76 | }, 77 | "nfs": { 78 | "server": nfsserver, 79 | "path": nfspath, 80 | "mountOptions": [ 81 | "nolock", 82 | "nfsvers=3" 83 | ] 84 | } 85 | }, 86 | # User configurable timeout 87 | timeout=helm_timeout, 88 | # By default, Release resource will wait till all created resources 89 | # are available. Set this to true to skip waiting on resources being 90 | # available. 91 | skip_await=False, 92 | cleanup_on_fail=True, 93 | # Provide a name for our release 94 | name="nfsvols", 95 | # Lint the chart before installing 96 | lint=True, 97 | # Force update if required 98 | force_update=True) 99 | 100 | nfsvols_release = Release("nfsvols", args=nfsvols_release_args) 101 | 102 | nfsvols_status = nfsvols_release.status 103 | 104 | pulumi.export('nfsvols_status', pulumi.Output.unsecret(nfsvols_status)) 105 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: kic-image-build 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Builds a new KIC image 8 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/__main__.py: -------------------------------------------------------------------------------- 1 | import pulumi 2 | 3 | from ingress_controller_image import IngressControllerImage 4 | from ingress_controller_image_builder_args import IngressControllerImageBuilderArgs 5 | from ingress_controller_image_puller_args import IngressControllerImagePullerArgs 6 | from nginx_plus_args import NginxPlusArgs 7 | 8 | DEFAULT_KIC = "nginx/nginx-ingress:2.4.2" 9 | 10 | stack_name = pulumi.get_stack() 11 | project_name = pulumi.get_project() 12 | 13 | config = pulumi.Config('kic') 14 | image_origin = config.get('image_origin') 15 | if not image_origin: 16 | pulumi.log.info('kic:image_origin not specified, defaulting to: repository') 17 | image_origin = 'registry' 18 | 19 | make_target = config.get('make_target') 20 | kic_src_url = config.get('src_url') 21 | always_rebuild = config.get_bool('always_rebuild') 22 | 23 | plus_config = config.get_object('nginx_plus') 24 | if plus_config: 25 | nginx_plus_args = NginxPlusArgs(key_path=plus_config.get('kic:key_path'), 26 | cert_path=plus_config.get('kic:cert_path')) 27 | else: 28 | nginx_plus_args = None 29 | 30 | # Below is a crucial fork in logic where if 'source' is specified, we build the 31 | # KIC container image from source code. If 'registry' is specified, we pull an 32 | # existing image from a container registry. 33 | # 34 | # In the case of the registry workflow, authentication (login or certs) need to 35 | # be configured before this script is ran. 36 | 37 | if image_origin == 'source': 38 | image_args = IngressControllerImageBuilderArgs(make_target=make_target, 39 | kic_src_url=kic_src_url, 40 | always_rebuild=always_rebuild, 41 | nginx_plus_args=nginx_plus_args) 42 | 43 | # Download KIC source code, run `make`, and build Docker images 44 | ingress_image = IngressControllerImage(name='nginx-ingress-controller', 45 | kic_image_args=image_args) 46 | elif image_origin == 'registry': 47 | if not config.get('image_name'): 48 | image_args = IngressControllerImagePullerArgs(image_name=DEFAULT_KIC) 49 | else: 50 | image_args = IngressControllerImagePullerArgs(image_name=config.get('image_name')) 51 | 52 | ingress_image = IngressControllerImage(name='nginx-ingress-controller', 53 | kic_image_args=image_args) 54 | else: 55 | raise RuntimeError(f'unknown image_origin: {image_origin}') 56 | 57 | pulumi.export('ingress_image', ingress_image) 58 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_image.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | import pulumi 4 | from pulumi.dynamic import Resource 5 | 6 | from ingress_controller_image_builder_args import IngressControllerImageBuilderArgs 7 | from ingress_controller_image_puller_args import IngressControllerImagePullerArgs 8 | from ingress_controller_image_builder_provider import IngressControllerImageBuilderProvider 9 | from ingress_controller_image_puller_provider import IngressControllerImagePullerProvider 10 | from ingress_controller_source_archive_url import IngressControllerSourceArchiveUrl 11 | 12 | 13 | class IngressControllerImage(Resource): 14 | def __init__(self, 15 | name: str, 16 | kic_image_args: Optional[pulumi.Input[ 17 | Union['IngressControllerImageBuilderArgs', 'IngressControllerImagePullerArgs']]] = None, 18 | opts: Optional[pulumi.ResourceOptions] = None) -> None: 19 | 20 | if not opts: 21 | opts = pulumi.ResourceOptions() 22 | 23 | if not kic_image_args: 24 | props = dict() 25 | else: 26 | props = vars(kic_image_args) 27 | 28 | if isinstance(kic_image_args, IngressControllerImageBuilderArgs): 29 | if 'always_rebuild' not in props: 30 | props['always_rebuild'] = False 31 | if 'image_id' not in props: 32 | props['image_id'] = None 33 | if 'image_name' not in props: 34 | props['image_name'] = None 35 | if 'image_name_alias' not in props: 36 | props['image_name_alias'] = None 37 | if 'image_tag' not in props: 38 | props['image_tag'] = None 39 | if 'image_tag_alias' not in props: 40 | props['image_tag_alias'] = None 41 | if 'nginx_plus_args' not in props: 42 | props['nginx_plus_args'] = None 43 | 44 | if 'kic_src_url' not in props or not props['kic_src_url']: 45 | pulumi.log.warn("No source url specified for 'kic_src_url', using latest tag from github", self) 46 | props['kic_src_url'] = IngressControllerSourceArchiveUrl.from_github() 47 | if 'make_target' not in props or not props['make_target']: 48 | pulumi.log.warn("'make_target' not specified, using " + 49 | f"{IngressControllerImageBuilderProvider.MAKE_TARGET}", self) 50 | props['make_target'] = IngressControllerImageBuilderProvider.MAKE_TARGET 51 | provider = IngressControllerImageBuilderProvider(self) 52 | elif isinstance(kic_image_args, IngressControllerImagePullerArgs): 53 | if 'image_name' not in props or not props['image_name']: 54 | repository = 'nginx/nginx-ingress' 55 | latest = IngressControllerSourceArchiveUrl.latest_version().lstrip('v') 56 | image_name = f'{repository}:{latest}' 57 | pulumi.log.info(f'kic:image_name was not specified, defaulting to: {image_name}', self) 58 | props['image_name'] = image_name 59 | props['image_id'] = None 60 | props['image_tag'] = None 61 | 62 | provider = IngressControllerImagePullerProvider(self) 63 | else: 64 | raise ValueError(f'unknown kic_image_args provided: {kic_image_args}') 65 | 66 | super().__init__(name=name, opts=opts, props=props, provider=provider) 67 | 68 | @property 69 | def image_id(self) -> pulumi.Output[str]: 70 | return pulumi.get(self, 'image_id') 71 | 72 | @property 73 | def image_name(self) -> pulumi.Output[str]: 74 | return pulumi.get(self, 'image_name') 75 | 76 | @property 77 | def image_name_alias(self) -> pulumi.Output[str]: 78 | return pulumi.get(self, 'image_name_alias') 79 | 80 | @property 81 | def image_tag(self) -> pulumi.Output[str]: 82 | return pulumi.get(self, 'image_tag') 83 | 84 | @property 85 | def image_tag_alias(self) -> pulumi.Output[str]: 86 | return pulumi.get(self, 'image_tag_alias') 87 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_image_base_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Dict, List, Any, Callable 3 | 4 | import pulumi 5 | from pulumi import Resource 6 | from pulumi.dynamic import ResourceProvider, CheckFailure 7 | 8 | from kic_util import external_process 9 | 10 | 11 | class IngressControllerBaseProvider(ResourceProvider): 12 | """Extends Pulumi dynamic provider with methods used for invoking Docker and common dynamic calls""" 13 | resource: Resource 14 | 15 | def __init__(self, 16 | resource: Optional[pulumi.Resource] = None, 17 | runner=external_process.run, 18 | debug_logger_func=None): 19 | self.resource = resource 20 | self.runner = runner 21 | 22 | if debug_logger_func: 23 | self.debug_logger = debug_logger_func 24 | elif self._debug_logger_func: 25 | self.debug_logger = self._debug_logger_func 26 | 27 | super().__init__() 28 | 29 | def delete(self, _id: str, _props: Any) -> None: 30 | if 'image_id' in _props and _props['image_id']: 31 | image_id = _props['image_id'] 32 | pulumi.log.info(f'deleting image {image_id}') 33 | self._docker_delete_image(image_id) 34 | 35 | def _debug_logger_func(self, msg): 36 | pulumi.log.debug(msg, self.resource) 37 | 38 | def _run_docker(self, cmd: str, suppress_error: bool = False) -> (str, str): 39 | self.debug_logger(f'running Docker cmd: {cmd}') 40 | res, err = self.runner(cmd=cmd, suppress_error=suppress_error) 41 | self.debug_logger(os.linesep.join([res, err])) 42 | 43 | return res, err 44 | 45 | def _docker_pull(self, image_name: str) -> str: 46 | """Pull a container image from a registry 47 | :param image_name: full container image name in the format of repository:tag 48 | :return full image name with server name (e.g. docker.io/library/debian:buster-slim) 49 | """ 50 | cmd = f'docker pull --quiet "{image_name}"' 51 | res, _ = self._run_docker(cmd=cmd) 52 | image_name = res.strip() 53 | return image_name 54 | 55 | def _docker_tag(self, source_image_identifier: str, target_image_identifier: str) -> None: 56 | """Creates a tag image that refers to the source image 57 | :param source_image_identifier: container id or name 58 | :param target_image_identifier: container id or name""" 59 | cmd = f'docker tag "{source_image_identifier}" "{target_image_identifier}"' 60 | res, _ = self._run_docker(cmd=cmd) 61 | 62 | def _docker_image_id_from_image_name(self, image_name: str) -> str: 63 | """Get the image id of an image from Docker 64 | :param image_name: full container image name in the format of repository:tag 65 | :return: checksum id of the image 66 | """ 67 | cmd = f'docker image ls --quiet --no-trunc "{image_name}"' 68 | res, _ = self._run_docker(cmd=cmd) 69 | image_id = res.strip() 70 | return image_id 71 | 72 | def _docker_delete_image(self, image_identifier: str) -> Dict[str, List[str]]: 73 | """Delete image from Docker 74 | :param image_identifier: image id or image name 75 | :return dictionary of the ids deleted and tags removed 76 | """ 77 | cmd = f'docker image rm --force "{image_identifier}"' 78 | res, _ = self._run_docker(cmd=cmd, suppress_error=True) 79 | 80 | output = {} 81 | 82 | for line in res.splitlines(): 83 | parts = line.split(': ', 2) 84 | if len(parts) == 2: 85 | key = parts[0].lower() 86 | val = parts[1].lower() 87 | if key not in output: 88 | output[key] = [val] 89 | else: 90 | output[key].append(val) 91 | 92 | return output 93 | 94 | @staticmethod 95 | def _is_key_defined(key: str, props: dict) -> bool: 96 | return key in props and props[key] 97 | 98 | @staticmethod 99 | def _new_and_old_val_equal(key: str, _news: Any, _olds: Any) -> bool: 100 | in_news = IngressControllerBaseProvider._is_key_defined(key, _news) 101 | in_olds = IngressControllerBaseProvider._is_key_defined(key, _olds) 102 | 103 | if in_news and in_olds: 104 | return _news[key] == _olds[key] 105 | else: 106 | return False 107 | 108 | @staticmethod 109 | def _check_for_required_params(news: Any, required: List[str]) -> List[CheckFailure]: 110 | failures: List[CheckFailure] = [] 111 | 112 | for param in required: 113 | if param not in news: 114 | failures.append(CheckFailure(property_=param, reason=f'{param} must be specified')) 115 | 116 | return failures 117 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_image_builder_args.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import pulumi 3 | 4 | 5 | @pulumi.input_type 6 | class IngressControllerImageBuilderArgs: 7 | """Arguments needed for instantiating the IngressControllerImageBuilderProvider""" 8 | 9 | def __init__(self, 10 | kic_src_url: Optional[pulumi.Input[str]] = None, 11 | make_target: Optional[pulumi.Input[str]] = None, 12 | always_rebuild: Optional[bool] = False, 13 | nginx_plus_args: Optional[pulumi.InputType['NginxPlusArgs']] = None): 14 | self.__dict__ = dict() 15 | pulumi.set(self, 'kic_src_url', kic_src_url) 16 | pulumi.set(self, 'make_target', make_target) 17 | pulumi.set(self, 'always_rebuild', always_rebuild) 18 | pulumi.set(self, 'nginx_plus_args', nginx_plus_args) 19 | 20 | @property 21 | @pulumi.getter 22 | def kic_src_url(self) -> Optional[pulumi.Input[str]]: 23 | return pulumi.get(self, "kic_src_url") 24 | 25 | @property 26 | @pulumi.getter 27 | def make_target(self) -> Optional[pulumi.Input[str]]: 28 | return pulumi.get(self, "make_target") 29 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_image_puller_args.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import pulumi 3 | 4 | 5 | @pulumi.input_type 6 | class IngressControllerImagePullerArgs: 7 | """Arguments needed for instantiating the IngressControllerImagePullerProvider""" 8 | def __init__(self, image_name: Optional[pulumi.Input[str]] = None): 9 | self.__dict__ = dict() 10 | pulumi.set(self, 'image_name', image_name) 11 | 12 | @property 13 | @pulumi.getter 14 | def image_name(self) -> Optional[pulumi.Input[str]]: 15 | return pulumi.get(self, "image_name") 16 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_image_puller_provider.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional, Any, Dict, List 3 | 4 | import pulumi 5 | from pulumi.dynamic import CreateResult, UpdateResult, DiffResult, CheckResult, CheckFailure, ReadResult 6 | 7 | from ingress_controller_image_base_provider import IngressControllerBaseProvider as BaseProvider 8 | from kic_util.docker_image_name import DockerImageName, DockerImageNameError 9 | from kic_util import external_process 10 | 11 | 12 | class IngressControllerImagePullerProvider(BaseProvider): 13 | """Pulumi dynamic provider that pulls ingress container images from an external registry""" 14 | 15 | REQUIRED_PROPS: List[str] = ['image_name'] 16 | 17 | def __init__(self, 18 | resource: Optional[pulumi.Resource] = None, 19 | debug_logger_func=None): 20 | super().__init__(resource=resource, debug_logger_func=debug_logger_func, runner=external_process.run) 21 | 22 | def pull(self, props: Any) -> Dict[str, str]: 23 | image_name = props['image_name'] 24 | 25 | pulumi.log.info(f'pulling from registry: {image_name}', self.resource) 26 | full_image_name = self._docker_pull(image_name) 27 | if full_image_name != image_name: 28 | pulumi.log.info(f'full image name: {full_image_name}', self.resource) 29 | 30 | image_id = self._docker_image_id_from_image_name(image_name) 31 | image = DockerImageName.from_name(image_name=image_name, image_id=image_id) 32 | 33 | return {'image_id': image.id, 34 | 'image_name': str(image), 35 | 'image_name_alias': None, 36 | 'image_tag': image.tag, 37 | 'image_tag_alias': None} 38 | 39 | def create(self, props: Any) -> CreateResult: 40 | outputs = self.pull(props) 41 | id_ = str(uuid.uuid4()) 42 | 43 | return CreateResult(id_=id_, outs=outputs) 44 | 45 | def update(self, _id: str, _olds: Any, _news: Any) -> UpdateResult: 46 | outputs = self.pull(props=_news) 47 | return UpdateResult(outs=outputs) 48 | 49 | def diff(self, _id: str, _olds: Any, _news: Any) -> DiffResult: 50 | # Always assume that the container image has changed so that we run 51 | # docker pull to see if a newer image is available 52 | return DiffResult(changes=True) 53 | 54 | def check(self, _olds: Any, news: Any) -> CheckResult: 55 | failures = BaseProvider._check_for_required_params(news, IngressControllerImagePullerProvider.REQUIRED_PROPS) 56 | 57 | try: 58 | DockerImageName.from_name(news['image_name']) 59 | except DockerImageNameError as e: 60 | failures.append(CheckFailure(property_='image_name', reason=str(e))) 61 | 62 | return CheckResult(inputs=news, failures=failures) 63 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/ingress_controller_source_archive_url.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import requests 3 | from urllib import parse 4 | 5 | 6 | class IngressControllerSourceArchiveUrl: 7 | """Utility class that allows the retreival of the latest version number of the 8 | NGINX Kubernetes Ingress Controller""" 9 | DOWNLOAD_URL = 'https://github.com/nginxinc/kubernetes-ingress.git' 10 | 11 | @staticmethod 12 | def latest_version() -> str: 13 | ping_url = 'https://github.com/nginxinc/kubernetes-ingress/releases/latest' 14 | response = requests.head(ping_url) 15 | redirect = response.headers.get('location') 16 | tag_url = parse.urlparse(redirect) 17 | tag_url_path = tag_url.path 18 | elements = tag_url_path.split('/') 19 | version = str(elements[-1]) 20 | 21 | return version 22 | 23 | @staticmethod 24 | def from_github(version: Optional[str] = None) -> str: 25 | if not version: 26 | version = IngressControllerSourceArchiveUrl.latest_version() 27 | 28 | return f'{IngressControllerSourceArchiveUrl.DOWNLOAD_URL}#{version}' 29 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/nginx_plus_args.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import pulumi 3 | 4 | 5 | @pulumi.input_type 6 | class NginxPlusArgs: 7 | def __init__(self, key_path: pulumi.Input[str], cert_path: pulumi.Input[str]): 8 | self.__dict__ = dict() 9 | pulumi.set(self, 'key_path', key_path) 10 | pulumi.set(self, 'cert_path', cert_path) 11 | 12 | @property 13 | @pulumi.getter 14 | def key_path(self) -> Optional[pulumi.Input[str]]: 15 | return pulumi.get(self, "key_path") 16 | 17 | @property 18 | @pulumi.getter 19 | def cert_path(self) -> Optional[pulumi.Input[str]]: 20 | return pulumi.get(self, "cert_path") 21 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-build/test_ingress_controller_image_base_provider.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ingress_controller_image_base_provider import IngressControllerBaseProvider 4 | 5 | 6 | class TestIngressControllerBaseProvider(unittest.TestCase): 7 | @staticmethod 8 | def mock_provider(runner) -> IngressControllerBaseProvider: 9 | return IngressControllerBaseProvider(debug_logger_func=print, 10 | runner=runner) 11 | 12 | def test_docker_image_id_from_image_name_found(self): 13 | expected_id = 'sha256:5c3d57f3e47a49213497259cf9b1b462024ca945e4cccf2c6568cb86ee03e46d' 14 | 15 | def output(**kwargs): 16 | return f'{expected_id}\n', '' 17 | 18 | provider = TestIngressControllerBaseProvider.mock_provider(output) 19 | image_name = 'this value is never read because docker is not run' 20 | actual_id = provider._docker_image_id_from_image_name(image_name) 21 | 22 | self.assertEqual(expected_id, actual_id) 23 | 24 | def test_docker_image_id_from_image_name_not_found(self): 25 | expected_id = '' 26 | 27 | def output(**kwargs): 28 | return '\n', '' 29 | 30 | provider = TestIngressControllerBaseProvider.mock_provider(output) 31 | image_name = 'this value is never read because docker is not run' 32 | actual_id = provider._docker_image_id_from_image_name(image_name) 33 | 34 | self.assertEqual(expected_id, actual_id) 35 | 36 | def test_docker_delete_image_from_existing_image(self): 37 | def output(**kwargs): 38 | return '''Untagged: aevea/commitsar:latest 39 | Untagged: aevea/commitsar@sha256:f16e13252ddfae6db046be1ff390d21c526c315d1074ac45fef6c92d00537cbc 40 | Deleted: sha256:b4781ea172863b494400fafce1f0048f7079d35a4b3d9d8f8e1f582f55f7c625 41 | Deleted: sha256:f580a2506098d5900873a982fa01ef3556b214e2fd2ea758ce0c6b0e059badca 42 | Deleted: sha256:4883003e2490b26c00d4b45034a9787e14a3bcbb5c130f3e341e64d219029db1 43 | Deleted: sha256:11836812529cf64f9682608771e092da0ee9cd8f8001a41f21d3c82621627b3a 44 | Deleted: sha256:5c550a8007af51b5d0c96eeecb979a79d51a3213075e8bbf6c8aa1a10440f5a4''', '' 45 | 46 | expected = { 47 | 'untagged': ['aevea/commitsar:latest', 48 | 'aevea/commitsar@sha256:f16e13252ddfae6db046be1ff390d21c526c315d1074ac45fef6c92d00537cbc'], 49 | 'deleted': ['sha256:b4781ea172863b494400fafce1f0048f7079d35a4b3d9d8f8e1f582f55f7c625', 50 | 'sha256:f580a2506098d5900873a982fa01ef3556b214e2fd2ea758ce0c6b0e059badca', 51 | 'sha256:4883003e2490b26c00d4b45034a9787e14a3bcbb5c130f3e341e64d219029db1', 52 | 'sha256:11836812529cf64f9682608771e092da0ee9cd8f8001a41f21d3c82621627b3a', 53 | 'sha256:5c550a8007af51b5d0c96eeecb979a79d51a3213075e8bbf6c8aa1a10440f5a4'] 54 | } 55 | 56 | provider = TestIngressControllerBaseProvider.mock_provider(output) 57 | image_name = 'this value is never read because docker is not run' 58 | deleted = provider._docker_delete_image(image_name) 59 | 60 | self.assertDictEqual(expected, deleted) 61 | 62 | def test_docker_delete_image_from_unknown_image(self): 63 | def output(**kwargs): 64 | return '', "Error: No such container: mycontainer:1.1.1" 65 | 66 | expected = {} 67 | provider = TestIngressControllerBaseProvider.mock_provider(output) 68 | image_name = 'this value is never read because docker is not run' 69 | deleted = provider._docker_delete_image(image_name) 70 | self.assertDictEqual(expected, deleted) 71 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: kic-image-push 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: ../../venv 6 | config: ../../../../config/pulumi 7 | description: Pushes KIC image to ECR 8 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/__main__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import pulumi 4 | from pulumi import Output 5 | from kic_util import pulumi_config 6 | from registries.base_registry import ContainerRegistry 7 | 8 | from repository_push import RepositoryPush, RepositoryPushArgs 9 | 10 | 11 | def project_name_from_project_dir(dirname: str): 12 | script_dir = os.path.dirname(os.path.abspath(__file__)) 13 | project_path = os.path.join(script_dir, '..', dirname) 14 | return pulumi_config.get_pulumi_project_name(project_path) 15 | 16 | 17 | def select_image_name(image): 18 | if 'image_name_alias' in image: 19 | return image['image_name_alias'] 20 | else: 21 | return image['image_name'] 22 | 23 | 24 | def select_image_tag_alias(image): 25 | if 'image_tag_alias' in image: 26 | return image['image_tag_alias'] 27 | else: 28 | return '' 29 | 30 | 31 | def select_image_id(image): 32 | if 'image_id' not in image or not image['image_id']: 33 | raise ValueError(f'no image id found in kic-image-build-stack: {image}') 34 | return image['image_id'] 35 | 36 | 37 | def select_image_tag(image): 38 | if 'image_tag' not in image or not image['image_tag']: 39 | raise ValueError(f'no image tag found in kic-image-build-stack: {image}') 40 | return image['image_tag'] 41 | 42 | 43 | stack_name = pulumi.get_stack() 44 | project_name = pulumi.get_project() 45 | pulumi_user = pulumi_config.get_pulumi_user() 46 | k8s_config = pulumi.Config('kubernetes') 47 | 48 | kic_image_build_project_name = project_name_from_project_dir('kic-image-build') 49 | kic_image_build_stack_ref_id = f"{pulumi_user}/{kic_image_build_project_name}/{stack_name}" 50 | kick_image_build_stack_ref = pulumi.StackReference(kic_image_build_stack_ref_id) 51 | ingress_image = kick_image_build_stack_ref.require_output('ingress_image') 52 | 53 | # We default to using the image name alias because it is a more precise definition 54 | # of the image type when we build from source. 55 | image_name = ingress_image.apply(select_image_name) 56 | image_tag_alias = ingress_image.apply(select_image_tag_alias) 57 | image_id = ingress_image.apply(select_image_id) 58 | image_tag = ingress_image.apply(select_image_tag) 59 | 60 | 61 | def push_to_container_registry(container_registry: ContainerRegistry) -> RepositoryPush: 62 | if container_registry.login_to_registry(): 63 | repo_args = RepositoryPushArgs(repository_url=container_registry.registry_url, 64 | image_id=image_id, 65 | image_name=image_name, 66 | image_tag=image_tag, 67 | image_tag_alias=image_tag_alias) 68 | 69 | # Push the images to the container registry 70 | _repo_push = RepositoryPush(name='ingress-controller-registry-push', 71 | repository_args=repo_args, 72 | check_if_id_matches_tag_func=container_registry.check_if_id_matches_tag) 73 | 74 | pulumi.info('Pushing NGINX Ingress Controller container image to ' 75 | f'{container_registry.registry_implementation_name()}') 76 | 77 | return _repo_push 78 | else: 79 | raise 'Unable to log into container registry' 80 | 81 | 82 | # Dynamically determine the infrastructure provider and instantiate the 83 | # correlated class, then apply the pulumi async closures. 84 | infra_type = k8s_config.require('infra_type').lower() 85 | module = importlib.import_module(name=f'registries.{infra_type}') 86 | container_registry_class = module.CLASS 87 | repo_push: Output[RepositoryPush] = container_registry_class.instance(stack_name, pulumi_user)\ 88 | .apply(push_to_container_registry) 89 | 90 | pulumi.export('container_repo_push', Output.unsecret(repo_push)) 91 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/registries/aws.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from typing import List, Any 5 | 6 | from pulumi import Output, StackReference, log 7 | from pulumi_aws import ecr 8 | from kic_util import pulumi_config 9 | from registries.base_registry import ContainerRegistry, RegistryCredentials 10 | 11 | 12 | class ElasticContainerRegistry(ContainerRegistry): 13 | @classmethod 14 | def instance(cls, stack_name: str, pulumi_user: str) -> Output[ContainerRegistry]: 15 | super().instance(stack_name, pulumi_user) 16 | ecr_project_name = ElasticContainerRegistry.aws_project_name_from_project_dir('ecr') 17 | ecr_stack_ref_id = f"{pulumi_user}/{ecr_project_name}/{stack_name}" 18 | stack_ref = StackReference(ecr_stack_ref_id) 19 | # Async query for credentials from stack reference 20 | ecr_registry_id = stack_ref.require_output('registry_id') 21 | credentials_output = ecr_registry_id.apply(ElasticContainerRegistry.get_ecr_credentials) 22 | # Async query for repository url from stack reference 23 | # Note that AWS ECR refers to itself as a repository and not a registry, we aim to keep 24 | # that naming consistent when referring directly to ECR nouns 25 | repository_url_output = stack_ref.require_output('repository_url') 26 | 27 | def _make_instance(params: List[Any]) -> ElasticContainerRegistry: 28 | return cls(stack_name=stack_name, pulumi_user=pulumi_user, registry_url=params[0], credentials=params[1]) 29 | 30 | return Output.all(repository_url_output, credentials_output).apply(_make_instance) 31 | 32 | @staticmethod 33 | def aws_project_name_from_project_dir(dirname: str): 34 | script_dir = os.path.dirname(os.path.abspath(__file__)) 35 | project_path = os.path.join(script_dir, '..', '..', '..', 'infrastructure', 'aws', dirname) 36 | return pulumi_config.get_pulumi_project_name(project_path) 37 | 38 | @staticmethod 39 | def get_ecr_credentials(registry_id: str) -> RegistryCredentials: 40 | credentials = ecr.get_credentials(registry_id) 41 | token = credentials.authorization_token 42 | return ContainerRegistry.decode_credentials(token) 43 | 44 | def registry_implementation_name(self) -> str: 45 | return 'AWS Elastic Container Registry (ECR)' 46 | 47 | def _ecr_docker_api_url(self, ) -> str: 48 | registry_url_parts = self.registry_url.split('/') 49 | ecr_host = registry_url_parts[0] 50 | ecr_path = registry_url_parts[1] 51 | return f'https://{ecr_host}/v2/{ecr_path}' 52 | 53 | def check_if_id_matches_tag(self, image_tag: str, new_image_id: str) -> bool: 54 | docker_api_url = self._ecr_docker_api_url() 55 | auth_tuple = (self.credentials.username, self.credentials.password) 56 | 57 | log.debug(f'Querying for latest image id: {docker_api_url}/manifests/{image_tag}') 58 | with requests.get(f'{docker_api_url}/manifests/{image_tag}', auth=auth_tuple) as response: 59 | if response.status_code != 200: 60 | log.warn(f'Unable to query ECR directly for image id') 61 | return False 62 | json_response = response.json() 63 | if 'config' in json_response and 'digest' in json_response['config']: 64 | remote_image_id = json_response['config']['digest'] 65 | return remote_image_id != new_image_id 66 | else: 67 | return True 68 | 69 | 70 | CLASS = ElasticContainerRegistry 71 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/registries/base_registry.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import urllib 3 | from urllib import parse 4 | from typing import Optional 5 | 6 | import pulumi.log 7 | from pulumi import Input 8 | import pulumi_docker as docker 9 | 10 | from kic_util import external_process 11 | 12 | 13 | class RegistryCredentials: 14 | username: Input[str] 15 | password: Input[str] 16 | 17 | def __init__(self, 18 | username: Input[str], 19 | password: Input[str]): 20 | self.username = username 21 | self.password = password 22 | 23 | 24 | class ContainerRegistry: 25 | stack_name: str 26 | pulumi_user: str 27 | credentials: Optional[RegistryCredentials] 28 | registry_url: str 29 | 30 | def __init__(self, 31 | stack_name: str, 32 | pulumi_user: str, 33 | registry_url: str, 34 | credentials: Optional[RegistryCredentials]) -> None: 35 | super().__init__() 36 | self.stack_name = stack_name 37 | self.pulumi_user = pulumi_user 38 | self.registry_url = registry_url 39 | self.credentials = credentials 40 | 41 | def format_registry_url_for_docker_login(self): 42 | # We assume that the scheme is https because that's what is used most everywhere 43 | registry_host_url = urllib.parse.urlparse(f'https://{self.registry_url}') 44 | # We strip out the path from the URL because it isn't used when logging into a repository 45 | return f'{registry_host_url.scheme}://{registry_host_url.hostname}' 46 | 47 | def login_to_registry(self) -> Optional[docker.LoginResult]: 48 | registry = docker.Registry(registry=self.format_registry_url_for_docker_login(), 49 | username=self.credentials.username, 50 | password=self.credentials.password) 51 | 52 | docker.login_to_registry(registry=registry, log_resource=None) 53 | pulumi.log.info(f'Logged into container registry: {registry.registry}') 54 | 55 | if not docker.login_results: 56 | return None 57 | if docker.login_results[0]: 58 | return docker.login_results[0] 59 | 60 | def logout_of_registry(self): 61 | docker_cmd = f'docker logout {self.format_registry_url_for_docker_login()}' 62 | res, _ = external_process.run(cmd=docker_cmd) 63 | pulumi.log.info(res) 64 | 65 | def check_if_id_matches_tag(self, image_tag: str, new_image_id: str) -> bool: 66 | return False 67 | 68 | def registry_implementation_name(self) -> str: 69 | raise NotImplemented 70 | 71 | @classmethod 72 | def instance(cls, stack_name: str, pulumi_user: str): 73 | pass 74 | 75 | @staticmethod 76 | def decode_credentials(encoded_token: str) -> RegistryCredentials: 77 | decoded = str(base64.b64decode(encoded_token), 'ascii') 78 | parts = decoded.split(':', 2) 79 | if len(parts) != 2: 80 | raise ValueError("Unexpected format for decoded ECR authorization token") 81 | username = parts[0] 82 | password = parts[1] 83 | return RegistryCredentials(username=username, password=password) 84 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/registries/do.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import List, Any 4 | from pulumi import Output, StackReference, ResourceOptions 5 | from pulumi_digitalocean import ContainerRegistryDockerCredentials 6 | 7 | from kic_util import pulumi_config 8 | from registries.base_registry import ContainerRegistry, RegistryCredentials 9 | 10 | 11 | class DigitalOceanContainerRegistry(ContainerRegistry): 12 | @classmethod 13 | def instance(cls, stack_name: str, pulumi_user: str) -> Output[ContainerRegistry]: 14 | super().instance(stack_name, pulumi_user) 15 | # Pull properties from the Pulumi project that defines the Digital Ocean repository 16 | container_registry_project_name = DigitalOceanContainerRegistry.project_name_from_do_dir( 17 | 'container-registry') 18 | container_registry_stack_ref_id = f"{pulumi_user}/{container_registry_project_name}/{stack_name}" 19 | stack_ref = StackReference(container_registry_stack_ref_id) 20 | container_registry_output = stack_ref.require_output('container_registry') 21 | registry_name_output = stack_ref.require_output('container_registry_name') 22 | 23 | def _docker_credentials() -> Output[str]: 24 | one_hour = 3_600 * 4 25 | registry_credentials = ContainerRegistryDockerCredentials(resource_name='do_docker_credentials', 26 | registry_name=registry_name_output, 27 | expiry_seconds=one_hour, 28 | write=True, 29 | opts=ResourceOptions(delete_before_replace=True)) 30 | return registry_credentials.docker_credentials 31 | 32 | def _make_instance(params: List[Any]) -> DigitalOceanContainerRegistry: 33 | container_registry = params[0] 34 | do_docker_creds = params[1] 35 | server_url = container_registry['server_url'] 36 | endpoint = container_registry['endpoint'] 37 | registry_url = f'{endpoint}/nginx-ingress' 38 | _credentials = DigitalOceanContainerRegistry._decode_docker_credentials(server_url, do_docker_creds) 39 | 40 | return cls(stack_name=stack_name, pulumi_user=pulumi_user, 41 | registry_url=registry_url, credentials=_credentials) 42 | 43 | return Output.all(container_registry_output, _docker_credentials()).apply(_make_instance) 44 | 45 | def registry_implementation_name(self) -> str: 46 | return 'Digital Ocean Container Registry' 47 | 48 | @staticmethod 49 | def project_name_from_do_dir(dirname: str): 50 | script_dir = os.path.dirname(os.path.abspath(__file__)) 51 | project_path = os.path.join(script_dir, '..', '..', '..', 'infrastructure', 'digitalocean', dirname) 52 | return pulumi_config.get_pulumi_project_name(project_path) 53 | 54 | @staticmethod 55 | def _decode_docker_credentials(server_url: str, 56 | docker_credentials_json: str) -> RegistryCredentials: 57 | credential_json = json.loads(docker_credentials_json) 58 | auths_json = credential_json['auths'] 59 | return ContainerRegistry.decode_credentials(auths_json[server_url]['auth']) 60 | 61 | 62 | CLASS = DigitalOceanContainerRegistry 63 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-image-push/registries/lke.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import List, Any 4 | from pulumi import Output, StackReference, ResourceOptions, log 5 | 6 | from kic_util import pulumi_config 7 | from registries.base_registry import ContainerRegistry, RegistryCredentials 8 | 9 | 10 | class LinodeHarborRegistry(ContainerRegistry): 11 | @classmethod 12 | def instance(cls, stack_name: str, pulumi_user: str) -> Output[ContainerRegistry]: 13 | super().instance(stack_name, pulumi_user) 14 | # Pull properties from the Pulumi project that defines the Linode Harbor repository 15 | container_registry_project_name = LinodeHarborRegistry.project_name_from_linode_dir('harbor') 16 | container_registry_stack_ref_id = f"{pulumi_user}/{container_registry_project_name}/{stack_name}" 17 | stack_ref = StackReference(container_registry_stack_ref_id) 18 | harbor_hostname_output = stack_ref.require_output('harbor_hostname') 19 | harbor_user_output = stack_ref.require_output('harbor_user') 20 | harbor_password_output = stack_ref.require_output('harbor_password') 21 | 22 | def _make_instance(params: List[Any]) -> LinodeHarborRegistry: 23 | hostname = params[0] 24 | username = params[1] 25 | password = params[2] 26 | 27 | registry_url = f'{hostname}/library/ingress-controller' 28 | credentials = RegistryCredentials(username=username, password=password) 29 | 30 | return cls(stack_name=stack_name, pulumi_user=pulumi_user, registry_url=registry_url, credentials=credentials) 31 | 32 | return Output.all(harbor_hostname_output, harbor_user_output, harbor_password_output).apply(_make_instance) 33 | 34 | @staticmethod 35 | def project_name_from_linode_dir(dirname: str): 36 | script_dir = os.path.dirname(os.path.abspath(__file__)) 37 | project_path = os.path.join(script_dir, '..', '..', '..', 'infrastructure', 'linode', dirname) 38 | return pulumi_config.get_pulumi_project_name(project_path) 39 | 40 | def registry_implementation_name(self) -> str: 41 | return 'Harbor' 42 | 43 | 44 | CLASS = LinodeHarborRegistry 45 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nginxinc/kic-reference-architectures/aa8ee9358ed65fcf1460e82b0d943b93652ec867/pulumi/python/utility/kic-pulumi-utils/kic_util/__init__.py -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/archive_download.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import gzip 3 | import os 4 | import shutil 5 | import tarfile 6 | import tempfile 7 | from git import Repo 8 | from typing import Optional 9 | from urllib import request, parse 10 | from kic_util.url_type import URLType 11 | 12 | 13 | class DownloadExtractError(RuntimeError): 14 | """Error class thrown when there is a problem downloading and extracting an archive""" 15 | url: Optional[str] 16 | temp_dir: Optional[str] 17 | 18 | def __init__(self, url: Optional[str], temp_dir: Optional[str]) -> None: 19 | self.url = url 20 | self.temp_dir = temp_dir 21 | 22 | def msg(self) -> str: 23 | msg = f'Unable to download and/or extract archive from [{self.url}] to directory [{self.temp_dir}]\n' \ 24 | f'Cause: {self.__cause__}' 25 | return msg 26 | 27 | def __str__(self) -> str: 28 | return self.msg() 29 | 30 | 31 | def download_and_extract_archive_from_url(url: str) -> str: 32 | parsed_url = parse.urlparse(url) 33 | archive_url_type = URLType.from_parsed_url(parsed_url) 34 | 35 | if archive_url_type == URLType.GENERAL_TAR_GZ: 36 | return download_and_extract_targz_archive_from_url(url=url, temp_prefix='archive_download_') 37 | elif archive_url_type == URLType.LOCAL_TAR_GZ: 38 | return download_and_extract_targz_archive_from_url(url=url, temp_prefix='archive_local_') 39 | elif archive_url_type == URLType.LOCAL_PATH: 40 | return parsed_url.path 41 | elif archive_url_type == URLType.GIT_REPO: 42 | return checkout_from_git(parsed_url=parsed_url, temp_prefix='archive_git_') 43 | 44 | raise ValueError(f'Unable to download archive for unsupported url: {url}') 45 | 46 | 47 | def download_and_extract_targz_archive_from_url(url: str, temp_prefix: Optional[str]) -> str: 48 | def download(extract_dir: tempfile): 49 | with request.urlopen(url) as response: 50 | with gzip.GzipFile(fileobj=response) as uncompressed: 51 | with tarfile.TarFile(fileobj=uncompressed) as tarball: 52 | tarball.extractall(path=extract_dir) 53 | 54 | try: 55 | temp_dir = extract_stream_into_temp_dir(extract_func=download, temp_prefix=temp_prefix) 56 | return str(temp_dir) 57 | except DownloadExtractError as e: 58 | e.url = url 59 | raise e 60 | except Exception as e: 61 | raise DownloadExtractError(url=url, temp_dir=None) from e 62 | 63 | 64 | def checkout_from_git(parsed_url: parse.ParseResult, temp_prefix: Optional[str]) -> str: 65 | # Rebuild the parsed URL without the fragment so that git understands it. 66 | url = clone_and_clean_parsed_url(parsed_url).geturl() 67 | tag = parsed_url.fragment 68 | 69 | def checkout(working_dir: tempfile): 70 | opts = ['--depth', '1'] 71 | 72 | if tag: 73 | opts.append('--branch') 74 | opts.append(tag) 75 | 76 | Repo.clone_from(url=url, to_path=working_dir, multi_options=opts) 77 | 78 | try: 79 | temp_dir = extract_stream_into_temp_dir(extract_func=checkout, temp_prefix=temp_prefix) 80 | return str(temp_dir) 81 | except DownloadExtractError as e: 82 | e.url = url 83 | raise e 84 | except Exception as e: 85 | raise DownloadExtractError(url=url, temp_dir=None) from e 86 | 87 | 88 | def clone_and_clean_parsed_url(parsed_url: parse.ParseResult) -> parse.ParseResult: 89 | """Clones the passed ParseResult object without a fragment and removes 90 | ssh scheme so that the resulting ParseResult object can covert to a git compatible URL. 91 | 92 | :rtype: parse.ParseResult 93 | :param parsed_url: URL object to clone 94 | :return: A new URL object without a fragment and/or without a scheme if the input scheme is 'ssh' 95 | """ 96 | 97 | if parsed_url.scheme == 'ssh': 98 | # noinspection PyArgumentList 99 | return parse.ParseResult(scheme='', 100 | netloc='', 101 | path=parsed_url.netloc + parsed_url.path, 102 | query=parsed_url.query, 103 | fragment='', 104 | params='') 105 | 106 | # noinspection PyArgumentList 107 | return parse.ParseResult(scheme=parsed_url.scheme, 108 | netloc=parsed_url.netloc, 109 | path=parsed_url.path, 110 | query=parsed_url.query, 111 | fragment='', 112 | params='') 113 | 114 | 115 | def extract_stream_into_temp_dir(extract_func, temp_prefix: Optional[str]) -> str: 116 | temp_dir = tempfile.mkdtemp(prefix=temp_prefix) 117 | # Limit access of directory to only the creating user 118 | os.chmod(path=temp_dir, mode=0o0700) 119 | # Delete extracted directory upon exit, so that we don't have cruft lying around 120 | atexit.register(lambda: shutil.rmtree(temp_dir)) 121 | 122 | # Download archive 123 | try: 124 | extract_func(temp_dir) 125 | except Exception as e: 126 | raise DownloadExtractError(url=None, temp_dir=temp_dir) from e 127 | 128 | return str(temp_dir) 129 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/docker_image_name.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from typing import Optional, Pattern 4 | 5 | IMAGE_NAME_REGX: str = r'^(?P.+):(?P.*)$' 6 | IMAGE_NAME_MATCHER: Pattern[str] = re.compile(IMAGE_NAME_REGX) 7 | 8 | 9 | class DockerImageNameError(RuntimeError): 10 | """Error class thrown when there is a problem parsing image names""" 11 | pass 12 | 13 | 14 | class DockerImageName: 15 | tag: str 16 | repository: str 17 | id: str 18 | 19 | def __init__(self, repository: str, tag: str, image_id: Optional[str] = None): 20 | if ':' in tag: 21 | raise DockerImageNameError(f'invalid tag - contains colon: {tag}') 22 | 23 | self.repository = repository 24 | self.tag = tag 25 | self.id = image_id 26 | 27 | @staticmethod 28 | def from_name(image_name: str, image_id: Optional[str] = None): 29 | if ':' not in image_name: 30 | raise DockerImageNameError(f'invalid image name - no tag specified: {image_name}') 31 | 32 | matches = IMAGE_NAME_MATCHER.match(image_name) 33 | 34 | if not matches: 35 | raise DockerImageNameError(f'unable to parse image name: {image_name}') 36 | 37 | values = matches.groupdict() 38 | 39 | repository = values.get('repository') 40 | tag = values.get('tag') 41 | 42 | return DockerImageName(repository=repository, tag=tag, image_id=image_id) 43 | 44 | def __str__(self) -> str: 45 | return f'{self.repository}:{self.tag}' 46 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/external_process.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from typing import Optional, Dict 3 | 4 | 5 | class ExternalProcessExecError(RuntimeError): 6 | """Error when an external process fails to run successfully""" 7 | def __init__(self, cmd: str, message: str): 8 | self.cmd = cmd 9 | self.message = message 10 | super().__init__(f"{message} when running: {cmd}") 11 | 12 | 13 | def run(cmd: str, suppress_error=False, env: Optional[Dict[str, str]] = None) -> (str, str): 14 | """Runs an external command and returns back its stdout and stderr""" 15 | 16 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) 17 | (res, err) = proc.communicate() 18 | res = res.decode(encoding="utf-8", errors="ignore") 19 | err = err.decode(encoding="utf-8", errors="ignore") 20 | 21 | if proc.returncode != 0 and not suppress_error: 22 | msg = f"Failed to execute external process: {cmd}\n{res}\nError: {err}" 23 | raise ExternalProcessExecError(msg, cmd) 24 | 25 | return res, err 26 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/pulumi_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os import path 3 | import yaml 4 | from kic_util import external_process 5 | 6 | 7 | class PulumiConfigError(RuntimeError): 8 | """Base error class for Pulumi related errors""" 9 | 10 | def __init__(self, file, message): 11 | self.file = file 12 | self.message = message 13 | super().__init__(f"{message} in file: {file}") 14 | 15 | 16 | class InvalidPulumiConfigError(PulumiConfigError): 17 | """Error when Pulumi config files have an invalid syntax""" 18 | pass 19 | 20 | 21 | class PulumiExecError(RuntimeError): 22 | """Error when a Pulumi CLI command can't be run""" 23 | 24 | def __init__(self, message): 25 | self.message = message 26 | super().__init__(message) 27 | 28 | 29 | def get_pulumi_project_name(directory: str) -> str: 30 | """Reads the project name from the Pulumi.yaml file located in the specified directory""" 31 | config_path = path.join(directory, 'Pulumi.yaml') 32 | with open(config_path, 'r') as stream: 33 | config_data = yaml.safe_load(stream) 34 | if type(config_data) is not dict: 35 | raise InvalidPulumiConfigError(file=config_path, 36 | message=f"Configuration is not in key/value format [type={type(config_data)}") 37 | if config_data is None: 38 | raise InvalidPulumiConfigError(file=config_path, 39 | message="No configuration data found") 40 | if len(config_data) == 0: 41 | raise InvalidPulumiConfigError(file=config_path, 42 | message="No configuration entries found") 43 | if 'name' not in config_data: 44 | raise InvalidPulumiConfigError(file=config_path, 45 | message="No project name specified") 46 | return config_data['name'] 47 | 48 | 49 | def get_pulumi_user() -> str: 50 | """Gets the current Pulumi user by executing the pulumi CLI tool""" 51 | try: 52 | env = dict(os.environ) 53 | env['PULUMI_SKIP_UPDATE_CHECK'] = 'true' 54 | 55 | user, _ = external_process.run(cmd='pulumi --non-interactive whoami', env=env) 56 | except RuntimeError as e: 57 | raise PulumiExecError("Unable to query pulumi username") from e 58 | return user.strip() 59 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/test_archive_download.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from urllib import parse 4 | from kic_util.archive_download import clone_and_clean_parsed_url 5 | 6 | 7 | class TestArchiveDownload(unittest.TestCase): 8 | without_schema = None 9 | with_schema = None 10 | 11 | def test_clone_and_clean_parsed_url_https_scheme(self): 12 | url = 'https://github.com/nginxinc/kubernetes-ingress.git' 13 | parsed_result = parse.urlparse(url=url) 14 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 15 | self.with_schema = cleaned_result 16 | actual = cleaned_result.geturl() 17 | expected = 'https://github.com/nginxinc/kubernetes-ingress.git' 18 | 19 | self.assertEqual(expected, actual) 20 | 21 | def test_clone_and_clean_parsed_url_https_scheme_and_fragment(self): 22 | url = 'https://github.com/nginxinc/kubernetes-ingress.git#v1.11.2' 23 | parsed_result = parse.urlparse(url=url) 24 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 25 | self.with_schema = cleaned_result 26 | actual = cleaned_result.geturl() 27 | expected = 'https://github.com/nginxinc/kubernetes-ingress.git' 28 | 29 | self.assertEqual(expected, actual) 30 | 31 | def test_clone_and_clean_parsed_url_ssh_scheme(self): 32 | url = 'ssh://git@github.com:nginxinc/kubernetes-ingress.git' 33 | parsed_result = parse.urlparse(url=url) 34 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 35 | self.with_schema = cleaned_result 36 | actual = cleaned_result.geturl() 37 | expected = 'git@github.com:nginxinc/kubernetes-ingress.git' 38 | 39 | self.assertEqual(expected, actual) 40 | 41 | def test_clone_and_clean_parsed_url_ssh_scheme_and_fragment(self): 42 | url = 'ssh://git@github.com:nginxinc/kubernetes-ingress.git#v1.11.2' 43 | parsed_result = parse.urlparse(url=url) 44 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 45 | self.with_schema = cleaned_result 46 | actual = cleaned_result.geturl() 47 | expected = 'git@github.com:nginxinc/kubernetes-ingress.git' 48 | 49 | self.assertEqual(expected, actual) 50 | 51 | def test_clone_and_clean_parsed_url_without_scheme(self): 52 | url = 'git@github.com:nginxinc/kubernetes-ingress.git' 53 | parsed_result = parse.urlparse(url=url) 54 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 55 | self.without_schema = cleaned_result 56 | actual = cleaned_result.geturl() 57 | expected = 'git@github.com:nginxinc/kubernetes-ingress.git' 58 | 59 | self.assertEqual(expected, actual) 60 | 61 | def test_clone_and_clean_parsed_url_without_scheme_and_fragment(self): 62 | url = 'git@github.com:nginxinc/kubernetes-ingress.git#v1.11.2' 63 | parsed_result = parse.urlparse(url=url) 64 | cleaned_result = clone_and_clean_parsed_url(parsed_url=parsed_result) 65 | self.without_schema = cleaned_result 66 | actual = cleaned_result.geturl() 67 | expected = 'git@github.com:nginxinc/kubernetes-ingress.git' 68 | 69 | self.assertEqual(expected, actual) 70 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/test_docker_image_name.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from kic_util import docker_image_name 3 | 4 | 5 | class TestDockerImageName(unittest.TestCase): 6 | def test_from_name_with_tag_with_id(self): 7 | image_name = 'docker.io/nginx/nginx-ingress:1.12.1' 8 | id = 'sha256:6fafafb2227fef917a61b91d127977adf5b5f1d615c3cf7ac37eb6e223771664' 9 | docker_image = docker_image_name.DockerImageName.from_name(image_name=image_name, image_id=id) 10 | expected_repository = 'docker.io/nginx/nginx-ingress' 11 | expected_tag = '1.12.1' 12 | self.assertEqual(expected_repository, docker_image.repository) 13 | self.assertEqual(expected_tag, docker_image.tag) 14 | self.assertEqual(id, docker_image.id) 15 | 16 | def test_from_name_with_tag_with_no_id(self): 17 | image_name = 'docker.io/nginx/nginx-ingress:1.12.1' 18 | docker_image = docker_image_name.DockerImageName.from_name(image_name=image_name) 19 | expected_repository = 'docker.io/nginx/nginx-ingress' 20 | expected_tag = '1.12.1' 21 | self.assertEqual(expected_repository, docker_image.repository) 22 | self.assertEqual(expected_tag, docker_image.tag) 23 | self.assertIsNone(docker_image.id) 24 | 25 | def test_from_name_with_no_tag_with_no_id(self): 26 | image_name = 'docker.io/nginx/nginx-ingress' 27 | self.assertRaises(docker_image_name.DockerImageNameError, 28 | lambda: docker_image_name.DockerImageName.from_name(image_name=image_name)) 29 | 30 | def test_from_name_with_port_in_repository(self): 31 | image_name = 'myregistryhost:5000/fedora/nginx-kic:1.10.3-alpine' 32 | docker_image = docker_image_name.DockerImageName.from_name(image_name=image_name) 33 | expected_repository = 'myregistryhost:5000/fedora/nginx-kic' 34 | expected_tag = '1.10.3-alpine' 35 | self.assertEqual(expected_repository, docker_image.repository) 36 | self.assertEqual(expected_tag, docker_image.tag) 37 | self.assertIsNone(docker_image.id) 38 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/test_pulumi_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import tempfile 4 | from kic_util import pulumi_config 5 | 6 | 7 | class TestPulumiConfig(unittest.TestCase): 8 | 9 | def test_get_pulumi_project_name_with_non_existent_dir(self): 10 | with self.assertRaises(FileNotFoundError): 11 | bad_dir = '/nonsense-1-2-7' 12 | pulumi_config.get_pulumi_project_name(bad_dir) 13 | 14 | def test_get_pulumi_project_name_with_empty_config(self): 15 | tmp_dir = tempfile.TemporaryDirectory() 16 | try: 17 | config_file_path = os.path.join(tmp_dir.name, 'Pulumi.yaml') 18 | with open(config_file_path, 'w') as stream: 19 | stream.write("\n") 20 | with self.assertRaises(pulumi_config.InvalidPulumiConfigError): 21 | pulumi_config.get_pulumi_project_name(tmp_dir.name) 22 | finally: 23 | tmp_dir.cleanup() 24 | 25 | def test_get_pulumi_project_name_with_no_name_attribute(self): 26 | tmp_dir = tempfile.TemporaryDirectory() 27 | try: 28 | config_file_path = os.path.join(tmp_dir.name, 'Pulumi.yaml') 29 | with open(config_file_path, 'w') as stream: 30 | stream.write("""runtime: 31 | name: python 32 | options: 33 | virtualenv: venv 34 | config: config 35 | """) 36 | with self.assertRaises(pulumi_config.InvalidPulumiConfigError): 37 | pulumi_config.get_pulumi_project_name(tmp_dir.name) 38 | finally: 39 | tmp_dir.cleanup() 40 | 41 | def test_get_pulumi_user_cant_find_cmd(self): 42 | try: 43 | pulumi_config.get_pulumi_user() 44 | except pulumi_config.PulumiExecError as e: 45 | if e.message.startswith('PULUMI_ACCESS_TOKEN must be set for login during non-interactive CLI sessions'): 46 | self.skipTest('Skipping error because we are running in an environment that does not ' 47 | f'have the Pulumi CLI configured. Error: {e.message}') 48 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/test_url_type.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import os 3 | import shutil 4 | import tempfile 5 | import unittest 6 | 7 | from kic_util.url_type import URLType 8 | 9 | 10 | class TestURLType(unittest.TestCase): 11 | def test_identify_url_type_remote_http(self): 12 | url = 'http://github.com/nginxinc/kubernetes-ingress/archive/refs/tags/v1.11.1.tar.gz' 13 | expected = URLType.GENERAL_TAR_GZ 14 | actual = URLType.from_url(url) 15 | self.assertEqual(expected, actual) 16 | 17 | def test_identify_url_type_remote_https(self): 18 | url = 'https://github.com/nginxinc/kubernetes-ingress/archive/refs/tags/v1.11.1.tar.gz' 19 | expected = URLType.GENERAL_TAR_GZ 20 | actual = URLType.from_url(url) 21 | self.assertEqual(expected, actual) 22 | 23 | def test_identify_url_type_remote_ftp(self): 24 | url = 'ftp://github.com/nginxinc/kubernetes-ingress/archive/refs/tags/v1.11.1.tar.gz' 25 | expected = URLType.GENERAL_TAR_GZ 26 | actual = URLType.from_url(url) 27 | self.assertEqual(expected, actual) 28 | 29 | def test_identify_url_type_local_file_with_scheme(self): 30 | url = 'file:///tmp/v1.11.1.tar.gz' 31 | expected = URLType.LOCAL_TAR_GZ 32 | actual = URLType.from_url(url) 33 | self.assertEqual(expected, actual) 34 | 35 | def test_identify_url_type_local_file_without_scheme(self): 36 | _, local_path = tempfile.mkstemp(prefix='unit_test_file', suffix='.tar.gz', text=True) 37 | atexit.register(lambda: os.unlink(local_path)) 38 | expected = URLType.LOCAL_TAR_GZ 39 | actual = URLType.from_url(local_path) 40 | self.assertEqual(expected, actual, f'path [{local_path}] was misidentified') 41 | 42 | def test_identify_url_type_local_dir_with_scheme(self): 43 | url = 'file:///usr/local/src/kic' 44 | expected = URLType.LOCAL_PATH 45 | actual = URLType.from_url(url) 46 | self.assertEqual(expected, actual, f'url [{url}] was misidentified') 47 | 48 | def test_identify_url_type_local_dir_without_scheme(self): 49 | local_path = tempfile.mkdtemp(prefix='unit_test_dir') 50 | atexit.register(lambda: shutil.rmtree(local_path)) 51 | expected = URLType.LOCAL_PATH 52 | actual = URLType.from_url(local_path) 53 | self.assertEqual(expected, actual, f'path [{local_path}] was misidentified') 54 | 55 | def test_identify_url_type_github_https_without_tag(self): 56 | url = 'https://github.com/nginxinc/kubernetes-ingress.git' 57 | expected = URLType.GIT_REPO 58 | actual = URLType.from_url(url) 59 | self.assertEqual(expected, actual) 60 | 61 | def test_identify_url_type_github_https_with_tag(self): 62 | url = 'https://github.com/nginxinc/kubernetes-ingress.git#v1.12.0' 63 | expected = URLType.GIT_REPO 64 | actual = URLType.from_url(url) 65 | self.assertEqual(expected, actual) 66 | 67 | def test_identify_url_type_github_no_schema_without_tag(self): 68 | url = 'git@github.com:nginxinc/kubernetes-ingress.git' 69 | expected = URLType.GIT_REPO 70 | actual = URLType.from_url(url) 71 | self.assertEqual(expected, actual) 72 | 73 | def test_identify_url_type_github_no_schema_with_tag(self): 74 | url = 'git@github.com:nginxinc/kubernetes-ingress.git#v1.11.3' 75 | expected = URLType.GIT_REPO 76 | actual = URLType.from_url(url) 77 | self.assertEqual(expected, actual) 78 | 79 | def test_identify_url_type_github_ssh_without_tag(self): 80 | url = 'ssh://git@github.com:nginxinc/kubernetes-ingress.git' 81 | expected = URLType.GIT_REPO 82 | actual = URLType.from_url(url) 83 | self.assertEqual(expected, actual) 84 | 85 | def test_identify_url_type_github_ssh_with_tag(self): 86 | url = 'ssh://git@github.com:nginxinc/kubernetes-ingress.git#v1.12.0' 87 | expected = URLType.GIT_REPO 88 | actual = URLType.from_url(url) 89 | self.assertEqual(expected, actual) 90 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/kic_util/url_type.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from enum import Enum 3 | from urllib import parse 4 | 5 | 6 | class URLType(Enum): 7 | GENERAL_TAR_GZ = 1 8 | LOCAL_TAR_GZ = 2 9 | LOCAL_PATH = 4 10 | GIT_REPO = 8 11 | UNKNOWN = 16 12 | 13 | @staticmethod 14 | def from_url(url: str) -> Enum: 15 | """ 16 | :rtype: URLType 17 | """ 18 | result = parse.urlparse(url) 19 | return URLType.from_parsed_url(result) 20 | 21 | @staticmethod 22 | def from_parsed_url(result: parse.ParseResult) -> Enum: 23 | """ 24 | :rtype: URLType 25 | """ 26 | is_tarball = result.path.endswith('.tar.gz') 27 | 28 | if result.scheme == 'file': 29 | return URLType.LOCAL_TAR_GZ if is_tarball else URLType.LOCAL_PATH 30 | 31 | if result.path.endswith('.git'): 32 | return URLType.GIT_REPO 33 | 34 | if result.scheme == '': 35 | path = pathlib.Path(result.path) 36 | if path.is_dir(): 37 | return URLType.LOCAL_PATH 38 | elif path.is_file() and is_tarball: 39 | return URLType.LOCAL_TAR_GZ 40 | 41 | if is_tarball: 42 | return URLType.GENERAL_TAR_GZ 43 | 44 | return URLType.UNKNOWN 45 | 46 | -------------------------------------------------------------------------------- /pulumi/python/utility/kic-pulumi-utils/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='kic-pulumi-utils', 4 | description='Shared utilities functions for KIC stand up using Pulumi', 5 | license='Apache-2.0', 6 | setup_requires=['setuptools-git-versioning'], 7 | version_config=True, 8 | packages=['kic_util'], 9 | install_requires=[ 10 | 'pyyaml>=5.3.1,<6.0', 'passlib>=1.7.4,<2.0.0', 'GitPython>=3.1.18,<3.2.0' 11 | ]) 12 | --------------------------------------------------------------------------------