=====================================================================

Excited to bring V5 to life. This includes some BREAKING CHANGES to
several aspects of ZSH-type scwrypts. Please refer to the readme
for upgrade details (specifically docs/upgrade/v4-to-v5.md)

--- New Features -------------------------

- ZSH testing library with basic mock capabilities

- new scwrypts environment file format includes metadata and more
  advanced features like optional parent env overrides, selection
  inheritence, and improved structurual flexibility

- speedup cache for non-CI runs of ZSH-type scwrypts

- ${scwryptsmodule} syntax now allows a consistent unique-naming
  scheme for functions in ZSH-type scwrypts while providing better
  insight into origin of API calls in other modules

- reusable, case-statement-driven argument parsers in ZSH-type scwrypts

--- Changes ------------------------------

- several utility function renames in ZSH-type scwrypts to improve
  consistency

- documentation comments included in ZSH libraries

- ZSH-type scwrypts now allow library modules to live alongside
  executables
  (zsh/lib still supported; autodetection determines default)

--- Bug Fixes ----------------------------

- hardened environment checking for REQUIRED_ENV variables; this removes
  the ability to overwrite variables in local function contexts
This commit is contained in:
Wryn (yage) Wagner 2024-05-10 13:32:02 -06:00 committed by Wryn (yage) Wagner
parent 1b4060dd1c
commit 7f14edd039
271 changed files with 11459 additions and 10516 deletions

View File

@ -22,10 +22,14 @@ executors:
- image: node:18 - image: node:18
resource_class: medium resource_class: medium
zsh:
docker:
- image: alpine:3
resource_class: small
commands: commands:
archlinux-run: archlinux-run:
description: execute command steps in the archlinux container from the CI user description: execute steps in the archlinux container as the CI user
parameters: parameters:
_name: _name:
type: string type: string
@ -243,6 +247,47 @@ jobs:
- run: pip install build && python -m build - run: pip install build && python -m build
- run: pip install twine && twine upload dist/* - run: pip install twine && twine upload dist/*
zsh-test:
executor: zsh
working_directory: ~/scwrypts
steps:
- checkout:
path: ~/scwrypts
- run:
name: install dependencies
command: |
: \
&& apk add \
coreutils \
findutils \
fzf \
perl \
sed \
gawk \
git \
jo \
jq \
util-linux \
uuidgen \
yq \
zsh \
;
- run:
name: scwrypts zsh/unittest
command: |
~/scwrypts/scwrypts run unittest \
;
- run:
name: scwrypts returns proper success codes
command: |
~/scwrypts/scwrypts -n sanity check -- --exit-code 0
[[ $? -eq 0 ]] || exit 1
- run:
shell: /bin/sh
name: scwrypts returns proper error codes
command: |
~/scwrypts/scwrypts -n sanity check -- --exit-code 101
[[ $? -eq 101 ]] || exit 1
workflows: workflows:
test: test:
@ -255,6 +300,7 @@ workflows:
- python-test: *dev-filters - python-test: *dev-filters
- nodejs-test: *dev-filters - nodejs-test: *dev-filters
- zsh-test: *dev-filters
publish: publish:
jobs: jobs:
@ -285,6 +331,7 @@ workflows:
- aur-test - aur-test
- python-publish - python-publish
- nodejs-publish - nodejs-publish
- zsh-test
- python-test: *only-publish-for-full-semver - python-test: *only-publish-for-full-semver
- python-publish: - python-publish:
@ -292,6 +339,7 @@ workflows:
context: [pypi-yage] context: [pypi-yage]
requires: requires:
- python-test - python-test
- zsh-test
- nodejs-test: *only-publish-for-full-semver - nodejs-test: *only-publish-for-full-semver
- nodejs-publish: - nodejs-publish:
@ -299,3 +347,6 @@ workflows:
context: [npm-wrynegade] context: [npm-wrynegade]
requires: requires:
- nodejs-test - nodejs-test
- zsh-test
- zsh-test: *only-publish-for-full-semver

89
.config/create-new-env Executable file
View File

@ -0,0 +1,89 @@
#!/bin/zsh
#
# a temporary template conversion utility for env.template (<=v4)
# to env.yaml (>=v5)
#
eval $(scwrypts --config)
use -c scwrypts/environment-files
ENVIRONMENT_ROOT="$1"
[ "$ENVIRONMENT_ROOT" ] || ENVIRONMENT_ROOT="${0:a:h}"
OLDENV="$ENVIRONMENT_ROOT/env.template"
NEWENV="$ENVIRONMENT_ROOT/env.yaml"
ENVMAP="$ENVIRONMENT_ROOT/.map.txt"
GROUP="$2"
[ $GROUP ] || GROUP=scwrypts
GENERATE_TEMPLATE \
| sed '1,4d; /^$/d' \
| sed -z 's/# \([^\n]*\)\n\([^\n]*\)=/\2=\n\2=DESCRIPTION=\1/g' \
| sed '
s/^export //
/./i---
s/\s\+$//
s/__/=/g
s/^\(AWS\|REDIS\)_/\1=/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:\n \4:\n \5:/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:\n \4:/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:/
s/^\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:/
s/^\([^=]*\)=$/\L\1:/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3:\n \4:\n \5: \E\6/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3:\n \4: \E\5/
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3: \E\4/
s/^\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2: \E\3/
s/^\([^=]*\)=\([^=]\+\)$/\L\1: \E\2/
s/: (\(.*\))/: [\1]/
/^/,/:/{s/_/-/g}
' \
| sed '
s/^ \(description:.*\)/ \1/
s/description:/.DESCRIPTION:/
' \
| sed -z 's/\n\(\s\+\).DESCRIPTION:\([^\n]\+\)/\n\1.DESCRIPTION: >-\n\1 \2/g' \
| yq eval-all '. as $item ireduce ({}; . *+ $item)' \
> "$NEWENV" \
;
cat -- "$OLDENV" \
| sed '
s/#.*//
/^$/d
s/^export //
s/\s\+$//
s/^\([^=]*\)=.*/\1=\n\1/
' \
| sed '
/^/s/.*/\L&/
/^/s/__/./g
/^/s/_/-/g
s/^/./
s/\(aws\|redis\)-/\1./
' \
| perl -pe 's/=\n/^/' \
| column -ts '^' \
> "$ENVMAP" \
;
while read line
do
ENV_VAR=$(echo $line | awk '{print $1;}')
LOOKUP=$(echo $line | awk '{print $2;}')
cp "$NEWENV" "$NEWENV.temp"
cat "$NEWENV.temp" \
| yq ". | $LOOKUP.[\".ENVIRONMENT\"] = \"$ENV_VAR\"" \
| yq 'sort_keys(...)' \
> "$NEWENV"
;
done < "$ENVMAP"
rm -- "$NEWENV.temp" "$ENVMAP" &>/dev/null
head -n1 -- "$NEWENV" | grep -q "^{}$" && {
echo '---' > "$NEWENV"
}
cat -- "$NEWENV" | yq
SUCCESS "new environment saved to '$NEWENV'"

66
.config/env.yaml Normal file
View File

@ -0,0 +1,66 @@
---
aws:
.DESCRIPTION: >-
standard AWS environment variables used by awscli and other tools
account:
.ENVIRONMENT: AWS_ACCOUNT
efs:
local-mount-point:
.DESCRIPTION: >-
fully-qualified path to mount the EFS drive
.ENVIRONMENT: AWS__EFS__LOCAL_MOUNT_POINT
profile:
.ENVIRONMENT: AWS_PROFILE
region:
.ENVIRONMENT: AWS_REGION
directus:
.DESCRIPTION: >-
details for a directus instance
api-token:
.ENVIRONMENT: DIRECTUS__API_TOKEN
base-url:
.ENVIRONMENT: DIRECTUS__BASE_URL
discord:
.DESCRIPTION: >-
details for discord bot
bot-token:
.ENVIRONMENT: DISCORD__BOT_TOKEN
content-footer:
.ENVIRONMENT: DISCORD__CONTENT_FOOTER
content-header:
.ENVIRONMENT: DISCORD__CONTENT_HEADER
default-avatar-url:
.ENVIRONMENT: DISCORD__DEFAULT_AVATAR_URL
default-channel-id:
.ENVIRONMENT: DISCORD__DEFAULT_CHANNEL_ID
default-username:
.ENVIRONMENT: DISCORD__DEFAULT_USERNAME
default-webhook:
.ENVIRONMENT: DISCORD__DEFAULT_WEBHOOK
linear:
.DESCRIPTION: >-
linear.app project management configuration
api-token:
.ENVIRONMENT: LINEAR__API_TOKEN
redis:
.DESCRIPTION: >-
redis connection credentials
auth:
.ENVIRONMENT: REDIS_AUTH
host:
.ENVIRONMENT: REDIS_HOST
port:
.ENVIRONMENT: REDIS_PORT
twilio:
.DESCRIPTION: >-
twilio account / credentials
account-sid:
.ENVIRONMENT: TWILIO__ACCOUNT_SID
api-key:
.ENVIRONMENT: TWILIO__API_KEY
api-secret:
.ENVIRONMENT: TWILIO__API_SECRET
default-phone-from:
.ENVIRONMENT: TWILIO__DEFAULT_PHONE_FROM
default-phone-to:
.ENVIRONMENT: TWILIO__DEFAULT_PHONE_TO

View File

@ -1,59 +1,76 @@
# *Scwrypts* (Wryn + Scripts) # *Scwrypts*
Scwrypts is a friendly CLI / API for quickly running *sandboxed scripts* in the terminal. Scwrypts is a CLI and API for safely running scripts in the terminal, CI, and other automated environments.
In modern developer / dev-ops workflows, scripts require a complex configurations. Local runs provide a user-friendly approach to quickly execute CI workflows and automations in your terminal.
Without a better solution, the developer is cursed to copy lines-upon-lines of variables into terminals, create random text artifacts, or maybe even commit secure credentials into source. Each local run runs through an interactive, *sandboxed environment* so you never accidentally run dev credentials in production ever again!
Scwrypts leverages ZSH to give hot-key access to run scripts in such environments.
## Major Version Upgrade Notice ## Major Version Upgrade Notice
Please refer to [Version 3 to Version 4 Upgrade Path](./docs/upgrade/v3-to-v4.md) when upgrading from scwrypts v3 to scwrypts v4! Please refer to [Version 4 to Version 5 Upgrade Path](./docs/upgrade/v4-to-v5.md) when upgrading from scwrypts v4 to scwrypts v5!
## Dependencies ## Installation
Due to the wide variety of resources used by scripting libraries, the user is expected to manually resolve dependencies.
Dependencies are lazy-loaded, and more information can be found by command error messages or in the appropriate README.
Because Scwrypts relies on Scwrypts (see [Meta Scwrypts](./zsh/scwrypts)), `zsh` must be installed and [`junegunn/fzf`](https://github.com/junegunn/fzf) must be available on your PATH. Quick installation is supported through both the [Arch User Repository](https://aur.archlinux.org/packages/scwrypts) and [Homebrew](https://github.com/wrynegade/homebrew-brew/tree/main/Formula)
## Usage ```bash
Install Scwrypts by cloning this repository and sourcing `scwrypts.plugin.zsh` in your `zshrc`. # AUR
You can now run Scwrypts using the ZLE hotkey bound to `SCWRYPTS_SHORTCUT` (default `CTRL + W`). yay -Syu scwrypts
```console # homebrew
% cd <path-to-cloned-repo> brew install wrynegade/scwrypts
% echo "source $(pwd)/scwrypts.plugin.zsh >> $HOME/.zshrc"
``` ```
Check out [Meta Scwrypts](./zsh/scwrypts) to quickly set up environments and adjust configuration. ### Manual Installation
To install scwrypts manually, clone this repository (and take note of where it is installed)
### No Install / API Usage Replacing the `/path/to/cloned-repo` appropriately, add the following line to your `~/.zshrc`:
Alternatively, the `scwrypts` API can be used directly:
```zsh ```zsh
./scwrypts [--env environment-name] (...script-name-patterns...) [-- ...passthrough arguments... ] source /path/to/cloned-repo/scwrypts.plugin.zsh
``` ```
Given one or more script patterns, Scwrypts will filter the commands by pattern conjunction. The next time you start your terminal, you can now execute scwrypts by using the plugin shortcut(s) (by default `CTRL + SPACE`).
If only one command is found which matches the pattern(s), it will immediately begin execution. Plugin shortcuts are configurable in your scwrypts configuration file found in `~/.config/scwrypts/config.zsh`, and [here is the default config](./zsh/config.user.zsh).
If multiple commands match, the user will be prompted to select from the filtered list.
Of course, if no commands match, Scwrypts will exit with an error.
Given no script patterns, Scwrypts becomes an interactive CLI, prompting the user to select a command. If you want to use the `scwrypts` program directly, you can either invoke the executable `./scwrypts` or link it in your PATH for easy access.
For example, if you have `~/.local/bin` in your PATH, you might run:
```zsh
ln -s /path/to/cloned-repo/scwrypts "${HOME}/.local/bin/scwrypts"
```
After determining which script to run, if no environment has been specified, Scwrypts prompts the user to choose one. #### PATH Dependencies
Scwrypts provides a framework for workflows which often depend on a variety of other tools.
Although the lazy-loaded dependency model allows hardening in CI and extendability, the user is expected to _resolve required PATH dependencies_.
When running locally, this is typically as simple as "install the missing program," but this may require additional steps when working in automated environments.
By default, the `ci` plugin is enabled which provides the `check all dependencies` scwrypt.
You can run this to output a comprehensive list of PATH dependencies across all scwrypts groups, but, at a bare minimum, you will need the following applications in your PATH:
```bash
zsh
grep # GNU
sed # GNU
sort # GNU
fzf # https://github.com/junegunn/fzf (only required for interactive / local)
jo # https://github.com/jpmens/jo
jq # https://github.com/jqlang/jq
yq # https://github.com/mikefarah/yq
```
### Using in CI/CD or Automated Workflows ## Usage in CI and Automated Environments
Set environment variable `CI=true` (and use the no install method) to run in an automated pipeline.
Set environment variable `CI=true` to run scwrypts in an automated environment.
There are a few notable changes to this runtime: There are a few notable changes to this runtime:
- **The Scwrypts sandbox environment will not load.** All variables will be read from context. - **The Scwrypts sandbox environment will not load.** All variables will be read directly from the current context.
- User yes/no prompts will **always be YES** - User yes/no prompts will **always be YES**
- Other user input will default to an empty string - Other user input will default to an empty string
- Logs will not be captured - Logs will not be captured in the user's local cache
- Setting the environment variable `SCWRYPTS_GROUP_LOADER__[a-z_]\+` will source the file indicated in the variable (this allows custom groups without needing to modify the `config.zsh` directly) - In GitHub actions, `*.scwrypts.zsh` groups are detected automatically from the `$GITHUB_WORKSPACE`; set `SCWRYPTS_GITHUB_NO_AUTOLOAD=true` to disable
- In GitHub actions, `*.scwrypts.zsh` groups are detected automatically from the `$GITHUB_WORKSPACE`; set `SCWRYPTS_GITHUB_NO_AUTOLOAD=true` to disable
## Contributing ## Contributing

View File

@ -1,6 +1,6 @@
___________________________ ___________________________
< ~thanks for your support~ > < ~thanks for your support~ >
--------------------------- ---------------------------
\ ^__^ \ ^__^
\ (oo)\_______ \ (oo)\_______
(__)\ )\/\ (__)\ )\/\

View File

@ -57,7 +57,7 @@ Don't worry, it's easy.
Take your original scwrypt, and slap the executable stuff into a function called `MAIN` (yes, it must be _exactly_, all-caps `MAIN`): Take your original scwrypt, and slap the executable stuff into a function called `MAIN` (yes, it must be _exactly_, all-caps `MAIN`):
```diff ```diff
#!/bin/zsh #!/usr/bin/env zsh
##################################################################### #####################################################################
DEPENDENCIES+=(dep-function-a dep-function-b) DEPENDENCIES+=(dep-function-a dep-function-b)
REQUIRED_ENV+=() REQUIRED_ENV+=()
@ -69,11 +69,11 @@ CHECK_ENVIRONMENT
- echo "do some stuff here" - echo "do some stuff here"
- # ... etc ... - # ... etc ...
- SUCCESS "completed the stuff" - echo.success "completed the stuff"
+ MAIN() { + MAIN() {
+ echo "do some stuff here" + echo "do some stuff here"
+ # ... etc ... + # ... etc ...
+ SUCCESS "completed the stuff + echo.success "completed the stuff
+ } + }
``` ```
@ -85,7 +85,7 @@ All I had to do in this case was delete the function invocation at the end:
```diff ```diff
# ... top boilerplate ... # ... top boilerplate ...
MAIN() { MAIN() {
SUCCESS "look at me I'm so cool I already wrote this in a main function" echo.success "look at me I'm so cool I already wrote this in a main function"
} }
- -
- ##################################################################### - #####################################################################
@ -115,20 +115,20 @@ Also you can ditch the `CHECK_ENVIRONMENT`.
While it won't hurt, v4 already does this, so just get rid of it. While it won't hurt, v4 already does this, so just get rid of it.
Here's my recommended formatting: Here's my recommended formatting:
```diff ```diff
#!/bin/zsh #!/usr/bin/env zsh
- ##################################################################### - #####################################################################
DEPENDENCIES+=(dep-function-a dep-function-b) DEPENDENCIES+=(dep-function-a dep-function-b)
- REQUIRED_ENV+=() - REQUIRED_ENV+=()
use do/awesome/stuff --group my-custom-library use do/awesome/stuff --group my-custom-library
- -
- CHECK_ENVIRONMENT - CHECK_ENVIRONMENT
##################################################################### #####################################################################
MAIN() { MAIN() {
echo "do some stuff here" echo "do some stuff here"
# ... etc ... # ... etc ...
SUCCESS "completed the stuff echo.success "completed the stuff
} }
``` ```
@ -164,7 +164,7 @@ If you _have_ done it already, typically by writing a variable called "USAGE" in
Returning to our original `MAIN()` example, I'll add some options parsing so we should now look something like this: Returning to our original `MAIN()` example, I'll add some options parsing so we should now look something like this:
```sh ```sh
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(dep-function-a dep-function-b) DEPENDENCIES+=(dep-function-a dep-function-b)
use do/awesome/stuff --group my-custom-library use do/awesome/stuff --group my-custom-library
@ -200,7 +200,7 @@ I want to call out a few specific ones:
Just add another section to define these values before declaring `MAIN`: Just add another section to define these values before declaring `MAIN`:
```sh ```sh
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(dep-function-a dep-function-b) DEPENDENCIES+=(dep-function-a dep-function-b)
use do/awesome/stuff --group my-custom-library use do/awesome/stuff --group my-custom-library

136
docs/upgrade/v4-to-v5.md Normal file
View File

@ -0,0 +1,136 @@
# Scwrypts Upgrade v4 to v5 Notes
Although scwrypts v4 brings a number of new features, most functionality is backwards-compatible.
## Lots of renames!
Nearly every module received a rename.
This was a decision made to improve both style-consistency and import transparency, but has resulted in a substantial number of breaking changes to `zsh-type scwrypts modules`.
### `zsh/utils` Functions
The functions in the underlying library have all been renamed, but otherwise maintain the same functionality.
For a full reference, check out the [zsh/utils](../../zsh/utils/utils.module.zsh), but some critical renames are:
```bash
FZF >> utils.fzf
FZF_USER_INPUT >> utils.fzf.user-input
LESS >> utils.less
YQ >> utils.yq
SUCCESS >> echo.success
ERROR >> echo.error
REMINDER >> echo.reminder
STATUS >> echo.status
WARNING >> echo.warning
DEBUG >> echo.debug
FAIL >> utils.fail
ABORT >> utils.abort
CHECK_ERRORS >> utils.check-errors
Yn >> utils.Yn
yN >> utils.yN
EDIT >> utils.io.edit
CHECK_ENVIRONMENT >> utils.check-environment
```
### `zsh/utils` Color Functions
Rather than storing ANSI colors as a variable, colors are now stored as a function which prints the color code.
Doing this has proven more versatile than trying to extract the value of the variable in several contexts.
Rename looks like this for all named ANSI colors:
```bash
$__GREEN >> utils.colors.green
$__BRIGHT_RED >> utils.colors.bright-red
```
The most common use case of colors is indirectly through the `echo.*` commands, so a new function now provides _the color used by the associated `echo.*` command_:
```bash
# instead of
STATUS "Hello there, ${_BRIGHT_GREEN}bobby${_YELLOW}. How are you?"
# use
echo.status "Hello there, $(utils.colors.bright-green)bobby$(echo.status.color). How are you?
```
### ZSH Scwrypts Module Naming
**This is the biggest point of refactor.**
You will notice that modules now declare their functions using a `${scwryptsmodule}` notation.
This notation provides a dot-notated name which is intended to provide a consistent, unique naming system in ZSH (remember, everything loaded into the same shell script must have a globally-unique name).
Consider the new naming method for the following:
```bash
# v4: zsh/lib/helm/template.module.zsh
HELM__TEMPLATE__GET() {
# ...
}
# v5: zsh/helm/get-template.module.zsh
${scwryptsmodule}() {
# ...
}
```
Although the import syntax is generally the same, now we reference the full name of the module instead of the arbitrarily defined `HELM__TEMPLATE__GET`:
```
# in some other scwrypt
use helm/get-template
helm.get-template --raw ./my-helm-chart
```
The name `${scwryptsmodule}` is depended on the scwrypts library path.
Since there is not an easy way to provide an exhaustive list, go through all the places where you `use` something from the scwrypts core library, and check to see where it is now.
One of the critical call-outs is the AWS CLI, which no longer follows the "just use ALL CAPS for function names," but instead is a proper module.
Both of the following are valid ways to use the scwrypts-safe aws-cli (`AWS` in v4):
```bash
# to import _only_ AWS cli
use cloud.aws.cli
cloud.aws.cli sts get-caller-identity
# importing the full AWS module also provides an alias
use cloud.aws
cloud.aws sts get-caller-identity
```
### Great news!
Great news!
We have finished with **all of the necessary steps** to migrate to v5!
If you still have the energy, take some time to make these _recommended_ adjustments too.
### Use the new `${scwryptsmodule}` syntax
The `${scwryptsmodule}` name is now automatically available in any module.
The one change from the `${scwryptsmodule}` in scwrypts core is that **your scwrypts group name is the first dot**.
If I'm building the scwrypts group called `my-cool-stuff` and open the file `my-cool-stuff/zsh/module-a.module.zsh`, then `${scwryptsmodule}` will refer to `my-cool-stuff.module-a`.
### Update your `*.scwrypts.zsh` declaration file
In v4 and earlier, it was tricky to create your own scwrypts group, since you had to create a particular folder structure, and write a `group-name.scwrypts.zsh` file with some somewhat arbitrary requirements.
In v5, you can now make any folder a scwrypts group by simply _creating the `*.scwrypts.zsh` file_.
```bash
# this will turn the current folder into the root of a scwrypts group called `my-cool-stuff`
touch 'my-cool-stuff.scwrypts.zsh'
├── zsh
├── zx
└── py
```
Advanced options for scwrypts are now [documented in the example](../../scwrypts.scwrypts.zsh), so please refer to it for any additional changes you may need for existing scwrypts modules.

View File

@ -0,0 +1 @@
---

View File

@ -1,10 +1,3 @@
# Kubernetes `kubectl` Helper Plugin # CI Helper
Leverages a local `redis` application to quickly and easily set an alias `k` for `kubectl --context <some-context> --namespace <some-namespace>`. Disabled by default, this is used in CI contexts to try and identify missing requirements for the current workflow.
Much like scwrypts environments, `k` aliases are *only* shared amongst session with the same `SCWRYPTS_ENV` to prevent accidental cross-contamination.
## Getting Started
Enable the plugin in `~/.config/scwrypts/config.zsh` by adding `SCWRYPTS_PLUGIN_ENABLED__KUBECTL=1`.
Use `k` as your new `kubectl` and checkout `k --help` and `k meta --help`.

View File

@ -1,7 +1,7 @@
#!/bin/zsh #!/usr/bin/env zsh
##################################################################### #####################################################################
MAIN() { MAIN() {
cd "$SCWRYPTS_ROOT__scwrypts/" cd "$(scwrypts.config.group scwrypts root)"
DEPENDENCIES+=() DEPENDENCIES+=()
for group in ${SCWRYPTS_GROUPS[@]} for group in ${SCWRYPTS_GROUPS[@]}
@ -11,7 +11,7 @@ MAIN() {
GROUP_HOME="$(eval 'echo $SCWRYPTS_ROOT__'$group)" GROUP_HOME="$(eval 'echo $SCWRYPTS_ROOT__'$group)"
[ $GROUP_HOME ] && [ -d "$GROUP_HOME" ] || continue [ $GROUP_HOME ] && [ -d "$GROUP_HOME" ] || continue
STATUS "checking dependencies for $group" echo.status "checking dependencies for $group"
DEPENDENCIES+=($( DEPENDENCIES+=($(
for file in $( for file in $(
{ {
@ -21,13 +21,13 @@ MAIN() {
} | grep -v '\.md$' | grep -v 'check-all-dependencies$') } | grep -v '\.md$' | grep -v 'check-all-dependencies$')
do do
sed -z 's/.*DEPENDENCIES+=(\([^)]*\)).*/\1\n/; s/#.*\n//g; s/\s\+/\n/g' "$GROUP_HOME/$file" sed -z 's/.*DEPENDENCIES+=(\([^)]*\)).*/\1\n/; s/#.*\n//g; s/\s\+/\n/g' "$GROUP_HOME/$file"
done done
)) ))
done done
DEPENDENCIES=(zsh $(echo $DEPENDENCIES | sed 's/ /\n/g' | sort -u | grep '^[-_a-zA-Z]\+$')) DEPENDENCIES=(zsh $(echo $DEPENDENCIES | sed 's/ /\n/g' | sort -u | grep '^[-_a-zA-Z]\+$'))
STATUS "discovered dependencies: ($DEPENDENCIES)" echo.status "discovered dependencies: ($DEPENDENCIES)"
echo $DEPENDENCIES | sed 's/ /\n/g' echo $DEPENDENCIES | sed 's/ /\n/g'
CHECK_ENVIRONMENT && SUCCESS "all dependencies satisfied" utils.check-environment && echo.success "all dependencies satisfied"
} }

View File

@ -1,5 +1 @@
SCWRYPTS_GROUPS+=(ci) export ${scwryptsgroup}__type=zsh
export SCWRYPTS_TYPE__ci=zsh
export SCWRYPTS_ROOT__ci="$SCWRYPTS_ROOT__scwrypts/plugins/ci"
export SCWRYPTS_COLOR__ci='\033[0m'

View File

@ -0,0 +1,5 @@
---
scwrypts-kubectl-redis:
.DESCRIPTION: >-
[currently only 'managed'] 'managed' or 'custom' redis configuration
.ENVIRONMENT: SCWRYPTS_KUBECTL_REDIS

View File

@ -6,10 +6,11 @@ for CLI in kubectl helm flux
do do
eval "_${CLI[1]}() { eval "_${CLI[1]}() {
local SUBSESSION=0 local SUBSESSION=0
echo \${words[2]} | grep -q '^[0-9]\\+$' && SUBSESSION=\${words[2]} local SUBSESSION_OFFSET=2
echo \${words[2]} | grep -q '^[0-9]\\+$' && SUBSESSION=\${words[2]} && SUBSESSION_OFFSET=3
local PASSTHROUGH_WORDS=($CLI) local PASSTHROUGH_WORDS=($CLI)
[[ \$CURRENT -gt 2 ]] && echo \${words[2]} | grep -qv '^[0-9]\\+$' && { [[ \$CURRENT -gt \${SUBSESSION_OFFSET} ]] && echo \${words[\${SUBSESSION_OFFSET}]} | grep -qv '^[0-9]\\+$' && {
local KUBECONTEXT=\$(k \$SUBSESSION meta get context) local KUBECONTEXT=\$(k \$SUBSESSION meta get context)
local NAMESPACE=\$(k \$SUBSESSION meta get namespace) local NAMESPACE=\$(k \$SUBSESSION meta get namespace)
@ -26,8 +27,8 @@ do
for WORD in \${words[@]:1} for WORD in \${words[@]:1}
do do
case \$WORD in case \$WORD in
[0-9]* ) continue ;; ( [0-9]* ) continue ;;
-- ) ( -- )
echo \$words | grep -q 'exec' && ((DELIMIT_COUNT+=1)) echo \$words | grep -q 'exec' && ((DELIMIT_COUNT+=1))
[[ \$DELIMIT_COUNT -eq 0 ]] && ((DELIMIT_COUNT+=1)) && continue [[ \$DELIMIT_COUNT -eq 0 ]] && ((DELIMIT_COUNT+=1)) && continue
;; ;;
@ -37,7 +38,7 @@ do
echo \"\$words\" | grep -q '\\s\\+$' && PASSTHROUGH_WORDS+=(' ') echo \"\$words\" | grep -q '\\s\\+$' && PASSTHROUGH_WORDS+=(' ')
words=\"\$PASSTHROUGH_WORDS\" words=\"\${PASSTHROUGH_WORDS[@]}\"
_$CLI _$CLI
} }
" "

View File

@ -7,12 +7,12 @@ f() { _SCWRYPTS_KUBECTL_DRIVER flux $@; }
_SCWRYPTS_KUBECTL_DRIVER() { _SCWRYPTS_KUBECTL_DRIVER() {
[ ! $SCWRYPTS_ENV ] && { [ ! $SCWRYPTS_ENV ] && {
ERROR "must set SCWRYPTS_ENV in order to use '$(echo $CLI | head -c1)'" echo.error "must set SCWRYPTS_ENV in order to use '$(echo $CLI | head -c1)'"
return 1 return 1
} }
which REDIS >/dev/null 2>&1 \ which kube.redis >/dev/null 2>&1 \
|| eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kubectl)" || eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kube)"
local CLI="$1"; shift 1 local CLI="$1"; shift 1
@ -38,16 +38,16 @@ _SCWRYPTS_KUBECTL_DRIVER() {
description: - description: -
" "
local USAGE__usage=$(echo $CLI | head -c1) local USAGE__usage=$(echo $CLI | head -c1)
local USAGE__args="$( local USAGE__args="$(
{ {
echo "\\033[0;32m[0-9]\\033[0m^if the first argument is a number 0-9, uses or creates a subsession (default 0)" echo "$(utils.colors.print green '[0-9]')^if the first argument is a number 0-9, uses or creates a subsession (default 0)"
echo " ^ " echo " ^ "
for C in ${CUSTOM_COMMANDS[@]} for C in ${CUSTOM_COMMANDS[@]}
do do
echo "\\033[0;32m$C\\033[0m^$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__$C 2>/dev/null)" echo "$(utils.colors.print green ${C})^$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__$C 2>/dev/null)"
done done
} | column -ts '^' } | column -ts '^'
)" )"
@ -61,18 +61,18 @@ _SCWRYPTS_KUBECTL_DRIVER() {
utility. These functions leverage redis and scwrypts environments to utility. These functions leverage redis and scwrypts environments to
allow quick selection of contexts and namespaces usable across all allow quick selection of contexts and namespaces usable across all
active shell instances. active shell instances.
The scwrypts group 'kubectl' has simple selection executables for The scwrypts group 'kubectl' has simple selection executables for
kubecontext and namespace, but also provides the library to enable kubecontext and namespace, but also provides the library to enable
enriched, use-case-sensitive setup of kubernetes context. enriched, use-case-sensitive setup of kubernetes context.
All actions are scoped to the current SCWRYPTS_ENV All actions are scoped to the current SCWRYPTS_ENV
currently : \\033[0;33m$SCWRYPTS_ENV\\033[0m currently : $(utils.colors.print yellow ${SCWRYPTS_ENV})
" "
########################################## ##########################################
local USER_ARGS=() local USER_ARGS=()
local CUSTOM_COMMAND=0 local CUSTOM_COMMAND=0
@ -134,9 +134,9 @@ _SCWRYPTS_KUBECTL_DRIVER() {
while [[ $# -gt 0 ]]; do USER_ARGS+=($1); shift 1; done while [[ $# -gt 0 ]]; do USER_ARGS+=($1); shift 1; done
CHECK_ERRORS --no-fail || return 1 utils.check-errors || return 1
[[ $HELP -eq 1 ]] && { USAGE; return 0; } [[ $HELP -eq 1 ]] && { utils.io.usage; return 0; }
##################################################################### #####################################################################
@ -154,12 +154,12 @@ _SCWRYPTS_KUBECTL_DRIVER() {
[ $CONTEXT ] && [[ $CLI =~ ^flux$ ]] && CLI_ARGS+=(--context $CONTEXT) [ $CONTEXT ] && [[ $CLI =~ ^flux$ ]] && CLI_ARGS+=(--context $CONTEXT)
[[ $STRICT -eq 1 ]] && { [[ $STRICT -eq 1 ]] && {
[ $CONTEXT ] || ERROR "missing kubectl 'context'" [ $CONTEXT ] || echo.error "missing kubectl 'context'"
[ $NAMESPACE ] || ERROR "missing kubectl 'namespace'" [ $NAMESPACE ] || echo.error "missing kubectl 'namespace'"
CHECK_ERRORS --no-fail --no-usage || { utils.check-errors --no-fail --no-usage || {
ERROR "with 'strict' settings enabled, context and namespace must be set!" echo.error "with 'strict' settings enabled, context and namespace must be set!"
REMINDER " echo.reminder "
these values can be set directly with these values can be set directly with
$(echo $CLI | head -c1) meta set (namespace|context) $(echo $CLI | head -c1) meta set (namespace|context)
" "
@ -170,16 +170,16 @@ _SCWRYPTS_KUBECTL_DRIVER() {
[ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE) [ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE)
[[ $VERBOSE -eq 1 ]] && { [[ $VERBOSE -eq 1 ]] && {
REMINDER " echo.reminder "
context '$CONTEXT' context '$CONTEXT'
namespace '$NAMESPACE' namespace '$NAMESPACE'
environment '$SCWRYPTS_ENV' environment '$SCWRYPTS_ENV'
subsession '$SUBSESSION' subsession '$SUBSESSION'
" "
STATUS "running $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}" echo.status "running $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
} || { } || {
[[ $(_SCWRYPTS_KUBECTL_SETTINGS get context) =~ ^show$ ]] && { [[ $(_SCWRYPTS_KUBECTL_SETTINGS get context) =~ ^show$ ]] && {
REMINDER "$SCWRYPTS_ENV.$SUBSESSION : $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}" echo.reminder "$SCWRYPTS_ENV.$SUBSESSION : $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
} }
} }
$CLI ${CLI_ARGS[@]} ${USER_ARGS[@]} $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}
@ -190,7 +190,7 @@ _SCWRYPTS_KUBECTL_DRIVER() {
_SCWRYPTS_KUBECTL_SETTINGS() { _SCWRYPTS_KUBECTL_SETTINGS() {
# (get setting-name) or (set setting-name setting-value) # (get setting-name) or (set setting-name setting-value)
REDIS h$1 ${SCWRYPTS_ENV}:kubectl:settings ${@:2} | grep . kube.redis h$1 ${SCWRYPTS_ENV}:kubectl:settings ${@:2} | grep .
} }
##################################################################### #####################################################################

View File

@ -23,16 +23,16 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__meta() {
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
do do
case $1 in case $1 in
-h | --help ) HELP=1 ;; ( -h | --help ) HELP=1 ;;
set ) ( set )
USAGE__usage+=" set" USAGE__usage+=" set"
USAGE__args="set (namespace|context)" USAGE__args="set (namespace|context)"
USAGE__description="interactively set a namespace or context for '$SCWRYPTS_ENV'" USAGE__description="interactively set a namespace or context for '$SCWRYPTS_ENV'"
case $2 in case $2 in
namespace | context ) USER_ARGS+=($1 $2 $3); [ $3 ] && shift 1 ;; ( namespace | context ) USER_ARGS+=($1 $2 $3); [ $3 ] && shift 1 ;;
-h | --help ) HELP=1 ;; ( -h | --help ) HELP=1 ;;
'' ) ( '' )
: \ : \
&& SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set context \ && SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set context \
&& SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set namespace \ && SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set namespace \
@ -40,40 +40,40 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__meta() {
return $? return $?
;; ;;
* ) ERROR "cannot set '$2'" ;; ( * ) echo.error "cannot set '$2'" ;;
esac esac
shift 1 shift 1
;; ;;
get ) ( get )
USAGE__usage+=" get" USAGE__usage+=" get"
USAGE__args="get (namespace|context|all)" USAGE__args="get (namespace|context|all)"
USAGE__description="output the current namespace or context for '$SCWRYPTS_ENV'" USAGE__description="output the current namespace or context for '$SCWRYPTS_ENV'"
case $2 in case $2 in
namespace | context | all ) USER_ARGS+=($1 $2) ;; ( namespace | context | all ) USER_ARGS+=($1 $2) ;;
-h | --help ) HELP=1 ;; ( -h | --help ) HELP=1 ;;
* ) ERROR "cannot get '$2'" ;; ( * ) echo.error "cannot get '$2'" ;;
esac esac
shift 1 shift 1
;; ;;
copy ) ( copy )
USAGE__usage+=" copy" USAGE__usage+=" copy"
USAGE__args+="copy [0-9]" USAGE__args+="copy [0-9]"
USAGE__description="copy current subsession ($SUBSESSION) to target subsession id" USAGE__description="copy current subsession ($SUBSESSION) to target subsession id"
case $2 in case $2 in
[0-9] ) USER_ARGS+=($1 $2) ;; ( [0-9] ) USER_ARGS+=($1 $2) ;;
-h | --help ) HELP=1 ;; ( -h | --help ) HELP=1 ;;
* ) ERROR "target session must be a number [0-9]" ;; ( * ) echo.error "target session must be a number [0-9]" ;;
esac esac
shift 1 shift 1
;; ;;
clear | show | hide | strict | loose ) USER_ARGS+=($1) ;; ( clear | show | hide | strict | loose ) USER_ARGS+=($1) ;;
* ) ERROR "no meta command '$1'" ( * ) echo.error "no meta command '$1'"
esac esac
shift 1 shift 1
done done
@ -81,10 +81,10 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__meta() {
SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta() { SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta() {
case $1 in case $1 in
get ) ( get )
[[ $2 =~ ^all$ ]] && { [[ $2 =~ ^all$ ]] && {
local CONTEXT=$(REDIS get --prefix current:context | grep . || echo "\\033[1;31mnone set\\033[0m") local CONTEXT=$(kube.redis get --prefix current:context | grep . || utils.colors.print bright-red "none set")
local NAMESPACE=$(REDIS get --prefix current:namespace | grep . || echo "\\033[1;31mnone set\\033[0m") local NAMESPACE=$(kube.redis get --prefix current:namespace | grep . || utils.colors.print bright-red "none set")
echo " echo "
environment : $SCWRYPTS_ENV environment : $SCWRYPTS_ENV
context : $CONTEXT context : $CONTEXT
@ -92,51 +92,53 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta() {
CLI settings CLI settings
command context : $(_SCWRYPTS_KUBECTL_SETTINGS get context) command context : $(_SCWRYPTS_KUBECTL_SETTINGS get context)
strict mode : $([[ $STRICT -eq 1 ]] && echo "on" || echo "\\033[1;31moff\\033[0m") strict mode : $([[ $STRICT -eq 1 ]] && utils.colors.print green on || utils.colors.print bright-red off)
" | sed 's/^ \+//' >&2 " | sed 's/^ \+//' >&2
return 0 return 0
} }
REDIS get --prefix current:$2 kube.redis get --prefix current:$2
;; ;;
set ) ( set )
scwrypts -n --name set-$2 --type zsh --group kubectl -- $3 --subsession $SUBSESSION >/dev/null \
&& SUCCESS "$2 set"
;;
copy )
: \ : \
&& STATUS "copying $1 to $2" \ && scwrypts -n --name set-$2 --type zsh --group kube -- $3 --subsession $SUBSESSION >/dev/null \
&& scwrypts -n --name set-context --type zsh --group kubectl -- --subsession $2 $(k meta get context | grep . || echo 'reset') \ && k $SUBSESSION meta get $2 \
&& scwrypts -n --name set-namespace --type zsh --group kubectl -- --subsession $2 $(k meta get namespace | grep . || echo 'reset') \
&& SUCCESS "subsession $1 copied to $2" \
; ;
;; ;;
clear ) ( copy )
scwrypts -n --name set-context --type zsh --group kubectl -- --subsession $SUBSESSION reset >/dev/null \ : \
&& SUCCESS "subsession $SUBSESSION reset to default" && echo.status "copying $1 to $2" \
&& scwrypts -n --name set-context --type zsh --group kube -- --subsession $2 $(k $1 meta get context | grep . || echo 'reset') \
&& scwrypts -n --name set-namespace --type zsh --group kube -- --subsession $2 $(k $1 meta get namespace | grep . || echo 'reset') \
&& echo.success "subsession $1 copied to $2" \
;
;; ;;
show ) ( clear )
scwrypts -n --name set-context --type zsh --group kube -- --subsession $SUBSESSION reset >/dev/null \
&& echo.success "subsession $SUBSESSION reset to default"
;;
( show )
_SCWRYPTS_KUBECTL_SETTINGS set context show >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set context show >/dev/null \
&& SUCCESS "now showing full command context" && echo.success "now showing full command context"
;; ;;
hide ) ( hide )
_SCWRYPTS_KUBECTL_SETTINGS set context hide >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set context hide >/dev/null \
&& SUCCESS "now hiding command context" && echo.success "now hiding command context"
;; ;;
loose ) ( loose )
_SCWRYPTS_KUBECTL_SETTINGS set strict 0 >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set strict 0 >/dev/null \
&& WARNING "now running in 'loose' mode" && echo.warning "now running in 'loose' mode"
;; ;;
strict ) ( strict )
_SCWRYPTS_KUBECTL_SETTINGS set strict 1 >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set strict 1 >/dev/null \
&& SUCCESS "now running in 'strict' mode" && echo.success "now running in 'strict' mode"
;; ;;
esac esac
} }

View File

@ -1,7 +1,7 @@
#!/bin/zsh #!/usr/bin/env zsh
use kubectl --group kubectl use kubectl --group kube
##################################################################### #####################################################################
MAIN() { MAIN() {
KUBECTL__GET_CONTEXT kube.kubectl.context.get
} }

View File

@ -1,7 +1,7 @@
#!/bin/zsh #!/usr/bin/env zsh
use kubectl --group kubectl use kubectl --group kube
##################################################################### #####################################################################
MAIN() { MAIN() {
KUBECTL__GET_NAMESPACE kube.kubectl.namespace.get
} }

View File

@ -0,0 +1,10 @@
export ${scwryptsgroup}__type=zsh
export ${scwryptsgroup}__color=$(utils.colors.red)
#####################################################################
SCWRYPTS_STATIC_CONFIG__kubectl+=(
"${scwryptsgrouproot}/.config/static/redis.zsh"
)
source "${scwryptsgrouproot}/driver/kubectl.driver.zsh"

View File

@ -0,0 +1,18 @@
#####################################################################
DEPENDENCIES+=(kubectl)
use redis --group kube
#####################################################################
kube.cli() {
local NAMESPACE="$(kube.redis get --prefix "current:namespace")"
local CONTEXT="$(kube.kubectl.context.get)"
local ARGS=()
[ "${NAMESPACE}" ] && ARGS+=(--namespace "${NAMESPACE}")
[ "${CONTEXT}" ] && ARGS+=(--context "${CONTEXT}")
kubectl ${ARGS[@]} $@
}

View File

@ -0,0 +1,56 @@
#####################################################################
use --group kube kubectl/cli
use --group kube kubectl/namespace
use --group kube redis
#####################################################################
${scwryptsmodule}.get() { kube.redis get --prefix "current:context"; }
${scwryptsmodule}.set() {
local CONTEXT=$1
[ ! "${CONTEXT}" ] && return 1
[[ "${CONTEXT}" =~ reset ]] && {
: \
&& kube.redis del --prefix "current:context" \
&& kube.kubectl.namespace.set reset \
;
return $?
}
: \
&& kube.redis set --prefix "current:context" "${CONTEXT}" \
&& kube.kubectl.namespace.set reset \
;
}
${scwryptsmodule}.select() {
case "$(kube.kubectl.context.list | grep -v '^reset$' | wc -l)" in
( 0 )
echo.error "no contexts available"
return 1
;;
( 1 )
kube.kubectl.context.list | tail -n1
;;
( * )
kube.kubectl.context.list | utils.fzf 'select a context'
;;
esac
}
${scwryptsmodule}.list() {
echo reset
local ALL_CONTEXTS="$(kube.cli config get-contexts -o name | sort -u)"
echo "${ALL_CONTEXTS}" | grep -v '^arn:aws:eks'
[[ "${AWS_ACCOUNT}" ]] && {
echo "${ALL_CONTEXTS}" | grep "^arn:aws:eks:.*:${AWS_ACCOUNT}"
true
} || {
echo "${ALL_CONTEXTS}" | grep '^arn:aws:eks'
}
}

View File

@ -0,0 +1,17 @@
#
# combines kubectl with redis to both facilitate use of kubectl
# between varying contexts/namespaces AND grant persistence between
# terminal sessions
#
# redis wrapper for kubectl
use --group kube kubectl/cli
# simplify commands for kubecontexts
use --group kube kubectl/context
# simplify commands for namespaces
use --group kube kubectl/namespace
# local redirect commands for remote kubernetes services
use --group kube kubectl/service

View File

@ -0,0 +1,23 @@
${scwryptsmodule}.get() { kube.redis get --prefix "current:namespace"; }
${scwryptsmodule}.set() {
local NAMESPACE=$1
[ ! "${NAMESPACE}" ] && return 1
[[ "${NAMESPACE}" =~ reset ]] && {
kube.redis del --prefix "current:namespace"
return $?
}
kube.redis set --prefix "current:namespace" "${NAMESPACE}"
}
${scwryptsmodule}.select() {
kube.kubectl.namespace.list | utils.fzf 'select a namespace'
}
${scwryptsmodule}.list() {
echo reset
echo default
kube.cli get namespaces -o name | sed 's/^namespace\///' | sort | grep -v '^default$'
}

View File

@ -0,0 +1,77 @@
#####################################################################
use --group kube kubectl/cli
use --group kube kubectl/context
use --group kube kubectl/namespace
#####################################################################
${scwryptsmodule}.serve() {
[ "${CONTEXT}" ] || local CONTEXT="$(kube.kubectl.context.get)"
[ "${CONTEXT}" ] || echo.error 'must configure a context in which to serve'
[ "${NAMESPACE}" ] || local NAMESPACE="$(kube.kubectl.namespace.get)"
[ "${NAMESPACE}" ] || echo.error 'must configure a namespace in which to serve'
utils.check-errors --no-usage || return 1
[ "${SERVICE}" ] && SERVICE="$(kube.kubectl.service.list | jq -c "select (.service == \"${SERVICE}\")" || echo ${SERVICE})"
[ "${SERVICE}" ] || local SERVICE="$(kube.kubectl.service.select)"
[ "${SERVICE}" ] || echo.error 'must provide or select a service'
kube.kubectl.service.list | grep -q "^${SERVICE}$"\
|| echo.error "no service '${SERVICE}' in '${CONFIG}/${NAMESPACE}'"
utils.check-errors --no-usage || return 1
##########################################
SERVICE_PASSWORD="$(kube.kubectl.service.get-password)"
kube.kubectl.service.parse
echo.reminder "attempting to serve ${NAMESPACE}/${SERVICE_NAME}:${SERVICE_PORT}"
[ "${SERVICE_PASSWORD}" ] && echo.reminder "password : ${SERVICE_PASSWORD}"
kube.cli port-forward "service/${SERVICE_NAME}" "${SERVICE_PORT}"
}
#####################################################################
${scwryptsmodule}.select() {
[ "${NAMESPACE}" ] || local NAMESPACE="$(kube.kubectl.namespace.get)"
[ "${NAMESPACE}" ] || return 1
local SERVICES="$(kube.kubectl.service.list)"
local SELECTED="$({
echo "namespace service port"
echo ${SERVICES} \
| jq -r '.service + " " + .port' \
| sed "s/^/${NAMESPACE} /" \
;
} \
| column -t \
| utils.fzf 'select a service' --header-lines=1 \
| awk '{print $2;}' \
)"
echo "${SERVICES}" | jq -c "select (.service == \"${SELECTED}\")"
}
${scwryptsmodule}.list() {
kube.cli get service --no-headers\
| awk '{print "{\"service\":\""$1"\",\"ip\":\""$3"\",\"port\":\""$5"\"}"}' \
| jq -c 'select (.ip != "None")' \
;
}
${scwryptsmodule}.get-password() {
[ "${PASSWORD_SECRET}" ] && [ "${PASSWORD_KEY}" ] || return 0
kube.cli get secret "${PASSWORD_SECRET}" -o jsonpath="{.data.${PASSWORD_KEY}}" \
| base64 --decode
}
${scwryptsmodule}.parse() {
SERVICE_NAME="$(echo "${SERVICE}" | jq -r .service)"
SERVICE_PORT="$(echo "${SERVICE}" | jq -r .port | sed 's|/.*$||')"
}

View File

@ -0,0 +1,7 @@
#!/usr/bin/env zsh
use redis --group kube
#####################################################################
MAIN() {
echo $(kube.redis --get-static-definition)
}

View File

@ -5,16 +5,13 @@ DEPENDENCIES+=(
docker docker
) )
# TODO; allow custom redis configuration REQUIRED_ENV+=()
export SCWRYPTS_KUBECTL_REDIS=managed
REQUIRED_ENV+=( utils.environment.check SCWRYPTS_KUBECTL_REDIS --default managed
SCWRYPTS_KUBECTL_REDIS
)
##################################################################### #####################################################################
REDIS() { kube.redis() {
[ ! $USAGE ] && local USAGE=" [ ! $USAGE ] && local USAGE="
usage: [...options...] usage: [...options...]
@ -24,7 +21,7 @@ REDIS() {
-p, --prefix apply dynamic prefix to the next command line argument -p, --prefix apply dynamic prefix to the next command line argument
--get-prefix output key prefix for current session+subsession --get-prefix output key prefix for current session+subsession
--get-static-definition output the static ZSH function definition for REDIS --get-static-definition output the static ZSH function definition for kube.redis
additional arguments and options are passed through to 'redis-cli' additional arguments and options are passed through to 'redis-cli'
" "
@ -38,15 +35,15 @@ REDIS() {
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
do do
case $1 in case $1 in
-p | --prefix ) USER_ARGS+=("${REDIS_PREFIX}${SCWRYPTS_ENV}:${SUBSESSION}:$2"); shift 1 ;; ( -p | --prefix ) USER_ARGS+=("${REDIS_PREFIX}${SCWRYPTS_ENV}:${SUBSESSION}:$2"); shift 1 ;;
--subsession ) SUBSESSION=$2; shift 1 ;; ( --subsession ) SUBSESSION=$2; shift 1 ;;
--get-prefix ) echo $REDIS_PREFIX; return 0 ;; ( --get-prefix ) echo $REDIS_PREFIX; return 0 ;;
--get-static-definition ) ECHO_STATIC_DEFINITION=1 ;; ( --get-static-definition ) ECHO_STATIC_DEFINITION=1 ;;
* ) USER_ARGS+=($1) ;; ( * ) USER_ARGS+=($1) ;;
esac esac
shift 1 shift 1
done done
@ -62,14 +59,14 @@ REDIS() {
REDIS_ARGS+=(--raw) REDIS_ARGS+=(--raw)
[[ $ECHO_STATIC_DEFINITION -eq 1 ]] && { [[ $ECHO_STATIC_DEFINITION -eq 1 ]] && {
echo "REDIS() {\ echo "kube.redis() {\
local USER_ARGS=(); \ local USER_ARGS=(); \
[ ! \$SUBSESSION ] && local SUBSESSION=0 ;\ [ ! \$SUBSESSION ] && local SUBSESSION=0 ;\
while [[ \$# -gt 0 ]]; \ while [[ \$# -gt 0 ]]; \
do \ do \
case \$1 in case \$1 in
-p | --prefix ) USER_ARGS+=(\"${REDIS_PREFIX}\${SCWRYPTS_ENV}:\${SUBSESSION}:\$2\"); shift 1 ;; \ ( -p | --prefix ) USER_ARGS+=(\"${REDIS_PREFIX}\${SCWRYPTS_ENV}:\${SUBSESSION}:\$2\"); shift 1 ;; \
* ) USER_ARGS+=(\$1) ;; \ ( * ) USER_ARGS+=(\$1) ;; \
esac; \ esac; \
shift 1; \ shift 1; \
done; \ done; \
@ -81,9 +78,9 @@ REDIS() {
redis-cli ${REDIS_ARGS[@]} ${USER_ARGS[@]} redis-cli ${REDIS_ARGS[@]} ${USER_ARGS[@]}
} }
REDIS ping | grep -qi pong || { kube.redis ping 2>/dev/null | grep -qi pong || {
RPID=$(docker ps -a | grep scwrypts-kubectl-redis | awk '{print $1;}') RPID=$(docker ps -a | grep scwrypts-kubectl-redis | awk '{print $1;}')
[ $RPID ] && STATUS 'refreshing redis instance' && docker rm -f $RPID [ $RPID ] && echo.status 'refreshing redis instance' && docker rm -f $RPID
unset RPID unset RPID
docker run \ docker run \
@ -92,6 +89,6 @@ REDIS ping | grep -qi pong || {
--publish $SCWRYPTS_KUBECTL_REDIS_PORT__managed:6379 \ --publish $SCWRYPTS_KUBECTL_REDIS_PORT__managed:6379 \
redis >/dev/null 2>&1 redis >/dev/null 2>&1
STATUS 'awaiting redis connection' echo.status 'awaiting redis connection'
until REDIS ping 2>/dev/null | grep -qi pong; do sleep 0.5; done until kube.redis ping 2>/dev/null | grep -qi pong; do sleep 0.5; done
} }

View File

@ -1,5 +1,5 @@
#!/bin/zsh #!/usr/bin/env zsh
use kubectl --group kubectl use kubectl --group kube
##################################################################### #####################################################################
MAIN() { MAIN() {
@ -12,7 +12,7 @@ MAIN() {
options: options:
--context override context --context override context
--namespace override namespace --namespace override namespace
--subsession REDIS subsession (default 0) --subsession kube.redis subsession (default 0)
to show a required password on screen, use both: to show a required password on screen, use both:
--password-secret Secret resource --password-secret Secret resource
@ -33,17 +33,17 @@ MAIN() {
--password-secret ) PASSWORD_SECRET=$2; shift 1 ;; --password-secret ) PASSWORD_SECRET=$2; shift 1 ;;
--password-key ) PASSWORD_KEY=$2; shift 1 ;; --password-key ) PASSWORD_KEY=$2; shift 1 ;;
-h | --help ) USAGE; return 0 ;; -h | --help ) utils.io.usage; return 0 ;;
* ) * )
[ $SERVICE ] && ERROR "unexpected argument '$2'" [ $SERVICE ] && echo.error "unexpected argument '$2'"
SERVICE=$1 SERVICE=$1
;; ;;
esac esac
shift 1 shift 1
done done
CHECK_ERRORS utils.check-errors --fail
KUBECTL__SERVE kube.kubectl.serve
} }

View File

@ -1,5 +1,5 @@
#!/bin/zsh #!/usr/bin/env zsh
use kubectl --group kubectl use kubectl --group kube
##################################################################### #####################################################################
MAIN() { MAIN() {
@ -10,7 +10,7 @@ MAIN() {
context (optional) the full name of the kubeconfig context to set context (optional) the full name of the kubeconfig context to set
options: options:
--subsession REDIS subsession (default 0) --subsession kube.redis subsession (default 0)
-h, --help show this dialogue and exit -h, --help show this dialogue and exit
" "
@ -22,20 +22,18 @@ MAIN() {
case $1 in case $1 in
--subsession ) SUBSESSION=$2; shift 1 ;; --subsession ) SUBSESSION=$2; shift 1 ;;
-h | --help ) USAGE; return 0 ;;
* ) * )
[ $CONTEXT ] && ERROR "unexpected argument '$2'" [ $CONTEXT ] && echo.error "unexpected argument '$2'"
CONTEXT=$1 CONTEXT=$1
;; ;;
esac esac
shift 1 shift 1
done done
[ $CONTEXT ] || CONTEXT=$(KUBECTL__SELECT_CONTEXT) [ $CONTEXT ] || CONTEXT=$(kube.kubectl.context.select)
[ $CONTEXT ] || ERROR 'must provide or select a valid kube context' [ $CONTEXT ] || echo.error 'must provide or select a valid kube context'
CHECK_ERRORS utils.check-errors --fail
KUBECTL__SET_CONTEXT $CONTEXT kube.kubectl.context.set $CONTEXT
} }

View File

@ -1,5 +1,5 @@
#!/bin/zsh #!/usr/bin/env zsh
use kubectl --group kubectl use kubectl --group kube
##################################################################### #####################################################################
MAIN() { MAIN() {
@ -10,7 +10,7 @@ MAIN() {
namespace (optional) the full name of the namespace context to set namespace (optional) the full name of the namespace context to set
options: options:
--subsession REDIS subsession (default 0) --subsession kube.redis subsession (default 0)
-h, --help show this dialogue and exit -h, --help show this dialogue and exit
" "
@ -22,20 +22,20 @@ MAIN() {
case $1 in case $1 in
--subsession ) SUBSESSION=$2; shift 1 ;; --subsession ) SUBSESSION=$2; shift 1 ;;
-h | --help ) USAGE; return 0 ;; -h | --help ) utils.io.usage; return 0 ;;
* ) * )
[ $NAMESPACE ] && ERROR "unexpected argument '$2'" [ $NAMESPACE ] && echo.error "unexpected argument '$2'"
NAMESPACE=$1 NAMESPACE=$1
;; ;;
esac esac
shift 1 shift 1
done done
[ $NAMESPACE ] || NAMESPACE=$(KUBECTL__SELECT_NAMESPACE) [ $NAMESPACE ] || NAMESPACE=$(kube.kubectl.namespace.select)
[ $NAMESPACE ] || ERROR 'must provide or select a valid namespace' [ $NAMESPACE ] || echo.error 'must provide or select a valid namespace'
CHECK_ERRORS utils.check-errors --fail
KUBECTL__SET_NAMESPACE $NAMESPACE kube.kubectl.namespace.set $NAMESPACE
} }

View File

@ -1,11 +0,0 @@
SCWRYPTS_GROUPS+=(kubectl)
export SCWRYPTS_TYPE__kubectl=zsh
export SCWRYPTS_ROOT__kubectl="$SCWRYPTS_ROOT__scwrypts/plugins/kubectl"
export SCWRYPTS_COLOR__kubectl='\033[0;31m'
SCWRYPTS_STATIC_CONFIG__kubectl+=(
"$SCWRYPTS_ROOT__kubectl/.config/static/redis.zsh"
)
source "$SCWRYPTS_ROOT__kubectl/driver/kubectl.driver.zsh"

View File

@ -1,158 +0,0 @@
#####################################################################
DEPENDENCIES+=(
kubectl
)
REQUIRED_ENV+=()
use redis --group kubectl
#####################################################################
KUBECTL() {
local NAMESPACE=$(REDIS get --prefix "current:namespace")
local CONTEXT=$(KUBECTL__GET_CONTEXT)
local KUBECTL_ARGS=()
[ $NAMESPACE ] && KUBECTL_ARGS+=(--namespace $NAMESPACE)
[ $CONTEXT ] && KUBECTL_ARGS+=(--context $CONTEXT)
kubectl ${KUBECTL_ARGS[@]} $@
}
#####################################################################
KUBECTL__GET_CONTEXT() { REDIS get --prefix "current:context"; }
KUBECTL__SET_CONTEXT() {
local CONTEXT=$1
[ ! $CONTEXT ] && return 1
[[ $CONTEXT =~ reset ]] && {
: \
&& REDIS del --prefix "current:context" \
&& KUBECTL__SET_NAMESPACE reset \
;
return $?
}
: \
&& REDIS set --prefix "current:context" "$CONTEXT" \
&& KUBECTL__SET_NAMESPACE reset \
;
}
KUBECTL__SELECT_CONTEXT() {
KUBECTL__LIST_CONTEXTS | FZF 'select a context'
}
KUBECTL__LIST_CONTEXTS() {
echo reset
local ALL_CONTEXTS=$(KUBECTL config get-contexts -o name | sort)
echo $ALL_CONTEXTS | grep -v '^arn:aws:eks'
[[ $AWS_ACCOUNT ]] && {
echo $ALL_CONTEXTS | grep "^arn:aws:eks:.*:$AWS_ACCOUNT"
true
} || {
echo $ALL_CONTEXTS | grep '^arn:aws:eks'
}
}
#####################################################################
KUBECTL__GET_NAMESPACE() { REDIS get --prefix "current:namespace"; }
KUBECTL__SET_NAMESPACE() {
local NAMESPACE=$1
[ ! $NAMESPACE ] && return 1
[[ $NAMESPACE =~ reset ]] && {
REDIS del --prefix "current:namespace"
return $?
}
REDIS set --prefix "current:namespace" "$NAMESPACE"
}
KUBECTL__SELECT_NAMESPACE() {
KUBECTL__LIST_NAMESPACES | FZF 'select a namespace'
}
KUBECTL__LIST_NAMESPACES() {
echo reset
echo default
KUBECTL get namespaces -o name | sed 's/^namespace\///' | sort
}
#####################################################################
KUBECTL__SERVE() {
[ $CONTEXT ] || local CONTEXT=$(KUBECTL__GET_CONTEXT)
[ $CONTEXT ] || ERROR 'must configure a context in which to serve'
[ $NAMESPACE ] || local NAMESPACE=$(KUBECTL__GET_NAMESPACE)
[ $NAMESPACE ] || ERROR 'must configure a namespace in which to serve'
CHECK_ERRORS --no-fail --no-usage || return 1
[ $SERVICE ] && SERVICE=$(KUBECTL__LIST_SERVICES | jq -c "select (.service == \"$SERVICE\")" || echo $SERVICE)
[ $SERVICE ] || local SERVICE=$(KUBECTL__SELECT_SERVICE)
[ $SERVICE ] || ERROR 'must provide or select a service'
KUBECTL__LIST_SERVICES | grep -q "^$SERVICE$"\
|| ERROR "no service '$SERVICE' in '$CONFIG/$NAMESPACE'"
CHECK_ERRORS --no-fail --no-usage || return 1
##########################################
SERVICE_PASSWORD="$(KUBECTL__GET_SERVICE_PASSWORD)"
KUBECTL__SERVICE_PARSE
REMINDER "attempting to serve ${NAMESPACE}/${SERVICE_NAME}:${SERVICE_PORT}"
[ $SERVICE_PASSWORD ] && REMINDER "password : $SERVICE_PASSWORD"
KUBECTL port-forward service/$SERVICE_NAME $SERVICE_PORT
}
KUBECTL__SELECT_SERVICE() {
[ $NAMESPACE ] || local NAMESPACE=$(KUBECTL__GET_NAMESPACE)
[ $NAMESPACE ] || return 1
local SERVICES=$(KUBECTL__LIST_SERVICES)
local SELECTED=$({
echo "namespace service port"
echo $SERVICES \
| jq -r '.service + " " + .port' \
| sed "s/^/$NAMESPACE /" \
;
} \
| column -t \
| FZF 'select a service' --header-lines=1 \
| awk '{print $2;}' \
)
echo $SERVICES | jq -c "select (.service == \"$SELECTED\")"
}
KUBECTL__LIST_SERVICES() {
KUBECTL get service --no-headers\
| awk '{print "{\"service\":\""$1"\",\"ip\":\""$3"\",\"port\":\""$5"\"}"}' \
| jq -c 'select (.ip != "None")' \
;
}
KUBECTL__GET_SERVICE_PASSWORD() {
[ $PASSWORD_SECRET ] && [ $PASSWORD_KEY ] || return 0
KUBECTL get secret $PASSWORD_SECRET -o jsonpath="{.data.$PASSWORD_KEY}" \
| base64 --decode
}
KUBECTL__SERVICE_PARSE() {
SERVICE_NAME=$(echo $SERVICE | jq -r .service)
SERVICE_PORT=$(echo $SERVICE | jq -r .port | sed 's|/.*$||')
}

View File

@ -1,7 +0,0 @@
#!/bin/zsh
use redis --group kubectl
#####################################################################
MAIN() {
echo $(REDIS --get-static-definition)
}

View File

@ -1,10 +1,27 @@
from os import getenv as os_getenv from json import loads
from .scwrypts import scwrypts
from .scwrypts.exceptions import MissingVariableError from .scwrypts.exceptions import MissingVariableError
ENV = {}
def getenv(name, required=True): def getenv(name, required=True, default=None):
value = os_getenv(f'{name}__override', os_getenv(name)) if ENV.get('configuration') is None or ENV.get('environment') is None:
full_environment = loads(
scwrypts(
name = 'scwrypts/environment/getenv',
group = 'scwrypts',
_type = 'zsh',
executable_args = '-n',
args = '--all',
).stdout
)
ENV['configuration'] = full_environment['configuration']
ENV['environment'] = full_environment['environment']
value = ENV.get('environment', {}).get(name, default)
if required and not value: if required and not value:
raise MissingVariableError(name) raise MissingVariableError(name)

View File

@ -5,7 +5,7 @@ from pytest import fixture
from .client import request from .client import request
def test_discord_request(sample, _response): def test_discord_request(sample, _mock_getenv, _response):
assert _response == sample.response assert _response == sample.response
def test_discord_request_client_setup(sample, mock_get_request_client, _mock_getenv, _response): def test_discord_request_client_setup(sample, mock_get_request_client, _mock_getenv, _response):
@ -41,7 +41,7 @@ def fixture_mock_get_request_client(sample):
@fixture(name='_mock_getenv') @fixture(name='_mock_getenv')
def fixture_mock_getenv(sample): def fixture_mock_getenv(sample):
with patch('scwrypts.http.discord.client.getenv',) as mock: with patch('scwrypts.http.discord.client.getenv') as mock:
mock.side_effect = lambda name, **kwargs: { mock.side_effect = lambda name, **kwargs: {
'DISCORD__BOT_TOKEN': sample.bot_token, 'DISCORD__BOT_TOKEN': sample.bot_token,
}[name] }[name]
@ -49,6 +49,6 @@ def fixture_mock_getenv(sample):
@fixture(name='_mock_getenv_optional') @fixture(name='_mock_getenv_optional')
def fixture_mock_getenv_optional(): def fixture_mock_getenv_optional():
with patch('scwrypts.http.discord.client.getenv',) as mock: with patch('scwrypts.http.discord.client.getenv') as mock:
mock.side_effect = lambda name, **kwargs: None mock.side_effect = lambda name, **kwargs: None
yield mock yield mock

View File

@ -4,9 +4,11 @@ from json import dumps, loads
from random import randint, uniform, choice from random import randint, uniform, choice
from re import sub from re import sub
from string import printable from string import printable
from typing import Hashable, Callable from typing import Callable
from uuid import uuid4 from uuid import uuid4
from collections.abc import Hashable
from requests import Response, status_codes from requests import Response, status_codes
from yaml import safe_dump from yaml import safe_dump

401
scwrypts
View File

@ -1,12 +1,16 @@
#!/bin/zsh #!/usr/bin/env zsh
export EXECUTION_DIR=$(pwd) export EXECUTION_DIR=$(pwd)
export SCWRYPTS_RUNTIME_ID=$(uuidgen) export SCWRYPTS_RUNTIME_ID=$(uuidgen)
source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42 source "$(dirname -- $(readlink -f -- "$0"))/zsh/import.driver.zsh" || return 42
use scwrypts/environment
use scwrypts/list-available
use scwrypts/get-runstring
##################################################################### #####################################################################
() { () {
cd "$SCWRYPTS_ROOT__scwrypts" cd "$(scwrypts.config.group scwrypts root)"
GIT_SCWRYPTS() { git -C "$SCWRYPTS_ROOT__scwrypts" $@; } GIT_SCWRYPTS() { git -C "$(scwrypts.config.group scwrypts root)" $@; }
local ERRORS=0 local ERRORS=0
local USAGE=' local USAGE='
@ -39,7 +43,7 @@ source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42
--list-envs print out environment list and exit --list-envs print out environment list and exit
--list-groups print out configured scwrypts groups and exit --list-groups print out configured scwrypts groups and exit
--config "eval"-ed to enable config and "use" import in non-scwrypts environments --config "eval"-ed to enable config and "use" import in non-scwrypts environments
--root print out SCWRYPTS_ROOT__scwrypts and exit --root print out scwrypts.config.group.scwrypts.root and exit
--update update scwrypts library to latest version --update update scwrypts library to latest version
--version print out scwrypts version and exit --version print out scwrypts version and exit
@ -55,76 +59,76 @@ source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42
### cli argument parsing and global configuration ################### ### cli argument parsing and global configuration ###################
##################################################################### #####################################################################
local ENV_NAME="$SCWRYPTS_ENV" local ENV_NAME="${SCWRYPTS_ENV}"
local SEARCH_PATTERNS=() local SEARCH_PATTERNS=()
local VARSPLIT SEARCH_GROUP SEARCH_TYPE SEARCH_NAME local VARSPLIT SEARCH_GROUP SEARCH_TYPE SEARCH_NAME
[ ! $SCWRYPTS_LOG_LEVEL ] && local SCWRYPTS_LOG_LEVEL=3 [ ! ${SCWRYPTS_LOG_LEVEL} ] && local SCWRYPTS_LOG_LEVEL=3
local SHIFT_COUNT local SHIFT_COUNT
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
do do
SHIFT_COUNT=1 SHIFT_COUNT=1
case $1 in case $1 in
-[a-z][a-z]* ) ( -[a-z][a-z]* )
VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/') VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/')
set -- throw-away $(echo " $VARSPLIT ") ${@:2} set -- throw-away $(echo " ${VARSPLIT} ") ${@:2}
;; ;;
### alternate commands ################### ### alternate commands ###################
-h | --help ) ( -h | --help )
USAGE utils.io.usage
return 0 return 0
;; ;;
-l | --list ) ( -l | --list )
SCWRYPTS__GET_AVAILABLE_SCWRYPTS scwrypts.list-available
return 0 return 0
;; ;;
--list-envs ) ( --list-envs )
SCWRYPTS__GET_ENV_NAMES scwrypts.environment.common.get-env-names
return 0 return 0
;; ;;
--list-groups ) ( --list-groups )
echo "${SCWRYPTS_GROUPS[@]}" | sed 's/\s\+/\n/g' | sort -u echo "${SCWRYPTS_GROUPS[@]}" | sed 's/\s\+/\n/g' | sort -u
return 0 return 0
;; ;;
--version ) ( --version )
case $SCWRYPTS_INSTALLATION_TYPE in case ${SCWRYPTS_INSTALLATION_TYPE} in
manual ) echo "scwrypts $(GIT_SCWRYPTS describe --tags) (via GIT)" ;; ( manual ) echo "scwrypts $(GIT_SCWRYPTS describe --tags) (via GIT)" ;;
* ) echo "scwrypts $(cat "$SCWRYPTS_ROOT__scwrypts/VERSION")" ;; ( * ) echo "scwrypts $(cat "$(scwrypts.config.group scwrypts root)/VERSION")" ;;
esac esac
return 0 return 0
;; ;;
--root ) ( --root )
echo "$SCWRYPTS_ROOT__scwrypts" scwrypts.config.group scwrypts root
return 0 return 0
;; ;;
--config ) ( --config )
echo "source '$SCWRYPTS_ROOT__scwrypts/zsh/lib/import.driver.zsh'" echo "source '$(scwrypts.config.group scwrypts root)/zsh/import.driver.zsh'"
echo "CHECK_ENVIRONMENT --no-fail --no-usage" echo "utils.check-environment --no-fail --no-usage"
echo "unset __SCWRYPT" echo "unset __SCWRYPT"
return 0 return 0
;; ;;
--update ) ( --update )
case $SCWRYPTS_INSTALLATION_TYPE in case ${SCWRYPTS_INSTALLATION_TYPE} in
aur ) aur )
SCWRYPTS_LOG_LEVEL=3 REMINDER " echo.reminder --force-print "
This installation is built from the AUR. Update through 'makepkg' or use This installation is built from the AUR. Update through 'makepkg' or use
your preferred AUR package management tool (e.g. 'yay -Syu scwrypts') your preferred AUR package management tool (e.g. 'yay -Syu scwrypts')
" "
;; ;;
homebrew ) homebrew )
SCWRYPTS_LOG_LEVEL=3 REMINDER "This installation is managed by homebrew. Update me with 'brew update'" echo.reminder --force-print "This installation is managed by homebrew. Update me with 'brew update'"
;; ;;
manual ) manual )
@ -135,22 +139,22 @@ source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42
GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1
local DIFF_STATUS=$? local DIFF_STATUS=$?
[[ $SYNC_STATUS -eq 0 ]] && [[ $DIFF_STATUS -eq 0 ]] && { [[ ${SYNC_STATUS} -eq 0 ]] && [[ ${DIFF_STATUS} -eq 0 ]] && {
SUCCESS 'already up-to-date with origin/main' echo.success 'already up-to-date with origin/main'
} || { } || {
GIT_SCWRYPTS rebase --autostash origin/main \ GIT_SCWRYPTS rebase --autostash origin/main \
&& SUCCESS 'up-to-date with origin/main' \ && echo.success 'up-to-date with origin/main' \
&& GIT_SCWRYPTS log -n1 \ && GIT_SCWRYPTS log -n1 \
|| { || {
GIT_SCWRYPTS rebase --abort GIT_SCWRYPTS rebase --abort
ERROR 'unable to update scwrypts; please try manual upgrade' echo.error 'unable to update scwrypts; please try manual upgrade'
REMINDER "installation in '$SCWRYPTS_ROOT__scwrypts'" echo.reminder "installation in '$(scwrypts.config.group scwrypts root)'"
} }
} }
;; ;;
* ) * )
SCWRYPTS_LOG_LEVEL=3 REMINDER " echo.reminder --force-print "
This is a managed installation of scwrypts. Please update through your This is a managed installation of scwrypts. Please update through your
system package manager. system package manager.
" "
@ -161,228 +165,236 @@ source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42
### scwrypts filters ##################### ### scwrypts filters #####################
-m | --name ) ( -m | --name )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
[ $2 ] || { ERROR "missing value for argument $1"; break; } [ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_NAME=$2 SEARCH_NAME=$2
;; ;;
-g | --group ) ( -g | --group )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
[ $2 ] || { ERROR "missing value for argument $1"; break; } [ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_GROUP=$2 SEARCH_GROUP=$2
GROUP=$2 GROUP=$2
;; ;;
-t | --type ) ( -t | --type )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
[ $2 ] || { ERROR "missing value for argument $1"; break; } [ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_TYPE=$2 SEARCH_TYPE=$2
TYPE=$2 TYPE=$2
;; ;;
### runtime settings ##################### ### runtime settings #####################
-y | --yes ) export __SCWRYPTS_YES=1 ;; ( -y | --yes ) export __SCWRYPTS_YES=1 ;;
-n ) SCWRYPTS_LOG_LEVEL=0 ;; ( -n ) SCWRYPTS_LOG_LEVEL=0 ;;
-v | --log-level ) ( -v | --log-level )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
[[ $2 =~ ^[0-4]$ ]] || ERROR "invalid setting for log-level '$2'" [[ $2 =~ ^[0-4]$ ]] || echo.error "invalid setting for log-level '$2'"
SCWRYPTS_LOG_LEVEL=$2 SCWRYPTS_LOG_LEVEL=$2
;; ;;
-o | --output ) ( -o | --output )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
export SCWRYPTS_OUTPUT_FORMAT=$2 export SCWRYPTS_OUTPUT_FORMAT=$2
case $SCWRYPTS_OUTPUT_FORMAT in case ${SCWRYPTS_OUTPUT_FORMAT} in
pretty | json ) ;; ( pretty | json ) ;;
* ) ERROR "unsupported format '$SCWRYPTS_OUTPUT_FORMAT'" ;; * ) echo.error "unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" ;;
esac esac
;; ;;
-e | --env ) ( -e | --env )
((SHIFT_COUNT+=1)) ((SHIFT_COUNT+=1))
[ $2 ] || { ERROR "missing value for argument $1"; break; } [ $2 ] || { echo.error "missing value for argument $1"; break; }
[ $ENV_NAME ] && DEBUG 'overwriting session environment' [ ${ENV_NAME} ] && echo.debug 'overwriting session environment'
ENV_NAME="$2" ENV_NAME="$2"
STATUS "using CLI environment '$ENV_NAME'" echo.status "using CLI environment '${ENV_NAME}'"
;; ;;
########################################## ##########################################
-- ) shift 1; break ;; # pass arguments after '--' to the scwrypt ( -- ) shift 1; break ;; # pass arguments after '--' to the scwrypt
--* ) ERROR "unrecognized argument '$1'" ;; ( --* ) echo.error "unrecognized argument '$1'" ;;
* ) SEARCH_PATTERNS+=($1) ;; ( * ) SEARCH_PATTERNS+=($1) ;;
esac esac
shift $SHIFT_COUNT [[ ${SHIFT_COUNT} -le $# ]] \
&& shift ${SHIFT_COUNT} \
|| echo.error "missing argument for '$1'" \
|| shift $# \
;
done done
[ $SCWRYPTS_OUTPUT_FORMAT ] || export SCWRYPTS_OUTPUT_FORMAT=pretty [ ${SCWRYPTS_OUTPUT_FORMAT} ] || export SCWRYPTS_OUTPUT_FORMAT=pretty
[ $SEARCH_NAME ] && { [ ${SEARCH_NAME} ] && {
[ $SEARCH_TYPE ] || ERROR '--name requires --type argument' [ ${SEARCH_TYPE} ] || echo.error '--name requires --type argument'
[ $SEARCH_GROUP ] || ERROR '--name requires --group argument' [ ${SEARCH_GROUP} ] || echo.error '--name requires --group argument'
} }
CHECK_ERRORS utils.check-errors --fail
##################################################################### #####################################################################
### scwrypts selection / filtering ################################## ### scwrypts selection / filtering ##################################
##################################################################### #####################################################################
local SCWRYPTS_AVAILABLE local SCWRYPTS_AVAILABLE=$(scwrypts.list-available)
SCWRYPTS_AVAILABLE=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS)
########################################## ##########################################
[ $SEARCH_NAME ] && SCWRYPTS_AVAILABLE=$({ [ ${SEARCH_NAME} ] && SCWRYPTS_AVAILABLE=$({
echo $SCWRYPTS_AVAILABLE | head -n1 echo ${SCWRYPTS_AVAILABLE} | head -n1
echo $SCWRYPTS_AVAILABLE | sed -e 's/\x1b\[[0-9;]*m//g' | grep "^$SEARCH_NAME *$SEARCH_TYPE *$SEARCH_GROUP\$" echo ${SCWRYPTS_AVAILABLE} | utils.colors.remove | grep "^${SEARCH_NAME} *${SEARCH_TYPE} *${SEARCH_GROUP}\$"
}) || { }) || {
[ $SEARCH_TYPE ] && { [ ${SEARCH_TYPE} ] && {
SCWRYPTS_AVAILABLE=$(\ SCWRYPTS_AVAILABLE=$(\
{ {
echo $SCWRYPTS_AVAILABLE | head -n1 echo ${SCWRYPTS_AVAILABLE} | head -n1
echo $SCWRYPTS_AVAILABLE | grep ' [^/]*'$SEARCH_TYPE'[^/]* ' echo ${SCWRYPTS_AVAILABLE} | grep ' [^/]*'${SEARCH_TYPE}'[^/]* '
} \ } \
| sed 's/ \+$/'$(printf $__COLOR_RESET)'/; s/ \+/^/g' \ | sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \
| column -ts '^' | column -ts '^'
) )
} }
[ $SEARCH_GROUP ] && { [ ${SEARCH_GROUP} ] && {
SCWRYPTS_AVAILABLE=$( SCWRYPTS_AVAILABLE=$(
{ {
echo $SCWRYPTS_AVAILABLE | head -n1 echo ${SCWRYPTS_AVAILABLE} | head -n1
echo $SCWRYPTS_AVAILABLE | grep "$SEARCH_GROUP"'[^/ ]*$' echo ${SCWRYPTS_AVAILABLE} | grep "${SEARCH_GROUP}"'[^/ ]*$'
} \ } \
| sed 's/ \+$/'$(printf $__COLOR_RESET)'/; s/ \+/^/g' \ | sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \
| column -ts '^' | column -ts '^'
) )
} }
[[ ${#SEARCH_PATTERNS[@]} -gt 0 ]] && { [[ ${#SEARCH_PATTERNS[@]} -gt 0 ]] && {
POTENTIAL_ERROR+="\n PATTERNS : $SEARCH_PATTERNS" POTENTIAL_ERROR+="\n PATTERNS : ${SEARCH_PATTERNS}"
local P local P
for P in ${SEARCH_PATTERNS[@]} for P in ${SEARCH_PATTERNS[@]}
do do
SCWRYPTS_AVAILABLE=$( SCWRYPTS_AVAILABLE=$(
{ {
echo $SCWRYPTS_AVAILABLE | head -n1 echo ${SCWRYPTS_AVAILABLE} | head -n1
echo $SCWRYPTS_AVAILABLE | grep $P echo ${SCWRYPTS_AVAILABLE} | grep ${P}
} }
) )
done done
} }
} }
[[ $(echo $SCWRYPTS_AVAILABLE | wc -l) -lt 2 ]] && { [[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -lt 2 ]] && {
FAIL 1 "$(echo " utils.fail 1 "$(echo "
no such scwrypt exists no such scwrypt exists
NAME : '$SEARCH_NAME' NAME : '${SEARCH_NAME}'
TYPE : '$SEARCH_TYPE' TYPE : '${SEARCH_TYPE}'
GROUP : '$SEARCH_GROUP' GROUP : '${SEARCH_GROUP}'
PATTERNS : '$SEARCH_PATTERNS' PATTERNS : '${SEARCH_PATTERNS}'
" | sed "1d; \$d; /''$/d")" " | sed "1d; \$d; /''$/d")"
} }
########################################## ##########################################
[[ $(echo $SCWRYPTS_AVAILABLE | wc -l) -eq 2 ]] \ [[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -eq 2 ]] \
&& SCWRYPT_SELECTION=$(echo $SCWRYPTS_AVAILABLE | tail -n1) \ && SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | tail -n1) \
|| SCWRYPT_SELECTION=$(echo $SCWRYPTS_AVAILABLE | FZF "select a script to run" --header-lines 1) \ || SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | utils.fzf "select a script to run" --header-lines 1) \
; ;
[ $SCWRYPT_SELECTION ] || exit 2 [ ${SCWRYPT_SELECTION} ] || utils.abort
########################################## ##########################################
local NAME TYPE GROUP () {
SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION set -- $(echo $@ | utils.colors.remove)
export SCWRYPT_NAME=$1
export SCWRYPT_TYPE=$2
export SCWRYPT_GROUP=$3
} ${SCWRYPT_SELECTION}
export SCWRYPT_NAME=$NAME
export SCWRYPT_TYPE=$TYPE
export SCWRYPT_GROUP=$GROUP
##################################################################### #####################################################################
### environment variables and configuration validation ############## ### environment variables and configuration validation ##############
##################################################################### #####################################################################
local ENV_REQUIRED=true \ local ENV_REQUIRED=true \
&& [ ! $CI ] \ && [ ! ${CI} ] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/logs ]] \ && [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/environment ]] \ && [[ ! ${SCWRYPT_NAME} =~ scwrypts/environment ]] \
|| ENV_REQUIRED=false || ENV_REQUIRED=false
local REQUIRED_ENVIRONMENT_REGEX=$(eval echo '$SCWRYPTS_REQUIRED_ENVIRONMENT_REGEX__'$SCWRYPT_GROUP) local REQUIRED_ENVIRONMENT_REGEX="$(scwrypts.config.group ${SCWRYPT_GROUP} required_environment_regex)"
[ $ENV_NAME ] && [ $REQUIRED_ENVIRONMENT_REGEX ] && { [ ${ENV_NAME} ] && [ ${REQUIRED_ENVIRONMENT_REGEX} ] && {
[[ $ENV_NAME =~ $REQUIRED_ENVIRONMENT_REGEX ]] \ [[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \
|| FAIL 5 "group '$SCWRYPT_GROUP' requires current environment name to match '$REQUIRED_ENVIRONMENT_REGEX' (currently $ENV_NAME)" || utils.fail 5 "group '${SCWRYPT_GROUP}' requires current environment name to match '${REQUIRED_ENVIRONMENT_REGEX}' (currently ${ENV_NAME})"
} }
[[ $ENV_REQUIRED =~ true ]] && { [[ ${ENV_REQUIRED} =~ true ]] && {
[ ! $ENV_NAME ] && ENV_NAME=$(SCWRYPTS__SELECT_ENV) [ ! ${ENV_NAME} ] && {
[ ! $ENV_NAME ] && ABORT scwrypts.environment.init \
|| echo.error "failed to initialize scwrypts environments (see above)" \
|| return 1 \
;
export ENV_NAME ENV_NAME=$(scwrypts.environment.select-env)
export SCWRYPTS_ENV=$ENV_NAME [ "${ENV_NAME}" ] || user.abort
}
for GROUP in ${SCWRYPTS_GROUPS[@]} for GROUP in ${SCWRYPTS_GROUPS[@]}
do do
local REQUIRED_REGEX=$(eval echo '$SCWRYPTS_REQUIRED_ENVIRONMENT_REGEX__'$GROUP) local REQUIRED_REGEX="$(scwrypts.config.group ${GROUP} required_environment_regex)"
[ $REQUIRED_REGEX ] && { [ ${REQUIRED_REGEX} ] && {
[[ $ENV_NAME =~ $REQUIRED_REGEX ]] || continue [[ ${ENV_NAME} =~ ${REQUIRED_REGEX} ]] || continue
} }
local ENV_FILE=$(SCWRYPTS__GET_ENV_FILE "$ENV_NAME" "$GROUP")
source "$ENV_FILE" || FAIL 5 "missing or invalid environment '$GROUP/$ENV_NAME'"
for f in $(eval 'echo $SCWRYPTS_STATIC_CONFIG__'$GROUP) for f in $(find "$(scwrypts.config.group ${GROUP} root)/.config/static" -type f 2>/dev/null)
do do
source "$f" || FAIL 5 "invalid static config '$f'" source "${f}" || utils.fail 5 "invalid static config '${f}'"
done done
done done
} }
[ $REQUIRED_ENVIRONMENT_REGEX ] && { [ ${REQUIRED_ENVIRONMENT_REGEX} ] && {
[[ $ENV_NAME =~ $REQUIRED_ENVIRONMENT_REGEX ]] \ [[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \
|| FAIL 5 "group '$SCWRYPT_GROUP' requires current environment name to match '$REQUIRED_ENVIRONMENT_REGEX' (currently $ENV_NAME)" || utils.fail 5 "group '${SCWRYPT_GROUP}' requires current environment name to match '${REQUIRED_ENVIRONMENT_REGEX}' (currently ${ENV_NAME})"
} }
export SCWRYPTS_ENV=${ENV_NAME}
########################################## ##########################################
[ ! $SUBSCWRYPT ] && export SUBSCWRYPT=0 [ ! ${SUBSCWRYPT} ] && export SUBSCWRYPT=0
[[ $SCWRYPTS_INSTALLATION_TYPE =~ ^manual$ ]] && { [[ ${SCWRYPTS_INSTALLATION_TYPE} =~ ^manual$ ]] && {
[[ $SUBSCWRYPT -eq 0 ]] && [[ $ENV_NAME =~ prod ]] && [[ $SCWRYPTS_LOG_LEVEL -gt 0 ]] && { [[ ${SUBSCWRYPT} -eq 0 ]] && [[ ${SCWRYPTS_ENV} =~ prod ]] && [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] && {
STATUS "on '$ENV_NAME'; checking diff against origin/main" echo.status "on '${SCWRYPTS_ENV}'; checking diff against origin/main"
local WARNING_MESSAGE local WARNING_MESSAGE
[ ! $WARNING_MESSAGE ] && { [ ! ${WARNING_MESSAGE} ] && {
GIT_SCWRYPTS fetch --quiet origin main \ GIT_SCWRYPTS fetch --quiet origin main \
|| WARNING_MESSAGE='I am unable to verify your scwrypts version' || WARNING_MESSAGE='I am unable to verify your scwrypts version'
} }
[ ! $WARNING_MESSAGE ] && { [ ! ${WARNING_MESSAGE} ] && {
GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 \ GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 \
|| WARNING_MESSAGE='your scwrypts is currently out-of-date' || WARNING_MESSAGE='your scwrypts is currently out-of-date'
} }
[ $WARNING_MESSAGE ] && { [ ${WARNING_MESSAGE} ] && {
[[ $SCWRYPTS_LOG_LEVEL -lt 3 ]] && { [[ ${SCWRYPTS_LOG_LEVEL} -lt 3 ]] && {
REMINDER "you are running in ${__BRIGHT_RED}production${__BRIGHT_MAGENTA} and $WARNING_MESSAGE" echo.reminder "you are running in $(utils.colors.bright-red)production$(utils.colors.bright-magenta) and ${WARNING_MESSAGE}"
} || { } || {
GIT_SCWRYPTS diff --exit-code origin/main -- . >&2 GIT_SCWRYPTS diff --exit-code origin/main -- . >&2
WARNING "you are trying to run in ${__BRIGHT_RED}production${__YELLOW} but $WARNING_MESSAGE (relevant diffs and errors above)" echo.warning "you are trying to run in $(utils.colors.bright-red)production$(echo.warning.color) but ${WARNING_MESSAGE} (relevant diffs and errors above)"
yN 'continue?' || { yN 'continue?' || {
REMINDER "you can use 'scwrypts --update' to quickly update scwrypts to latest" echo.reminder "you can use 'scwrypts --update' to quickly update scwrypts to latest"
ABORT user.abort
} }
} }
} }
@ -391,92 +403,123 @@ source "$(dirname $(readlink -f "$0"))/zsh/lib/import.driver.zsh" || exit 42
########################################## ##########################################
local RUN_STRING=$(SCWRYPTS__GET_RUNSTRING $SCWRYPT_NAME $SCWRYPT_TYPE $SCWRYPT_GROUP) local RUN_STRING=$(scwrypts.get-runstring ${SCWRYPT_NAME} ${SCWRYPT_TYPE} ${SCWRYPT_GROUP})
[ "$RUN_STRING" ] || return 42 [ "${RUN_STRING}" ] || return 42
##################################################################### #####################################################################
### logging and pretty header/footer setup ########################## ### logging and pretty header/footer setup ##########################
##################################################################### #####################################################################
local RUN_MODE=normal
[[ ${SCWRYPT_NAME} =~ interactive ]] && RUN_MODE=interactive
local LOGFILE \ local LOGFILE \
&& [[ $SCWRYPTS_LOG_LEVEL -gt 0 ]] \ && [[ ${RUN_MODE} =~ normal ]] \
&& [[ $SUBSCWRYPT -eq 0 ]] \ && [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/logs ]] \ && [[ ${SUBSCWRYPT} -eq 0 ]] \
&& [[ ! $SCWRYPT_NAME =~ interactive ]] \ && [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \
&& LOGFILE="$SCWRYPTS_LOG_PATH/$(echo $GROUP/$TYPE/$NAME | sed 's/^\.\///; s/\//\%/g').log" \ && LOGFILE="${SCWRYPTS_LOG_PATH}/$(echo ${GROUP}/${TYPE}/${NAME} | sed 's/^\.\///; s/\//\%/g').log" \
|| LOGFILE='/dev/null' \ || LOGFILE='/dev/null' \
; ;
local RUN_MODE=normal
[[ $LOGFILE =~ ^/dev/null$ ]] && RUN_MODE=no-logfile
[[ $SCWRYPT_NAME =~ interactive ]] && RUN_MODE=interactive
local HEADER FOOTER local HEADER FOOTER
[[ $SCWRYPTS_LOG_LEVEL -ge 2 ]] && { [[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && {
case $SCWRYPTS_OUTPUT_FORMAT in case ${SCWRYPTS_OUTPUT_FORMAT} in
pretty ) ( raw )
HEADER="--- start scwrypt ${SCWRYPT_GROUP}/${SCWRYPT_TYPE} ${SCWRYPT_NAME} in ${SCWRYPTS_ENV} ---"
FOOTER="--- end scwrypt ---"
;;
( pretty )
HEADER=$( HEADER=$(
echo " echo "
===================================================================== =====================================================================
scwrypt : $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME scwrypt : ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME}
run at : $(date) run at : $(date)
config : $ENV_NAME config : ${SCWRYPTS_ENV}
log level : $SCWRYPTS_LOG_LEVEL log level : ${SCWRYPTS_LOG_LEVEL}
\\033[1;33m--- SCWRYPT BEGIN ---------------------------------------------------\\033[0m $(utils.colors.print bright-yellow '--- SCWRYPT BEGIN ---------------------------------------------------')
" | sed 's/^\s\+//; 1d' " | sed 's/^\s\+//; 1d'
) )
FOOTER="\\033[1;33m--- SCWRYPT END ---------------------------------------------------\\033[0m" FOOTER="$(utils.colors.print bright-yellow '--- SCWRYPT END ---------------------------------------------------')"
;; ;;
json ) ( json )
HEADER=$(echo '{}' | jq -c ". HEADER=$(echo '{}' | jq -c ".
| .timestamp = \"$(date +%s)\" | .timestamp = \"$(date +%s)\"
| .runtime = \"$SCWRYPTS_RUNTIME_ID\" | .runtime = \"${SCWRYPTS_RUNTIME_ID}\"
| .scwrypt = \"start of $SCWRYPT_NAME $SCWRYPT_GROUP $SCWRYPT_TYPE\" | .scwrypt = \"start of ${SCWRYPT_NAME} ${SCWRYPT_GROUP} ${SCWRYPT_TYPE}\"
| .config = \"$ENV_NAME\" | .config = \"${SCWRYPTS_ENV}\"
| .logLevel = \"$SCWRYPTS_LOG_LEVEL\" | .logLevel = \"${SCWRYPTS_LOG_LEVEL}\"
| .subscwrypt = $SUBSCWRYPT | .subscwrypt = ${SUBSCWRYPT}
") ")
;; ;;
esac esac
} }
[[ $SUBSCWRYPT -eq 0 ]] || { [[ ${SUBSCWRYPT} -eq 0 ]] || {
case $SCWRYPTS_OUTPUT_FORMAT in case ${SCWRYPTS_OUTPUT_FORMAT} in
pretty ) ( pretty )
HEADER="\\033[0;33m--- ($SUBSCWRYPT) BEGIN $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME ---" HEADER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) BEGIN ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")"
FOOTER="\\033[0;33m--- ($SUBSCWRYPT) END $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME ---" FOOTER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) END ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")"
;; ;;
esac esac
} }
##################################################################### #####################################################################
### run the scwrypt ################################################# ### run the scwrypt #################################################
##################################################################### #####################################################################
set -o pipefail set -o pipefail
{ {
[ $HEADER ] && echo $HEADER [[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && __SCWRYPTS_PRINT_EXIT_CODE=true
case $RUN_MODE in
normal )
(eval "$RUN_STRING $(printf "%q " "$@")")
EXIT_CODE=$?
;;
no-logfile )
eval "$RUN_STRING $(printf "%q " "$@")"
EXIT_CODE=$?
;;
interactive )
eval "$RUN_STRING $(printf "%q " "$@")" </dev/tty >/dev/tty 2>&1
EXIT_CODE=$?
;;
esac
[ $FOOTER ] && echo $FOOTER
[[ $EXIT_CODE -eq 0 ]] && EXIT_COLOR='32m' || EXIT_COLOR='31m'
[[ $SCWRYPTS_LOG_LEVEL -ge 2 ]] && [ ! $SUBSCWRYPT ] \ [ ${HEADER} ] && echo ${HEADER} >&2
&& echo "terminated with\\033[1;$EXIT_COLOR code $EXIT_CODE\\033[0m"
return $EXIT_CODE (
} | tee --append "$LOGFILE" case ${RUN_MODE} in
( normal )
eval "${RUN_STRING} $(printf "%q " "$@")"
;;
( interactive )
eval "${RUN_STRING} $(printf "%q " "$@")" </dev/tty &>/dev/tty
;;
esac
)
EXIT_CODE=$?
[ ${FOOTER} ] && echo ${FOOTER} >&2
[[ ${__SCWRYPTS_PRINT_EXIT_CODE} =~ true ]] && {
EXIT_COLOR=$( [[ ${EXIT_CODE} -eq 0 ]] && utils.colors.bright-green || utils.colors.bright-red )
case ${SCWRYPTS_OUTPUT_FORMAT} in
( raw )
echo "terminated with code ${EXIT_CODE}" >&2
;;
( pretty )
echo "terminated with ${EXIT_COLOR}code ${EXIT_CODE}$(utils.colors.reset)" >&2
;;
( json )
[[ ${EXIT_CODE} =~ 0 ]] \
&& echo.success --force-print "terminated with code ${EXIT_CODE}" \
|| echo.error --force-print "terminated with code ${EXID_CODE}" \
;
;;
esac
}
return ${EXIT_CODE}
} | tee --append "${LOGFILE}"
} $@ } $@
EXIT_CODE=$?
[ "${SCWRYPTS_TEMP_PATH}" ] && [ -d "${SCWRYPTS_TEMP_PATH}" ] \
&& {
rm -- $(find "${SCWRYPTS_TEMP_PATH}" -mindepth 1 -maxdepth 1 -type f)
rmdir "${SCWRYPTS_TEMP_PATH}"
} &>/dev/null
return ${EXIT_CODE}

View File

@ -204,7 +204,7 @@ command -v compdef &>/dev/null && {
# stop providing suggestions if your pattern is sufficient # stop providing suggestions if your pattern is sufficient
[[ $(echo $_remaining_scwrypts | wc -l) -le 1 ]] && return 0 [[ $(echo $_remaining_scwrypts | wc -l) -le 1 ]] && return 0
local _remaining_patterns="$(echo "$_remaining_scwrypts" | sed 's/\s\+/\n/g; s|/|\n|g;' | sort -u)" local _remaining_patterns="$(echo "$_remaining_scwrypts" | sed 's/\s\+/\n/g; s|/|\n|g; s/-/\n/g;' | sort -u)"
for _pattern in ${_patterns[@]} for _pattern in ${_patterns[@]}
do do

View File

@ -1,11 +1,177 @@
SCWRYPTS_GROUPS+=(scwrypts) #
# configuration for a scwrypts "group" or "plugin"
#
export SCWRYPTS_ROOT__scwrypts="$SCWRYPTS_ROOT" # this file defines the configuration for the 'scwrypts' group which
export SCWRYPTS_COLOR__scwrypts='\033[0;32m' # is required for proper operation, but otherwise loads exactly like
#export SCWRYPTS_TYPE__scwrypts= # any other group/plugin
#export SCWRYPTS_LIBRARY_ROOT__scwrypts=
export SCWRYPTS_VIRTUALENV_PATH__scwrypts="$SCWRYPTS_DATA_PATH/virtualenv" #
# both ${scwryptsgroup} and ${scwryptsgrouproot} are set automatically
#
# ${scwryptsgroup} is determined by the filename 'NAME.scwrypts.zsh'
#
# NAME must be unique and match : ^[a-z][a-z0-9_]*[a-z0-9]$
# - STARTS with a lower letter
# - ENDS with a lower letter or number
# - contains only lower-alphanumeric and underscores
# - is at least two characters long
#
# ${scwryptsgrouproot} is automatically set as the parent directory
# /path/to/group-source <-- this will be ${scwryptsgrouproot}
# ├── groupname.scwrypts.zsh
# └── your-scwrypts-source-here
#
export SCWRYPTS_PREFERRED_PYTHON_VERSIONS__scwrypts=(3.12 3.11 3.10) #####################################################################
export SCWRYPTS_NODE_VERSION__scwrypts=18.0.0 ### REQUIRED CONFIGURATION ##########################################
#####################################################################
# Currently, no configuration is required; simply creating the
# groupname.scwrypts.zsh is sufficient to define a new group
#####################################################################
### OPTIONAL CONFIGURATION ##########################################
#####################################################################
# ${scwryptsgroup}__option_key configuration values can be accessed anywhere in zsh scwrypts
# with $(scwrypts.config.group group-name option_key)
readonly ${scwryptsgroup}__type=
#
# ${scwryptsgroup}__type (optional) (default = not set)
#
# used when only one scwrypt "type" (e.g. 'zsh' or 'py') is declared
# in the group
#
# WHEN THIS IS SET, scwrypts will lookup executables starting from the
# base directory (using type ${scwryptsgroup}__type):
#
# /path/to/group-source
# ├── groupname.scwrypts.zsh
# ├── valid-scwrypts-executable
# └── some-other
# ├── valid-scwrypts-executable
# └── etc
#
# when this is NOT set, scwrypts must be nested inside a directory
# which matches the type name
#
# /path/to/group-source
# ├── groupname.scwrypts.zsh
# │
# ├── zsh
# │ ├── valid-scwrypts-executable
# │ └── some-other
# │ ├── valid-scwrypts-executable
# │ └── etc
# │
# └── py
# ├── valid-scwrypts-executable.py
# └── some-other
# ├── valid-scwrypts-executable.py
# └── etc
#
readonly ${scwryptsgroup}__color=$(utils.colors.green)
#
# ${scwryptsgroup}__color (optional) (default = no color / regular text color)
#
# an ANSI color sequence which determines the color of scwrypts in
# interactive menus
#
readonly ${scwryptsgroup}__zshlibrary=
#
# ${scwryptsgroup}__zshlibrary (optional) (default = *see below*)
#
# allows arbitrary 'use module/name --group groupname' imports
# within zsh-type scwrypts
#
# usually this is set at or within ${scwryptsgrouproot}
#
# by default, this uses either:
# 1. ${scwryptsgrouproot}/zsh/lib (compatibility)
# 2. ${scwryptsgrouproot}/zsh (preferred)
#
readonly ${scwryptsgroup}__virtualenv_path="${SCWRYPTS_STATE_PATH}/virtualenv"
#
# ${scwryptsgroup}__virtualenv_path
# (optional)
# (default = ~/.local/state/scwrypts/virtualenv)
#
# defines the path in which virtual environments are stored for
# the group
#
readonly ${scwryptsgroup}__required_environment_regex=
#
# ${scwryptsgroup}__required_environment_regex (optional) (default = allow any)
#
# helps isolate environment by locking group execution to
# environment names which match the regex
#
# when not set, no environment name restrictions are enforced
#
# when set, interactive menus will be adjusted and non-interactive
# execution will fail if the name of the environment does not match
#
#####################################################################
### ADVANCED CONFIGURATION ##########################################
#####################################################################
#${scwryptsgroup}.list-available() {}
#
# ${scwryptsgroup}.list-available()
#
# a function which outputs lines of "${SCWRYPT_TYPE}/${SCWRYPT_NAME}"
# to stdout
#
# by default, looks for executable files in ${scwryptsgrouproot}
#
# during execution of this function, the following variables are
# available:
#
# - $GROUP_ROOT : USE THIS instead of ${scwryptsgrouproot}
# - $GROUP_TYPE : USE THIS instead of ${scwryptsgroup}__type
#
# (see ./zsh/scwrypts/list-available.module.zsh for more details)
#
#${scwryptsgroup}.TYPE.get-runstring() {}
#
# a function which outputs what should be literally run when executing
# the indicated type; scwrypts already implements runstring generators
# for supported types (that's the main thing which makes them "supported")
#
# configuration variables are still automatically included as a
# prefix to the runstring
#
# (see ./zsh/scwrypts/get-runstring.module.zsh for more details)
#
#####################################################################
### HYPER-ADVANCED CONFIGURATION ####################################
#####################################################################
#
# additional zsh can be defined or run arbitrarily; this is NOT recommended
# unless you understand the implications of the various places where
# this code is loaded
#
# if you want to know where to get started (it will take some learning!),
# review the execution process in:
# - ./scwrypts
# - ./zsh/scwrypts/get-runstring.module.zsh
# - ./zsh/scwrypts/environment/user.module.zsh
#

View File

@ -1,21 +1,113 @@
# ZSH Scwrypts # ZSH Scwrypts
[![Generic Badge](https://img.shields.io/badge/1password-op-informational.svg)](https://1password.com/downloads/command-line) [![Generic Badge](https://img.shields.io/badge/1password-op-informational.svg)](https://1password.com/downloads/command-line)
[![Generic Badge](https://img.shields.io/badge/BurntSushi-rg-informational.svg)](https://github.com/BurntSushi/ripgrep) [![Generic Badge](https://img.shields.io/badge/BurntSushi-rg-informational.svg)](https://github.com/BurntSushi/ripgrep)
[![Generic Badge](https://img.shields.io/badge/dbcli-pgcli-informational.svg)](https://github.com/dbcli/pgcli)
[![Generic Badge](https://img.shields.io/badge/junegunn-fzf-informational.svg)](https://github.com/junegunn/fzf) [![Generic Badge](https://img.shields.io/badge/junegunn-fzf-informational.svg)](https://github.com/junegunn/fzf)
[![Generic Badge](https://img.shields.io/badge/mikefarah-yq-informational.svg)](https://github.com/mikefarah/yq) [![Generic Badge](https://img.shields.io/badge/mikefarah-yq-informational.svg)](https://github.com/mikefarah/yq)
[![Generic Badge](https://img.shields.io/badge/stedolan-jq-informational.svg)](https://github.com/stedolan/jq) [![Generic Badge](https://img.shields.io/badge/stedolan-jq-informational.svg)](https://github.com/stedolan/jq)
[![Generic Badge](https://img.shields.io/badge/dbcli-pgcli-informational.svg)](https://github.com/dbcli/pgcli)
<br> <br>
Since they emulate direct user interaction, shell scripts are often the straightforward choice for task automation. Since they emulate direct user interaction, shell scripts are a (commonly dreaded) go-to for automation.
## Basic Utilities Although the malleability of shell scripts can make integrations quickly, the ZSH-type scwrypt provides a structure to promote extendability and clean code while performing a lot of the heavy lifting to ensure consistent execution across different runtimes.
One of my biggest pet-peeves with scripting is when every line of a *(insert-language-here)* program is escaped to shell. ## The Basic Framework
This kind of program, which doesn't use language features, should be a shell script.
While there are definitely unavoidable limitations to shell scripting, we can minimize a variety of problems with a modern shell and shared utilities library.
Loaded by `common.zsh`, the [`utils/` library](./utils) provides: Take a look at the simplest ZSH-type scwrypt: [hello-world](./hello-world).
- common function wrappers to unify flags and context The bare minimum API for ZSH-type scwrypts is to:
- lazy dependency and environment variable validation
- consistent (and pretty) user input / output 1. include the shebang `#!/usr/bin/env zsh` on the first line of the file
2. wrap your zsh in a function called `MAIN()`
3. make the file executable (e.g. `chmod +x hello-world`)
Once this is complete, you are free to simply _write valid zsh_ then execute the scwrypt with `scwrypts hello world zsh`!
## Basics+
While it would be perfectly fine to use the `echo` function in our scwrypt, you'll notice that the `hello-world` scwrypt instead uses `echo.success` which is _not_ valid ZSH by default.
This is a helper function provided by the scwrypts ZSH library, and it does a lot more work than you'd expect.
Although this function defaults to print user messages in color, notice what happens when you run `scwrypts --output json hello world zsh`:
```json
{"timestamp":1745674060,"runtime":"c62737da-481e-4013-a370-4dedc76bf4d2","scwrypt":"start of hello-world scwrypts zsh","logLevel":"3","subscwrypt":0}
{"timestamp":1745674060,"runtime":"c62737da-481e-4013-a370-4dedc76bf4d2","status":"SUCCESS","message":"\"Hello, World!\""}
{"timestamp":1745674060,"runtime":"c62737da-481e-4013-a370-4dedc76bf4d2","status":"SUCCESS","message":"\"terminated with code 0\""}
```
We get a LOT more information.
It's 100% possible for you to include your own take on printing messages, but it is highly recommended to use the tools provided here.
### What is loaded by default?
By default, every ZSH-type scwrypt will load [the basic utilities suite](./utils), which is a little different from scwrypts ZSH modules, and a little bit complex.
Although it's totally worth a deep-dive, here are the fundamentals you should ALWAYS use:
#### Printing User Messages or Logs
Whenever you want to print a message to the user or logs, rather than using `echo`, use the following:
<!------------------------------------------------------------------------>
| function name | minimum log level | description |
| --------------- | ----------------- | --------------------------------- |
| `echo.success` | 1 | indicate successful completion |
| `echo.error` | 1 | indicate an error has occurred |
| `echo.reminder` | 1 | an important, information message |
| `echo.status` | 2 | a regular, information message |
| `echo.warning` | 3 | a non-critical warning |
| `echo.debug` | 4 | a message for scwrypt developers |
<!------------------------------------------------------------------------>
Of the `echo` family, there are two unique functions:
- `echo.error` will **increment the `ERRORS` variable** then return an error code of `$ERRORS` (this makes it easy to chain with command failure by using `||`)
- `echo.debug` will inject state information like the timestamp and current function stack
#### Yes / No Prompts
The two helpers `utils.Yn` and `utils.yN` take a user-friendly yes/no question as an argument.
- when the user responds "yes", the command returns 0 / success / `&&`
- when the user responds "no", the command returns 1 / error / `||`
- when the user responds with _nothing_ (e.g. just presses enter), the _default_ is used
The two commands work identically; however, the capitalization denotes the default:
- `utils.Yn` = default "yes"
- `utils.yN` = default "no"
#### Select from a List Prompt
When you want the user to select an item from a list, scwrypts typically use `fzf`.
There are a LOT of options to `fzf`, so there are two provided helpers.
The basic selector, `utils.fzf` (most of the time, you want to use this one) which outputs:
- _the selection_ if the user made a choice
- _nothing / empty string_ if the user cancelled or made an invalid choice
The user-input selector, `utils.fzf.user-input` which outputs:
- _the selection_ if the user made a choice
- _the text typed by the user_ if the user typed something other than the listed choices
- _nothing / empty string_ if the user cancelled
- _a secondary `utils.fzf` prompt_ if the user's choice was ambiguous
### Imports
Don't use `source` in ZSH-type scwrypts (I mean, if you're pretty clever you can get it to work, but DON'T THOUGH).
Instead, use `use`!
The `use` command, rather than specifying file directories, you reference the path to `*.module.zsh`.
This means you don't have to know the exact path to any given file.
For example, if I wanted to import the safety tool for `aws` CLI commands, I can do the following:
```zsh
#!/usr/bin/env zsh
use cloud/aws
#####################################################################
MAIN() {
cloud.aws sts get-caller-identity
}
```

View File

@ -0,0 +1,13 @@
#
# provides utilities for interacting with Amazon Web Services (AWS)
#
# context wrapper for AWS CLI v2
use cloud/aws/cli
eval "${scwryptsmodule}() { ${scwryptsmodule}.cli \$@; }"
# simplify context commands for kubectl on EKS
use cloud/aws/eks
# context wrapper for eksctl
use cloud/aws/eksctl

View File

@ -0,0 +1,28 @@
#####################################################################
use unittest
testmodule=cloud.aws
#####################################################################
beforeall() {
use cloud/aws
}
#####################################################################
test.provides-aws-cli() {
unittest.test.provides ${testmodule}.cli
}
test.provides-aws-cli-alias() {
unittest.test.provides ${testmodule}
}
test.provides-eks() {
unittest.test.provides ${testmodule}.eks
}
test.provides-eksctl() {
unittest.test.provides ${testmodule}.eksctl
}

View File

@ -0,0 +1,36 @@
#####################################################################
DEPENDENCIES+=(aws)
use cloud/aws/zshparse
#####################################################################
${scwryptsmodule}() {
local PARSERS=(cloud.aws.zshparse.overrides)
local ARGS=()
local DESCRIPTION="
Safe context wrapper for aws cli commands; prevents accidental local environment
bleed-through, but otherwise works exactly like 'aws'. For help with awscli, try
'AWS [command] help' (no -h or --help)
This wrapper should be used in place of _all_ 'aws' usages within scwrypts.
"
eval "$(utils.parse.autosetup)"
##########################################
echo.debug "invoking '$(echo "$AWS_EVAL_PREFIX" | sed 's/AWS_\(ACCESS_KEY_ID\|SECRET_ACCESS_KEY\)=[^ ]\+ //g')aws ${AWS_CONTEXT_ARGS[@]} ${ARGS[@]}'"
eval "${AWS_EVAL_PREFIX}aws ${AWS_CONTEXT_ARGS[@]} ${ARGS[@]}"
}
#####################################################################
${scwryptsmodule}.parse() {
return 0 # uses default args parser
}
${scwryptsmodule}.parse.usage() {
USAGE__args+='\$@ arguments forwarded to the AWS CLI'
}

View File

@ -0,0 +1,65 @@
#####################################################################
use unittest
testmodule=cloud.aws.cli
#####################################################################
beforeall() {
use cloud/aws/cli
}
beforeeach() {
unittest.mock aws
unittest.mock echo.debug
_ARGS=($(uuidgen) $(uuidgen) $(uuidgen))
_AWS_REGION=$(uuidgen)
_AWS_PROFILE=$(uuidgen)
unittest.mock.env AWS_ACCOUNT --value $(uuidgen)
unittest.mock.env AWS_PROFILE --value ${_AWS_PROFILE}
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
}
aftereach() {
unset _AWS_REGION
unset _AWS_PROFILE
}
#####################################################################
test.forwards-arguments() {
${testmodule} ${_ARGS[@]}
aws.assert.callstack \
--output json \
--region ${_AWS_REGION} \
--profile ${_AWS_PROFILE} \
${_ARGS[@]} \
;
}
test.overrides-region() {
local OVERRIDE_REGION=$(uuidgen)
${testmodule} --region ${OVERRIDE_REGION} ${_ARGS[@]}
aws.assert.callstack \
--output json \
--region ${OVERRIDE_REGION} \
--profile ${_AWS_PROFILE} \
${_ARGS[@]} \
;
}
test.overrides-account() {
local OVERRIDE_ACCOUNT=$(uuidgen)
${testmodule} --account ${OVERRIDE_ACCOUNT} ${_ARGS[@]}
echo.debug.assert.callstackincludes \
AWS_ACCOUNT=${OVERRIDE_ACCOUNT} \
;
}

View File

@ -0,0 +1,6 @@
#
# common operations for AWS Elastic Container Registry (ECR)
#
# that obnoxious command which pushes the AWS temporary credentials to 'docker login'
use cloud/aws/ecr/login

View File

@ -0,0 +1,16 @@
#####################################################################
use unittest
testmodule=cloud.aws.ecr
#####################################################################
beforeall() {
use cloud/aws/ecr
}
#####################################################################
test.provides-ecr-login() {
unittest.test.provides ${testmodule}.login
}

View File

@ -1,7 +1,17 @@
#!/bin/zsh #!/usr/bin/env zsh
use cloud/aws/ecr
##################################################################### #####################################################################
MAIN() { use cloud/aws/ecr/login
ECR_LOGIN $@ use cloud/aws/zshparse
}
#####################################################################
USAGE__description='
interactively setup temporary credentials for ECR in the given region
'
cloud.aws.zshparse.overrides.usage
#####################################################################
MAIN() { cloud.aws.ecr.login $@; }

View File

@ -0,0 +1,30 @@
#####################################################################
use cloud/aws/cli
use cloud/aws/zshparse
DEPENDENCIES+=(docker)
#####################################################################
${scwryptsmodule}() {
local DESCRIPTION="
Performs the appropriate 'docker login' command with temporary
credentials from AWS.
"
local PARSERS=(cloud.aws.zshparse.overrides)
eval "$(utils.parse.autosetup)"
##########################################
${AWS} ecr get-login-password \
| docker login "${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com" \
--username AWS \
--password-stdin \
&>/dev/null \
&& echo.success "authenticated docker for '${ACCOUNT}' in '${REGION}'" \
|| echo.error "unable to authenticate docker for '${ACCOUNT}' in '${REGION}'" \
;
}

View File

@ -0,0 +1,47 @@
#####################################################################
use unittest
testmodule=cloud.aws.ecr.login
#####################################################################
beforeall() {
use cloud/aws/ecr/login
}
beforeeach() {
unittest.mock cloud.aws.cli
unittest.mock docker
_AWS_ACCOUNT=$(uuidgen)
_AWS_PROFILE=$(uuidgen)
_AWS_REGION=$(uuidgen)
unittest.mock.env AWS_ACCOUNT --value ${_AWS_ACCOUNT}
unittest.mock.env AWS_PROFILE --value ${_AWS_PROFILE}
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
_EXPECTED_AWS_ARGS=(
--account ${_AWS_ACCOUNT}
--region ${_AWS_REGION}
)
}
aftereach() {
unset \
_AWS_ACCOUNT _AWS_PROFILE _AWS_REGION \
_EXPECTED_AWS_ARGS \
;
}
#####################################################################
test.login-forwards-credentials-to-docker() {
${testmodule}
docker.assert.callstack \
login "${_AWS_ACCOUNT}.dkr.ecr.${_AWS_REGION}.amazonaws.com" \
--username AWS \
--password-stdin \
;
}

View File

@ -1,64 +1,82 @@
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(jq) #####################################################################
REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT)
use cloud/aws/cli use cloud/aws/cli
use cloud/aws/zshparse/overrides
DEPENDENCIES+=(jq mount sort sudo)
REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT)
#####################################################################
USAGE__description='
interactively mount an AWS EFS volume to the local filesystem
'
##################################################################### #####################################################################
MAIN() { MAIN() {
GETSUDO || exit 1 local PARSERS=(cloud.aws.zshparse.overrides)
[ ! -d $AWS__EFS__LOCAL_MOUNT_POINT ] && { eval "$(utils.parse.autosetup)"
sudo mkdir $AWS__EFS__LOCAL_MOUNT_POINT \ utils.io.getsudo || return 1
&& STATUS "created local mount point '$AWS__EFS__LOCAL_MOUNT_POINT'" ##########################################
}
{
mkdir -p -- "${AWS__EFS__LOCAL_MOUNT_POINT}" \
|| sudo mkdir -p -- "${AWS__EFS__LOCAL_MOUNT_POINT}"
} &>/dev/null
[ -d "${AWS__EFS__LOCAL_MOUNT_POINT}" ] \
|| echo.error "unable to create local mount point '${AWS__EFS__LOCAL_MOUNT_POINT}'" \
|| return
local FS_ID=$(\ local FS_ID=$(\
AWS efs describe-file-systems \ $AWS efs describe-file-systems \
| jq -r '.[] | .[] | .FileSystemId' \ | jq -r '.[] | .[] | .FileSystemId' \
| FZF 'select a filesystem to mount' \ | utils.fzf 'select a filesystem to mount' \
) )
[ ! $FS_ID ] && ABORT [ ! ${FS_ID} ] && utils.abort
local MOUNT_POINT="$AWS__EFS__LOCAL_MOUNT_POINT/$FS_ID" local MOUNT_POINT="${AWS__EFS__LOCAL_MOUNT_POINT}/${FS_ID}"
[ -d "$MOUNT_POINT" ] && sudo rmdir "$MOUNT_POINT" >/dev/null 2>&1 [ -d "${MOUNT_POINT}" ] && sudo rmdir "${MOUNT_POINT}" &>/dev/null
[ -d "$MOUNT_POINT" ] && { [ -d "${MOUNT_POINT}" ] && {
STATUS "$FS_ID is already mounted" echo.status "${FS_ID} is already mounted"
exit 0 return 0
} }
local MOUNT_TARGETS=$(AWS efs describe-mount-targets --file-system-id $FS_ID) local MOUNT_TARGETS=$($AWS efs describe-mount-targets --file-system-id ${FS_ID})
local ZONE=$(\ local ZONE=$(\
echo $MOUNT_TARGETS \ echo ${MOUNT_TARGETS} \
| jq -r '.[] | .[] | .AvailabilityZoneName' \ | jq -r '.[] | .[] | .AvailabilityZoneName' \
| sort -u | FZF 'select availability zone'\ | sort -u | utils.fzf 'select availability zone'\
) )
[ ! $ZONE ] && ABORT [ ! "${ZONE}" ] && utils.abort
local MOUNT_IP=$(\ local MOUNT_IP=$(\
echo $MOUNT_TARGETS \ echo ${MOUNT_TARGETS} \
| jq -r ".[] | .[] | select (.AvailabilityZoneName == \"$ZONE\") | .IpAddress" \ | jq -r ".[] | .[] | select (.AvailabilityZoneName == \"${ZONE}\") | .IpAddress" \
| head -n1 \ | head -n1 \
) )
SUCCESS 'ready to mount!' echo.success 'ready to mount!'
REMINDER 'for private file-systems, you must be connected to the appropriate VPN' echo.status "
file system id : ${FS_ID}
availability zone : ${ZONE}
file system ip : ${MOUNT_IP}
local mount point : ${MOUNT_POINT}
"
echo.reminder 'for private file-systems, you must be connected to the appropriate VPN'
Yn 'proceed?' || utils.abort
STATUS "file system id : $FS_ID" sudo mkdir -- "${MOUNT_POINT}" \
STATUS "availability zone : $ZONE"
STATUS "file system ip : $MOUNT_IP"
STATUS "local mount point : $MOUNT_POINT"
Yn 'proceed?' || ABORT
sudo mkdir $MOUNT_POINT \
&& sudo mount \ && sudo mount \
-t nfs4 \ -t nfs4 \
-o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport \ -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport \
$MOUNT_IP:/ \ "${MOUNT_IP}:/" \
"$MOUNT_POINT" \ "${MOUNT_POINT}" \
&& SUCCESS "mounted at '$MOUNT_POINT'" \ && echo.success "mounted at '${MOUNT_POINT}'" \
|| { || {
sudo rmdir $MOUNT_POINT >/dev/null 2>&1 sudo rmdir -- "${MOUNT_POINT}" &>/dev/null
FAIL 2 "unable to mount '$FS_ID'" echo.error "unable to mount '${FS_ID}'"
} }
} }

View File

@ -1,32 +1,39 @@
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(jq)
REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT)
use cloud/aws/cli
##################################################################### #####################################################################
DEPENDENCIES+=(jq umount sudo)
REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT)
#####################################################################
USAGE__description='
interactively unmount an AWS EFS volume to the local filesystem
'
MAIN() { MAIN() {
[ ! -d "$AWS__EFS__LOCAL_MOUNT_POINT" ] && { eval "$(utils.parse.autosetup)"
STATUS 'no efs currently mounted' ##########################################
exit 0
[ ! -d "${AWS__EFS__LOCAL_MOUNT_POINT}" ] && {
echo.status 'no efs currently mounted'
return 0
} }
local MOUNTED=$(ls "$AWS__EFS__LOCAL_MOUNT_POINT") local MOUNTED=$(cd -- "${AWS__EFS__LOCAL_MOUNT_POINT}" | find . -type -f | sed 's|^\./.||')
[ ! $MOUNTED ] && { [ "${MOUNTED}" ] && {
STATUS 'no efs currently mounted' echo.status 'no efs currently mounted'
exit 0 return 0
} }
GETSUDO || exit 1 utils.io.getsudo || return 1
local SELECTED=$(echo ${MOUNTED} | utils.fzf 'select a file system to unmount')
[ "${SELECTED}" ] || user.abort
local SELECTED=$(echo $MOUNTED | FZF 'select a file system to unmount') local EFS="${AWS__EFS__LOCAL_MOUNT_POINT}/${SELECTED}"
[ ! $SELECTED ] && ABORT echo.status "unmounting '${SELECTED}'"
sudo umount "${EFS}" >/dev/null 2>&1
local EFS="$AWS__EFS__LOCAL_MOUNT_POINT/$SELECTED" sudo rmdir -- "${EFS}" \
STATUS "unmounting '$SELECTED'" && echo.success "done" \
sudo umount $EFS >/dev/null 2>&1 || utils.fail 2 "failed to unmount '${EFS}'"
sudo rmdir $EFS \
&& SUCCESS "done" \
|| FAIL 2 "failed to unmount '$EFS'"
} }

View File

@ -0,0 +1,86 @@
#####################################################################
use cloud/aws/eks/cluster-login
use cloud/aws/zshparse
use cloud/aws/eks/zshparse
#####################################################################
${scwryptsmodule}() {
local DESCRIPTION="
Context wrapper for kubernetes CLI commands on AWS EKS. This
will automatically attempt login for first-time connections,
and ensures the correct kubecontext is used for the expected
command.
EKS --cluster-name my-cluster kubectl get pods
EKS --cluster-name my-cluster helm history my-deployment
... etc ...
"
local ARGS=() PARSERS=(
cloud.aws.zshparse.overrides
cloud.aws.eks.zshparse.cluster-name
)
eval "$(utils.parse.autosetup)"
##########################################
local CONTEXT="arn:aws:eks:${REGION}:${ACCOUNT}:cluster/${CLUSTER_NAME}"
local ALREADY_LOGGED_IN
kubectl config get-contexts --output=name | grep -q "^${CONTEXT}$" \
&& ALREADY_LOGGED_IN=true \
|| ALREADY_LOGGED_IN=false \
;
case ${ALREADY_LOGGED_IN} in
( true ) ;;
( false )
cloud.aws.eks.cluster-login \
${AWS_PASSTHROUGH[@]} \
--cluster-name ${CLUSTER_NAME} \
>/dev/null \
|| echo.error "unable to login to cluster '${CLUSTER_NAME}'" \
|| return 1
;;
esac
local CONTEXT_ARGS=()
case ${KUBECLI} in
( helm )
CONTEXT_ARGS+=(--kube-context ${CONTEXT}) # *rolls eyes* THANKS, helm
;;
( * )
CONTEXT_ARGS+=(--context ${CONTEXT})
;;
esac
${KUBECLI} ${CONTEXT_ARGS[@]} ${ARGS[@]}
}
#####################################################################
${scwryptsmodule}.parse() { return 0; }
${scwryptsmodule}.parse.locals() {
local KUBECLI # extracted from default ARGS parser
}
${scwryptsmodule}.parse.usage() {
USAGE__usage+=' kubecli [...kubecli-args...]'
USAGE__args+='
kubecli cli which uses kubernetes context arguments (e.g. kubectl, helm, flux)
kubecli-args arguments forwarded to the kubectl-style CLI
'
}
${scwryptsmodule}.parse.validate() {
KUBECLI="${ARGS[1]}"
ARGS=(${ARGS[@]:1})
[ ${KUBECLI} ] \
|| echo.error "missing argument for 'kubecli'"
}

View File

@ -0,0 +1,105 @@
#####################################################################
use unittest
testmodule=cloud.aws.eks.cli
#####################################################################
beforeall() {
use cloud/aws/eks/cli
use cloud/aws/eks/cluster-login
}
beforeeach() {
unittest.mock cloud.aws.eks.cluster-login
_CLUSTER_NAME=$(uuidgen)
_AWS_ACCOUNT=$(uuidgen)
_AWS_PROFILE=$(uuidgen)
_AWS_REGION=$(uuidgen)
_KUBECLI=$(uuidgen)
_KUBECLI_ARGS=($(uuidgen) $(uuidgen) $(uuidgen))
unittest.mock.env AWS_ACCOUNT --value ${_AWS_ACCOUNT}
unittest.mock.env AWS_PROFILE --value ${_AWS_PROFILE}
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
_EXPECTED_KUBECONTEXT="arn:aws:eks:${_AWS_REGION}:${_AWS_ACCOUNT}:cluster/${_CLUSTER_NAME}"
_KUBECTL_KUBECONTEXTS="$(uuidgen)\n${_EXPECTED_KUBECONTEXT}\n$(uuidgen)"
_EXPECTED_AWS_ARGS=(
--account ${_AWS_ACCOUNT}
--region ${_AWS_REGION}
)
}
aftereach() {
unset \
_CLUSTER_NAME \
_AWS_ACCOUNT _AWS_PROFILE _AWS_REGION \
_EXPECTED_AWS_ARGS \
;
}
mock.kubectl() {
unittest.mock kubectl --stdout "${_KUBECTL_KUBECONTEXTS}"
}
mock.kubecli() {
command -v ${_KUBECLI} &>/dev/null || ${_KUBECLI}() { true; }
unittest.mock ${_KUBECLI}
}
#####################################################################
test.uses-correct-kubecli-args() {
mock.kubectl
mock.kubecli
${testmodule} --cluster-name ${_CLUSTER_NAME} ${_KUBECLI} ${_KUBECLI_ARGS[@]}
${_KUBECLI}.assert.callstack \
--context ${_EXPECTED_KUBECONTEXT} \
${_KUBECLI_ARGS[@]}
;
}
test.uses-correct-helm-args() {
_KUBECLI=helm
mock.kubectl
mock.kubecli
${testmodule} --cluster-name ${_CLUSTER_NAME} ${_KUBECLI} ${_KUBECLI_ARGS[@]}
${_KUBECLI}.assert.callstack \
--kube-context ${_EXPECTED_KUBECONTEXT} \
${_KUBECLI_ARGS[@]}
;
}
test.performs-login() {
_KUBECTL_KUBECONTEXTS="$(uuidgen)\n$(uuidgen)"
mock.kubectl
mock.kubecli
${testmodule} --cluster-name ${_CLUSTER_NAME} ${_KUBECLI} ${_KUBECLI_ARGS[@]}
cloud.aws.eks.cluster-login.assert.callstack \
${_EXPECTED_AWS_ARGS[@]} \
--cluster-name ${_CLUSTER_NAME} \
;
}
test.does-not-perform-login-if-already-logged-in() {
mock.kubectl
mock.kubecli
${testmodule} --cluster-name ${_CLUSTER_NAME} ${_KUBECLI} ${_KUBECLI_ARGS[@]}
cloud.aws.eks.cluster-login.assert.not.called
}

View File

@ -0,0 +1,43 @@
#####################################################################
use cloud/aws/cli
use cloud/aws/zshparse
use cloud/aws/eks/zshparse
#####################################################################
${scwryptsmodule}() {
local DESCRIPTION='
Interactively sets the default kubeconfig to match the selected
cluster in EKS. Also creates the kubeconfig entry if it does not
already exist.
'
local PARSERS=(
cloud.aws.zshparse.overrides
cloud.aws.eks.zshparse.cluster-name
)
local EKS_CLUSTER_NAME_INTERACTIVE=allowed
eval "$(utils.parse.autosetup)"
#####################################################################
[ ${CLUSTER_NAME} ] || CLUSTER_NAME=$(\
${AWS} eks list-clusters \
| jq -r '.[] | .[]' \
| utils.fzf "select an eks cluster (${ACCOUNT}/${REGION})"
)
[ ${CLUSTER_NAME} ] || echo.error 'must select a valid cluster or use --cluster-name'
utils.check-errors || return $?
##########################################
echo.status 'updating kubeconfig for EKS cluster '${CLUSTER_NAME}''
${AWS} eks update-kubeconfig --name ${CLUSTER_NAME} \
&& echo.success "kubeconfig updated with '${CLUSTER_NAME}'" \
|| echo.error "failed to update kubeconfig; do you have permission to access '${CLUSTER_NAME}'?"
}

View File

@ -0,0 +1,66 @@
#####################################################################
use unittest
testmodule=cloud.aws.eks.cluster-login
#####################################################################
beforeall() {
use cloud/aws/eks/cluster-login
}
beforeeach() {
unittest.mock cloud.aws.cli
_CLUSTER_NAME=$(uuidgen)
_AWS_ACCOUNT=$(uuidgen)
_AWS_PROFILE=$(uuidgen)
_AWS_REGION=$(uuidgen)
unittest.mock.env AWS_ACCOUNT --value ${_AWS_ACCOUNT}
unittest.mock.env AWS_PROFILE --value ${_AWS_PROFILE}
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
_EXPECTED_AWS_ARGS=(
--account ${_AWS_ACCOUNT}
--region ${_AWS_REGION}
)
}
aftereach() {
unset \
_CLUSTER_NAME \
_AWS_ACCOUNT _AWS_PROFILE _AWS_REGION \
_EXPECTED_AWS_ARGS \
;
}
#####################################################################
test.login-to-correct-cluster() {
${testmodule} --cluster-name ${_CLUSTER_NAME}
cloud.aws.cli.assert.callstack \
${_EXPECTED_AWS_ARGS[@]} \
eks update-kubeconfig \
--name ${_CLUSTER_NAME} \
;
}
test.interactive-login-ignored-on-ci() {
${testmodule}
cloud.aws.cli.assert.not.called
}
test.interactive-login-to-correct-cluster() {
unittest.mock utils.fzf --stdout ${_CLUSTER_NAME}
${testmodule}
cloud.aws.cli.assert.callstack \
${_EXPECTED_AWS_ARGS[@]} \
eks update-kubeconfig \
--name ${_CLUSTER_NAME} \
;
}

View File

@ -0,0 +1,10 @@
#
# run kubectl/helm/etc commands on AWS Elastic Kubernetes Service (EKS)
#
# provides an EKS connection wrapper for any kubectl-like cli
use cloud/aws/eks/cli
eval "${scwryptsmodule}() { ${scwryptsmodule}.cli $@; }"
# sets up kubeconfig to connect to EKS
use cloud/aws/eks/cluster-login

View File

@ -0,0 +1,24 @@
#####################################################################
use unittest
testmodule=cloud.aws.eks
#####################################################################
beforeall() {
use cloud/aws
}
#####################################################################
test.provides-eks-cli() {
unittest.test.provides ${testmodule}.cli
}
test.provides-eks-cli-alias() {
unittest.test.provides ${testmodule}
}
test.provides-cluster-login() {
unittest.test.provides ${testmodule}.cluster-login
}

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use cloud/aws/eks use cloud/aws/eks
##################################################################### #####################################################################

View File

@ -0,0 +1,44 @@
${scwryptsmodule}.locals() {
local CLUSTER_NAME
# set to 'allowed' to enable interactive cluster select
# by default, the '--cluster-name' flag is required
local EKS_CLUSTER_NAME_INTERACTIVE
}
${scwryptsmodule}() {
local PARSED=0
case $1 in
( -c | --cluster-name )
CLUSTER_NAME="$2"
((PARSED+=2))
;;
esac
return $PARSED
}
${scwryptsmodule}.usage() {
[[ "$USAGE__usage" =~ '\[...options...\]' ]] || USAGE__usage+=' [...options...]'
USAGE__options+="\n
-c, --cluster-name <string> EKS cluster name identifier string
"
}
${scwryptsmodule}.validate() {
[ $CLUSTER_NAME ] && return 0
[[ $EKS_CLUSTER_NAME_INTERACTIVE =~ allowed ]] \
|| echo.error 'missing cluster name' \
|| return
CLUSTER_NAME=$(\
$AWS eks list-clusters \
| jq -r '.[] | .[]' \
| utils.fzf "select an eks cluster ($ACCOUNT/$REGION)" \
)
[ $CLUSTER_NAME ] || echo.error 'must select a valid cluster or use --cluster-name'
}

View File

@ -0,0 +1,6 @@
#
# argument parsers for common EKS arguments
#
# get the EKS "ClusterName" identifier
use cloud/aws/eks/zshparse/cluster-name

View File

@ -0,0 +1,40 @@
#####################################################################
use cloud/aws/zshparse/overrides
DEPENDENCIES+=(eksctl)
#####################################################################
${scwryptsmodule}() {
local PARSERS=(cloud.aws.zshparse.overrides)
local DESCRIPTION="
Context wrapper for eksctl commands; prevents accidental local environment
bleed-through, but otherwise works exactly like 'eksctl'.
This wrapper should be used in place of _all_ 'eksctl' usages within scwrypts.
"
eval "$(utils.parse.autosetup)"
##########################################
echo.debug "invoking '$(echo "$AWS_EVAL_PREFIX" | sed 's/AWS_\(ACCESS_KEY_ID\|SECRET_ACCESS_KEY\)=[^ ]\+ //g')eksctl ${ARGS[@]}'"
eval "${AWS_EVAL_PREFIX}eksctl ${ARGS[@]}"
}
#####################################################################
${scwryptsmodule}.parse() {
return 0
}
${scwryptsmodule}.parse.locals() {
local ARGS=()
}
${scwryptsmodule}.parse.usage() {
USAGE__args+='
args all remaining arguments are forwarded to eksctl
'
}

View File

@ -0,0 +1,72 @@
#####################################################################
use unittest
testmodule=cloud.aws.eksctl.cli
#####################################################################
beforeall() {
use cloud/aws/eksctl/cli
}
beforeeach() {
unittest.mock eksctl
unittest.mock echo.debug
_ARGS=($(uuidgen) $(uuidgen) $(uuidgen))
_AWS_REGION=$(uuidgen)
_AWS_PROFILE=$(uuidgen)
unittest.mock.env AWS_ACCOUNT --value $(uuidgen)
unittest.mock.env AWS_PROFILE --value ${_AWS_PROFILE}
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
}
aftereach() {
unset _AWS_REGION
unset _AWS_PROFILE
}
#####################################################################
test.forwards-arguments() {
${testmodule} ${_ARGS[@]}
eksctl.assert.callstack \
${_ARGS[@]} \
;
}
test.forwards-profile() {
#
# --profile is an invalid argument for eksctl, so it
# MUST be forwarded as AWS_PROFILE to prevent environment
# bleeding
#
${testmodule} ${_ARGS[@]}
echo.debug.assert.callstackincludes \
AWS_PROFILE=${OVERRIDE_REGION} \
;
}
test.overrides-region() {
local OVERRIDE_REGION=$(uuidgen)
${testmodule} --region ${OVERRIDE_REGION} ${_ARGS[@]}
echo.debug.assert.callstackincludes \
AWS_REGION=${OVERRIDE_REGION} \
;
}
test.overrides-account() {
local OVERRIDE_ACCOUNT=$(uuidgen)
${testmodule} --account ${OVERRIDE_ACCOUNT} ${_ARGS[@]}
echo.debug.assert.callstackincludes \
AWS_ACCOUNT=${OVERRIDE_ACCOUNT} \
;
}

View File

@ -0,0 +1,10 @@
#
# module for eksctl actions
#
# context wrapper for direct use of eksctl
use cloud/aws/eksctl/cli
eval "${scwryptsmodule}() { ${scwryptsmodule}.cli \$@; }"
# argument helper for creating a standard iamserviceaccount
use cloud/aws/eksctl/iamserviceaccount

View File

@ -0,0 +1,20 @@
#####################################################################
use unittest
testmodule=cloud.aws.eksctl
#####################################################################
beforeall() {
use cloud/aws/eksctl
}
#####################################################################
test.provides-eksctl-cli() {
unittest.test.provides ${testmodule}.cli
}
test.provides-eksctl-alias() {
unittest.test.provides ${testmodule}
}

View File

@ -0,0 +1,57 @@
#####################################################################
use cloud/aws/eks/cli
use cloud/aws/eksctl/iamserviceaccount/zshparse
use cloud/aws/zshparse/overrides
DEPENDENCIES+=(kubectl yq)
#####################################################################
${scwryptsmodule}() {
local DESCRIPTION="
determine whether the target iamserviceaccount already
exists on Kubernetes
OK:
exit code 0 : the serviceaccount exists on kubernetes
exit code 100 : the serviceaccount does not exist on kubernetes
ERROR:
exit code 200 : the serviceaccount exists on kubernetes, but does not match the target role
"
local PARSERS=(
cloud.aws.eksctl.iamserviceaccount.zshparse
cloud.aws.zshparse.overrides
)
eval "$(utils.parse.autosetup)"
##########################################
echo.status "checking for existing role-arn"
local CURRENT_ROLE_ARN=$(
cloud.aws.eks.cli kubectl ${AWS_PASSTHROUGH_ARGS[@]} --namespace "${NAMESPACE}" get serviceaccount "${SERVICEACCOUNT}" -o yaml \
| utils.yq -r '.metadata.annotations["eks.amazonaws.com/role-arn"]' \
| grep -v '^null$' \
)
[ "${CURRENT_ROLE_ARN}" ] || {
echo.status "serviceaccount does not exist or has no configured role"
return 100
}
[[ ${CURRENT_ROLE_ARN} =~ "${ROLE_NAME}$" ]] || {
echo.status "\
serviceaccount current role does not match desired role:
CURRENT : ${CURRENT_ROLE_ARN}
DESIRED : arn:aws:iam::${AWS_ACCOUNT}:role/${ROLE_NAME}
"
return 200
}
echo.status "serviceaccount current role matches desired role"
return 0
}

View File

@ -0,0 +1,63 @@
#####################################################################
use unittest
testmodule=cloud.aws.eksctl.iamserviceaccount.check-exists
#####################################################################
beforeall() {
use cloud/aws/eksctl/iamserviceaccount/check-exists
use cloud/aws/eks/cli
}
beforeeach() {
_SERVICEACCOUNT=$(uuidgen)
_NAMEPACE=$(uuidgen)
_ROLE_NAME=$(uuidgen)
_ROLE_ARN="$(uuidgen)/${_ROLE_NAME}"
unittest.mock.env AWS_ACCOUNT --value $(uuidgen)
unittest.mock.env AWS_PROFILE --value $(uuidgen)
unittest.mock.env AWS_REGION --value $(uuidgen)
_ARGS=(
--serviceaccount ${_SERVICEACCOUNT}
--namespace ${_NAMEPACE}
--role-name ${_ROLE_NAME}
)
}
aftereach() {
unset _SERVICEACCOUNT _NAMESPACE _ROLE_NAME _ARGS
}
#####################################################################
test.detects-exists() {
unittest.mock cloud.aws.eks.cli \
--stdout '{"metadata":{"annotations":{"eks.amazonaws.com/role-arn":"'$_ROLE_ARN'"}}}' \
;
${testmodule} ${_ARGS[@]}
[[ $? -eq 0 ]]
}
test.detects-not-exists() {
unittest.mock cloud.aws.eks.cli \
--stdout '{}'
${testmodule} ${_ARGS[@]}
[[ $? -eq 100 ]]
}
test.detects-exists-but-does-not-match() {
unittest.mock cloud.aws.eks.cli \
--stdout '{"metadata":{"annotations":{"eks.amazonaws.com/role-arn":"'$(uuidgen)'"}}}' \
;
${testmodule} ${_ARGS[@]}
[[ $? -eq 200 ]]
}

View File

@ -0,0 +1,93 @@
#####################################################################
use cloud/aws/eksctl/cli
use cloud/aws/eksctl/iamserviceaccount/check-exists
use cloud/aws/eksctl/iamserviceaccount/zshparse
use cloud/aws/zshparse/overrides
use cloud/aws/eks/zshparse/cluster-name
#####################################################################
${scwryptsmodule}() {
local DESCRIPTION="
creates an 'iamserviceaccount' which provides a Kubernetes
serviceaccount with AWS role identity and access control
"
local PARSERS=(
cloud.aws.eksctl.iamserviceaccount.zshparse
cloud.aws.zshparse.overrides
cloud.aws.eks.zshparse.cluster-name
)
eval "$(utils.parse.autosetup)"
##########################################
case ${FORCE} in
( true ) ;;
( false )
cloud.aws.eksctl.iamserviceaccount.check-exists \
--serviceaccount "${SERVICEACCOUNT}" \
--namespace "${NAMESPACE}" \
--role-name "${ROLE_NAME}" \
${AWS_PASSTHROUGH[@]} \
;
case $? in
( 0 ) echo.success "'${NAMESPACE}/${SERVICEACCOUNT}' already configured with '${ROLE_NAME}'"
return 0
;;
( 100 ) # role does not exist yet; continue with rollout
;;
( 200 ) echo.error "'${NAMESPACE}/${SERVICEACCOUNT}' has been configured with a different role than '${ROLE_NAME}'"
echo.reminder "must use --force flag to overwrite"
return 2
;;
esac
;;
esac
echo.status "creating iamserviceaccount" \
&& cloud.aws.eksctl.cli ${AWS_PASSTHROUGH_ARGS[@]} create iamserviceaccount \
--cluster "${CLUSTER_NAME}" \
--namespace "${NAMESPACE}" \
--name "${SERVICEACCOUNT}" \
--role-name "${ROLE_NAME}" \
--override-existing-serviceaccounts \
--approve \
${ARGS[@]} \
&& echo.success "successfully configured '${NAMESPACE}/${SERVICEACCOUNT}' with IAM role '${ROLE_NAME}'" \
|| echo.error "unable to configure '${NAMESPACE}/${SERVICEACCOUNT}' with IAM role '${ROLE_NAME}'\n(check cloudformation dashboard for details)" \
;
}
#####################################################################
${scwryptsmodule}.parse.locals() {
local FORCE=false # whether or not to force a new eksctl deployment
local ARGS=()
}
${scwryptsmodule}.parse() {
local PARSED=0
case $1 in
--force ) PARSED=1; FORCE=true ;;
esac
return ${PARSED}
}
${scwryptsmodule}.parse.usage() {
USAGE__options+="
--force don't check for existing serviceaccount and override any existing configuration
"
USAGE__args+="
args all remaining arguments are forwarded to 'eksctl create iamserviceaccount'
eksctl create iamserviceaccount args:
$(eksctl create iamserviceaccount --help 2>&1 | grep -v -- '--name' | grep -v -- '--namespace' | grep -v -- '--role-name' | sed 's/^/ /')
"
}

View File

@ -0,0 +1,150 @@
#####################################################################
use unittest
testmodule=cloud.aws.eksctl.iamserviceaccount.create
#####################################################################
beforeall() {
use cloud/aws/eksctl/iamserviceaccount/create
}
beforeeach() {
unittest.mock cloud.aws.eksctl.cli
_SERVICEACCOUNT=$(uuidgen)
_NAMESPACE=$(uuidgen)
_ROLE_NAME=$(uuidgen)
_ROLE_ARN="$(uuidgen)/${_ROLE_NAME}"
_CLUSTER_NAME=$(uuidgen)
_AWS_ACCOUNT=$(uuidgen)
_AWS_REGION=$(uuidgen)
unittest.mock.env AWS_ACCOUNT --value ${_AWS_ACCOUNT}
unittest.mock.env AWS_PROFILE --value $(uuidgen)
unittest.mock.env AWS_REGION --value ${_AWS_REGION}
_IAMSERVICEACCOUNT_ARGS=(
--serviceaccount ${_SERVICEACCOUNT}
--namespace ${_NAMESPACE}
--role-name ${_ROLE_NAME}
)
_EXTRA_ARGS=($(uuidgen) $(uuidgen) $(uuidgen))
_ARGS=(
--cluster-name ${_CLUSTER_NAME}
${_IAMSERVICEACCOUNT_ARGS[@]}
--
${_EXTRA_ARGS[@]}
)
_EXPECTED_AWS_PASSTHROUGH=(
--account ${_AWS_ACCOUNT}
--region ${_AWS_REGION}
)
}
aftereach() {
unset \
_SERVICEACCOUNT _NAMESPACE _ROLE_NAME \
_CLUSTER_NAME \
_AWS_ACCOUNT _AWS_REGION \
_ARGS _EXPECTED_AWS_PASSTHROUGH_ARGS \
;
}
#####################################################################
test.performs-check-exists() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 0 \
;
${testmodule} ${_ARGS[@]}
cloud.aws.eksctl.iamserviceaccount.check-exists.assert.callstack \
${_IAMSERVICEACCOUNT_ARGS[@]} \
${_EXPECTED_AWS_PASSTHROUGH[@]} \
;
}
test.ignores-check-exist-on-force() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 0 \
;
${testmodule} ${_ARGS[@]} --force
cloud.aws.eksctl.iamserviceaccount.check-exists.assert.not.called
}
test.does-not-create-if-exists() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 0 \
;
${testmodule} ${_ARGS[@]}
cloud.aws.eksctl.cli.assert.not.called
}
test.creates-role() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 100 \
;
${testmodule} ${_ARGS[@]}
cloud.aws.eksctl.cli.assert.callstack \
create iamserviceaccount \
--cluster ${_CLUSTER_NAME} \
--namespace ${_NAMESPACE} \
--name ${_SERVICEACCOUNT} \
--role-name ${_ROLE_NAME} \
--override-existing-serviceaccounts \
--approve \
${_EXTRA_ARGS[@]} \
;
}
test.creates-role-on-force() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 0 \
;
${testmodule} ${_ARGS[@]} --force
cloud.aws.eksctl.cli.assert.callstack \
create iamserviceaccount \
--cluster ${_CLUSTER_NAME} \
--namespace ${_NAMESPACE} \
--name ${_SERVICEACCOUNT} \
--role-name ${_ROLE_NAME} \
--override-existing-serviceaccounts \
--approve \
${_EXTRA_ARGS[@]} \
;
}
test.does-not-create-if-mismatched-role() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 200 \
;
${testmodule} ${_ARGS[@]}
cloud.aws.eksctl.cli.assert.not.called
}
test.returns-correct-error-if-mismatched-role() {
unittest.mock cloud.aws.eksctl.iamserviceaccount.check-exists \
--exit-code 200 \
;
${testmodule} ${_ARGS[@]}
[[ $? -eq 2 ]]
}

View File

@ -0,0 +1,9 @@
#
# build 'iamserviceaccount' to enable IAM identity / access control
#
# create the iamserviceaccount
use cloud/aws/eksctl/iamserviceaccount/create
# check whether the iamserviceaccount exists in kubernetes
use cloud/aws/eksctl/iamserviceaccount/check-exists

View File

@ -0,0 +1,20 @@
#####################################################################
use unittest
testmodule=cloud.aws.eksctl.iamserviceaccount
#####################################################################
beforeall() {
use cloud/aws/eksctl/iamserviceaccount
}
#####################################################################
test.provides-create() {
unittest.test.provides ${testmodule}.create
}
test.provides-check-exists() {
unittest.test.provides ${testmodule}.check-exists
}

View File

@ -0,0 +1,35 @@
#####################################################################
${scwryptsmodule}.locals() {
local SERVICEACCOUNT
local NAMESPACE
local ROLE_NAME
}
${scwryptsmodule}() {
local PARSED=0
case $1 in
( --serviceaccount ) PARSED=2; SERVICEACCOUNT=$2 ;;
( --namespace ) PARSED=2; NAMESPACE=$2 ;;
( --role-name ) PARSED=2; ROLE_NAME=$2 ;;
esac
return ${PARSED}
}
${scwryptsmodule}.usage() {
USAGE__options+="
--serviceaccount (required) target k8s:ServiceAccount
--namespace (required) target k8s:Namespace
--role-name (required) name of the IAM role to assign
"
}
${scwryptsmodule}.validate() {
[ "${SERVICEACCOUNT}" ] || echo.error "--serviceaccount is required"
[ "${NAMESPACE}" ] || echo.error "--namespace is required"
[ "${ROLE_NAME}" ] || echo.error "--role-name is required"
}
#####################################################################

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use cloud/aws/rds use cloud/aws/rds
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use cloud/aws/rds use cloud/aws/rds
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use cloud/aws/rds use cloud/aws/rds
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -1,13 +1,7 @@
##################################################################### #####################################################################
DEPENDENCIES+=( DEPENDENCIES+=(docker)
docker REQUIRED_ENV+=(AWS_ACCOUNT AWS_REGION)
)
REQUIRED_ENV+=(
AWS_ACCOUNT
AWS_REGION
)
use cloud/aws/cli use cloud/aws/cli
@ -15,13 +9,13 @@ use cloud/aws/cli
RDS__SELECT_DATABASE() { RDS__SELECT_DATABASE() {
local DATABASES=$(_RDS__GET_AVAILABLE_DATABASES) local DATABASES=$(_RDS__GET_AVAILABLE_DATABASES)
[ ! $DATABASES ] && FAIL 1 'no databases available' [ ! $DATABASES ] && utils.fail 1 'no databases available'
local ID=$(\ local ID=$(\
echo $DATABASES | jq -r '.instance + " @ " + .cluster' \ echo $DATABASES | jq -r '.instance + " @ " + .cluster' \
| FZF 'select a database (instance@cluster)' \ | utils.fzf 'select a database (instance@cluster)' \
) )
[ ! $ID ] && ABORT [ ! $ID ] && user.abort
local INSTANCE=$(echo $ID | sed 's/ @ .*$//') local INSTANCE=$(echo $ID | sed 's/ @ .*$//')
local CLUSTER=$(echo $ID | sed 's/^.* @ //') local CLUSTER=$(echo $ID | sed 's/^.* @ //')
@ -49,24 +43,24 @@ RDS__GET_DATABASE_CREDENTIALS() {
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
do do
case $1 in case $1 in
--print-password ) PRINT_PASSWORD=1 ;; ( --print-password ) PRINT_PASSWORD=1 ;;
* ) ( * )
WARNING "unrecognized argument $1" echo.warning "unrecognized argument $1"
ERRORS+=1 ERRORS+=1
;; ;;
esac esac
shift 1 shift 1
done done
CHECK_ERRORS utils.check-errors --fail
########################################## ##########################################
local DATABASE=$(RDS__SELECT_DATABASE) local DATABASE=$(RDS__SELECT_DATABASE)
[ ! $DATABASE ] && ABORT [ ! $DATABASE ] && user.abort
DB_HOST="$(echo $DATABASE | jq -r '.host')" DB_HOST="$(echo $DATABASE | jq -r '.host')"
[ ! $DB_HOST ] && { ERROR 'unable to find host'; return 2; } [ ! $DB_HOST ] && { echo.error 'unable to find host'; return 2; }
DB_PORT="$(echo $DATABASE | jq -r '.port')" DB_PORT="$(echo $DATABASE | jq -r '.port')"
[ ! $DB_PORT ] && DB_PORT=5432 [ ! $DB_PORT ] && DB_PORT=5432
@ -76,17 +70,17 @@ RDS__GET_DATABASE_CREDENTIALS() {
local AUTH_METHOD=$(\ local AUTH_METHOD=$(\
echo "iam\nsecretsmanager\nuser-input" \ echo "iam\nsecretsmanager\nuser-input" \
| FZF 'select an authentication method' \ | utils.fzf 'select an authentication method' \
) )
[ ! $AUTH_METHOD ] && ABORT [ ! $AUTH_METHOD ] && user.abort
case $AUTH_METHOD in case $AUTH_METHOD in
iam ) _RDS_AUTH__iam ;; ( iam ) _RDS_AUTH__iam ;;
secretsmanager ) _RDS_AUTH__secretsmanager ;; ( secretsmanager ) _RDS_AUTH__secretsmanager ;;
user-input ) _RDS_AUTH__userinput ;; ( user-input ) _RDS_AUTH__userinput ;;
esac esac
[[ $PRINT_PASSWORD -eq 1 ]] && DEBUG "password : $DB_PASS" [[ $PRINT_PASSWORD -eq 1 ]] && echo.debug "password : $DB_PASS"
return 0 return 0
} }
@ -104,19 +98,19 @@ _RDS_AUTH__secretsmanager() {
local CREDENTIALS=$(_RDS__GET_SECRETSMANAGER_CREDENTIALS) local CREDENTIALS=$(_RDS__GET_SECRETSMANAGER_CREDENTIALS)
echo $CREDENTIALS | jq -e '.pass' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.pass' >/dev/null 2>&1 \
&& DB_PASS="$(echo $CREDENTIALS | jq -r '.pass')" && DB_PASS="$(echo $CREDENTIALS | jq -r '.pass')"
echo $CREDENTIALS | jq -e '.password' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.password' >/dev/null 2>&1 \
&& DB_PASS="$(echo $CREDENTIALS | jq -r '.password')" && DB_PASS="$(echo $CREDENTIALS | jq -r '.password')"
echo $CREDENTIALS | jq -e '.user' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.user' >/dev/null 2>&1 \
&& DB_USER=$(echo $CREDENTIALS | jq -r '.user') && DB_USER=$(echo $CREDENTIALS | jq -r '.user')
echo $CREDENTIALS | jq -e '.username' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.username' >/dev/null 2>&1 \
&& DB_USER=$(echo $CREDENTIALS | jq -r '.username') && DB_USER=$(echo $CREDENTIALS | jq -r '.username')
echo $CREDENTIALS | jq -e '.name' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.name' >/dev/null 2>&1 \
&& DB_NAME=$(echo $CREDENTIALS | jq -r '.name') && DB_NAME=$(echo $CREDENTIALS | jq -r '.name')
echo $CREDENTIALS | jq -e '.dbname' >/dev/null 2>&1 \ echo $CREDENTIALS | jq -e '.dbname' >/dev/null 2>&1 \
&& DB_NAME=$(echo $CREDENTIALS | jq -r '.dbname') && DB_NAME=$(echo $CREDENTIALS | jq -r '.dbname')
} }
@ -125,7 +119,7 @@ _RDS__GET_SECRETSMANAGER_CREDENTIALS() {
local ID=$(\ local ID=$(\
AWS secretsmanager list-secrets \ AWS secretsmanager list-secrets \
| jq -r '.[] | .[] | .Name' \ | jq -r '.[] | .[] | .Name' \
| FZF 'select a secret' \ | utils.fzf 'select a secret' \
) )
[ ! $ID ] && return 1 [ ! $ID ] && return 1

View File

@ -1,33 +1,30 @@
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(cli53) DEPENDENCIES+=(cli53)
REQUIRED_ENV+=(AWS_PROFILE) REQUIRED_ENV+=(AWS_PROFILE AWS_ACCOUNT)
##################################################################### #####################################################################
utils.cli53() {
AWS_ACCOUNT=${AWS_ACCOUNT} \
cli53 --profile ${AWS_PROFILE} $@;
}
MAIN() { MAIN() {
local BACKUP_PATH="$SCWRYPTS_OUTPUT_PATH/$ENV_NAME/aws-dns-backup/$(date '+%Y-%m-%d')" local BACKUP_BASE_PATH="${SCWRYPTS_DATA_PATH}/route53-backup/${SCWRYPTS_ENV}"
mkdir -p $BACKUP_PATH >/dev/null 2>&1
local DOMAIN local DOMAIN
local JOBS=() local JOBS=()
for DOMAIN in $(ROUTE53_GET_DOMAINS) for DOMAIN in $(utils.cli53 list | awk '{print $2;}' | sed '1d; s/\.$//')
do do
( STATUS "creating '$BACKUP_PATH/$DOMAIN.txt'" \ (
&& cli53 export --profile $AWS_PROFILE $DOMAIN > "$BACKUP_PATH/$DOMAIN.txt" \ utils.cli53 export ${DOMAIN} > "${BACKUP_BASE_PATH}/${DOMAIN}/$(date '+%Y-%m-%d_%H%M').cli53.txt" \
&& SUCCESS "backed up '$DOMAIN'" \ && echo.success "backed up '${DOMAIN}'" \
|| ERROR "failed to back up '$DOMAIN'" \ || echo.error "failed to back up '${DOMAIN}'" \
) & ) &
JOBS+=$! JOBS+=$!
done done
local P local P
for P in ${JOBS[@]}; do wait $P >/dev/null 2>&1; done for P in ${JOBS[@]}; do wait ${P} &>/dev/null; done
}
##################################################################### echo.reminder "successful backups can be found in '${BACKUP_BASE_PATH}'"
ROUTE53_GET_DOMAINS() {
cli53 list --profile $AWS_PROFILE \
| awk '{print $2;}' \
| sed '1d; s/\.$//'\
;
} }

View File

@ -0,0 +1,79 @@
${scwryptsmodule}.locals() {
local ACCOUNT # parsed/configured AWS_ACCOUNT (use this instead of the env var!)
local REGION # parsed/configured AWS_REGION (use this instead of the env var!)
local AWS_PASSTHROUGH=() # used to forward parsed overrides to cloud.aws.cli calls (e.g. 'cloud.aws.cli ${AWS_PASSTHROUGH[@]} your command')
local AWS=() # used to forward parsed overrides to cloud.aws.cli calls (e.g. '$AWS your command')
# should only be used by cloud/aws/cli
local AWS_EVAL_PREFIX
local AWS_CONTEXT_ARGS=()
}
${scwryptsmodule}() {
local PARSED=0
case $1 in
( --account ) PARSED+=2; ACCOUNT=$2 ;;
( --region ) PARSED+=2; REGION=$2 ;;
esac
return $PARSED
}
${scwryptsmodule}.usage() {
[[ "$USAGE__usage" =~ ' \[...options...\]' ]] || USAGE__usage+=' [...options...]'
USAGE__options+="\n
--account overrides required AWS_ACCOUNT scwrypts env value
--region overrides required AWS_REGION scwrypts env value
"
}
${scwryptsmodule}.validate() {
AWS_CONTEXT_ARGS=(--output json)
[ $ACCOUNT ] || { utils.environment.check AWS_ACCOUNT &>/dev/null && ACCOUNT=$AWS_ACCOUNT; }
[ $ACCOUNT ] \
&& AWS_EVAL_PREFIX+="AWS_ACCOUNT=$ACCOUNT " \
&& AWS_PASSTHROUGH+=(--account $ACCOUNT) \
|| echo.error "missing either --account or AWS_ACCOUNT" \
;
[ $REGION ] || { utils.environment.check AWS_REGION &>/dev/null && REGION=$AWS_REGION; }
[ $REGION ] \
&& AWS_EVAL_PREFIX+="AWS_REGION=$REGION AWS_DEFAULT_REGION=$REGION " \
&& AWS_CONTEXT_ARGS+=(--region $REGION) \
&& AWS_PASSTHROUGH+=(--region $REGION) \
|| echo.error "missing either --region or AWS_REGION" \
;
utils.environment.check AWS_PROFILE &>/dev/null
[ $AWS_PROFILE ] \
&& AWS_EVAL_PREFIX+="AWS_PROFILE=$AWS_PROFILE " \
&& AWS_CONTEXT_ARGS+=(--profile $AWS_PROFILE) \
;
AWS=(cloud.aws.cli ${AWS_PASSTHROUGH[@]})
[ ! $CI ] && {
# non-CI must use PROFILE authentication
[ $AWS_PROFILE ] || echo.error "missing either --profile or AWS_PROFILE";
[[ $AWS_PROFILE =~ ^default$ ]] \
&& echo.warning "it is HIGHLY recommended to NOT use the 'default' profile for aws operations\nconsider using '$USER.$SCWRYPTS_ENV' instead"
}
[ $CI ] && {
# CI can use 'profile' or envvar 'access key' authentication
[ $AWS_PROFILE ] && return 0 # 'profile' preferred
[ $AWS_ACCESS_KEY_ID ] && [ $AWS_SECRET_ACCESS_KEY ] \
&& AWS_EVAL_PREFIX+="AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID " \
&& AWS_EVAL_PREFIX+="AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY " \
&& return 0
echo.error "running in CI, but missing both profile and access-key configuration\n(one AWS authentication method *must* be used)"
}
}

View File

@ -0,0 +1,6 @@
#
# argument parsers for common AWS-CLI arguments
#
# load/override AWS_* variables
use cloud/aws/zshparse/overrides

View File

@ -1,7 +0,0 @@
#!/bin/zsh
use cloud/media-sync
#####################################################################
MAIN() {
MEDIA_SYNC__PULL $@
}

View File

@ -1,7 +0,0 @@
#!/bin/zsh
use cloud/media-sync
#####################################################################
MAIN() {
MEDIA_SYNC__PUSH $@
}

9
zsh/config.global.zsh Normal file
View File

@ -0,0 +1,9 @@
scwrypts.config() {
[ $1 ] || return 1
case $1 in
( python.versions ) echo "3.13\n3.12\n3.11\n3.10" ;;
( nodejs.version ) echo "22.0.0" ;;
( * ) return 1 ;;
esac
}

BIN
zsh/config.user.zsh Normal file

Binary file not shown.

183
zsh/config.zsh Normal file
View File

@ -0,0 +1,183 @@
#####################################################################
### preflight config validation #####################################
#####################################################################
[[ ${__SCWRYPT} -eq 1 ]] && return 0 # avoid config reload if already active
# Apparently MacOS puts ALL of the homebrew stuff inside of a top level git repository
# with bizarre git ignores; so:
# - USE the git root if it's a manual install...
# - UNLESS that git root is just the $(brew --prefix)
__SCWRYPTS_ROOT="$(cd -- "${0:a:h}"; git rev-parse --show-toplevel 2>/dev/null | grep -v "^$(brew --prefix 2>/dev/null)$")"
[ ${__SCWRYPTS_ROOT} ] && [ -d "${__SCWRYPTS_ROOT}" ] \
|| __SCWRYPTS_ROOT="$(echo "${0:a:h}" | sed -n 's|\(share/scwrypts\).*$|\1|p')"
[ ${__SCWRYPTS_ROOT} ] && [ -d "${__SCWRYPTS_ROOT}" ] || {
echo "cannot determine scwrypts root path for current installation; aborting"
exit 1
}
[ -f "${__SCWRYPTS_ROOT}/MANAGED_BY" ] \
&& readonly SCWRYPTS_INSTALLATION_TYPE=$(cat "${__SCWRYPTS_ROOT}/MANAGED_BY") \
|| readonly SCWRYPTS_INSTALLATION_TYPE=manual \
;
#####################################################################
#
# this is like a manual "use" invocation, but the import driver relies
# on this file, so here we go ahead and do it by hand once
#
# equivalent to "use utils"
# (which is unnecessary since utils are ALWAYS loaded with import.driver.zsh)
#
# normally more logic is necessary for "use", but utils.module.zsh is a
# special module which supports direct-sourcing from any zsh environment
#
source "${__SCWRYPTS_ROOT}/zsh/utils/utils.module.zsh"
SCWRYPTS_LIBRARY_LOADED__scwrypts__utils=true
#####################################################################
### scwrypts global configuration ###################################
#####################################################################
readonly SCWRYPTS_CONFIG_PATH="${XDG_CONFIG_HOME:-${HOME}/.config}/scwrypts"
readonly SCWRYPTS_ENV_PATH="${SCWRYPTS_CONFIG_PATH}/environments"
readonly SCWRYPTS_DATA_PATH="${XDG_DATA_HOME:-${HOME}/.local/share}/scwrypts"
readonly SCWRYPTS_STATE_PATH="${XDG_STATE_HOME:-${HOME}/.local/state}/scwrypts"
readonly SCWRYPTS_LOG_PATH="${SCWRYPTS_STATE_PATH}/logs"
[ -d /tmp ] \
&& readonly SCWRYPTS_TEMP_PATH="/tmp/scwrypts/${SCWRYPTS_RUNTIME_ID}" \
|| readonly SCWRYPTS_TEMP_PATH="${XDG_RUNTIME_DIR:-/run/user/${UID}}/scwrypts/${SCWRYPTS_RUNTIME_ID}" \
;
mkdir -p \
"${SCWRYPTS_ENV_PATH}" \
"${SCWRYPTS_LOG_PATH}" \
"${SCWRYPTS_TEMP_PATH}" \
;
DEFAULT_CONFIG="${__SCWRYPTS_ROOT}/zsh/config.user.zsh"
source "${DEFAULT_CONFIG}"
USER_CONFIG_OVERRIDES="${SCWRYPTS_CONFIG_PATH}/config.zsh"
[ ! -f "${USER_CONFIG_OVERRIDES}" ] && {
mkdir -p $(dirname "${USER_CONFIG_OVERRIDES}")
cp "${DEFAULT_CONFIG}" "${USER_CONFIG_OVERRIDES}"
}
source "${USER_CONFIG_OVERRIDES}"
source "${__SCWRYPTS_ROOT}/zsh/config.global.zsh"
#####################################################################
### load groups and plugins #########################################
#####################################################################
SCWRYPTS_GROUPS=()
command -v echo.warning &>/dev/null || WARNING() { echo "echo.warning : $@" >&2; }
command -v echo.error &>/dev/null || ERROR() { echo "echo.error : $@" >&2; return 1; }
command -v utils.fail &>/dev/null || FAIL() { echo.error "${@:2}"; exit $1; }
__SCWRYPTS_GROUP_LOADERS=(
"${__SCWRYPTS_ROOT}/scwrypts.scwrypts.zsh"
)
[ "${GITHUB_WORKSPACE}" ] && [ ! "${SCWRYPTS_GITHUB_NO_AUTOLOAD}" ] && {
SCWRYPTS_GROUP_DIRS+=("${GITHUB_WORKSPACE}")
}
for __SCWRYPTS_GROUP_DIR in ${SCWRYPTS_GROUP_DIRS[@]}
do
[ -d "${__SCWRYPTS_GROUP_DIR}" ] || continue
for __SCWRYPTS_GROUP_LOADER in $(find "${__SCWRYPTS_GROUP_DIR}" -type f -name \*scwrypts.zsh)
do
__SCWRYPTS_GROUP_LOADERS+=("${__SCWRYPTS_GROUP_LOADER}")
done
done
scwrypts.config.group() {
local GROUP_NAME="$1"
local CONFIG_KEY="$2"
[ "$GROUP_NAME" ] && [ "$CONFIG_KEY" ] \
|| return 1
echo ${(P)$(echo SCWRYPTS_GROUP_CONFIGURATION__${GROUP_NAME}__${CONFIG_KEY})}
}
for __SCWRYPTS_GROUP_LOADER in ${__SCWRYPTS_GROUP_LOADERS}
do
__SCWRYPTS_GROUP_LOADER_REALPATH="$(readlink -f -- "${__SCWRYPTS_GROUP_LOADER}")"
[ -f "${__SCWRYPTS_GROUP_LOADER_REALPATH}" ] || {
echo.warning "error loading group '${__SCWRYPTS_GROUP_LOADER}': cannot read file"
continue
}
__SCWRYPTS_GROUP_NAME="$(\
basename -- "${__SCWRYPTS_GROUP_LOADER_REALPATH}" \
| sed -n 's/^\([a-z][a-z0-9_]*[a-z0-9]\).scwrypts.zsh$/\1/p' \
)"
[ "$__SCWRYPTS_GROUP_NAME" ] || {
echo.warning "unable to load group '${__SCWRYPTS_GROUP_LOADER_REALPATH}': invalid group name" >&2
continue
}
[[ $(scwrypts.config.group "$__SCWRYPTS_GROUP_NAME" loaded) =~ true ]] && {
echo.warning "unable to load group '${__SCWRYPTS_GROUP_NAME}': duplicate name"
continue
}
scwryptsgroup="SCWRYPTS_GROUP_CONFIGURATION__${__SCWRYPTS_GROUP_NAME}"
scwryptsgrouproot="$(dirname -- "${__SCWRYPTS_GROUP_LOADER_REALPATH}")"
: \
&& readonly ${scwryptsgroup}__root="${scwryptsgrouproot}" \
&& source "${__SCWRYPTS_GROUP_LOADER_REALPATH}" \
&& SCWRYPTS_GROUPS+=(${__SCWRYPTS_GROUP_NAME}) \
&& readonly ${scwryptsgroup}__loaded=true \
|| echo.warning "error encountered when loading group '${__SCWRYPTS_GROUP_NAME}'" \
;
[[ ! ${__SCWRYPTS_GROUP_NAME} =~ ^scwrypts$ ]] && [ ! "$(scwrypts.config.group ${__SCWRYPTS_GROUP_NAME} zshlibrary)" ] && {
case $(scwrypts.config.group ${__SCWRYPTS_GROUP_NAME} type) in
( zsh )
[ -d "${scwryptsgrouproot}/lib" ] \
&& readonly ${scwryptsgroup}__zshlibrary="${scwryptsgrouproot}/lib" \
|| readonly ${scwryptsgroup}__zshlibrary="${scwryptsgrouproot}" \
;
;;
( '' )
[ -d "${scwryptsgrouproot}/zsh/lib" ] \
&& readonly ${scwryptsgroup}__zshlibrary="${scwryptsgrouproot}/zsh/lib" \
|| readonly ${scwryptsgroup}__zshlibrary="${scwryptsgrouproot}/zsh" \
;
;;
esac
}
done
[[ ${SCWRYPTS_GROUPS[1]} =~ ^scwrypts$ ]] \
|| utils.fail 69 "encountered error when loading essential group 'scwrypts'; aborting"
#####################################################################
### cleanup #########################################################
#####################################################################
unset __SCWRYPTS_ROOT # you should now use '$(scwrypts.config.group scwrypts root)'
unset \
__SCWRYPTS_GROUP_LOADER __SCWRYPTS_GROUP_LOADERS __SCWRYPTS_GROUP_LOADER_REALPATH \
__SCWRYPTS_GROUP_DIR SCWRYPTS_GROUP_DIRS \
__SCWRYPTS_GROUP_NAME \
scwryptsgroup scwryptsgrouproot \
;
__SCWRYPT=1 # arbitrary; indicates currently inside a scwrypt

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -1,4 +1,4 @@
#!/bin/zsh #!/usr/bin/env zsh
use db/postgres use db/postgres
##################################################################### #####################################################################

View File

@ -25,7 +25,7 @@ PG_DUMP() {
STATUS " echo.status "
making backup of : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME making backup of : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME
(compressed) : '$OUTPUT_FILE.dump' (compressed) : '$OUTPUT_FILE.dump'
@ -34,21 +34,21 @@ PG_DUMP() {
" "
: \ : \
&& STATUS "creating compressed backup..." \ && echo.status "creating compressed backup..." \
&& eval PGPASSWORD=$(printf '%q ' "$DB_PASS") psql ${PSQL_ARGS[@]} \ && eval PGPASSWORD=$(printf '%q ' "$DB_PASS") psql ${PSQL_ARGS[@]} \
--format custom \ --format custom \
--file "$OUTPUT_FILE.dump" \ --file "$OUTPUT_FILE.dump" \
--verbose \ --verbose \
&& SUCCESS "completed compressed backup" \ && echo.success "completed compressed backup" \
&& STATUS "creating raw backup..." \ && echo.status "creating raw backup..." \
&& pg_restore -f "$OUTPUT_FILE.raw.sql" "$OUTPUT_FILE.dump" \ && pg_restore -f "$OUTPUT_FILE.raw.sql" "$OUTPUT_FILE.dump" \
&& SUCCESS "completed raw backup" \ && echo.success "completed raw backup" \
&& STATUS "creating single-transaction raw backup..." \ && echo.status "creating single-transaction raw backup..." \
&& { echo "BEGIN;\n"; cat "$OUTPUT_FILE.raw.sql"; echo "\nEND;" } > "$OUTPUT_FILE.sql" \ && { echo "BEGIN;\n"; cat "$OUTPUT_FILE.raw.sql"; echo "\nEND;" } > "$OUTPUT_FILE.sql" \
&& SUCCESS "completed single-transaction raw backup" \ && echo.success "completed single-transaction raw backup" \
|| { ERROR "error creating backup for '$DB_HOST/$DB_NAME' (see above)"; return 1; } || { echo.error "error creating backup for '$DB_HOST/$DB_NAME' (see above)"; return 1; }
SUCCESS " echo.success "
completed backup : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME completed backup : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME
(compressed) : '$OUTPUT_FILE.dump' (compressed) : '$OUTPUT_FILE.dump'
@ -64,11 +64,11 @@ PG_RESTORE() {
local FILE local FILE
POSTGRES__SET_LOGIN_ARGS $@ POSTGRES__SET_LOGIN_ARGS $@
local INPUT_FILE=$(find "$DATA_DIR"/backup.* -type f | FZF 'select database file to restore') local INPUT_FILE=$(find "$DATA_DIR"/backup.* -type f | utils.fzf 'select database file to restore')
[ $INPUT_FILE ] && [ -f "$INPUT_FILE" ] || { [ $INPUT_FILE ] && [ -f "$INPUT_FILE" ] || {
ERROR 'no file selected or missing backup file; aborting' echo.error 'no file selected or missing backup file; aborting'
REMINDER " echo.reminder "
backups must be *.sql or *.dump files starting with the prefix 'backup.' backups must be *.sql or *.dump files starting with the prefix 'backup.'
in the following directory: in the following directory:
@ -80,7 +80,7 @@ PG_RESTORE() {
local RAW=1 local RAW=1
[[ $INPUT_FILE =~ \\.dump$ ]] && RAW=0 [[ $INPUT_FILE =~ \\.dump$ ]] && RAW=0
STATUS " echo.status "
loading backup for : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME loading backup for : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME
file : '$INPUT_FILE' file : '$INPUT_FILE'
@ -88,13 +88,13 @@ PG_RESTORE() {
local EXIT_CODE local EXIT_CODE
[[ $RAW -eq 1 ]] && { [[ $RAW -eq 1 ]] && {
REMINDER " echo.reminder "
loading a backup from a raw sql dump may result in data loss loading a backup from a raw sql dump may result in data loss
make sure your database is ready to accept the database file! make sure your database is ready to accept the database file!
" "
yN 'continue?' || ABORT yN 'continue?' || user.abort
PSQL < "$INPUT_FILE" PSQL < "$INPUT_FILE"
EXIT_CODE=$? EXIT_CODE=$?
@ -110,8 +110,8 @@ PG_RESTORE() {
} }
[[ $EXIT_CODE -eq 0 ]] \ [[ $EXIT_CODE -eq 0 ]] \
&& SUCCESS "finished restoring backup for '$DB_HOST/$DB_NAME'" \ && echo.success "finished restoring backup for '$DB_HOST/$DB_NAME'" \
|| ERROR "error restoring backup for '$DB_HOST/$DB_NAME' (see above)" \ || echo.error "error restoring backup for '$DB_HOST/$DB_NAME' (see above)" \
; ;
return $EXIT_CODE return $EXIT_CODE
@ -120,14 +120,14 @@ PG_RESTORE() {
##################################################################### #####################################################################
POSTGRES__LOGIN_INTERACTIVE() { POSTGRES__LOGIN_INTERACTIVE() {
DEPENDENCIES=(pgcli) CHECK_ENVIRONMENT --optional \ utils.dependencies.check pgcli --optional \
&& COMMAND=pgcli || COMMAND=psql && COMMAND=pgcli || COMMAND=psql
[[ $COMMAND =~ psql ]] && WARNING "using 'psql' instead" [[ $COMMAND =~ psql ]] && echo.warning "using 'psql' instead"
POSTGRES__SET_LOGIN_ARGS $@ POSTGRES__SET_LOGIN_ARGS $@
STATUS " echo.status "
performing login : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME performing login : $DB_USER@$DB_HOST:$DB_PORT/$DB_NAME
working directory : $DATA_DIR working directory : $DATA_DIR
" "
@ -146,25 +146,25 @@ POSTGRES__SET_LOGIN_ARGS() {
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
do do
case $1 in case $1 in
-h | --host ) DB_HOST="$2"; shift 1 ;; ( -h | --host ) DB_HOST="$2"; shift 1 ;;
-p | --port ) DB_PORT="$2"; shift 1 ;; ( -p | --port ) DB_PORT="$2"; shift 1 ;;
-d | --name ) DB_NAME="$2"; shift 1 ;; ( -d | --name ) DB_NAME="$2"; shift 1 ;;
-U | --user ) DB_USER="$2"; shift 1 ;; ( -U | --user ) DB_USER="$2"; shift 1 ;;
-P | --pass ) DB_PASS="$2"; shift 1 ;; ( -P | --pass ) DB_PASS="$2"; shift 1 ;;
--file ) PSQL_FILE="$2"; shift 1 ;; ( --file ) PSQL_FILE="$2"; shift 1 ;;
--data-dir-prefix ) DATA_DIR_PREFIX="$2"; shift 1 ;; ( --data-dir-prefix ) DATA_DIR_PREFIX="$2"; shift 1 ;;
* ) PSQL_ARGS+=($1) ;; ( * ) PSQL_ARGS+=($1) ;;
esac esac
shift 1 shift 1
done done
[ $PSQL_FILE ] && [ ! -f "$PSQL_FILE" ] \ [ $PSQL_FILE ] && [ ! -f "$PSQL_FILE" ] \
&& ERROR "no such file available:\n'$PSQL_FILE'" && echo.error "no such file available:\n'$PSQL_FILE'"
CHECK_ERRORS utils.check-errors --fail
########################################## ##########################################

View File

@ -1,9 +1,9 @@
#!/bin/zsh #!/usr/bin/env zsh
use db/postgres use db/postgres
##################################################################### #####################################################################
MAIN() { MAIN() {
WARNING " \nthis function is in a beta state\n " echo.warning " \nthis function is in a beta state\n "
local _PASS _ARGS=() local _PASS _ARGS=()
POSTGRES__SET_LOGIN_ARGS $@ POSTGRES__SET_LOGIN_ARGS $@
@ -15,27 +15,27 @@ MAIN() {
cd $SQL_DIR cd $SQL_DIR
[[ $(ls "*.sql" 2>&1 | wc -l) -eq 0 ]] && { [[ $(ls "*.sql" 2>&1 | wc -l) -eq 0 ]] && {
ERROR "you haven't made any SQL commands yet" echo.error "you haven't made any SQL commands yet"
REMINDER "add '.sql' files here: '$SQL_DIR/'" echo.reminder "add '.sql' files here: '$SQL_DIR/'"
return 1 return 1
} }
[ ! $INPUT_FILE ] && INPUT_FILE=$(FZF 'select a sql file to run') [ ! $INPUT_FILE ] && INPUT_FILE=$(utils.fzf 'select a sql file to run')
[ ! $INPUT_FILE ] && ABORT [ ! $INPUT_FILE ] && user.abort
[ ! -f "$INPUT_FILE" ] && FAIL 2 "no such sql file '$SQL_DIR/$INPUT_FILE'" [ ! -f "$INPUT_FILE" ] && utils.fail 2 "no such sql file '$SQL_DIR/$INPUT_FILE'"
STATUS "loading '$INPUT_FILE' preview..." echo.status "loading '$INPUT_FILE' preview..."
LESS "$INPUT_FILE" LESS "$INPUT_FILE"
STATUS "login : $_USER@$_HOST:$_PORT/$_NAME" echo.status "login : $_USER@$_HOST:$_PORT/$_NAME"
STATUS "command : '$INPUT_FILE'" echo.status "command : '$INPUT_FILE'"
yN 'run this command?' || ABORT yN 'run this command?' || user.abort
STATUS "running '$INPUT_FILE'" echo.status "running '$INPUT_FILE'"
PSQL < $INPUT_FILE \ PSQL < $INPUT_FILE \
&& SUCCESS "finished running '$INPUT_FILE'" \ && echo.success "finished running '$INPUT_FILE'" \
|| FAIL 3 "something went wrong running '$INPUT_FILE' (see above)" || utils.fail 3 "something went wrong running '$INPUT_FILE' (see above)"
} }

View File

@ -1,13 +1,29 @@
#!/bin/zsh #!/usr/bin/env zsh
DEPENDENCIES+=(docker) DEPENDENCIES+=(docker)
##################################################################### #####################################################################
MAIN() { MAIN() {
WARNING 'this will prune all docker resources from the current machine' local RESOURCES=(
WARNING 'pruned resources are PERMANENTLY DELETED' container
yN 'continue?' || return 1 image
volume
system
)
SUCCESS "CONTAINER : $(docker container prune -f 2>/dev/null | tail -n 1)" echo.warning "
SUCCESS "IMAGE : $(docker image prune -f 2>/dev/null | tail -n 1)" this will prune the following docker resources from the
SUCCESS "VOLUME : $(docker volume prune -f 2>/dev/null | tail -n 1)" current machine:
(${RESOURCES[@]})
pruned resources are PERMANENTLY DELETED
"
utils.yN 'continue?' || utils.abort
echo.success "$(
for RESOURCE in ${RESOURCES[@]}
do
echo "${RESOURCE}^:^$(docker ${RESOURCE} prune -f 2>/dev/null | tail -n 1)"
done | column -ts '^'
)"
} }

View File

@ -1,6 +1,6 @@
#!/bin/zsh #!/usr/bin/env zsh
##################################################################### #####################################################################
MAIN() { MAIN() {
SUCCESS 'Hello, World!' echo.success 'Hello, World!'
} }

View File

@ -1,9 +1,13 @@
#!/bin/zsh #!/usr/bin/env zsh
use helm
use scwrypts
##################################################################### #####################################################################
MAIN() { use helm
unset USAGE
HELM__TEMPLATE__GET $@ #####################################################################
}
helm.zshparse.usage
helm.get-template.parse.usage
#####################################################################
MAIN() { helm.get-template $@; }

View File

@ -0,0 +1,135 @@
#####################################################################
use scwrypts/get-realpath
use helm/zshparse
DEPENDENCIES+=(helm)
#####################################################################
${scwryptsmodule}() {
local PARSERS=(helm.zshparse)
eval "$(utils.parse.autosetup)"
##########################################
local HELM_LINT_ARGS=(${HELM_ARGS[@]})
[[ ${USE_CHART_ROOT} =~ false ]] \
&& HELM_ARGS+=(--show-only "$(echo "${TEMPLATE_FILENAME}" | sed "s|^${CHART_ROOT}/||")")
local TEMPLATE_OUTPUT DEBUG_OUTPUT
utils.io.capture TEMPLATE_OUTPUT DEBUG_OUTPUT \
helm template "${CHART_ROOT}" ${HELM_ARGS[@]} --debug \
;
local EXIT_CODE
[ "${TEMPLATE_OUTPUT}" ] && EXIT_CODE=0 || EXIT_CODE=1
case ${OUTPUT_MODE} in
( raw )
[[ ${EXIT_CODE} -eq 0 ]] \
&& echo "${TEMPLATE_OUTPUT}" | grep -v '^# Source:.*$' \
|| echo "${DEBUG_OUTPUT}" >&2 \
;
;;
( color )
[[ ${EXIT_CODE} -eq 0 ]] \
&& echo "${TEMPLATE_OUTPUT}" | bat --language yaml --color always \
|| echo "${DEBUG_OUTPUT}" >&2 \
;
;;
( debug )
[[ ${EXIT_CODE} -eq 0 ]] \
&& KUBEVAL_RAW=$(
echo "${TEMPLATE_OUTPUT}" \
| kubeval --schema-location https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master \
) \
|| KUBEVAL_RAW='no template output; kubeval skipped'
echo "
${TEMPLATE_OUTPUT}
---
debug: |\n$(echo ${DEBUG_OUTPUT} | sed 's/^/ /g')
kubeval: |\n$(echo ${KUBEVAL_RAW} | sed 's/^/ /g')
lint: |\n$(helm lint "${CHART_ROOT}" ${HELM_LINT_ARGS[@]} 2>&1 | sed 's/^/ /g')
" | sed 's/^ \+//; 1d; $d'
;;
esac
return ${EXIT_CODE}
}
#####################################################################
${scwryptsmodule}.parse() {
local PARSED=0
case $1 in
( --colorize ) PARSED=1; OUTPUT_MODE=color ;;
( --raw ) PARSED=1; OUTPUT_MODE=raw ;;
( --debug ) PARSED=1; OUTPUT_MODE=debug ;;
( --update ) PARSED=1; UPDATE_DEPENDENCIES=true ;;
esac
return ${PARSED}
}
${scwryptsmodule}.parse.locals() {
local OUTPUT_MODE=default
local UPDATE_DEPENDENCIES=false
}
${scwryptsmodule}.parse.usage() {
USAGE__description="
Smart helm-template generator which auto-detects the chart
and sample values for testing and developing helm charts.
"
local DEFAULT
utils.dependencies.check bat &>/dev/null \
&& DEFAULT=color || DEFAULT=raw
USAGE__options+="
--colorize $([[ ${DEFAULT} =~ color ]] && printf '(default) ')use 'bat' to colorize output
--raw $([[ ${DEFAULT} =~ raw ]] && printf '(default) ')remove scwrypts-added fluff and only output helm template details
--debug debug template with kubeval and helm-lint
--update update dependencies before generating template output
"
}
${scwryptsmodule}.parse.validate() {
case ${OUTPUT_MODE} in
( default )
utils.dependencies.check bat &>/dev/null \
&& OUTPUT_MODE=color \
|| OUTPUT_MODE=raw \
;
;;
( color )
utils.dependencies.check bat || ((ERRORS+=1))
;;
( debug )
utils.dependencies.check kubeval \
|| echo.error 'kubeval is required for --debug'
;;
esac
case ${UPDATE_DEPENDENCIES} in
( false ) ;;
( true )
use helm/update-dependencies
helm.update-dependencies --template-filename "${TEMPLATE_FILENAME}" &>/dev/null \
|| echo.error 'failed to update dependencies'
;;
esac
}

15
zsh/helm/helm.module.zsh Normal file
View File

@ -0,0 +1,15 @@
#
# helm template testing and generation helpers
#
# ensures default values are injected from local Chart dependencies
use helm/update-dependencies
# template generation
use helm/get-template
# shared argument parser
use helm/zshparse

View File

@ -1,9 +1,13 @@
#!/bin/zsh #!/usr/bin/env zsh
use helm
use scwrypts
##################################################################### #####################################################################
MAIN() { use helm
unset USAGE
HELM__DEPENDENCY__UPDATE $@ #####################################################################
}
helm.zshparse.usage
helm.update-dependencies.parse.usage
#####################################################################
MAIN() { helm.update-dependencies $@; }

View File

@ -0,0 +1,31 @@
#####################################################################
use helm/zshparse
DEPENDENCIES+=(helm)
#####################################################################
${scwryptsmodule}() {
local PARSERS=(helm.zshparse)
eval "$(utils.parse.autosetup)"
##########################################
echo.status "updating helm dependencies for '${CHART_ROOT}'" \
helm dependency update "${CHART_ROOT}" \
&& echo.success "helm chart dependencies updated" \
|| echo.error "unable to update helm chart dependencies (see above)" \
|| return 1
}
#####################################################################
${scwryptsmodule}.parse() { return 0; }
${scwryptsmodule}.parse.usage() {
USAGE__description='
Auto-detect chart and build dependencies for any file within a helm chart.
'
}

View File

@ -0,0 +1,119 @@
#####################################################################
DEPENDENCIES+=(yq)
REQUIRED_ENV+=()
#####################################################################
${scwryptsmodule}() {
local USAGE="
Smart helm-detection / validator which determines the helm
chart root and other details given a particular filename.
You must set TEMPLATE_FILENAME, and this function will verify
the template is part of a helm chart and set up the following
variables:
- CHART_ROOT : the fully qualified path to the directory
containing Chart.yaml
- CHART_NAME : Chart.yaml > .name
- USE_CHART_ROOT (true/false)
true : operations should use/output the entire chart
false : operations should use/output a single template
- HELM_ARGS : an array of arguments which apply to any 'helm'
command
"
[ "${TEMPLATE_FILENAME}" ] && [ -f "${TEMPLATE_FILENAME}" ] \
|| echo.error 'must provide a template filename' \
|| return 1
##########################################
CHART_ROOT="$(helm.validate.get-chart-root)"
[ ${CHART_ROOT} ] && [ -d "${CHART_ROOT}" ] \
|| echo.error 'unable to determine helm root; is this a helm template file?' \
|| return 1
##########################################
CHART_NAME=$(utils.yq -r .name "${CHART_ROOT}/Chart.yaml")
##########################################
USE_CHART_ROOT=false
case "${TEMPLATE_FILENAME}" in
( *values.*.yaml | *tests/*.yaml )
HELM_ARGS+=(--values ${TEMPLATE_FILENAME})
USE_CHART_ROOT=true
;;
( *.tpl )
USE_CHART_ROOT=true
;;
esac
[[ $(dirname -- "${TEMPLATE_FILENAME}") =~ ^${CHART_ROOT}$ ]] \
&& USE_CHART_ROOT=true
##########################################
HELM_ARGS=($(helm.validate.get-default-values-args) ${HELM_ARGS[@]})
##########################################
return 0
}
${scwryptsmodule}.get-chart-root() {
local SEARCH_DIR=$(dirname -- "${TEMPLATE_FILENAME}")
while [[ ! ${SEARCH_DIR} =~ ^/$ ]]
do
[ -f "${SEARCH_DIR}/Chart.yaml" ] \
&& echo "${SEARCH_DIR}" \
&& return 0 \
;
SEARCH_DIR="$(dirname -- "${SEARCH_DIR}")"
done
return 1
}
${scwryptsmodule}.get-default-values-args() {
local F
local VALUES_FILES_ORDER=(
values.yaml # the default values of the chart
tests/default.yaml # a template test which provides any required values not included in the default values
)
local LOCAL_DEPENDENCY_CHART LOCAL_CHART_ROOT
for LOCAL_DEPENDENCY_CHART in $(\
cat "${CHART_ROOT}/Chart.yaml" \
| utils.yq -r '.dependencies[] | .repository' \
| grep '^file://' \
| sed 's|file://||' \
)
do
[[ "${LOCAL_DEPENDENCY_CHART}" =~ ^[/~] ]] \
&& LOCAL_CHART_ROOT="${LOCAL_DEPENDENCY_CHART}" \
|| LOCAL_CHART_ROOT=$(readlink -f -- "${CHART_ROOT}/${LOCAL_DEPENDENCY_CHART}") \
;
for F in ${VALUES_FILES_ORDER[@]}
do
[ -f "${LOCAL_CHART_ROOT}/${F}" ] \
&& echo --values "${LOCAL_CHART_ROOT}/${F}"
done
done
local HELM_VALUES_ARGS=()
for F in ${VALUES_FILES_ORDER[@]}
do
[ -f "${CHART_ROOT}/${F}" ] \
&& echo --values "${CHART_ROOT}/${F}"
done
}

View File

@ -0,0 +1,54 @@
#####################################################################
use helm/validate
use scwrypts/get-realpath
#####################################################################
${scwryptsmodule}() {
local PARSED=0
case $1 in
( -t | --template-filename )
PARSED=2
TEMPLATE_FILENAME="$(scwrypts.get-realpath "$2")"
;;
esac
return ${PARSED}
}
${scwryptsmodule}.locals() {
local TEMPLATE_FILENAME
local ARGS=()
# configured by helm.validate
local CHART_NAME
local CHART_ROOT
local HELM_ARGS=()
}
${scwryptsmodule}.usage() {
USAGE__options+='
-t, --template-filename path to a template/*.yaml file of a helm chart
'
USAGE__args+='
\$@ additional args are forwarded to helm
'
}
${scwryptsmodule}.validate() {
helm.validate || return 1
HELM_ARGS+=(${ARGS[@]})
echo.debug "
template filename : ${TEMPLATE_FILENAME}
chart name : ${CHART_NAME}
chart root : ${CHART_ROOT}
helm args : ${HELM_ARGS[@]}
"
}
#####################################################################

340
zsh/import.driver.zsh Normal file
View File

@ -0,0 +1,340 @@
command -v use use.is-loaded use.get-library-root &>/dev/null && return 0
###################################################################
# #
# usage: use [OPTIONS ...] zsh/module/path #
# #
###################################################################
# #
# OPTIONS: #
# #
# -g, --group lookup library root from friendly group #
# name (requires configuration) #
# (default: scwrypts) #
# #
# -r, --library-root fully qualified path to a library root #
# #
# --check-environment check environment immediately rather than #
# wait for downstream call to #
# utils.check-environment #
# #
# #
# ZSHIMPORT_USE_CACHE (true|false; default: true) #
# setting this to false will always require direct, source files #
# #
# Allows for import-style library loading in zsh. No matter what #
# scwrypt is run, this function (and required helpers) are *also* #
# loaded, ensuring that 'use' is always available in scwrypts #
# context. #
# #
# #
# Friendly group-names can be configured by setting the variable #
# 'SCWRYPTS_GROUP_CONFIGRUATION__<group-name>__root' to the fully #
# qualified path to the root directory of the modules library. #
# #
# #
###################################################################
# I have no idea why, but CircleCI obliterates this env var in particular
# every time. Just going to force it like this for now
[ $CIRCLECI ] && export ZSHIMPORT_USE_CACHE=false
[ ${ZSHIMPORT_USE_CACHE} ] || export ZSHIMPORT_USE_CACHE=true
[[ ${ZSHIMPORT_USE_CACHE} =~ true ]] && {
command -v jo jq sha1sum &>/dev/null || {
echo.warning "missing utilities prevents import cache"
export ZSHIMPORT_USE_CACHE=false
}
}
[ ${ZSHIMPORT_CACHE_DIR} ] || export ZSHIMPORT_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/zshimport"
source "${0:a:h}/config.zsh"
use() {
local SCWRYPTS_LIBRARY SCWRYPTS_LIBRARY_ROOT SCWRYPTS_LIBRARY_GROUP
local DEFER_ENVIRONMENT_CHECK=true
local ONLY_OUTPUT_METADATA=false
local ONLY_GENERATE_CACHE=false
while [[ $# -gt 0 ]]
do
case $1 in
( -g | --group )
[ "${SCWRYPTS_LIBRARY_ROOT}" ] && echo.error 'specify only one of {(-g), (-r)}'
SCWRYPTS_LIBRARY_GROUP=$2
shift 1
;;
( -r | --library-root )
[ "${SCWRYPTS_LIBRARY_GROUP}" ] && echo.error 'specify only one of {(-g), (-r)}'
SCWRYPTS_LIBRARY_ROOT=$2
shift 1
;;
( -c | --check-environment )
DEFER_ENVIRONMENT_CHECK=false
;;
( --meta )
ONLY_OUTPUT_METADATA=true
;;
( --generate-cache )
ONLY_GENERATE_CACHE=true
;;
( * )
[ ! "${SCWRYPTS_LIBRARY}" ] \
&& SCWRYPTS_LIBRARY=$1 \
|| echo.error 'too many arguments; expected exactly 1 argument' \
;;
esac
shift 1
done
[ ! "${SCWRYPTS_LIBRARY}" ] && echo.error 'no library specified for import'
: \
&& [ ! "${SCWRYPTS_LIBRARY_GROUP}" ] \
&& [ ! "${SCWRYPTS_LIBRARY_ROOT}" ] \
&& SCWRYPTS_LIBRARY_GROUP=scwrypts
# bail ASAP, check errors later
use.is-loaded && [[ ${ONLY_OUTPUT_METADATA} =~ false ]] && [[ ${ONLY_GENERATE_CACHE} =~ false ]] && return 0
[ ! "${SCWRYPTS_LIBRARY_ROOT}" ] && SCWRYPTS_LIBRARY_ROOT="$(use.get-scwrypts-library-root)"
[ ! "${SCWRYPTS_LIBRARY_ROOT}" ] && echo.error "unable to determine library root from group name '${SCWRYPTS_LIBRARY_GROUP}'"
#####################################################################
local LIBRARY_FILE LIBRARY_FILE_TEMP CACHE_FILE
[ ! "${LIBRARY_FILE}" ] && {
LIBRARY_FILE_TEMP="${SCWRYPTS_LIBRARY_ROOT}/${SCWRYPTS_LIBRARY}.module.zsh"
[ -f "${LIBRARY_FILE_TEMP}" ] && {
LIBRARY_FILE="${LIBRARY_FILE_TEMP}"
CACHE_FILE="${SCWRYPTS_LIBRARY}.module.zsh"
}
}
[ ! "${LIBRARY_FILE}" ] && { # "group" library reference
LIBRARY_FILE_TEMP="${SCWRYPTS_LIBRARY_ROOT}/${SCWRYPTS_LIBRARY}/$(basename -- "${SCWRYPTS_LIBRARY}").module.zsh"
[ -f "${LIBRARY_FILE_TEMP}" ] && {
LIBRARY_FILE="${LIBRARY_FILE_TEMP}"
CACHE_FILE="${SCWRYPTS_LIBRARY}/$(basename -- "${SCWRYPTS_LIBRARY}").module.zsh"
}
}
[ "${LIBRARY_FILE}" ] \
|| echo.error "no such library '${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}'"
#####################################################################
local LIBRARY_HASH LIBRARY_CACHE_DIR LIBRARY_CACHE_FILE
#####################################################################
utils.check-errors || {
((IMPORT_ERRORS+=1))
return 1
}
#####################################################################
local SCWRYPTS_MODULE_BEFORE=${scwryptsmodule}
[[ ${SCWRYPTS_LIBRARY_GROUP} =~ ^scwrypts$ ]] \
&& export scwryptsmodule="$(echo "${SCWRYPTS_LIBRARY}" | sed 's|/|.|g')" \
|| export scwryptsmodule="${SCWRYPTS_LIBRARY_GROUP}.$(echo "${SCWRYPTS_LIBRARY}" | sed 's|/|.|g')" \
;
[[ ${ONLY_OUTPUT_METADATA} =~ true ]] && {
use.get-metadata
return 0
}
case "${ZSHIMPORT_USE_CACHE}" in
( false ) ;;
( true )
LIBRARY_HASH="$(use.compute-scwrypts-library-hash)"
LIBRARY_CACHE_DIR="${ZSHIMPORT_CACHE_DIR}/${SCWRYPTS_LIBRARY_GROUP}-${LIBRARY_HASH}"
LIBRARY_CACHE_FILE="${LIBRARY_CACHE_DIR}/${CACHE_FILE}"
[ "${LIBRARY_HASH}" ] && [ "${LIBRARY_CACHE_DIR}" ] && [ "${LIBRARY_CACHE_FILE}" ] \
|| echo.error "error when computing library hash for ${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}"
use.generate-cache \
|| echo.error "error generating cache for ${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}" \
|| return 1
;;
esac
[[ ${ONLY_GENERATE_CACHE} =~ true ]] && {
cat "${LIBRARY_CACHE_FILE}" | grep .
return $?
}
case ${ZSHIMPORT_USE_CACHE} in
( true )
source "${LIBRARY_CACHE_FILE}" || {
((IMPORT_ERRORS+=1))
echo.error "import error for '${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}'"
echo.debug "cache : '${LIBRARY_CACHE_FILE}'"
return 1
}
;;
( false )
source "${LIBRARY_FILE}" || {
((IMPORT_ERRORS+=1))
echo.error "import error for '${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}'"
export scwryptsmodule=${SCWRYPTS_MODULE_BEFORE}
return 1
}
[[ ${DEFER_ENVIRONMENT_CHECK} =~ false ]] && {
utils.check-environment || {
((IMPORT_ERRORS+=1))
echo.error "import error for '${SCWRYPTS_LIBRARY_GROUP}/${SCWRYPTS_LIBRARY}'"
return 1
}
}
use.is-loaded --set
[[ ${SCWRYPTS_MODULE_BEFORE} ]] \
&& export scwryptsmodule=${SCWRYPTS_MODULE_BEFORE} \
|| unset scwryptsmodule \
;
;;
esac
return 0
}
use.get-scwrypts-library-root() {
local VARIABLE_NAME="SCWRYPTS_LIBRARY_ROOT__${SCWRYPTS_LIBRARY_GROUP}"
echo "${(P)VARIABLE_NAME}" | grep . && return 0
##########################################
local ROOT
ROOT="$(scwrypts.config.group "${SCWRYPTS_LIBRARY_GROUP}" zshlibrary)"
[ "${ROOT}" ] && eval ${VARIABLE_NAME}="${ROOT}" && echo "${ROOT}" && return 0
##########################################
local GROUP_ROOT="$(scwrypts.config.group "${SCWRYPTS_LIBRARY_GROUP}" root)"
local GROUP_TYPE="$(scwrypts.config.group "${SCWRYPTS_LIBRARY_GROUP}" type)"
[[ ${GROUP_TYPE} =~ zsh ]] \
&& ROOT="${GROUP_ROOT}/lib" \
|| ROOT="${GROUP_ROOT}/zsh/lib" \
;
[ -d "${ROOT}" ] || ROOT="$(dirname -- "${ROOT}")"
[ "${ROOT}" ] && [ -d "${ROOT}" ] \
|| echo.error "unable to determine library root" \
|| return 1
eval ${VARIABLE_NAME}="${ROOT}"
echo "${ROOT}"
}
use.compute-scwrypts-library-hash() {
LC_ALL=POSIX find "${SCWRYPTS_LIBRARY_ROOT}" -type f -name \*.module.zsh -print0 \
| sort -z \
| xargs -0 sha1sum \
| sha1sum \
| awk '{print $1;}' \
;
}
use.is-loaded() {
local VARIABLE_NAME="SCWRYPTS_LIBRARY_LOADED__${SCWRYPTS_LIBRARY_GROUP}__$(echo ${SCWRYPTS_LIBRARY} | sed 's|[/-]|_|g')"
[[ $1 =~ ^--set$ ]] && eval ${VARIABLE_NAME}=true
[[ ${(P)VARIABLE_NAME} =~ true ]]
}
use.get-metadata() {
jo \
LIBRARY_FILE="${LIBRARY_FILE}" \
SCWRYPTS_LIBRARY_GROUP="${SCWRYPTS_LIBRARY_GROUP}" \
SCWRYPTS_LIBRARY="${SCWRYPTS_LIBRARY}" \
scwryptsmodule="${scwryptsmodule}" \
;
}
#####################################################################
use.generate-cache() {
[ "${LIBRARY_CACHE_FILE}" ] || return 1
[ -f "${LIBRARY_CACHE_FILE}" ] && return 0
##########################################
mkdir -p -- "$(dirname -- "${LIBRARY_CACHE_FILE}")"
local IMPORTS=":${LIBRARY_FILE}:"
use.generate-cache.create-import $(use.get-metadata) > "${LIBRARY_CACHE_FILE}"
local METADATA SUBFILE SUBMODULE
while $(grep -q '^use\s' "${LIBRARY_CACHE_FILE}")
do
NEXT_IMPORT=$(grep '^use\s' ${LIBRARY_CACHE_FILE} | head -n1)
METADATA="$(eval "${NEXT_IMPORT} --meta")"
[ "${METADATA}" ] \
|| echo.error "error getting metadata for '${NEXT_IMPORT}'" \
|| return 1
SUBFILE="$(echo "${METADATA}" | jq -r .LIBRARY_FILE)"
[[ "${IMPORTS}" =~ ":${SUBFILE}:" ]] && {
grep -v "^${NEXT_IMPORT}$" "${LIBRARY_CACHE_FILE}" > "${LIBRARY_CACHE_FILE}.tmp"
mv "${LIBRARY_CACHE_FILE}.tmp" "${LIBRARY_CACHE_FILE}"
continue
}
IMPORTS+="${SUBFILE}:"
SUBMODULE="$(echo "${METADATA}" | jq -r .scwryptsmodule)"
use.generate-cache.create-import "${METADATA}" > "${LIBRARY_CACHE_FILE}.subfile"
{
sed -n '0,/^use\s/p' ${LIBRARY_CACHE_FILE} | sed '$d'
echo '() {'
cat "${LIBRARY_CACHE_FILE}.subfile"
echo '}'
sed -n '/^use\s/,$p' ${LIBRARY_CACHE_FILE} | sed '1d'
} > "${LIBRARY_CACHE_FILE}.tmp"
mv "${LIBRARY_CACHE_FILE}.tmp" "${LIBRARY_CACHE_FILE}"
rm "${LIBRARY_CACHE_FILE}.subfile"
done
}
use.generate-cache.create-import() {
local METADATA="$1"
local FILENAME="$(echo "${METADATA}" | jq -r .LIBRARY_FILE)"
local SCWRYPTSMODULE="$(echo "${METADATA}" | jq -r .scwryptsmodule)"
local GROUP="$(echo "${METADATA}" | jq -r .SCWRYPTS_LIBRARY_GROUP)"
local LIBRARY="$(echo "${METADATA}" | jq -r .SCWRYPTS_LIBRARY | sed 's|[/-]|_|g')"
local IS_LOADED_VARIABLE="SCWRYPTS_LIBRARY_LOADED__${GROUP}__${LIBRARY}"
echo "[[ \$${IS_LOADED_VARIABLE} =~ true ]] && return 0"
echo "export scwryptsmodule=${SCWRYPTSMODULE}"
sed "/^use\\s/aexport scwryptsmodule=${SCWRYPTSMODULE}" "${FILENAME}"
echo "${IS_LOADED_VARIABLE}=true"
echo "unset scwryptsmodule"
}

Some files were not shown because too many files have changed in this diff Show More