Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
emilhdiaz committed Oct 14, 2020
1 parent 2c0c874 commit 606f645
Show file tree
Hide file tree
Showing 39 changed files with 1,181 additions and 0 deletions.
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Vulcan

The Vulcan project helps developers install and maintain CLI tooling and packages on their Mac OSX machines across a
variety of package managers. Vulcan uses a declarative YAML based configuration file to pin the packages and versions
that should be installed on the machine.

The main interface is through a CLI tool called `vulcan`, which will automatically look for a file named
`vulcan-config.yml` in your current directory unless passed the `--config` option.

## Pre-requisites

At minimum the Mac OSX environment should have [homebrew](https://brew.sh) and [yq](https://github.com/mikefarah/yq)
installed.


## Usage

```bash
Usage: vulcan ACTION [OPTIONS]

ACTIONS:
install Installs development tools
help Prints this usage menu


Global OPTIONS:
--config Path to the configuration file (default: vulcan-config.yml)
--log-level The log level (default INFO)
--dry-run Flag to indicate that the install is just a dry-run
```


### vulcan install

```
vulcan install [--config <vulcan-config.yml>]
```

Vulcan can help developers install and maintain other CLI tools and packages necessary for development. It utilizes a
declarative specification to define the packages that should be installed, the desired versions of those packages,
and the package manager (a.k.a installer) that should be used to install those packages.

Sample configuration in `vulcan-config.yml`:

```yaml
installers:
- name: brew
- name: asdf

programs:
- name: awscli
installer: brew
- name: asdf
installer: brew
- name: direnv
installer: asdf
- name: helm
installer: asdf
version: 3.3.4
- name: nodejs
installer: asdf
version: 14.13.0
```
*NOTE:* If a package version is omitted, then Vulcan assumes that you want to track the `latest` version of that package,
and will check for updates every time the `install` command is run.


Currently Vulcan supports installations through the following package managers:
* brew (homebrew)
* asdf
* sdk (sdkman)
* pipx
* nvm (nodejs versions)
* pyenv (python versions)
* tfenv (terraform versions)

*NOTE:* We highly recommend that were possible you avoid using `brew` as the package manager as it does not allow you
to pin the exact minor version of a package that you need. Homebrew also generally promotes an upgrade only model and
makes it rather difficult to downgrade to a specific minor version of a package.

*NOTE:* We also highly recommend to use the `asdf` package manager when possible, as this package manager supports using
a configuration file call `.tool-versions` within specific directories to pin which versions of a package should be
activated when navigating into that directory (similar to direnv). This `.tool-versions` file can be committed to
version control to synchronize environment and package requirements amongst a team of developers.


## Future Enhancements

* Docker container rather than local dependencies.

38 changes: 38 additions & 0 deletions adt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env zsh

# Stop script execution on any errors
set -euE -o pipefail

USAGE="
Usage: $(basename $0) ACTION [OPTIONS]
ACTIONS:
install Installs development tools
help Prints this usage menu
Global Options:
--config Path to the configuration file (default: vulcan-config.yml)
--log-level The log level (default INFO)
--dry-run Flag to indicate that the install is just a dry-run
"

# Imports
DIR="$( cd "$( dirname "${(%):-%x}" )" >/dev/null 2>&1 && pwd )"
source ${DIR}/lib/install.sh

# Arguments
ACTION=$(parse_arg 'ACTION' 1 'install' "$@")
DRY_RUN=$(parse_long_opt 'dry-run' 'true' "$@")
CONFIG=$(parse_long_opt 'config' '' "$@")
CONFIG=${CONFIG:-"vulcan-config.yml"}
LOGLEVEL=$(parse_long_opt 'log-level' '' "$@")
LOGLEVEL=${LOGLEVEL:-"INFO"}

# Run the desired command

case "${ACTION}" in
install)
install_from_config "${CONFIG}"
;;
esac
76 changes: 76 additions & 0 deletions adt-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
installers:
# - name: brew
# - name: asdf
# - name: sdk
- name: pipx
# - name: nvm
# - name: pyenv
# - name: tfenv

packages:
# - name: zsh
# installer: brew
# - name: git
# installer: brew
# - name: dos2unix
# installer: brew
# - name: jq
# installer: brew
# - name: yq
# installer: brew
# - name: unzip
# installer: brew
# - name: coreutils
# installer: brew
# - name: gnupg
# installer: brew
# - name: bison
# installer: brew
# - name: gettext
# installer: brew
# - name: gnu-tar
# installer: brew
# - name: zlib
# installer: brew
# - name: curl
# installer: brew
# - name: awscli
# installer: brew
# - name: serverless
# installer: brew
# - name: direnv
# installer: asdf
# - name: helm
# installer: asdf
# version: 3.3.4
# plugins:
# - https://github.com/hypnoglow/helm-s3.git
# - https://github.com/databus23/helm-diff
# - name: helmsman
# installer: asdf
# version: 3.4.4
# - name: kubectl
# installer: asdf
# version: 1.19.2
# - name: nodejs
# installer: asdf
# version: 14.13.0
# - name: packer
# installer: asdf
# version: 1.6.4
# - name: python
# installer: asdf
# version: 3.8.5
# - name: poetry
# installer: asdf
# - name: terraform
# installer: asdf
# version: 0.12.29
# - name: java
# installer: sdk
# version: 11.0.8-amzn
# - name: groovy
# installer: sdk
# version: 3.0.6
- name: aws-sso-credential-process
installer: pipx
99 changes: 99 additions & 0 deletions lib/args.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
require() {
: <<DOC
Accepts an array of variable names and checks if each of those variables is set
and not empty
--------------------------------------------------------------------------------
VARS A series of variable names passed as function arguments
DOC
VARS=("$@")
for VAR in "${VARS[@]}"; do
if [[ -z ${!VAR} ]]; then
log_error "Option '--$(echo ${VAR} | tr '[:upper:]' '[:lower:]')' is required"
return 1
fi
done
return 0
}

parse_arg() {
: <<DOC
Parses a positional argument supplied to the command and returns it's value
--------------------------------------------------------------------------------
ARG The name to give the positional argument
IDX The index of the positional argument
VALUES (Optional) The acceptable range of values for this argument
DOC
local ARG=$1 && shift
local IDX=$1 && shift
local VALUES=$1 && shift
local VAL=${argv[IDX]}
# check if no value was supplied
if [[ ${VAL} =~ ^\- ]] || [[ -z ${VAL} ]]; then
log_error "Argument '${ARG}' requires a value to be supplied"
return 1
fi
# check if value is in list of acceptable values
if [[ ! -z "${VALUES}" ]]; then
declare a=("${(@s/|/)VALUES}")
if [[ ! ${a[(ie)${VAL}]} -le ${#a} ]]; then
log_error "Argument '${ARG}=${VAL}' is invalid. Valid values are [${VALUES}]"
return 1
fi
fi
echo ${VAL}
return 0
}

parse_long_opt() {
: <<DOC
Parses a long option supplied to the command and returns it's value
--------------------------------------------------------------------------------
OPT The name of the option to parse
VALUES (Optional) The acceptable range of values for this option
DOC
local OPT=$1 && shift
local VALUES=$1 && shift
local ARGS=("$@")
local OPT_IDX=
local VAL_IDX=
local VAL=
# identify the position of the option
regexp="--${OPT}"
for i in {1..$#argv}; do
[[ ${argv[i]} =~ $regexp ]] && OPT_IDX=${i} && break
done
# if not found, then set val to null
if [[ -z "${OPT_IDX}" ]]; then
VAL=
# else if option is a boolean, then value is just "true"
elif [[ "${VALUES}" == "true" ]]; then
VAL="true"
# else if format [opt=val], then attempt to get value from splitting on the '='
elif [[ ${argv[OPT_IDX]} =~ ^(.*)=(.*)$ ]]; then
VAL=${argv[OPT_IDX]#*=}
# check if no value was supplied
if [[ -z ${VAL} ]]; then
log_error "Option '--${OPT}' requires a value to be supplied"
return 1
fi
# else (format [opt val]), attempt then grab value from next positional argument
else
VAL_IDX=$((OPT_IDX+1))
VAL=${argv[VAL_IDX]}
# check if no value was supplied
if [[ ${VAL} =~ ^\- ]] || [[ -z ${VAL} ]]; then
log_error "Option '--${OPT}' requires a value to be supplied"
return 1
fi
fi
# check value against range of acceptable values
if [[ ! -z ${VAL} ]] && [[ ! -z "${VALUES}" ]]; then
declare a=("${(@s/|/)VALUES}")
if [[ ! ${a[(ie)${VAL}]} -le ${#a} ]]; then
log_error "Option --${OPT}=${VAL} is invalid. Valid options are [${VALUES}]"
return 1
fi
fi
echo ${VAL}
return 0
}
42 changes: 42 additions & 0 deletions lib/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
INCLUDE_DIR="$( cd "$( dirname "${(%):-%x}" )" >/dev/null 2>&1 && pwd )"
source ${INCLUDE_DIR}/args.sh
source ${INCLUDE_DIR}/logs.sh

rr() {
find $1 -name "$2" -type f
}

grep2() {
grep -lZR "$1" | xargs -0 grep -l "$2"
}

quote() {
ruby -rcsv -ne 'puts CSV.generate_line(CSV.parse_line($_), :force_quotes=>true)' $1
}

version() {
: <<DOC
Parses a semantic version so that it can easily be compared to another semantic
version using less than and greater than operators.
--------------------------------------------------------------------------------
DOC
echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'
return 0
}

func_exists() {
# appended double quote is an ugly trick to make sure we do get a string -- if $1 is not a known command, type does not output anything
[ x$(type -t $1) = xfunction ];
}

require_tool() {
local TOOL=$1 && shift
if ! command -v ${TOOL} &> /dev/null; then
log_error "The '${TOOL}' command is required but cannot be found!" && exit 1
fi
}

get_github_latest_release() {
local REPO=$1 && shift
curl --silent "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}
38 changes: 38 additions & 0 deletions lib/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env zsh

_DIR="$( cd "$( dirname "${(%):-%x}" )" >/dev/null 2>&1 && pwd )"
source ${_DIR}/common.sh
source ${_DIR}/installers/darwin.sh
source ${_DIR}/installers/asdf.sh
source ${_DIR}/installers/brew.sh

install_from_config() {
local CONFIG=$1
local LENGTH
local PROGRAM
local INSTALLER
local VERSION

LENGTH=$(yq r "${CONFIG}" --length installers)
for ((i=0; i<=LENGTH-1; i++)); do
INSTALLER=$(yq r "${CONFIG}" "installers[$i].name")

log_info "Found installer ${YELLOW}${INSTALLER}${NC} in configuration file"

darwin_install_or_upgrade_installer "${INSTALLER}"
echo "\n"
done


LENGTH=$(yq r "${CONFIG}" --length packages)
for ((i=0; i<=LENGTH-1; i++)); do
PROGRAM=$(yq r "${CONFIG}" "packages[$i].name")
INSTALLER=$(yq r "${CONFIG}" "packages[$i].installer")
VERSION=$(yq r "${CONFIG}" "packages[$i].version")

log_info "Found package ${YELLOW}${INSTALLER}:${PROGRAM}@${VERSION}${NC} in configuration file"

darwin_install_or_upgrade_package "${INSTALLER}" "${PROGRAM}" "${VERSION}"
echo "\n"
done
}
Loading

0 comments on commit 606f645

Please sign in to comment.