Compare commits

...

33 Commits
v4.0.0 ... main

Author SHA1 Message Date
7f14edd039 v5.0.0
=====================================================================

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
2025-05-24 08:10:33 -06:00
1b4060dd1c v4.4.4
=====================================================================

- improved compatibility of scwrypts.plugin.zsh; no longer relies on
  user default fzf configuration

- added temporary downgrade of fakeroot to archlinux publish CI due
  to a confirmed bug in v1.35 and v1.36
2024-08-23 11:43:42 -06:00
6aba11d0be v4.4.3
=====================================================================

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

- fixed a(nother) bug which emerged from the latest version of github's
  actions/runner-images; zx is now forcibly managed by the runner, so
  install scripts must be skipped in CI
2024-05-14 12:36:44 -06:00
a945daeecc v4.4.2
=====================================================================

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

- fixed a bug which emerged from the latest version of github's
  actions/runner-images
2024-05-10 14:14:08 -06:00
2ef20860c4 v4.4.1
=====================================================================

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

- scwrypts now run when using --group and --type options without --name
- fixed spacing issues in scwrypts --help
- added missing = sign in config

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

- zsh completion now loads with scwrypts.plugin.zsh (requires compdef)
2024-05-09 16:46:57 -06:00
3fe01a7263 v4.4.0
=====================================================================

Increased non-scwrypts-runtime compatibility and improved clarity
in user environments after sourcing the scwrypts.plugin.zsh.

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

- added experimental support for --output json
- added 'scwrypts --list-groups' to output the SCWRYPTS_GROUPS value
- added 'scwrypts --config' to be `eval`-ed in non-scwrypts-runtime zsh

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

- removed config variables which pertained to old scwrypts

- removed deprecated --no-log

- cleaned up environment requirements and improved import saftey for
  scwrypts.plugin.zsh; scwrypts is now *required* on $PATH in order to work

- refactored group configuration to match external group configuration
  (configuration now in scwrypts.scwrypts.zsh rather than zsh/lib/config.group.zsh)

- plugins/kubectl now forces an unalias of `f` (for fluxcd) on load

- the 'use' command now supports the '-c' short flag for ease of quick
  use

- upgraded max supported python version to 3.12; dropped support for
  python 3.9 (>3.10 required)

- remove old references to SCWRYPTS_ROOT in favour of SCWRYPTS_ROOT__scwrypts

- SCWRYPTS_LOG_LEVEL setting is now forwarded when using the SCWRYPTS__RUN
  meta execution function
2024-05-08 23:11:09 -06:00
4146a0d297 v4.3.1
=====================================================================

- MacOS does some weird stuff with the homebrew prefix apparently
2024-04-16 10:39:19 -06:00
8f3e862086 v4.3.0
=====================================================================

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

- updated python scwrypts API to use latest pattern established in the
  nodejs library

- SCWRYPTS_ROOT__scwrypts is now supports loading with each run and
  detects managed installations vs manual installations; this now means
  SCWRYPTS_ROOT can no longer be injected to scwrypts (this was a v2
  legacy support thing and probably does not apply to you)

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

- pypi/scwrypts )
     added 'get_generator' API to testing utilities to provide a nice
     way to include default generator options

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

- scwrypts groups which use a required environment name regex no longer
  load specialized static files outside of the required environments.
2024-04-16 09:17:40 -06:00
aefd575539 v4.2.6
=====================================================================

- OK, I trust that the AUR testing pipeline will only publish proper
  builds which match all the correct versions and have all the correct
  checksums... so it's enabled now
2024-04-14 02:26:56 -06:00
26992d2f01 v4.2.5
=====================================================================

- Remove testing artifact from AUR publish
2024-04-14 02:21:40 -06:00
487fa65d38 v4.2.4
=====================================================================

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

- Reinclude publish credentials for pypi and npm
2024-04-14 02:17:16 -06:00
3ff44f8e58 v4.2.3
=====================================================================

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

- AUR test-before-publish requires usage of the CIRCLE_TAG rather than
  the CIRCLE_BRANCH
2024-04-14 02:15:31 -06:00
f0a6b16a0c v4.2.2
=====================================================================

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

- Ensure CircleCI automatic publishes actually trigger on expected tags
2024-04-14 02:04:44 -06:00
74fe48cc4a v4.2.1
=====================================================================

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

- Automatic test-stable rolling release to AUR
2024-04-14 01:59:01 -06:00
6fe5b8e26a v4.2.0
=====================================================================

DEPRECATION REMINDER!

The following functions and APIS are no longer available:

- FZF_HEAD : use FZF as a drop-in replacement
- FZF_TAIL : use FZF as a drop-in replacement

- SCWRYPTS__GET_RUNSTRING__zsh_v3 : upgrade to zsh/scwrypts v4
                                    runstrings

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

- removed legacy INFO references in plugins/kubectl

- the zsh-builder plugin (CTRL+Y) now show clean helpdocs (no more
  visual terminal artifacts)

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

- differentiate manual / managed versions of scwrypts in versioning;
  this will prevent 'scwrypts --update' from operating against managed
  installations

- created SCWRYPTS__GET_RUNSTRING__zsh__generic to provide an easy way
  to write custom runstrings; this will do all the nice things default
  zsh/scwrypts v4 do (multiflag separation, help flag injection, USAGE
  definitions, and required MAIN() {} wrapper).
2024-04-13 17:34:22 -06:00
3d1eb9e03d v4.1.5
=====================================================================

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

- corrected some output being piped to stdin during EKS login
2024-03-14 10:02:45 -06:00
bd554f1460 v4.1.4
=====================================================================

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

- removed an error message when using installation through aur
2024-03-11 20:55:14 -06:00
768bd1444e v4.1.3
=====================================================================

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

- added one more protection against sneaky BSD utils (fixes #1)

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

- added a GH action to automatically create release binaries
- added SCWRYPTS_ROOT when installed from aur package build
2024-03-11 20:14:44 -06:00
4ccb79f1e4 v4.1.2
=====================================================================

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

- resolved an error with VARPARSE style arguments
2024-03-07 12:04:58 -07:00
6b15491066 v4.1.1
=====================================================================

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

- resolved a typo when running in prod environments
2024-03-07 11:58:58 -07:00
8427ad40f0 v4.1.0
=====================================================================

Reminder of the deprecation notices for FZF_(HEAD|TAIL) and v3
runstrings! These will be removed in 4.2 (the next minor release!)

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

- removed executable "run" in favor of single "scwrypts" entrypoint (this
  used to make more sense when running from the executable in the
  repository directly, but now scwrypts is primarily interfaced through
  the application itself)

- EKS commands will now attempt cluster login if the kubectl context is not
  detected on your machine

- improved warning messages for when scwrypts is out-of-date in API / CI
  contexts

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

- zsh/utils/io print functions will now always print if no
  SCWRYPTS_LOG_LEVEL is defined
2024-03-06 11:29:59 -07:00
1d3eb77235 v4.0.12
=====================================================================

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

- ensure proper return status from zsh/lib/utils/io print functions

- ensure user prompt is displayed when required if log-level is 0
2024-02-21 09:28:34 -07:00
406ee85d46 v4.0.11
=====================================================================

- ghaction fixed; quiet up the default logs again
2024-02-21 09:13:39 -07:00
7709c7e3db v4.0.10
=====================================================================

- actually let's just fetch tags in gh actions
2024-02-21 09:10:13 -07:00
8d3e6ae46f v4.0.9
=====================================================================

- include default-tag so scwrypts doesn't break in ghactions
2024-02-21 09:00:07 -07:00
fec8a7ec94 v4.0.8
=====================================================================

- more logs in virtual dependency update ghaction
2024-02-21 08:52:41 -07:00
534a2011a1 v4.0.7
=====================================================================

- unlock logs in virtual dependency update
2024-02-21 08:46:41 -07:00
e1b6c3e4f0 v4.0.6
=====================================================================

- bugfix in python scwrypts.scwrypts
2024-02-21 00:37:44 -07:00
1adb45d75e v4.0.5
=====================================================================

- one last python semver fix and then BOOM
2024-02-21 00:29:33 -07:00
62ab5404cf v4.0.4
=====================================================================

- ensure that full python semver is published to pypi
2024-02-21 00:22:49 -07:00
e43c07f75a v4.0.3
=====================================================================

- circleci: additional branch/tag trigger tweaks
2024-02-21 00:13:21 -07:00
fc5f80232e v4.0.2
=====================================================================

- circleci: more trigger fixes; add python build to python test
2024-02-21 00:05:40 -07:00
695eea2985 v4.0.1
=====================================================================

- circleci: trigger test workflows on tags too
2024-02-20 23:57:43 -07:00
287 changed files with 12453 additions and 10807 deletions

View File

@ -4,7 +4,14 @@ version: 2.1
orbs: orbs:
python: circleci/python@2.1.1 python: circleci/python@2.1.1
executors: executors:
archlinux:
docker:
- image: archlinux:base-devel
resource_class: small
working_directory: /
python: python:
docker: docker:
- image: cimg/python:3.11 - image: cimg/python:3.11
@ -15,30 +22,139 @@ executors:
- image: node:18 - image: node:18
resource_class: medium resource_class: medium
zsh:
docker:
- image: alpine:3
resource_class: small
commands:
archlinux-run:
description: execute steps in the archlinux container as the CI user
parameters:
_name:
type: string
command:
type: string
working_directory:
type: string
default: /home/ci
steps:
- run:
name: << parameters._name >>
working_directory: << parameters.working_directory >>
command: su ci -c '<< parameters.command >>'
custom:
archlinux:
prepare:
- &archlinux-prepare
run:
name: prepare archlinux dependencies
command: |
pacman --noconfirm -Syu git openssh ca-certificates-utils
useradd -m ci
echo "ci ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
temp-downgrade-fakeroot:
- &archlinux-temp-downgrade-fakeroot
run:
name: downgrade fakeroot to v1.34 (v1.35 and v1.36 are confirmed to break)
command: |
pacman -U --noconfirm https://archive.archlinux.org/packages/f/fakeroot/fakeroot-1.34-1-x86_64.pkg.tar.zst
clone-aur:
- &archlinux-clone-aur
archlinux-run:
_name: clone aur/scwrypts
command: git clone https://aur.archlinux.org/scwrypts.git aur
clone-scwrypts:
- &archlinux-clone-scwrypts
run:
name: clone wrynegade/scwrypts
working_directory: /home/ci
command: |
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git clone -b "$(echo $CIRCLE_BRANCH | grep . || echo $CIRCLE_TAG)" "$CIRCLE_REPOSITORY_URL" scwrypts
chown -R ci:ci ./scwrypts
jobs: jobs:
python-test: require-full-semver:
executor: python executor: python
working_directory: ~/scwrypts/py/lib
steps: steps:
- checkout:
path: ~/scwrypts
- run: - run:
name: pytest name: check CIRCLE_TAG for full semantic version
command: | command: |
: \ : \
&& pip install . .[test] \ && [ $CIRCLE_TAG ] \
&& pytest \ && [[ $CIRCLE_TAG =~ ^v[0-9]*.[0-9]*.[0-9]*$ ]] \
; ;
python-publish: aur-test:
executor: python executor: archlinux
working_directory: ~/scwrypts/py/lib
steps: steps:
- checkout: - *archlinux-prepare
path: ~/scwrypts - *archlinux-temp-downgrade-fakeroot
- python/dist - *archlinux-clone-aur
- run: pip install twine && twine upload dist/* - *archlinux-clone-scwrypts
- archlinux-run:
_name: test aur build on current source
working_directory: /home/ci/aur
command: >-
:
&& PKGVER=$(sed -n "s/^pkgver=//p" ./PKGBUILD)
&& cp -r ../scwrypts ../scwrypts-$PKGVER
&& rm -rf ../scwrypts-$PKGVER/.circleci
&& rm -rf ../scwrypts-$PKGVER/.git
&& rm -rf ../scwrypts-$PKGVER/.gitattributes
&& rm -rf ../scwrypts-$PKGVER/.gitignore
&& rm -rf ../scwrypts-$PKGVER/.github
&& tar -czf scwrypts.tar.gz ../scwrypts-$PKGVER
&& echo "source=(scwrypts.tar.gz)" >> PKGBUILD
&& echo "sha256sums=(SKIP)" >> PKGBUILD
&& makepkg --noconfirm -si
&& echo validating scwrypts version
&& scwrypts --version | grep "^scwrypts v$PKGVER$"
;
aur-publish:
executor: archlinux
steps:
- *archlinux-prepare
- *archlinux-temp-downgrade-fakeroot
- *archlinux-clone-aur
- archlinux-run:
_name: update PKGBUILD and .SRCINFO
working_directory: /home/ci/aur
command: >-
:
&& NEW_VERSION=$(echo $CIRCLE_TAG | sed 's/^v//')
&& sed "s/pkgver=.*/pkgver=$NEW_VERSION/; s/^pkgrel=.*/pkgrel=1/; /sha256sums/d" PKGBUILD -i
&& makepkg -g >> PKGBUILD
&& makepkg --printsrcinfo > .SRCINFO
;
- archlinux-run:
_name: sanity check for version build
working_directory: /home/ci/aur
command: >-
:
&& makepkg --noconfirm -si
&& scwrypts --version
&& scwrypts --version | grep -q "^scwrypts $CIRCLE_TAG\$"
;
- archlinux-run:
_name: publish new version
working_directory: /home/ci/aur
command: >-
:
&& git add PKGBUILD .SRCINFO
&& git -c user.email=yage@yage.io -c user.name=yage commit -am "$CIRCLE_TAG"
&& eval $(ssh-agent)
&& echo -e $SSH_KEY_PRIVATE__AUR | ssh-add -
&& git remote add upstream ssh://aur@aur.archlinux.org/scwrypts.git
&& GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git push upstream
;
nodejs-test: nodejs-test:
executor: nodejs executor: nodejs
@ -102,32 +218,135 @@ jobs:
: \ : \
&& [ $CIRCLE_TAG ] \ && [ $CIRCLE_TAG ] \
&& pnpm build \ && pnpm build \
&& pnpm version $(git describe --tags) \ && pnpm version $CIRCLE_TAG \
&& pnpm set //registry.npmjs.org/:_authToken=$NPM_TOKEN \ && pnpm set //registry.npmjs.org/:_authToken=$NPM_TOKEN \
&& pnpm publish --no-git-checks \ && pnpm publish --no-git-checks \
; ;
python-test:
executor: python
working_directory: ~/scwrypts/py/lib
steps:
- checkout:
path: ~/scwrypts
- run:
name: pytest
command: |
: \
&& pip install . .[test] \
&& pytest \
;
- run: pip install build && python -m build
python-publish:
executor: python
working_directory: ~/scwrypts/py/lib
steps:
- checkout:
path: ~/scwrypts
- run: pip install build && python -m build
- 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:
python: test:
jobs: jobs:
- python-test - aur-test:
- python-publish: &dev-filters
requires: [python-test]
context: [pypi-yage]
filters: filters:
branches:
ignore: /^main$/
- python-test: *dev-filters
- nodejs-test: *dev-filters
- zsh-test: *dev-filters
publish:
jobs:
- require-full-semver:
filters:
&only-run-on-full-semver-tag-filters
tags: tags:
only: /^v.*$/ only: /^v\d+\.\d+\.\d+.*$/
branches: branches:
ignore: /^.*$/ ignore: /^.*$/
nodejs: - aur-test:
jobs: &only-publish-for-full-semver
- nodejs-test filters: *only-run-on-full-semver-tag-filters
requires:
- require-full-semver
- aur-publish:
#
# there's a crazy-low-chance race-condition between this job and the GH Action '../.github/workflows/automatic-release.yaml'
# - automatic-release creates the release artifact, but takes no more than 15-30 seconds (current avg:16s max:26s)
# - this publish step requires the release artifact, but waits for all language-repository publishes to complete first (a few minutes at least)
#
# if something goes wrong, this step can be safely rerun after fixing the release artifact :)
#
filters: *only-run-on-full-semver-tag-filters
context: [aur-yage]
requires:
- aur-test
- python-publish
- nodejs-publish
- zsh-test
- python-test: *only-publish-for-full-semver
- python-publish:
filters: *only-run-on-full-semver-tag-filters
context: [pypi-yage]
requires:
- python-test
- zsh-test
- nodejs-test: *only-publish-for-full-semver
- nodejs-publish: - nodejs-publish:
requires: [nodejs-test] filters: *only-run-on-full-semver-tag-filters
context: [npm-wrynegade] context: [npm-wrynegade]
filters: requires:
tags: - nodejs-test
only: /^v.*$/ - zsh-test
branches:
ignore: /^.*$/ - 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'"

View File

@ -12,13 +12,9 @@ export DISCORD__DEFAULT_AVATAR_URL=
export DISCORD__DEFAULT_CHANNEL_ID= export DISCORD__DEFAULT_CHANNEL_ID=
export DISCORD__DEFAULT_USERNAME= export DISCORD__DEFAULT_USERNAME=
export DISCORD__DEFAULT_WEBHOOK= export DISCORD__DEFAULT_WEBHOOK=
export I3__BORDER_PIXEL_SIZE=
export I3__DMENU_FONT_SIZE=
export I3__GLOBAL_FONT_SIZE=
export I3__MODEL_CONFIG=
export LINEAR__API_TOKEN= export LINEAR__API_TOKEN=
export MEDIA_SYNC__S3_BUCKET export MEDIA_SYNC__S3_BUCKET=
export MEDIA_SYNC__TARGETS export MEDIA_SYNC__TARGETS=
export REDIS_AUTH= export REDIS_AUTH=
export REDIS_HOST= export REDIS_HOST=
export REDIS_PORT= export REDIS_PORT=

View File

@ -15,11 +15,6 @@ DISCORD__DEFAULT_CHANNEL_ID |
DISCORD__DEFAULT_USERNAME | DISCORD__DEFAULT_USERNAME |
DISCORD__DEFAULT_WEBHOOK | DISCORD__DEFAULT_WEBHOOK |
I3__BORDER_PIXEL_SIZE | custom i3 configuration settings
I3__DMENU_FONT_SIZE |
I3__GLOBAL_FONT_SIZE |
I3__MODEL_CONFIG |
LINEAR__API_TOKEN | linear.app project management configuration LINEAR__API_TOKEN | linear.app project management configuration
MEDIA_SYNC__S3_BUCKET | s3 bucket name and filesystem targets for media backups MEDIA_SYNC__S3_BUCKET | s3 bucket name and filesystem targets for media backups

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

@ -0,0 +1,19 @@
---
name: Automatic Tag-release
on: # yamllint disable-line rule:truthy
push:
branches-ignore:
- '**'
tags:
- 'v*.*.*'
jobs:
automatic-tag-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: marvinpinto/action-automatic-releases@latest
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false

View File

@ -1,60 +1,77 @@
# *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
Before contributing an issue, idea, or pull request, check out the [super-brief contributing guide](./docs/CONTRIBUTING.md) Before contributing an issue, idea, or pull request, check out the [super-brief contributing guide](./docs/CONTRIBUTING.md)

View File

@ -29,6 +29,7 @@ runs:
repository: wrynegade/scwrypts repository: wrynegade/scwrypts
path: ./wrynegade/scwrypts path: ./wrynegade/scwrypts
ref: ${{ inputs.version }} ref: ${{ inputs.version }}
fetch-tags: true
- name: check dependencies - name: check dependencies
shell: bash shell: bash
@ -51,7 +52,7 @@ runs:
} > $HOME/.scwrypts.apt-get.log 2>&1 } > $HOME/.scwrypts.apt-get.log 2>&1
echo "updating virtual dependencies" echo "updating virtual dependencies"
$GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts -n \ $GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts \
--name scwrypts/virtualenv/update-all \ --name scwrypts/virtualenv/update-all \
--group scwrypts \ --group scwrypts \
--type zsh \ --type zsh \

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,7 +115,7 @@ 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+=()
@ -128,7 +128,7 @@ use do/awesome/stuff --group my-custom-library
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 $(
{ {
@ -27,7 +27,7 @@ MAIN() {
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

@ -1,19 +1,18 @@
[[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0 [[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0
unalias k h >/dev/null 2>&1 unalias k h f >/dev/null 2>&1
k() { _SCWRYPTS_KUBECTL_DRIVER kubectl $@; } k() { _SCWRYPTS_KUBECTL_DRIVER kubectl $@; }
h() { _SCWRYPTS_KUBECTL_DRIVER helm $@; } h() { _SCWRYPTS_KUBECTL_DRIVER helm $@; }
f() { _SCWRYPTS_KUBECTL_DRIVER flux $@; } 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
@ -43,11 +42,11 @@ _SCWRYPTS_KUBECTL_DRIVER() {
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 '^'
)" )"
@ -68,7 +67,7 @@ _SCWRYPTS_KUBECTL_DRIVER() {
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})
" "
@ -135,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; }
##################################################################### #####################################################################
@ -155,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)
" "
@ -171,16 +170,16 @@ _SCWRYPTS_KUBECTL_DRIVER() {
[ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE) [ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE)
[[ $VERBOSE -eq 1 ]] && { [[ $VERBOSE -eq 1 ]] && {
INFO " 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$ ]] && {
INFO "$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[@]}
@ -191,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'
" "
@ -39,14 +36,14 @@ 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
INFO "attempting to serve ${NAMESPACE}/${SERVICE_NAME}:${SERVICE_PORT}"
[ $SERVICE_PASSWORD ] && INFO "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)
}

3
py/lib/.gitignore vendored
View File

@ -1 +1,4 @@
dist/ dist/
__pycache__/
*.py[cod]
*.so

View File

@ -55,6 +55,5 @@ source = 'versioningit'
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ['./'] packages = ['./']
[tool.versioningit] [tool.versioningit.vcs]
match = ['v*'] match = ['v[0-9]*.[0-9]*.[0-9]*']

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(name, None) 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

@ -2,23 +2,22 @@ from types import SimpleNamespace
from pytest import fixture from pytest import fixture
from scwrypts.test import generate from scwrypts.test import get_generator
from scwrypts.test.character_set import uri from scwrypts.test.character_set import uri
options = { generate = get_generator({
'str_length_minimum': 8, 'str_length_minimum': 8,
'str_length_maximum': 128, 'str_length_maximum': 128,
'uuid_output_type': str, 'uuid_output_type': str,
} })
def get_request_client_sample_data(): def get_request_client_sample_data():
return { return {
'base_url' : generate(str, options | {'character_set': uri}), 'base_url' : generate(str, {'character_set': uri}),
'endpoint' : generate(str, options | {'character_set': uri}), 'endpoint' : generate(str, {'character_set': uri}),
'method' : generate(str, options), 'method' : generate(str),
'response' : generate('requests_Response', options | {'depth': 4}), 'response' : generate('requests_Response', {'depth': 4}),
'payload' : generate(dict, { 'payload' : generate(dict, {
**options,
'depth': 1, 'depth': 1,
'data_types': { str, 'uuid' }, 'data_types': { str, 'uuid' },
}), }),
@ -30,13 +29,11 @@ def fixture_sample():
**get_request_client_sample_data(), **get_request_client_sample_data(),
headers = generate(dict, { headers = generate(dict, {
**options,
'depth': 1, 'depth': 1,
'data_types': { str, 'uuid' }, 'data_types': { str, 'uuid' },
}), }),
payload_headers = generate(dict, { payload_headers = generate(dict, {
**options,
'depth': 1, 'depth': 1,
'data_types': { str, 'uuid' }, 'data_types': { str, 'uuid' },
}), }),

View File

@ -2,15 +2,14 @@ from types import SimpleNamespace
from pytest import fixture from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri from scwrypts.test.character_set import uri
from ..conftest import options, get_request_client_sample_data from ..conftest import generate, get_request_client_sample_data
@fixture(name='sample') @fixture(name='sample')
def fixture_sample(): def fixture_sample():
return SimpleNamespace( return SimpleNamespace(
**get_request_client_sample_data(), **get_request_client_sample_data(),
api_token = generate(str, options | {'character_set': uri}), api_token = generate(str, {'character_set': uri}),
query = generate(str, options), query = generate(str),
) )

View File

@ -3,9 +3,8 @@ from types import SimpleNamespace
from pytest import fixture from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri from scwrypts.test.character_set import uri
from ..conftest import options, get_request_client_sample_data from ..conftest import generate, get_request_client_sample_data
@fixture(name='sample') @fixture(name='sample')
def fixture_sample(): def fixture_sample():
@ -14,12 +13,12 @@ def fixture_sample():
**get_request_client_sample_data(), **get_request_client_sample_data(),
'base_url': 'https://discord.com/api', 'base_url': 'https://discord.com/api',
}, },
bot_token = generate(str, options | {'character_set': uri}), bot_token = generate(str, {'character_set': uri}),
username = generate(str, options | {'character_set': ascii_letters + digits}), username = generate(str, {'character_set': ascii_letters + digits}),
avatar_url = generate(str, options | {'character_set': uri}), avatar_url = generate(str, {'character_set': uri}),
webhook = generate(str, options | {'character_set': uri}), webhook = generate(str, {'character_set': uri}),
channel_id = generate(str, options | {'character_set': uri}), channel_id = generate(str, {'character_set': uri}),
content_header = generate(str, options), content_header = generate(str),
content_footer = generate(str, options), content_footer = generate(str),
content = generate(str, options), content = generate(str),
) )

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

@ -3,9 +3,8 @@ from types import SimpleNamespace
from pytest import fixture from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri from scwrypts.test.character_set import uri
from ..conftest import options, get_request_client_sample_data from ..conftest import generate, get_request_client_sample_data
@fixture(name='sample') @fixture(name='sample')
def fixture_sample(): def fixture_sample():
@ -14,6 +13,6 @@ def fixture_sample():
**get_request_client_sample_data(), **get_request_client_sample_data(),
'base_url': 'https://api.linear.app', 'base_url': 'https://api.linear.app',
}, },
api_token = generate(str, options | {'character_set': uri}), api_token = generate(str, {'character_set': uri}),
query = generate(str, options), query = generate(str),
) )

View File

@ -13,7 +13,7 @@ class MissingFlagAndEnvironmentVariableError(EnvironmentError, ArgumentError):
class MissingScwryptsExecutableError(EnvironmentError): class MissingScwryptsExecutableError(EnvironmentError):
def __init__(self): def __init__(self):
super().__init__(f'scwrypts must be installed and available on your PATH') super().__init__('scwrypts must be installed and available on your PATH')
class BadScwryptsLookupError(ValueError): class BadScwryptsLookupError(ValueError):

View File

@ -5,44 +5,54 @@ from subprocess import run
from .exceptions import MissingScwryptsExecutableError, BadScwryptsLookupError, MissingScwryptsGroupOrTypeError from .exceptions import MissingScwryptsExecutableError, BadScwryptsLookupError, MissingScwryptsGroupOrTypeError
def scwrypts(*args, patterns=None, name=None, group=None, _type=None, log_level=None): def scwrypts(patterns=None, args=None, executable_args=None, name=None, group=None, _type=None):
''' '''
top-level scwrypts invoker from python top-level scwrypts invoker from python
- patterns allows for pattern-based scwrypt lookup patterns str / list pattern-based scwrypt lookup
- name/group/type allos for precise-match lookup args str / list arguments forwarded to the invoked scwrypt
executable_args str / list arguments for the 'scwrypts' executable
(str above assumes space-delimited values)
*args should be a list of strings and is forwarded to the name str exact scwrypt lookup name (requires group and _type)
invoked scwrypt group str exact scwrypt lookup group
_type str exact scwrypt lookup type
SCWRYPTS_EXECUTABLE configuration variable which defines the full path to scwrypts executable
see 'scwrypts --help' for more information see 'scwrypts --help' for more information
''' '''
executable = which('scwrypts')
if executable is None:
raise MissingScwryptsExecutableError()
if patterns is None and name is None: if patterns is None and name is None:
raise BadScwryptsLookupError() raise BadScwryptsLookupError()
pre_args = [] if name is not None and (group is None or _type is None):
if name is None:
pre_args += patterns
else:
pre_args += ['--name', name, '--group', group, '--type', _type]
if group is None or _type is None:
raise MissingScwryptsGroupOrTypeError(group, _type) raise MissingScwryptsGroupOrTypeError(group, _type)
if log_level is not None: executable = which(getenv('SCWRYPTS_EXECUTABLE', 'scwrypts'))
pre_args += ['--log-level', log_level]
if executable is None:
raise MissingScwryptsExecutableError()
lookup = _parse(patterns) if name is None else f'--name {name} --group {group} --type {_type}'
depth = getenv('SUBSCWRYPT', '') depth = getenv('SUBSCWRYPT', '')
if depth != '': if depth != '':
depth = int(depth) + 1 depth = int(depth) + 1
return run( return run(
f'SUBSCWRYPT={depth} {executable} {pre_args} -- {" ".join(args)}', f'SUBSCWRYPT={depth} {executable} {_parse(executable_args)} {lookup} -- {_parse(args)}',
shell=True, shell=True,
executable='/bin/zsh', executable='/bin/zsh',
check=False, check=False,
capture_output=True,
text=True,
) )
def _parse(string_or_list_args):
if string_or_list_args is None:
return ''
if isinstance(string_or_list_args, list):
return ' '.join(string_or_list_args)
return str(string_or_list_args)

View File

@ -0,0 +1,184 @@
from random import choice
from re import search
from string import ascii_letters, digits
from types import SimpleNamespace
from unittest.mock import patch
from pytest import fixture, raises
from scwrypts.test import get_generator
from .exceptions import MissingScwryptsExecutableError, BadScwryptsLookupError, MissingScwryptsGroupOrTypeError
from .scwrypts import scwrypts
#####################################################################
def test_scwrypts(sample, _scwrypts):
assert validate_scwrypts_output(sample, _scwrypts)
def test_scwrypts_finds_system_executable(sample, _scwrypts, mock_which):
mock_which.assert_called_once_with(sample.env['SCWRYPTS_EXECUTABLE'])
def test_scwrypts_uses_configured_executable_path(_scwrypts, mock_getenv):
mock_getenv.assert_any_call('SCWRYPTS_EXECUTABLE', 'scwrypts')
def test_scwrypts_uses_correct_depth(_scwrypts, mock_getenv):
mock_getenv.assert_any_call('SUBSCWRYPT', '')
def test_scwrypts_runs_subprocess(_scwrypts, mock_run):
mock_run.assert_called_once()
##########################################
def test_scwrypts_omit_optionals(sample, _scwrypts_omit_optionals):
assert validate_scwrypts_output(sample, _scwrypts_omit_optionals)
def test_scwrypts_omit_optionals_finds_system_executable(sample, _scwrypts_omit_optionals, mock_which):
mock_which.assert_called_once_with('scwrypts')
def test_scwrypts_omit_optionals_uses_configured_executable_path(_scwrypts_omit_optionals, mock_getenv):
mock_getenv.assert_any_call('SCWRYPTS_EXECUTABLE', 'scwrypts')
def test_scwrypts_omit_optionals_uses_correct_depth(_scwrypts_omit_optionals, mock_getenv):
mock_getenv.assert_any_call('SUBSCWRYPT', '')
def test_scwrypts_omit_optionals_runs_subprocess(_scwrypts_omit_optionals, mock_run):
mock_run.assert_called_once()
##########################################
def test_invalid_lookup_missing_patterns_and_name(sample):
sample.patterns = None
sample.name = None
with raises(BadScwryptsLookupError):
scwrypts(**get_scwrypts_args(sample))
def test_invalid_name_lookup_missing_group(sample):
sample.group = None
with raises(MissingScwryptsGroupOrTypeError):
scwrypts(**get_scwrypts_args(sample))
def test_invalid_name_lookup_missing_type(sample):
sample._type = None # pylint: disable=protected-access
with raises(MissingScwryptsGroupOrTypeError):
scwrypts(**get_scwrypts_args(sample))
def test_invalid_scwrypts_installation(sample, mock_which):
mock_which.return_value = None
with raises(MissingScwryptsExecutableError):
scwrypts(**get_scwrypts_args(sample))
#####################################################################
generate = get_generator({
'str_length_minimum': 8,
'str_length_maximum': 128,
'character_set': ascii_letters + digits + '/-_'
})
def _generate_str_or_list_arg():
random_arg = generate(list, {'data_types': {str}})
return random_arg if choice([str, list]) == list else ' '.join(random_arg)
@fixture(name='sample')
def fixture_sample():
sample = SimpleNamespace(
patterns = _generate_str_or_list_arg(),
args = _generate_str_or_list_arg(),
executable_args = _generate_str_or_list_arg(),
name = generate(str),
group = generate(str),
_type = generate(str),
executable = generate(str),
env = {
'SCWRYPTS_EXECUTABLE': generate(str),
'SUBSCWRYPT': str(generate(int, {'minimum': 1, 'maximum': 99})),
},
returncode = generate(int),
stdout = generate(str),
stderr = generate(str),
)
return sample
def get_scwrypts_args(sample):
return {
key: getattr(sample, key)
for key in [
'patterns',
'args',
'executable_args',
'name',
'group',
'_type',
]
}
#####################################################################
@fixture(name='mock_which', autouse=True)
def fixture_mock_which(sample):
with patch('scwrypts.scwrypts.scwrypts.which') as mock:
mock.return_value = sample.executable
yield mock
@fixture(name='mock_getenv', autouse=True)
def fixture_mock_getenv(sample):
with patch('scwrypts.scwrypts.scwrypts.getenv') as mock:
mock.side_effect = sample.env.get
yield mock
@fixture(name='mock_run', autouse=True)
def fixture_mock_run(sample):
with patch('scwrypts.scwrypts.scwrypts.run') as mock:
mock.side_effect = lambda *args, **_kwargs: SimpleNamespace(
args = args,
returncode = sample.returncode,
stdout = sample.stdout,
stderr = sample.stderr,
)
yield mock
#####################################################################
@fixture(name='_scwrypts')
def fixture_scwrypts(sample):
return scwrypts(**get_scwrypts_args(sample))
@fixture(name='_scwrypts_omit_optionals')
def fixture_scwrypts_omit_optionals(sample):
sample.args = None
sample.executable_args = None
del sample.env['SCWRYPTS_EXECUTABLE']
del sample.env['SUBSCWRYPT']
return scwrypts(**get_scwrypts_args(sample))
def validate_scwrypts_output(sample, output):
#
# I would love to use 'assert _scwrypts == SimpleNamespace(...expected...)'
# but the output.args is difficult to recreate without copying all the
# processing logic over from the scwrypts function
#
# opting for a bit of a strange equality test here, checking the args
# as closely as possible without copying parsing logic
#
run_args_reduced_to_a_single_string = len(output.args) == 1
run_args_follow_expected_form = search(
fr'^SUBSCWRYPT=.* {sample.executable} .*-- .*$',
output.args[0],
)
return all([
run_args_reduced_to_a_single_string,
run_args_follow_expected_form,
output.returncode == sample.returncode,
output.stdout == sample.stdout,
output.stderr == sample.stderr,
])

View File

@ -5,6 +5,6 @@ __all__ = [
'generate', 'generate',
] ]
from .generate import generate from .generate import generate, get_generator
from .character_set import * from .character_set import *

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
@ -45,6 +47,21 @@ DEFAULT_OPTIONS = {
'requests_response_status_code': status_codes.codes[200], 'requests_response_status_code': status_codes.codes[200],
} }
def get_generator(default_options=None):
if default_options is None:
default_options = {}
def generator_function(data_type=None, options_overrides=None):
if options_overrides is None:
options_overrides = {}
return generate(
data_type = data_type,
options = default_options | options_overrides,
)
return generator_function
def generate(data_type=None, options=None): def generate(data_type=None, options=None):
''' '''
generate random data with the call of a function generate random data with the call of a function

388
run
View File

@ -1,388 +0,0 @@
#!/bin/zsh
export EXECUTION_DIR=$(pwd)
source "${0:a:h}/zsh/lib/import.driver.zsh" || exit 42
#####################################################################
() {
cd "$SCWRYPTS_ROOT__scwrypts"
GIT_SCWRYPTS() { git -C "$SCWRYPTS_ROOT__scwrypts" $@; }
local ERRORS=0
local USAGE='
usage: scwrypts [...options...] [...patterns...] -- [...script options...]
options:
selection
-m, --name <scwrypt-name> only run the script if there is an exact match
(requires type and group)
-g, --group <group-name> only use scripts from the indicated group
-t, --type <type-name> only use scripts of the indicated type
runtime
-y, --yes auto-accept all [yn] prompts through current scwrypt
-e, --env <env-name> set environment; overwrites SCWRYPTS_ENV
-n shorthand for "--log-level 0"
-v, --log-level [0-4] set scwrypts log level to one of the following:
0 : only command output and critical failures; skips logfile
1 : add success / failure messages
2 : (default) include status update messages
3 : (CI default) include warning messages
4 : include debug messages
alternate commands
-h, --help display this message and exit
-l, --list print out command list and exit
--list-envs print out environment list and exit
--update update scwrypts library to latest version
--version print out scwrypts version and exit
patterns:
- a list of glob patterns to loose-match a scwrypt by name
script options:
- everything after "--" is forwarded to the scwrypt you run
("-- --help" will provide more information)
'
#####################################################################
### cli argument parsing and global configuration ###################
#####################################################################
local ENV_NAME="$SCWRYPTS_ENV"
local SEARCH_PATTERNS=()
local VARSPLIT SEARCH_GROUP SEARCH_TYPE SEARCH_NAME
[ ! $SCWRYPTS_LOG_LEVEL ] && {
local SCWRYPTS_LOG_LEVEL
[ $CI ] && SCWRYPTS_LOG_LEVEL=3 || SCWRYPTS_LOG_LEVEL=2
}
while [[ $# -gt 0 ]]
do
case $1 in
-[a-z][a-z]* )
VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/')
set -- $(echo " $VARSPLIT ") ${@:2}
;;
### alternate commands ###################
-h | --help )
USAGE
return 0
;;
-l | --list )
SCWRYPTS__GET_AVAILABLE_SCWRYPTS
return 0
;;
--list-envs )
SCWRYPTS__GET_ENV_NAMES
return 0
;;
--version )
echo scwrypts $(GIT_SCWRYPTS describe --tags)
return 0
;;
--update )
GIT_SCWRYPTS fetch --quiet origin main
GIT_SCWRYPTS fetch --quiet origin main --tags
local SYNC_STATUS=$?
GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1
local DIFF_STATUS=$?
[[ $SYNC_STATUS -eq 0 ]] && [[ $DIFF_STATUS -eq 0 ]] && {
SUCCESS 'already up-to-date with origin/main'
} || {
GIT_SCWRYPTS rebase --autostash origin/main \
&& SUCCESS 'up-to-date with origin/main' \
&& GIT_SCWRYPTS log -n1 \
|| {
GIT_SCWRYPTS rebase --abort
ERROR 'unable to update scwrypts; please try manual upgrade'
REMINDER "installation in '$(pwd)'"
}
}
return 0
;;
### scwrypts filters #####################
-m | --name )
[ $2 ] || { ERROR "missing value for argument $1"; break; }
SEARCH_NAME=$2
shift 1
;;
-g | --group )
[ $2 ] || { ERROR "missing value for argument $1"; break; }
SEARCH_GROUP=$2
shift 1
;;
-t | --type )
[ $2 ] || { ERROR "missing value for argument $1"; break; }
SEARCH_TYPE=$2
shift 1
;;
### runtime settings #####################
-y | --yes ) export __SCWRYPTS_YES=1 ;;
-n | --no-log )
SCWRYPTS_LOG_LEVEL=0
[[ $1 =~ ^--no-log$ ]] && WARNING 'the --no-log flag is deprecated and will be removed in scwrypts v4.2'
;;
-v | --log-level )
[[ $2 =~ ^[0-4]$ ]] || ERROR "invalid setting for log-level '$2'"
SCWRYPTS_LOG_LEVEL=$2
shift 1
;;
-e | --env )
[ $2 ] || { ERROR "missing value for argument $1"; break; }
[ $ENV_NAME ] && DEBUG 'overwriting session environment'
ENV_NAME="$2"
STATUS "using CLI environment '$ENV_NAME'"
shift 1
;;
##########################################
-- ) shift 1; break ;; # pass arguments after '--' to the scwrypt
--* ) ERROR "unrecognized argument '$1'" ;;
* ) SEARCH_PATTERNS+=($1) ;;
esac
shift 1
done
[ $SEARCH_NAME ] && {
[ $SEARCH_TYPE ] || ERROR '--name requires --type argument'
[ $SEARCH_GROUP ] || ERROR '--name requires --group argument'
}
CHECK_ERRORS
#####################################################################
### scwrypts selection / filtering ##################################
#####################################################################
local SCWRYPTS_AVAILABLE
SCWRYPTS_AVAILABLE=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS)
##########################################
[ $SEARCH_NAME ] && SCWRYPTS_AVAILABLE=$({
echo $SCWRYPTS_AVAILABLE | head -n1
echo $SCWRYPTS_AVAILABLE | sed -e 's/\x1b\[[0-9;]*m//g' | grep "^$SEARCH_NAME *$SEARCH_TYPE *$SEARCH_GROUP\$"
}) || {
[ $SEARCH_TYPE ] && {
SCWRYPTS_AVAILABLE=$(\
{
echo $SCWRYPTS_AVAILABLE | head -n1
echo $SCWRYPTS_AVAILABLE | grep ' [^/]*'$SEARCH_TYPE'[^/]* '
} \
| awk '{$2=""; print $0;}' \
| sed 's/ \+$/'$(printf $__COLOR_RESET)'/; s/ \+/^/g' \
| column -ts '^'
)
}
[ $SEARCH_GROUP ] && {
SCWRYPTS_AVAILABLE=$(
{
echo $SCWRYPTS_AVAILABLE | head -n1
echo $SCWRYPTS_AVAILABLE | grep "$SEARCH_GROUP"'[^/]*$'
} \
| awk '{$NF=""; print $0;}' \
| sed 's/ \+$/'$(printf $__COLOR_RESET)'/; s/ \+/^/g' \
| column -ts '^'
)
}
[[ ${#SEARCH_PATTERNS[@]} -gt 0 ]] && {
POTENTIAL_ERROR+="\n PATTERNS : $SEARCH_PATTERNS"
local P
for P in ${SEARCH_PATTERNS[@]}
do
SCWRYPTS_AVAILABLE=$(
{
echo $SCWRYPTS_AVAILABLE | head -n1
echo $SCWRYPTS_AVAILABLE | grep $P
}
)
done
}
}
[[ $(echo $SCWRYPTS_AVAILABLE | wc -l) -lt 2 ]] && {
FAIL 1 "$(echo "
no such scwrypt exists
NAME : '$SEARCH_NAME'
TYPE : '$SEARCH_TYPE'
GROUP : '$SEARCH_GROUP'
PATTERNS : '$SEARCH_PATTERNS'
" | sed "1d; \$d; /''$/d")"
}
##########################################
[[ $(echo $SCWRYPTS_AVAILABLE | wc -l) -eq 2 ]] \
&& SCWRYPT_SELECTION=$(echo $SCWRYPTS_AVAILABLE | tail -n1) \
|| SCWRYPT_SELECTION=$(echo $SCWRYPTS_AVAILABLE | FZF "select a script to run" --header-lines 1) \
;
[ $SCWRYPT_SELECTION ] || exit 2
##########################################
local NAME TYPE GROUP
SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION
export SCWRYPT_NAME=$NAME
export SCWRYPT_TYPE=$TYPE
export SCWRYPT_GROUP=$GROUP
#####################################################################
### environment variables and configuration validation ##############
#####################################################################
local ENV_REQUIRED=true \
&& [ ! $CI ] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/logs ]] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/environment ]] \
|| ENV_REQUIRED=false
local REQUIRED_ENVIRONMENT_REGEX=$(eval echo '$SCWRYPTS_REQUIRED_ENVIRONMENT_REGEX__'$SCWRYPT_GROUP)
[[ $ENV_REQUIRED =~ true ]] && {
[ ! $ENV_NAME ] && ENV_NAME=$(SCWRYPTS__SELECT_ENV)
for GROUP in ${SCWRYPTS_GROUPS[@]}
do
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)
do
source "$f" || FAIL 5 "invalid static config '$f'"
done
done
export 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)"
}
##########################################
[ ! $SUBSCWRYPT ] && [[ $ENV_NAME =~ prod ]] && {
STATUS "on '$ENV_NAME'; checking diff against origin/main"
GIT_SCWRYPTS fetch --quiet origin main
local SYNC_STATUS=$?
GIT_SCWRYPTS diff --exit-code origin/main -- . >&2
local DIFF_STATUS=$?
[[ $SYNC_STATUS -eq 0 ]] && [[ $DIFF_STATUS -eq 0 ]] && {
SUCCESS 'up-to-date with origin/main'
} || {
SCWRYPTS_LOG_LEVEL=3 WARNING "you are trying to run in ${__BRIGHT_RED}production${__YELLOW} but $([[ $SYNC_STATUS -ne 0 ]] && echo 'I am unable to verify your scwrypts version')$([[ $DIFF_STATUS -ne 0 ]] && echo 'your scwrypts is out-of-date (diff listed above)')"
yN 'continue?' || {
REMINDER "you can use 'scwrypts --update' to quickly update scwrypts to latest"
ABORT
}
}
}
##########################################
local RUN_STRING=$(SCWRYPTS__GET_RUNSTRING $SCWRYPT_NAME $SCWRYPT_TYPE $SCWRYPT_GROUP)
[ "$RUN_STRING" ] || return 42
#####################################################################
### logging and pretty header/footer setup ##########################
#####################################################################
local LOGFILE \
&& [[ $SCWRYPTS_LOG_LEVEL -gt 0 ]] \
&& [ ! $SUBSCWRYPT ] \
&& [[ ! $SCWRYPT_NAME =~ scwrypts/logs ]] \
&& [[ ! $SCWRYPT_NAME =~ interactive ]] \
&& LOGFILE="$SCWRYPTS_LOG_PATH/$(echo $GROUP/$TYPE/$NAME | sed 's/^\.\///; s/\//\%/g').log" \
|| LOGFILE='/dev/null' \
;
local RUN_MODE=normal
[[ $LOGFILE =~ ^/dev/null$ ]] && RUN_MODE=no-logfile
[[ $SCWRYPT_NAME =~ interactive ]] && RUN_MODE=interactive
local HEADER FOOTER
[[ $SCWRYPTS_LOG_LEVEL -ge 2 ]] && {
HEADER=$(
echo "
=====================================================================
script : $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME
run at : $(date)
config : $ENV_NAME
log level : $SCWRYPTS_LOG_LEVEL
\\033[1;33m--- SCWRYPT BEGIN ---------------------------------------------------\\033[0m
" | sed 's/^\s\+//; 1d'
)
FOOTER="\\033[1;33m--- SCWRYPT END ---------------------------------------------------\\033[0m"
}
[ $SUBSCWRYPT ] && {
HEADER="\\033[0;33m--- ($SUBSCWRYPT) BEGIN $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME ---"
FOOTER="\\033[0;33m--- ($SUBSCWRYPT) END $SCWRYPT_GROUP $SCWRYPT_TYPE $SCWRYPT_NAME ---"
}
#####################################################################
### run the scwrypt #################################################
#####################################################################
[ ! $SUBSCWRYPT ] && export SUBSCWRYPT=0
set -o pipefail
{
[ $HEADER ] && echo $HEADER
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 ] \
&& echo "terminated with\\033[1;$EXIT_COLOR code $EXIT_CODE\\033[0m"
return $EXIT_CODE
} 2>&1 | tee --append "$LOGFILE"
} $@

527
scwrypts
View File

@ -1,2 +1,525 @@
#!/bin/zsh #!/usr/bin/env zsh
source "${0:a:h}/run" $@ export EXECUTION_DIR=$(pwd)
export SCWRYPTS_RUNTIME_ID=$(uuidgen)
source "$(dirname -- $(readlink -f -- "$0"))/zsh/import.driver.zsh" || return 42
use scwrypts/environment
use scwrypts/list-available
use scwrypts/get-runstring
#####################################################################
() {
cd "$(scwrypts.config.group scwrypts root)"
GIT_SCWRYPTS() { git -C "$(scwrypts.config.group scwrypts root)" $@; }
local ERRORS=0
local USAGE='
usage: scwrypts [...options...] [...patterns...] -- [...script options...]
options:
selection
-m, --name <scwrypt-name> only run the script if there is an exact match
(requires type and group)
-g, --group <group-name> only use scripts from the indicated group
-t, --type <type-name> only use scripts of the indicated type
runtime
-y, --yes auto-accept all [yn] prompts through current scwrypt
-e, --env <env-name> set environment; overwrites SCWRYPTS_ENV
-n shorthand for "--log-level 0"
-v, --log-level <0-4> set incremental scwrypts log level to one of the following:
0 : only command output and critical failures; skips logfile
1 : include success / failure messages
2 : include status update messages
3 : (default) include warning messages
4 : include debug messages
-o, --output <format> specify output format; one of: pretty,json (default: pretty)
alternate commands
-h, --help display this message and exit
-l, --list print out command list and exit
--list-envs print out environment list and exit
--list-groups print out configured scwrypts groups and exit
--config "eval"-ed to enable config and "use" import in non-scwrypts environments
--root print out scwrypts.config.group.scwrypts.root and exit
--update update scwrypts library to latest version
--version print out scwrypts version and exit
patterns:
- a list of glob patterns to loose-match a scwrypt by name
script options:
- everything after "--" is forwarded to the scwrypt you run
("-- --help" will provide more information)
'
#####################################################################
### cli argument parsing and global configuration ###################
#####################################################################
local ENV_NAME="${SCWRYPTS_ENV}"
local SEARCH_PATTERNS=()
local VARSPLIT SEARCH_GROUP SEARCH_TYPE SEARCH_NAME
[ ! ${SCWRYPTS_LOG_LEVEL} ] && local SCWRYPTS_LOG_LEVEL=3
local SHIFT_COUNT
while [[ $# -gt 0 ]]
do
SHIFT_COUNT=1
case $1 in
( -[a-z][a-z]* )
VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/')
set -- throw-away $(echo " ${VARSPLIT} ") ${@:2}
;;
### alternate commands ###################
( -h | --help )
utils.io.usage
return 0
;;
( -l | --list )
scwrypts.list-available
return 0
;;
( --list-envs )
scwrypts.environment.common.get-env-names
return 0
;;
( --list-groups )
echo "${SCWRYPTS_GROUPS[@]}" | sed 's/\s\+/\n/g' | sort -u
return 0
;;
( --version )
case ${SCWRYPTS_INSTALLATION_TYPE} in
( manual ) echo "scwrypts $(GIT_SCWRYPTS describe --tags) (via GIT)" ;;
( * ) echo "scwrypts $(cat "$(scwrypts.config.group scwrypts root)/VERSION")" ;;
esac
return 0
;;
( --root )
scwrypts.config.group scwrypts root
return 0
;;
( --config )
echo "source '$(scwrypts.config.group scwrypts root)/zsh/import.driver.zsh'"
echo "utils.check-environment --no-fail --no-usage"
echo "unset __SCWRYPT"
return 0
;;
( --update )
case ${SCWRYPTS_INSTALLATION_TYPE} in
aur )
echo.reminder --force-print "
This installation is built from the AUR. Update through 'makepkg' or use
your preferred AUR package management tool (e.g. 'yay -Syu scwrypts')
"
;;
homebrew )
echo.reminder --force-print "This installation is managed by homebrew. Update me with 'brew update'"
;;
manual )
GIT_SCWRYPTS fetch --quiet origin main
GIT_SCWRYPTS fetch --quiet origin main --tags
local SYNC_STATUS=$?
GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1
local DIFF_STATUS=$?
[[ ${SYNC_STATUS} -eq 0 ]] && [[ ${DIFF_STATUS} -eq 0 ]] && {
echo.success 'already up-to-date with origin/main'
} || {
GIT_SCWRYPTS rebase --autostash origin/main \
&& echo.success 'up-to-date with origin/main' \
&& GIT_SCWRYPTS log -n1 \
|| {
GIT_SCWRYPTS rebase --abort
echo.error 'unable to update scwrypts; please try manual upgrade'
echo.reminder "installation in '$(scwrypts.config.group scwrypts root)'"
}
}
;;
* )
echo.reminder --force-print "
This is a managed installation of scwrypts. Please update through your
system package manager.
"
;;
esac
return 0
;;
### scwrypts filters #####################
( -m | --name )
((SHIFT_COUNT+=1))
[ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_NAME=$2
;;
( -g | --group )
((SHIFT_COUNT+=1))
[ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_GROUP=$2
GROUP=$2
;;
( -t | --type )
((SHIFT_COUNT+=1))
[ $2 ] || { echo.error "missing value for argument $1"; break; }
SEARCH_TYPE=$2
TYPE=$2
;;
### runtime settings #####################
( -y | --yes ) export __SCWRYPTS_YES=1 ;;
( -n ) SCWRYPTS_LOG_LEVEL=0 ;;
( -v | --log-level )
((SHIFT_COUNT+=1))
[[ $2 =~ ^[0-4]$ ]] || echo.error "invalid setting for log-level '$2'"
SCWRYPTS_LOG_LEVEL=$2
;;
( -o | --output )
((SHIFT_COUNT+=1))
export SCWRYPTS_OUTPUT_FORMAT=$2
case ${SCWRYPTS_OUTPUT_FORMAT} in
( pretty | json ) ;;
* ) echo.error "unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" ;;
esac
;;
( -e | --env )
((SHIFT_COUNT+=1))
[ $2 ] || { echo.error "missing value for argument $1"; break; }
[ ${ENV_NAME} ] && echo.debug 'overwriting session environment'
ENV_NAME="$2"
echo.status "using CLI environment '${ENV_NAME}'"
;;
##########################################
( -- ) shift 1; break ;; # pass arguments after '--' to the scwrypt
( --* ) echo.error "unrecognized argument '$1'" ;;
( * ) SEARCH_PATTERNS+=($1) ;;
esac
[[ ${SHIFT_COUNT} -le $# ]] \
&& shift ${SHIFT_COUNT} \
|| echo.error "missing argument for '$1'" \
|| shift $# \
;
done
[ ${SCWRYPTS_OUTPUT_FORMAT} ] || export SCWRYPTS_OUTPUT_FORMAT=pretty
[ ${SEARCH_NAME} ] && {
[ ${SEARCH_TYPE} ] || echo.error '--name requires --type argument'
[ ${SEARCH_GROUP} ] || echo.error '--name requires --group argument'
}
utils.check-errors --fail
#####################################################################
### scwrypts selection / filtering ##################################
#####################################################################
local SCWRYPTS_AVAILABLE=$(scwrypts.list-available)
##########################################
[ ${SEARCH_NAME} ] && SCWRYPTS_AVAILABLE=$({
echo ${SCWRYPTS_AVAILABLE} | head -n1
echo ${SCWRYPTS_AVAILABLE} | utils.colors.remove | grep "^${SEARCH_NAME} *${SEARCH_TYPE} *${SEARCH_GROUP}\$"
}) || {
[ ${SEARCH_TYPE} ] && {
SCWRYPTS_AVAILABLE=$(\
{
echo ${SCWRYPTS_AVAILABLE} | head -n1
echo ${SCWRYPTS_AVAILABLE} | grep ' [^/]*'${SEARCH_TYPE}'[^/]* '
} \
| sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \
| column -ts '^'
)
}
[ ${SEARCH_GROUP} ] && {
SCWRYPTS_AVAILABLE=$(
{
echo ${SCWRYPTS_AVAILABLE} | head -n1
echo ${SCWRYPTS_AVAILABLE} | grep "${SEARCH_GROUP}"'[^/ ]*$'
} \
| sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \
| column -ts '^'
)
}
[[ ${#SEARCH_PATTERNS[@]} -gt 0 ]] && {
POTENTIAL_ERROR+="\n PATTERNS : ${SEARCH_PATTERNS}"
local P
for P in ${SEARCH_PATTERNS[@]}
do
SCWRYPTS_AVAILABLE=$(
{
echo ${SCWRYPTS_AVAILABLE} | head -n1
echo ${SCWRYPTS_AVAILABLE} | grep ${P}
}
)
done
}
}
[[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -lt 2 ]] && {
utils.fail 1 "$(echo "
no such scwrypt exists
NAME : '${SEARCH_NAME}'
TYPE : '${SEARCH_TYPE}'
GROUP : '${SEARCH_GROUP}'
PATTERNS : '${SEARCH_PATTERNS}'
" | sed "1d; \$d; /''$/d")"
}
##########################################
[[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -eq 2 ]] \
&& SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | tail -n1) \
|| SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | utils.fzf "select a script to run" --header-lines 1) \
;
[ ${SCWRYPT_SELECTION} ] || utils.abort
##########################################
() {
set -- $(echo $@ | utils.colors.remove)
export SCWRYPT_NAME=$1
export SCWRYPT_TYPE=$2
export SCWRYPT_GROUP=$3
} ${SCWRYPT_SELECTION}
#####################################################################
### environment variables and configuration validation ##############
#####################################################################
local ENV_REQUIRED=true \
&& [ ! ${CI} ] \
&& [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \
&& [[ ! ${SCWRYPT_NAME} =~ scwrypts/environment ]] \
|| ENV_REQUIRED=false
local REQUIRED_ENVIRONMENT_REGEX="$(scwrypts.config.group ${SCWRYPT_GROUP} required_environment_regex)"
[ ${ENV_NAME} ] && [ ${REQUIRED_ENVIRONMENT_REGEX} ] && {
[[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \
|| utils.fail 5 "group '${SCWRYPT_GROUP}' requires current environment name to match '${REQUIRED_ENVIRONMENT_REGEX}' (currently ${ENV_NAME})"
}
[[ ${ENV_REQUIRED} =~ true ]] && {
[ ! ${ENV_NAME} ] && {
scwrypts.environment.init \
|| echo.error "failed to initialize scwrypts environments (see above)" \
|| return 1 \
;
ENV_NAME=$(scwrypts.environment.select-env)
[ "${ENV_NAME}" ] || user.abort
}
for GROUP in ${SCWRYPTS_GROUPS[@]}
do
local REQUIRED_REGEX="$(scwrypts.config.group ${GROUP} required_environment_regex)"
[ ${REQUIRED_REGEX} ] && {
[[ ${ENV_NAME} =~ ${REQUIRED_REGEX} ]] || continue
}
for f in $(find "$(scwrypts.config.group ${GROUP} root)/.config/static" -type f 2>/dev/null)
do
source "${f}" || utils.fail 5 "invalid static config '${f}'"
done
done
}
[ ${REQUIRED_ENVIRONMENT_REGEX} ] && {
[[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \
|| 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
[[ ${SCWRYPTS_INSTALLATION_TYPE} =~ ^manual$ ]] && {
[[ ${SUBSCWRYPT} -eq 0 ]] && [[ ${SCWRYPTS_ENV} =~ prod ]] && [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] && {
echo.status "on '${SCWRYPTS_ENV}'; checking diff against origin/main"
local WARNING_MESSAGE
[ ! ${WARNING_MESSAGE} ] && {
GIT_SCWRYPTS fetch --quiet origin main \
|| WARNING_MESSAGE='I am unable to verify your scwrypts version'
}
[ ! ${WARNING_MESSAGE} ] && {
GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 \
|| WARNING_MESSAGE='your scwrypts is currently out-of-date'
}
[ ${WARNING_MESSAGE} ] && {
[[ ${SCWRYPTS_LOG_LEVEL} -lt 3 ]] && {
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
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?' || {
echo.reminder "you can use 'scwrypts --update' to quickly update scwrypts to latest"
user.abort
}
}
}
}
}
##########################################
local RUN_STRING=$(scwrypts.get-runstring ${SCWRYPT_NAME} ${SCWRYPT_TYPE} ${SCWRYPT_GROUP})
[ "${RUN_STRING}" ] || return 42
#####################################################################
### logging and pretty header/footer setup ##########################
#####################################################################
local RUN_MODE=normal
[[ ${SCWRYPT_NAME} =~ interactive ]] && RUN_MODE=interactive
local LOGFILE \
&& [[ ${RUN_MODE} =~ normal ]] \
&& [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] \
&& [[ ${SUBSCWRYPT} -eq 0 ]] \
&& [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \
&& LOGFILE="${SCWRYPTS_LOG_PATH}/$(echo ${GROUP}/${TYPE}/${NAME} | sed 's/^\.\///; s/\//\%/g').log" \
|| LOGFILE='/dev/null' \
;
local HEADER FOOTER
[[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && {
case ${SCWRYPTS_OUTPUT_FORMAT} in
( raw )
HEADER="--- start scwrypt ${SCWRYPT_GROUP}/${SCWRYPT_TYPE} ${SCWRYPT_NAME} in ${SCWRYPTS_ENV} ---"
FOOTER="--- end scwrypt ---"
;;
( pretty )
HEADER=$(
echo "
=====================================================================
scwrypt : ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME}
run at : $(date)
config : ${SCWRYPTS_ENV}
log level : ${SCWRYPTS_LOG_LEVEL}
$(utils.colors.print bright-yellow '--- SCWRYPT BEGIN ---------------------------------------------------')
" | sed 's/^\s\+//; 1d'
)
FOOTER="$(utils.colors.print bright-yellow '--- SCWRYPT END ---------------------------------------------------')"
;;
( json )
HEADER=$(echo '{}' | jq -c ".
| .timestamp = \"$(date +%s)\"
| .runtime = \"${SCWRYPTS_RUNTIME_ID}\"
| .scwrypt = \"start of ${SCWRYPT_NAME} ${SCWRYPT_GROUP} ${SCWRYPT_TYPE}\"
| .config = \"${SCWRYPTS_ENV}\"
| .logLevel = \"${SCWRYPTS_LOG_LEVEL}\"
| .subscwrypt = ${SUBSCWRYPT}
")
;;
esac
}
[[ ${SUBSCWRYPT} -eq 0 ]] || {
case ${SCWRYPTS_OUTPUT_FORMAT} in
( pretty )
HEADER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) BEGIN ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")"
FOOTER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) END ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")"
;;
esac
}
#####################################################################
### run the scwrypt #################################################
#####################################################################
set -o pipefail
{
[[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && __SCWRYPTS_PRINT_EXIT_CODE=true
[ ${HEADER} ] && echo ${HEADER} >&2
(
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

@ -1,59 +1,81 @@
NO_EXPORT_CONFIG=1 source "${0:a:h}/zsh/lib/import.driver.zsh" || return 42 #
# typically you do not need to reload this plugin in a single session;
# if for some reason you do, you can run the following command and
# source this file again
#
# unset __SCWRYPTS_PLUGIN_LOADED
#
[[ $__SCWRYPTS_PLUGIN_LOADED =~ true ]] && return 0
##################################################################### #####################################################################
SCWRYPTS__ZSH_PLUGIN() {
local SCWRYPT_SELECTION=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS | FZF 'select a script' --header-lines 1) : \
local NAME && command -v scwrypts &>/dev/null \
local TYPE && eval "$(scwrypts --config)" \
local GROUP || {
echo 'scwrypts must be in PATH and properly configured; skipping zsh plugin setup' >&2
return 0
}
__SCWRYPTS_PARSE() {
SCWRYPT_SELECTION=$(scwrypts --list | fzf --ansi --prompt 'select a script : ' --header-lines 1)
LBUFFER= RBUFFER= LBUFFER= RBUFFER=
[ ! $SCWRYPT_SELECTION ] && { zle accept-line; return 0; } [ $SCWRYPT_SELECTION ] || return 1
SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION NAME=$(echo "$SCWRYPT_SELECTION" | awk '{print $1;}')
TYPE=$(echo "$SCWRYPT_SELECTION" | awk '{print $2;}')
GROUP=$(echo "$SCWRYPT_SELECTION" | awk '{print $3;}')
which scwrypts >/dev/null 2>&1\ [ $NAME ] && [ $TYPE ] && [ $GROUP ]
&& RBUFFER="scwrypts" || RBUFFER="$SCWRYPTS_ROOT/scwrypts" }
RBUFFER+=" --name $NAME --group $GROUP --type $TYPE" #####################################################################
[ $SCWRYPTS_SHORTCUT ] && {
SCWRYPTS__ZSH_PLUGIN() {
local SCWRYPT_SELECTION NAME TYPE GROUP
__SCWRYPTS_PARSE || { zle accept-line; return 0; }
RBUFFER="scwrypts --name $NAME --type $TYPE --group $GROUP"
zle accept-line zle accept-line
} }
zle -N scwrypts SCWRYPTS__ZSH_PLUGIN zle -N scwrypts SCWRYPTS__ZSH_PLUGIN
bindkey $SCWRYPTS_SHORTCUT scwrypts bindkey $SCWRYPTS_SHORTCUT scwrypts
unset SCWRYPTS_SHORTCUT
}
##################################################################### #####################################################################
[ $SCWRYPTS_BUILDER_SHORTCUT ] && {
SCWRYPTS__ZSH_BUILDER_PLUGIN() { SCWRYPTS__ZSH_BUILDER_PLUGIN() {
local SCWRYPT_SELECTION=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS | FZF 'select a script' --header-lines 1) local SCWRYPT_SELECTION NAME TYPE GROUP
local NAME __SCWRYPTS_PARSE || { echo >&2; zle accept-line; return 0; }
local TYPE echo $SCWRYPT_SELECTION >&2
local GROUP
LBUFFER= RBUFFER=
[ ! $SCWRYPT_SELECTION ] && { zle accept-line; return 0; }
SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION scwrypts -n --name $NAME --group $GROUP --type $TYPE -- --help >&2 || {
scwrypts --name $NAME --group $GROUP --type $TYPE -- --help >&2 || {
zle accept-line zle accept-line
return 0 return 0
} }
echo echo
zle reset-prompt zle reset-prompt
which scwrypts >/dev/null 2>&1\ LBUFFER="scwrypts --name $NAME --type $TYPE --group $GROUP -- "
&& LBUFFER="scwrypts" || LBUFFER="$SCWRYPTS_ROOT/scwrypts"
LBUFFER+=" --name $NAME --group $GROUP --type $TYPE -- "
} }
zle -N scwrypts-builder SCWRYPTS__ZSH_BUILDER_PLUGIN zle -N scwrypts-builder SCWRYPTS__ZSH_BUILDER_PLUGIN
bindkey $SCWRYPTS_BUILDER_SHORTCUT scwrypts-builder bindkey $SCWRYPTS_BUILDER_SHORTCUT scwrypts-builder
unset SCWRYPTS_BUILDER_SHORTCUT
}
##################################################################### #####################################################################
[ $SCWRYPTS_ENV_SHORTCUT ] && {
SCWRYPTS__ZSH_PLUGIN_ENV() { SCWRYPTS__ZSH_PLUGIN_ENV() {
local RESET='reset' local RESET='reset'
local SELECTED=$(\ local SELECTED=$(\
{ [ $SCWRYPTS_ENV ] && echo $RESET; SCWRYPTS__GET_ENV_NAMES; } \ { [ $SCWRYPTS_ENV ] && echo $RESET; scwrypts --list-envs; } \
| FZF 'select an environment' \ | fzf --prompt 'select an environment : ' \
) )
zle clear-command-line zle clear-command-line
@ -67,3 +89,135 @@ SCWRYPTS__ZSH_PLUGIN_ENV() {
zle -N scwrypts-setenv SCWRYPTS__ZSH_PLUGIN_ENV zle -N scwrypts-setenv SCWRYPTS__ZSH_PLUGIN_ENV
bindkey $SCWRYPTS_ENV_SHORTCUT scwrypts-setenv bindkey $SCWRYPTS_ENV_SHORTCUT scwrypts-setenv
unset SCWRYPTS_ENV_SHORTCUT
}
#####################################################################
# badass(/terrifying?) zsh autocompletion
command -v compdef &>/dev/null && {
_scwrypts() {
echo $words | grep -q "\s--\s" && _arguments && return 0
eval "_arguments $(
{
HELP=$(scwrypts --help 2>&1 | sed -n 's/^\s\+\(-.* .\)/\1/p' | sed 's/[[]/(/g; s/[]]/)/g')
echo $HELP \
| sed 's/^\(\(-[^-\s]\),*\s*\|\)\(\(--[-a-z0-9A-Z\]*\)\s\(<\([^>]*\)>\|\)\|\)\s\+\(.*\)/\2[\7]:\6:->\2/' \
| grep -v '^[[]' \
;
echo $HELP \
| sed 's/^\(\(-[^-\s]\),*\s*\|\)\(\(--[-a-z0-9A-Z\]*\)\s\(<\([^>]*\)>\|\)\|\)\s\+\(.*\)/\4[\7]:\6:->\4/' \
| grep -v '^[[]' \
;
echo ":pattern:->pattern"
echo ":pattern:->pattern"
echo ":pattern:->pattern"
echo ":pattern:->pattern"
echo ":pattern:->pattern"
} | sed 's/::->.*$//g' | sed "s/\\(^\\|$\\)/'/g" | tr '\n' ' '
)"
local _group=''
echo $words | grep -q ' -g [^\s]' \
&& _group=$(echo $words | sed 's/.*-g \([^ ]\+\)\s*.*/\1/')
echo $words | grep -q ' --group .' \
&& _group=$(echo $words | sed 's/.*--group \([^ ]\+\)\s*.*/\1/')
local _type=''
echo $words | grep -q ' -t [^\s]' \
&& _type=$(echo $words | sed 's/.*-t \([^ ]\+\)\s*.*/\1/')
echo $words | grep -q ' --type .' \
&& _type=$(echo $words | sed 's/.*--type \([^ ]\+\)\s*.*/\1/')
local _name=''
echo $words | grep -q ' -m [^\s]' \
&& _name=$(echo $words | sed 's/.*-m \([^ ]\+\)\s*.*/\1/')
echo $words | grep -q ' --name .' \
&& _name=$(echo $words | sed 's/.*--name \([^ ]\+\)\s*.*/\1/')
local _pattern _patterns=()
[ ! $_name ] \
&& _patterns=($(echo "${words[@]:1}" | sed 's/\s\+/\n/g' | grep -v '^-'))
_get_remaining_scwrypts() {
[ $_name ] || local _name='[^ ]\+'
[ $_type ] || local _type='[^ ]\+'
[ $_group ] || local _group='[^ ]\+'
local remaining=$(\
scwrypts --list \
| sed "1d; s,\x1B\[[0-9;]*[a-zA-Z],,g" \
| grep "^$_name\s" \
| grep "\s$_group$" \
| grep "\s$_type\s" \
)
for _pattern in ${_patterns[@]}
do
remaining=$(echo "$remaining" | grep "$_pattern")
done
echo "$remaining"
}
case $state in
( -m | --name )
compadd $(_get_remaining_scwrypts | awk '{print $1;}' | sort -u)
;;
( -t | --type )
compadd $(_get_remaining_scwrypts | awk '{print $2;}' | sort -u)
;;
( -g | --group )
[[ $_name$_type$_group =~ ^$ ]] \
&& compadd $(scwrypts --list-groups) \
|| compadd $(_get_remaining_scwrypts | awk '{print $3;}' | sort -u) \
;;
( -e | --env )
compadd $(scwrypts --list-envs)
;;
( -v | --log-level )
local _help="$(\
scwrypts --help 2>&1 \
| sed -n '/-v, --log-level/,/^$/p' \
| sed -n 's/\s\+\([0-9]\) : \(.*\)/\1 -- \2/p' \
)"
eval "local _descriptions=($(echo "$_help" | sed "s/\\(^\|$\\)/'/g"))"
local _values=($(echo "$_help" | sed 's/ --.*//'))
compadd -d _descriptions -a _values
;;
( -o | --output )
compadd pretty json
;;
( pattern )
[[ $_name =~ ^$ ]] && {
local _remaining_scwrypts="$(_get_remaining_scwrypts)"
# stop providing suggestions if your pattern is sufficient
[[ $(echo $_remaining_scwrypts | wc -l) -le 1 ]] && return 0
local _remaining_patterns="$(echo "$_remaining_scwrypts" | sed 's/\s\+/\n/g; s|/|\n|g; s/-/\n/g;' | sort -u)"
for _pattern in ${_patterns[@]}
do
_remaining_patterns="$(echo "$_remaining_patterns" | grep -v "^$_pattern$")"
done
compadd $(echo $_remaining_patterns)
}
;;
( * ) ;;
esac
}
compdef _scwrypts scwrypts
}
__SCWRYPTS_PLUGIN_LOADED=true

177
scwrypts.scwrypts.zsh Normal file
View File

@ -0,0 +1,177 @@
#
# configuration for a scwrypts "group" or "plugin"
#
# this file defines the configuration for the 'scwrypts' group which
# is required for proper operation, but otherwise loads exactly like
# any other group/plugin
#
# 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
#
#####################################################################
### 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
} }
@ -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
##################################################################### #####################################################################

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