#!/bin/bash COLOR_NC='\033[0m' # No Color COLOR_GRAY='\033[1;30m' COLOR_RED='\033[0;31m' COLOR_LCYAN='\033[1;36m' COLOR_YELLOW='\033[1;33m' COLOR_LGREEN='\033[1;32m' logv() { [[ -z "$VERBOSE" ]] && return 0; local msg=$(echo "$@" | sed "s/$CONTENTFUL_MANAGEMENT_TOKEN/\*\*\*\*\*/" ) >&2 echo -e "${COLOR_GRAY}$msg${COLOR_NC}" || true } logerr() { >&2 echo -e "${COLOR_RED}$@${COLOR_NC}" } curlv() { logv "curl" $@ curl "$@" } execv() { logv "$@" "$@" } ## *** Argument Parsing & validation *** usage() { echo "$0 [opts] Commands: migrate [dir|file] runs pending migration files in the given directory * [dir|file] optional - Default: db/migrate setup [file] initializes a space with bare-minimum schema and seeds * [file] optional - default: db/contentful-schema.json backup [file] downloads a backup of the current space to the given file * [file] optional - default: timestamped file in current directory clean [no-init] Deletes all data in a given space and optionally sets it up again using 'bin/contentful setup'. * [no-init] optional - Skips the 'setup' step at the end. restore [file] restores a given backup file into the current space * [file] optional - default: the most recent backup file in the current directory new_env deletes the current working environment if it exists and makes a new clone of 'master'. * -e [to environment ID] optional - the current working environment. Default: \$USER generate [name] Creates a sample migration in the db/migrate directory * [name] optional - default: 'contentful_migration' Flags:" && \ grep " .)\ #" $0 echo " Examples:" && \ grep -i "#\ example:" $0 | awk '{$1=""; $2=""; print " "$0}' } parse_args() { OPTIND=1 local s=$(echo "$1" | tr '[:upper:]' '[:lower:]') case "$s" in migrate|setup|backup|export|restore|new_env|import|generate|clean|help|h|\?) export subcommand=$s OPTIND=2 ;; esac # Parse flags while getopts ":hyvse:a:" arg; do case $arg in y) # Yes - skip prompts export YES="-y" ;; s) # Contentful Space ID - overrides env var CONTENTFUL_SPACE_ID export CONTENTFUL_SPACE_ID=$OPTARG ;; a) # Contentful Mgmt Token - overrides env var CONTENTFUL_MANAGEMENT_TOKEN export CONTENTFUL_MANAGEMENT_TOKEN=$OPTARG ;; e) # Contentful environment ID - overrides env var CONTENTFUL_ENVIRONMENT export CONTENTFUL_ENVIRONMENT=$OPTARG ;; v) # Verbose mode - extra output export VERBOSE=true ;; h) # Display help. usage exit 0 ;; *) logerr "Unknown option: '$OPTARG'" usage exit -1 ;; esac done export OPTIND } parse_args $@ && shift $(($OPTIND - 1)) # If they put args before the command like 'bin/contentful -s 1xab migrate -y', try parsing again [[ -z "$subcommand" ]] && parse_args $@ && shift $(($OPTIND - 1)) require_environment() { [[ -z "$CONTENTFUL_SPACE_ID" ]] && logerr "Please set CONTENTFUL_SPACE_ID environment variable or use '-s' flag." && exit -1; [[ -z "$CONTENTFUL_MANAGEMENT_TOKEN" ]] && logerr "Please set CONTENTFUL_MANAGEMENT_TOKEN environment variable or use '-a' flag." && exit -1; if [[ ! -f node_modules/.bin/contentful-migration ]]; then command -v npm >/dev/null 2>&1 || (logerr "I require 'npm' but it's not installed. Please install nodejs."; exit -1) execv npm install [[ -f node_modules/.bin/contentful-migration ]] || (logerr "Failed installing node modules - please ensure contentful CLI is installed"; exit -1) fi } ## *** Utility functions *** confirm() { [[ -z "$2" ]] && [[ ! -z "$YES" ]] && logv "$1 (y/n): confirmed by -y flag" && return 0; while true; do if [[ -z "$2" ]]; then read -p $'\033[1;36m'"$1"' (y/n): '$'\033[0m' yn else # double confirm - extra dangerous. read -p $'\033[0;31m'"$1"' (y/n): '$'\033[0m' yn fi case $yn in [Yy]* ) return 0;; [Nn]* ) return 1;; * ) echo "Please answer yes or no.";; esac done } get_space_name() { curlv -s https://api.contentful.com/spaces/$1?access_token=$CONTENTFUL_MANAGEMENT_TOKEN | jq -r .name | tr '[:upper:]' '[:lower:]' } # Man I wish I understood sed... https://stackoverflow.com/a/29060802 # $1 File # $2 Find # $3 Replace / Append replace_append() { if grep -q "^$2" "$1" then sed -i.bak "s/^$2.*$/$3/" "$1" else echo "\n$3" >> "$1" fi } set -e # *** Commands *** # Example: bin/contentful migrate -y -s 1xab -a $MY_TOKEN db/migrate/20180101120000_add_content_type_dog.ts # equivalent to: bin/rake db:migrate migrate() { ARG="$1" [[ -z "$ARG" ]] && ARG="db/migrate" [[ -d "$ARG" ]] && ARG="batch $ARG" require_environment execv node_modules/.bin/ts-node node_modules/.bin/contentful-migration \ -s $CONTENTFUL_SPACE_ID -a $CONTENTFUL_MANAGEMENT_TOKEN \ $YES -p $ARG mkdir -p db execv node_modules/.bin/contentful-export --export-dir db --content-file contentful-schema.json \ --space-id $CONTENTFUL_SPACE_ID --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --query-entries 'content_type=migrationHistory' \ --query-assets 'sys.id=false' if [[ $(git diff-index --name-only HEAD | grep 'db/contentful-schema.json') == "" ]]; then echo -e "${COLOR_LGREEN}✓ Schema in contentful space is equivalent to stored schema${COLOR_NC}" else echo -e "${COLOR_YELLOW}⚠️ Schema changed after running migrations${COLOR_NC}" fi } # Example: bin/contentful backup -s 1xab -a $MY_TOKEN 2018_01_01.1xab.dump.json # equivalent to: bin/rake db:dump[2018_01_01.dump] backup() { FILE="$1" [[ ! -z "$FILE" ]] && FILE="--content-file $FILE" && shift require_environment [[ ! -z "$CONTENTFUL_ENVIRONMENT" ]] && ENV="--environment-id $CONTENTFUL_ENVIRONMENT" execv node_modules/.bin/contentful-export $FILE \ --space-id $CONTENTFUL_SPACE_ID $ENV --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ $@ } # Example: bin/contentful restore -y -s 1xab -a $MY_TOKEN 2018_01_01.1xab.dump.json # equivalent to: bin/rake db:restore[2018_01_01.dump] restore() { FILE="$1" if [[ -z "$FILE" ]]; then FILE=$(ls contentful-export-$CONTENTFUL_SPACE_ID-* | sort -r | head -n 1) [[ -z "$FILE" ]] && logerr "No file given on command line" && exit -1 fi name=$(get_space_name $CONTENTFUL_SPACE_ID) if [[ ! -z "$CONTENTFUL_ENVIRONMENT" ]]; then ENV="--environment-id $CONTENTFUL_ENVIRONMENT" name="$name/$CONTENTFUL_ENVIRONMENT" fi confirm "Import $FILE into $name?" || exit -1 require_environment execv node_modules/.bin/contentful-import \ --space-id $CONTENTFUL_SPACE_ID $ENV --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --content-file $FILE } # Example: bin/contentful setup -y -s 1xab -a $MY_TOKEN db/my-schema.json # equivalent to: bin/rake db:setup setup() { FILE="$1" [[ -z "$FILE" ]] && FILE=db/contentful-schema.json name=$(get_space_name $CONTENTFUL_SPACE_ID) if [[ ! -z "$CONTENTFUL_ENVIRONMENT" ]]; then ENV="--environment-id $CONTENTFUL_ENVIRONMENT" name="$name/$CONTENTFUL_ENVIRONMENT" fi confirm "Initialize space $name from seed file $FILE?" || exit -1 require_environment execv node_modules/.bin/contentful-import \ --space-id $CONTENTFUL_SPACE_ID $ENV --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --content-file $FILE migrate } # Example: bin/contentful clean -s 1xab -a $MY_TOKEN clean() { command -v jq >/dev/null 2>&1 || (logerr "I require 'jq' but it's not installed. Please run 'brew install jq'"; exit -1) require_environment [[ -z "$CONTENTFUL_ENVIRONMENT" ]] && CONTENTFUL_ENVIRONMENT="$USER" [[ "$CONTENTFUL_ENVIRONMENT" == "master" ]] && logerr "cannot delete the master environment" && exit -1 name=$(get_space_name $CONTENTFUL_SPACE_ID) code=$(curlv -s -o /dev/null -w "%{http_code}" https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/environments/$CONTENTFUL_ENVIRONMENT\?access_token\=$CONTENTFUL_MANAGEMENT_TOKEN) [[ $code == "404" ]] && logerr "$CONTENTFUL_ENVIRONMENT does not exist in $name" && return 0; confirm "This will delete the '$CONTENTFUL_ENVIRONMENT' environment from $name. Are you sure?" || exit -1 local bkup_file="contentful-export-$CONTENTFUL_SPACE_ID-${CONTENTFUL_ENVIRONMENT}-`date +"%Y-%m-%dT%H-%M-%S"`.json" backup $bkup_file curlv --fail -XDELETE https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/environments/$CONTENTFUL_ENVIRONMENT\?access_token\=$CONTENTFUL_MANAGEMENT_TOKEN } # Example: bin/contentful new_env -e gordon_dev new_env() { command -v jq >/dev/null 2>&1 || (logerr "I require 'jq' but it's not installed. Please run 'brew install jq'"; exit -1) require_environment name=$(get_space_name $CONTENTFUL_SPACE_ID) [[ -z "$CONTENTFUL_ENVIRONMENT" ]] && CONTENTFUL_ENVIRONMENT="$USER" [[ "$CONTENTFUL_ENVIRONMENT" == "master" ]] && logerr "cannot delete the master environment" && exit -1 echo -e "${COLOR_LCYAN}This will delete '$CONTENTFUL_ENVIRONMENT' and recreate it from master.${COLOR_NC}" confirm "Continue?" || exit -1 export YES='-y' # don't keep bugging the user clean # make the environment resp=$(curlv -s --fail -XPUT https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/environments/$CONTENTFUL_ENVIRONMENT \ -H "Authorization: Bearer ${CONTENTFUL_MANAGEMENT_TOKEN}" \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -d "{ \"name\": \"${CONTENTFUL_ENVIRONMENT}\" }") while [ ! $(echo "$resp" | jq -r .sys.status.sys.id) == "ready" ] do logv "waiting for environment $CONTENTFUL_ENVIRONMENT to be ready..." sleep 1 resp=$(curlv -s https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/environments/$CONTENTFUL_ENVIRONMENT \ -H "Authorization: Bearer ${CONTENTFUL_MANAGEMENT_TOKEN}") done logv "get the API keys and update the one matching ours to point to the new environment" keys=$(curlv -s --fail https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/api_keys\?access_token\=$CONTENTFUL_MANAGEMENT_TOKEN) my_key=$(echo "$keys" | jq -r ".items[] | select(.accessToken == \"$CONTENTFUL_ACCESS_TOKEN\")") my_key_id=$(echo "$my_key" | jq -r ".sys.id") my_key_version=$(echo "$my_key" | jq -r ".sys.version") new_env_links=$(echo "$my_key" | jq ".environments + [{ \"sys\": { \"id\": \"$CONTENTFUL_ENVIRONMENT\", \"type\": \"Link\", \"linkType\": \"Environment\" } }] | { \"environments\": . }") curlv -s -o /dev/null --fail -XPUT https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/api_keys/$my_key_id \ -H "Authorization: Bearer ${CONTENTFUL_MANAGEMENT_TOKEN}" \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -H "X-Contentful-Version: ${my_key_version}" \ -d "${new_env_links}" execv replace_append .env.local "CONTENTFUL_ENVIRONMENT\=" "CONTENTFUL_ENVIRONMENT=$CONTENTFUL_ENVIRONMENT" execv replace_append .env.test.local "CONTENTFUL_ENVIRONMENT\=" "CONTENTFUL_ENVIRONMENT=$CONTENTFUL_ENVIRONMENT" echo "Environment ${CONTENTFUL_ENVIRONMENT} successfully created!" } # Example: bin/contentful generate add content type dog # equivalent to: bin/rails generate migration add_content_type_dog generate() { migration=" import Migration from 'contentful-migration-cli' export = function (migration: Migration) { const dog = migration.createContentType('dog', { name: 'Dog' }) const name = dog.createField('name') name.name('Name') .type('Symbol') .required(true) } " timestamp=$(date +%Y%m%d%H%M%S) filename="$@" [[ -z "$filename" ]] && filename="contentful_migration" filename=${filename// /\_} filename="db/migrate/${timestamp}_${filename}.ts" echo "$migration" > $filename echo "generated file $filename" } case $subcommand in migrate) migrate $@ ;; backup|export) backup $@ ;; restore|import) restore $@ ;; setup) setup $@ ;; generate) generate $@ ;; clean) clean $@ ;; new_env) new_env $@ ;; help|h|\?) usage ;; *) logerr "Unknown command: '$1'" usage exit -1 ;; esac