Compare commits

..

14 Commits

Author SHA1 Message Date
79adf18d9a fix bug with virtualenv loading 2024-02-18 02:35:10 -07:00
7ce71dfca8 verbosity is stupid lets call it log-level 2024-02-18 02:34:43 -07:00
3bcd4f3f6d refactor py/lib into python-scwrypts subproject 2024-02-18 02:31:13 -07:00
d4ef1c70e0 plugins/ci migration from v3 to v4 2024-02-12 23:45:37 -07:00
c9e107d2fd plugins/kubectl migration from v3 to v4 2024-02-12 23:45:05 -07:00
b6b4f2e5b8 FZF_(HEAD|TAIL) refactor to FZF_USER_INPUT 2024-02-12 23:44:43 -07:00
432593a0f3 update ZLE plugin so it no more make errors 2024-02-12 23:43:28 -07:00
6629caf459 FINALLY fix the weird cases for zsh/read builtin (particularly around reading one character from tty/pipe/file); also gave a --force-user-input flag in case you want to require user input on a yn prompt 2024-02-12 23:42:55 -07:00
8bcc99b898 improved i/o handling on the run executable means this is no longer relevant 2024-02-12 23:41:42 -07:00
05694ed022 bring some much-needed tender love and care to the scwrypts runner 2024-02-12 23:39:56 -07:00
67bd712590 v3-to-v4 upgrade docs 2024-02-12 23:39:00 -07:00
a90482de8c swap INFO for DEBUG 2024-02-07 15:16:51 -07:00
261bbee1a4 introduce --verbosity flag rather than mixed logging settings; correct color misnaming to ANSI convention; added sanity-check; simplified hello-world; created FZF_USER_INPUT to replace the confusing FZF_HEAD and FZF_TAIL 2024-02-07 15:14:36 -07:00
fcf492c661 basic runner format; write a MAIN function 2024-02-06 14:06:44 -07:00
315 changed files with 5034 additions and 20299 deletions

View File

@@ -1,352 +0,0 @@
---
version: 2.1
orbs:
python: circleci/python@2.1.1
executors:
archlinux:
docker:
- image: archlinux:base-devel
resource_class: small
working_directory: /
python:
docker:
- image: cimg/python:3.11
resource_class: small
nodejs:
docker:
- image: node:18
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:
require-full-semver:
executor: python
steps:
- run:
name: check CIRCLE_TAG for full semantic version
command: |
: \
&& [ $CIRCLE_TAG ] \
&& [[ $CIRCLE_TAG =~ ^v[0-9]*.[0-9]*.[0-9]*$ ]] \
;
aur-test:
executor: archlinux
steps:
- *archlinux-prepare
- *archlinux-temp-downgrade-fakeroot
- *archlinux-clone-aur
- *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:
executor: nodejs
working_directory: ~/scwrypts/zx/lib
steps:
- checkout:
path: ~/scwrypts
- restore_cache:
name: restore pnpm cache
keys:
- pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
- run:
name: pnpm install
command: |
corepack enable
corepack prepare pnpm@latest-8 --activate
pnpm config set store-dir .pnpm-store
pnpm install
- save_cache:
name: save pnpm cache
key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
paths:
- .pnpm-store
- run: pnpm test
- run: pnpm lint
- run: pnpm build
nodejs-publish:
executor: nodejs
working_directory: ~/scwrypts/zx/lib
steps:
- checkout:
path: ~/scwrypts
- restore_cache:
name: restore pnpm cache
keys:
- pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
- run:
name: pnpm install
command: |
corepack enable
corepack prepare pnpm@latest-8 --activate
pnpm config set store-dir .pnpm-store
pnpm install
- save_cache:
name: save pnpm cache
key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
paths:
- .pnpm-store
- run:
name: publish
command: |
: \
&& [ $CIRCLE_TAG ] \
&& pnpm build \
&& pnpm version $CIRCLE_TAG \
&& pnpm set //registry.npmjs.org/:_authToken=$NPM_TOKEN \
&& 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:
test:
jobs:
- aur-test:
&dev-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:
only: /^v\d+\.\d+\.\d+.*$/
branches:
ignore: /^.*$/
- aur-test:
&only-publish-for-full-semver
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:
filters: *only-run-on-full-semver-tag-filters
context: [npm-wrynegade]
requires:
- nodejs-test
- zsh-test
- zsh-test: *only-publish-for-full-semver

View File

@@ -1,89 +0,0 @@
#!/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,9 +12,13 @@ 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,6 +15,11 @@ 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

View File

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

View File

@@ -1,19 +0,0 @@
---
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,77 +1,60 @@
# *Scwrypts* # *Scwrypts* (Wryn + Scripts)
Scwrypts is a CLI and API for safely running scripts in the terminal, CI, and other automated environments. Scwrypts is a friendly CLI / API for quickly running *sandboxed scripts* in the terminal.
Local runs provide a user-friendly approach to quickly execute CI workflows and automations in your terminal. In modern developer / dev-ops workflows, scripts require a complex configurations.
Each local run runs through an interactive, *sandboxed environment* so you never accidentally run dev credentials in production ever again! 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.
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 4 to Version 5 Upgrade Path](./docs/upgrade/v4-to-v5.md) when upgrading from scwrypts v4 to scwrypts v5! Please refer to [Version 3 to Version 4 Upgrade Path](./docs/upgrade/v3-to-v4.md) when upgrading from scwrypts v3 to scwrypts v4!
## Installation ## Dependencies
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.
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) 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.
```bash ## Usage
# AUR Install Scwrypts by cloning this repository and sourcing `scwrypts.plugin.zsh` in your `zshrc`.
yay -Syu scwrypts You can now run Scwrypts using the ZLE hotkey bound to `SCWRYPTS_SHORTCUT` (default `CTRL + W`).
# homebrew ```console
brew install wrynegade/scwrypts % cd <path-to-cloned-repo>
% echo "source $(pwd)/scwrypts.plugin.zsh >> $HOME/.zshrc"
``` ```
### Manual Installation Check out [Meta Scwrypts](./zsh/scwrypts) to quickly set up environments and adjust configuration.
To install scwrypts manually, clone this repository (and take note of where it is installed)
Replacing the `/path/to/cloned-repo` appropriately, add the following line to your `~/.zshrc`: ### No Install / API Usage
Alternatively, the `scwrypts` API can be used directly:
```zsh ```zsh
source /path/to/cloned-repo/scwrypts.plugin.zsh ./scwrypts [--env environment-name] (...script-name-patterns...) [-- ...passthrough arguments... ]
``` ```
The next time you start your terminal, you can now execute scwrypts by using the plugin shortcut(s) (by default `CTRL + SPACE`). Given one or more script patterns, Scwrypts will filter the commands by pattern conjunction.
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 only one command is found which matches the pattern(s), it will immediately begin execution.
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.
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. Given no script patterns, Scwrypts becomes an interactive CLI, prompting the user to select a command.
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"
```
#### PATH Dependencies After determining which script to run, if no environment has been specified, Scwrypts prompts the user to choose one.
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
```
## Usage in CI and Automated Environments ### Using in CI/CD or Automated Workflows
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 directly from the current context. - **The Scwrypts sandbox environment will not load.** All variables will be read from 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 in the user's local cache - Logs will not be captured
- 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,7 +29,6 @@ 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
@@ -52,7 +51,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 \ $GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts -n \
--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
#!/usr/bin/env zsh #!/bin/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 ...
- echo.success "completed the stuff" - SUCCESS "completed the stuff"
+ MAIN() { + MAIN() {
+ echo "do some stuff here" + echo "do some stuff here"
+ # ... etc ... + # ... etc ...
+ echo.success "completed the stuff + 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() {
echo.success "look at me I'm so cool I already wrote this in a main function" 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
#!/usr/bin/env zsh #!/bin/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 ...
echo.success "completed the stuff 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
#!/usr/bin/env zsh #!/bin/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
#!/usr/bin/env zsh #!/bin/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

View File

@@ -1,136 +0,0 @@
# 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

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

View File

@@ -1,3 +1,10 @@
# CI Helper # Kubernetes `kubectl` Helper Plugin
Disabled by default, this is used in CI contexts to try and identify missing requirements for the current workflow. Leverages a local `redis` application to quickly and easily set an alias `k` for `kubectl --context <some-context> --namespace <some-namespace>`.
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 @@
#!/usr/bin/env zsh #!/bin/zsh
##################################################################### #####################################################################
MAIN() { MAIN() {
cd "$(scwrypts.config.group scwrypts root)" cd "$SCWRYPTS_ROOT__scwrypts/"
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
echo.status "checking dependencies for $group" 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]\+$'))
echo.status "discovered dependencies: ($DEPENDENCIES)" STATUS "discovered dependencies: ($DEPENDENCIES)"
echo $DEPENDENCIES | sed 's/ /\n/g' echo $DEPENDENCIES | sed 's/ /\n/g'
utils.check-environment && echo.success "all dependencies satisfied" CHECK_ENVIRONMENT && SUCCESS "all dependencies satisfied"
} }

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
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

@@ -1,18 +0,0 @@
#####################################################################
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

@@ -1,56 +0,0 @@
#####################################################################
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

@@ -1,17 +0,0 @@
#
# 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

@@ -1,23 +0,0 @@
${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

@@ -1,77 +0,0 @@
#####################################################################
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

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

View File

@@ -6,11 +6,10 @@ for CLI in kubectl helm flux
do do
eval "_${CLI[1]}() { eval "_${CLI[1]}() {
local SUBSESSION=0 local SUBSESSION=0
local SUBSESSION_OFFSET=2 echo \${words[2]} | grep -q '^[0-9]\\+$' && SUBSESSION=\${words[2]}
echo \${words[2]} | grep -q '^[0-9]\\+$' && SUBSESSION=\${words[2]} && SUBSESSION_OFFSET=3
local PASSTHROUGH_WORDS=($CLI) local PASSTHROUGH_WORDS=($CLI)
[[ \$CURRENT -gt \${SUBSESSION_OFFSET} ]] && echo \${words[\${SUBSESSION_OFFSET}]} | grep -qv '^[0-9]\\+$' && { [[ \$CURRENT -gt 2 ]] && echo \${words[2]} | 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)
@@ -27,8 +26,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
;; ;;
@@ -38,7 +37,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,18 +1,19 @@
[[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0 [[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0
unalias k h f >/dev/null 2>&1 unalias k h >/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 ] && {
echo.error "must set SCWRYPTS_ENV in order to use '$(echo $CLI | head -c1)'" ERROR "must set SCWRYPTS_ENV in order to use '$(echo $CLI | head -c1)'"
return 1 return 1
} }
which kube.redis >/dev/null 2>&1 \ which REDIS >/dev/null 2>&1 \
|| eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kube)" || eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kubectl)"
local CLI="$1"; shift 1 local CLI="$1"; shift 1
@@ -42,11 +43,11 @@ _SCWRYPTS_KUBECTL_DRIVER() {
local USAGE__args="$( local USAGE__args="$(
{ {
echo "$(utils.colors.print green '[0-9]')^if the first argument is a number 0-9, uses or creates a subsession (default 0)" 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 " ^ " echo " ^ "
for C in ${CUSTOM_COMMANDS[@]} for C in ${CUSTOM_COMMANDS[@]}
do do
echo "$(utils.colors.print green ${C})^$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__$C 2>/dev/null)" echo "\\033[0;32m$C\\033[0m^$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__$C 2>/dev/null)"
done done
} | column -ts '^' } | column -ts '^'
)" )"
@@ -67,7 +68,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 : $(utils.colors.print yellow ${SCWRYPTS_ENV}) currently : \\033[0;33m$SCWRYPTS_ENV\\033[0m
" "
@@ -134,9 +135,9 @@ _SCWRYPTS_KUBECTL_DRIVER() {
while [[ $# -gt 0 ]]; do USER_ARGS+=($1); shift 1; done while [[ $# -gt 0 ]]; do USER_ARGS+=($1); shift 1; done
utils.check-errors || return 1 CHECK_ERRORS --no-fail || return 1
[[ $HELP -eq 1 ]] && { utils.io.usage; return 0; } [[ $HELP -eq 1 ]] && { USAGE; return 0; }
##################################################################### #####################################################################
@@ -154,12 +155,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 ] || echo.error "missing kubectl 'context'" [ $CONTEXT ] || ERROR "missing kubectl 'context'"
[ $NAMESPACE ] || echo.error "missing kubectl 'namespace'" [ $NAMESPACE ] || ERROR "missing kubectl 'namespace'"
utils.check-errors --no-fail --no-usage || { CHECK_ERRORS --no-fail --no-usage || {
echo.error "with 'strict' settings enabled, context and namespace must be set!" ERROR "with 'strict' settings enabled, context and namespace must be set!"
echo.reminder " REMINDER "
these values can be set directly with these values can be set directly with
$(echo $CLI | head -c1) meta set (namespace|context) $(echo $CLI | head -c1) meta set (namespace|context)
" "
@@ -170,16 +171,16 @@ _SCWRYPTS_KUBECTL_DRIVER() {
[ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE) [ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE)
[[ $VERBOSE -eq 1 ]] && { [[ $VERBOSE -eq 1 ]] && {
echo.reminder " INFO "
context '$CONTEXT' context '$CONTEXT'
namespace '$NAMESPACE' namespace '$NAMESPACE'
environment '$SCWRYPTS_ENV' environment '$SCWRYPTS_ENV'
subsession '$SUBSESSION' subsession '$SUBSESSION'
" "
echo.status "running $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}" STATUS "running $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
} || { } || {
[[ $(_SCWRYPTS_KUBECTL_SETTINGS get context) =~ ^show$ ]] && { [[ $(_SCWRYPTS_KUBECTL_SETTINGS get context) =~ ^show$ ]] && {
echo.reminder "$SCWRYPTS_ENV.$SUBSESSION : $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}" INFO "$SCWRYPTS_ENV.$SUBSESSION : $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
} }
} }
$CLI ${CLI_ARGS[@]} ${USER_ARGS[@]} $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}
@@ -190,7 +191,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)
kube.redis h$1 ${SCWRYPTS_ENV}:kubectl:settings ${@:2} | grep . 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 $?
;; ;;
( * ) echo.error "cannot set '$2'" ;; * ) 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 ;;
( * ) echo.error "cannot get '$2'" ;; * ) 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 ;;
( * ) echo.error "target session must be a number [0-9]" ;; * ) 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) ;;
( * ) echo.error "no meta command '$1'" * ) 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=$(kube.redis get --prefix current:context | grep . || utils.colors.print bright-red "none set") local CONTEXT=$(REDIS get --prefix current:context | grep . || echo "\\033[1;31mnone set\\033[0m")
local NAMESPACE=$(kube.redis get --prefix current:namespace | grep . || utils.colors.print bright-red "none set") local NAMESPACE=$(REDIS get --prefix current:namespace | grep . || echo "\\033[1;31mnone set\\033[0m")
echo " echo "
environment : $SCWRYPTS_ENV environment : $SCWRYPTS_ENV
context : $CONTEXT context : $CONTEXT
@@ -92,53 +92,51 @@ 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 ]] && utils.colors.print green on || utils.colors.print bright-red off) strict mode : $([[ $STRICT -eq 1 ]] && echo "on" || echo "\\033[1;31moff\\033[0m")
" | sed 's/^ \+//' >&2 " | sed 's/^ \+//' >&2
return 0 return 0
} }
kube.redis get --prefix current:$2 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 )
: \ : \
&& scwrypts -n --name set-$2 --type zsh --group kube -- $3 --subsession $SUBSESSION >/dev/null \ && STATUS "copying $1 to $2" \
&& k $SUBSESSION meta get $2 \ && scwrypts -n --name set-context --type zsh --group kubectl -- --subsession $2 $(k meta get context | grep . || echo 'reset') \
&& scwrypts -n --name set-namespace --type zsh --group kubectl -- --subsession $2 $(k meta get namespace | grep . || echo 'reset') \
&& SUCCESS "subsession $1 copied to $2" \
; ;
;; ;;
( copy ) clear )
: \ scwrypts -n --name set-context --type zsh --group kubectl -- --subsession $SUBSESSION reset >/dev/null \
&& echo.status "copying $1 to $2" \ && SUCCESS "subsession $SUBSESSION reset to default"
&& 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" \
;
;; ;;
( clear ) show )
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 \
&& echo.success "now showing full command context" && SUCCESS "now showing full command context"
;; ;;
( hide ) hide )
_SCWRYPTS_KUBECTL_SETTINGS set context hide >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set context hide >/dev/null \
&& echo.success "now hiding command context" && SUCCESS "now hiding command context"
;; ;;
( loose ) loose )
_SCWRYPTS_KUBECTL_SETTINGS set strict 0 >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set strict 0 >/dev/null \
&& echo.warning "now running in 'loose' mode" && WARNING "now running in 'loose' mode"
;; ;;
( strict ) strict )
_SCWRYPTS_KUBECTL_SETTINGS set strict 1 >/dev/null \ _SCWRYPTS_KUBECTL_SETTINGS set strict 1 >/dev/null \
&& echo.success "now running in 'strict' mode" && SUCCESS "now running in 'strict' mode"
;; ;;
esac esac
} }

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,158 @@
#####################################################################
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

@@ -5,13 +5,16 @@ DEPENDENCIES+=(
docker docker
) )
REQUIRED_ENV+=() # TODO; allow custom redis configuration
export SCWRYPTS_KUBECTL_REDIS=managed
utils.environment.check SCWRYPTS_KUBECTL_REDIS --default managed REQUIRED_ENV+=(
SCWRYPTS_KUBECTL_REDIS
)
##################################################################### #####################################################################
kube.redis() { REDIS() {
[ ! $USAGE ] && local USAGE=" [ ! $USAGE ] && local USAGE="
usage: [...options...] usage: [...options...]
@@ -21,7 +24,7 @@ kube.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 kube.redis --get-static-definition output the static ZSH function definition for REDIS
additional arguments and options are passed through to 'redis-cli' additional arguments and options are passed through to 'redis-cli'
" "
@@ -36,14 +39,14 @@ kube.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
@@ -59,14 +62,14 @@ kube.redis() {
REDIS_ARGS+=(--raw) REDIS_ARGS+=(--raw)
[[ $ECHO_STATIC_DEFINITION -eq 1 ]] && { [[ $ECHO_STATIC_DEFINITION -eq 1 ]] && {
echo "kube.redis() {\ echo "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; \
@@ -78,9 +81,9 @@ kube.redis() {
redis-cli ${REDIS_ARGS[@]} ${USER_ARGS[@]} redis-cli ${REDIS_ARGS[@]} ${USER_ARGS[@]}
} }
kube.redis ping 2>/dev/null | grep -qi pong || { REDIS ping | 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 ] && echo.status 'refreshing redis instance' && docker rm -f $RPID [ $RPID ] && STATUS 'refreshing redis instance' && docker rm -f $RPID
unset RPID unset RPID
docker run \ docker run \
@@ -89,6 +92,6 @@ kube.redis ping 2>/dev/null | 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
echo.status 'awaiting redis connection' STATUS 'awaiting redis connection'
until kube.redis ping 2>/dev/null | grep -qi pong; do sleep 0.5; done until REDIS ping 2>/dev/null | grep -qi pong; do sleep 0.5; done
} }

View File

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

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env zsh #!/bin/zsh
use kubectl --group kube use kubectl --group kubectl
##################################################################### #####################################################################
MAIN() { MAIN() {
@@ -12,7 +12,7 @@ MAIN() {
options: options:
--context override context --context override context
--namespace override namespace --namespace override namespace
--subsession kube.redis subsession (default 0) --subsession 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 ) utils.io.usage; return 0 ;; -h | --help ) USAGE; return 0 ;;
* ) * )
[ $SERVICE ] && echo.error "unexpected argument '$2'" [ $SERVICE ] && ERROR "unexpected argument '$2'"
SERVICE=$1 SERVICE=$1
;; ;;
esac esac
shift 1 shift 1
done done
utils.check-errors --fail CHECK_ERRORS
kube.kubectl.serve KUBECTL__SERVE
} }

View File

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

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env zsh #!/bin/zsh
use kubectl --group kube use kubectl --group kubectl
##################################################################### #####################################################################
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 kube.redis subsession (default 0) --subsession 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 ) utils.io.usage; return 0 ;; -h | --help ) USAGE; return 0 ;;
* ) * )
[ $NAMESPACE ] && echo.error "unexpected argument '$2'" [ $NAMESPACE ] && ERROR "unexpected argument '$2'"
NAMESPACE=$1 NAMESPACE=$1
;; ;;
esac esac
shift 1 shift 1
done done
[ $NAMESPACE ] || NAMESPACE=$(kube.kubectl.namespace.select) [ $NAMESPACE ] || NAMESPACE=$(KUBECTL__SELECT_NAMESPACE)
[ $NAMESPACE ] || echo.error 'must provide or select a valid namespace' [ $NAMESPACE ] || ERROR 'must provide or select a valid namespace'
utils.check-errors --fail CHECK_ERRORS
kube.kubectl.namespace.set $NAMESPACE KUBECTL__SET_NAMESPACE $NAMESPACE
} }

3
py/lib/.gitignore vendored
View File

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

View File

@@ -36,6 +36,7 @@ dev = [
test = [ test = [
'pytest', 'pytest',
'mergedeep',
] ]
[project.urls] [project.urls]
@@ -55,5 +56,6 @@ source = 'versioningit'
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ['./'] packages = ['./']
[tool.versioningit.vcs] [tool.versioningit]
match = ['v[0-9]*.[0-9]*.[0-9]*'] match = ['v*']

View File

@@ -4,10 +4,6 @@ scwrypts
python library functions and invoker for scwrypts python library functions and invoker for scwrypts
''' '''
__all__ = [ from .scwrypts.execute import execute
'scwrypts', from .scwrypts.interactive import interactive
'execute', from .scwrypts.scwrypts import scwrypts
'interactive',
]
from .scwrypts import scwrypts, execute, interactive

View File

@@ -1,4 +1,6 @@
from io import StringIO from io import StringIO
#from string import ascii_letters, digits
from unittest.mock import patch
from pytest import raises from pytest import raises
@@ -54,7 +56,7 @@ def test_convert_to_yaml():
def test_convert_deep_json_to_yaml(): def test_convert_deep_json_to_yaml():
input_stream = generate('json', {**GENERATE_OPTIONS, 'depth': 4}) input_stream = StringIO(generate('json', {**GENERATE_OPTIONS, 'depth': 4}))
convert(input_stream, 'json', StringIO(), 'yaml') convert(input_stream, 'json', StringIO(), 'yaml')
def test_convert_deep_yaml_to_json(): def test_convert_deep_yaml_to_json():

View File

@@ -1,27 +1,10 @@
from json import loads from os import getenv as os_getenv
from .scwrypts import scwrypts
from .scwrypts.exceptions import MissingVariableError from .scwrypts.exceptions import MissingVariableError
ENV = {}
def getenv(name, required=True, default=None): def getenv(name, required=True):
if ENV.get('configuration') is None or ENV.get('environment') is None: value = os_getenv(name, 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

@@ -1,14 +1,11 @@
from requests import request from requests import request
CLIENTS = {}
def get_request_client(base_url, headers=None): def get_request_client(base_url, headers=None):
if CLIENTS.get(base_url, None) is None:
if headers is None: if headers is None:
headers = {} headers = {}
CLIENTS[base_url] = lambda method, endpoint, **kwargs: request( return lambda method, endpoint, **kwargs: request(
method = method, method = method,
url = f'{base_url}/{endpoint}', url = f'{base_url}/{endpoint}',
headers = { headers = {
@@ -21,5 +18,3 @@ def get_request_client(base_url, headers=None):
if key != 'headers' if key != 'headers'
}, },
) )
return CLIENTS[base_url]

View File

@@ -1,40 +0,0 @@
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test import get_generator
from scwrypts.test.character_set import uri
generate = get_generator({
'str_length_minimum': 8,
'str_length_maximum': 128,
'uuid_output_type': str,
})
def get_request_client_sample_data():
return {
'base_url' : generate(str, {'character_set': uri}),
'endpoint' : generate(str, {'character_set': uri}),
'method' : generate(str),
'response' : generate('requests_Response', {'depth': 4}),
'payload' : generate(dict, {
'depth': 1,
'data_types': { str, 'uuid' },
}),
}
@fixture(name='sample')
def fixture_sample():
return SimpleNamespace(
**get_request_client_sample_data(),
headers = generate(dict, {
'depth': 1,
'data_types': { str, 'uuid' },
}),
payload_headers = generate(dict, {
'depth': 1,
'data_types': { str, 'uuid' },
}),
)

View File

@@ -1,22 +1,4 @@
''' from .client import *
basic scwrypts.http client for directus
configured by setting DIRECTUS__BASE_URL and DIRECTUS__API_TOKEN in
scwrypts environment
'''
__all__ = [
'request',
'graphql',
'get_collections',
'get_fields',
'FILTER_OPERATORS',
]
from .client import request
from .graphql import graphql
from .collections import get_collections
from .fields import get_fields
FILTER_OPERATORS = { FILTER_OPERATORS = {
'_eq', '_eq',

View File

@@ -3,10 +3,55 @@ from scwrypts.env import getenv
from .. import get_request_client from .. import get_request_client
REQUEST = None
COLLECTIONS = None
FIELDS = {}
def request(method, endpoint, **kwargs): def request(method, endpoint, **kwargs):
return get_request_client( global REQUEST # pylint: disable=global-statement
if REQUEST is None:
REQUEST = get_request_client(
base_url = getenv("DIRECTUS__BASE_URL"), base_url = getenv("DIRECTUS__BASE_URL"),
headers = { headers = {
'Authorization': f'bearer {getenv("DIRECTUS__API_TOKEN")}', 'Authorization': f'bearer {getenv("DIRECTUS__API_TOKEN")}',
} }
)(method, endpoint, **kwargs) )
return REQUEST(method, endpoint, **kwargs)
def graphql(query, system=False):
return request(
'POST',
'graphql' if system is True else 'graphql/system',
json={'query': query},
)
def get_collections():
global COLLECTIONS # pylint: disable=global-statement
if COLLECTIONS is None:
COLLECTIONS = [
item['collection']
for item in request(
'GET',
'collections?limit=-1&fields[]=collection',
).json()['data']
]
return COLLECTIONS
def get_fields(collection):
if FIELDS.get(collection) is None:
FIELDS[collection] = [
item['field']
for item in request(
'GET',
f'fields/{collection}?limit=-1&fields[]=field',
).json()['data']
]
return FIELDS[collection]

View File

@@ -1,18 +0,0 @@
from .client import request
COLLECTIONS = None
def get_collections():
global COLLECTIONS # pylint: disable=global-statement
if COLLECTIONS is None:
COLLECTIONS = [
item['collection']
for item in request(
'GET',
'collections?limit=-1&fields[]=collection',
).json()['data']
]
return COLLECTIONS

View File

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

View File

@@ -1,16 +0,0 @@
from .client import request
FIELDS = {}
def get_fields(collection):
if FIELDS.get(collection) is None:
FIELDS[collection] = [
item['field']
for item in request(
'GET',
f'fields/{collection}?limit=-1&fields[]=field',
).json()['data']
]
return FIELDS[collection]

View File

@@ -1,9 +0,0 @@
from .client import request
def graphql(query, system=False):
return request(
'POST',
'graphql' if system is False else 'graphql/system',
json={'query': query},
)

View File

@@ -1,43 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .client import request
def test_directus_request(sample, _response):
assert _response == sample.response
def test_directus_request_client_setup(sample, _response, mock_get_request_client):
mock_get_request_client.assert_called_once_with(
base_url = sample.base_url,
headers = { 'Authorization': f'bearer {sample.api_token}' },
)
#####################################################################
@fixture(name='_response')
def fixture_response(sample):
return request(
method = sample.method,
endpoint = sample.endpoint,
**sample.payload,
)
#####################################################################
@fixture(name='mock_getenv', autouse=True)
def fixture_mock_getenv(sample):
with patch('scwrypts.http.directus.client.getenv',) as mock:
mock.side_effect = lambda name: {
'DIRECTUS__BASE_URL': sample.base_url,
'DIRECTUS__API_TOKEN': sample.api_token,
}[name]
yield mock
@fixture(name='mock_get_request_client', autouse=True)
def fixture_mock_get_request_client(sample):
with patch('scwrypts.http.directus.client.get_request_client') as mock:
mock.return_value = lambda method, endpoint, **kwargs: sample.response
yield mock

View File

@@ -1,45 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .graphql import graphql
def test_directus_graphql(sample, _response, _mock_request):
assert _response == sample.response
def test_directus_graphql_request_payload(sample, _response, _mock_request):
_mock_request.assert_called_once_with(
'POST',
'graphql',
json = {'query': sample.query},
)
def test_directus_graphql_system(sample, _response_system):
assert _response_system == sample.response
def test_directus_graphql_system_request_payload(sample, _response_system, _mock_request):
_mock_request.assert_called_once_with(
'POST',
'graphql/system',
json = {'query': sample.query},
)
#####################################################################
@fixture(name='_response')
def fixture_response(sample, _mock_request):
return graphql(sample.query)
@fixture(name='_response_system')
def fixture_response_system(sample, _mock_request):
return graphql(sample.query, system=True)
#####################################################################
@fixture(name='_mock_request')
def fixture_mock_request(sample):
with patch('scwrypts.http.directus.graphql.request') as mock:
mock.return_value = sample.response
yield mock

View File

@@ -1,14 +1,2 @@
''' from .client import *
basic scwrypts.http client for discord from .send_message import *
configured by setting various DISCORD__* options in the
scwrypts environment
'''
__all__ = [
'request',
'send_message',
]
from .client import request
from .send_message import send_message

View File

@@ -1,15 +1,20 @@
from scwrypts.env import getenv from scwrypts.env import getenv
from scwrypts.http import get_request_client
from .. import get_request_client REQUEST = None
def request(method, endpoint, **kwargs): def request(method, endpoint, **kwargs):
global REQUEST # pylint: disable=global-statement
if REQUEST is None:
headers = {} headers = {}
if (token := getenv("DISCORD__BOT_TOKEN", required = False)) is not None: if (token := getenv("DISCORD__BOT_TOKEN", required = False)) is not None:
headers['Authorization'] = f'Bot {token}' headers['Authorization'] = f'Bot {token}'
return get_request_client( REQUEST = get_request_client(
base_url = 'https://discord.com/api', base_url = 'https://discord.com/api',
headers = headers, headers = headers,
)(method, endpoint, **kwargs) )
return REQUEST(method, endpoint, **kwargs)

View File

@@ -1,24 +0,0 @@
from string import ascii_letters, digits
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test.character_set import uri
from ..conftest import generate, get_request_client_sample_data
@fixture(name='sample')
def fixture_sample():
return SimpleNamespace(
**{
**get_request_client_sample_data(),
'base_url': 'https://discord.com/api',
},
bot_token = generate(str, {'character_set': uri}),
username = generate(str, {'character_set': ascii_letters + digits}),
avatar_url = generate(str, {'character_set': uri}),
webhook = generate(str, {'character_set': uri}),
channel_id = generate(str, {'character_set': uri}),
content_header = generate(str),
content_footer = generate(str),
content = generate(str),
)

View File

@@ -1,54 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .client import request
def test_discord_request(sample, _mock_getenv, _response):
assert _response == sample.response
def test_discord_request_client_setup(sample, mock_get_request_client, _mock_getenv, _response):
mock_get_request_client.assert_called_once_with(
base_url = sample.base_url,
headers = { 'Authorization': f'Bot {sample.bot_token}' },
)
def test_discord_request_client_setup_public(sample, mock_get_request_client, _mock_getenv_optional, _response):
mock_get_request_client.assert_called_once_with(
base_url = sample.base_url,
headers = {},
)
#####################################################################
@fixture(name='_response')
def fixture_response(sample):
return request(
method = sample.method,
endpoint = sample.endpoint,
**sample.payload,
)
#####################################################################
@fixture(name='mock_get_request_client', autouse=True)
def fixture_mock_get_request_client(sample):
with patch('scwrypts.http.discord.client.get_request_client') as mock:
mock.return_value = lambda method, endpoint, **kwargs: sample.response
yield mock
@fixture(name='_mock_getenv')
def fixture_mock_getenv(sample):
with patch('scwrypts.http.discord.client.getenv') as mock:
mock.side_effect = lambda name, **kwargs: {
'DISCORD__BOT_TOKEN': sample.bot_token,
}[name]
yield mock
@fixture(name='_mock_getenv_optional')
def fixture_mock_getenv_optional():
with patch('scwrypts.http.discord.client.getenv') as mock:
mock.side_effect = lambda name, **kwargs: None
yield mock

View File

@@ -1,91 +0,0 @@
from unittest.mock import patch
from pytest import fixture, raises
from .send_message import send_message
def test_discord_send_message(sample, mock_request, _mock_getenv):
expected = get_default_called_with(sample)
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_without_username(sample, mock_request, _mock_getenv):
sample.username = None
expected = get_default_called_with(sample)
del expected['json']['username']
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_without_avatar_url(sample, mock_request, _mock_getenv):
sample.avatar_url = None
expected = get_default_called_with(sample)
del expected['json']['avatar_url']
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_to_channel_id(sample, mock_request, _mock_getenv):
sample.webhook = None
expected = get_default_called_with(sample)
expected['endpoint'] = f'channels/{sample.channel_id}/messages'
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_without_content_header(sample, mock_request, _mock_getenv):
sample.content_header = None
expected = get_default_called_with(sample)
expected['json']['content'] = f'{sample.content}{sample.content_footer}'
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_without_content_footer(sample, mock_request, _mock_getenv):
sample.content_footer = None
expected = get_default_called_with(sample)
expected['json']['content'] = f'{sample.content_header}{sample.content}'
assert send_message(sample.content) == sample.response
mock_request.assert_called_once_with(**expected)
def test_discord_send_message_error(sample, mock_request, _mock_getenv):
with raises(ValueError):
sample.webhook = None
sample.channel_id = None
send_message(sample.content)
#####################################################################
def get_default_called_with(sample):
return {
'method': 'POST',
'endpoint': f'webhooks/{sample.webhook}',
'json': {
'content': f'{sample.content_header}{sample.content}{sample.content_footer}',
'username': sample.username,
'avatar_url': sample.avatar_url,
},
}
@fixture(name='mock_request', autouse=True)
def fixture_mock_request(sample):
with patch('scwrypts.http.discord.send_message.request') as mock:
mock.return_value = sample.response
yield mock
@fixture(name='_mock_getenv')
def fixture_mock_getenv(sample):
with patch('scwrypts.http.discord.send_message.getenv',) as mock:
mock.side_effect = lambda name, **kwargs: {
'DISCORD__DEFAULT_USERNAME': sample.username,
'DISCORD__DEFAULT_AVATAR_URL': sample.avatar_url,
'DISCORD__DEFAULT_WEBHOOK': sample.webhook,
'DISCORD__DEFAULT_CHANNEL_ID': sample.channel_id,
'DISCORD__CONTENT_HEADER': sample.content_header,
'DISCORD__CONTENT_FOOTER': sample.content_footer,
}[name]
yield mock

View File

@@ -1,14 +1 @@
''' from .client import *
basic scwrypts.http client for linear
configured by setting the LINEAR__API_TOKEN option in the
scwrypts environment
'''
__all__ = [
'request',
'graphql',
]
from .client import request
from .graphql import graphql

View File

@@ -2,11 +2,20 @@ from scwrypts.env import getenv
from .. import get_request_client from .. import get_request_client
REQUEST = None
def request(method, endpoint, **kwargs): def request(method, endpoint, **kwargs):
return get_request_client( global REQUEST # pylint: disable=global-statement
if REQUEST is None:
REQUEST = get_request_client(
base_url = 'https://api.linear.app', base_url = 'https://api.linear.app',
headers = { headers = {
'Authorization': f'bearer {getenv("LINEAR__API_TOKEN")}', 'Authorization': f'bearer {getenv("LINEAR__API_TOKEN")}',
}, }
)(method, endpoint, **kwargs) )
return REQUEST(method, endpoint, **kwargs)
def graphql(query):
return request('POST', 'graphql', json={'query': query})

View File

@@ -1,18 +0,0 @@
from string import ascii_letters, digits
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test.character_set import uri
from ..conftest import generate, get_request_client_sample_data
@fixture(name='sample')
def fixture_sample():
return SimpleNamespace(
**{
**get_request_client_sample_data(),
'base_url': 'https://api.linear.app',
},
api_token = generate(str, {'character_set': uri}),
query = generate(str),
)

View File

@@ -1,5 +0,0 @@
from .client import request
def graphql(query):
return request('POST', 'graphql', json={'query': query})

View File

@@ -1,42 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .client import request
def test_discord_request(sample, _response):
assert _response == sample.response
def test_discord_request_client_setup(sample, mock_get_request_client, _response):
mock_get_request_client.assert_called_once_with(
base_url = sample.base_url,
headers = { 'Authorization': f'bearer {sample.api_token}' },
)
#####################################################################
@fixture(name='_response')
def fixture_response(sample):
return request(
method = sample.method,
endpoint = sample.endpoint,
**sample.payload,
)
#####################################################################
@fixture(name='mock_get_request_client', autouse=True)
def fixture_mock_get_request_client(sample):
with patch('scwrypts.http.linear.client.get_request_client') as mock:
mock.return_value = lambda method, endpoint, **kwargs: sample.response
yield mock
@fixture(name='mock_getenv', autouse=True)
def fixture_mock_getenv(sample):
with patch('scwrypts.http.linear.client.getenv',) as mock:
mock.side_effect = lambda name, **kwargs: {
'LINEAR__API_TOKEN': sample.api_token,
}[name]
yield mock

View File

@@ -1,35 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .graphql import graphql
def test_directus_graphql(sample, _response, _mock_request):
assert _response == sample.response
def test_directus_graphql_request_payload(sample, _response, _mock_request):
_mock_request.assert_called_once_with(
'POST',
'graphql',
json = {'query': sample.query},
)
#####################################################################
@fixture(name='_response')
def fixture_response(sample, _mock_request):
return graphql(sample.query)
@fixture(name='_response_system')
def fixture_response_system(sample, _mock_request):
return graphql(sample.query)
#####################################################################
@fixture(name='_mock_request')
def fixture_mock_request(sample):
with patch('scwrypts.http.linear.graphql.request') as mock:
mock.return_value = sample.response
yield mock

View File

@@ -1,55 +0,0 @@
from unittest.mock import patch
from pytest import fixture
from .client import get_request_client
def test_request_client(sample, _response_basic):
assert _response_basic == sample.response
def test_request_client_forwards_default_headers(sample, mock_request, _response_basic):
mock_request.assert_called_once_with(
method = sample.method,
url = f'{sample.base_url}/{sample.endpoint}',
headers = sample.headers,
)
def test_get_request_client_payload(sample, _response_payload):
assert _response_payload == sample.response
def test_request_client_forwards_payload_headers(sample, mock_request, _response_payload):
assert mock_request.call_args.kwargs['headers'] == sample.headers | sample.payload_headers
#####################################################################
@fixture(name='mock_request', autouse=True)
def fixture_mock_request(sample):
with patch('scwrypts.http.client.request') as mock:
mock.return_value = sample.response
yield mock
@fixture(name='request_client', autouse=True)
def fixture_request_client(sample):
return get_request_client(sample.base_url, sample.headers)
#####################################################################
@fixture(name='_response_basic')
def fixture_response_basic(sample, request_client):
return request_client(
method = sample.method,
endpoint = sample.endpoint,
)
@fixture(name='_response_payload')
def fixture_response_payload(sample, request_client):
return request_client(
method = sample.method,
endpoint = sample.endpoint,
**{
**sample.payload,
'headers': sample.payload_headers,
},
)

View File

@@ -5,46 +5,6 @@ from sys import stdin, stdout, stderr
from scwrypts.env import getenv from scwrypts.env import getenv
@contextmanager
def get_combined_stream(input_file=None, output_file=None):
'''
context manager to open an "input_file" and "output_file"
But the "files" can be pipe-streams, stdin/stdout, or even
actual files! Helpful when trying to write CLI scwrypts
which would like to accept all kinds of input and output
configurations.
'''
with get_stream(input_file, 'r') as input_stream, get_stream(output_file, 'w+') as output_stream:
yield CombinedStream(input_stream, output_stream)
def add_io_arguments(parser, allow_input=True, allow_output=True):
'''
slap these puppies onto your argparse.ArgumentParser to
allow easy use of the get_combined_stream at the command line
'''
if allow_input:
parser.add_argument(
'-i', '--input-file',
dest = 'input_file',
default = None,
help = 'path to input file; omit for stdin',
required = False,
)
if allow_output:
parser.add_argument(
'-o', '--output-file',
dest = 'output_file',
default = None,
help = 'path to output file; omit for stdout',
required = False,
)
#####################################################################
@contextmanager @contextmanager
def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwargs): def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwargs):
allowed_modes = {'r', 'w', 'w+'} allowed_modes = {'r', 'w', 'w+'}
@@ -74,6 +34,32 @@ def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwarg
stdout.flush() stdout.flush()
def add_io_arguments(parser, allow_input=True, allow_output=True):
if allow_input:
parser.add_argument(
'-i', '--input-file',
dest = 'input_file',
default = None,
help = 'path to input file; omit for stdin',
required = False,
)
if allow_output:
parser.add_argument(
'-o', '--output-file',
dest = 'output_file',
default = None,
help = 'path to output file; omit for stdout',
required = False,
)
@contextmanager
def get_combined_stream(input_file=None, output_file=None):
with get_stream(input_file, 'r') as input_stream, get_stream(output_file, 'w+') as output_stream:
yield CombinedStream(input_stream, output_stream)
class CombinedStream: class CombinedStream:
def __init__(self, input_stream, output_stream): def __init__(self, input_stream, output_stream):
self.input = input_stream self.input = input_stream

View File

@@ -1,23 +0,0 @@
'''
scwrypts meta-configuration
provides a helpful three ways to run "scwrypts"
'scwrypts' is an agnostic, top-level executor allowing any scwrypt to be called from python workflows
'execute' is the default context set-up for python-based scwrypts
'interactive' is a context set-up for interactive, python-based scwrypts
after execution, you are dropped in a bpython shell with all the variables
configured during main() execution
'''
__all__ = [
'scwrypts',
'execute',
'interactive',
]
from .scwrypts import scwrypts
from .execute import execute
from .interactive import interactive

View File

@@ -13,14 +13,4 @@ class MissingFlagAndEnvironmentVariableError(EnvironmentError, ArgumentError):
class MissingScwryptsExecutableError(EnvironmentError): class MissingScwryptsExecutableError(EnvironmentError):
def __init__(self): def __init__(self):
super().__init__('scwrypts must be installed and available on your PATH') super().__init__(f'scwrypts must be installed and available on your PATH')
class BadScwryptsLookupError(ValueError):
def __init__(self):
super().__init__('must provide name/group/type or scwrypt lookup patterns')
class MissingScwryptsGroupOrTypeError(ValueError):
def __init__(self, group, _type):
super().__init__(f'missing required group or type (group={group} | type={_type}')

View File

@@ -2,57 +2,29 @@ from os import getenv
from shutil import which from shutil import which
from subprocess import run from subprocess import run
from .exceptions import MissingScwryptsExecutableError, BadScwryptsLookupError, MissingScwryptsGroupOrTypeError from .exceptions import MissingScwryptsExecutableError
def scwrypts(patterns=None, args=None, executable_args=None, name=None, group=None, _type=None): def scwrypts(name, group, _type, *args, log_level=None):
''' '''
top-level scwrypts invoker from python invoke non-python scwrypts from python
patterns str / list pattern-based scwrypt 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)
name str exact scwrypt lookup name (requires group and _type)
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
''' '''
if patterns is None and name is None: executable = which('scwrypts')
raise BadScwryptsLookupError()
if name is not None and (group is None or _type is None):
raise MissingScwryptsGroupOrTypeError(group, _type)
executable = which(getenv('SCWRYPTS_EXECUTABLE', 'scwrypts'))
if executable is None: if executable is None:
raise MissingScwryptsExecutableError() raise MissingScwryptsExecutableError()
lookup = _parse(patterns) if name is None else f'--name {name} --group {group} --type {_type}' pre_args = ''
if log_level is not None:
pre_args += '--log-level {log_level}'
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} {_parse(executable_args)} {lookup} -- {_parse(args)}', f'SUBSCWRYPT={depth} {executable} --name {name} --group {group} --type {_type} {pre_args} -- {" ".join(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

@@ -1,184 +0,0 @@
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

@@ -1,10 +1 @@
''' from .random_generator import generate
automated testing utilties, but primarily a random data generator
'''
__all__ = [
'generate',
]
from .generate import generate, get_generator
from .character_set import *

View File

@@ -1,13 +0,0 @@
'''
string constants typically used for randomly generated data
the 'string' standard library already contains many character sets,
but not these :)
'''
__all__ = [
'uri',
]
uri = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&\'()*+,;='

View File

@@ -1,389 +0,0 @@
from csv import writer, QUOTE_NONNUMERIC
from io import StringIO
from json import dumps, loads
from random import randint, uniform, choice
from re import sub
from string import printable
from typing import Callable
from uuid import uuid4
from collections.abc import Hashable
from requests import Response, status_codes
from yaml import safe_dump
from .exceptions import NoDataTypeError, BadGeneratorTypeError
DEFAULT_OPTIONS = {
'data_types': None,
'minimum': 0,
'maximum': 64,
'depth': 1,
'character_set': None,
'bool_nullable': False,
'str_length': None,
'str_length_minimum': 0,
'str_length_maximum': 64,
'uuid_output_type': 'uuid', # str or 'uuid'
'list_length': 8,
'set_length': 8,
'dict_length': 8,
'csv_bool_nullable': True,
'csv_columns': None,
'csv_columns_minimum': 1,
'csv_columns_maximum': 16,
'csv_rows': None,
'csv_rows_minimum': 2,
'csv_rows_maximum': 16,
'csv_output_type': 'stringio', # str or 'stringio'
'json_initial_type': dict, # typically dict or list
'json_bool_nullable': True,
'json_output_type': 'stringio', # str or 'stringio'
'yaml_initial_type': dict, # typically dict or list
'yaml_bool_nullable': True,
'yaml_use_default_flow_style': False,
'yaml_output_type': 'stringio', # str or 'stringio'
'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):
'''
generate random data with the call of a function
use data_type to generate a single value
use options to set generation options (key = type, value = kwargs)
use options.data_types and omit data_type to generate a random type
'''
if options is None:
options = {}
options = DEFAULT_OPTIONS | options
if data_type is None:
if options['data_types'] is None or len(options['data_types']) == 0:
raise NoDataTypeError()
return generate(
data_type=choice(list(options['data_types'])),
options=options,
)
if not isinstance(data_type, str):
data_type = data_type.__name__
if data_type not in Generator.get_supported_data_types():
raise BadGeneratorTypeError(data_type)
return getattr(Generator, f'_{data_type}')(options)
#####################################################################
SUPPORTED_DATA_TYPES = None
class Generator:
@classmethod
def get_supported_data_types(cls):
global SUPPORTED_DATA_TYPES # pylint: disable=global-statement
if SUPPORTED_DATA_TYPES is None:
SUPPORTED_DATA_TYPES = {
sub('^_', '', data_type)
for data_type, method in Generator.__dict__.items()
if isinstance(method, staticmethod)
}
return SUPPORTED_DATA_TYPES
#####################################################################
@classmethod
def filter_data_types(cls, options, filters=None):
'''
returns an options dict with appropriately filtered data_types
if data_types are not yet defined, starts with all supported data_types
'''
if options['data_types'] is None:
options['data_types'] = Generator.get_supported_data_types()
if filters is None or len(filters) == 0:
return options
return {
**options,
'data_types': set(filter(
lambda data_type: all(( f(data_type, options) for f in filters )),
options['data_types'],
)),
}
class Filters:
@staticmethod
def hashable(data_type, _options):
if isinstance(data_type, Callable):
return isinstance(data_type(), Hashable)
if not isinstance(data_type, str):
data_type = data_type.__name__
return data_type in { 'bool', 'int', 'float', 'chr', 'str', 'uuid' }
@staticmethod
def filelike(data_type, _options):
return data_type in { 'csv', 'json', 'yaml' }
@staticmethod
def complex(data_type, _options):
return data_type in { 'requests_Response' }
@staticmethod
def basic(data_type, options):
return all([
not Generator.Filters.filelike(data_type, options),
not Generator.Filters.complex(data_type, options),
])
@staticmethod
def pythonset(data_type, _options):
if not isinstance(data_type, str):
data_type = data_type.__name__
return data_type == 'set'
@staticmethod
def csvsafe(data_type, options):
options['depth'] = max(1, options['depth'])
return all([
Generator.Filters.basic(data_type, options),
not Generator.Filters.pythonset(data_type, options),
])
@staticmethod
def jsonsafe(data_type, options):
return all([
Generator.Filters.basic(data_type, options),
not Generator.Filters.pythonset(data_type, options),
])
@staticmethod
def yamlsafe(data_type, options):
return all([
Generator.Filters.basic(data_type, options),
not Generator.Filters.pythonset(data_type, options),
])
#####################################################################
@classmethod
def get_option_with_range(cls, options, option_key, data_type=int):
'''
typically an integer range, allows both:
- setting a fixed configuration (e.g. 'str_length')
- allowing a configuration range (e.g. 'str_length_minimum' and 'str_length_maximum')
'''
fixed = options.get(option_key, None)
if fixed is not None:
return fixed
return generate(data_type, {
'minimum': options[f'{option_key}_minimum'],
'maximum': options[f'{option_key}_maximum'],
})
#####################################################################
@staticmethod
def _bool(options):
return choice([True, False, None]) if options['bool_nullable'] else choice([True, False])
@staticmethod
def _int(options):
return randint(options['minimum'], options['maximum'])
@staticmethod
def _float(options):
return uniform(options['minimum'], options['maximum'])
@staticmethod
def _chr(options):
character_set = options['character_set']
return choice(character_set) if character_set is not None else chr(randint(0,65536))
@staticmethod
def _str(options):
return ''.join((
generate(chr, options)
for _ in range(Generator.get_option_with_range(options, 'str_length'))
))
@staticmethod
def _uuid(options):
'''
creates a UUID object or a str containing a uuid (v4)
'''
uuid = uuid4()
return str(uuid) if options['uuid_output_type'] == str else uuid
@staticmethod
def _list(options):
if options['depth'] <= 0:
return []
options['depth'] -= 1
options = Generator.filter_data_types(options, [
Generator.Filters.basic,
])
return [ generate(None, {**options}) for _ in range(options['list_length']) ]
@staticmethod
def _set(options):
if options['depth'] <= 0:
return set()
options['depth'] -= 1
options = Generator.filter_data_types(options, [
Generator.Filters.hashable,
])
return { generate(None, options) for _ in range(options['set_length']) }
@staticmethod
def _dict(options):
if options['depth'] <= 0:
return {}
options['depth'] -= 1
options = Generator.filter_data_types(options, [
Generator.Filters.basic,
])
key_options = Generator.filter_data_types(options, [
Generator.Filters.hashable,
])
if len(options['data_types']) == 0 or len(key_options['data_types']) == 0:
return {}
return {
generate(None, key_options): generate(None, options)
for _ in range(options['dict_length'])
}
@staticmethod
def _csv(options):
'''
creates a StringIO object containing csv data
'''
if options['character_set'] is None:
options['character_set'] = printable
options['bool_nullable'] = options['csv_bool_nullable']
options = Generator.filter_data_types(options, [
Generator.Filters.csvsafe,
])
columns = Generator.get_option_with_range(options, 'csv_columns')
rows = Generator.get_option_with_range(options, 'csv_rows')
csv = StringIO()
csv_writer = writer(csv, quoting=QUOTE_NONNUMERIC)
options['list_length'] = columns
[ # pylint: disable=expression-not-assigned
csv_writer.writerow(generate(list, options))
for _ in range(rows)
]
csv.seek(0)
return csv.getvalue() if options['csv_output_type'] == str else csv
@staticmethod
def _json(options):
'''
creates a StringIO object or str containing json data
'''
if options['character_set'] is None:
options['character_set'] = printable
options['bool_nullable'] = options['json_bool_nullable']
options['uuid_output_type'] = str
options = Generator.filter_data_types(options, [
Generator.Filters.jsonsafe,
])
json = dumps(generate(
options['json_initial_type'],
{**options},
))
return json if options['json_output_type'] == str else StringIO(json)
@staticmethod
def _yaml(options):
'''
creates a StringIO object or str containing yaml data
'''
if options['character_set'] is None:
options['character_set'] = printable
options['bool_nullable'] = options['yaml_bool_nullable']
options['uuid_output_type'] = str
options = Generator.filter_data_types(options, [
Generator.Filters.yamlsafe,
])
yaml = StringIO()
safe_dump(
generate(options['yaml_initial_type'], {**options}),
yaml,
default_flow_style=options['yaml_use_default_flow_style'],
)
yaml.seek(0)
return yaml.getvalue() if options['yaml_output_type'] == str else yaml
@staticmethod
def _requests_Response(options):
'''
creates a requests.Response-like object containing json data
'''
options['json_output_type'] = str
response = Response()
response.status_code = options['requests_response_status_code']
json = loads(generate('json', options))
response.json = lambda: json
return response

View File

@@ -0,0 +1,240 @@
from csv import writer, QUOTE_NONNUMERIC
from io import StringIO
from json import dumps
from random import randint, uniform, choice
from re import sub
from string import printable
from yaml import safe_dump
from .exceptions import NoDataTypeError, BadGeneratorTypeError
SUPPORTED_DATA_TYPES = None
DEFAULT_OPTIONS = {
'data_types': None,
'minimum': 0,
'maximum': 64,
'depth': 1,
'character_set': None,
'bool_nullable': False,
'str_length': None,
'str_minimum_length': 0,
'str_maximum_length': 32,
'list_length': 8,
'set_length': 8,
'dict_length': 8,
'dict_key_types': {int, float, chr, str},
'csv_columns': None,
'csv_columns_minimum': 1,
'csv_columns_maximum': 16,
'csv_rows': None,
'csv_rows_minimum': 2,
'csv_rows_maximum': 16,
'json_initial_type': dict,
'yaml_initial_type': dict,
}
def generate(data_type=None, options=None):
'''
generate random data with the call of a function
use data_type to generate a single value
use options to set generation options (key = type, value = kwargs)
use options.data_types and omit data_type to generate a random type
'''
if options is None:
options = {**DEFAULT_OPTIONS}
else:
options = DEFAULT_OPTIONS | options
if data_type is None and options['data_types'] is None:
raise NoDataTypeError()
if data_type is None and options['data_types'] is not None:
return generate(data_type=choice(list(options['data_types'])), options=options)
if not isinstance(data_type, str):
data_type = data_type.__name__
if data_type not in Generator.get_supported_data_types():
raise BadGeneratorTypeError(data_type)
return getattr(Generator, f'_{data_type}')(options)
#####################################################################
class Generator:
@classmethod
def get_supported_data_types(cls):
global SUPPORTED_DATA_TYPES # pylint: disable=global-statement
if SUPPORTED_DATA_TYPES is None:
SUPPORTED_DATA_TYPES = {
sub('^_', '', data_type)
for data_type, method in Generator.__dict__.items()
if isinstance(method, staticmethod)
}
return SUPPORTED_DATA_TYPES
@staticmethod
def _bool(options):
return choice([True, False, None]) if options['bool_nullable'] else choice([True, False])
@staticmethod
def _int(options):
return randint(options['minimum'], options['maximum'])
@staticmethod
def _float(options):
return uniform(options['minimum'], options['maximum'])
@staticmethod
def _chr(options):
character_set = options['character_set']
return choice(character_set) if character_set is not None else chr(randint(0,65536))
@staticmethod
def _str(options):
length = options['str_length']
if length is None:
length = generate(int, {
'minimum': options['str_minimum_length'],
'maximum': options['str_maximum_length'],
})
return ''.join((generate(chr, options) for _ in range(length)))
@staticmethod
def _list(options):
if options['depth'] <= 0:
return []
options['depth'] -= 1
if options['data_types'] is None:
options['data_types'] = {bool, int, float, chr, str}
return [ generate(None, options) for _ in range(options['list_length']) ]
@staticmethod
def _set(options):
if options['depth'] <= 0:
return set()
options['depth'] -= 1
if options['data_types'] is None:
options['data_types'] = {bool, int, float, chr, str}
set_options = options | {'data_types': options['data_types'] - {list, dict, set}}
return { generate(None, set_options) for _ in range(options['set_length']) }
@staticmethod
def _dict(options):
if options['depth'] <= 0:
return {}
options['depth'] -= 1
if options['data_types'] is None:
options['data_types'] = {bool, int, float, chr, str, list, set, dict}
if len(options['data_types']) == 0:
return {}
key_options = options | {'data_types': options['dict_key_types']}
return {
generate(None, key_options): generate(None, options)
for _ in range(options['dict_length'])
}
@staticmethod
def _csv(options):
'''
creates a StringIO object containing csv data
'''
if options['data_types'] is None:
options['data_types'] = {int, float, str}
columns = options['csv_columns']
if columns is None:
columns = max(1, generate(int, {
'minimum': options['csv_columns_minimum'],
'maximum': options['csv_columns_maximum'],
}))
rows = options['csv_rows']
if rows is None:
rows = max(1, generate(int, {
'minimum': options['csv_rows_minimum'],
'maximum': options['csv_rows_maximum'],
}))
if options['character_set'] is None:
options['character_set'] = printable
csv = StringIO()
csv_writer = writer(csv, quoting=QUOTE_NONNUMERIC)
options['list_length'] = columns
for line in [ generate(list, options) for _ in range(rows) ]:
csv_writer.writerow(line)
csv.seek(0)
return csv
@staticmethod
def _json(options):
'''
creates a str containing json data
'''
if options['data_types'] is None:
options['data_types'] = {bool, int, float, str, list, dict}
if options['character_set'] is None:
options['character_set'] = printable
options['dict_key_types'] = { int, float, str }
data = generate(options['json_initial_type'], options)
return dumps(data)
@staticmethod
def _yaml(options):
'''
creates a StringIO object containing yaml data
'''
if options['data_types'] is None:
options['data_types'] = {bool, int, float, str, list, dict}
if options['character_set'] is None:
options['character_set'] = printable
options['dict_key_types'] = { int, float, str }
yaml = StringIO()
safe_dump(generate(options['yaml_initial_type'], options), yaml, default_flow_style=False)
yaml.seek(0)
return yaml
#####################################################################
if __name__ == '__main__':
print(generate('json', {'depth': 3}))

View File

@@ -1,24 +1,14 @@
from os import getenv from os import getenv
from pprint import pprint
from random import randint from random import randint
from .generate import generate, Generator from .random_generator import generate, Generator
ITERATIONS = int(
getenv(
'PYTEST_ITERATIONS__scwrypts__test__generator',
getenv('PYTEST_ITERATIONS', '99'), # CI should use at least 999
)
)
FILE_LIKE_DATA_TYPES = { 'csv', 'json', 'yaml' } ITERATIONS = int(getenv('PYTEST_ITERATIONS__scwrypts__test__random_generator', getenv('PYTEST_ITERATIONS', '999')))
def test_generate(): # generators should be quick and "just work" (no Exceptions) def test_generate(): # generators should be quick and "just work" (no Exceptions)
print()
for data_type in Generator.get_supported_data_types(): for data_type in Generator.get_supported_data_types():
print(f'------- {data_type} -------')
sample = generate(data_type)
pprint(sample.getvalue() if data_type in {'csv', 'json', 'yaml'} else sample)
for _ in range(ITERATIONS): for _ in range(ITERATIONS):
generate(data_type) generate(data_type)
@@ -51,4 +41,4 @@ def test_generate_range_negative():
def test_generate_bool_nullable(): def test_generate_bool_nullable():
for data_type in Generator.get_supported_data_types(): for data_type in Generator.get_supported_data_types():
generate(data_type, {'bool_nullable': True}) generate(data_type, {'bool': {'nullable': True}})

View File

@@ -1,13 +1,2 @@
'''
loads the twilio.rest.Client by referencing TWILIO__API_KEY,
TWILIO__API_SECRET, and TWILIO__ACCOUNT_SID in your scwrypts
environment
'''
__all__ = [
'get_client',
'send_sms',
]
from .client import get_client from .client import get_client
from .send_sms import send_sms from .send_sms import send_sms

377
run Executable file
View File

@@ -0,0 +1,377 @@
#!/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 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
[[ $LOGFILE =~ ^/dev/null$ ]] && {
eval "$RUN_STRING $(printf "%q " "$@")" </dev/tty >/dev/tty 2>&1
EXIT_CODE=$?
} || {
(eval "$RUN_STRING $(printf "%q " "$@")")
EXIT_CODE=$?
}
[ $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,525 +1,2 @@
#!/usr/bin/env zsh #!/bin/zsh
export EXECUTION_DIR=$(pwd) source "${0:a:h}/run" $@
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,81 +1,59 @@
# 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
##################################################################### #####################################################################
: \
&& command -v scwrypts &>/dev/null \
&& eval "$(scwrypts --config)" \
|| {
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=
[ $SCWRYPT_SELECTION ] || return 1
NAME=$(echo "$SCWRYPT_SELECTION" | awk '{print $1;}')
TYPE=$(echo "$SCWRYPT_SELECTION" | awk '{print $2;}')
GROUP=$(echo "$SCWRYPT_SELECTION" | awk '{print $3;}')
[ $NAME ] && [ $TYPE ] && [ $GROUP ]
}
#####################################################################
[ $SCWRYPTS_SHORTCUT ] && {
SCWRYPTS__ZSH_PLUGIN() { SCWRYPTS__ZSH_PLUGIN() {
local SCWRYPT_SELECTION NAME TYPE GROUP local SCWRYPT_SELECTION=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS | FZF 'select a script' --header-lines 1)
__SCWRYPTS_PARSE || { zle accept-line; return 0; } local NAME
local TYPE
local GROUP
LBUFFER= RBUFFER=
[ ! $SCWRYPT_SELECTION ] && { zle accept-line; return 0; }
RBUFFER="scwrypts --name $NAME --type $TYPE --group $GROUP" SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION
which scwrypts >/dev/null 2>&1\
&& RBUFFER="scwrypts" || RBUFFER="$SCWRYPTS_ROOT/scwrypts"
RBUFFER+=" --name $NAME --group $GROUP --type $TYPE"
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 NAME TYPE GROUP local SCWRYPT_SELECTION=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS | FZF 'select a script' --header-lines 1)
__SCWRYPTS_PARSE || { echo >&2; zle accept-line; return 0; } local NAME
echo $SCWRYPT_SELECTION >&2 local TYPE
local GROUP
LBUFFER= RBUFFER=
[ ! $SCWRYPT_SELECTION ] && { zle accept-line; return 0; }
scwrypts -n --name $NAME --group $GROUP --type $TYPE -- --help >&2 || { SCWRYPTS__SEPARATE_SCWRYPT_SELECTION $SCWRYPT_SELECTION
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
LBUFFER="scwrypts --name $NAME --type $TYPE --group $GROUP -- " which scwrypts >/dev/null 2>&1\
&& 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 --list-envs; } \ { [ $SCWRYPTS_ENV ] && echo $RESET; SCWRYPTS__GET_ENV_NAMES; } \
| fzf --prompt 'select an environment : ' \ | FZF 'select an environment' \
) )
zle clear-command-line zle clear-command-line
@@ -89,135 +67,3 @@ __SCWRYPTS_PARSE() {
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

View File

@@ -1,177 +0,0 @@
#
# 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,113 +1,21 @@
# 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 a (commonly dreaded) go-to for automation. Since they emulate direct user interaction, shell scripts are often the straightforward choice for task automation.
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. ## Basic Utilities
## The Basic Framework One of my biggest pet-peeves with scripting is when every line of a *(insert-language-here)* program is escaped to shell.
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.
Take a look at the simplest ZSH-type scwrypt: [hello-world](./hello-world). Loaded by `common.zsh`, the [`utils/` library](./utils) provides:
The bare minimum API for ZSH-type scwrypts is to: - common function wrappers to unify flags and context
- lazy dependency and environment variable validation
1. include the shebang `#!/usr/bin/env zsh` on the first line of the file - consistent (and pretty) user input / output
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

@@ -1,13 +0,0 @@
#
# 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

@@ -1,28 +0,0 @@
#####################################################################
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

@@ -1,36 +0,0 @@
#####################################################################
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

@@ -1,65 +0,0 @@
#####################################################################
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

@@ -1,6 +0,0 @@
#
# 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

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

View File

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

View File

@@ -1,30 +0,0 @@
#####################################################################
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

@@ -1,47 +0,0 @@
#####################################################################
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,82 +1,64 @@
#!/usr/bin/env zsh #!/bin/zsh
##################################################################### DEPENDENCIES+=(jq)
use cloud/aws/cli
use cloud/aws/zshparse/overrides
DEPENDENCIES+=(jq mount sort sudo)
REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT) REQUIRED_ENV+=(AWS__EFS__LOCAL_MOUNT_POINT)
##################################################################### use cloud/aws/cli
USAGE__description='
interactively mount an AWS EFS volume to the local filesystem
'
##################################################################### #####################################################################
MAIN() { MAIN() {
local PARSERS=(cloud.aws.zshparse.overrides) GETSUDO || exit 1
eval "$(utils.parse.autosetup)" [ ! -d $AWS__EFS__LOCAL_MOUNT_POINT ] && {
utils.io.getsudo || return 1 sudo mkdir $AWS__EFS__LOCAL_MOUNT_POINT \
########################################## && 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=$(\
$AWS efs describe-file-systems \
| jq -r '.[] | .[] | .FileSystemId' \
| utils.fzf 'select a filesystem to mount' \
)
[ ! ${FS_ID} ] && utils.abort
local MOUNT_POINT="${AWS__EFS__LOCAL_MOUNT_POINT}/${FS_ID}"
[ -d "${MOUNT_POINT}" ] && sudo rmdir "${MOUNT_POINT}" &>/dev/null
[ -d "${MOUNT_POINT}" ] && {
echo.status "${FS_ID} is already mounted"
return 0
} }
local MOUNT_TARGETS=$($AWS efs describe-mount-targets --file-system-id ${FS_ID}) local FS_ID=$(\
local ZONE=$(\ AWS efs describe-file-systems \
echo ${MOUNT_TARGETS} \ | jq -r '.[] | .[] | .FileSystemId' \
| jq -r '.[] | .[] | .AvailabilityZoneName' \ | FZF 'select a filesystem to mount' \
| sort -u | utils.fzf 'select availability zone'\
) )
[ ! "${ZONE}" ] && utils.abort [ ! $FS_ID ] && ABORT
local MOUNT_POINT="$AWS__EFS__LOCAL_MOUNT_POINT/$FS_ID"
[ -d "$MOUNT_POINT" ] && sudo rmdir "$MOUNT_POINT" >/dev/null 2>&1
[ -d "$MOUNT_POINT" ] && {
STATUS "$FS_ID is already mounted"
exit 0
}
local MOUNT_TARGETS=$(AWS efs describe-mount-targets --file-system-id $FS_ID)
local ZONE=$(\
echo $MOUNT_TARGETS \
| jq -r '.[] | .[] | .AvailabilityZoneName' \
| sort -u | FZF 'select availability zone'\
)
[ ! $ZONE ] && 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 \
) )
echo.success 'ready to mount!' SUCCESS 'ready to mount!'
echo.status " REMINDER 'for private file-systems, you must be connected to the appropriate VPN'
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
sudo mkdir -- "${MOUNT_POINT}" \ STATUS "file system id : $FS_ID"
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" \
&& echo.success "mounted at '${MOUNT_POINT}'" \ && SUCCESS "mounted at '$MOUNT_POINT'" \
|| { || {
sudo rmdir -- "${MOUNT_POINT}" &>/dev/null sudo rmdir $MOUNT_POINT >/dev/null 2>&1
echo.error "unable to mount '${FS_ID}'" FAIL 2 "unable to mount '$FS_ID'"
} }
} }

View File

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

View File

@@ -1,86 +0,0 @@
#####################################################################
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

@@ -1,105 +0,0 @@
#####################################################################
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

@@ -1,43 +0,0 @@
#####################################################################
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

@@ -1,66 +0,0 @@
#####################################################################
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

@@ -1,10 +0,0 @@
#
# 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

@@ -1,24 +0,0 @@
#####################################################################
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 @@
#!/usr/bin/env zsh #!/bin/zsh
use cloud/aws/eks use cloud/aws/eks
##################################################################### #####################################################################

View File

@@ -1,44 +0,0 @@
${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'
}

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