diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e69de29
diff --git a/.gittrees b/.gittrees
new file mode 100644
index 0000000..020bd5c
--- /dev/null
+++ b/.gittrees
@@ -0,0 +1,13 @@
+# Git Subtrees
+
+# The advantage of subtrees is that users don't have to care about them - its
+# just a single repo. The disadvantage is that git doesn't track the metadata
+# as it does for submodules.
+
+# This file provides an enumeration of the subtrees in this repo, and the URLs
+# they came from.
+
+# subtree_path remote_url remote_name
+test-utils/bats https://github.com/bats-core/bats bats
+test-utils/bats-support https://github.com/ztombol/bats-support bats-support
+test-utils/bats-assert-1 https://github.com/jasonkarns/bats-assert-1 bats-assert-1
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..61c37d3
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,47 @@
+#!/usr/bin/env groovy
+
+pipeline {
+ agent { label 'executor-v2' }
+
+ options {
+ timestamps()
+ buildDiscarder(logRotator(numToKeepStr: '30'))
+ }
+
+ triggers {
+ cron(getDailyCronString())
+ }
+
+ environment {
+ BATS_OUTPUT_FORMAT="junit"
+ }
+
+ stages {
+
+ stage('BATS Tests') {
+ steps {
+ sh './tests-for-this-repo/run-bats-tests'
+ }
+ }
+
+ stage('Python Linting') {
+ steps {
+ sh './tests-for-this-repo/python-lint'
+ }
+ }
+
+ stage('Secrets Leak Check') {
+ steps {
+ sh './tests-for-this-repo/run-gitleaks'
+ }
+ }
+
+ }
+
+ post {
+ always {
+ junit '*-junit.xml'
+ cleanupAndNotify(currentBuild.currentResult)
+ }
+ }
+}
diff --git a/README.md b/README.md
index d8aa8e3..eb8dd01 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,201 @@
-# bashlib
-Common bash functions for use in test pipelines
+# bash-lib
+```
+ _______________ _______________
+ .' .' .|
+ .' .' .' |
+ .'_______________.'______________ .' |
+ | ___ _____ ___ || ___ _____ ___ | |
+ ||_=_|__=__|_=_||||_=_|__=__|_=_|| |
+ ______||_____===_____||||_____===_____|| | __________
+ .' ||_____===_____||||_____===_____|| .' .'|
+ .' ||_____===_____||||_____===_____|| .' .' |
+.'___________|_______________||_______________|.'__________.' |
+|.----------.|.-----___-----.||.-----___-----.|| |_____.----------.
+|] |||_____________||||_____________||| .' [ |
+|| ||.-----___-----.||.-----___-----.||.' | |
+|| |||_____________||||_____________|||==========| |
+|| ||.-----___-----.||.-----___-----.|| |_____| |
+|] o|||_____________||||_____________||| .' [ 'o|
+|| ||.-----___-----.||.-----___-----.||.' | |
+|| ||| ||||_____________|||==========| |
+|| ||| |||.-----___-----.|| |_____| |
+|] ||| |||| ||| .' [ |
+||__________|||_____________||||_____________|||.'________|__________|
+''----------'''------------------------------'''----------''
+ (o)LGB (o)
+```
+
+The place to store functions that are used in pipelines for multiple repos.
+
+Please add whatever is useful to you, but keep it tidy so its still useful to everyone else :)
+
+## Usage
+
+Add bash-lib into your project in the way that best fits your workflow. The only requirement is that you **pin the version of
+bash-lib that you use**. This is important so that changes to bash-lib do not have the power to break all projects that use
+bash-lib. Your project can then test updates to bash-lib and roll forward periodicly.
+
+Options:
+* Add a submodule: they are an easy way to integrate bash-lib and automatically use a single SHA until manually updated. Submodules add a pointer from a mount point in your repo to the external repo (bash-lib), and require workflow changes to ensure that pointer is derferenced during clone, checkout and some other opertaions.
+* Add a subtree: This repo uses subtrees to pull in test dependencies. Various helpers for working with subtrees can be found in [git/lib](git/lib). Subtrees copy an external repo into a subdirectory of the host repo, no workflow changes are required. Tools are provided for updating the subtree. Subtrees naturally keep a single version of bash-lib until explicitly updated. Note that subtree merge commits do not rebase well :warning:, so best to keep subtree updates in separate PRs from normal commits.
+* Clone bash-lib in your deployment process, bash-lib doesn't have to be within your repo, just needs to be somewhere where your scripts can source [init](init). This is where it's most important that you implement a mechanism to always use the same SHA, as a **clone will track master by default, which is not an allowed use of bash-lib**.
+
+Once you have bash-lib cloned in your project, you source two things:
+
+1. Source `bash-lib/init`. This ensures submodules are initalised and sets the BASH_LIB_DIR env var to the absolute path to the bash-lib dir. This makes it easy to source libraries from other scripts.
+2. Source `${BASH_LIB_DIR}/lib-name/lib` for any libraries you are interested in.
+
+You are now ready to use bash-lib functions :)
+
+## Structure
+The `/init` script sets up everything required to use the library, most
+importantly the `BASH_LIB_DIR` variable which gives the absolute path to the root
+of the library and should be used for sourcing the modules.
+
+The repo is organized into libraries, each library is a directory that has a
+lib file. Sourcing the lib for a library should expose all the functions
+that library offers. The lib file may source or reference other supporting
+files within it's directory.
+
+```
+.
+├── libname
+│ ├── lib
+│ └── supporting-file
+├── init # init script, source this first
+├── run-tests # top level test script, executes all tests
+├── secrets.yml # secrets required for executing tests
+├── test-utils
+│ ├── bats # subtree
+│ ├── bats-assert-1 # subtree
+│ ├── bats-support # subtree
+│ ├── lib
+│ └── tap2junit
+└── tests-for-this-repo
+ ├── filehandling.bats
+ ├── fixtures #
+ │ └── libname # Dir containing test fixtures for a library
+ ├── tap2junit
+ ├── libname.bats # contains tests for libname/lib
+ ├── python-lint # supporting files for python lint
+ ├── run-bats-tests # script to run bats tests
+ ├── run-gitleaks # script to check for leaked secrets
+ └── run-python-lint # script to run python lint
+```
+## Style Guide
+Follow the [google shell style guide](https://google.github.io/styleguide/shell.xml#Naming_Conventions).
+TL;DR:
+1. Use snake_case function and variable names
+1. Use `function` when declaring functions.
+
+
+## Contents
+
+
+
+
+
+ Library |
+ Description |
+ Functions |
+
+
+
+
+ filehandling |
+ Functions relating to file and path handling
+ |
+
+ - abs_path: Ensure a path is absolute
+
+ |
+
+
+ git |
+ Git helpers |
+
+
+ - repo_root: Find the root of the current git repo.
+ - all_files_in_repo: List files tracked by git.
+ - tracked_files_excluding_subtrees: List files tracked by git, but excluding any files that are in paths listed in
.gittrees .
+ - cat_gittrees: Returns the contents of .gittrees from the top level of the repo, excluding any comments. Fails if .gittrees is not present.
+ - ensure_gittrees: Creates git trees with header comments if it doesn't already exist.
+ - add_subtree: Adds a subtree to the current repo, and stores the path, remote_url and remote_name in .gittrees so that this subtree can be maniuplated in future.
+ - add_gittree_remotes: Read .gittrees and add git remotes for each remote_url and remote_name pair.
+ - update_subtree: Changes a subtree to reflect a different ref in the upstream repo. Checks if the new ref is the same as the current ref before proceeding.
+ - remote_latest_tag: Returns the symbolic name of the latest tag from a remote.
+ - remote_latest_tagged_commit: Returns the SHA of the most recently tagged commit in a remote repo (
tag^{} ).
+ - remote_sha_for_ref: Returns the SHA for a given ref from a named remote.
+ - remote_tag_for_sha: Returns the tag corresponding to a SHA from a named remote - if there is one.
+ - update_subtrees_to_latest_tag: Read .gittrees and update all listed subtrees to the newest tag from their upstream repo.
+
+ |
+
+ helpers |
+ Bash scripting helpers |
+
+
+ - die: print message and exit 1
+ - spushd/spopd: Safe verisons of pushd & popd that call die if the push/pop fails, they also drop stdout.
+
+ |
+
+
+ k8s |
+ Utils for connecting to K8s |
+
+
+ - build_gke_image: Build docker image for running kubectl commands against GKE.
+ - delete_gke_image: Delete image from GKE.
+ - run_docker_gke_command: Run command in gke-utils container, already authenticated to k8s cluster.
+
+ |
+
+
+ logging |
+ Helpers related to login |
+
+
+ - announce: Echo message in ascii banner to distinguish it from other log messages.
+
+ |
+
+
+ test-utils |
+ Helpers for executing tests |
+
+
+ - shellcheck_script: Execute shellcheck against a script, uses docker.
+ - find_scripts: Find git tracked files with extension.
+ - tap2junit: Convert a subset of TAP to JUnit XML. Retains logs for errors.
+
+ |
+
+
+
+
+## Testing
+Tests are written using [BATS](https://github.com/bats-core/bats). Each libould have a `lib-name.bats` file in [tests-for-this-repo](/tests-for-this-repo).
+Asserts are provided by [bats-assert-1](https://github.com/jasonkarns/bats-assert-1). The value in these is that they provide useful debugging output when the assertion fails, eg expected x got y.
+
+Example:
+```bash
+# source support and assert libraries
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+# source the library under test
+. "${BASH_LIB_DIR}/git/lib"
+
+# define a test that calls a library function
+@test "it does the thing" {
+ some_prep_work
+ # run is a wrapper that catches failures so that assertsions can be run,
+ # otherwise the test would immediately fail.
+ run does_the_thing
+ assert_success
+ assert_output "thing done"
+}
+```
+
+Test fixtures should go in /tests-for-this-repo/[fixtures](tests-for-this-repo/fixtures)/lib-name.
\ No newline at end of file
diff --git a/filehandling/lib b/filehandling/lib
new file mode 100644
index 0000000..4cd3f47
--- /dev/null
+++ b/filehandling/lib
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+#https://stackoverflow.com/a/23002317
+function abs_path() {
+ # generate absolute path from relative path
+ # $1 : relative filename
+ # return : absolute path
+ if [ -d "$1" ]; then
+ # dir
+ (spushd "$1"; pwd)
+ elif [ -f "$1" ]; then
+ # file
+ if [[ $1 = /* ]]; then
+ echo "$1"
+ elif [[ $1 == */* ]]; then
+ echo "$(spushd "${1%/*}"; pwd)/${1##*/}"
+ else
+ echo "$(pwd)/$1"
+ fi
+ fi
+}
\ No newline at end of file
diff --git a/git/lib b/git/lib
new file mode 100644
index 0000000..7194f20
--- /dev/null
+++ b/git/lib
@@ -0,0 +1,171 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+# Get the top level of a git repo
+function repo_root(){
+ git rev-parse --show-toplevel
+}
+
+# List files tracked by git
+function all_files_in_repo(){
+ git ls-tree -r HEAD --name-only
+}
+
+# List files tracked by git, excluding paths
+# listed as subtrees in /.gittrees
+function tracked_files_excluding_subtrees(){
+ subtrees="$(cat_gittrees | awk '{print $1}' | paste -sd '|' -)"
+ all_files_in_repo | grep -E -v "${subtrees}"
+}
+
+####
+# Subtree Reference:
+# https://github.com/git/git/blob/master/contrib/subtree/git-subtree.txt
+####
+
+# Read the .gittrees file from the root of a repo,
+# Die with message describing the format of the file
+# if it doesn't exist.
+function cat_gittrees(){
+ local -r git_trees="$(repo_root)/.gittrees"
+ local -r subtrees_file_format=".gittrees should contain one subtree per line,\
+space seperated with three fields: subtree_path renmote_url remote_name"
+ [[ -e "${git_trees}" ]] || die ".gittrees file ${git_trees} not found. ${subtrees_file_format}"
+ grep -E -v '^\s*$|^\s*#' "$(repo_root)/.gittrees"
+}
+
+function ensure_gittrees(){
+ local -r gittrees="$(repo_root)/.gittrees"
+ if [[ -e "${gittrees}" ]]; then
+ echo "${gittrees} already exists"
+ else
+ cat > "${gittrees}" <> "${gittrees}"
+ echo "Added Subtree ${subtree_prefix} to ${gittrees}"
+ add_gittree_remotes
+ git subtree add --squash --prefix "${subtree_prefix}" "${remote_name}" "${ref}"
+ echo "Merged ${remote_url}@${ref} to ${subtree_prefix}"
+ fi
+}
+
+# Read .gittrees and add git remote for each subtree
+function add_gittree_remotes(){
+ cat_gittrees | while read -r _ url remote
+ do git remote rm "${remote}" &>/dev/null || true
+ git remote add -f "${remote}" "${url}"
+ done
+}
+
+# Update a subtree to a new ref
+function update_subtree(){
+ local -r subtree_prefix="${1}"
+ local -r remote_name="${2}"
+ local -r remote_ref="${3}"
+
+ local -r current_subtree_sha="$(git subtree split --prefix "${subtree_prefix}")"
+ local -r remote_sha="$(remote_sha_for_ref "${remote_name}" "${remote_ref}")"
+
+ if [[ "${current_subtree_sha}" == "${remote_sha}" ]]; then
+ echo "${subtree_prefix}@${current_subtree_sha} is up to date with ${remote_url}@${remote_sha} (${remote_ref}), not updating."
+ else
+ echo "${subtree_prefix}@${current_subtree_sha} does not match ${remote_url}@${remote_sha} (${remote_ref}), updating."
+ spushd "$(repo_root)"
+ git subtree pull \
+ --squash \
+ "--prefix=${subtree_prefix}" \
+ "${remote_name}" \
+ "${remote_ref}"
+ spopd
+ fi
+}
+
+# Find the latest tag available at a repo url
+# Returns tag name, not sha
+function remote_latest_tag(){
+ local -r remote_url="${1}"
+ # In ls-remote the ^{} suffix refers to a peeled/dereferenced object.
+ # eg refs/tags/v0.0.1^{} shows the SHA of the commit that was tagged,
+ # not the SHA of the tag itself.
+ # Adding --refs hides peeled tags
+ git ls-remote --tags --refs --quiet \
+ "${remote_url}" \
+ | tail -n 1 \
+ | cut -f 2 \
+ | sed -e 's+refs/tags/++'
+}
+
+# Find the SHA of the latests commit to be tagged in a remote repo
+function remote_latest_tagged_commit(){
+ local -r remote="${1}"
+ local -r tag="$(remote_latest_tag "${remote}")"
+ git ls-remote "${remote}" | awk "/refs\/tags\/${tag}\^/{print \$1}"
+}
+
+function remote_sha_for_ref(){
+ local -r remote="${1}"
+ local -r ref="${2}"
+
+ # First try adding ^{} to the ref, incase it's a tag
+ # and needs peeling. If nothing is found for that,
+ # try without.
+ peeled_ref=$(
+ git ls-remote "${remote}" \
+ | awk "/${ref}\^{}/{print \$1}"
+ )
+
+ if [[ -n "${peeled_ref}" ]]; then
+ echo "${peeled_ref}"
+ else
+ git ls-remote "${remote}" \
+ | awk "/${ref}/{print \$1}"
+ fi
+}
+
+function remote_tag_for_sha(){
+ local -r remote="${1}"
+ local -r sha="${2}"
+ git ls-remote "${remote}" \
+ | awk -F'/' "/${sha}.*tag/{ gsub(/\^{}\$/, \"\"); print \$3 }"
+}
+
+# Read .gittrees and update all to the latest
+# tag available from the upstream.
+function update_subtrees_to_latest_tag(){
+ add_gittree_remotes
+ cat_gittrees | while read -r subtree_prefix remote_url remote_name
+ do
+ local latest_remote_tag
+ latest_remote_tag="$(remote_latest_tag "${remote_name}")"
+ update_subtree "${subtree_prefix}" "${remote_name}" "${latest_remote_tag}"
+ done
+}
\ No newline at end of file
diff --git a/helpers/lib b/helpers/lib
new file mode 100644
index 0000000..f4cf58e
--- /dev/null
+++ b/helpers/lib
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+
+function die(){
+ echo "${@}"
+ exit 1
+}
+
+#safe pushd
+function spushd(){
+ if ! pushd "${1}" >/dev/null; then
+ die "pushd ${1} failed :("
+ fi
+}
+
+#safe popd
+function spopd(){
+ popd >/dev/null || die "popd failed :("
+}
\ No newline at end of file
diff --git a/init b/init
new file mode 100644
index 0000000..c55fd63
--- /dev/null
+++ b/init
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+## Initialisation Functions for the
+## Conjurinc Bash Library
+
+# Shell Otions
+set -euo pipefail
+
+# This script should be sourced before any of
+# the other scripts in this repo. Other scripts
+# make use of ${BASH_LIB_DIR} to find each other.
+
+# Get the relative path to the repo root
+# shellcheck disable=SC2086
+BASH_LIB_DIR_RELATIVE="$(dirname ${BASH_SOURCE[0]})"
+
+# Must be set in order to load the filehandling
+# module. Will be updated when abs_path is available.
+BASH_LIB_DIR="${BASH_LIB_DIR_RELATIVE}"
+
+# Load the filehandling module for the abspath
+# function
+. "${BASH_LIB_DIR_RELATIVE}/filehandling/lib"
+
+# Export the absolute path
+# shellcheck disable=SC2086
+BASH_LIB_DIR="$(abs_path ${BASH_LIB_DIR_RELATIVE})"
+export BASH_LIB_DIR
+
+. "${BASH_LIB_DIR}/helpers/lib"
+
+# Update Submodules
+spushd "${BASH_LIB_DIR}"
+ git submodule update --init --recursive
+spopd
+
+export BATS_CMD="${BASH_LIB_DIR}/test-utils/bats/bin/bats"
\ No newline at end of file
diff --git a/k8s/Dockerfile b/k8s/Dockerfile
new file mode 100644
index 0000000..f1a8e2f
--- /dev/null
+++ b/k8s/Dockerfile
@@ -0,0 +1,33 @@
+FROM google/cloud-sdk
+
+ARG KUBECTL_CLI_URL
+
+RUN mkdir -p /src \
+ /scripts
+WORKDIR /src
+COPY platform_login /scripts
+
+# Install Docker client
+RUN apt-get update -y \
+ && apt-get install \
+ -y \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ gnupg2 \
+ software-properties-common \
+ wget \
+ && curl \
+ -fsSL \
+ https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg \
+ | apt-key add - \
+ && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable"\
+ && apt-get update \
+ && apt-get install -y docker-ce \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install kubectl CLI
+RUN wget -O \
+ /usr/local/bin/kubectl \
+ ${KUBECTL_CLI_URL:-https://storage.googleapis.com/kubernetes-release/release/v1.7.6/bin/linux/amd64/kubectl} \
+ && chmod +x /usr/local/bin/kubectl
\ No newline at end of file
diff --git a/k8s/lib b/k8s/lib
new file mode 100644
index 0000000..6bea766
--- /dev/null
+++ b/k8s/lib
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+# Sets additional required environment variables that aren't available in the
+# secrets.yml file, and performs other preparatory steps
+function build_gke_image() {
+ local image="gke-utils:latest"
+ local rc=0
+ docker rmi ${image} || true
+ spushd "${BASH_LIB_DIR}/k8s"
+ # Prepare Docker images
+ docker build --tag "${image}"\
+ --build-arg KUBECTL_CLI_URL="${KUBECTL_CLI_URL}" \
+ . 1>&2
+ rc=${?}
+ spopd
+
+ return ${rc}
+}
+
+# Delete an image from GCR, unless it is has multiple tags pointing to it
+# This means another parallel build is using the image and we should
+# just untag it to be deleted by the later job
+function delete_gke_image() {
+ local image_and_tag="${1}"
+
+ run_docker_gke_command "
+ gcloud container images delete --force-delete-tags -q ${image_and_tag}
+ "
+}
+
+function run_docker_gke_command() {
+ docker run --rm \
+ -i \
+ -e DOCKER_REGISTRY_URL \
+ -e DOCKER_REGISTRY_PATH \
+ -e GCLOUD_SERVICE_KEY="/tmp${GCLOUD_SERVICE_KEY}" \
+ -e GCLOUD_CLUSTER_NAME \
+ -e GCLOUD_ZONE \
+ -e SECRETLESS_IMAGE \
+ -e KUBECTL_CLI_URL \
+ -e GCLOUD_PROJECT_NAME \
+ -v "${GCLOUD_SERVICE_KEY}:/tmp${GCLOUD_SERVICE_KEY}" \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v ~/.config:/root/.config \
+ -v "${PWD}:/src" \
+ -w /src \
+ "gke-utils:latest" \
+ bash -ec "
+ /scripts/platform_login
+ ${1}
+ "
+}
\ No newline at end of file
diff --git a/k8s/platform_login b/k8s/platform_login
new file mode 100755
index 0000000..ef00b5b
--- /dev/null
+++ b/k8s/platform_login
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -euo pipefail
+
+function main() {
+ echo Starting platform login
+
+ gcloud auth activate-service-account \
+ --key-file "${GCLOUD_SERVICE_KEY}"
+
+ gcloud container clusters get-credentials \
+ "${GCLOUD_CLUSTER_NAME}" \
+ --zone "${GCLOUD_ZONE}" \
+ --project "${GCLOUD_PROJECT_NAME}"
+
+ docker login "${DOCKER_REGISTRY_URL}" \
+ -u oauth2accesstoken \
+ -p "$(gcloud auth print-access-token)"
+
+ echo Platform Login Complete
+}
+
+main
diff --git a/logging/lib b/logging/lib
new file mode 100644
index 0000000..76d31b5
--- /dev/null
+++ b/logging/lib
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+
+# Add logging functions here
+
+function announce() {
+ echo "++++++++++++++++++++++++++++++++++++++"
+ echo " "
+ echo "$@"
+ echo " "
+ echo "++++++++++++++++++++++++++++++++++++++"
+}
\ No newline at end of file
diff --git a/run-tests b/run-tests
new file mode 100755
index 0000000..f099ee4
--- /dev/null
+++ b/run-tests
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# This script is an entry point, so init
+# is not assumed to have been run
+# shellcheck disable=SC2086
+. "$(dirname ${BASH_SOURCE[0]})/init"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+# Run BATS Tests
+"${BASH_LIB_DIR}/tests-for-this-repo/run-bats-tests"
+
+# Run Python Lint
+"${BASH_LIB_DIR}/tests-for-this-repo/run-python-lint"
+
+# Run gitleaks
+"${BASH_LIB_DIR}/tests-for-this-repo/run-gitleaks"
diff --git a/secrets.yml b/secrets.yml
new file mode 100644
index 0000000..321796f
--- /dev/null
+++ b/secrets.yml
@@ -0,0 +1,9 @@
+KUBECTL_CLI_URL: https://storage.googleapis.com/kubernetes-release/release/v1.9.7/bin/linux/amd64/kubectl
+
+GCLOUD_CLUSTER_NAME: !var ci/google-container-engine-testbed/gcloud-cluster-name
+GCLOUD_ZONE: !var ci/google-container-engine-testbed/gcloud-zone
+GCLOUD_PROJECT_NAME: !var ci/google-container-engine-testbed/gcloud-project-name
+GCLOUD_SERVICE_KEY: !var:file ci/google-container-engine-testbed/gcloud-service-key
+
+DOCKER_REGISTRY_URL: us.gcr.io
+DOCKER_REGISTRY_PATH: us.gcr.io/conjur-gke-dev
diff --git a/test-utils/lib b/test-utils/lib
new file mode 100644
index 0000000..5eab41f
--- /dev/null
+++ b/test-utils/lib
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+: "${BASH_LIB_DIR:?BASH_LIB_DIR must be set. Please source bash-lib/init before other scripts from bash-lib.}"
+. "${BASH_LIB_DIR}/git/lib"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+readonly SHELLCHECK_IMAGE="${SHELLCHECK_IMAGE:-koalaman/shellcheck}"
+readonly SHELLCHECK_TAG="${SHELLCHECK_TAG:-v0.6.0}"
+
+# Check a single shell script for syntax
+# and common errors.
+function shellcheck_script(){
+ # NOTE (HughSaunders): I tried using the checkstyle output of
+ # _shellcheck along with a checkstyle2junit xslt stylesheet
+ # from the shellcheck author. However Jenkins only reported
+ # on error per file, as the style sheet created a test element
+ # per file, with mulitple failure elements within.
+ # Jenkins expects one failure element per test.
+
+ local -r script="${1}"
+ echo -e "\nChecking ${script}"
+
+ # SC1091 - sourced scripts are not followed, ok because all scripts in the repo are found.
+ # SC1090 - can't follow non-constant source, ok for because all scripts are checked.
+ local -r ignores="-e SC1091 -e SC1090"
+
+ if bash -n "${script}"; then
+ # shellcheck disable=SC2086
+ docker run --rm -v "${PWD}:/mnt" ${SHELLCHECK_IMAGE}:${SHELLCHECK_TAG} ${ignores} ${script}
+ else
+ return 1
+ fi
+}
+
+function find_scripts(){
+ tracked_files_excluding_subtrees \
+ | while read -r file; do
+ grep --files-with-match '^#!.*bash' "${file}" || true
+ done
+}
+
+function tap2junit(){
+ local -r suite="${1:-BATS}"
+
+ spushd "${BASH_LIB_DIR}/test-utils/tap2junit"
+ docker build . -t tap-junit 1>&2
+ spopd
+
+ # Run tap-junit docker image to convert BATS TAP output to Junit for consumption by jenkins
+ # filters stdin to stdout
+ docker run --rm -i tap-junit -s "${suite}"
+}
diff --git a/test-utils/tap2junit/Dockerfile b/test-utils/tap2junit/Dockerfile
new file mode 100644
index 0000000..107fab6
--- /dev/null
+++ b/test-utils/tap2junit/Dockerfile
@@ -0,0 +1,4 @@
+FROM python:3.7-alpine
+ENTRYPOINT python tap2junit.py
+COPY requirements.txt constraints.txt tap2junit.py /
+RUN pip install --no-cache-dir -r requirements.txt -c constraints.txt
\ No newline at end of file
diff --git a/test-utils/tap2junit/constraints.txt b/test-utils/tap2junit/constraints.txt
new file mode 100644
index 0000000..9990d59
--- /dev/null
+++ b/test-utils/tap2junit/constraints.txt
@@ -0,0 +1,2 @@
+junit-xml==1.8
+six==1.12.0
diff --git a/test-utils/tap2junit/requirements.txt b/test-utils/tap2junit/requirements.txt
new file mode 100644
index 0000000..ac04d3f
--- /dev/null
+++ b/test-utils/tap2junit/requirements.txt
@@ -0,0 +1 @@
+junit-xml
diff --git a/test-utils/tap2junit/tap2junit.py b/test-utils/tap2junit/tap2junit.py
new file mode 100755
index 0000000..620c3ac
--- /dev/null
+++ b/test-utils/tap2junit/tap2junit.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python
+
+import os
+import re
+import sys
+from junit_xml import TestSuite, TestCase
+
+
+class Tap2JUnit:
+ """ This class reads a subset of TAP (Test Anything protocol)
+ and writes JUnit XML.
+
+ Two line formats are read:
+ 1. (not )?ok testnum testname
+ 2. # diagnostic output
+
+ 1. Starts a new test result.
+ 2. Adds diagnostic information to the last read result
+
+ Any 2. lines found before a 1. line are ignored.
+ Any lines not matching either pattern are ignored.
+
+ This script was written because none of the tap2junit converters
+ I could find inserted the failure output into the junit correctly.
+ And IMO a failed test with no indication of why is useless.
+ """
+
+ def __init__(self, test_suite, test_class):
+ self.test_suite = test_suite
+ self.test_class = test_class
+ # This Regex matches a (not) ok testnum testname line from the
+ # TAP specification, using named capture groups
+ self.result_re = re.compile(
+ r"^(?Pnot )?ok\s*(?P[0-9])+\s*(?P.*)$")
+ self.comment_re = re.compile(r"^\s*#")
+ self.case = None
+ self.cases = []
+
+ def process_line(self, line):
+ """ This funuction reads a tap stream line by line
+ and groups the diagnostic output with the relevant
+ result in a dictionary.
+
+ Outputs a list of dicts, one for each result
+ """
+ match = self.result_re.match(line)
+ if match:
+ # This line starts a new test result
+ self.case = match.groupdict()
+ self.case['stderr'] = []
+ self.cases.append(self.case)
+
+ return
+
+ match = self.comment_re.match(line)
+ if match and self.case:
+ # This line contains diagnostic
+ # output from a failed test
+ self.case['stderr'].append(re.sub(r'^\s*#', '', line).rstrip())
+
+ def convert(self, infile=sys.stdin, out=sys.stdout):
+ """ Reads a subset of TAP and writes JUnit XML """
+ # read lines
+ for line in infile.readlines():
+ self.process_line(line)
+
+ # Convert line dicts to test case objects
+ case_objs = []
+ for case in self.cases:
+ case_obj = TestCase(case['testname'], self.test_class, 0, '', '')
+ if case['result'] == 'not ':
+ case_obj.add_failure_info(output="\n".join(case['stderr']))
+ case_objs.append(case_obj)
+
+ # Combine test cases into a suite
+ suite = TestSuite(self.test_suite, case_objs)
+
+ # Write the suite out as XML
+ TestSuite.to_file(out, [suite])
+
+
+def main():
+ t2j = Tap2JUnit(
+ os.environ.get('JUNIT_TEST_SUITE', 'tap2junit'),
+ os.environ.get('JUNIT_TEST_CLASS', 'tap2junit')
+ )
+ t2j.convert()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests-for-this-repo/filehandling.bats b/tests-for-this-repo/filehandling.bats
new file mode 100644
index 0000000..1b309fb
--- /dev/null
+++ b/tests-for-this-repo/filehandling.bats
@@ -0,0 +1,16 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/filehandling/lib"
+
+@test "abs_path returns absolute path for PWD" {
+ run abs_path .
+ assert_output $PWD
+ assert_success
+}
+
+@test "abs_path returns same path when already absolute" {
+ run abs_path /tmp
+ assert_output /tmp
+ assert_success
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/fixtures/test-utils/tap2junit.in b/tests-for-this-repo/fixtures/test-utils/tap2junit.in
new file mode 100644
index 0000000..f05a61f
--- /dev/null
+++ b/tests-for-this-repo/fixtures/test-utils/tap2junit.in
@@ -0,0 +1,6 @@
+1..3
+ok 1 Passed Test
+not ok 2 Failed Test
+# Diagnostic output
+# more diagnostic output
+ok 3 Another Passed Test
\ No newline at end of file
diff --git a/tests-for-this-repo/fixtures/test-utils/tap2junit.out b/tests-for-this-repo/fixtures/test-utils/tap2junit.out
new file mode 100644
index 0000000..680ad96
--- /dev/null
+++ b/tests-for-this-repo/fixtures/test-utils/tap2junit.out
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Diagnostic output
+ more diagnostic output
+
+
+
+
diff --git a/tests-for-this-repo/git.bats b/tests-for-this-repo/git.bats
new file mode 100644
index 0000000..d2e06f2
--- /dev/null
+++ b/tests-for-this-repo/git.bats
@@ -0,0 +1,310 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/git/lib"
+
+# run before every test
+setup(){
+ local -r temp_dir="${BATS_TMPDIR}/testtemp"
+ local -r repo_dir="${temp_dir/}/repo"
+ rm -rf "${temp_dir}"
+ mkdir -p "${repo_dir}"
+ pushd ${repo_dir}
+
+ git init
+ git config user.email "ci@cyberark.com"
+ git config user.name "Jenkins"
+ git commit --allow-empty -m "initial"
+ echo "some content" > a_file
+ git add a_file
+ git commit -a -m "some operations fail on empty repos"
+}
+
+teardown(){
+ local -r temp_dir="${BATS_TMPDIR}/testtemp"
+ rm -rf "${temp_dir}"
+}
+
+@test "repo_root returns root of current repo" {
+ pushd ${BASH_LIB_DIR}
+ run repo_root
+ assert_output $PWD
+ assert_success
+}
+
+@test "repo_root fails when not run from a git repo" {
+ pushd /tmp
+ run repo_root
+ assert_failure
+}
+
+@test "all_files_in_repo lists all git tracked files" {
+ # untracked file shouldn't be listed in output
+ date > b
+ run all_files_in_repo
+ assert_output "a_file"
+ assert_success
+}
+
+@test "tracked_files_excluding_subtrees excludes files in subtrees" {
+ run add_subtree bats "https://github.com/bats-core/bats" bats v1.0.0
+ assert_success
+ assert [ -e bats/README.md ]
+
+ date > untracked_file
+
+ run tracked_files_excluding_subtrees
+ refute_output --partial bats
+ refute_output --partial untracked_file
+ assert_output --partial a_file
+ assert_success
+}
+
+@test "cat_gittrees dies when gittrees doesn't exist" {
+ run cat_gittrees
+ assert_failure
+ assert_output --partial "should contain"
+}
+
+@test "cat_gitrees skips comments" {
+ cat >.gittrees <> .gittrees
+ add_gittree_remotes
+ run git remote -v
+ assert_output --regexp 'bats.*https://github.com/bats-core/bats'
+}
+
+@test "update_subtree doesn't add extra commits when no update is necessary" {
+ add_subtree bats "https://github.com/bats-core/bats" bats v1.0.0
+
+ HEAD="$(git rev-parse HEAD)"
+ assert_equal "$(git rev-list --count HEAD)" "4"
+
+ run update_subtree bats bats v1.0.0
+ assert_success
+ assert_output --partial "up to date"
+
+ # HEAD should not have moved, as no update is necessary
+ run git rev-parse HEAD
+ assert_output --partial "${HEAD}"
+ assert_equal "$(git rev-list --count HEAD)" "4"
+}
+
+@test "update_subtree correctly updates subtree to new ref" {
+ v1_1_0_sha="c706d1470dd1376687776bbe985ac22d09780327"
+
+ add_subtree bats "https://github.com/bats-core/bats" bats v1.0.0
+ HEAD="$(git rev-parse HEAD)"
+ assert_equal "$(git rev-list --count HEAD)" "4"
+
+ update_subtree bats bats v1.1.0
+
+ run git subtree split --prefix bats
+ assert_output --partial "${v1_1_0_sha}"
+
+ # HEAD should have moved as git subtree will
+ # have created a squash merge.
+ run git rev-parse HEAD
+ refute_output --partial "${HEAD}"
+ assert_equal "$(git rev-list --count HEAD)" "6"
+}
+
+@test "remote_latest_tag gets latest tag from a remote" {
+ # For this test the "remote" will be local,
+ # because It hard to guarantee an actual remote
+ # won't gain new tags over time.
+
+ date > a
+ git add a
+ git commit -m v1
+ git tag -a -m v1 v1
+
+ date > b
+ git add b
+ git commit -m v2
+ git tag -a -m v2 v2
+
+ run remote_latest_tag .
+ assert_output v2
+ assert_success
+}
+
+@test "remote_latest_tagged_commit returns sha of last tagged commit, not sha of the tag" {
+ date > a
+ git add a
+ git commit -m v1
+ git tag -a -m v1 v1
+
+ date > b
+ git add b
+ git commit -m v2
+
+ run remote_latest_tagged_commit .
+ assert_output "$(git rev-parse v1^{})"
+ assert_success
+}
+
+@test "remote_sha_for_ref looks up a sha for a given ref" {
+ git checkout -b testbranch
+ run remote_sha_for_ref . testbranch
+ assert_output "$(git rev-parse HEAD)"
+ assert_success
+}
+
+@test "remote_tag_for_sha looks up a tag for a given sha" {
+ git tag -a -m v1 v1
+ date > a
+ git add a
+ git commit -m v2
+ git tag -a -m v2 v2
+
+ run remote_tag_for_sha . "$(git rev-parse v1^{})"
+ assert_output v1
+ assert_success
+}
+
+@test "update_subtrees_to_latest_tag updates subtrees to latest respective tag" {
+
+ ## 1. Create the repo that will be added into the main repo as a subtree
+
+ pushd ../
+ mkdir -p subtree_repo
+ pushd subtree_repo
+ subtree_repo="${PWD}"
+ popd
+ popd
+ pushd "${subtree_repo}"
+ git init
+ git config user.email "ci@cyberark.com"
+ git config user.name "Jenkins"
+ git commit --allow-empty -m "initial"
+
+ # commit and tag v1
+ date > file
+ git add file
+ git commit -a -m "added file"
+ v1_sha="$(git rev-parse HEAD)"
+ git tag -a -m v1 v1
+
+ # commit and tag v2
+ date > a
+ git add a
+ git commit -m v2
+ v2_sha="$(git rev-parse HEAD)"
+ git tag -a -m v2 v2
+
+ # commit without tag
+ date > b
+ git add b
+ git commit -m notag
+
+ popd
+
+
+ ## 2. Add the subtree repo twice, at different tags
+ add_subtree test_subtree_1 "${subtree_repo}" tst1_remote v1
+ add_subtree test_subtree_2 "${subtree_repo}" tst2_remote v2
+
+ # Check subtree SHAs are as expected
+ assert_equal "$(git subtree split --prefix test_subtree_1)" "${v1_sha}"
+ assert_equal "$(git subtree split --prefix test_subtree_2)" "${v2_sha}"
+
+ # Check commit count: two initial commits + two per subtree
+ assert_equal "$(git rev-list --count HEAD)" "6"
+
+ # Check "file" exists in boths subtrees
+ assert [ -e test_subtree_1/file ]
+ assert [ -e test_subtree_2/file ]
+
+ # Check "a" only exists in the subtree2
+ assert [ ! -e test_subtree_1/a ]
+ assert [ -e test_subtree_2/a ]
+
+ # Check "b" doesn't exist in either
+ assert [ ! -e test_subtree/b ]
+ assert [ ! -e test_subtree2/b ]
+
+
+ ## 3. Update subtrees
+ run update_subtrees_to_latest_tag
+ assert_success
+
+ # Ensure only one subtree got updated
+ assert_output --regexp 'subtree_2@.*is up to date'
+ assert_output --regexp 'subtree_1@.*does not match'
+
+ # Check subtree SHAs are as expected
+ assert_equal "$(git subtree split --prefix test_subtree_1)" "${v2_sha}"
+ assert_equal "$(git subtree split --prefix test_subtree_2)" "${v2_sha}"
+
+ # Two commits should have been added for a single subtree update
+ assert_equal "$(git rev-list --count HEAD)" "8"
+
+ # Check "file" exists in both subtrees
+ assert [ -e test_subtree_1/file ]
+ assert [ -e test_subtree_2/file ]
+
+ # Check "a" exists in both subtrees
+ assert [ -e test_subtree_1/a ]
+ assert [ -e test_subtree_2/a ]
+
+ # Check "b" doesn't exist in either
+ assert [ ! -e test_subtree/b ]
+ assert [ ! -e test_subtree2/b ]
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/helpers.bats b/tests-for-this-repo/helpers.bats
new file mode 100644
index 0000000..af61b94
--- /dev/null
+++ b/tests-for-this-repo/helpers.bats
@@ -0,0 +1,35 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/helpers/lib"
+
+@test "die exits and prints message" {
+ run bash -c ". ${BASH_LIB_DIR}/helpers/lib; die msg"
+ assert_output msg
+ assert_failure
+}
+
+@test "spushd is quiet on stdout" {
+ run spushd /tmp
+ refute_output
+ assert_success
+}
+
+@test "spopd is quiet on stdout" {
+ pushd .
+ run spopd
+ refute_output
+ assert_success
+}
+
+@test "spushd dies on failure" {
+ run bash -c ". ${BASH_LIB_DIR}/helpers/lib; spushd /this-doesnt-exist"
+ assert_output --partial "No such file or directory"
+ assert_failure
+}
+
+@test "spopd dies on failure" {
+ run bash -c ". ${BASH_LIB_DIR}/helpers/lib; spopd"
+ assert_output --partial "stack empty"
+ assert_failure
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/k8s.bats b/tests-for-this-repo/k8s.bats
new file mode 100755
index 0000000..48e1bc2
--- /dev/null
+++ b/tests-for-this-repo/k8s.bats
@@ -0,0 +1,27 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/k8s/lib"
+
+@test "gke-utils image builds" {
+ run build_gke_image
+ assert_success
+}
+
+@test "Kubernetes Cluster Is Available" {
+ : ${KUBECTL_CLI_URL:?Required Var, did you run tests via summon?}
+ run run_docker_gke_command "kubectl cluster-info"
+ assert_output --regexp "Kubernetes master.* is running at .*https://"
+ assert_success
+}
+
+@test "Can delete gke image" {
+ local -r image="${DOCKER_REGISTRY_PATH}/alpine-test-${RANDOM}"
+ run_docker_gke_command "
+ docker pull alpine
+ docker tag alpine ${image}
+ docker push ${image}
+ "
+ run delete_gke_image "${image}"
+ assert_success
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/lint.bats b/tests-for-this-repo/lint.bats
new file mode 100755
index 0000000..dab5d97
--- /dev/null
+++ b/tests-for-this-repo/lint.bats
@@ -0,0 +1,69 @@
+. ${BASH_LIB_DIR}/git/lib
+. ${BASH_LIB_DIR}/test-utils/lib
+. ${BASH_LIB_DIR}/helpers/lib
+
+setup() {
+ spushd ${BASH_LIB_DIR}
+}
+
+# Find and check shell scripts
+@test "Syntax and Shellcheck" {
+ FAILED=""
+ echo "Starting Bash Lint checks"
+ for script in $(find_scripts); do
+ shellcheck_script "${script}"\
+ || FAILED="${FAILED} ${script}"
+ done
+
+ [[ "${FAILED}" == "" ]]
+}
+
+@test "Bash scripts do not have .sh suffix" {
+ rc=0
+ for file in $(find_scripts); do
+ if [[ "${file}" =~ .sh$ ]]; then
+ # script has .sh suffix
+ echo "Script found with .sh suffix: ${file}, please rename"
+ rc=1
+ fi
+ done
+ return ${rc}
+}
+
+@test "All functions referenced in readme" {
+ rc=0
+ for file in $(find_scripts | grep "/lib$"); do
+ for func_name in $(grep 'function.*()\s*{\s*$' ${file} | awk '{print $2}'| tr -dc '[a-zA-Z0-9_-\n]'); do
+ if ! grep -q "${func_name}" "${BASH_LIB_DIR}/README.md"; then
+ echo "Function ${func_name} from libriary ${file} is not mentioned in the README.md, please add a description"
+ rc=1
+ fi
+ done
+
+ if ! grep -q "${file}" "${BASH_LIB_DIR}/README.md"; then
+ echo "Library ${file} is not mentioned in the README.md, please add a description"
+ rc=1
+ fi
+ done
+ return ${rc}
+}
+
+@test "All functions tested" {
+ local rc=0
+ for file in $(find_scripts | grep "/lib$"); do
+ local lib_name="$(dirname ${file})"
+ local bats_file="tests-for-this-repo/${lib_name}.bats"
+ if [[ ! -e "${bats_file}" ]]; then
+ echo "BATS test file ${bats_file} is missing for library ${file}"
+ rc=1
+ else
+ for func_name in $(grep 'function.*()\s*{' "${file}" | awk '{print $2}'| tr -dc '[a-zA-Z0-9_\n]'); do
+ if ! grep -q "${func_name}" "${bats_file}"; then
+ echo "Function ${func_name} from libriary ${file} is not tested in ${bats_file}, please add a test."
+ rc=1
+ fi
+ done
+ fi
+ done
+ return ${rc}
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/logging.bats b/tests-for-this-repo/logging.bats
new file mode 100644
index 0000000..47bc684
--- /dev/null
+++ b/tests-for-this-repo/logging.bats
@@ -0,0 +1,10 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/logging/lib"
+
+@test "announce prints all arguments" {
+ run announce one two one two
+ assert_output --partial "one two one two"
+ assert_success
+}
\ No newline at end of file
diff --git a/tests-for-this-repo/python-lint/Dockerfile b/tests-for-this-repo/python-lint/Dockerfile
new file mode 100644
index 0000000..5867bd0
--- /dev/null
+++ b/tests-for-this-repo/python-lint/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.7-alpine
+
+ENTRYPOINT pytest --flake8 --junit-xml junit.xml
+VOLUME ["/mnt"]
+COPY requirements.txt constraints.txt /
+RUN pip install --no-cache-dir -r requirements.txt -c constraints.txt
+WORKDIR /mnt
\ No newline at end of file
diff --git a/tests-for-this-repo/python-lint/constraints.txt b/tests-for-this-repo/python-lint/constraints.txt
new file mode 100644
index 0000000..ab6d3d5
--- /dev/null
+++ b/tests-for-this-repo/python-lint/constraints.txt
@@ -0,0 +1,13 @@
+atomicwrites==1.3.0
+attrs==19.1.0
+entrypoints==0.3
+flake8==3.7.7
+mccabe==0.6.1
+more-itertools==7.0.0
+pluggy==0.9.0
+py==1.8.0
+pycodestyle==2.5.0
+pyflakes==2.1.1
+pytest==4.4.1
+pytest-flake8==1.0.4
+six==1.12.0
diff --git a/tests-for-this-repo/python-lint/requirements.txt b/tests-for-this-repo/python-lint/requirements.txt
new file mode 100644
index 0000000..5cc3adc
--- /dev/null
+++ b/tests-for-this-repo/python-lint/requirements.txt
@@ -0,0 +1 @@
+pytest-flake8
diff --git a/tests-for-this-repo/run-bats-tests b/tests-for-this-repo/run-bats-tests
new file mode 100755
index 0000000..455319e
--- /dev/null
+++ b/tests-for-this-repo/run-bats-tests
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# This script runs the self tests for the bash-lib repo.
+
+# This script is an entry point, so init
+# is not assumed to have been run
+# shellcheck disable=SC2086,SC2046
+. $(dirname ${BASH_SOURCE[0]})/../init
+. "${BASH_LIB_DIR}/test-utils/lib"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+
+# Check vital tools are installed
+if ! command -v summon >/dev/null; then
+ die "Summon must be installed and configured in order to run tests"
+fi
+
+if ! command -v docker >/dev/null; then
+ die "Docker must be installed and configured in order to run tests"
+fi
+
+# could be tap, junit or pretty
+readonly BATS_OUTPUT_FORMAT="${BATS_OUTPUT_FORMAT:-pretty}"
+readonly BATS_SUITE="${BATS_SUITE:-BATS}"
+readonly TAP_FILE="${BASH_LIB_DIR}/bats.tap"
+
+# return code
+rc=0
+
+if [[ ${#} -eq 0 ]]; then
+ echo "No test scripts specified, running all."
+ scripts="${BASH_LIB_DIR}/tests-for-this-repo/*.bats"
+else
+ scripts="${*}"
+fi
+
+readonly summon_cmd="summon -f ${BASH_LIB_DIR}/secrets.yml"
+
+case $BATS_OUTPUT_FORMAT in
+ pretty)
+ # shellcheck disable=SC2086
+ if ! ${summon_cmd} ${BATS_CMD} ${scripts}; then
+ rc=1
+ fi
+ ;;
+ tap | junit)
+ # shellcheck disable=SC2086
+ if ! ${summon_cmd} ${BATS_CMD} ${scripts} > ${TAP_FILE}; then
+ rc=1
+ fi
+ echo "TAP Output written to ${TAP_FILE}"
+ ;;
+ *)
+ echo "Invalid BATS_OUTPUT_FORMAT: ${BATS_OUTPUT_FORMAT}, valid options: pretty, junit, tap."
+ exit 1
+ ;;
+esac
+
+#Convert TAP to Junit when required
+if [[ "${BATS_OUTPUT_FORMAT}" == junit ]]; then
+ # Run tap2junit docker image to convert BATS TAP output to Junit for consumption by jenkins
+ readonly JUNIT_FILE="${BASH_LIB_DIR}/${BATS_SUITE}-junit.xml"
+ tap2junit < "${TAP_FILE}" > "${JUNIT_FILE}"
+ echo "Junit output written to ${JUNIT_FILE}"
+fi
+
+exit ${rc}
\ No newline at end of file
diff --git a/tests-for-this-repo/run-gitleaks b/tests-for-this-repo/run-gitleaks
new file mode 100755
index 0000000..0faf3fd
--- /dev/null
+++ b/tests-for-this-repo/run-gitleaks
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# Check this repo for secret leaks
+
+# This script is an entry point, so init
+# is not assumed to have been run
+# shellcheck disable=SC2086,SC2046
+. $(dirname "${BASH_SOURCE[0]}")/../init
+. "${BASH_LIB_DIR}/test-utils/lib"
+. "${BASH_LIB_DIR}/helpers/lib"
+
+docker run \
+ --rm \
+ --name=gitleaks \
+ -v ${BASH_LIB_DIR}:/code/ \
+ zricethezav/gitleaks:v2.0.0 \
+ -v \
+ --repo-path=/code/
diff --git a/tests-for-this-repo/run-python-lint b/tests-for-this-repo/run-python-lint
new file mode 100755
index 0000000..c14e96b
--- /dev/null
+++ b/tests-for-this-repo/run-python-lint
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# This script is an entry point, so init
+# is not assumed to have been run
+
+# shellcheck disable=SC2086,SC2046
+. $(dirname ${BASH_SOURCE[0]})/../init
+. "${BASH_LIB_DIR}/helpers/lib"
+
+rc=0
+
+spushd ${BASH_LIB_DIR}/tests-for-this-repo/python-lint
+ docker build . -t pytest-flake8
+ if ! docker run --rm -v "${BASH_LIB_DIR}:/mnt" pytest-flake8; then
+ rc=1
+ fi
+ mv "${BASH_LIB_DIR}/junit.xml" "${BASH_LIB_DIR}/python-lint-junit.xml"
+spopd
+
+exit ${rc}
\ No newline at end of file
diff --git a/tests-for-this-repo/test-utils.bats b/tests-for-this-repo/test-utils.bats
new file mode 100644
index 0000000..c723c34
--- /dev/null
+++ b/tests-for-this-repo/test-utils.bats
@@ -0,0 +1,80 @@
+. "${BASH_LIB_DIR}/test-utils/bats-support/load.bash"
+. "${BASH_LIB_DIR}/test-utils/bats-assert-1/load.bash"
+
+. "${BASH_LIB_DIR}/helpers/lib"
+. "${BASH_LIB_DIR}/test-utils/lib"
+
+docker_safe_tmp(){
+ # neither mktemp -d not $BATS_TMPDIR
+ # produce dirs that docker can mount from
+ # in macos.
+ local -r tmp_dir="/tmp/${RANDOM}/spgs"
+ (
+ rm -rf "${tmp_dir}"
+ mkdir -p "${tmp_dir}"
+ ) 1>&2
+ echo "${tmp_dir}"
+}
+
+@test "shellcheck notices compile error" {
+ tmp_dir="$(docker_safe_tmp)"
+ spushd "${tmp_dir}"
+
+ echo "'" > bad_script
+ run shellcheck_script bad_script
+ assert_failure
+ assert_output --partial "syntax error"
+
+ spopd
+ rm -rf "/tmp/${tmp_dir#/tmp/}"
+}
+
+@test "shellcheck passes good script" {
+ tmp_dir="$(docker_safe_tmp)"
+ spushd "${tmp_dir}"
+
+ echo -e "#!/bin/bash\n:" > good_script
+ run shellcheck_script good_script
+ rm -rf "${tmp_dir}"
+ assert_output --partial "Checking good_script"
+ assert_success
+
+ spopd
+ rm -rf "/tmp/${tmp_dir#/tmp/}"
+}
+
+@test "find_scripts finds git tracked files containing bash shebang" {
+ tmp_dir="${BATS_TMPDIR}/ffgtfwse"
+ rm -rf "${tmp_dir}"
+ mkdir -p "${tmp_dir}"
+ pushd ${tmp_dir}
+ git init
+ git config user.email "ci@ci.ci"
+ git config user.name "Jenkins"
+
+ echo '#!/bin/bash' > a
+ echo '#!/bin/bash' > b
+ date > c
+ date > d
+
+ git add a c
+ git commit -a -m "initial"
+
+ run find_scripts
+ assert_output "a"
+ assert_success
+ popd
+}
+
+@test "tap2junit correctly converts test file" {
+ rc=0
+ fdir="${BASH_LIB_DIR}/tests-for-this-repo/fixtures/test-utils"
+ # Can't use run / assert_output here
+ # because assert_output uses $output
+ # which is a combination of stdout and stderr
+ # and we are only interested in stdout.
+ stdout=$(tap2junit < "${fdir}/tap2junit.in")
+ rc=${?}
+ assert_equal "${stdout}" "$(cat ${fdir}/tap2junit.out)"
+ assert_equal "${rc}" "0"
+}
\ No newline at end of file