#!/usr/bin/env bash if [ -z "$MSCI_HOME" ]; then echo "MSCI_HOME is not set" exit 1 fi deps=("git" "jq" "openssl" "docker" "crontab") for dep in "${deps[@]}"; do if ! command -v "$dep" >/dev/null 2>&1; then echo "command '$dep' not found" exit 1 fi done set -e shopt -s nullglob mkdir -p "$MSCI_HOME"/projects \ "$MSCI_HOME"/stdout/public \ "$MSCI_HOME"/stdout/private \ "$MSCI_HOME"/tmp [ ! -f "$MSCI_HOME"/cron ] && touch "$MSCI_HOME"/cron confirm_in() { read -rp "$1 (y/N): " confirm [[ $confirm == [yY]* ]] && return 0 || return 1 } find_project() { if list_projects | grep -xFq -- "$1"; then return 0 fi return 1 } list_projects() { paths=("$MSCI_HOME"/projects/*) names=("${paths[@]##*/}") projects=("${names[@]%.json}") printf '%s\n' "${projects[@]}" } write_cron() { cfile="MSCI_HOME=$MSCI_HOME\nPATH=$PATH:\$PATH\n" for project in $(list_projects); do ppath="$MSCI_HOME"/projects/"$project".json schedule=$(jq -r '.cron' "$ppath") [ "$schedule" = 'null' ] && continue cfile="${cfile}$schedule msci --run $project\n" done echo -e "$cfile" >"$MSCI_HOME"/tmp.cron if crontab "$MSCI_HOME"/tmp.cron; then mv -fv "$MSCI_HOME"/tmp.cron "$MSCI_HOME"/cron else rm -fv "$MSCI_HOME"/tmp.cron fi } run_project() { ( set -euo pipefail ppath="$MSCI_HOME"/projects/"$1".json git_params=() repo_name="$1" repo_branch=$(jq -r '.branch // empty' "$ppath") if [ -n "$repo_branch" ]; then git_params+=(-b "$repo_branch") fi repo_url=$(jq -r '.url' "$ppath") repo_path="$MSCI_HOME"/tmp/"$repo_name" git_params+=("$repo_url" "$repo_path") echo "cloning repo: $repo_url" | tee -a "$2" ! git clone "${git_params[@]}" &>/dev/null && echo "failed to clone repo" | tee -a "$2" && exit 1 ! pushd "$repo_path" &>/dev/null && echo "unable to cd into cloned repo" | tee -a "$2" && exit 1 for job in "$repo_path"/.makeshiftci/*; do pname=$(jq -r '.name' "$job") echo "running job: $pname" | tee -a "$2" pimage=$(jq -r '.image' "$job") psecrets=$(jq -r '.secrets' "$job") prun=$(jq -r '.run[]' "$job") env_secrets=() secret_mounts=() if [ ! "$psecrets" = 'null' ]; then for secret_key in $(echo "$psecrets" | jq -r '. | keys[]'); do secret_value=$(echo "$psecrets" | jq -r ".$secret_key") secret_name=$(openssl rand -hex 16) secret_mounts+=("--mount=type=bind,source=$secret_value,target=/$secret_name,readonly") env_secrets+=("$secret_key=/$secret_name") done fi ! docker run --rm \ "${env_secrets[@]/#/--env=}" \ --mount type=bind,source="$repo_path",target=/"$repo_name" \ "${secret_mounts[@]}" \ --workdir="/$repo_name" \ "$pimage" \ sh -c "$prun" | tee -a "$2" && echo "docker failed" | tee -a "$2" && exit 1 done popd &>/dev/null ! rm -rf "$repo_path" && echo "failed to delete tmp repo" | tee -a "$2" && exit 1 echo "finished" | tee -a "$2" ) } create_project() { name="$1" if [[ ! $name =~ ^[A-Za-z0-9_]+$ ]]; then echo "allowed project file name regex: ^[A-Za-z0-9_]+$" return 1 fi while true; do read -rp "project name: " pname [[ -n ${pname//[[:space:]]/} ]] && break echo "name can't be empty" done while true; do read -rp ".makeshiftci repo URL: " purl [[ -n ${purl//[[:space:]]/} ]] && break echo "URL can't be empty" done read -rp ".makeshiftci repo branch (optional): " pbranch phidden=$(confirm_in "hidden?" && echo true || echo false) read -rp "cron (optional): " pcron jq -n \ --arg pname "$pname" \ --arg purl "$purl" \ --arg pbranch "$pbranch" \ --argjson phidden "$phidden" \ --arg pcron "$pcron" \ '{ name: $pname, url: $purl, branch: (if $pbranch == "" then null else $pbranch end), hidden: $phidden, cron: (if $pcron == "" then null else $pcron end) }' \ >"$MSCI_HOME"/projects/"$name".json [ -n "$pcron" ] && write_cron ([ "$phidden" = "true" ] && mkdir -p "$MSCI_HOME"/stdout/private/"$1") || mkdir -p "$MSCI_HOME"/stdout/public/"$1" } edit_project() { name="$1" if ! find_project "$name"; then echo "project file doesn't exist" return 1 fi ppath="$MSCI_HOME"/projects/"$name".json echo "editing $ppath" oldname=$(jq -r '.name' "$ppath") oldurl=$(jq -r '.url' "$ppath") oldbranch=$(jq -r '.branch // empty' "$ppath") washidden=$(jq '.hidden' "$ppath") keephidden=$([ "$washidden" = 'true' ] && echo "keep" || echo "make") oldcron=$(jq -r '.cron // empty' "$ppath") echo "project name: $oldname" read -rp "new name (optional): " pname [ -z "${pname//[[:space:]]/}" ] && pname=$oldname echo ".makeshiftci repo URL: $oldurl" read -rp "new URL (optional): " purl [ -z "${purl//[[:space:]]/}" ] && purl=$oldurl echo ".makeshiftci repo branch: $oldbranch" read -rp "new branch (optional): " pbranch [ -z "${pbranch//[[:space:]]/}" ] && pbranch=$oldbranch echo "hidden: $washidden" phidden=$(confirm_in "$keephidden hidden?" && echo true || echo false) echo "cron: $oldcron" read -rp "new cron (optional): " pcron [ -z "${pcron//[[:space:]]/}" ] && pcron=$oldcron jq -n \ --arg pname "$pname" \ --arg purl "$purl" \ --arg pbranch "$pbranch" \ --argjson phidden "$phidden" \ --arg pcron "$pcron" \ '{ name: $pname, url: $purl, branch: (if $pbranch == "" then null else $pbranch end), hidden: $phidden, cron: (if $pcron == "" then null else $pcron end) }' \ >"$ppath" [ ! "$pcron" = "$oldcron" ] && write_cron privout="$MSCI_HOME"/stdout/private/"$1" pubout="$MSCI_HOME"/stdout/public/"$1" ( [ "$phidden" = "true" ] && ([ ! -d "$privout" ] && mv "$pubout" "$privout") ) || ([ ! -d "$pubout" ] && mv "$privout" "$pubout") } case $1 in --create) if [ -z "$2" ]; then echo "no project file name is supplied" exit 1 fi if find_project "$2"; then echo "project file already exists" return 1 fi create_project "$2" ;; --edit) if [ -z "$2" ]; then echo "no project file name is supplied" exit 1 fi if ! find_project "$2"; then echo "project file doesn't exist" exit 1 fi edit_project "$2" ;; --delete) if [ -z "$2" ]; then echo "no project file name is supplied" exit 1 fi if ! find_project "$2"; then echo "project file doesn't exist" exit 1 fi ppath="$MSCI_HOME"/projects/"$2".json cat "$ppath" confirm_in "delete '$2' (details above)?" && rm -vf "$MSCI_HOME"/projects/"$2".json || exit 0 rm -rf "$MSCI_HOME"/stdout/**/"$2" write_cron ;; --run) if [ -z "$2" ]; then echo "no project file name is supplied" exit 1 fi if ! find_project "$2"; then echo "project file doesn't exist" exit 1 fi if [[ ! $2 =~ ^[A-Za-z0-9_]+$ ]]; then echo "project file name is not allowed, rename it" exit 1 fi ppath="$MSCI_HOME"/projects/"$2".json phidden=$(jq -r '.hidden' "$ppath") stdout_type=$([ "$phidden" = 'false' ] && echo "public" || echo "private") stdout_path="$MSCI_HOME"/stdout/"$stdout_type"/"$2" last_run=$( find "$stdout_path" \ -maxdepth 1 \ -mindepth 1 \ -type f -printf '%f\n' | grep -E '^[0-9]+$' | sort -n | tail -n 1 ) next_run=$( [ -z "$last_run" ] && echo "1" || echo $((last_run + 1)) ) stdout_path="$stdout_path"/"$next_run" echo -e "--MSCI_DATE($(date))--" >>"$stdout_path" if ! run_project "$2" "$stdout_path"; then echo -e "--MSCI_EXIT_FAILURE--" >>"$stdout_path" && rm -rf "$MSCI_HOME"/tmp/"$2" exit 1 fi echo -e "--MSCI_EXIT_SUCCESS--" >>"$stdout_path" ;; --list) list_projects ;; --clean) case "$2" in --tmp) echo "cleaning tmp for all projects" rm -rf "$MSCI_HOME"/tmp/* ;; --stdout) echo "cleaning stdout for all projects" rm -rf "$MSCI_HOME"/stdout/**/**/* ;; --all) echo "cleaning stdout and tmp for all projects" rm -rf "$MSCI_HOME"/tmp/* rm -rf "$MSCI_HOME"/stdout/**/**/* ;; *) if [ -z "$2" ]; then echo "no project file name is supplied" exit 1 fi if ! find_project "$2"; then echo "project file doesn't exist" exit 1 fi confirm_in "clear tmp?" && rm -rf "$MSCI_HOME"/tmp/"$2" && echo "cleared tmp for $2" confirm_in "clear stdout?" && rm -rf "$MSCI_HOME"/stdout/**/"$2"/* && echo "cleared stdout for $2" ;; esac ;; *) echo " usage: $(basename "$0") [OPTIONS] options: is the name of a file: - matching the regex ^[A-Za-z0-9_]+\$ - that is not necessarily the project name --create create a new project --edit edit a project --delete delete a project and clean up --run run a project --list list projects --clean |[SELECTION] if , prompts for cleaning a particular project selections: --tmp clean tmp files, such as cloned repos NOTE: tmp files are automatically removed after running, but they may persist in cases of errors or unexpected behavior only run this command in such cases --stdout clean stdout NOTE: stdout is where the output for each run is stored everything that was printed to stdout during the run of any project, is stored under the directory by the name of the project, in a file named as the run number " exit 1 ;; esac # vim: set filetype=bash: