From 222017e554deab1268d4a1f493ba9e7dc4d71597 Mon Sep 17 00:00:00 2001 From: Piotr Mossakowski Date: Fri, 23 Dec 2022 11:19:05 +0100 Subject: [PATCH] ci: import getindata/docker-image-template --- .github/workflows/pr_opened.yml | 119 +++++++++++++++++++++++++++++ .github/workflows/pr_title.yml | 56 ++++++++++++++ .github/workflows/release.yml | 91 +++++++++++++++++++++++ .gitignore | 1 + CODE_OF_CONDUCT.md | 128 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 37 +++++++++ README.md | 100 +++++++++++++++++++++++++ app/.dockerignore | 16 ++++ app/Dockerfile | 92 +++++++++++++++++++++++ app/check-gitlab-approvals.sh | 80 ++++++++++++++++++++ app/docker-entrypoint.sh | 46 ++++++++++++ approval-config-example.yaml | 8 ++ 12 files changed, 774 insertions(+) create mode 100644 .github/workflows/pr_opened.yml create mode 100644 .github/workflows/pr_title.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 app/.dockerignore create mode 100644 app/Dockerfile create mode 100755 app/check-gitlab-approvals.sh create mode 100755 app/docker-entrypoint.sh create mode 100644 approval-config-example.yaml diff --git a/.github/workflows/pr_opened.yml b/.github/workflows/pr_opened.yml new file mode 100644 index 0000000..fd76b45 --- /dev/null +++ b/.github/workflows/pr_opened.yml @@ -0,0 +1,119 @@ +name: build test scan docker images + +on: + pull_request: + branches: + - 'main' + - 'master' +# paths: +# - ${{ env.DOCKERFILE_PATH }}/Dockerfile + +env: + DOCKERFILE_PATH: app + DOCKERFILE_TAG: ${{ github.event.pull_request.head.sha }} + REGISTRY_PATH: gcr.io/getindata-images-public/docker-atlantis + REGISTRY_TYPE: "gcr.io" # If not set then will default to Docker Hub + REGISTRY_USERNAME: _json_key + +jobs: + buildtestscan: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 100 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.2.1 + + - name: Cache Docker layers + uses: actions/cache@v3.2.0 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ env.DOCKERFILE_TAG }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to registry "${{ env.REGISTRY_TYPE }}" + uses: docker/login-action@v2.1.0 + with: + registry: ${{ env.REGISTRY_TYPE }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3.2.0 + with: + context: "${{ env.DOCKERFILE_PATH }}" + push: true + tags: "${{ env.REGISTRY_PATH }}:${{ env.DOCKERFILE_TAG }}" + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Run Checkov action + id: checkov + uses: bridgecrewio/checkov-action@v12.1347.0 + with: + quiet: true # optional: display only failed checks + soft_fail: true # optional: do not return an error code if there are failed checks + framework: dockerfile + output_format: github_failed_only + log_level: WARNING # optional: set log level. Default WARNING + dockerfile_path: "${{ env.DOCKERFILE_PATH }}/Dockerfile" # path to the Dockerfile + + - name: Show Checkov results + uses: actions-ecosystem/action-create-comment@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + body: | + ## Checkov + ${{ env.CHECKOV_RESULTS }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.8.0 + env: + TRIVY_USERNAME: ${{ env.REGISTRY_USERNAME }} + TRIVY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + with: + image-ref: "${{ env.REGISTRY_PATH }}:${{ env.DOCKERFILE_TAG }}" + format: 'json' + exit-code: '0' + output: results_trivy.json + ignore-unfixed: false + vuln-type: 'os,library' + severity: 'CRITICAL' + + - name: Parse Trivy results + run: | + echo "| PkgName | InstalledVersion | Severity | Title | CVE URL | + | ------ | ------ | ------ | ------ | ------ |" > results_trivy.md + cat results_trivy.json | jq -r '.Results[].Vulnerabilities[] | [.PkgName, .InstalledVersion, .Severity, .Title, .PrimaryURL]| @tsv' | + awk ' + BEGIN{ FS = "\t" } # Set field separator to tab + { + # Step 2: Replace all tab characters with pipe characters + gsub("\t", " | ", $0) + + # Step 3: Print fields with Markdown table formatting + printf "| %s |\n", $0 + }' >> results_trivy.md + + - name: Export Trivy results + run: | + echo 'TRIVY_RESULTS<> $GITHUB_ENV + cat results_trivy.md >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + - name: Show Trivy results + uses: actions-ecosystem/action-create-comment@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + body: | + ## Trivy + ${{ env.TRIVY_RESULTS }} + + - name: Move cache + if: always() # always run even if the previous step fails + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache \ No newline at end of file diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml new file mode 100644 index 0000000..ab62f75 --- /dev/null +++ b/.github/workflows/pr_title.yml @@ -0,0 +1,56 @@ +name: validate PR title + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + lint_pr: + name: Validate PR title + runs-on: ubuntu-22.04 + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + docs + style + refactor + test + ci + chore + build + requireScope: false + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The description "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the description + doesn't start with an uppercase character. + wip: true + + - uses: marocchino/sticky-pull-request-comment@v2.3.1 + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Our pull requests titles follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary) + + Details: + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + - uses: marocchino/sticky-pull-request-comment@v2.3.1 + if: ${{ steps.lint_pr_title.outputs.error_message == null }} + with: + header: pr-title-lint-error + delete: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cb2cd1f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +name: create new release with changelog + +on: + pull_request: + branches: + - 'main' + - 'master' + types: [closed] + +env: + DOCKERFILE_PATH: app + DOCKERFILE_TAG: ${{ github.event.pull_request.head.sha }} + REGISTRY_PATH: gcr.io/getindata-images-public/docker-atlantis + REGISTRY_TYPE: "gcr.io" # If not set then will default to Docker Hub + REGISTRY_USERNAME: _json_key + +jobs: + release: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 100 + + - name: Check release label + id: release-label + uses: actions-ecosystem/action-release-label@v1.2.0 + if: ${{ github.event.pull_request.merged == true }} + + - name: Get latest tag + id: get-latest-tag + uses: actions-ecosystem/action-get-latest-tag@v1.6.0 + if: ${{ steps.release-label.outputs.level != null }} + + - name: Bump semantic version + id: bump-semver + uses: actions-ecosystem/action-bump-semver@v1 + if: ${{ steps.release-label.outputs.level != null }} + with: + current_version: ${{ steps.get-latest-tag.outputs.tag }} + level: ${{ steps.release-label.outputs.level }} + + - name: Tag release + id: tag-relese + uses: actions-ecosystem/action-push-tag@v1 + if: ${{ steps.release-label.outputs.level != null }} + with: + tag: ${{ steps.bump-semver.outputs.new_version }} + message: "${{ steps.bump-semver.outputs.new_version }}: PR #${{ github.event.pull_request.number }} ${{ github.event.pull_request.title }}" + + - name: Login to registry ${{ env.REGISTRY_TYPE }} + uses: docker/login-action@v2.1.0 + if: ${{ steps.release-label.outputs.level != null }} + with: + registry: ${{ env.REGISTRY_TYPE }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Tag final docker image + if: ${{ steps.release-label.outputs.level != null }} + run: | + docker pull ${{ env.REGISTRY_PATH }}:${{ github.event.pull_request.head.sha }} + docker tag ${{ env.REGISTRY_PATH }}:${{ github.event.pull_request.head.sha }} ${{ env.REGISTRY_PATH }}:${{ steps.bump-semver.outputs.new_version }} + docker push ${{ env.REGISTRY_PATH }}:${{ steps.bump-semver.outputs.new_version }} + + - name: Generate new release with changelog + id: release-with-changelog + uses: fregante/release-with-changelog@v3.6.0 + if: ${{ steps.bump-semver.outputs.new_version != null }} + with: + token: "${{ secrets.GITHUB_TOKEN }}" + exclude: '^chore|^docs|^ci|^build|^refactor|^style|^v?\d+\.\d+\.\d+' + tag: "${{ steps.bump-semver.outputs.new_version }}" + title: "Version ${{ steps.bump-semver.outputs.new_version }}" + commit-template: "- {title} ← {hash}" + skip-on-empty: true + template: | + ### Changelog + + {commits} + + {range} + + - name: Comment PR + id: add-comment + uses: actions-ecosystem/action-create-comment@v1 + if: ${{ steps.bump-semver.outputs.new_version != null }} + with: + github_token: "${{ secrets.GITHUB_TOKEN }}" + body: | + The new version [${{ steps.bump-semver.outputs.new_version }}](https://github.com/${{ github.repository }}/releases/tag/${{ steps.bump-semver.outputs.new_version }}) has been released :tada: \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58c4bae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +### Examples: https://github.com/github/gitignore \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e96966e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hello@getindata.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..47c787b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Update the README.md with details of changes including new [example Dockerfile](./app) if appropriate. +2. Once all outstanding comments and checklist items have been addressed, your contribution will be merged! +3. Merged PRs will be included in the next release. The module maintainers take care of updating the CHANGELOG as they merge. + +## Checklists for contributions + +- [ ] Add [semantics prefix](#semantic-pull-requests) to your PR or Commits (at least one of your commit groups) +- [ ] CI tests are passing +- [ ] README.md has been updated after any changes. + +## Semantic Pull Requests + +To generate changelog, Pull Requests or Commits must have semantic and must follow [conventional specs](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#rules) below: + +- `feat:` for new features +- `fix:` for bug fixes +- `perf:` for performance improvements +- `docs:` for documentation and examples +- `style`: for formatting changes +- `refactor:` for refactoring production code +- `test:` for adding missing tests +- `ci:` for CI purpose +- `chore:` for chores stuff +- `build:` for updating build configuration + +`chore docs ci build refactor style` prefixes are skipped during changelog generation. + +They can be used for `chore: update changelog` commit message by example. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddd800f --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Docker Atlantis Image + +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/getindata/docker-atlantis?arch=amd64&logo=docker&sort=date&style=for-the-badge) +![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/getindata/docker-atlantis?logo=docker&sort=date&style=for-the-badge) +![Docker Pulls](https://img.shields.io/docker/pulls/getindata/docker-atlantis?logo=docker&style=for-the-badge) + + +![Docker](https://badgen.net/badge/icon/docker?icon=docker&label) +![License](https://badgen.net/github/license/getindata/docker-atlantis/) +![Release](https://badgen.net/github/release/getindata/docker-atlantis/) + + +

+ +

We help companies turn their data into assets

+

+ +That custom `atlantis` docker image was created in order to install few helpful tools into "stock" solution: + +- `terragrunt-atlantis-config` - script that dynamically generates `atlantis.yaml` for terragrunt configurations +- `checkov` - security and "best-practice" scanner (static code analysis) +- `asdf` - version manager used to install needed packeges and versions +- `terragrunt` (via asdf) - thin terraform wrapper +- `terraform` (via asdf) - IaC automation +- `helm` (via asdf) - k8s package manager used by `helm` terraform provider +- `kubectl` (via asdf) - k8s CLI tool used by `kubernetes` terraform provider +- `jq` (via asdf) - command line JSON parser +- `yq` (via asdf) - command like YAML parser +- `glab` (via asdf) - GitLab CLI client + +Files found in the repo: + +- `Dockerfile` is based on an official atlantis docker file () with some additional tweaks (asdf installation and configuration) +- `docker-entrypoint.sh` is based on original file from atlantis repo with additional tweaks like invoking `bash` to run `atlantis` (due to `asdf` needs) +- `check-gitlab-approvals.sh` is a script, intended to work around GitLab CE repository security limitations (CODEOWNERS, allowed approvers, etc.) +- `approval-config-example.yaml` is a sample approver config used by `check-gitlab-approvers.sh` script + +--- + +## Work around Free GitLab limitations + +Free versions of all major VCS systems (GitHub, GitLab, Bitbucket) introduce a set of limitations that should encourage it's users to pay for the service. One of those limitations is no `CODEOWNERS` support +and no ability to configure "allowed approvers" in free repositories. + +Since Atlantis security depends on VCS level reviews (every approved MR/PR can be `atlantis apply`ed) it is crucial to somehow workaround this limitations. + +We use hosted GitLab as our primary VCS in GetInData, also self-hosted version of GitLab is very popular among our clients. We're also big fans of Atlantis and engineers in the same time - which took us to obvious conclusions - +we should create a solution that allows our clients to use self-hosted GitLab CE and Atlantis securely. + +As a result we created a simple bash script [check-gitlab-approval.sh](check-gitlab-approvals.sh) that uses GitLab CLI called `glab` and few other popular bash tools to verify MR approvals. Script's configuration is stored in +yaml format and can be mounted/saved into the image or passed via environment variable, example configuration can be found [here](approval-config-example.yaml). + +This script is intended to be used as one of `apply` steps in custom Atlantis workflow, example: + +```yaml +workflows: + myworkflow: + plan: + steps: + - init + - plan + apply: + steps: + - run: check-gitlab-approvals.sh + - apply +``` + +During the execution, script checks if any of approving users are present in `approval-config.yaml` file. It fails (returns error) when none of approving users were allowed by configuration, blocking atlantis workflow (and apply step). + +--- + +## BUILDING + +```bash +docker build -t IMAGE_REPO/IMAGE_NAME:TAG . +docker push docker push IMAGE_REPO/IMAGE_NAME:TAG +``` + +## IMAGES + +## USAGE + +## CONTRIBUTING + +Contributions are very welcomed! + +Start by reviewing [contribution guide](CONTRIBUTING.md) and our [code of conduct](CODE_OF_CONDUCT.md). After that, start coding and ship your changes by creating a new PR. + +## LICENSE + +Apache 2 Licensed. See [LICENSE](LICENSE) for full details. + +## AUTHORS + + + + + + +Made with [contrib.rocks](https://contrib.rocks) diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..7ff1b4e --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,16 @@ +# git files +.git +.gitattributes +.gitignore +.cache + +# markdown files +*.md +LICENSE + +# docker files +docker-compose.yml +Dockerfile + +# Don't include example config files +approval-config-example.yaml \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..9800d14 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1 +ARG ATLANTIS_BASE_VERSION=2022.09.08 +# The runatlantis/atlantis-base is created by docker-base/Dockerfile. +FROM ghcr.io/runatlantis/atlantis-base:${ATLANTIS_BASE_VERSION} AS base + +# Default tool versions installed in that image +ARG ATLANTIS_VERSION=v0.19.8 +ARG ASDF_VERSION=v0.10.2 +ARG K8S_VERSION=1.25.0 +ARG HELM_VERSION=3.8.0 +ARG TF_VERSION=1.2.3 +ARG TG_VERSION=0.38.4 +ARG TG_ATLANTIS_VERSION=1.15.0 +ARG CONFTEST_VERSION=v0.34.0 +ARG GLAB_VERSION=1.22.0 +ARG JQ_VERSION=1.6 +ARG YQ_VERSION=4.9.8 + + +# Install awscli and checkov dependencies +# RUN apk --no-cache add grep zlib-dev libffi-dev gcompat groff openssl3-dev python3 python3-dev py3-pip build-base gcc && \ +# pip3 --no-cache-dir install wheel && \ +# pip3 --no-cache-dir install checkov && \ +# pip3 cache purge && \ +# apk --no-cache del python3-dev build-base gcc + +# Download and install Atlantis +RUN curl -LOs https://github.com/runatlantis/atlantis/releases/download/${ATLANTIS_VERSION}/atlantis_linux_amd64.zip && \ + unzip atlantis_linux_amd64.zip -d /usr/bin && \ + chmod a+x /usr/bin/atlantis && \ + rm atlantis_linux_amd64.zip + +# Download and install terragrunt-atlantis-config +RUN curl -LOs https://github.com/transcend-io/terragrunt-atlantis-config/releases/download/v${TG_ATLANTIS_VERSION}/terragrunt-atlantis-config_${TG_ATLANTIS_VERSION}_linux_amd64.tar.gz && \ + tar xzf terragrunt-atlantis-config_${TG_ATLANTIS_VERSION}_linux_amd64.tar.gz && \ + mv terragrunt-atlantis-config_${TG_ATLANTIS_VERSION}_linux_amd64/terragrunt-atlantis-config_${TG_ATLANTIS_VERSION}_linux_amd64 /usr/bin/terragrunt-atlantis-config && \ + chmod a+x /usr/bin/terragrunt-atlantis-config && \ + rm -rf terragrunt-atlantis-config_${TG_ATLANTIS_VERSION}_linux_amd64* + +# Download and install asdf, create .profile and source asdf inside +RUN gosu atlantis bash -l -c " \ + git clone --quiet https://github.com/asdf-vm/asdf.git /home/atlantis/.asdf --branch ${ASDF_VERSION} && \ + echo '. /home/atlantis/.asdf/asdf.sh' >> /home/atlantis/.profile && \ + chown atlantis.atlantis /home/atlantis/.profile && \ + chmod u+rw /home/atlantis/.profile" + +# Install all needed plugins +RUN gosu atlantis bash -l -c " \ + asdf plugin-add kubectl && \ + asdf plugin-add helm && \ + asdf plugin-add terragrunt && \ + asdf plugin-add terraform && \ + asdf plugin-add conftest && \ + asdf plugin-add glab && \ + asdf plugin-add jq && \ + asdf plugin-add yq" +# Install default versions and define them globally +RUN gosu atlantis bash -l -c " \ + cd /home/atlantis/ && \ + asdf install kubectl ${K8S_VERSION} && \ + asdf install helm ${HELM_VERSION} && \ + asdf install terraform ${TF_VERSION} && \ + asdf install terragrunt ${TG_VERSION} && \ + asdf install conftest ${CONFTEST_VERSION} && \ + asdf install glab ${GLAB_VERSION} && \ + asdf install jq ${JQ_VERSION} && \ + asdf install yq ${YQ_VERSION} && \ + asdf global kubectl ${K8S_VERSION} && \ + asdf global helm ${HELM_VERSION} && \ + asdf global terraform ${TF_VERSION} && \ + asdf global terragrunt ${TG_VERSION} && \ + asdf global conftest ${CONFTEST_VERSION} && \ + asdf global glab ${GLAB_VERSION} && \ + asdf global jq ${JQ_VERSION} && \ + asdf global yq ${YQ_VERSION}" + +# Additional cleanup +RUN rm -rf /tmp/* + +# Set atlantis login shell to bash +RUN sed -i s#atlantis:/sbin/nologin#atlantis:/bin/bash#g /etc/passwd + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY check-gitlab-approvals.sh /usr/local/bin/check-gitlab-approvals.sh + +RUN chmod a+x /usr/local/bin/docker-entrypoint.sh && \ + chmod a+x /usr/local/bin/check-gitlab-approvals.sh + +USER atlantis + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["server"] diff --git a/app/check-gitlab-approvals.sh b/app/check-gitlab-approvals.sh new file mode 100755 index 0000000..1cce37a --- /dev/null +++ b/app/check-gitlab-approvals.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Script checks if GitLab MR was approved by allowed user. It's intention is to work around +# free GitLab limitations (no CODEOWNERS, required approvals). +# +# This script makes great use of yq, jq, glab commands and they should be installed +# and availiable in $PATH priror script execution +# +# Approvers configuration can be set in number of ways: +# - By pointing `APPROVAL_CONFIG_PATH` environment variable to proper APPROVAL_CONFIG yaml file +# - By passing path to APPROVAL_CONFIG yaml file as the first input argument to this script +# - By directly populating `APPROVAL_CONFIG` environment variable with configuration +# +# APPROVAL_CONFIG is a `yaml` file with list of approved GitLab usernames per project, example: +# --- +# repository: +# REPOSITORY: +# allowed_approvers: +# - GITLAB_USERNAME +# getindata/devops/dummy-test-project: +# allowed_approvers: +# - john.doe +# getindata/devops/aws/gid-aws-terragrunt-platform: +# allowed_approvers: +# - jane.doe +# - example_username +# +# We assume that env variables are populated correctly (according to Atlantis dockumentation) +# and script is executed in proper custom workflow context: +# - ATLANTIS_GITLAB_TOKEN +# - HEAD_REPO_OWNER +# - HEAD_REPO_NAME +# - PULL_NUM + +# Get approval-config.yaml file path from: environment variable or 1st argument, +# use default when nothing is set +if [ $# -gt 0 ]; then + # if ARG is passed to script, use it + APPROVAL_CONFIG_PATH=${1} +elif [ ! -v APPROVAL_CONFIG_PATH ] || [ -z APPROVAL_CONFIG_PATH ]; then + # if env is not set or is empty set default + APPROVAL_CONFIG_PATH="/atlantis-data/approval-config.yaml" +fi + +# Variable needed for `glab` +export GITLAB_TOKEN=${ATLANTIS_GITLAB_TOKEN} + +REPO_NAME="${HEAD_REPO_OWNER}/${HEAD_REPO_NAME}" + +# Declare arrays +declare -a APPROVERS_ALLOWED +declare -a APPROVERS_GITLAB +declare -a RESULT + +# Get repository approvers configuration +if [ -v APPROVAL_CONFIG ] && [ ! -z APPROVAL_CONFIG ]; then + # If env is set and not empty - read approvers configuration yaml directly from ENV + APPROVERS_ALLOWED=($(yq --null-input eval "env(APPROVAL_CONFIG)" | yq eval ".repository.$REPO_NAME.allowed_approvers.[]" - | sort)) +elif [ -f $APPROVAL_CONFIG_PATH ]; then + # If file passed through APPROVAL_CONFIG_PATH env exists, try to parse it + APPROVERS_ALLOWED=($(yq eval ".repository.$REPO_NAME.allowed_approvers.[]" ${APPROVAL_CONFIG_PATH} | sort)) +else + printf "GitLab approval configuration file not found in '%s' nor in \$APPROVAL_CONFIG - will not continue...\n" ${APPROVAL_CONFIG_PATH} + exit 1; +fi + +# Get list of MR approvals from GitLab API +APPROVERS_GITLAB=($(jq -rn --arg x "${REPO_NAME}" '$x|@uri' | xargs -i glab api projects/{}/merge_requests/$PULL_NUM/approvals | jq '.approved_by[].user.username' | tr -d \" | sort)) + +# Find intersection between Allowed and Actual approvers of MR +RESULT=($(comm -12 <(printf '%s\n' ${APPROVERS_ALLOWED[@]}) <(printf '%s\n' ${APPROVERS_GITLAB[@]}))) + +if [ ${#RESULT[@]} -gt 0 ]; then + printf "MR approved correctly by [%s]\n" $(IFS=,; printf %s "${RESULT[*]}") +elif [ ${#APPROVERS_ALLOWED[@]} -eq 0 ]; then + printf "Missing or bad configuration for '$REPO_NAME' repo in approval configuration - will not continue...\n" + exit 1; +else + printf "Your MR has to be approved by at least one of those users [%s] to continue !!!\n" $(IFS=,; printf %s "${APPROVERS_ALLOWED[*]}") + exit 1; +fi diff --git a/app/docker-entrypoint.sh b/app/docker-entrypoint.sh new file mode 100755 index 0000000..049f302 --- /dev/null +++ b/app/docker-entrypoint.sh @@ -0,0 +1,46 @@ +#!/usr/bin/dumb-init /bin/sh +set -e + +# Modified: https://github.com/hashicorp/docker-consul/blob/2c2873f9d619220d1eef0bc46ec78443f55a10b5/0.X/docker-entrypoint.sh + +# If the user is trying to run atlantis directly with some arguments, then +# pass them to atlantis. +if [ "${1:0:1}" = '-' ]; then + set -- atlantis "@" +fi + +# If the user is running an atlantis subcommand (ex. server) then we want to prepend +# atlantis as the first arg to exec. To detect if they're running a subcommand +# we take the potential subcommand and run it through atlantis help {subcommand}. +# If the output contains "atlantis subcommand" then we know it's a subcommand +# since the help output contains that string. For anything else (ex. sh) +# it won't contain that string. +# NOTE: We use grep instead of the exit code since help always returns 0. +if atlantis help "$1" 2>&1 | grep -q "atlantis $1"; then + # We can't use the return code to check for the existence of a subcommand, so + # we have to use grep to look for a pattern in the help output. + set -- bash --login -c "atlantis $@" +fi + +# If the current uid running does not have a user create one in /etc/passwd +if ! whoami &> /dev/null; then + if [ -w /etc/passwd ]; then + echo "${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:/home/atlantis:/sbin/bash" >> /etc/passwd + fi +fi + +# If we're running as root and we're trying to execute atlantis then we use +# gosu to step down from root and run as the atlantis user. +# In OpenShift, containers are run as a random users so we don't need to use gosu. +if [[ $(id -u) == 0 ]] && [[ "$1" = 'atlantis' ]]; then + # If requested, set the capability to bind to privileged ports before + # we drop to the non-root user. Note that this doesn't work with all + # storage drivers (it won't work with AUFS). + if [ ! -z ${ATLANTIS_ALLOW_PRIVILEGED_PORTS+x} ]; then + setcap "cap_net_bind_service=+ep" /bin/atlantis + fi + + set -- gosu atlantis "$@" +fi + +exec "$@" \ No newline at end of file diff --git a/approval-config-example.yaml b/approval-config-example.yaml new file mode 100644 index 0000000..b6289bd --- /dev/null +++ b/approval-config-example.yaml @@ -0,0 +1,8 @@ +repository: + owner/dummy-test-project: + allowed_approvers: + - john.doe + organization/test/test-repo: + allowed_approvers: + - jane.doe + - example.user