diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d143c2d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +env_make diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..01ea10f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: generic + +services: + - docker + +env: + global: + - REPO=docksal/ci-agent + +install: + - curl -fsSL get.docksal.io | sh + - fin version + - fin sysinfo + +script: + # Build base first, then php + - make VERSION=base && make test VERSION=base + - make VERSION=php && make test VERSION=php + +after_success: | + # Push images to Docker Hub + if [[ "${TRAVIS_PULL_REQUEST}" == "false" ]]; then + scripts/push-image.sh base + scripts/push-image.sh php + fi + +after_failure: + - make logs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d34136 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +-include env_make + +VERSION ?= base + +REPO = docksal/ci-agent +NAME = ci-agent + +.PHONY: build test push shell run start stop logs clean release + +build: + docker build -t $(REPO):$(VERSION) $(VERSION) + +test: + IMAGE=$(REPO):$(VERSION) NAME=$(NAME) tests/$(VERSION).bats + +push: + docker push $(REPO):$(VERSION) + +shell: clean + docker run --rm --name $(NAME) -it $(PORTS) $(VOLUMES) $(ENV) $(REPO):$(VERSION) /bin/bash + +exec: + docker exec $(NAME) $(COMMAND) + +run: clean + docker run --rm --name $(NAME) $(PORTS) $(VOLUMES) $(ENV) $(REPO):$(VERSION) + +start: clean + docker run -d --name $(NAME) $(PORTS) $(VOLUMES) $(ENV) $(REPO):$(VERSION) top -b + +stop: + docker stop $(NAME) + +logs: + docker logs $(NAME) + +clean: + docker rm -f $(NAME) || true + +release: build + make push -e VERSION=$(VERSION) + +default: build diff --git a/README.md b/README.md index 1d8f7c7..0068339 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,59 @@ -# Bitbucket Pipelines Agent Docker image for Docksal CI +# CI agent Docker image for Docksal Sandboxes -A thin agent used to provision Docksal powered sandboxes on a remote Docker host. +A thin agent used to provision Docksal Sandboxes on a remote Docker host. + +Supported CI providers: + +- Bitbucket Pipelines (with build status integration) +- CircleCI (with build status integration) +- GitLab This image(s) is part of the [Docksal](http://docksal.io) image library. -## Docksal CI Sandboxes +## Docksal Sandboxes + +Docksal Sandboxes are continuous integration environments powered by Docksal. +They can be provisioned for any git branch and are feature-wise identical to project's local Docksal environment. -For any Docksal powered project enabling Bitbucket Pipelines and dropping [this](bitbucket-pipelines.yml) file -into the project repo will enable per-branch sandbox provisioning. -URLs to sandbox environments can be found in the build logs and can also published to a desired Slack channel. +Use cases: + +- automated testing (full stack) +- manual testing +- enhanced pull request review experience +- demos + +Build status updates (and sandbox URLs) can be posted to Github and Bitbucket via respective build status APIs. +URLs to sandbox environments can also be published to a Slack channel. ## Image variants and versions ### Stable -- `docksal/bitbucket-pipelines-agent` - basic (bash, curl, git) -- `docksal/bitbucket-pipelines-agent:php` - basic + php stack tools (composer, drush, drupal console, wp-cli, etc) -- `docksal/bitbucket-pipelines-agent:1.0` - basic, specific version -- `docksal/bitbucket-pipelines-agent:1.0-php` - php, specific version +- `base` - basic (bash, curl, git, etc.), latest version +- `php`, `latest` - basic + php stack tools (composer, drush, drupal console, wp-cli, etc.), latest version +- `1.2-base` - basic, a specific stable version +- `1.2-php` - php, a specific stable version ### Development -- `docksal/bitbucket-pipelines-agent:edge` -- `docksal/bitbucket-pipelines-agent:edge-php` +- `edge-base` - base, latest development version +- `edge-php`, php, latest development version + -## Configuration +## Global Configuration -### Global Pipelines variables +### Required -The agent expects the following required variables to be defined. +The following required variables are usually configured at the organization level. This way, all project repos will +have access to them. They can as well be configured at the repo level. `DOCKSAL_HOST` or `DOCKSAL_HOST_IP` -The address of the remote Docksal host, which will be hosting sandboxes. Configure one of the other. +The address of the remote Docksal host, which is hosting sandboxes. Configure one of the other. If using `DOCKSAL_HOST`, make sure the domain is configured as a wildcard DNS entry. -If using `DOCKSAL_HOST_IP`, the agent will use `xip.io` for dynamic wildcard domain names for sandboxes. +If using `DOCKSAL_HOST_IP`, the agent will use `nip.io` for dynamic wildcard domain names for sandboxes. `DOCKSAL_HOST_SSH_KEY` @@ -46,13 +63,118 @@ tutorial for details. `CI_SSH_KEY` -A second SSH keys (base64 encoded as well), which can be used for deployments and other remote operations run directly -on the agent. +A secondary SSH key (base64 encoded as well), which can be used for deployments and other remote operations run directly +on the agent. E.g. cloning/pushing a repo, running commands over SSH on a remote deployment environment. -### Project level Pipelines variables -The following environment variables can be configured in the project's Pipelines settings: +### Optional + +`DOCKSAL_DOMAIN` + +Can be used to set the base URL for sandbox builds (defaults to `DOCKSAL_HOST` if not set), individually from `DOCKSAL_HOST`. +This is useful when working with CDNs/ELBs/WAFs/etc (when `DOCKSAL_DOMAIN` is different from the `DOCKSAL_HOST`) + +`DOCKSAL_HOST_USER` + +The user's name that should have access to the remote Docksal host. Defaults to `ubuntu`. + +`REMOTE_BUILD_BASE` + +The directory location on the remote server where the repositories should be cloned down to and built. +Defaults to `/home/ubuntu/builds` + +`GITHUB_TOKEN` and `BITBUCKET_TOKEN` + +Used for access to post sandbox URLs via build status API as well as comments on pull requests. + +For Github, the token can be generated from the [user's account](https://github.com/settings/tokens). +Set access to "repo" (http://take.ms/nMqcW). + +For Bitbucket, the token can be generated from the user's settings. Instructions on creating an [app password](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html). +Set access to "Repositories: Write", "Pull requests: Write" (http://take.ms/98BG5). +When storing the app password it is in the format: `USER:PASSWORD`. + +`GIT_USER_EMAIL` + +The user's email to perform Git operations as. Defaults to `ci@docksal.io` + +`GIT_USER_NAME` + +The user's name to perform Git operations as. Defaults to `Docksal CI` + +`DOCKSAL_HOST_TUNNEL` + +If not empty, `localhost:2374` in the agent is mapped to `docker.sock` on the remote `DOCKSAL_HOST` via a secure SSH tunnel. +The agent can then run `docker` commands against the remote `DOCKSAL_HOST`. + + +Other features and integrations are usually configured at the repo level. See below. + + +## Project configuration + +For Bitbucket Pipelines, copy the example [bitbucket-pipelines.yml](examples/bitbucket-pipelines/bitbucket-pipelines.yml) +file into the project repo and adjust as necessary. + +For CircleCI, copy the example [config.yml](examples/.circleci/config.yml) file into the project repo and adjust as necessary. + +## Build environment variables + +The following environment variables are available to build scripts. +They are derived from respective Bitbucket Pipelines, Circle CI and GitLab CI build variables. + +- `GIT_REPO_OWNER` - git repo machine owner/slug name +- `GIT_REPO_NAME` - git repo machine name +- `GIT_REPO_URL` - git repo URL +- `GIT_BRANCH_NAME` - git branch name +- `GIT_COMMIT_HASH` - git commit hash +- `GIT_PR_NUMBER` - git pull request / merge request number +- `GIT_REPO_SERVICE` - `github`, `bitbucket` or `gitlab` (makes sense mostly for CircleCI) + + +## Feature: Basic HTTP Auth + +Protect sandboxes from public access using Basic HTTP authentication. + +### Configuration + +Set the following environment variables at the repo level: + +- `HTTP_USER` +- `HTTP_PASS` + + +## Feature: Build status notifications + +This integration allows the agent to post build status updates and sandbox URL via Github/Bitbucket build status API. +For CircleCI, it is also possible to enable posting the sandbox URL as a comment in pull requests. + +### Configuration + +`GITHUB_TOKEN` or `BITBUCKET_TOKEN` must be configured respectively (either globally or at the repo level). + +### Usage + +`build-notify ` + +Place the triggers right before and right after `fin init` call in your build script, e.g. + +```bash +build-notify pending +ssh docker-host "cd $REMOTE_BUILD_DIR && fin init" || ( build-notify failure && exit 1 ) +build-notify success +``` + +To enable posting sandbox URLs in comments on pull requests, do `export PR_COMMENT=1` prior to calling `build-notify` + + +## Feature: Slack notifications + +This integrations allows the agent to post messages to a given Slack channel. +It can be used for notification purposes when a build is started, completed, failed, etc. + +### Configuration `SLACK_WEBHOOK_URL` @@ -63,14 +185,111 @@ e.g. `SLACK_WEBHOOK_URL https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXx A public or private channel in Slack, e.g. `SLACK_CHANNEL #project-name-bots` -## Slack notifications +`SLACK_USER` + +The username the message should post to Slack as. Defaults to `Docksal CI` + +`SLACK_ICON` + +The icon the message should use to accompany the message: Defaults to `:desktop_computer:` ### Usage -`slack 'message' ['#channel'] ['webhook_url']` +```bash +slack 'message' ['#channel'] ['webhook_url'] ['slack_user'] ['slack_icon'] +``` -Channel and webhook url can be passed via environment variables. See below. +Channel and webhook url can be passed via environment variables. See above. ### Limitations Incoming Webhook integration won't work for private channels, which the owner of the integration does not belong to. + + +## Feature: Build artifact storage + +Build artifacts can be stored in an AWS S3 bucket. + +### Configuration + +Set the following environment variables at the organization or repo level: + +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` +- `ARTIFACTS_BUCKET_NAME` +- `ARTIFACTS_BASE_URL` (optional) +- `GITHUB_TOKEN` (optional) +- `BITBUCKET_TOKEN` (optional) + +### Usage + +To upload artifacts to the configured S3 bucket use the `build-acp` command. + +```bash +build-acp /source/path/ +``` + +There is no file browsing capability available for private S3 buckets. +An `index.html` file is used as the directory index, however it has to be created/generated manually. +When uploading a prepared artifacts folder with the `index.html` file in it, add a trailing slash to the source path to +upload the contents of the source folder vs the folder itself. + +You can upload additional folders/files by running the command multiple times. + +The optional `destination` argument can be used to define a subdirectory at the destination in the bucket. + +```bash +build-acp /source/path/ destination/path +``` + +### Advanced usage + +**Base URL** + +The base URL is derived from `ARTIFACTS_BUCKET_NAME` as follows (assuming AWS S3 `us-east-1` region by default): + +``` +https://${ARTIFACTS_BUCKET_NAME}.s3.amazonaws.com +``` + +It can be overridden via the optional `ARTIFACTS_BASE_URL` configuration variable at the organization/repo level: + +``` +ARTIFACTS_BASE_URL = https://artifacts.example.com +``` + +**Upload path** + +The upload path is unique for each commit and is derived as follows: + +```bash +${REPO_NAME_SAFE}/${BRANCH_NAME_SAFE}-${GIT_COMMIT_HASH} +``` + +In certain cases you may want to store build artifacts per branch instead of per commit. +To do this, override the `ARTIFACTS_BUCKET_PATH` variable before calling the `build-acp` command: + +```bash +export ARTIFACTS_BUCKET_PATH="${REPO_NAME_SAFE}/${BRANCH_NAME_SAFE}" +build-acp my-artifacts/ +``` + +**Posting build artifact URLs to Bitbucket** + +If `BITBUCKET_TOKEN` is set, the URL to the artifacts will be posted back to Bitbucket via +[Bitbucket Build Status API](https://blog.bitbucket.org/2015/11/18/introducing-the-build-status-api-for-bitbucket-cloud/). + +### Security + +If a bucket does not exist, it will be created automatically (with no public access). Existing bucket access permissions +are not automatically adjusted. It's up to you whether you want to keep them open or not. + +When artifacts are uploaded, the destination artifact folder in the bucket is set to be publicly accessible. +Anyone with the direct link will be able to access the artifacts, but will not be able to browse the list of all +available artifact folders in the bucket (so long as the bucket itself is set to be private). + +The URL by default includes a git commit hash, which serves as an authentication token (the URL is impossible to guess). +This provides a simple yet efficient level of security for artifacts. + +To add an additional level of security follow [this guide](https://medium.com/@lmakarov/serverless-password-protecting-a-static-website-in-an-aws-s3-bucket-bfaaa01b8666) +to set up username/password access to S3 via CloudFront and Lambda@Edge. diff --git a/base/Dockerfile b/base/Dockerfile index 8b77872..eb3d1e6 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -1,34 +1,58 @@ -FROM alpine:3.5 +FROM alpine:3.7 RUN apk add --update --no-cache \ - bash \ - curl \ - git \ - jq \ - openssh \ - sudo \ - && rm -rf /var/cache/apk/* - -ARG AGENT_USER=agent -ARG AGENT_HOME=/home/agent + bash \ + curl \ + git \ + jq \ + openssh \ + rsync \ + sudo \ + patch; \ + rm -rf /var/cache/apk/*; + +ARG GLIBC=2.27-r0 +RUN \ + # Install glibc libraries needed for docker-compose + apk update && apk add --no-cache openssl ca-certificates; \ + curl -L https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub; \ + curl -L -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC}/glibc-${GLIBC}.apk; \ + apk add --no-cache glibc-${GLIBC}.apk && rm glibc-${GLIBC}.apk; \ + ln -s /lib/libz.so.1 /usr/glibc-compat/lib/; \ + ln -s /lib/libc.musl-x86_64.so.1 /usr/glibc-compat/lib; + +ARG DOCKER_VERSION=17.09.0-ce +ARG DOCKER_COMPOSE_VERSION=1.19.0 RUN \ - # Create a non-root user with access to sudo - adduser -h $AGENT_HOME -s /bin/bash -D $AGENT_USER && \ - echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + # Install docker cli + curl -sSL -O "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"; \ + tar zxf docker-${DOCKER_VERSION}.tgz && mv docker/docker /usr/local/bin && rm -rf docker-${DOCKER_VERSION}*; \ + # Install docker-compose cli + curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Linux-x86_64 -o /usr/local/bin/docker-compose; \ + chmod +x /usr/local/bin/docker-compose; \ + # Install minio client (mc) + curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc; \ + chmod +x /usr/local/bin/mc + +ENV AGENT_USER=agent +ENV AGENT_HOME=/home/agent +RUN \ + # Create a non-root user with access to sudo + adduser -h $AGENT_HOME -s /bin/bash -D $AGENT_USER; \ + echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; # All further RUN commands will run as the "agent" user USER $AGENT_USER -ENV HOME $AGENT_HOME RUN \ - mkdir -p $AGENT_HOME/.ssh && \ - mkdir -p $AGENT_HOME/build + mkdir -p $AGENT_HOME/.ssh; \ + mkdir -p $AGENT_HOME/build; +COPY bin /usr/local/bin COPY config/.ssh/config $AGENT_HOME/.ssh/config -COPY bin/build-env /usr/local/bin/build-env -COPY bin/build-init /usr/local/bin/build-init -COPY bin/slack /usr/local/bin/slack -# Fix permissions after COPY -RUN sudo chown -R $AGENT_USER:$AGENT_USER $AGENT_HOME +# Fix permissions after COPY (could use COPY --chown, but will still need to run chmod) +RUN \ + sudo chown $AGENT_USER:$AGENT_USER $AGENT_HOME/.ssh/config; \ + sudo chmod 600 $AGENT_HOME/.ssh/config; WORKDIR $AGENT_HOME/build diff --git a/base/bin/build-acp b/base/bin/build-acp new file mode 100755 index 0000000..42f6e1a --- /dev/null +++ b/base/bin/build-acp @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Build Artifact Copy (build-acp) +# Copy build artifacts into S3 +# +# Usage: build-acp + +# Abort if anything failse +set -e + +if [[ "${AWS_ACCESS_KEY_ID}" == "" ]] || [[ "${AWS_SECRET_ACCESS_KEY}" == "" ]] || [[ "${ARTIFACTS_BUCKET_NAME}" == "" ]]; then + echo "Cannot upload artifacts." + echo "Please make sure AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and ARTIFACTS_BUCKET_NAME environment variables are configured." + exit 1 +fi + +# Configure ARTIFACTS_BASE_URL, unless it has been already defined +ARTIFACTS_BASE_URL=${ARTIFACTS_BASE_URL:-"https://${ARTIFACTS_BUCKET_NAME}.s3.amazonaws.com"} +# Configure ARTIFACTS_BUCKET_PATH, unless it has been already defined +ARTIFACTS_BUCKET_PATH=${ARTIFACTS_BUCKET_PATH:-"${REPO_NAME_SAFE}/${BRANCH_NAME_SAFE}-${GIT_COMMIT_HASH}"} +# Configure ARTIFACTS_URL, unless it has been already defined +# By default expecting index.html as the default entry point into the bucket/path +ARTIFACTS_URL=${ARTIFACTS_URL:-"${ARTIFACTS_BASE_URL}/${ARTIFACTS_BUCKET_PATH}/index.html"} + +# Sync source locally +source=${1} +# Sync destination on S3 +destination="s3/${ARTIFACTS_BUCKET_NAME}/${ARTIFACTS_BUCKET_PATH}" +# Allow specifying a subpath within the destination path +[[ "${2}" != "" ]] && destination="${destination}/${2}" + +if [[ "${source}" == "" ]]; then + echo "No source provided." + echo "Configuration: " + echo "source=${source}" + echo "destination=${destination}" + echo "ARTIFACTS_BASE_URL=${ARTIFACTS_BASE_URL}" + echo "ARTIFACTS_BUCKET_PATH=${ARTIFACTS_BUCKET_PATH}" + echo "ARTIFACTS_URL=${ARTIFACTS_URL}" + exit +fi + +# Initialize minio client (mc) configuration for S3 +# AWS API keys must be set in the build settings +mc config host add s3 https://s3.amazonaws.com ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} + +# Create bucket if it does not exist +mc mb "s3/${ARTIFACTS_BUCKET_NAME}" +# Upload artifact into the bucket +# TODO: figure out how to skip timestemp checking. Maybe use aws-cli instead on the minio client? +# Looks like mirror does not have any benefit here over cp. Files in build have newer timestamps and are always copied. +mc mirror --force --remove ${source} ${destination} +# Set read-only permissions on BUCKET_PATH +# Access is possible only if BUCKET_PATH is known (parent bucket stays private => XML listing of bucket objects disabled) +mc policy download "s3/${ARTIFACTS_BUCKET_NAME}/${ARTIFACTS_BUCKET_PATH}" + +echo "Build artifacts upload completed:" +echo "${ARTIFACTS_URL}" +echo "Files:" +mc ls ${destination} + +# Post artifacts to Bitbucket build status API +if [[ "${BITBUCKETCI}" != "" ]] && [[ "${BITBUCKET_TOKEN}" != "" ]]; then + echo "Posting artifacts URL to Bitbucket..." + + BUILD_STATUS_URL="${ARTIFACTS_URL}" + PAYLOAD="{\"key\": \"artifacts\", \"state\": \"SUCCESSFUL\", \"name\": \"Build artifacts\", \"url\": \"${BUILD_STATUS_URL}\"}" + API_URL="https://api.bitbucket.org/2.0/repositories/${GIT_REPO_OWNER}/${GIT_REPO_NAME}/commit/${GIT_COMMIT_HASH}/statuses/build" + + curl -sS --request POST "${API_URL}" \ + --header "Content-Type: application/json" \ + --user "${BITBUCKET_TOKEN}" \ + --data "${PAYLOAD}" +else + echo "This command only works in Bitbucket Pipelines" +fi diff --git a/base/bin/build-env b/base/bin/build-env index 7fbf023..ecbdb15 100755 --- a/base/bin/build-env +++ b/base/bin/build-env @@ -1,40 +1,166 @@ #!/usr/bin/env bash -# This script configures the pipelines build agent. +# Configures build environment variables +# This file should be sources at the beginning of a build +# +# Usage source build-env + +set -e # Abort if anything fails +#set -x # Echo commands + +# -------------------- Constants -------------------- # BRANCH_NAME_LENGTH_LIMIT=35 +DOCKER_HOST_TUNNEL=localhost:2374 +GIT_USER_EMAIL=${GIT_USER_EMAIL:-ci@docksal.io} +GIT_USER_NAME=${GIT_USER_NAME:-Docksal CI} -echo "Configuring build settings..." -# URL safe branch name -BRANCH_NAME_SAFE="$(echo -n $BITBUCKET_BRANCH | sed -e 's/[^A-Za-z0-9]/-/g' | awk '{print tolower($0)}')" -# Trim the branch name if longer than BRANCH_NAME_LENGTH_LIMIT and append the md5sum to keep the branch name unique -if (( "${#BRANCH_NAME_SAFE}" > "$BRANCH_NAME_LENGTH_LIMIT" )); then - BRANCH_NAME_SAFE="$(echo -n $BRANCH_NAME_SAFE | cut -c1-30)-$(echo -n $BRANCH_NAME_SAFE | md5sum | cut -c1-4)" -fi +# -------------------- Functions -------------------- # -# Trim repo slug to 30 characters. If someone has a repo name longer than that, then no mercy for them. -export BITBUCKET_REPO_SLUG_SAFE="${BITBUCKET_REPO_SLUG:0:30}" -export BRANCH_NAME_SAFE -export REMOTE_BUILD_DIR="/home/ubuntu/builds/$BITBUCKET_REPO_SLUG_SAFE-$BRANCH_NAME_SAFE" -export COMPOSE_PROJECT_NAME="$BITBUCKET_REPO_SLUG_SAFE-$BRANCH_NAME_SAFE" +# Initial build environment configuration +build_env () +{ + # Support for Bitbucket Pipelines + if [[ "$BITBUCKET_REPO_SLUG" != "" ]]; then + echo "Detected Bitbucket Pipelines build environment" + export BITBUCKETCI="true" + export GIT_REPO_SERVICE="bitbucket" + export GIT_REPO_OWNER="$BITBUCKET_REPO_OWNER" + export GIT_REPO_NAME="$BITBUCKET_REPO_SLUG" + export GIT_REPO_URL="git@bitbucket.org:$GIT_REPO_OWNER/$GIT_REPO_NAME.git" + export GIT_BRANCH_NAME="$BITBUCKET_BRANCH" + export GIT_COMMIT_HASH="$BITBUCKET_COMMIT" + # Bitbucket Pipelines does not work with PRs + #GIT_PR_NUMBER=? + fi -# Use DOCKSAL_HOST (domain name) if set, otherwise use DOCKSAL_HOST_IP (IP) with xip.io -DOCKSAL_HOST="${DOCKSAL_HOST:-$DOCKSAL_HOST_IP.xip.io}" + # Support for CircleCI 2.0 + if [[ "$CIRCLECI" != "" ]]; then + echo "Detected CircleCI build environment" + export GIT_REPO_OWNER="$CIRCLE_PROJECT_USERNAME" + export GIT_REPO_NAME="$CIRCLE_PROJECT_REPONAME" + export GIT_REPO_URL="$CIRCLE_REPOSITORY_URL" + export GIT_BRANCH_NAME="$CIRCLE_BRANCH" + export GIT_COMMIT_HASH="$CIRCLE_SHA1" -# NOTE: The length of any one label in the domain name is limited to between 1 and 63 octets. -export DOMAIN="$BRANCH_NAME_SAFE.$BITBUCKET_REPO_SLUG_SAFE.$DOCKSAL_HOST" + if [[ $CIRCLE_REPOSITORY_URL == *"github.com"* ]]; then + export GIT_REPO_SERVICE="github" + # Figure out the pull request number + # Cannot use $CIRCLE_PR_NUMBER as it's only available in forked PR builds + export GIT_PR_NUMBER="$(echo $CIRCLE_PULL_REQUEST | sed 's/.*\/pull\///')" + fi + if [[ $CIRCLE_REPOSITORY_URL == *"bitbucket.org"* ]]; then + export GIT_REPO_SERVICE="bitbucket" + # Figure out the pull request number + # Cannot use $CIRCLE_PR_NUMBER as it's only available in forked PR builds + export GIT_PR_NUMBER="$(echo $CIRCLE_PULL_REQUEST | sed 's/.*\/pull-requests\///')" + fi + fi -echo "Configuring ssh access..." -(umask 077 ; echo "$CI_SSH_KEY" | base64 -d > $HOME/.ssh/id_rsa) -(umask 077 ; echo "$DOCKSAL_HOST_SSH_KEY" | base64 -d > $HOME/.ssh/docksal_host_id_rsa) + # Support for GitLab 9.0+ + if [[ "$GITLAB_CI" != "" ]]; then + echo "Detected GitLabCI build environment" + export GIT_REPO_SERVICE="gitlab" + export GIT_REPO_OWNER="$CI_PROJECT_NAMESPACE" + export GIT_REPO_NAME="$CI_PROJECT_NAME" + export GIT_REPO_URL="$CI_REPOSITORY_URL" + export GIT_BRANCH_NAME="$CI_COMMIT_REF_NAME" + export GIT_COMMIT_HASH="$CI_COMMIT_SHA" + export GIT_PR_NUMBER="$CI_MERGE_REQUEST_ID" + fi + + # For debug purposes these variables can be set manually. + # If they are empty here, then we cannot proceed. + if [[ "$GIT_REPO_URL" == "" ]] || [[ "$GIT_BRANCH_NAME" == "" ]] || [[ "$GIT_COMMIT_HASH" == "" ]]; then + echo "Unsupported build environment! Quiting..." + exit 1 + fi + + # URL safe branch name + BRANCH_NAME_SAFE="$(echo -n "$GIT_BRANCH_NAME" | sed -e 's/[^A-Za-z0-9]/-/g' | awk '{print tolower($0)}')" + # Trim the branch name if longer than BRANCH_NAME_LENGTH_LIMIT and append the md5sum to keep the branch name unique + if (( "${#BRANCH_NAME_SAFE}" > "$BRANCH_NAME_LENGTH_LIMIT" )); then + BRANCH_NAME_SAFE="$(echo -n "$BRANCH_NAME_SAFE" | cut -c1-30)-$(echo -n "$BRANCH_NAME_SAFE" | md5sum | cut -c1-4)" + fi + export BRANCH_NAME_SAFE + + # Trim repo name to 30 characters. If someone has a repo name longer than that, then no mercy for them. + export REPO_NAME_SAFE="${GIT_REPO_NAME:0:30}" + # Short version of GIT_COMMIT_HASH + export COMMIT_HASH_SHORT="${GIT_COMMIT_HASH:0:7}" +} + +ssh_init () +{ + (umask 077 ; echo "$CI_SSH_KEY" | base64 -d > $HOME/.ssh/id_rsa) + (umask 077 ; echo "$DOCKSAL_HOST_SSH_KEY" | base64 -d > $HOME/.ssh/docksal_host_id_rsa) +} + +git_env () +{ + git config --global user.email "$GIT_USER_EMAIL" + git config --global user.name "$GIT_USER_NAME" +} + +ssh_tunnel_init () +{ + # Check if the tunnel is already active and return if so + ssh -O "check" docker-host >/dev/null 2>&1 + [[ $? == 0 ]] && return 0 + + echo "Setting up a secure tunnel to the Docker Engine on $DOCKSAL_HOST..." + # Black magic! Remote docker.sock access over SSH tunnel + # Credits: + # https://docs.docker.com/docker-for-aws/deploy/#connecting-via-ssh + # https://gist.github.com/scy/6781836#gistcomment-1559506 + ssh -fM -NL ${DOCKER_HOST_TUNNEL}:/var/run/docker.sock docker-host -# Use DOCKSAL_HOST (domain name) if set, otherwise use DOCKSAL_HOST_IP (IP) -if [[ $DOCKSAL_HOST != "" ]]; then - sed -i "s/DOCKSAL_HOST/$DOCKSAL_HOST/g" $HOME/.ssh/config -else - sed -i "s/DOCKSAL_HOST/$DOCKSAL_HOST_IP/g" $HOME/.ssh/config -fi + echo "Querying Docker Engine..." + docker --host ${DOCKER_HOST_TUNNEL} version + return $? +} + +sandbox_server_env () +{ + export REMOTE_BUILD_BASE=${REMOTE_BUILD_BASE:-/home/ubuntu/builds} + export REMOTE_BUILD_DIR="${REMOTE_BUILD_BASE}/$REPO_NAME_SAFE-$BRANCH_NAME_SAFE" + export COMPOSE_PROJECT_NAME="$REPO_NAME_SAFE-$BRANCH_NAME_SAFE" + export DOCKER_STACK_NAME="$REPO_NAME_SAFE-$BRANCH_NAME_SAFE" + + # Use DOCKSAL_HOST (domain name) if set, otherwise use DOCKSAL_HOST_IP (IP) with nip.io + export DOCKSAL_HOST="${DOCKSAL_HOST:-$DOCKSAL_HOST_IP.nip.io}" + sed -i "s/HostName DOCKSAL_HOST/HostName $DOCKSAL_HOST/g" $HOME/.ssh/config + + # Use ubuntu as the user by default + export DOCKSAL_HOST_USER="${DOCKSAL_HOST_USER:-ubuntu}" + sed -i "s/User DOCKSAL_HOST_USER/User $DOCKSAL_HOST_USER/g" $HOME/.ssh/config + + # Allow setting DOCKSAL_DOMAIN individually from DOCKSAL_HOST. Default to DOCKSAL_HOST if not set. + # This is useful when working with CDNs/ELBs/WAFs/etc (when DOCKSAL_DOMAIN is different from the DOCKSAL_HOST). + export DOCKSAL_DOMAIN="${DOCKSAL_DOMAIN:-$DOCKSAL_HOST}" + # NOTE: The length of any one label in the domain name is limited to between 1 and 63 octets. + export DOMAIN="$BRANCH_NAME_SAFE.$REPO_NAME_SAFE.$DOCKSAL_DOMAIN" + + # Initialize a tunnel to the Docker Engine on DOCKSAL_HOST + # Export local tunnel connection settings if it works + # Using full if form instead of the short one here, otherwise builds will fail, when the condition below is false + if [[ "$DOCKSAL_HOST_TUNNEL" != "" ]]; then + ssh_tunnel_init && export DOCKER_HOST=${DOCKER_HOST_TUNNEL} + fi +} + +# -------------------- Runtime -------------------- # + +echo "Configuring build settings..." +build_env + +echo "Configuring ssh access..." +ssh_init # Configure git -git config --global user.email "${CI_GIT_USER_EMAIL:-bitbucket-pipelines-agent@docksal.io}" -git config --global user.name "${CI_GIT_USER_NAME:-Bitbucket Pipelines Agent}" +echo "Configuring git settings..." +git_env + +# Sandbox server settings +echo "Configuring sandbox server settings..." +sandbox_server_env diff --git a/base/bin/build-init b/base/bin/build-init index afded26..6642fbd 100755 --- a/base/bin/build-init +++ b/base/bin/build-init @@ -8,13 +8,13 @@ ssh docker-host whoami ssh docker-host "(cd $REMOTE_BUILD_DIR && fin rm -f) || true" ssh docker-host "sudo rm -rf $REMOTE_BUILD_DIR; mkdir -p $REMOTE_BUILD_DIR" # Checkout sources -ssh docker-host "cd $REMOTE_BUILD_DIR && git clone --branch="$BITBUCKET_BRANCH" --depth 50 git@bitbucket.org:$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG.git . && git reset --hard $BITBUCKET_COMMIT && ls -la" +ssh docker-host "cd $REMOTE_BUILD_DIR && git clone --branch="$GIT_BRANCH_NAME" --depth 50 $GIT_REPO_URL . && git reset --hard $GIT_COMMIT_HASH && ls -la" # Configure sandbox settings echo "Configuring sandbox settings..." ssh docker-host "cd $REMOTE_BUILD_DIR && echo COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT_NAME | tee -a .docksal/docksal-local.env" ssh docker-host "cd $REMOTE_BUILD_DIR && echo VIRTUAL_HOST=$DOMAIN | tee -a .docksal/docksal-local.env" # Basic HTTP Auth -if [[ $HTTP_USER != "" ]] && [[ $HTTP_PASS != "" ]]; then +if [[ "${HTTP_USER}" != "" ]] && [[ "${HTTP_PASS}" != "" ]]; then ssh docker-host "cd $REMOTE_BUILD_DIR && echo APACHE_BASIC_AUTH_USER=$HTTP_USER | tee -a .docksal/docksal-local.env" ssh docker-host "cd $REMOTE_BUILD_DIR && echo APACHE_BASIC_AUTH_PASS=$HTTP_PASS | tee -a .docksal/docksal-local.env" fi diff --git a/base/bin/build-notify b/base/bin/build-notify new file mode 100755 index 0000000..cb5e58a --- /dev/null +++ b/base/bin/build-notify @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# Build status notifications +# Can post updates via builds status API (Github and Bitbucket) and/or pull request comments + +#-------------------------- SETTINGS ---------------------------- + +export CONTEXT="ci/docksal" # Context for notifications +export URL="http://${DOMAIN}" # Sandbox URL +export STATUS_API=${STATUS_API:-1} # Status API notifications enabled by default +export PR_COMMENT=${PR_COMMENT:-0} # PR comment notifications disabled by default + +#-------------------------- FUNCTIONS ---------------------------- + +# Calls Github status API (commit/branch level status updates) +# API docs: https://developer.github.com/v3/repos/statuses/ +# +# @param $1 context - unique key for this status (e.g. sandbox, artifacts) +# @param $2 state - error, failure, pending, or success +# @param $3 description - description of the status update +# @param $4 url - status link URL +# +# Example: github_status_api "sandbox" "pending" "Started building sandbox environment" "http://${DOMAIN}" +# +github_status_api () +{ + [[ "${GIT_REPO_SERVICE}" != "github" ]] && return + [[ "${GITHUB_TOKEN}" == "" ]] && echo "Warning: GITHUB_TOKEN not set" && return + + local context="$1" + local state="$2" + local description="$3" + local url="$4" + + echo "Notifying Github Status API: ${context} => ${state}" + + PAYLOAD="{\"state\": \"${state}\",\"target_url\": \"${url}\",\"description\": \"${description}\",\"context\": \"${context}\"}" + API_URL="https://api.github.com/repos/${GIT_REPO_OWNER}/${GIT_REPO_NAME}/statuses/${GIT_COMMIT_HASH}" + + curl -sS --request POST "${API_URL}" \ + --header "Content-Type: application/json" \ + --header "Accept: application/vnd.github.v3.full+json" \ + --header "Authorization: token ${GITHUB_TOKEN}" \ + --data "${PAYLOAD}" +} + +# Calls Bitbucket Pipelines status API (commit/branch level status updates) +# API docs: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/commit/%7Bnode%7D/statuses/build +# +# @param $1 context - unique key for this status (e.g. sandbox, artifacts) +# @param $2 state - SUCCESSFUL, FAILED, INPROGRESS, STOPPED +# @param $3 description - description of the status update +# @param $4 url - status link URL +# +# Example: bitbucket_status_api "sandbox" "INPROGRESS" "Sandbox environment" "http://${DOMAIN}" +# +bitbucket_status_api () +{ + [[ "${GIT_REPO_SERVICE}" != "bitbucket" ]] && return + [[ "${BITBUCKET_TOKEN}" == "" ]] && echo "Warning: BITBUCKET_TOKEN not set" && return + + local context="$1" + local state="$2" + local description="$3" + local url="$4" + + echo "Notifying Bitbucket Status API: ${context} => ${state}" + + PAYLOAD="{\"key\": \"${context}\", \"state\": \"${state}\", \"description\": \"${description}\", \"url\": \"${url}\"}" + API_URL="https://api.bitbucket.org/2.0/repositories/${GIT_REPO_OWNER}/${GIT_REPO_NAME}/commit/${GIT_COMMIT_HASH}/statuses/build" + + curl -sS --request POST "${API_URL}" \ + --header "Content-Type: application/json" \ + --user "${BITBUCKET_TOKEN}" \ + --data "${PAYLOAD}" +} + +# Posts a comment to a pull request in Github +# @param $1 - comment +github_pr_comment () +{ + [[ "${GIT_REPO_SERVICE}" != "github" ]] && return + [[ "${GIT_PR_NUMBER}" == "" ]] && return + [[ "${GITHUB_TOKEN}" == "" ]] && echo "Warning: GITHUB_TOKEN not set" && return + + local comment="$1" + + echo "Posting sandbox URL to Github pull request..." + PAYLOAD="{\"body\": \"${comment}\"}" + API_URL="https://api.github.com/repos/${GIT_REPO_OWNER}/${GIT_REPO_NAME}/issues/${GIT_PR_NUMBER}/comments" + + curl -sS --request POST "${API_URL}" \ + --header "Content-Type: application/json" \ + --header "Accept: application/vnd.github.v3.full+json" \ + --header "Authorization: token ${GITHUB_TOKEN}" \ + --data "${PAYLOAD}" +} + +# Posts a comment to a pull request in Bitbucket +# @param $1 - comment +bitbucket_pr_comment () +{ + [[ "${GIT_REPO_SERVICE}" != "bitbucket" ]] && return + [[ "${GIT_PR_NUMBER}" == "" ]] && return + [[ "${BITBUCKET_TOKEN}" == "" ]] && echo "Warning: BITBUCKET_TOKEN not set" && return + + local comment="$1" + + echo "Posting sandbox URL to Bitbucket pull request..." + PAYLOAD="content=${comment}" + # Posting comments to PRs is only available in BB API 1.0 + API_URL="https://api.bitbucket.org/1.0/repositories/${GIT_REPO_OWNER}/${GIT_REPO_NAME}/pullrequests/${GIT_PR_NUMBER}/comments" + + curl -sS --request POST "${API_URL}" \ + --user "${BITBUCKET_TOKEN}" \ + --data "${PAYLOAD}" +} + +notify_pending () +{ + description="Started building sandbox environment" + + [[ "$GIT_REPO_SERVICE" == "bitbucket" ]] && bitbucket_status_api "$CONTEXT" "INPROGRESS" "$description" "$URL" + [[ "$GIT_REPO_SERVICE" == "github" ]] && github_status_api "$CONTEXT" "pending" "$description" "$URL" + + return 0 +} + +notify_success () +{ + description="Sandbox environment build completed" + + [[ "$GIT_REPO_SERVICE" == "bitbucket" ]] && bitbucket_status_api "$CONTEXT" "SUCCESSFUL" "$description" "$URL" + [[ "$GIT_REPO_SERVICE" == "github" ]] && github_status_api "$CONTEXT" "success" "$description" "$URL" + + # Post comment to a pull request on success only + # Note: pull request builds are not supported by Bitbucket Pipelines + if [[ "$CIRCLECI" != "" ]] && [[ "$PR_COMMENT" != 0 ]]; then + comment="$description: http://${DOMAIN}" + [[ "${GIT_REPO_SERVICE}" == "github" ]] && github_pr_comment "$comment" + [[ "${GIT_REPO_SERVICE}" == "bitbucket" ]] && bitbucket_pr_comment "$comment" + fi + + return 0 +} + +notify_failure () +{ + description="Sandbox environment build failed" + + [[ "$GIT_REPO_SERVICE" == "bitbucket" ]] && bitbucket_status_api "$CONTEXT" "FAILED" "$description" "$URL" + [[ "$GIT_REPO_SERVICE" == "github" ]] && github_status_api "$CONTEXT" "failure" "$description" "$URL" + + return 0 +} + +#-------------------------- RUNTIME STARTS HERE ---------------------------- + +# Post build state via status API +case "$1" in + pending) + notify_pending + ;; + success) + notify_success + ;; + failure) + notify_failure + ;; + *) + echo "Usage: build-notify " + ;; +esac diff --git a/base/bin/slack b/base/bin/slack index ac3fb84..9ca379f 100755 --- a/base/bin/slack +++ b/base/bin/slack @@ -3,6 +3,10 @@ MESSAGE="${1}" SLACK_CHANNEL="${SLACK_CHANNEL:-$2}" SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-$3}" +SLACK_USER="${SLACK_USER:-$4}" +SLACK_USER="${SLACK_USER:-Docksal CI}" +SLACK_ICON="${SLACK_ICON:-$5}" +SLACK_ICON="${SLACK_ICON:-:desktop_computer:}" if [[ "$MESSAGE" == "" ]] || [[ "$SLACK_CHANNEL" == "" ]] || [[ "$SLACK_WEBHOOK_URL" == "" ]]; then echo "Usage: slack 'message' ['#channel'] ['webhook_url']" @@ -12,4 +16,4 @@ if [[ "$MESSAGE" == "" ]] || [[ "$SLACK_CHANNEL" == "" ]] || [[ "$SLACK_WEBHOOK_ exit 1 fi -curl -X POST --data-urlencode "payload={'channel': '${SLACK_CHANNEL}', 'username': 'Bitbucket Pipelines', 'text': '${MESSAGE}', 'icon_emoji': ':bitbucket:'}" "${SLACK_WEBHOOK_URL}" +curl -X POST --data-urlencode "payload={'channel': '${SLACK_CHANNEL}', 'username': '${SLACK_USER}', 'text': '${MESSAGE}', 'icon_emoji': '${SLACK_ICON}'}" "${SLACK_WEBHOOK_URL}" diff --git a/base/config/.ssh/config b/base/config/.ssh/config index 4ce81c2..932b797 100644 --- a/base/config/.ssh/config +++ b/base/config/.ssh/config @@ -6,8 +6,9 @@ Host * Host docker-host HostName DOCKSAL_HOST - User ubuntu + User DOCKSAL_HOST_USER StrictHostKeyChecking no UserKnownHostsFile=/dev/null LogLevel ERROR IdentityFile ~/.ssh/docksal_host_id_rsa + ControlPath ~/.ssh/docksal_host.ctl diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml deleted file mode 100644 index d11d071..0000000 --- a/bitbucket-pipelines.yml +++ /dev/null @@ -1,15 +0,0 @@ -image: docksal/bitbucket-pipelines-agent:1.0 - -pipelines: - # Build sandboxes for all branches - default: - - step: - script: - # Initialize the agent configuration - - source build-env - # Initialize the remote sandbox environment - - build-init - - slack "Started sandbox build for branch *${BITBUCKET_BRANCH}*" - # Run fin init on the remote docker host to provision a sandbox - - ssh docker-host "cd $REMOTE_BUILD_DIR && fin init" - - slack "Completed sandbox build for branch *${BITBUCKET_BRANCH}*:\n" diff --git a/examples/.circleci/config.yml b/examples/.circleci/config.yml new file mode 100644 index 0000000..d43a688 --- /dev/null +++ b/examples/.circleci/config.yml @@ -0,0 +1,28 @@ +version: 2 + +jobs: + build: + working_directory: /home/agent/build + docker: + - image: docksal/ci-agent:edge-php + environment: + BASH_ENV: /home/agent/.bashrc # Tell CircleCI where to read the shared build environment variables from + steps: + - checkout + - run: + name: Configure local build environment + command: | + touch $BASH_ENV + echo 'source build-env' >> $BASH_ENV + - run: + name: Sandbox provisioning + command: | + build-init + ssh docker-host "cd $REMOTE_BUILD_DIR && fin init" + +workflows: + version: 2 + default-workflow: + jobs: + - build: + context: org-global # Load org level environment variables from CircleCI diff --git a/examples/bitbucket-pipelines/bitbucket-pipelines.yml b/examples/bitbucket-pipelines/bitbucket-pipelines.yml new file mode 100644 index 0000000..a862b93 --- /dev/null +++ b/examples/bitbucket-pipelines/bitbucket-pipelines.yml @@ -0,0 +1,11 @@ +image: docksal/ci-agent:edge-php + +pipelines: + default: + - step: + script: + - source build-env # Initialize the agent configuration + - build-init # Initialize the remote sandbox environment + - slack "Started sandbox build for branch *${BITBUCKET_BRANCH}*" # post to Slack + - ssh docker-host "cd $REMOTE_BUILD_DIR && fin init" # Run fin init on the remote docker host to provision a sandbox + - slack "Completed sandbox build for branch *${BITBUCKET_BRANCH}*:\n" diff --git a/php/Dockerfile b/php/Dockerfile index bf205d4..cd22a6d 100644 --- a/php/Dockerfile +++ b/php/Dockerfile @@ -1,54 +1,57 @@ -FROM docksal/bitbucket-pipelines-agent:edge +FROM docksal/ci-agent:base # Switch to root to install some system-wide stuff USER root RUN apk add --update --no-cache \ - php7 \ - php7-ctype \ - php7-curl \ - php7-dom \ - php7-json \ - php7-mbstring \ - php7-openssl \ - php7-phar \ - php7-xml \ - && rm -rf /var/cache/apk/* \ - && ln -s /usr/bin/php7 /usr/bin/php + php7 \ + php7-ctype \ + php7-curl \ + php7-dom \ + php7-gd \ + php7-json \ + php7-mbstring \ + php7-openssl \ + php7-phar \ + php7-simplexml \ + php7-tokenizer \ + php7-xml \ + php7-xmlreader \ + php7-xmlwriter \ + php7-zlib; \ + rm -rf /var/cache/apk/*; -ENV COMPOSER_VERSION 1.3.0 -ENV DRUSH_VERSION 8.1.9 -ENV DRUPAL_CONSOLE_VERSION 1.0.0-rc15 -ENV WPCLI_VERSION 1.1.0 +ENV COMPOSER_VERSION=1.6.3 +ENV DRUSH_VERSION 8.1.16 +ENV DRUPAL_CONSOLE_VERSION 1.7.0 +ENV WPCLI_VERSION 1.5.0 RUN \ - # Composer - curl -sSL "https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar" -o /usr/local/bin/composer && \ - # Drush 8 (default) - curl -sSL "https://github.com/drush-ops/drush/releases/download/${DRUSH_VERSION}/drush.phar" -o /usr/local/bin/drush && \ - # Drupal Console - curl -sSL "https://github.com/hechoendrupal/drupal-console-launcher/releases/download/${DRUPAL_CONSOLE_VERSION}/drupal.phar" -o /usr/local/bin/drupal && \ - # Install wp-cli - curl -sSL "https://github.com/wp-cli/wp-cli/releases/download/v${WPCLI_VERSION}/wp-cli-${WPCLI_VERSION}.phar" -o /usr/local/bin/wp && \ - # Make all binaries executable - chmod +x /usr/local/bin/* + # Composer + curl -sSL "https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar" -o /usr/local/bin/composer; \ + # Drush 8 (default) + curl -sSL "https://github.com/drush-ops/drush/releases/download/${DRUSH_VERSION}/drush.phar" -o /usr/local/bin/drush; \ + # Drupal Console + curl -sSL "https://github.com/hechoendrupal/drupal-console-launcher/releases/download/${DRUPAL_CONSOLE_VERSION}/drupal.phar" -o /usr/local/bin/drupal; \ + # Install wp-cli + curl -sSL "https://github.com/wp-cli/wp-cli/releases/download/v${WPCLI_VERSION}/wp-cli-${WPCLI_VERSION}.phar" -o /usr/local/bin/wp; \ + # Make all binaries executable + chmod +x /usr/local/bin/*; # Switch back to user -ARG AGENT_USER=agent USER $AGENT_USER -ENV PATH $PATH:$HOME/.composer/vendor/bin +ENV PATH $PATH:$AGENT_HOME/.composer/vendor/bin RUN \ - # Add composer bin directory to PATH - echo "\n"'PATH="$PATH:$HOME/.composer/vendor/bin"' >> $HOME/.profile && \ - # Drush modules - drush dl registry_rebuild --default-major=7 --destination=$HOME/.drush && \ - drush cc drush && \ - # Drupal Coder w/ a matching version of PHP_CodeSniffer - composer global require drupal/coder && \ - phpcs --config-set installed_paths $HOME/.composer/vendor/drupal/coder/coder_sniffer && \ - # Cleanup - composer clear-cache + # Add composer bin directory to PATH + echo "\n"'PATH="$PATH:$AGENT_HOME/.composer/vendor/bin"' >> $AGENT_HOME/.profile; \ + # Drush modules + drush dl registry_rebuild --default-major=7 --destination=$AGENT_HOME/.drush; \ + drush cc drush; \ + # Drupal Coder w/ a matching version of PHP_CodeSniffer + composer global require drupal/coder; \ + phpcs --config-set installed_paths $AGENT_HOME/.composer/vendor/drupal/coder/coder_sniffer; \ + # Cleanup + composer clear-cache; -COPY config/.drush $HOME/.drush -# Fix permissions after COPY -RUN sudo chown -R $AGENT_USER:$AGENT_USER $HOME +# Cannot use variables with COPY --chown. See https://github.com/moby/moby/issues/35018 +COPY --chown=agent:agent config/.drush $AGENT_HOME/.drush diff --git a/scripts/push-image.sh b/scripts/push-image.sh new file mode 100755 index 0000000..b2fca72 --- /dev/null +++ b/scripts/push-image.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Pushes an image to Docker Hub + +VERSION=$1 + +[[ "${TRAVIS_BRANCH}" == "develop" ]] && TAG="edge-${VERSION}" +[[ "${TRAVIS_BRANCH}" == "master" ]] && TAG="${VERSION}" +[[ "${TRAVIS_TAG}" != "" ]] && TAG="${TRAVIS_TAG:1:3}-${VERSION}" + +if [[ "$TAG" != "" ]]; then + docker login -u "${DOCKER_USER}" -p "${DOCKER_PASS}" + # Push edge, stable and release tags + docker tag ${REPO}:${VERSION} ${REPO}:${TAG} + docker push ${REPO}:${TAG} + + # Push "latest" tag + if [[ "${TRAVIS_BRANCH}" == "master" ]] && [[ "${VERSION}" == "base" ]]; then + docker tag ${REPO}:${VERSION} ${REPO}:latest + docker push ${REPO}:latest + fi +fi diff --git a/tests/base.bats b/tests/base.bats new file mode 100755 index 0000000..3a3e402 --- /dev/null +++ b/tests/base.bats @@ -0,0 +1,41 @@ +#!/usr/bin/env bats + +# Debugging +teardown() { + echo + echo "Last command status: $status" + echo "Last command output:" + echo "================================================================" + echo "$output" + echo "================================================================" +} + +# Global skip +# Uncomment below, then comment skip in the test you want to debug. When done, reverse. +#SKIP=1 + +@test "Check binaries" { + [[ $SKIP == 1 ]] && skip + + ### Setup ### + make start + + ### Tests ### + run make exec COMMAND="docker --version" + [[ "$status" == 0 ]] + echo "$output" | grep "Docker version" + unset output + + run make exec COMMAND="docker-compose --version" + [[ "$status" == 0 ]] + echo "$output" | grep "docker-compose version" + unset output + + run make exec COMMAND="mc --help" + [[ "$status" == 0 ]] + echo "$output" | grep "VERSION" + unset output + + ### Cleanup ### + make clean +} diff --git a/tests/php-modules.txt b/tests/php-modules.txt new file mode 100644 index 0000000..5b03813 --- /dev/null +++ b/tests/php-modules.txt @@ -0,0 +1,28 @@ +[PHP Modules] +Core +ctype +curl +date +dom +filter +gd +hash +json +libxml +mbstring +openssl +pcre +Phar +readline +Reflection +SimpleXML +SPL +standard +tokenizer +xml +xmlreader +xmlwriter +zlib + +[Zend Modules] + diff --git a/tests/php.bats b/tests/php.bats new file mode 100755 index 0000000..5dc1e76 --- /dev/null +++ b/tests/php.bats @@ -0,0 +1,74 @@ +#!/usr/bin/env bats + +# Debugging +teardown() { + echo + echo "Last command status: $status" + echo "Last command output:" + echo "================================================================" + echo "$output" + echo "================================================================" +} + +# Global skip +# Uncomment below, then comment skip in the test you want to debug. When done, reverse. +#SKIP=1 + +@test "Base tests" { + [[ $SKIP == 1 ]] && skip + + run bats tests/base.bats + [[ "$status" == 0 ]] + unset output +} + +@test "Check binaries" { + [[ $SKIP == 1 ]] && skip + + ### Setup ### + make start + + ### Tests ### + run make exec COMMAND="php --version" + [[ "$status" == 0 ]] + echo "$output" | grep "PHP" + unset output + + run make exec COMMAND="composer --version" + [[ "$status" == 0 ]] + echo "$output" | grep "Composer version" + unset output + + run make exec COMMAND="drush --version" + [[ "$status" == 0 ]] + echo "$output" | grep "Drush Version" + unset output + + run make exec COMMAND="drupal --version" + [[ "$status" == 0 ]] + echo "$output" | grep "Drupal Console Launcher" + unset output + + run make exec COMMAND="wp --version" + [[ "$status" == 0 ]] + echo "$output" | grep "WP-CLI" + unset output + + ### Cleanup ### + make clean +} + +@test "Check PHP modules" { + [[ $SKIP == 1 ]] && skip + + ### Setup ### + make start + + # Check PHP modules + run bash -c "docker exec '${NAME}' php -m | diff tests/php-modules.txt -" + [[ ${status} == 0 ]] + unset output + + ### Cleanup ### + make clean +}