├── .gitignore ├── tests ├── project │ ├── Gunfile.prod │ ├── Gunfile.stage │ ├── cmds │ │ ├── foo.bash │ │ ├── auto.bash │ │ └── bar.bash │ └── Gunfile └── project.bash ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── lib ├── README.md ├── aws │ ├── aws.bash │ ├── util.bash │ └── ec2.bash ├── releases │ ├── config.bash │ └── releases.bash └── consul │ └── consul.bash ├── go.mod ├── example ├── cmds │ └── github.bash └── README.md ├── README.md ├── src ├── env.bash ├── color.bash ├── fn.bash ├── deps.bash ├── module.bash ├── cmd.bash └── gun.bash ├── go.sum ├── .circleci └── config.yml ├── LICENSE ├── Makefile ├── glidergun.go └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | release 3 | bindata.go 4 | tests/project/.gun 5 | -------------------------------------------------------------------------------- /tests/project/Gunfile.prod: -------------------------------------------------------------------------------- 1 | export AWS_ACCESS_KEY_ID=prod-access-key 2 | export AWS_SECRET_ACCESS_KEY=prod-secret -------------------------------------------------------------------------------- /tests/project/Gunfile.stage: -------------------------------------------------------------------------------- 1 | export AWS_ACCESS_KEY_ID=staging-access-key 2 | export AWS_SECRET_ACCESS_KEY=staging-secret -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /tests/project/cmds/foo.bash: -------------------------------------------------------------------------------- 1 | 2 | 3 | init() { 4 | cmd-export-ns foo "Foo namespace" 5 | cmd-export foo-hello 6 | env-import AWS_ACCESS_KEY_ID "" 7 | } 8 | 9 | foo-hello() { 10 | echo "Hello from foo" 11 | } 12 | -------------------------------------------------------------------------------- /tests/project/Gunfile: -------------------------------------------------------------------------------- 1 | 2 | init() { 3 | cmd-export hello 4 | cmd-export aliased-cmd aliased 5 | } 6 | 7 | hello() { 8 | declare desc="Says hello" 9 | echo "Hello!" 10 | } 11 | 12 | aliased-cmd() { 13 | echo "aliased" 14 | } 15 | -------------------------------------------------------------------------------- /tests/project/cmds/auto.bash: -------------------------------------------------------------------------------- 1 | cmd:exported() { 2 | declare desc="Automatically exported command" 3 | echo "exported" 4 | } 5 | 6 | cmd:lazy-command() { 7 | declare desc="Look ma no init" 8 | echo "exported too" 9 | } 10 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # glidergun module library 2 | 3 | ## Require example 4 | ``` 5 | init() { 6 | module-require github.com/gliderlabs/glidergun/lib/aws 7 | } 8 | ``` 9 | 10 | ## Updating a module 11 | 12 | $ gun :get github.com/gliderlabs/glidergun/lib/aws 13 | -------------------------------------------------------------------------------- /tests/project/cmds/bar.bash: -------------------------------------------------------------------------------- 1 | 2 | init() { 3 | cmd-export-ns bar "Bar namespace" 4 | cmd-export bar-hello 5 | #deps-require jq 1.4 6 | #module-require github.com/gliderlabs/glidergun/lib/aws 7 | } 8 | 9 | bar-hello() { 10 | echo "Hello from bar" 11 | } 12 | -------------------------------------------------------------------------------- /lib/aws/aws.bash: -------------------------------------------------------------------------------- 1 | # Shared AWS functions wrapping aws-cli 2 | 3 | init() { 4 | env-import AWS_DEFAULT_REGION 5 | env-import AWS_ACCESS_KEY_ID 6 | env-import AWS_SECRET_ACCESS_KEY 7 | 8 | deps-require aws 9 | deps-require jq 1.4 10 | } 11 | 12 | aws-json() { 13 | aws --output json $@ 14 | } 15 | 16 | aws-text() { 17 | aws --output text $@ 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module glidergun 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/progrium/go-basher v5.1.6+incompatible 7 | gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa 8 | ) 9 | 10 | require ( 11 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 12 | github.com/kr/binarydist v0.1.0 // indirect 13 | github.com/mitchellh/go-homedir v1.1.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /lib/aws/util.bash: -------------------------------------------------------------------------------- 1 | # Temporary place for utility functions 2 | 3 | parallel() { 4 | declare cmd="$@" 5 | declare -a pids 6 | for line in $(cat); do 7 | eval "${cmd//\{\}/$line} &" 8 | pids+=($!) 9 | done 10 | local failed=$((0)) 11 | for pid in ${pids[@]}; do 12 | if ! wait $pid; then 13 | failed=$((failed + 1)) 14 | fi 15 | done 16 | return $((failed)) 17 | } 18 | 19 | ssh-cmd() { 20 | echo "ssh -A $SSH_OPTS $@" 21 | } 22 | -------------------------------------------------------------------------------- /example/cmds/github.bash: -------------------------------------------------------------------------------- 1 | init() { 2 | deps-require jq 3 | 4 | cmd-export gh-orgs 5 | env-import GITHUB_TOKEN 6 | env-import PAGE_SIZE 5 7 | } 8 | 9 | gh-orgs() { 10 | declare desc="Lists your GithHub organizations" 11 | 12 | : ${GITHUB_TOKEN:? go to https://github.com/settings/tokens to create one} 13 | curl -s \ 14 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 15 | https://api.github.com/user/orgs?per_page=$PAGE_SIZE \ 16 | | jq ".[].login" -r 17 | 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # glidergun 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/gliderlabs/glidergun/release.svg)](https://circleci.com/gh/gliderlabs/glidergun) 4 | [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs) 5 | 6 | 7 | ## Install 8 | $ curl -L https://github.com/gliderlabs/glidergun/releases/download/v0.1.0/glidergun_0.1.0_$(uname -sm|tr \ _).tgz \ 9 | | tar -zxC /usr/local/bin 10 | 11 | ## Tutorial 12 | 13 | See [example/README.md](example/README.md) for a detailed tutorial. 14 | 15 | -------------------------------------------------------------------------------- /src/env.bash: -------------------------------------------------------------------------------- 1 | 2 | declare -a _env 3 | 4 | env-import() { 5 | declare var="$1" default="$2" 6 | if [[ -z "${!var+x}" ]]; then 7 | if [[ -z "${2+x}" ]]; then 8 | echo "!! Imported variable $var must be set in profile or environment." | >&2 red 9 | exit 2 10 | else 11 | export $var="$default" 12 | fi 13 | fi 14 | _env+=($var) 15 | } 16 | 17 | env-show() { 18 | declare desc="Shows relevant environment variables" 19 | local longest=0 20 | for var in "${_env[@]}"; do 21 | if [[ "${#var}" -gt "$longest" ]]; then 22 | longest="${#var}" 23 | fi 24 | done 25 | for var in "${_env[@]}"; do 26 | printf "%-${longest}s = %s\n" "$var" "${!var}" 27 | done 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - '*' 8 | push: 9 | branches: 10 | - 'master' 11 | - 'release' 12 | 13 | jobs: 14 | ci: 15 | name: ci 16 | runs-on: ubuntu-18.04 17 | strategy: 18 | fail-fast: true 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-go@v2 22 | with: 23 | go-version: '1.17.3' 24 | - run: make deps build 25 | - run: make test 26 | - name: upload packages 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: build 30 | path: build/**/* 31 | - name: make release-in-docker 32 | run: | 33 | if [[ "${GITHUB_REF#refs/heads/}" == "release" ]]; then 34 | make deps build release 35 | fi 36 | -------------------------------------------------------------------------------- /src/color.bash: -------------------------------------------------------------------------------- 1 | 2 | declare -A color_table=( 3 | ["red"]='\033[00;31m' 4 | ["green"]='\033[00;32m' 5 | ["yellow"]='\033[00;33m' 6 | ["blue"]='\033[00;34m' 7 | ["purple"]='\033[00;35m' 8 | ["cyan"]="\033[00;36m" 9 | ["white"]='\033[00;37m' 10 | ["red-bright"]='\033[01;31m' 11 | ["green-bright"]='\033[01;32m' 12 | ["yellow-bright"]='\033[01;33m' 13 | ["blue-bright"]='\033[01;34m' 14 | ["purple-bright"]='\033[01;35m' 15 | ["cyan-bright"]='\033[01;36m' 16 | ["white-bright"]='\033[01;37m' 17 | ) 18 | 19 | color-init() { 20 | for color in "${!color_table[@]}"; do 21 | eval "$color() { color-cat $color; }" 22 | done 23 | } 24 | 25 | color-cat() { 26 | declare color="$1" 27 | while read -r; do 28 | printf "${color_table[$color]}%s\033[0m\n" "$REPLY" 29 | done 30 | } 31 | 32 | color-cycle() { 33 | colors=(${!color_table[@]}) 34 | index="$((($BASHPID%${#color_table[@]})))" 35 | color-cat "${colors[$index]}" 36 | } 37 | -------------------------------------------------------------------------------- /src/fn.bash: -------------------------------------------------------------------------------- 1 | # Bash function introspection 2 | 3 | fn-args() { 4 | declare desc="Inspect a function's arguments" 5 | local argline=$(type $1 | grep declare | grep -v "declare desc" | head -1) 6 | echo -e "${argline// /"\n"}" | awk -F= '/=/{print "<"$1">"}' | tr "\n" " " 7 | } 8 | 9 | fn-desc() { 10 | declare desc="Inspect a function's description" 11 | desc="" 12 | eval "$(type $1 | grep desc | head -1)"; echo $desc 13 | } 14 | 15 | fn-info() { 16 | declare desc="Inspects a function" 17 | declare fn="$1" showsource="$2" 18 | echo "$fn $(fn-args $fn)" 19 | echo " $(fn-desc $fn)" 20 | echo 21 | if [[ "$showsource" ]]; then 22 | declare -f "$fn" | tail -n +1 23 | echo 24 | fi 25 | } 26 | 27 | fn-source() { 28 | declare desc="Shows function source" 29 | declare fn="$1" 30 | declare -f "$fn" | tail -n +2 31 | } 32 | 33 | fn-call() { 34 | declare desc="Run function in environment" 35 | declare fn="$1"; shift 36 | "$fn" "$@" 37 | } 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 2 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 3 | github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo= 4 | github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM= 5 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 6 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 7 | github.com/progrium/go-basher v5.1.6+incompatible h1:4C9YrvISyUN01W2gEruN1EzMecc75NDiHUjipKuOqls= 8 | github.com/progrium/go-basher v5.1.6+incompatible/go.mod h1:Oiy7jZEU1mm+gI1dt5MKYwxptmD37q8/UupxnwhMHtI= 9 | gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa h1:drvf2JoUL1fz3ttkGNkw+rf3kZa2//7XkYGpSO4NHNA= 10 | gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa/go.mod h1:tuNm0ntQ7IH9VSA39XxzLMpee5c2DwgIbjD4x3ydo8Y= 11 | -------------------------------------------------------------------------------- /lib/releases/config.bash: -------------------------------------------------------------------------------- 1 | # Release config commands 2 | # WARNING: work in progress 3 | 4 | init() { 5 | cmd-export-ns config "Manage service config" 6 | cmd-export config-get 7 | cmd-export config-set 8 | cmd-export config-unset 9 | cmd-export config-edit 10 | } 11 | 12 | config-edit() { 13 | declare desc="Edit service config in Consul UI (OS X)" 14 | declare service="$1" 15 | consul-open "/dc1/kv/${EC2_VPC:?}/config/${service:?}/" 16 | } 17 | 18 | config-get() { 19 | declare desc="Get service config" 20 | declare service="$1" key="$2" 21 | if [[ "$key" ]]; then 22 | consul-get "${EC2_VPC:?}/config/${service:?}/${key:?}" 23 | else 24 | consul-export "${EC2_VPC:?}/config/${service:?}" 25 | fi 26 | } 27 | 28 | config-set() { 29 | declare desc="Set service config" 30 | declare service="$1" key="$2" value="$3" 31 | consul-set "${EC2_VPC:?}/config/${service:?}/${key:?}" "$value" 32 | } 33 | 34 | config-unset() { 35 | declare desc="Unset service config" 36 | declare service="$1" key="$2" 37 | consul-del "${EC2_VPC:?}/config/${service:?}/${key:?}" 38 | } 39 | -------------------------------------------------------------------------------- /tests/project.bash: -------------------------------------------------------------------------------- 1 | 2 | export GUN="${GUN:-gun}" 3 | 4 | setup() { 5 | cd "$(dirname "${BASH_SOURCE[0]}")/project" 6 | $GUN &> /dev/null 7 | } 8 | setup 9 | 10 | T_hello-cmd() { 11 | result="$($GUN hello)" 12 | [[ "$result" == "Hello!" ]] 13 | } 14 | 15 | T_foo-namespace-cmd() { 16 | result="$($GUN foo hello)" 17 | [[ "$result" == "Hello from foo" ]] 18 | } 19 | 20 | T_bar-namespace-cmd() { 21 | result="$($GUN bar hello)" 22 | [[ "$result" == "Hello from bar" ]] 23 | } 24 | 25 | T_foo-env-import() { 26 | [[ "$($GUN :env | grep AWS_ACCESS_KEY_ID)" ]] 27 | } 28 | 29 | T_prod-profile() { 30 | result="$($GUN prod :env)" 31 | [[ "$result" == "AWS_ACCESS_KEY_ID = prod-access-key" ]] 32 | } 33 | 34 | T_stage-profile() { 35 | result="$($GUN stage :env)" 36 | [[ "$result" == "AWS_ACCESS_KEY_ID = staging-access-key" ]] 37 | } 38 | 39 | T_aliased-cmd() { 40 | result="$($GUN aliased)" 41 | [[ "$result" == "aliased" ]] 42 | } 43 | 44 | T_autoexported-cmd() { 45 | result="$($GUN exported)" 46 | [[ "$result" == "exported" ]] 47 | } 48 | 49 | T_autoexported-cmd-with-dash() { 50 | result="$($GUN lazy-command)" 51 | [[ "$result" == "exported too" ]] 52 | } 53 | 54 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | jobs: 5 | test: 6 | docker: 7 | - image: circleci/golang:1.12 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | keys: 12 | - go-mod-v4-{{ checksum "go.sum" }} 13 | - run: 14 | name: Prepare environment 15 | command: make deps build 16 | - run: 17 | name: Run unit tests 18 | command: make test 19 | - run: 20 | name: Prepare artifacts 21 | command: | 22 | mkdir -p build/workspace/ 23 | tar -czvf build/workspace/go-workspace.tgz -C ~/.go_workspace . 24 | - save_cache: 25 | key: go-mod-v4-{{ checksum "go.sum" }} 26 | paths: 27 | - "/go/pkg/mod" 28 | - store_artifacts: 29 | path: build 30 | destination: build 31 | 32 | release: 33 | docker: 34 | - image: circleci/golang:1.12 35 | steps: 36 | - checkout 37 | - restore_cache: 38 | keys: 39 | - go-mod-v4-{{ checksum "go.sum" }} 40 | - run: 41 | name: Release 42 | command: | 43 | echo "make deps build release" 44 | 45 | workflows: 46 | build: 47 | jobs: 48 | - test 49 | - release: 50 | filters: 51 | branches: 52 | only: 53 | - release 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Glider Labs 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of glidergun nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = glidergun 2 | BINARYNAME=gun 3 | OWNER =gliderlabs 4 | HARDWARE = $(shell uname -m) 5 | SYSTEM_NAME = $(shell uname -s | tr '[:upper:]' '[:lower:]') 6 | VERSION=0.1.0 7 | 8 | build: src 9 | mkdir -p build/Linux && GOOS=linux CGO_ENABLED=0 go build -a \ 10 | -installsuffix cgo \ 11 | -ldflags "-X main.Version=$(VERSION)" \ 12 | -o build/Linux/$(BINARYNAME) 13 | mkdir -p build/Darwin && GOOS=darwin CGO_ENABLED=0 go build -a \ 14 | -installsuffix cgo \ 15 | -ldflags "-X main.Version=$(VERSION)" \ 16 | -o build/Darwin/$(BINARYNAME) 17 | 18 | test: src 19 | go install 20 | GUN=glidergun basht tests/*.bash 21 | 22 | src: 23 | go-bindata src 24 | 25 | install: build 26 | install build/$(shell uname -s)/gun /usr/local/bin 27 | 28 | deps: gh-release 29 | cd / && go get -u github.com/jteeuwen/go-bindata/... 30 | cd / && go get -u github.com/progrium/basht/... 31 | 32 | gh-release: 33 | mkdir -p bin 34 | curl -o bin/gh-release.tgz -sL https://github.com/progrium/gh-release/releases/download/v2.3.3/gh-release_2.3.3_$(SYSTEM_NAME)_$(HARDWARE).tgz 35 | tar xf bin/gh-release.tgz -C /usr/local/bin 36 | chmod +x /usr/local/bin/gh-release 37 | 38 | release: 39 | rm -rf release && mkdir release 40 | tar -zcf release/$(NAME)_$(VERSION)_Linux_$(HARDWARE).tgz -C build/Linux $(BINARYNAME) 41 | tar -zcf release/$(NAME)_$(VERSION)_Darwin_$(HARDWARE).tgz -C build/Darwin $(BINARYNAME) 42 | gh-release checksums sha256 43 | gh-release create $(OWNER)/$(NAME) $(VERSION) $(shell git rev-parse --abbrev-ref HEAD) v$(VERSION) 44 | 45 | clean: 46 | rm -rf build release 47 | 48 | .PHONY: build release src 49 | -------------------------------------------------------------------------------- /lib/releases/releases.bash: -------------------------------------------------------------------------------- 1 | # Release management commands 2 | # WARNING: work in progress 3 | 4 | init() { 5 | cmd-export-ns release "Release management" 6 | cmd-export release-create 7 | cmd-export release-show 8 | cmd-export release-latest 9 | cmd-export release-current 10 | cmd-export release-get 11 | } 12 | 13 | release-create() { 14 | declare desc="Create a new release" 15 | declare service="$1" 16 | if ! release-latest "$service" > /dev/null 2>&1; then 17 | #consul-set "${EC2_VPC:?}/releases/${service:?}/current" "v0" 18 | consul-set "${EC2_VPC:?}/releases/${service:?}/latest" "v0" 19 | fi 20 | local snapshot latest 21 | snapshot="$(consul-export "${EC2_VPC:?}/config/${service:?}" || true)" 22 | latest="$(release-latest $service)" 23 | latest="v$((${latest/v/}+1))" 24 | consul-set "${EC2_VPC:?}/releases/${service:?}/$latest" "$snapshot" 25 | consul-set "${EC2_VPC:?}/releases/${service:?}/latest" "$latest" 26 | echo "$latest" 27 | } 28 | 29 | release-get() { 30 | declare desc="Get contents of existing release" 31 | declare service="$1" version="$2" 32 | consul-get "${EC2_VPC:?}/releases/${service:?}/v${version/v/}" 33 | } 34 | 35 | release-current() { 36 | declare desc="Get or set current release version" 37 | declare service="$1" version="$2" 38 | if [[ "$version" ]]; then 39 | release-get "$service" "$version" > /dev/null 40 | consul-set "${EC2_VPC:?}/releases/${service:?}/current" "v${version/v/}" 41 | fi 42 | consul-get "${EC2_VPC:?}/releases/${service:?}/current" 43 | } 44 | 45 | release-latest() { 46 | declare desc="Get latest release version" 47 | declare service="$1" 48 | consul-get "${EC2_VPC:?}/releases/${service:?}/latest" 49 | } 50 | 51 | release-show() { 52 | declare desc="Show releases information" 53 | declare service="$1" 54 | echo "Latest: $(release-latest $service)" 55 | echo "Current: $(release-current $service)" 56 | consul-ls "${EC2_VPC:?}/releases/${service:?}" \ 57 | | grep -v latest \ 58 | | grep -v current 59 | } 60 | -------------------------------------------------------------------------------- /src/deps.bash: -------------------------------------------------------------------------------- 1 | # Simple binary dependency management 2 | 3 | declare DEPS_REPO="${DEPS_REPO:-https://raw.githubusercontent.com/gliderlabs/glidergun-rack/master/index}" 4 | 5 | deps-init() { 6 | export PATH="$GUN_DIR/bin:$PATH" 7 | } 8 | 9 | deps-require() { 10 | declare name="$1" version="${2:-latest}" 11 | deps-check "$name" "$version" && return 12 | echo "* Dependency required, installing $name $version ..." | >&2 yellow 13 | deps-install "$name" "$version" 14 | } 15 | 16 | deps-check() { 17 | declare name="$1" version="${2:-latest}" 18 | [[ -e "$GUN_DIR/bin/$name" ]] 19 | } 20 | 21 | deps-install() { 22 | declare name="$1" version="${2:-latest}" 23 | local tag index gundir bindir tmpdir tmpfile dep filename extension install 24 | gundir="$(cd $GUN_DIR; pwd)" 25 | bindir="$gundir/bin" 26 | mkdir -p "$bindir" 27 | index=$(curl -s "$DEPS_REPO/$name") 28 | tag="$(uname -s)_$(uname -m | grep -s 64 > /dev/null && echo amd64 || echo 386)" 29 | if ! dep="$(echo "$index" | grep -i -e "^$version $tag " -e "^$version \* ")"; then 30 | echo "!! Dependency not in index: $name $version" | >&2 red 31 | exit 2 32 | fi 33 | IFS=' ' read v t url checksum <<< "$dep" 34 | tmpdir="$gundir/tmp" 35 | mkdir -p "$tmpdir" 36 | tmpfile="${tmpdir:?}/$name" 37 | curl -Ls $url > "$tmpfile" 38 | if [[ "$checksum" ]]; then 39 | if ! [[ "$(cat "$tmpfile" | checksum md5)" = "$checksum" ]]; then 40 | echo "!! Dependency checksum failed: $name $version $checksum" | >&2 red 41 | exit 2 42 | fi 43 | fi 44 | cd "$tmpdir" 45 | filename="$(basename "$url")" 46 | extension="${filename#*.}" 47 | case "$extension" in 48 | zip) unzip "$tmpfile" > /dev/null;; 49 | tgz|tar.gz) tar -zxf "$tmpfile" > /dev/null;; 50 | esac 51 | install="$(echo "$index" | grep "^# install: " || true)" 52 | if [[ "$install" ]]; then 53 | IFS=':' read _ script <<< "$install" 54 | export PREFIX="$gundir" 55 | eval "$script" > /dev/null 56 | unset PREFIX 57 | else 58 | chmod +x "$tmpfile" 59 | mv "$tmpfile" "$bindir" 60 | fi 61 | cd - > /dev/null 62 | rm -rf "${tmpdir:?}" 63 | deps-check "$name" "$version" 64 | } 65 | -------------------------------------------------------------------------------- /src/module.bash: -------------------------------------------------------------------------------- 1 | 2 | declare GUN_REMOTE_MODULES="${GUN_REMOTE_MODULES:-$GUN_DIR/modules}" 3 | 4 | module-load() { 5 | declare filename="$1" 6 | source "$filename" 7 | if grep '^init()' "$filename" > /dev/null; then 8 | init 9 | fi 10 | 11 | module-auto-export $filename 12 | } 13 | 14 | module-auto-export() { 15 | declare filename="$1" 16 | 17 | local autoprefix="cmd:" 18 | while read cmd; do 19 | cmd-export "cmd:$cmd" "$cmd" 20 | done < <( 21 | sed -n "s/^cmd:\([^(]*\)(.*/\1/p" "$filename" 22 | ) 23 | 24 | } 25 | 26 | module-load-dir() { 27 | declare dir="$1" 28 | shopt -s nullglob 29 | for path in $dir/*.bash; do 30 | module-load "$path" 31 | done 32 | shopt -u nullglob 33 | } 34 | 35 | module-load-remote() { 36 | shopt -s nullglob 37 | for module in $GUN_REMOTE_MODULES/**/*.bash; do 38 | module-load "$module" 39 | done 40 | shopt -u nullglob 41 | } 42 | 43 | module-require() { 44 | declare url="$1" as="$2" 45 | module-check "$url" "$as" && return 46 | if [[ ! "$as" ]]; then 47 | as="$(basename ${url%%.git})" 48 | fi 49 | echo "* Module required, installing $as from $url ..." | >&2 yellow 50 | module-get "$url" "$as" 51 | module-load-dir "$GUN_REMOTE_MODULES/$as" 52 | } 53 | 54 | module-check() { 55 | declare url="$1" as="$2" 56 | : "${url:?}" 57 | if [[ ! "$as" ]]; then 58 | as="$(basename ${url%%.git})" 59 | fi 60 | [[ -d "$GUN_REMOTE_MODULES/$as" ]] 61 | } 62 | 63 | module-get() { 64 | declare desc="Install or update a remote module by URL" 65 | declare url="$1" as="$2" 66 | : "${url:?}" 67 | if [[ ! "$(which git)" ]]; then 68 | echo "!! git is required for fetching modules"| >&2 red 69 | exit 2 70 | fi 71 | if [[ ! "$url" =~ ^http ]]; then 72 | url="http://$url" 73 | fi 74 | if [[ ! "$as" ]]; then 75 | as="$(basename ${url%%.git})" 76 | fi 77 | local tmproot tmprepo scheme domain user repo path 78 | IFS="/" read scheme _ domain user repo path <<< "$url" 79 | tmproot="${GUN_DIR:?}/tmp" 80 | tmprepo="$tmproot/$domain/$user/$repo" 81 | mkdir -p "$tmprepo" 82 | git clone --quiet --depth 1 "$scheme//$domain/$user/$repo" "$tmprepo" 83 | rm -rf "${tmprepo:?}/.git" 84 | rm -rf "${GUN_REMOTE_MODULES:?}/$as" 85 | mkdir -p "$GUN_REMOTE_MODULES" 86 | mv "$tmprepo/$path" "$GUN_REMOTE_MODULES/$as" 87 | rm -rf "${tmproot:?}" 88 | } 89 | -------------------------------------------------------------------------------- /glidergun.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "crypto/md5" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "encoding/hex" 11 | "fmt" 12 | "hash" 13 | "io" 14 | "io/ioutil" 15 | "net/http" 16 | "os" 17 | 18 | "github.com/progrium/go-basher" 19 | "gopkg.in/inconshreveable/go-update.v0" 20 | ) 21 | 22 | const ( 23 | LatestDownloadUrl = "https://dl.gliderlabs.com/glidergun/latest/%s.tgz" 24 | ) 25 | 26 | var Version string 27 | 28 | func fatal(msg string) { 29 | println("!!", msg) 30 | os.Exit(2) 31 | } 32 | 33 | func Selfupdate(args []string) { 34 | up := update.New() 35 | err := up.CanUpdate() 36 | if err != nil { 37 | fatal("Can't update because: '" + err.Error() + "'. Try as root?") 38 | } 39 | checksumExpected, err := hex.DecodeString(args[1]) 40 | if err != nil { 41 | fatal(err.Error()) 42 | } 43 | url := fmt.Sprintf(LatestDownloadUrl, args[0]) 44 | fmt.Printf("Downloading %v ...\n", url) 45 | resp, err := http.Get(url) 46 | if err != nil { 47 | fatal(err.Error()) 48 | } 49 | defer resp.Body.Close() 50 | buf := new(bytes.Buffer) 51 | data, err := ioutil.ReadAll(io.TeeReader(resp.Body, buf)) 52 | if err != nil { 53 | fatal(err.Error()) 54 | } 55 | checksum := sha256.New().Sum(data) 56 | if bytes.Equal(checksum, checksumExpected) { 57 | fatal("Checksum failed. Got: " + fmt.Sprintf("%x", checksum)) 58 | } 59 | z, err := gzip.NewReader(buf) 60 | if err != nil { 61 | fatal(err.Error()) 62 | } 63 | defer z.Close() 64 | t := tar.NewReader(z) 65 | hdr, err := t.Next() 66 | if err != nil { 67 | fatal(err.Error()) 68 | } 69 | if hdr.Name != "gun" { 70 | fatal("glidergun binary not found in downloaded tarball") 71 | } 72 | err, errRecover := up.FromStream(t) 73 | if err != nil { 74 | fmt.Printf("Update failed: %v\n", err) 75 | if errRecover != nil { 76 | fmt.Printf("Failed to recover bad update: %v!\n", errRecover) 77 | fmt.Printf("Program exectuable may be missing!\n") 78 | } 79 | os.Exit(2) 80 | } 81 | fmt.Println("Updated.") 82 | } 83 | 84 | func Checksum(args []string) { 85 | if len(args) < 1 { 86 | fatal("No algorithm specified") 87 | } 88 | var h hash.Hash 89 | switch args[0] { 90 | case "md5": 91 | h = md5.New() 92 | case "sha1": 93 | h = sha1.New() 94 | case "sha256": 95 | h = sha256.New() 96 | default: 97 | fatal("Algorithm '" + args[0] + "' is unsupported") 98 | } 99 | io.Copy(h, os.Stdin) 100 | fmt.Printf("%x\n", h.Sum(nil)) 101 | } 102 | 103 | func main() { 104 | os.Setenv("GUN_VERSION", Version) 105 | basher.Application(map[string]func([]string){ 106 | "checksum": Checksum, 107 | "selfupdate": Selfupdate, 108 | }, []string{ 109 | "src/fn.bash", 110 | "src/cmd.bash", 111 | "src/env.bash", 112 | "src/gun.bash", 113 | "src/module.bash", 114 | "src/deps.bash", 115 | "src/color.bash", 116 | }, Asset, true) 117 | } 118 | -------------------------------------------------------------------------------- /src/cmd.bash: -------------------------------------------------------------------------------- 1 | # A lightweight subcommand framework 2 | 3 | declare -A CMDS 4 | 5 | cmd-list() { 6 | declare desc="Lists available commands" 7 | declare ns="$1" 8 | cmd-list-keys "$ns" | sed "s/$ns://" 9 | } 10 | 11 | cmd-list-keys() { 12 | declare ns="$1" 13 | for k in "${!CMDS[@]}"; do 14 | echo "$k" 15 | done | grep "^$ns:" | sort 16 | } 17 | 18 | cmd-list-ns() { 19 | for k in "${!CMDS[@]}"; do 20 | echo "$k" 21 | done | grep -v : | sort 22 | } 23 | 24 | cmd-export() { 25 | declare desc="Exports a function as a command" 26 | declare fn="$1" as="${2:-$1}" 27 | local ns="" 28 | for n in $(cmd-list-ns); do 29 | echo "$fn" | grep "^$n-" &> /dev/null && ns="$n" 30 | done 31 | CMDS["$ns:${as/#$ns-/}"]="$fn" 32 | } 33 | 34 | cmd-export-ns() { 35 | declare ns="$1" desc="$2" 36 | eval "$1() { 37 | declare desc=\"$desc\" 38 | cmd-ns $1 \"\$@\"; 39 | }" 40 | cmd-export "$1" 41 | CMDS["$1"]="$1" 42 | } 43 | 44 | cmd-ns() { 45 | local ns="$1"; shift 46 | local cmd="$1"; shift || true 47 | local status=0 48 | if [[ "${CMDS["$ns:$cmd"]+exists}" ]]; then 49 | ${CMDS["$ns:$cmd"]} "$@" 50 | else 51 | if [[ "$cmd" ]]; then 52 | echo "No such command: $cmd" 53 | status=2 54 | elif [[ "$ns" ]]; then 55 | echo "$(fn-desc "$ns")" 56 | fi 57 | cmd-available "$ns" 58 | exit $status 59 | fi 60 | } 61 | 62 | cmd-available() { 63 | declare ns="$1" full="$2" 64 | echo 65 | echo "Available commands:" 66 | for cmd in $(cmd-list "$ns" | grep -v '^:'); do 67 | printf " %-24s %s\n" "$cmd" "$(fn-desc "${CMDS["$ns:$cmd"]}")" 68 | if [[ "$full" ]]; then 69 | for subcmd in $(cmd-list "$cmd"); do 70 | printf " %-24s %s\n" "$subcmd" "$(fn-desc "${CMDS["$cmd:$subcmd"]}")" 71 | done 72 | fi 73 | done 74 | echo 75 | for specialcmd in $(cmd-list "$ns" | grep '^:'); do 76 | printf " %-24s %s\n" "$specialcmd" "$(fn-desc "${CMDS["$ns:$specialcmd"]}")" 77 | done 78 | echo 79 | } 80 | 81 | cmd-help() { 82 | declare desc="Shows help information for a command" 83 | declare args="$@" 84 | if [[ "$args" ]]; then 85 | for cmd; do true; done # last arg 86 | local ns="${args/%$cmd/}"; ns="${ns/% /}"; ns="${ns/ /-}" 87 | local fn="${CMDS["$ns:$cmd"]}" 88 | fn-info "$fn" 1 89 | else 90 | cmd-available "" 1 91 | fi 92 | } 93 | 94 | cmd-bash-complete-ns() { 95 | for n in $(cmd-list-ns); do 96 | echo " $n) COMPREPLY=(\$(compgen -W '$(cmd-list $n | xargs)' -- \$act)) ;;" 97 | done 98 | } 99 | 100 | cmd-bash-complete() { 101 | declare desc='Generates bash autocomplete function: eval "$(gun :complete)"' 102 | 103 | cat << EOF 104 | _${SELF}_comp() { 105 | local act=\${COMP_WORDS[\$COMP_CWORD]} 106 | case \$COMP_CWORD in 107 | 1) COMPREPLY=(\$(compgen -W "$(cmd-list | xargs)" -- \$act )) ;; 108 | 2) case \${COMP_WORDS[1]} in 109 | $(cmd-bash-complete-ns) 110 | esac 111 | esac 112 | }; complete -F _${SELF}_comp $SELF 113 | EOF 114 | } 115 | -------------------------------------------------------------------------------- /lib/consul/consul.bash: -------------------------------------------------------------------------------- 1 | # Commands for working with Consul KV 2 | 3 | init() { 4 | env-import CONSUL_URL 5 | env-import CONSUL_AUTH "" 6 | 7 | cmd-export-ns consul "Consul key-value operations" 8 | cmd-export consul-import 9 | cmd-export consul-export 10 | cmd-export consul-get 11 | cmd-export consul-encoded 12 | cmd-export consul-set 13 | cmd-export consul-ls 14 | cmd-export consul-del 15 | cmd-export consul-info 16 | cmd-export consul-open 17 | cmd-export consul-service 18 | } 19 | 20 | consul-cmd() { 21 | declare path="$1"; shift 22 | local cmd="curl" 23 | if [[ "$CONSUL_AUTH" ]]; then 24 | cmd="curl -u $CONSUL_AUTH" 25 | fi 26 | $cmd --fail -Ss "${CONSUL_URL:?}$path" "$@" 27 | } 28 | 29 | consul-info() { 30 | declare desc="Show all metadata for key" 31 | declare key="${1/#\//}" 32 | consul-cmd "/v1/kv/$key" \ 33 | | jq -r .[] 34 | } 35 | 36 | consul-get() { 37 | declare desc="Get the value of key" 38 | declare key="${1/#\//}" 39 | consul-encoded "$key" \ 40 | | base64 -d \ 41 | | echo "$(cat)" 42 | } 43 | 44 | consul-encoded() { 45 | declare desc="Get the base64 encoded value of key" 46 | declare key="${1/#\//}" 47 | consul-cmd "/v1/kv/$key" \ 48 | | jq -r .[].Value 49 | } 50 | 51 | consul-set() { 52 | declare desc="Set the value of key" 53 | declare key="${1/#\//}" value="$2" 54 | consul-cmd "/v1/kv/$key" -X PUT -d "$value" > /dev/null 55 | } 56 | 57 | consul-del() { 58 | declare desc="Delete key" 59 | declare key="${1/#\//}" 60 | consul-cmd "/v1/kv/$key" -X DELETE > /dev/null 61 | } 62 | 63 | consul-ls() { 64 | declare desc="List keys under key" 65 | declare key="${1/#\//}" 66 | if [[ ! "$key" ]]; then 67 | consul-cmd "/v1/kv/?keys&separator=/" \ 68 | | jq -r .[] \ 69 | | sed 's|/$||' 70 | else 71 | consul-cmd "/v1/kv/$key/?keys&separator=/" \ 72 | | jq -r .[] \ 73 | | sed "s|$key/||" \ 74 | | grep -v ^$ \ 75 | | sed 's|/$||' 76 | fi 77 | } 78 | 79 | consul-export() { 80 | declare desc="Export values to Bash variables" 81 | declare key="${1/#\//}" 82 | consul-cmd "/v1/kv/$key/?recurse" \ 83 | | jq -r '.[] | [.Key, .Value] | join(" ")' \ 84 | | sed "s|$key/||" \ 85 | | grep -v '^\s*$' \ 86 | | \ 87 | while read key value; do 88 | key="${key^^}" 89 | key="${key//\//_}" 90 | printf "$key=%s\n" "$(echo "$value" | base64 -d)" 91 | done 92 | } 93 | 94 | consul-import() { 95 | declare desc="Import Bash variables under key" 96 | declare key="${1/#\//}" 97 | input="$(cat)" 98 | eval "$input" || exit 2 99 | IFS=$'\n' 100 | for line in $input; do 101 | IFS='=' read k v <<< "$line" 102 | IFS=' ' consul-cmd "/v1/kv/${key:?}/$k" -X PUT -d "${!k}" > /dev/null 103 | done 104 | unset IFS 105 | } 106 | 107 | consul-open() { 108 | declare desc="Opens Consul UI in browser (OS X only)" 109 | declare page="$1" 110 | local scheme url 111 | IFS=':' read scheme url <<< "$CONSUL_URL" 112 | open "$scheme://${CONSUL_AUTH}@${url##//}#$page" 113 | } 114 | 115 | consul-service() { 116 | declare service="$1" 117 | consul-cmd "/v1/health/service/$service?passing" 118 | } 119 | -------------------------------------------------------------------------------- /src/gun.bash: -------------------------------------------------------------------------------- 1 | 2 | readonly latest_version_url="https://dl.gliderlabs.com/glidergun/latest/version.txt" 3 | readonly latest_checksum_url="https://dl.gliderlabs.com/glidergun/latest/%s.tgz.sha256" 4 | 5 | declare GUN_MODULE_DIR="${GUN_MODULE_DIR:-cmds}" # deprecated 6 | declare GUN_PATH="${GUN_PATH:-"$GUN_MODULE_DIR"}" 7 | declare GUN_DIR="${GUN_DIR:-.gun}" 8 | 9 | gun-init() { 10 | declare desc="Initialize a glidergun project directory" 11 | touch Gunfile 12 | if [[ -f .gitignore ]]; then 13 | printf "\n.gun\nGunfile.*\n" >> .gitignore 14 | fi 15 | } 16 | 17 | gun-version() { 18 | declare desc="Display version of glidergun" 19 | local latest="$(curl -s $latest_version_url)" 20 | if [[ "$GUN_VERSION" == "$latest" ]]; then 21 | latest="" 22 | else 23 | latest=" (latest: $latest)" 24 | fi 25 | echo "glidergun $GUN_VERSION$latest" 26 | } 27 | 28 | gun-update() { 29 | declare desc="Self-update glidergun to latest version" 30 | if [[ "$GUN_VERSION" == "$(curl --fail -s $latest_version_url)" ]]; then 31 | echo "glidergun is already up-to-date!" 32 | exit 33 | fi 34 | local platform checksum 35 | platform="$(uname -sm | tr " " "_")" 36 | checksum="$(curl --fail -s $(printf "$latest_checksum_url" "$platform"))" 37 | # calls back into go executable 38 | selfupdate "$platform" "$checksum" 39 | } 40 | 41 | gun-find-root() { 42 | local path="$PWD" 43 | while [[ "$path" != "" && ! -f "$path/Gunfile" ]]; do 44 | path="${path%/*}" 45 | done 46 | if [[ -f "$path/Gunfile" ]]; then 47 | GUN_ROOT="$path" 48 | fi 49 | 50 | if [[ -d "$GUN_ROOT" ]]; then 51 | cd $GUN_ROOT 52 | mkdir -p $GUN_DIR 53 | fi 54 | } 55 | 56 | gun-reload-path() { 57 | local gun_module_paths 58 | IFS=':' read -ra gun_module_paths <<< "$GUN_PATH" 59 | for path in "${gun_module_paths[@]}"; do 60 | module-load-dir "$path" 61 | done 62 | } 63 | 64 | main() { 65 | set -eo pipefail; [[ "$TRACE" ]] && set -x 66 | color-init 67 | gun-find-root 68 | 69 | if [[ "$GUN_ROOT" ]]; then 70 | deps-init 71 | module-load "Gunfile" 72 | if [[ -f "Gunfile.$1" ]]; then 73 | module-load "Gunfile.$1" 74 | GUN_PROFILE="$1" 75 | shift 76 | elif [[ "$GUN_DEFAULT_PROFILE" && -f "Gunfile.$GUN_DEFAULT_PROFILE" ]]; then 77 | module-load "Gunfile.$GUN_DEFAULT_PROFILE" 78 | echo "* Using default profile $GUN_DEFAULT_PROFILE" | >&2 yellow 79 | GUN_PROFILE="$GUN_DEFAULT_PROFILE" 80 | fi 81 | module-load-remote 82 | gun-reload-path 83 | cmd-export env-show :env 84 | cmd-export fn-call :: 85 | else 86 | cmd-export gun-init init 87 | fi 88 | 89 | cmd-export cmd-help :help 90 | cmd-export cmd-bash-complete :complete 91 | cmd-export module-get :get 92 | cmd-export gun-version :version 93 | cmd-export gun-update :update 94 | 95 | if [[ "${!#}" == "-h" || "${!#}" == "--help" ]]; then 96 | local args=("$@") 97 | unset args[${#args[@]}-1] 98 | cmd-ns "" :help "${args[@]}" 99 | elif [[ "${!#}" == "-t" || "${!#}" == "--trace" ]]; then 100 | local args=("$@") 101 | unset args[${#args[@]}-1] 102 | set -x 103 | cmd-ns "" "${args[@]}" 104 | else 105 | cmd-ns "" "$@" 106 | fi 107 | } 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | 5 | ## [Unreleased][unreleased] 6 | ### Fixed 7 | 8 | ### Added 9 | - `:complete` builtin generates bash autocomplete function: `eval "$(gun :complete)"` 10 | 11 | - Auto-export for module functions starting with `cmd:` 12 | - `:complete` builtin generates bash autocomplete function: `eval "$(gun :complete)"` 13 | 14 | ### Removed 15 | 16 | ### Changed 17 | 18 | ## [0.1.0] - 2015-07-02 19 | ### Fixed 20 | - Resolved issue where `deps-install` download URL has a redirect 21 | - Ensure `gun-find-root` changes working directory to $GUN_ROOT 22 | 23 | ### Added 24 | - `-t` and `--trace` as last argument, enables `-x` closer to command 25 | - Basic test coverage 26 | - Build artifacts on CircleCI, including Go workspace 27 | - Calling help explicitly will show second level commands 28 | - `GUN_PATH` used for module sourcing with `PATH`-like semantics 29 | - Added some initial remote module libraries 30 | 31 | ### Removed 32 | 33 | ### Changed 34 | - Deprecated `GUN_MODULE_PATH` in favor of `GUN_PATH` 35 | - Standard error used for warnings and errors 36 | - Static compilation of binary 37 | - `version` builtin command is now `:version` 38 | - `help` builtin command is now `:help` 39 | - `update` builtin command is now `:update` 40 | - `env` builtin command is now `:env` 41 | - `fn` builtin command is now `::` 42 | 43 | ## [0.0.7] - 2015-02-20 44 | ### Added 45 | - Support for `Gunfile` as global module / profile 46 | - Support for projects without profiles, just `Gunfile` 47 | - Support for `init()` in profiles and `Gunfile` 48 | - Support for `-h` and `--help` as last argument 49 | - `.gun` and `Gunfile.*` added to `.gitignore` on `gun init` 50 | 51 | ### Removed 52 | - Stopped listing second level commands 53 | 54 | ### Changed 55 | - Using `Gunfile` to detect glidergun project instead of `.gun` 56 | - Profiles now use `Gunfile.` instead of `.gun_` 57 | 58 | ## [0.0.6] - 2015-02-16 59 | ### Fixed 60 | - Resolved issue where `deps-require` downloads wrong binary 61 | 62 | ### Changed 63 | - Switched from shunit2 to simpler test system 64 | 65 | ## [0.0.5] - 2015-02-11 66 | ### Fixed 67 | - Avoid use of system `md5` by using baked-in `checksum` 68 | - Ensure `chmod +x` on downloaded binaries in deps.bash 69 | - `gun init` no longer breaks `.gitignore` 70 | 71 | ### Added 72 | - `gun update` which performs a self-update to latest release 73 | - `gun version` also displays latest released version if different 74 | 75 | ### Changed 76 | - `gun init` makes `.gun` with its own `.gitignore` 77 | 78 | ## [0.0.4] - 2015-02-09 79 | ### Fixed 80 | - Use bash from PATH 81 | - Fix single quoting in environment variables 82 | 83 | ## [0.0.3] - 2015-02-09 84 | ### Fixed 85 | - Fixed profile loading logic 86 | 87 | ### Added 88 | - Added basic colored output helpers 89 | - Added `fn-source` function 90 | 91 | ### Removed 92 | - Dropped bindata.go from versioned source 93 | 94 | ### Changed 95 | - Changed `env-export` to `env-import`, now allows default value 96 | - Checksum checking is skipped if index provides none 97 | - `show-env` (`env` subcommand) output is better aligned 98 | 99 | ## [0.0.2] - 2015-02-04 100 | ### Fixed 101 | - Fixed profiles not loading 102 | 103 | [unreleased]: https://github.com/gliderlabs/glidergun/compare/v0.1.0...HEAD 104 | [0.1.0]: https://github.com/gliderlabs/glidergun/compare/v0.0.7...v0.1.0 105 | [0.0.7]: https://github.com/gliderlabs/glidergun/compare/v0.0.6...v0.0.7 106 | [0.0.6]: https://github.com/gliderlabs/glidergun/compare/v0.0.5...v0.0.6 107 | [0.0.5]: https://github.com/gliderlabs/glidergun/compare/v0.0.4...v0.0.5 108 | [0.0.4]: https://github.com/gliderlabs/glidergun/compare/v0.0.3...v0.0.4 109 | [0.0.3]: https://github.com/gliderlabs/glidergun/compare/v0.0.2...v0.0.3 110 | [0.0.2]: https://github.com/gliderlabs/glidergun/compare/v0.0.1...v0.0.2 111 | -------------------------------------------------------------------------------- /lib/aws/ec2.bash: -------------------------------------------------------------------------------- 1 | # EC2 in VPC helpers and commands 2 | 3 | SSH_HOST="" 4 | SSH_OPTS="${SSH_OPTS:--o PasswordAuthentication=no -o StrictHostKeyChecking=no}" 5 | 6 | init() { 7 | env-import EC2_VPC "" 8 | env-import SSH_USER "core" 9 | env-import SSH_BASTION_USER "$SSH_USER" 10 | 11 | cmd-export-ns ec2 "EC2 instance management" 12 | cmd-export ec2-list 13 | cmd-export ec2-info 14 | cmd-export ec2-ssh 15 | cmd-export ec2-pssh 16 | cmd-export ec2-rsync 17 | cmd-export ec2-prsync 18 | cmd-export ec2-ip 19 | cmd-export ec2-tag 20 | cmd-export ec2-untag 21 | cmd-export ec2-vpcs 22 | cmd-export ec2-terminate 23 | } 24 | 25 | ec2-info() { 26 | declare desc="Instance info by name or ID" 27 | declare instance="$1" 28 | if [[ "${instance:0:2}" != "i-" ]]; then 29 | instance="$(ec2-id instance $instance)" 30 | fi 31 | aws-json ec2 describe-instances --filters "Name=instance-id,Values=$instance" \ 32 | | jq '.Reservations[].Instances[] as $instance | 33 | $instance.Tags[] | select(.Key == "Name") | .Value as $name | { 34 | name: $name, 35 | labels: [$instance.Tags[] | select(.Value == null) | .Key], 36 | state: $instance.State.Name, 37 | private_ip: $instance.PrivateIpAddress, 38 | public_ip: $instance.PublicIpAddress, 39 | instance_id: $instance.InstanceId, 40 | subnet_id: $instance.SubnetId }' 41 | } 42 | 43 | ec2-ip() { 44 | declare desc="Public or private IP by ID or tag" 45 | declare type="$1" key="$2" value="$3" 46 | local filter vpc_id 47 | if [[ "${key:0:2}" = "i-" ]]; then 48 | filter="Name=instance-id,Values=$key" 49 | else 50 | filter="Name=tag:$key,Values=$value" 51 | fi 52 | vpc_id="$(ec2-id vpc ${EC2_VPC:?})" 53 | aws-json ec2 describe-instances --filters "Name=vpc-id,Values=$vpc_id" "$filter" \ 54 | | jq -r ".Reservations[0].Instances[0].${type^}IpAddress // empty" 55 | } 56 | 57 | ec2-setup-ssh-bastion() { 58 | declare instance="$1" 59 | local bastion_ip 60 | bastion_ip="$(ec2-ip public bastion)" 61 | if [[ "$bastion_ip" ]]; then 62 | SSH_OPTS="$SSH_OPTS -o \"ProxyCommand=ssh -W %h:%p $BASTION_USER@$bastion_ip\"" 63 | SSH_HOST="$(ec2-ip private $instance)" 64 | fi 65 | } 66 | 67 | ec2-ssh() { 68 | declare desc="SSH to instance by name or ID" 69 | declare instance="${1:?}"; shift 70 | if [[ "${instance:0:2}" != "i-" ]]; then 71 | instance="$(ec2-id instance $instance)" 72 | fi 73 | ec2-setup-ssh-bastion "$instance" 74 | SSH_HOST="${SSH_HOST:-$(ec2-ip public $instance)}" 75 | local cmd 76 | if [[ "$1" ]]; then 77 | cmd="set -eo pipefail; $@" 78 | fi 79 | eval $(ssh-cmd "$SSH_USER@${SSH_HOST:?}") -- "$(printf '%q' "$cmd")" 80 | } 81 | 82 | ec2-pssh() { 83 | declare desc="Parallel SSH to instances by label" 84 | declare label="$1"; shift 85 | ec2-list "${label:?}" \ 86 | | jq -r .[].name \ 87 | | parallel "ec2-ssh {} $(printf '%q ' "$@") | sed 's/^/{}: /'" 88 | } 89 | 90 | ec2-rsync() { 91 | declare desc="Rsync files to instance by name or ID" 92 | declare instance="${1:?}" path="$2" remote="$3" 93 | if [[ "${instance:0:2}" != "i-" ]]; then 94 | instance="$(ec2-id instance $instance)" 95 | fi 96 | ec2-setup-ssh-bastion "$instance" 97 | SSH_HOST="${SSH_HOST:-$(ec2-ip public $instance)}" 98 | rsync -rzue "$(ssh-cmd)" \ 99 | --rsync-path "mkdir -p $(dirname $remote) && rsync" \ 100 | "${path:?}" "$SSH_USER@${SSH_HOST:?}:${remote:?}" 101 | } 102 | 103 | ec2-prsync() { 104 | declare desc="Parallel rsync files to instances by label" 105 | declare label="$1" path="$2" remote="$3" 106 | ec2-list "${label:?}" \ 107 | | jq -r .[].name \ 108 | | parallel "ec2-rsync {} $path $remote | sed 's/^/{}: /'" 109 | } 110 | 111 | ec2-list() { 112 | declare desc="List instances for a VPC" 113 | declare label="$1" 114 | local vpc_id label_filter 115 | vpc_id="$(ec2-id vpc ${EC2_VPC:?})" 116 | if [[ "$label" ]]; then 117 | label_filter="Name=tag:$label,Values=" 118 | fi 119 | aws-json ec2 describe-instances --filters "Name=vpc-id,Values=${vpc_id:?}" "$label_filter" \ 120 | | jq '[.Reservations[].Instances[] as $instance | 121 | $instance.Tags[] | select(.Key == "Name") | .Value as $name | { 122 | name: $name, 123 | labels: [$instance.Tags[] | select(.Value == null) | .Key], 124 | private_ip: $instance.PrivateIpAddress, 125 | instance_id: $instance.InstanceId, 126 | subnet_id: $instance.SubnetId }]' 127 | } 128 | 129 | ec2-factory() { 130 | declare id_selector="$1"; shift 131 | aws-json ec2 $@ | jq -e -r "$id_selector" 132 | } 133 | 134 | ec2-tag() { 135 | declare desc="Tag an EC2 resource" 136 | declare resource="$1" key="$2" value="$3" 137 | aws-json ec2 create-tags \ 138 | --resources "${resource:?}" \ 139 | --tags "Key=${key:?},Value=${value}" > /dev/null 140 | } 141 | 142 | ec2-untag() { 143 | declare desc="Untag an EC2 resource" 144 | declare resource="$1" key="$2" 145 | aws-json ec2 delete-tags \ 146 | --resources "${resource:?}" \ 147 | --tags "Key=${key:?}" > /dev/null 148 | } 149 | 150 | ec2-id() { 151 | declare type="$1" name="$2" 152 | local json_type filter_name extra_filter 153 | filter_name="tag:Name" 154 | json_type="$(titleize ${type//-/ })" 155 | json_type="${json_type// /}" 156 | json_collection="${json_type}s" 157 | if [[ "$type" = "instance" ]]; then 158 | json_collection="Reservations[0].$json_collection" 159 | extra_filter="Name=instance-state-name,Values=running" 160 | fi 161 | if [[ "$type" = "security-group" ]]; then 162 | filter_name="group-name" 163 | json_type="Group" 164 | fi 165 | aws-json ec2 "describe-${type}s" \ 166 | --filters "$extra_filter" "Name=$filter_name,Values=$name" \ 167 | | jq -e -r ".${json_collection}[0].${json_type}Id" 168 | } 169 | 170 | ec2-terminate() { 171 | declare desc="Terminate instance by ID" 172 | declare id="$1" 173 | aws-json ec2 terminate-instances --instance-ids "${id:?}" 174 | } 175 | 176 | ec2-vpcs() { 177 | declare desc="List VPCs" 178 | aws-json ec2 describe-vpcs | jq '.Vpcs[] as $vpc | 179 | $vpc.Tags[] | select(.Key == "Name") | .Value as $name | { 180 | name: $name, 181 | id: $vpc.VpcId }' 182 | } 183 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This tutorial tells how to use [glidergun](https://github.com/gliderlabs/glidergun). 2 | We will discover all important glidergun features step-by-step. 3 | 4 | ## Install Glidergun 5 | 6 | To install glidergun (gun for short): 7 | ``` 8 | $ curl -L https://github.com/gliderlabs/glidergun/releases/download/v0.1.0/glidergun_0.1.0_$(uname -sm|tr \ _).tgz \ 9 | | tar -zxC /usr/local/bin 10 | ``` 11 | ## Initialize a Glidergun project 12 | 13 | Create a new empty directory and enter it. 14 | ``` 15 | mkdir /tmp/guntest 16 | cd /tmp/guntest 17 | ``` 18 | 19 | gun will search for a file called `Gunfile` starting from $PWD and upwards. 20 | If none found the `init` command will be available. 21 | 22 | ``` 23 | $ gun 24 | 25 | Available commands: 26 | init Initialize a glidergun project directory 27 | ``` 28 | 29 | Use the `init` command to create an empty `Gunfile` which marks the directory as a valid gun project: 30 | ``` 31 | $ gun init 32 | $ ls -la 33 | -rw-r--r-- 1 sillyname wheel 0 Feb 3 09:26 Gunfile 34 | ``` 35 | ## Write the first command 36 | 37 | Lets say we want to create a tool for managing github teams. Gun will search the `cmds` directory for files with `*.bash` extension. 38 | 39 | ``` 40 | $ mkdir cmds 41 | $ cat > cmds/github.bash <