--- version: '3' tasks: collections:download: cmds: - | PATH="$PATH:$HOME/.local/bin" ansible-galaxy collection download -r requirements.yml docs: deps: - docs:roles - docs:tags docs:roles: deps: - :install:software:jq - :install:software:yq log: error: Failed to acquire README chart data for the roles start: Scanning roles folder and generating chart data success: Finished populating roles folder chart data cmds: - | TMP="$(mktemp)" jq --arg name "Role Name" --arg description "Description" --arg github 'GitHub            ' \ '.role_var_chart = [[$name, $description, $github]]' .variables.json > "$TMP" mv "$TMP" .variables.json for ROLE_PATH in roles/*/*; do if [[ "$ROLE_PATH" != *"roles/deprecated/"* ]] && [[ "$ROLE_PATH" != *"roles/cloud/"* ]] && [[ "$ROLE_PATH" != *"roles/helpers/"* ]]; then if [ "$(yq e '.galaxy_info.project.documentation' "${ROLE_PATH}/meta/main.yml")" != 'null' ]; then DOCUMENTATION_LINK="*[Documentation]($(yq e '.galaxy_info.project.documentation' "${ROLE_PATH}/meta/main.yml" | sed 's/null//'))* | " else DOCUMENTATION_LINK="" fi if [ "$(yq e '.galaxy_info.project.homepage' "${ROLE_PATH}/meta/main.yml")" != 'null' ]; then HOMEPAGE_LINK="*[Homepage]($(yq e '.galaxy_info.project.homepage' "${ROLE_PATH}/meta/main.yml"))* | " else HOMEPAGE_LINK="" fi GITHUB_URL="$(yq e '.galaxy_info.project.github' "${ROLE_PATH}/meta/main.yml")" if [ "$GITHUB_URL" == 'Not open-source' ]; then GITHUB_SHIELD="**❌ Closed source**" elif [ "$GITHUB_URL" != 'null' ]; then GITHUB_PATH="$(echo "$GITHUB_URL" | sed 's/https:\/\/github.com\///' | sed 's/\/$//')" GITHUB_OWNER="$(echo "$GITHUB_PATH" | sed 's/\/.*$//')" GITHUB_PROJECT_SLUG="$(echo "$GITHUB_PATH" | sed 's/^.*\///')" GITHUB_SHIELD="[![GitHub Repo stars](https://img.shields.io/github/stars/$GITHUB_OWNER/$GITHUB_PROJECT_SLUG?style=social)]($GITHUB_URL)" else UPSTREAM_URL="$(yq e '.galaxy_info.project.upstream' "${ROLE_PATH}/meta/main.yml")" if [ "$UPSTREAM_URL" != 'null' ]; then GITHUB_SHIELD="**[✅ Open-source]($UPSTREAM_URL)**" else GITHUB_SHIELD="*N/A*" fi fi SOFTWARE_NAME="$(jq -r '.blueprint.name' "${ROLE_PATH}/package.json")" DESCRIPTION="$(jq -r '.blueprint.overview' "${ROLE_PATH}/package.json")" ROLE_GITHUB_LINK="*[Role on GitHub]($(jq -r '.blueprint.repository.github' "${ROLE_PATH}/package.json"))*" ANSIBLE_GALAXY_NAMESPACE="$(yq e '.galaxy_info.namespace' "${ROLE_PATH}/meta/main.yml")" ANSIBLE_GALAXY_ROLE_NAME="$(yq e '.galaxy_info.role_name' "${ROLE_PATH}/meta/main.yml")" ROLE_ANSIBLE_GALAXY_URL="https://galaxy.ansible.com/${ANSIBLE_GALAXY_NAMESPACE}/${ANSIBLE_GALAXY_ROLE_NAME}" if [ "$ANSIBLE_GALAXY_NAMESPACE" != 'null' ] && [ "$ANSIBLE_GALAXY_ROLE_NAME" != 'null' ]; then URL_LINK="**[${SOFTWARE_NAME}](${ROLE_ANSIBLE_GALAXY_URL})**" else URL_LINK="**${SOFTWARE_NAME}**" fi if [ "$(jq -r '.blueprint.ansible_galaxy_project_id' "${ROLE_PATH}/package.json")" == 'null' ]; then ROLE_GITHUB_LINK="" fi DESCRIPTION_LINKS="(${HOMEPAGE_LINK}${DOCUMENTATION_LINK}${ROLE_GITHUB_LINK})" if [ "$DESCRIPTION_LINKS" != "()" ]; then DESCRIPTION_LINKS=" $(echo "$DESCRIPTION_LINKS" | sed 's/ | )$/)/')" else DESCRIPTION_LINKS="" fi TMP="$(mktemp)" jq --arg name "${URL_LINK}" --arg description "${DESCRIPTION}${DESCRIPTION_LINKS}" --arg github "${GITHUB_SHIELD}" \ '.role_var_chart = .role_var_chart + [[$name, $description, $github]]' .variables.json > "$TMP" mv "$TMP" .variables.json fi done docs:tags: deps: - :install:npm:leasot summary: | ```shell # @description Processes leasot data and returns .variables.json data including charts written in @appnest/readme format # # @arg $1 The file that the leasot JSON was written to # @arg $2 The tag being processed function populateChartVar() { ... } ``` vars: DOC_IDS: '@binaryapp,@binarycli,@brew,@cask,@chrome,@firefox,@gem,@helm,@npm,@pypi,@vscode' log: error: Failed to acquire package information from comments via `leasot` start: Scanning and acquiring package information in comments via `leasot` success: Acquired package information from comments cmds: - | function populateChartVar() { CHART='[["Package", "Description", "GitHub            "]' jq --arg tag "$(echo $2 | tr '[a-z]' '[A-Z]')" -r '.[] | select(.tag == $tag) | . | del(. ["file", "ref", "line", "tag"]) | .text' "$1" | while read COMMENT; do if [ "$CHART" != '[' ]; then CHART="${CHART}," fi LINK="$(echo $COMMENT | sed 's/ - .*//')" URL_LINK="$(echo "$LINK" | sed 's/@tui //' | sed 's/@service //' | sed 's/@binary //' | sed 's/@cli //' | sed 's/@application //' | sed 's/@menubar //' | sed 's/@webapp //' | sed 's/^\[\([^)]*\)\](\([^)]*\)).*$/\[\1\](\2)/' | sed 's/ $//' | sed 's/\/$//')" if [[ "$LINK" == *"[GitHub](NO_GITHUB_REPOSITORY_LINK)"* ]] || [[ "$LINK" != *"https://github.com"* ]]; then GITHUB_SHIELD="*N/A*" else GITHUB_PATH="$(echo "$LINK" | sed 's/.*\[GitHub\](https:\/\/github.com\/\([^|]*\)).*/\1/' | \ sed 's/.*\[.*\](https:\/\/github.com\/\([^)]*\)).*/\1/' | sed 's/\/$//')" GITHUB_OWNER="$(echo "$GITHUB_PATH" | sed 's/\/.*$//')" GITHUB_PROJECT_SLUG="$(echo "$GITHUB_PATH" | sed 's/^.*\///')" GITHUB_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_PROJECT_SLUG}" GITHUB_SHIELD="[![GitHub Repo stars](https://img.shields.io/github/stars/$GITHUB_OWNER/$GITHUB_PROJECT_SLUG?style=social)]($GITHUB_URL)" fi if [[ "$LINK" == *"[Documentation]"* ]]; then DOCUMENTATION_LINK="*[Documentation]("$(echo "$LINK" | sed 's/.*\[Documentation\](\([^)]*\)).*/\1/')")* | " else DOCUMENTATION_LINK="" fi if [[ "$LINK" == *"[Homepage]"* ]]; then HOMEPAGE_LINK="*[Homepage]("$(echo "$LINK" | sed 's/.*\[Homepage\](\([^)]*\)).*/\1/' | sed 's/ $//')")* | " else HOMEPAGE_LINK="" fi if [[ "$LINK" == *"[Helm]"* ]] || [[ "$LINK" == *"[Operator]"* ]]; then REFERENCE_LINK="*[Helm Reference]("$(echo "$LINK" | sed 's/.*\[Helm\](\([^)]*\)).*/\1/' | sed 's/.*\[Operator\](\([^)]*\)).*/\1/')")* | " else REFERENCE_LINK="" fi DESCRIPTION_LINKS="(${HOMEPAGE_LINK}${DOCUMENTATION_LINK}${REFERENCE_LINK})" if [ "$DESCRIPTION_LINKS" != "()" ]; then DESCRIPTION_LINKS=" $(echo "$DESCRIPTION_LINKS" | sed 's/ | )$/)/')" else DESCRIPTION_LINKS="" fi DESCRIPTION="$(echo $COMMENT | sed 's/.* - //' | sed 's/\"/\\\"/g')" CHART="${CHART}[\"**$URL_LINK**\",\"${DESCRIPTION}${DESCRIPTION_LINKS}\",\"$GITHUB_SHIELD\"]" done CHART="${CHART}]" TMP_CHART="$(mktemp)" KEY="$(echo $2 | sed 's/^@//')" jq --arg chart "$CHART" --arg key "${KEY}_var_chart" '.[$key] = ($chart | fromjson)' .variables.json > "$TMP_CHART" mv "$TMP_CHART" .variables.json } TMP="$(mktemp)" leasot --tags '{{.DOC_IDS}}' --reporter json './environments/prod/group_vars/**/*.yml' > "$TMP" || true VARIABLES_JSON="$(jq '.' .variables.json)" for ID in {{replace "," " " .DOC_IDS}}; do populateChartVar "$TMP" "$ID" done environment: desc: Prompts for which environment to use and then symlinks to it hide: '{{ne (print .REPOSITORY_TYPE "-" .REPOSITORY_SUBTYPE) "ansible-playbook"}}' summary: | # Switch environments using an interactive dialogue Ansible does not really provide any great ways to switch between environments (or sets of `host_vars/`, `group_vars/` etc.). If you place all the files and folders you wish to constitute as an environment inside a folder named as the name of the environment then you can use this task to handle the symlinking and switching between environments. **Example of opening the interactive prompt:** `task ansible:environment` **You can directly switch enironments to `environments/prod/` by running:** `task ansible:environment -- prod` cmds: - task: environment:{{if .CLI_ARGS}}cli{{else}}prompt{{end}} environment:cli: log: error: Encountered an error while switching environments to `{{.CLI_ARGS}}` start: Switching environment to `{{.CLI_ARGS}}` success: Successfully switched environment to `{{.CLI_ARGS}}` cmds: - | {{if .CLI_ARGS}} for ITEM in environments/{{.CLI_ARGS}}/*; do ITEM="$(echo $ITEM | sed 's/.*\///')" if test -L "$ITEM" || ! test -e "$ITEM"; then rm -f "$ITEM" ln -s "./environments/{{.CLI_ARGS}}/$ITEM" "$ITEM" .config/log success "Successfully symlinked "'`'"$ITEM"'`'" from ./environments/{{.CLI_ARGS}}/$ITEM" else .config/log warn "$ITEM exists in the root and was not a symlink so it was skipped to prevent possible data loss" fi done {{else}} exit 69 {{end}} - .config/log finish 'Success `environment:cli`' environment:prompt: env: MARKDOWN: | # Symlink Environment Answer the prompt below to switch between environments. Each environment should be a folder with folders and files you wish to link to from the root of the project. They should normally consist of a host_vars, group_vars, inventory, and files folder (but can contain any files/folders you wish to link). Each environment should have its own folder in the `environments/` folder titled as the name of the environment. After you select an answer, the script will symlink all of the items in the environments folder to the root as long as there is not anything except a symlink to the target location (i.e. it will overwrite symlinks but not files). cmds: - TMP="$(mktemp)" && echo "$MARKDOWN" > "$TMP" && .config/log md "$TMP" - task: environment:prompt:continue environment:prompt:continue: interactive: true deps: - :install:software:gum - :install:software:jq cmds: - | ENVIRONMENT_OPTIONS="$(find ./environments -maxdepth 1 -mindepth 1 | sed 's/\.\/environments\///' | jq -R '[.]' | jq -s -c -r 'add | join(" ")')" .config/log prompt 'Select an environment from the `environments/` folder to symlink to' ENV_OPTION="$(gum choose $(echo $ENVIRONMENT_OPTIONS))" task ansible:playbook:environment:cli -- "$ENV_OPTION" find-missing:files: desc: Find roles that are missing files hide: '{{ne (print .REPOSITORY_TYPE "-" .REPOSITORY_SUBTYPE) "ansible-playbook"}}' summary: | # Find roles that are missing any given file This task scans through all the folders in the roles/ directory and checks for the presence of a file that you pass in through the CLI. **Example usage:** `task find-missing -- logo.png` The example above will look through all the folders two levels deep (e.g. `./roles/tools/nmap`, `./roles/system/snapd`) in the roles folder and display any roles that are missing the file. log: error: Failed to scan the `/roles/*` folders for roles missing `{{.CLI_ARGS}}` start: Determining which roles in the `/roles/*` folders are missing `{{.CLI_ARGS}}` success: Finished scanning for roles missing `{{.CLI_ARGS}}` (if there are any then they should be listed above) cmds: - | FILES=$(find ./roles -mindepth 2 -maxdepth 2 -type d '!' -exec test -e "{}/{{.CLI_ARGS}}" ';' -print) .config/log info 'Found '"$(echo "$FILES" | wc -l | xargs)"' roles missing {{.CLI_ARGS}}' echo "$FILES" preconditions: - sh: test -d roles msg: The `roles/` folder is missing. Is the project set up right? find-missing:roles: summary: | # Find roles that are not in main.yml This task will collect all the role paths in the `roles/` folder and return a list of roles that are not present in the `main.yml` file. If you want to scan something other than `main.yml`, you can pass the file in as a CLI argument like so: **Example scanning file other than `main.yml`:** `task ansible:playbook:find-missing:roles -- playbooks/qubes.yml` cmds: - | TMP="$(mktemp)" RESULTS="$(mktemp)" while read ROLE; do ROLE_TITLE="$(echo "$ROLE" | sed 's/^.\///')" if ! grep "$ROLE_TITLE" '{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}main.yml{{end}}' > /dev/null; then echo "$ROLE_TITLE" >> "$RESULTS" fi done< <(find ./roles -maxdepth 2 -mindepth 2) if grep ".*" "$RESULTS"; then .config/log info 'The roles missing from `{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}main.yml{{end}}` are:' cat "$RESULTS" else .config/log info 'All of the roles are included in `{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}main.yml{{end}}`' fi remotes: deps: - :install:software:git - :install:software:jq summary: | # Ensures each role is added as a remote and a sub-repo (if applicable) This task cycles through all the roles in the `/roles` folder and ensures they are added as remotes. This helps with managing the git trees used for combining the many role repositories into the playbook mono-repository. It also adds a remote for a private submodule that is intended to store files that are not meant to be shared. The remote is named `private`. run: once log: error: Failed to set remotes for one or more of the roles in the `/roles/*` folders start: Adding git remotes for all of the roles in the `/roles/*` folders success: Successfully added git remotes for all of the roles in the `/roles/*` folders cmds: - git init -q - | for ROLE_RELATIVE_PATH in roles/*/*; do if [ -f "${ROLE_RELATIVE_PATH}/package.json" ]; then ROLE_FOLDER="$(basename $ROLE_RELATIVE_PATH)" ROLE_HTTPS_REPO="$(jq -r '.blueprint.repository.gitlab' $ROLE_RELATIVE_PATH/package.json)" ROLE_SSH_REPO="$(echo $ROLE_HTTPS_REPO | sed 's/https:\/\/gitlab.com\//git@gitlab.com:/' | sed 's/$/.git/')" if git config "remote.${ROLE_FOLDER}.url" > /dev/null; then git remote set-url "$ROLE_FOLDER" "$ROLE_SSH_REPO" else git remote add "$ROLE_FOLDER" "$ROLE_SSH_REPO" fi if [ -d $ROLE_RELATIVE_PATH/.git ] && [ ! -f $ROLE_RELATIVE_PATH/.gitrepo ]; then task ansible:playbook:subrepo:init -- $ROLE_RELATIVE_PATH fi else .config/log warn "${ROLE_RELATIVE_PATH}/package.json is missing!" fi done run: cmds: - task: run:{{if .CLI_ARGS}}cli{{else}}prompt{{end}} run:cli: deps: - task: :install:python:requirements env: INSTALL_OPTIONS: --no-dev - :symlink:playbook vars: PLAYBOOK_MAIN: sh: | if [ -n "$PLAYBOOK_MAIN" ]; then echo "$PLAYBOOK_MAIN" else echo "main.yml" fi log: error: Error encounted while running `ansible-playbook -i inventories/{{.CLI_ARGS}} --ask-vault-pass main.yml` start: Running `ansible-playbook -i inventories/{{.CLI_ARGS}} --ask-vault-pass main.yml` success: Successfully ran `ansible-playbook -i inventories/{{.CLI_ARGS}} --ask-vault-pass main.yml` cmds: - | PATH="$PATH:$HOME/.local/bin" if [ -z "$ANSIBLE_VAULT_PASSWORD" ]; then .config/log info 'The `ANSIBLE_VAULT_PASSWORD` environment variable is not set so you will be prompted for the password' ansible-playbook --skip-tags "mas" -i {{.CLI_ARGS}} --ask-vault-pass "{{.PLAYBOOK_MAIN}}" else echo "$ANSIBLE_VAULT_PASSWORD" > "$HOME/.VAULT_PASSWORD" export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.VAULT_PASSWORD" .config/log info 'Bypassing Ansible Vault password prompt since the `ANSIBLE_VAULT_PASSWORD` environment variable is set' ansible-playbook --skip-tags "mas" -i {{.CLI_ARGS}} "{{.PLAYBOOK_MAIN}}" fi run:prompt: summary: '{{.MARKDOWN}}' vars: MARKDOWN: | # Run the Playbook These set of prompts will run the `main.yml` playbook after you specify: (1) The "environment" The environment is a collection of folders that should, at the very minimum, include "files", "group_vars", "host_vars", and "inventories". Each folder in the "environments" folder constitutes a different environment. By using environments, you can seperate different sets of variables/files or even seperate your private variables out into a sub-module. (2) An inventory file The Ansible inventory stored in the "inventories" folder. This will generally be a YML file with host connection information that also correlates the inventory with the proper host_vars and group_vars. It is assumed that your sudo username and password are encrypted inside the inventory (via "ansible-vault"). PLAYBOOK_DESCRIPTIONS: | # Playbook Descriptions Playbooks are where you store your main logic in Ansible. The `main.yml` file in the root of the repository is a generic one-size-fits-all approach. You could theoretically store all your logic in one playbook for multiple scenarios with conditional logic but it might be easier to create seperate playbooks in some cases. The `main.yml` should work in most scenarios and is a great starting point. ## Alternate Playbook Descriptions Alternate playbooks are stored in the `playbooks/` directory. They are described below. *If you want their description to show up in this message, you will have to edit the appropriate field in `package.json`.* env: MARKDOWN: '{{.MARKDOWN}}' cmds: - | MD_TMP="$(mktemp)" echo "$MARKDOWN" > "$MD_TMP" .config/log md "$MD_TMP" - task: run:prompt:continue run:prompt:continue: interactive: true deps: - :install:software:jq cmds: - task: environment - echo "\"inventories"$(find ./inventories/ -mindepth 1 -maxdepth 1 | sed 's/\.\/inventories\///' | jq -R '[.]' | jq -s -c -r 'add | join("\" \"inventories")')"\"" - | INVENTORY_OPTIONS_LENGTH="$(find ./inventories/ -mindepth 1 -maxdepth 1 | sed 's/\.\/inventories\///' | jq -R '[.]' | jq -s -c -r 'add | length')" if [[ "$INVENTORY_OPTIONS_LENGTH" == '0' ]]; then .config/log error 'There are no inventory files present in the `inventories/` folder' && exit 1 else INVENTORY_OPTIONS="$(echo "\""$(find ./inventories/ -mindepth 1 -maxdepth 1 | sed 's/\.\///' | jq -R '[.]' | jq -s -c -r 'add | join("\" \"")')"\"")" .config/log prompt 'Which inventory would you like to use?' CHOSEN_INVENTORY="$(.config/log choose $INVENTORY_OPTIONS)" export PLAYBOOK_MAIN="main.yml" if ! gum confirm "Would you like to use the main.yml playbook?"; then PLAYBOOKS_OPTIONS="$(echo "\"playbooks"$(find ./playbooks/ -mindepth 1 -maxdepth 1 -name "*.yml" | sed 's/\.\/playbooks\///' | jq -R '[.]' | jq -s -c -r 'add | join("\" \"playbooks")')"\"")" PLAYBOOK_DESCS_TMP="$(mktemp)" && echo "$PLAYBOOK_DESCRIPTIONS" > "$PLAYBOOK_DESCS_TMP" TMP_LIST="$(mktemp)" find ./playbooks/ -mindepth 1 -maxdepth 1 -name "*.yml" | sed 's/\.\/playbooks\///' | jq -R '[.]' | jq -s -c -r 'add' >> "$TMP_LIST" echo "* **main.yml:** A generic playbook that attempts to install everything" >> "$PLAYBOOK_DESCS_TMP" jq -c -r '.[]' "$TMP_LIST" | while read SLUG; do PLAYBOOK_DESC="$(jq -r '.blueprint.fileDescriptions[$file]' package.json)" if [ "$PLAYBOOK_DESC" != 'null' ]; then echo "* **playbooks/${SLUG}.yml:** $PLAYBOOK_DESC" >> "$PLAYBOOK_DESCS_TMP" fi done .config/log md "$PLAYBOOK_DESCS_TMP" .config/log prompt 'Select the playbook you would like to provision with.' export PLAYBOOK_MAIN="$(.config/log choose $PLAYBOOKS_OPTIONS)" fi task ansible:playbook:run:cli -- "$CHOSEN_INVENTORY" fi subrepo:init: deps: - :install:software:subrepo summary: | # Add Role as a Sub-Repository Since roles should each have their own repository for Ansible Galaxy and to make it easier for the community to download specific roles, they must be declared as sub-repositories. Submodules could also be used but we use sub-repos instead because they are more flexible. In the main playbook, if someone clones the playbook, the playbook and all the roles will download without any requirement to initialize submodules. At the same time, each role can be in its own repository. The playbook recognizes roles like this because they have a `.gitrepo` file that is only saved in the playbook version of the role. Users can interact with the playbook and its role repositories transparently without any need to understand what git subrepos are. Managers of roles can update the role repositories without any need to understand what git subrepos are. Managers of the playbook can use the tool [git-subrepo](https://github.com/ingydotnet/git-subrepo) to perform various actions including pulling changes from individual role repositories and other actions. Usage: `task ansible:playbook:subrepo:init -- path/to/folder/with.git/folder` cmds: - task: :git:commit:automated - | BASENAME="$(basename {{.CLI_ARGS}})" REMOTE="$(git remote get-url $BASENAME)" HUSKY=0 git subrepo init {{.CLI_ARGS}} -r "$REMOTE" -b master types: deps: - :install:npm:quicktype summary: | # Generate Types from Vaulted Files Automatically generate types from vaulted files. **Example Usage:** `task ansible:playbook:types -- ./environments/prod vars: TARGET_DIR: '{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}./environments/prod{{end}}' cmds: - task: vault:decrypt vars: TARGET_DIR: '{{.TARGET_DIR}}' - | qtype() { local FILE_NO_EXT="$(echo "$1" | sed 's/.yml$//')" yq e -o=json '.' $1 | quicktype -l schema -o ${FILE_NO_EXT}.schema.json } while read FILE; do qtype "$FILE" done < <(find {{.TARGET_DIR}} -type f -name "*vault.yml") - task: vault:encrypt vars: TARGET_DIR: '{{.TARGET_DIR}}' vault:decrypt: vars: TARGET_DIR: '{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}./environments/prod{{end}}' cmds: - find {{.TARGET_DIR}} -type f -name "*vault.yml" -printf "%h/\"%f\" " | xargs ansible-vault decrypt vault:encrypt: vars: TARGET_DIR: '{{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}./environments/prod{{end}}' cmds: - find {{.TARGET_DIR}} -type f -name "*vault.yml" -printf "%h/\"%f\" " | xargs ansible-vault encrypt