├── .gitignore ├── CLAUDE.md ├── CODEOWNERS ├── CONVENTIONS.md ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── aliases ├── bash_completion.sh ├── bin ├── aws-profiles ├── bma ├── cachews ├── cachews-replace ├── extras │ └── cached └── replacements │ └── aws ├── cloudformation ├── README.md ├── asg.yml ├── docker-stack.yml ├── domain-redirector.yml ├── ec2.yml ├── elb.yml ├── elbv2.yml ├── noop.yml ├── params │ ├── .keep │ └── docker-stack-params-skeleton.json ├── ssm.json ├── stars.yml └── tags │ └── .keep ├── contrib └── ai │ ├── bmai │ └── slop │ ├── README.md │ ├── bmai-list-ec2-instances │ └── storage-gateways ├── docs ├── .command-reference-intro.md ├── CNAME ├── bmai.md ├── cachews-caching-for-awscli.md ├── cloudformation-naming.md ├── command-reference.md ├── developer-guide.md ├── images │ ├── bma-02-2.gif │ └── cachews.png ├── index.md ├── linuxconf2020.md ├── pipe-skimming.md ├── slides │ ├── lca2020-lightning │ │ ├── assets │ │ │ ├── elb-asg-ec2.png │ │ │ ├── gccc.jpg │ │ │ ├── ph.jpg │ │ │ └── skim-stdin.png │ │ ├── index.html │ │ ├── out │ │ │ ├── remark.js │ │ │ ├── remark.min.js │ │ │ └── tests.js │ │ ├── script03 │ │ └── script03-timing │ └── lca2020 │ │ ├── assets │ │ ├── elb-asg-ec2.png │ │ ├── gccc.jpg │ │ └── ph.jpg │ │ ├── index.html │ │ ├── out │ │ ├── remark.js │ │ ├── remark.min.js │ │ └── tests.js │ │ ├── script01 │ │ └── script01-timing ├── specs │ └── feature-include-headers-in-output │ │ ├── header-comment-implementation.md │ │ ├── header-line-challenge.md │ │ ├── header-solutions-comparison.md │ │ ├── plan.md │ │ └── spec.md ├── style.md └── tour.md ├── functions ├── lib ├── asg-functions ├── aws-account-functions ├── azure-functions ├── azure.azcli ├── backup-functions ├── cert-functions ├── cloudfront-functions ├── cloudtrail-functions ├── cloudwatch-functions ├── ecr-functions ├── elb-functions ├── elbv2-functions ├── extras │ └── pkcs12-functions ├── iam-functions ├── image-functions ├── instance-functions ├── keypair-functions ├── kms-functions ├── lambda-functions ├── log-functions ├── rds-functions ├── region-functions ├── route53-functions ├── s3-functions ├── secretsmanager-functions ├── shared-functions ├── ssm-functions ├── stack-functions ├── sts-functions ├── tag-functions ├── target-group-functions └── vpc-functions ├── mkdocs.yml ├── scripts ├── build ├── build-completions ├── build-docs ├── completions │ ├── asg-completion │ ├── aws-account-completion │ ├── bma-completion │ ├── bucket-completion │ ├── cachews-completion │ ├── cert-completion │ ├── elb-completion │ ├── elbv2-completion │ ├── instance-completion │ ├── keypair-completion │ ├── region-completion │ ├── stack-completion │ ├── target-group-completion │ └── vpc-completion ├── extract-docs ├── localstack-stack-create.sh └── localstack.sh └── test ├── bash-spec.sh ├── check-completions ├── commands ├── exercise-functions.sh ├── shared-spec.sh ├── stack-spec.sh └── stub.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib/load.inc 3 | multipass-role-arns 4 | multipass-role-arns/* 5 | cloudformation/params/* 6 | !cloudformation/params/.keep 7 | !cloudformation/params/*-params-skeleton.json 8 | cloudformation/tags 9 | !cloudformation/tags/.keep 10 | test/cloudformation 11 | site/ 12 | *.swp 13 | cloudformation/stars2.yml 14 | lib/extras/github-functions 15 | .aider* 16 | .env 17 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | read README.md and CONVENTIONS.md to understand the project 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mbailey 2 | -------------------------------------------------------------------------------- /CONVENTIONS.md: -------------------------------------------------------------------------------- 1 | # Coding Conventions 2 | 3 | This document outlines the coding conventions used in our project, particularly for bash scripts and AWS CLI commands. 4 | 5 | ## Bash Scripts 6 | 7 | ### Function Naming 8 | - Use lowercase with hyphens for function names (e.g., `vpc-subnets`, `vpc-route-tables`). 9 | - Function names should be descriptive of their purpose. 10 | - For AWS resource-related functions, use the format `resource-type-action` (e.g., `vpc-igw`, `vpc-nat-gateways`). 11 | 12 | ### Comments 13 | - Use `#` for single-line comments. 14 | - Include a brief description of the function's purpose immediately after the function name. 15 | - For complex functions, add usage examples in the comments. 16 | 17 | ### AWS CLI Commands 18 | - Use `aws` followed by the service name (e.g., `aws ec2`, `aws rds`). 19 | - Prefer `--output text` for consistent and easily parseable output. 20 | - Use `--query` with JMESPath expressions for filtering and formatting output. 21 | 22 | ### Output Formatting 23 | - Use `columnise` (a custom function) to format output into columns. 24 | - Sort output when appropriate (e.g., `sort -k 5`). 25 | - For listing functions, output should typically include resource IDs and names. 26 | - Include 'NO_NAME' for resources without a Name tag. 27 | 28 | ### Error Handling 29 | - Check for required arguments using `[[ -z "$vpc_ids" ]] && __bma_usage "vpc-id [vpc-id]" && return 1`. 30 | 31 | ### Looping 32 | - Use `for` loops to iterate over multiple VPC IDs or other resources. 33 | 34 | ### Variable Naming 35 | - Use lowercase for local variables (e.g., `local vpc_ids`, `local filters`). 36 | - Use uppercase for environment variables or constants (e.g., `$AWS_DEFAULT_REGION`). 37 | 38 | ### Input Handling 39 | - Use `skim-stdin` (a custom function) to handle both piped input and arguments. 40 | - Allow functions to accept input from both command-line arguments and piped input. 41 | 42 | #### skim-stdin 43 | 44 | The `skim-stdin` function is a utility function that allows functions to accept input from both command-line arguments and piped input. It appends the first token from each line of STDIN to the argument list. 45 | 46 | ```bash 47 | skim-stdin() { 48 | 49 | # Append first token from each line of STDIN to argument list 50 | # 51 | # Implementation of `pipe-skimming` pattern. 52 | # 53 | # $ stacks | skim-stdin foo bar 54 | # foo bar huginn mastodon grafana 55 | # 56 | # $ stacks 57 | # huginn CREATE_COMPLETE 2020-01-11T06:18:46.905Z NEVER_UPDATED NOT_NESTED 58 | # mastodon CREATE_COMPLETE 2020-01-11T06:19:31.958Z NEVER_UPDATED NOT_NESTED 59 | # grafana CREATE_COMPLETE 2020-01-11T06:19:47.001Z NEVER_UPDATED NOT_NESTED 60 | # 61 | # Typical usage within Bash-my-AWS functions: 62 | # 63 | # local asg_names=$(skim-stdin "$@") # Append to arg list 64 | # local asg_names=$(skim-stdin) # Only draw from STDIN 65 | 66 | local skimmed_stdin="$([[ -t 0 ]] || awk 'ORS=" " { print $1 }')" 67 | 68 | printf -- '%s %s' "$*" "$skimmed_stdin" | 69 | awk '{$1=$1;print}' # trim leading/trailing spaces 70 | 71 | } 72 | ``` 73 | 74 | ### Function Structure 75 | - Start with input validation and error handling. 76 | - Use local variables to store intermediate results. 77 | - Prefer using AWS CLI with appropriate filters and queries over post-processing with grep/awk when possible. 78 | 79 | ## AWS Resource Conventions 80 | 81 | ### Resource Listing 82 | - Functions that list resources should follow a consistent output format. 83 | - Include relevant identifiers (e.g., VPC ID, Subnet ID) in the output. 84 | - Include resource names when available, use 'NO_NAME' for unnamed resources. 85 | 86 | ### Resource Manipulation 87 | - Functions that modify resources should include safety checks. 88 | - For destructive actions, include comments or echo statements explaining the process. 89 | 90 | ## Security Considerations 91 | - Be cautious with functions that perform destructive actions (e.g., `vpc-default-delete`). 92 | - Include safety checks before performing destructive actions. 93 | - When dealing with sensitive information, use appropriate AWS CLI options to handle credentials securely. 94 | 95 | ## Utility Functions 96 | - Create and use utility functions for common operations (e.g., `__bma_read_filters`, `__bma_usage`). 97 | - Prefix internal utility functions with double underscores (e.g., `__bma_error`). 98 | 99 | ## Documentation 100 | - Include a brief description of each function's purpose in comments. 101 | - For complex functions or those with multiple options, include usage examples in the comments. 102 | 103 | ## Consistency 104 | - Maintain consistent indentation (prefer 2 spaces). 105 | - Use consistent naming conventions across all functions. 106 | 107 | ## Shared Functions 108 | 109 | ### skim-stdin 110 | The `skim-stdin` function is a crucial utility for handling input in our scripts. It allows functions to accept input from both command-line arguments and piped input, providing flexibility in how users interact with our tools. 111 | 112 | ```bash 113 | skim-stdin() { 114 | # Append first token from each line of STDIN to argument list 115 | # 116 | # Implementation of `pipe-skimming` pattern. 117 | # 118 | # $ stacks | skim-stdin foo bar 119 | # foo bar huginn mastodon grafana 120 | # 121 | # $ stacks 122 | # huginn CREATE_COMPLETE 2020-01-11T06:18:46.905Z NEVER_UPDATED NOT_NESTED 123 | # mastodon CREATE_COMPLETE 2020-01-11T06:19:31.958Z NEVER_UPDATED NOT_NESTED 124 | # grafana CREATE_COMPLETE 2020-01-11T06:19:47.001Z NEVER_UPDATED NOT_NESTED 125 | 126 | local skimmed_stdin="$([[ -t 0 ]] || awk 'ORS=" " { print $1 }')" 127 | 128 | printf -- '%s %s' "$*" "$skimmed_stdin" | 129 | awk '{$1=$1;print}' # trim leading/trailing spaces 130 | } 131 | ``` 132 | 133 | Usage: 134 | - Place `local resource_ids=$(skim-stdin "$@")` at the beginning of functions to handle both piped input and arguments. 135 | - This allows users to either pipe in resource IDs or provide them as arguments. 136 | 137 | ### columnise 138 | The `columnise` function is used to format output into aligned columns, improving readability: 139 | 140 | ```bash 141 | columnise() { 142 | if [[ $BMA_COLUMNISE_ONLY_WHEN_TERMINAL_PRESENT == 'true' ]] && ! [[ -t 1 ]]; then 143 | cat 144 | else 145 | column -t -s $'\t' 146 | fi 147 | } 148 | ``` 149 | 150 | Usage: 151 | - Pipe the output of AWS CLI commands through `columnise` to format it into aligned columns. 152 | - Example: `aws ec2 describe-vpcs ... | columnise` 153 | 154 | These shared functions enhance the consistency and usability of our scripts across the project. 155 | 156 | ## Examples 157 | 158 | ### Example 1: Listing a Resource (VPCs) 159 | 160 | The `vpcs` function is an example of listing a resource type. It demonstrates how to query and format the output for VPCs: 161 | 162 | ```bash 163 | vpcs() { 164 | # List VPCs 165 | # 166 | # $ vpcs 167 | # vpc-018d9739 default-vpc NO_NAME 172.31.0.0/16 NO_STACK NO_VERSION 168 | 169 | local vpc_ids=$(skim-stdin) 170 | local filters=$(__bma_read_filters $@) 171 | 172 | aws ec2 describe-vpcs \ 173 | ${vpc_ids/#/'--vpc-ids '} \ 174 | --output text \ 175 | --query ' 176 | Vpcs[].[ 177 | VpcId, 178 | ((IsDefault==`false`)&&`not-default`)||`default-vpc`, 179 | join(`,`, [Tags[?Key==`Name`].Value || `NO_NAME`][]), 180 | CidrBlock, 181 | join(`,`, [Tags[?Key==`aws:cloudformation:stack-name`].Value || `NO_STACK`][]), 182 | join(`,`, [Tags[?Key==`version`].Value || `NO_VERSION`][]) 183 | ]' | 184 | grep -E -- "$filters" | 185 | columnise 186 | } 187 | ``` 188 | 189 | This function showcases: 190 | - Using `skim-stdin` to handle both piped input and arguments 191 | - Applying filters with `__bma_read_filters` 192 | - Using AWS CLI with a complex query to format the output 193 | - Using `columnise` for consistent output formatting 194 | 195 | ### Example 2: Listing Associated Resources (VPC Subnets) 196 | 197 | The `vpc-subnets` function is an example of listing resources associated with another resource. It demonstrates how to query and format the output for subnets associated with specific VPCs: 198 | 199 | ```bash 200 | vpc-subnets() { 201 | # List subnets for a specific VPC 202 | # 203 | # USAGE: vpc-subnets vpc-id [vpc-id] 204 | # 205 | # EXAMPLE: 206 | # $ vpc-subnets vpc-018d9739 207 | # subnet-34fd9cfa vpc-018d9739 ap-southeast-2c 172.31.32.0/20 NO_NAME 208 | # subnet-8bb774fe vpc-018d9739 ap-southeast-2a 172.31.0.0/20 NO_NAME 209 | # subnet-9eea2c07 vpc-018d9739 ap-southeast-2b 172.31.16.0/20 NO_NAME 210 | 211 | local vpc_ids=$(skim-stdin "$@") 212 | [[ -z "$vpc_ids" ]] && __bma_usage "vpc-id [vpc-id]" && return 1 213 | 214 | local vpc_id 215 | for vpc_id in $vpc_ids; do 216 | aws ec2 describe-subnets \ 217 | --output text \ 218 | --query " 219 | Subnets[?VpcId=='$vpc_id'].[ 220 | SubnetId, 221 | VpcId, 222 | AvailabilityZone, 223 | CidrBlock, 224 | join(',', [Tags[?Key=='Name'].Value || 'NO_NAME'][]) 225 | ]" 226 | done | 227 | columnise 228 | } 229 | ``` 230 | 231 | This function showcases: 232 | - Handling multiple VPC IDs 233 | - Error checking for empty input 234 | - Looping through VPC IDs to query associated subnets 235 | - Formatting output consistently with `columnise` 236 | 237 | These examples illustrate the consistent patterns used across the project for querying AWS resources and formatting the output. 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 REA Group Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: run_tests 2 | 3 | run_tests: 4 | './test/shared-spec.sh' 5 | './test/stack-spec.sh' 6 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Only `add_to_path_if_missing()` 4 | - (aws-)backup-functions 5 | 6 | -------------------------------------------------------------------------------- /bin/aws-profiles: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import configparser 3 | import os 4 | import sys 5 | 6 | # script.py - List AWS profile names with account IDs from aws_account_id or extracted from role_arn, sorted by profile name 7 | 8 | # Get patterns from command-line arguments 9 | patterns = sys.argv[1:] 10 | 11 | # Get patterns from stdin if available 12 | if not sys.stdin.isatty(): 13 | for line in sys.stdin: 14 | patterns.append(line.strip().split()[0]) 15 | 16 | # Define the path to the AWS credentials file 17 | credentials_file = os.environ.get('AWS_SHARED_CREDENTIALS_FILE', os.path.expanduser('~/.aws/credentials')) 18 | 19 | # Read and parse the AWS credentials file 20 | config = configparser.ConfigParser(interpolation=None) 21 | config.read(credentials_file) 22 | 23 | # Helper function to extract account ID from role_arn 24 | def get_account_id_from_arn(arn): 25 | # Split the ARN and extract the account ID 26 | parts = arn.split(':') 27 | if len(parts) > 4 and parts[0].startswith('arn') and parts[2] == 'iam': 28 | return parts[4] # The account ID is the 5th element in the ARN 29 | return 'Unknown' 30 | 31 | # Collect profile names and account IDs 32 | profiles_with_ids = [] 33 | 34 | # Iterate through the sections and collect profile names and account IDs 35 | for section in config.sections(): 36 | if not patterns or any(pattern in section for pattern in patterns): 37 | account_id = (config.get(section, 'aws_account_id') 38 | if config.has_option(section, 'aws_account_id') 39 | else 'Unknown') 40 | # If aws_account_id is not found, try to extract it from role_arn 41 | if account_id == 'Unknown' and config.has_option(section, 'role_arn'): 42 | role_arn = config.get(section, 'role_arn') 43 | account_id = get_account_id_from_arn(role_arn) 44 | 45 | profiles_with_ids.append((section, account_id)) 46 | 47 | # Sort the list by profile name 48 | sorted_profiles = sorted(profiles_with_ids) 49 | 50 | # Print sorted profiles and account IDs 51 | for profile, account_id in sorted_profiles: 52 | print(f"{profile}\t{account_id}") 53 | -------------------------------------------------------------------------------- /bin/bma: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # bma 4 | # 5 | # Provides alternative to loading the functions into your 6 | # shell environment. 7 | # 8 | # Useful for calling a function from another process (like 'env') 9 | # 10 | # e.g. env FOO=BAR bma instances 11 | # 12 | # Add it to your PATH or copy/link it to somewhere that is 13 | # 14 | # Assumes you have installed bash-my-aws to standard location 15 | 16 | for f in "${BMA_HOME:-$HOME/.bash-my-aws}"/lib/*-functions; do source $f; done 17 | 18 | # Disable awcli client side pager 19 | # 20 | # AWS CLI version 2 provides the use of a client-side pager program for output. 21 | # By default, this feature returns all output through your operating system’s 22 | # default pager program. 23 | # 24 | # https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-pagination.html#cli-usage-pagination-clientside 25 | # 26 | export AWS_PAGER='' 27 | 28 | # Optionally specify which installation of aws-cli to use 29 | # 30 | # Added for testing against breaking changes in aws-cli-v2 31 | # 32 | # Set it to `awslocal` for testing with the very useful 33 | # [localstack](https://github.com/localstack/localstack) 34 | # 35 | if [[ -n $BMA_AWSCLI ]]; then 36 | function aws(){ 37 | $BMA_AWSCLI "$@" 38 | } 39 | export aws 40 | fi 41 | 42 | # Print awscli version info to STDERR (if in debug mode) 43 | [[ -n $BMA_DEBUG ]] && aws --version >&2 44 | 45 | "$@" 46 | -------------------------------------------------------------------------------- /bin/cachews: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import subprocess 6 | import jmespath 7 | import argparse 8 | import sys 9 | import shlex 10 | 11 | CACHEWS_DIR = ( 12 | os.environ.get('CACHEWS_DIR') or 13 | os.environ.get('XDG_CACHE_HOME') or 14 | os.path.expanduser('~/.cache/cachews') 15 | ) 16 | AWS_DEFAULT_REGION = os.environ.get('AWS_DEFAULT_REGION') 17 | AWS_PROFILE = os.environ.get('AWS_PROFILE') 18 | AWS_ACCOUNT_ALIAS = AWS_PROFILE[:-6] if AWS_PROFILE and AWS_PROFILE.endswith('-admin') else AWS_PROFILE 19 | 20 | def list_or_describe(service, api_call, output_format='json', query=None): 21 | file_path = f"{CACHEWS_DIR}/{AWS_ACCOUNT_ALIAS}/{AWS_DEFAULT_REGION}/{service}/{api_call}.json" 22 | 23 | try: 24 | with open(file_path, 'r') as file: 25 | data = json.load(file) 26 | except FileNotFoundError: 27 | print(f'Cache miss for file: {file_path}', file=sys.stderr) 28 | args = ' '.join(shlex.quote(arg) for arg in sys.argv[1:]) 29 | cmd = f"aws {args} --output json" 30 | print(f"Executing command: {cmd}", file=sys.stderr) 31 | try: 32 | data = json.loads(subprocess.check_output(cmd, shell=True, timeout=60)) 33 | except subprocess.TimeoutExpired: 34 | print(f"Command timed out: {cmd}") 35 | return 36 | 37 | os.makedirs(os.path.dirname(file_path), exist_ok=True) 38 | with open(file_path, 'w') as file: 39 | json.dump(data, file) 40 | 41 | if query: 42 | data = jmespath.search(query, data) 43 | 44 | if output_format == 'text': 45 | if data is not None: 46 | for item in data: 47 | print('\t'.join(str(value) for value in item)) 48 | elif output_format == 'table': 49 | print('Not yet implemented') 50 | else: 51 | print(json.dumps(data, indent=4)) 52 | 53 | def main(): 54 | parser = argparse.ArgumentParser(description='Mimics AWS list and describe functions') 55 | parser.add_argument('service', help='AWS service name') 56 | parser.add_argument('api_call', help='API call to execute (list or describe)') 57 | parser.add_argument('--output', choices=['json', 'text', 'table'], default='json', help='Output format') 58 | parser.add_argument('--query', help='JMESPath query string') 59 | 60 | args, unknown_args = parser.parse_known_args() 61 | list_or_describe(args.service, args.api_call, args.output, args.query) 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /bin/cachews-replace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # cachews-replace - Run a command with awscli replaced by cachews 4 | # 5 | # This allows scripts that call awscli to use caching without modification. 6 | # 7 | # It works by prepending BMA_HOME/bin/replacements to your PATH 8 | # 9 | # For running bash-my-aws commands, a built-in approach is BMA_AWSCLI: 10 | # 11 | # BMA_AWSCLI=cachews instances 12 | 13 | SLF_DIR="$(readlink -f "$(dirname -- "${BASH_SOURCE[0]:-${0:A}}")/../")" 14 | export PATH="${SLF_DIR}/bin/replacements:${PATH}" 15 | 16 | "$@" 17 | -------------------------------------------------------------------------------- /bin/extras/cached: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # cached - Cache the output of shell commands 4 | 5 | # example usage: 6 | # 7 | # $ cached date 8 | # Cache hit (35 seconds ago) 9 | # Mon 16 Oct 2023 10:46:31 AEDT 10 | # 11 | # $ cached -f date 12 | # Mon 16 Oct 2023 10:47:11 AEDT 13 | # 14 | # $ cached date 15 | # Cache hit (3 seconds ago) 16 | # Mon 16 Oct 2023 10:47:11 AEDT 17 | 18 | import os 19 | import sys 20 | import subprocess 21 | import hashlib 22 | import time 23 | from pathlib import Path 24 | from datetime import datetime, timedelta 25 | 26 | def print_help(): 27 | print("Usage: cached [-f|--force] [-h|--help] ") 28 | print(" -f, --force Force the command to run and update the cache") 29 | print(" -h, --help Show this help message and exit") 30 | sys.exit(0) 31 | 32 | def calculate_age_in_human_friendly_format(timestamp): 33 | now = time.time() 34 | age_seconds = now - timestamp 35 | age = timedelta(seconds=age_seconds) 36 | 37 | if age_seconds < 60: 38 | return f"{age_seconds:.0f} seconds ago" 39 | elif age_seconds < 3600: 40 | return f"{age.seconds // 60} minutes ago" 41 | else: 42 | return f"{age.seconds // 3600} hours ago" 43 | 44 | # Get XDG cache directory 45 | cache_dir = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "cached" 46 | cache_dir.mkdir(parents=True, exist_ok=True) 47 | 48 | # Separate script flags from the command to run 49 | force_update = False 50 | command_start_idx = 1 51 | 52 | for idx, arg in enumerate(sys.argv[1:]): 53 | if arg == '-f' or arg == '--force': 54 | force_update = True 55 | command_start_idx = idx + 2 56 | elif arg == '-h' or arg == '--help': 57 | print_help() 58 | elif arg == '-s' or arg == '--show-cache': # New flag 59 | print(f"Cache directory: {cache_dir}") 60 | sys.exit(0) # Exit after showing the cache directory 61 | else: 62 | command_start_idx = idx + 1 63 | break 64 | 65 | # Check if a command is provided 66 | if command_start_idx >= len(sys.argv) or not sys.argv[command_start_idx:]: 67 | print("Error: No command provided") 68 | print_help() 69 | sys.exit(1) # Explicitly exit the script 70 | 71 | # Get timeout from environment or default to 60 minutes 72 | timeout_minutes = int(os.environ.get("CACHED_TIMEOUT", 60)) 73 | timeout_seconds = timeout_minutes * 60 74 | 75 | # Create a safe filename based on the command and its arguments 76 | cmd_str = " ".join(sys.argv[command_start_idx:]) 77 | filename = hashlib.sha256(cmd_str.encode()).hexdigest() 78 | cache_file = cache_dir / filename 79 | 80 | if cache_file.exists() and not force_update: 81 | file_age_seconds = time.time() - cache_file.stat().st_mtime 82 | file_age_humanized = calculate_age_in_human_friendly_format(cache_file.stat().st_mtime) 83 | 84 | if file_age_seconds < timeout_seconds: 85 | print(f"Cache hit ({file_age_humanized})", file=sys.stderr) 86 | with open(cache_file, 'r') as f: 87 | print(f.read()) 88 | sys.exit(0) 89 | else: 90 | print(f"Cache miss ({file_age_humanized})", file=sys.stderr) 91 | 92 | # Run the command 93 | try: 94 | output = subprocess.check_output(cmd_str, shell=True, text=True) 95 | except subprocess.CalledProcessError as e: 96 | print(f"Command failed with error code {e.returncode}", file=sys.stderr) 97 | sys.exit(1) 98 | 99 | # Cache the output 100 | with open(cache_file, 'w') as f: 101 | f.write(output) 102 | 103 | print(output) 104 | -------------------------------------------------------------------------------- /bin/replacements/aws: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cachews "$@" 4 | -------------------------------------------------------------------------------- /cloudformation/README.md: -------------------------------------------------------------------------------- 1 | bash-my-aws example CloudFormation 2 | ================================== 3 | 4 | The CloudFormation templates in this directory are used by the maintainers to: 5 | 6 | - manage the documentation website 7 | - generate stacks to help create documentation and demos 8 | 9 | It's useful to have a number of stacks with various resources to exercise 10 | the different functionality of the toolset. These may prove useful for 11 | people exploring the tools. 12 | -------------------------------------------------------------------------------- /cloudformation/asg.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Parameters: 4 | AsgMinSize: 5 | Type: Number 6 | Description: Minimum size of the ASG 7 | Default: '1' 8 | AsgMaxSize: 9 | Type: Number 10 | Description: Maximum size of the ASG 11 | Default: '3' 12 | InstanceType: 13 | Type: String 14 | Description: EC2 Instance Type to use 15 | Default: t3.nano 16 | KeyName: 17 | Type: String 18 | Description: Name of an existing EC2 KeyPair to enable SSH access to the instance 19 | Default: '' 20 | LatestAmiId: 21 | Type: AWS::SSM::Parameter::Value 22 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 23 | VPCZoneIdentifier: 24 | Type: List 25 | Description: List of subnet-ids for the instances 26 | Default: '' 27 | 28 | Resources: 29 | 30 | SSMRole: 31 | Type: AWS::IAM::Role 32 | Properties: 33 | AssumeRolePolicyDocument: 34 | Version: '2012-10-17' 35 | Statement: 36 | - Effect: Allow 37 | Principal: 38 | Service: ec2.amazonaws.com 39 | Action: 'sts:AssumeRole' 40 | Path: "/" 41 | Policies: 42 | - PolicyName: SSMInstancePolicy 43 | PolicyDocument: 44 | Version: '2012-10-17' 45 | Statement: 46 | - Effect: Allow 47 | Action: 48 | - 'ssm:DescribeAssociation' 49 | - 'ssm:GetDeployablePatchSnapshotForInstance' 50 | - 'ssm:GetDocument' 51 | - 'ssm:DescribeDocument' 52 | - 'ssm:GetManifest' 53 | - 'ssm:GetParameter' 54 | - 'ssm:GetParameters' 55 | - 'ssm:ListAssociations' 56 | - 'ssm:ListInstanceAssociations' 57 | - 'ssm:PutInventory' 58 | - 'ssm:PutComplianceItems' 59 | - 'ssm:PutConfigurePackageResult' 60 | - 'ssm:UpdateAssociationStatus' 61 | - 'ssm:UpdateInstanceAssociationStatus' 62 | - 'ssm:UpdateInstanceInformation' 63 | - 'ec2messages:GetEndpoint' 64 | - 'ec2messages:GetMessages' 65 | - 'ec2messages:SendReply' 66 | - 'cloudwatch:PutMetricData' 67 | - 'ssmmessages:CreateControlChannel' 68 | - 'ssmmessages:CreateDataChannel' 69 | - 'ssmmessages:OpenControlChannel' 70 | - 'ssmmessages:OpenDataChannel' 71 | Resource: '*' 72 | 73 | SSMInstanceProfile: 74 | Type: AWS::IAM::InstanceProfile 75 | Properties: 76 | Path: "/" 77 | Roles: 78 | - Ref: SSMRole 79 | 80 | LaunchConfiguration: 81 | Type: AWS::AutoScaling::LaunchConfiguration 82 | Properties: 83 | AssociatePublicIpAddress: true 84 | ImageId: !Ref 'LatestAmiId' 85 | InstanceType: !Ref 'InstanceType' 86 | KeyName: !Ref 'KeyName' 87 | IamInstanceProfile: !Ref SSMInstanceProfile 88 | UserData: 89 | Fn::Base64: !Sub | 90 | #!/bin/bash 91 | set -xeuo pipefail 92 | exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 93 | echo "I have booted" 94 | 95 | AutoScalingGroup: 96 | Type: AWS::AutoScaling::AutoScalingGroup 97 | Properties: 98 | VPCZoneIdentifier: !Ref VPCZoneIdentifier 99 | LaunchConfigurationName: !Ref 'LaunchConfiguration' 100 | MinSize: !Ref 'AsgMinSize' 101 | MaxSize: !Ref 'AsgMaxSize' 102 | DesiredCapacity: !Ref 'AsgMinSize' 103 | VPCZoneIdentifier: !Ref VPCZoneIdentifier 104 | Tags: 105 | - Key: Name 106 | Value: !Sub '${AWS::StackName}' 107 | PropagateAtLaunch: 'true' 108 | -------------------------------------------------------------------------------- /cloudformation/domain-redirector.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Parameters: 4 | 5 | DestinationHostName: 6 | Type: String 7 | Default: bash-my-aws.org 8 | 9 | DestinationProtocol: 10 | Type: String 11 | Default: https 12 | 13 | SourceHostName: 14 | Type: String 15 | Default: bashmyaws.com 16 | 17 | Mappings: 18 | RegionMap: 19 | us-east-1: 20 | S3hostedzoneID: Z3AQBSTGFYJSTF 21 | websiteendpoint: s3-website-us-east-1.amazonaws.com 22 | us-west-1: 23 | S3hostedzoneID: Z2F56UZL2M1ACD 24 | websiteendpoint: s3-website-us-west-1.amazonaws.com 25 | us-west-2: 26 | S3hostedzoneID: Z3BJ6K6RIION7M 27 | websiteendpoint: s3-website-us-west-2.amazonaws.com 28 | eu-west-1: 29 | S3hostedzoneID: Z1BKCTXD74EZPE 30 | websiteendpoint: s3-website-eu-west-1.amazonaws.com 31 | ap-southeast-1: 32 | S3hostedzoneID: Z3O0J2DXBE1FTB 33 | websiteendpoint: s3-website-ap-southeast-1.amazonaws.com 34 | ap-southeast-2: 35 | S3hostedzoneID: Z1WCIGYICN2BYD 36 | websiteendpoint: s3-website-ap-southeast-2.amazonaws.com 37 | ap-northeast-1: 38 | S3hostedzoneID: Z2M4EHUR26P7ZW 39 | websiteendpoint: s3-website-ap-northeast-1.amazonaws.com 40 | sa-east-1: 41 | S3hostedzoneID: Z31GFT0UA1I2HV 42 | websiteendpoint: s3-website-sa-east-1.amazonaws.com 43 | 44 | 45 | Resources: 46 | 47 | S3Bucket: 48 | Type: AWS::S3::Bucket 49 | Properties: 50 | AccessControl: LogDeliveryWrite 51 | BucketName: !Ref SourceHostName 52 | LoggingConfiguration: 53 | DestinationBucketName: !Ref SourceHostName 54 | # MetricsConfigurations: 55 | # - MetricsConfiguration 56 | PublicAccessBlockConfiguration: 57 | BlockPublicAcls: true 58 | BlockPublicPolicy: true 59 | IgnorePublicAcls: true 60 | RestrictPublicBuckets: true 61 | WebsiteConfiguration: 62 | RedirectAllRequestsTo: 63 | HostName: !Ref DestinationHostName 64 | Protocol: !Ref DestinationProtocol 65 | 66 | Route53RecordSet: 67 | Type: AWS::Route53::RecordSet 68 | Properties: 69 | HostedZoneName: !Sub "${SourceHostName}." 70 | Comment: Web Redirector 71 | Name: !Ref SourceHostName 72 | Type: A 73 | AliasTarget: 74 | HostedZoneId: !FindInMap [ RegionMap, !Ref 'AWS::Region', S3hostedzoneID] 75 | DNSName: !FindInMap [ RegionMap, !Ref 'AWS::Region', websiteendpoint] 76 | 77 | 78 | Outputs: 79 | S3WebsiteURL: 80 | Description: WebsiteURL 81 | Value: !GetAtt S3Bucket.WebsiteURL 82 | SourceURL: 83 | Description: Address we're redirecting from 84 | Value: !Sub "http://${SourceHostName}" 85 | DestinationURL: 86 | Description: Address we're redirecting to 87 | Value: !Sub "${DestinationProtocol}://${DestinationHostName}/" 88 | -------------------------------------------------------------------------------- /cloudformation/ec2.yml: -------------------------------------------------------------------------------- 1 | # Use public SSM Parameter for latest Amazon Linux 2 AMI ID 2 | Parameters: 3 | InstanceType: 4 | Type: String 5 | Description: EC2 Instance Type to use 6 | Default: t3.nano 7 | LatestAmiId: 8 | Type: AWS::SSM::Parameter::Value 9 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 10 | # Default: ami-123456789012 # XXX for localstack 11 | KeyName: 12 | Type: String 13 | Description: Name of an existing EC2 KeyPair to enable SSH access to the instance 14 | Default: '' 15 | 16 | Conditions: 17 | UseKeyPair: !Not [!Equals [ !Ref KeyName, '' ] ] 18 | 19 | Resources: 20 | Instance: 21 | Type: 'AWS::EC2::Instance' 22 | Properties: 23 | ImageId: !Ref LatestAmiId 24 | InstanceType: t3.nano 25 | KeyName: !If [UseKeyPair, !Ref KeyName, !Ref "AWS::NoValue"] 26 | Tags: 27 | - 28 | Key: Name 29 | Value: !Ref AWS::StackName 30 | -------------------------------------------------------------------------------- /cloudformation/elb.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | MyLoadBalancer: 3 | Type: AWS::ElasticLoadBalancing::LoadBalancer 4 | Properties: 5 | AvailabilityZones: 6 | - !Sub "${AWS::Region}a" 7 | Listeners: 8 | - LoadBalancerPort: '80' 9 | InstancePort: '80' 10 | Protocol: HTTP 11 | -------------------------------------------------------------------------------- /cloudformation/elbv2.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | AmiId: 4 | Type: AWS::SSM::Parameter::Value 5 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2 6 | Description: Amazon Linux 2 AMI 7 | KeyName: 8 | ConstraintDescription: must be the name of an existing EC2 KeyPair. 9 | Description: Name of an existing EC2 KeyPair to enable SSH access to the instances 10 | Type: AWS::EC2::KeyPair::KeyName 11 | Subnets: 12 | ConstraintDescription: must be a list of at least two existing subnets associated with at least two different availability zones. They should be residing in the selected Virtual Private Cloud. 13 | Description: The list of SubnetIds in your Virtual Private Cloud (VPC) 14 | Type: List 15 | VpcId: 16 | ConstraintDescription: must be the VPC Id of an existing Virtual Private Cloud. 17 | Description: VpcId of your existing Virtual Private Cloud (VPC) 18 | Type: AWS::EC2::VPC::Id 19 | Resources: 20 | EC2Instance1: 21 | Properties: 22 | ImageId: !Ref AmiId 23 | InstanceType: t4g.nano 24 | KeyName: !Ref KeyName 25 | SecurityGroupIds: 26 | - Ref: InstanceSecurityGroup 27 | SubnetId: 28 | Fn::Select: 29 | - 0 30 | - Ref: Subnets 31 | Type: AWS::EC2::Instance 32 | EC2Instance2: 33 | Properties: 34 | InstanceType: t4g.nano 35 | ImageId: !Ref AmiId 36 | KeyName: !Ref KeyName 37 | SecurityGroupIds: 38 | - Ref: InstanceSecurityGroup 39 | SubnetId: 40 | Fn::Select: 41 | - 1 42 | - Ref: Subnets 43 | Type: AWS::EC2::Instance 44 | InstanceSecurityGroup: 45 | Properties: 46 | GroupDescription: Enable SSH access and HTTP access on the inbound port 47 | SecurityGroupIngress: 48 | - CidrIp: 0.0.0.0/0 49 | FromPort: '22' 50 | IpProtocol: tcp 51 | ToPort: '22' 52 | - CidrIp: 0.0.0.0/0 53 | FromPort: '80' 54 | IpProtocol: tcp 55 | ToPort: '80' 56 | VpcId: !Ref VpcId 57 | Type: AWS::EC2::SecurityGroup 58 | 59 | # ALB 60 | ApplicationLoadBalancer: 61 | Properties: 62 | Subnets: 63 | Ref: Subnets 64 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 65 | ALBListener: 66 | Properties: 67 | DefaultActions: 68 | - TargetGroupArn: !Ref ALBTargetGroup 69 | Type: forward 70 | LoadBalancerArn: !Ref ApplicationLoadBalancer 71 | Port: '80' 72 | Protocol: HTTP 73 | Type: AWS::ElasticLoadBalancingV2::Listener 74 | ALBTargetGroup: 75 | Properties: 76 | HealthCheckIntervalSeconds: 30 77 | HealthCheckTimeoutSeconds: 5 78 | HealthyThresholdCount: 3 79 | Port: 80 80 | Protocol: HTTP 81 | TargetGroupAttributes: 82 | - Key: stickiness.enabled 83 | Value: 'true' 84 | Targets: 85 | - Id: !Ref EC2Instance1 86 | Port: 80 87 | - Id: !Ref EC2Instance2 88 | Port: 80 89 | UnhealthyThresholdCount: 5 90 | VpcId: !Ref VpcId 91 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 92 | 93 | # NLB 94 | NetworkLoadBalancer: 95 | Properties: 96 | Subnets: !Ref Subnets 97 | Type: network 98 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 99 | NLBListener: 100 | Properties: 101 | DefaultActions: 102 | - TargetGroupArn: !Ref NLBTargetGroup 103 | Type: forward 104 | LoadBalancerArn: !Ref NetworkLoadBalancer 105 | Port: '80' 106 | Protocol: TCP 107 | Type: AWS::ElasticLoadBalancingV2::Listener 108 | NLBTargetGroup: 109 | Properties: 110 | Port: 80 111 | Protocol: TCP 112 | TargetGroupAttributes: 113 | - Key: stickiness.enabled 114 | Value: 'true' 115 | Targets: 116 | - Id: !Ref EC2Instance1 117 | Port: 80 118 | - Id: !Ref EC2Instance2 119 | Port: 80 120 | VpcId: !Ref VpcId 121 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 122 | 123 | -------------------------------------------------------------------------------- /cloudformation/noop.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: | 3 | Simple CloudFormation template to quickly create a stack. 4 | Used to test bash-my-aws stack-functions 5 | 6 | Resources: 7 | WaitConditionHandle: 8 | Type: "AWS::CloudFormation::WaitConditionHandle" 9 | -------------------------------------------------------------------------------- /cloudformation/params/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/cloudformation/params/.keep -------------------------------------------------------------------------------- /cloudformation/params/docker-stack-params-skeleton.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "CertArn", 4 | "ParameterValue": "" 5 | }, 6 | { 7 | "ParameterKey": "ELBSubnets", 8 | "ParameterValue": "" 9 | }, 10 | { 11 | "ParameterKey": "HostedZone", 12 | "ParameterValue": "" 13 | }, 14 | { 15 | "ParameterKey": "InstanceSubnets", 16 | "ParameterValue": "" 17 | }, 18 | { 19 | "ParameterKey": "KeyName", 20 | "ParameterValue": "" 21 | }, 22 | { 23 | "ParameterKey": "SSHOrigin", 24 | "ParameterValue": "127.0.0.1/0" 25 | }, 26 | { 27 | "ParameterKey": "VpcId", 28 | "ParameterValue": "" 29 | }, 30 | { 31 | "ParameterKey": "WebOrigin", 32 | "ParameterValue": "0.0.0.0/0" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /cloudformation/ssm.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Parameters": { 4 | "AmiId": { 5 | "Type": "AWS::SSM::Parameter::Value", 6 | "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2", 7 | "Description": "Amazon Linux 2 AMI" 8 | }, 9 | "VpcId": { 10 | "Type": "String", 11 | "Description": "VPC to launch the EC2 instance into" 12 | }, 13 | "SubnetId": { 14 | "Type": "String", 15 | "Description": "Subnet to launch the EC2 instance into" 16 | } 17 | }, 18 | "Resources": { 19 | "ec2Test": { 20 | "Type": "AWS::EC2::Instance", 21 | "Properties": { 22 | "IamInstanceProfile": {"Ref": "iamipTestProfile"}, 23 | "ImageId": {"Ref": "AmiId"}, 24 | "InstanceType": "t4g.nano", 25 | "SecurityGroupIds": [{"Ref": "ec2sgAllowVPC"}], 26 | "SubnetId": {"Ref": "SubnetId"}, 27 | "UserData": { 28 | "Fn::Base64": { 29 | "Fn::Join": ["\n", ["#!/bin/bash -ex", "yum udpate -y"]] 30 | } 31 | }, 32 | "Tags": [ 33 | {"Key": "Name", "Value": "SSMTestInstance"} 34 | ] 35 | } 36 | }, 37 | "ec2sgAllowVPC": { 38 | "Type": "AWS::EC2::SecurityGroup", 39 | "Properties": { 40 | "GroupDescription": "Allow SSM access", 41 | "VpcId": {"Ref": "VpcId"}, 42 | "SecurityGroupIngress": [], 43 | "SecurityGroupEgress": [] 44 | } 45 | }, 46 | "iamipTestProfile": { 47 | "Type": "AWS::IAM::InstanceProfile", 48 | "Properties": {"Path": "/", "Roles": [{"Ref": "iamrTestRole"}]} 49 | }, 50 | "iamrTestRole": { 51 | "Type": "AWS::IAM::Role", 52 | "Properties": { 53 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"], 54 | "AssumeRolePolicyDocument": { 55 | "Version": "2012-10-17", 56 | "Statement": [ 57 | {"Effect": "Allow", "Principal": {"Service": ["ec2.amazonaws.com"]}, "Action": ["sts:AssumeRole"]} 58 | ] 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /cloudformation/stars.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Parameters: 4 | 5 | SlackChannel: 6 | Description: Slack channel to notify 7 | Type: String 8 | Default: bash-my-aws 9 | 10 | KmsEncryptedHookUrl: 11 | Description: Slack integration URL 12 | Type: String 13 | Default: AQICAHgcyN4vd3V/OB7NKI6IMbpENEu1+UfyiUjVj1ieYvnwnwGKZgnw3mBHkREl+HT4/unzAAAApzCBpAYJKoZIhvcNAQcGoIGWMIGTAgEAMIGNBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDICRSzFNhvHkbsPqfQIBEIBgEIEihdMA/sl0duOQrrK4zmWaaZJahGkMT0ziSTJpDvzMox6m0iABhi2YZ0KILVFp9NkHGNWgcaMv3/OV1mErObuMXCPbU2a/feIl4D+yYN/OSzRBTrAejlutHRcq1UqZ 14 | 15 | Resources: 16 | 17 | API: 18 | Type: AWS::ApiGatewayV2::Api 19 | Properties: 20 | Description: !Ref AWS::StackName 21 | Name: !Ref AWS::StackName 22 | ProtocolType: HTTP 23 | Target: !GetAtt Lambda.Arn 24 | 25 | Permission: 26 | Type: AWS::Lambda::Permission 27 | Properties: 28 | FunctionName: !GetAtt 'Lambda.Arn' 29 | Action: lambda:InvokeFunction 30 | Principal: apigateway.amazonaws.com 31 | SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${API}/*/*" 32 | 33 | LambdaExecutionRole: 34 | Type: AWS::IAM::Role 35 | Properties: 36 | AssumeRolePolicyDocument: 37 | Version: '2012-10-17' 38 | Statement: 39 | - Effect: Allow 40 | Principal: 41 | Service: 42 | - lambda.amazonaws.com 43 | Action: 44 | - sts:AssumeRole 45 | Path: "/" 46 | Policies: 47 | - PolicyName: root 48 | PolicyDocument: 49 | Version: '2012-10-17' 50 | Statement: 51 | - Effect: Allow 52 | Action: 53 | - logs:* 54 | Resource: arn:aws:logs:*:*:* 55 | - Effect: Allow 56 | Action: 57 | - kms:Decrypt 58 | Resource: '*' 59 | 60 | Lambda: 61 | Type: AWS::Lambda::Function 62 | Properties: 63 | FunctionName: !Ref AWS::StackName 64 | Description: !Ref AWS::StackName 65 | Environment: 66 | Variables: 67 | SlackChannel: !Ref SlackChannel 68 | KmsEncryptedHookUrl: !Ref KmsEncryptedHookUrl 69 | Handler: index.handler 70 | Runtime: python3.7 71 | Role: !GetAtt LambdaExecutionRole.Arn 72 | Timeout: 10 73 | Code: 74 | ZipFile: | 75 | import boto3 76 | import json 77 | import logging 78 | import os 79 | 80 | from base64 import b64decode 81 | from urllib.request import Request, urlopen 82 | from urllib.error import URLError, HTTPError 83 | 84 | # The base-64 encoded, encrypted key (CiphertextBlob) stored in the KmsEncryptedHookUrl environment variable 85 | ENCRYPTED_HOOK_URL = os.environ['KmsEncryptedHookUrl'] 86 | # The Slack channel to send a message to stored in the SlackChannel environment variable 87 | SLACK_CHANNEL = os.environ['SlackChannel'] 88 | 89 | HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext'].decode('utf-8') 90 | 91 | logger = logging.getLogger() 92 | logger.setLevel(logging.INFO) 93 | 94 | def handler(event, context): 95 | 96 | logger.info("Event: " + str(event)) 97 | 98 | data = json.loads(event['body']) 99 | 100 | message = f"{data['sender']['login']} { 'un-' if data['action'] == 'deleted' else ''}starred {data['repository']['name']}. New total: {data['repository']['stargazers_count']}" 101 | logger.debug(message) 102 | 103 | slack_message = { 104 | 'channel': SLACK_CHANNEL, 105 | 'text': message 106 | } 107 | 108 | req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8')) 109 | try: 110 | response = urlopen(req) 111 | response.read() 112 | logger.info("Message posted to %s", slack_message['channel']) 113 | except HTTPError as e: 114 | logger.error("Request failed: %d %s", e.code, e.reason) 115 | except URLError as e: 116 | logger.error("Server connection failed: %s", e.reason) 117 | 118 | Outputs: 119 | APIEndpoint: 120 | Description: API Endpoint 121 | Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/" 122 | -------------------------------------------------------------------------------- /cloudformation/tags/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/cloudformation/tags/.keep -------------------------------------------------------------------------------- /contrib/ai/bmai: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # bmai - Wrapper for llm prompt command to generate functions 4 | 5 | bmai() { 6 | 7 | if [ $# -lt 1 ]; then 8 | echo "Usage: bmai " >&2 9 | return 1 10 | fi 11 | 12 | local conventions="${BMA_HOME:-$HOME/.bash-my-aws}/CONVENTIONS.md" 13 | 14 | if [ ! -f "$conventions" ]; then 15 | echo "Error: CONVENTIONS.md file not found: $conventions" >&2 16 | return 1 17 | fi 18 | 19 | local prompt=" 20 | Create an authentic working bash-my-aws command that does the following: '$@' 21 | 22 | - Do not provide a preamble or closing comments. 23 | " 24 | 25 | # Create a filename by converting the prompt to lowercase, replacing spaces with hyphens, 26 | # and truncating to 50 chars 27 | local filename=$(echo "$*" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | cut -c1-50) 28 | 29 | local ai_slop_dir="${BMA_HOME:-$HOME/.bash-my-aws}/contrib/ai/slop" 30 | mkdir -p "${ai_slop_dir}" 31 | local output_file="${ai_slop_dir}/${filename}" 32 | 33 | llm prompt \ 34 | -s "$(<"${conventions}")" \ 35 | "Do not provide a preamble or closing comments. Output only an authentic bash-my-aws command that follows the conventions and does the following: '$@'" \ 36 | | tee "${output_file}" 37 | } 38 | 39 | # Execute function with same name as file called unless sourced 40 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 41 | "$(basename "$0")" "$@" 42 | fi 43 | -------------------------------------------------------------------------------- /contrib/ai/slop/README.md: -------------------------------------------------------------------------------- 1 | # AI Slop Directory 2 | 3 | This directory contains outputs from the `bmai` command which generates bash-my-aws functions on the fly. 4 | 5 | "AI Slop" refers to raw, unfiltered output from AI language models that may need human review before use. 6 | 7 | Each file in this directory represents a generated function, named after the prompt that created it. 8 | These files serve as a reference and audit trail of AI-generated commands. 9 | 10 | Files are created automatically when you run the `bmai` command to generate missing bash-my-aws 11 | functions. 12 | -------------------------------------------------------------------------------- /contrib/ai/slop/bmai-list-ec2-instances: -------------------------------------------------------------------------------- 1 | instances() { 2 | local instance_ids=$(skim-stdin "$@") 3 | local filters=$(__bma_read_filters $@) 4 | 5 | aws ec2 describe-instances \ 6 | ${instance_ids/#/'--instance-ids '} \ 7 | --query " 8 | Reservations[].Instances[][ 9 | InstanceId, 10 | State.Name, 11 | InstanceType, 12 | LaunchTime, 13 | join(',', [Tags[?Key=='Name'].Value || 'NO_NAME'][]), 14 | join(',', [Tags[?Key=='application'].Value || 'NO_APP'][]), 15 | Placement.AvailabilityZone, 16 | VpcId, 17 | PrivateIpAddress, 18 | PublicIpAddress || 'NO_PUBLIC_IP' 19 | ]" \ 20 | --output text | 21 | grep -E -- "$filters" | 22 | sort -k 5,5 | 23 | columnise 24 | } 25 | -------------------------------------------------------------------------------- /contrib/ai/slop/storage-gateways: -------------------------------------------------------------------------------- 1 | storage-gateways() { 2 | # List Storage Gateway instances 3 | # 4 | # $ storage-gateways 5 | # gw-12345678 my-gateway 12345678-1234-1234-1234-123456789012 cached-iscsi ACTIVATED 1.2.3.4 6 | 7 | local filters=$(__bma_read_filters $@) 8 | 9 | aws storagegateway list-gateways \ 10 | --output text \ 11 | --query " 12 | Gateways[].[ 13 | GatewayARN, 14 | GatewayName, 15 | GatewayId, 16 | GatewayType, 17 | GatewayOperationalState, 18 | GatewayNetworkInterfaces[0].IpAddress 19 | ]" | 20 | grep -E -- "$filters" | 21 | columnise 22 | } 23 | 24 | -------------------------------------------------------------------------------- /docs/.command-reference-intro.md: -------------------------------------------------------------------------------- 1 | title: Bash-my-AWS Command Reference 2 | description: Command reference for Bash-my-AWS - CLI Tools for AWS. 3 | Bash-my-AWS provides short memorable commands for managing resources 4 | in Amazon Web Services. 5 | 6 | Bash-my-AWS provides over 120 commands for managing AWS Resources but fear not! 7 | 8 | Effort has been put into making them *discoverable*, *memorable* and hopefully in most 9 | cases *obvious*. 10 | 11 | The reference material below is all extracted from the source of the commands. 12 | 13 | Lists in this project are alphabetised except where it makes sense not to. 14 | The first few sets of commands were chosen because they are likely to be of 15 | the most interest to readers. 16 | 17 | !!! Note "General Rules" 18 | - Commands expect `$AWS_DEFAULT_REGION` environment variable to be set 19 | (check/set with `region` command) 20 | - Most commands that list resources (`stacks`, `instances , etc) 21 | accept filter term as first arg. 22 | - *e.g. `stacks blah` is equivalent to `stacks | grep blah`* 23 | - Most commands accept resource identifiers via STDIN 24 | (first token of each line) 25 | - Resources are generally listed in chronological order of creation. 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | bash-my-aws.org 2 | -------------------------------------------------------------------------------- /docs/bmai.md: -------------------------------------------------------------------------------- 1 | # 🤖 bmai - Generate BMA commands from natural language 2 | 3 | > 🎉 **Experimental Feature Release - January 13, 2025** 4 | > Released to celebrate Bash-my-AWS's 10th Anniversary! 🎂 5 | > Using AI to help create the next decade of AWS CLI wizardry ✨ 6 | 7 | The `bmai` command helps generate bash-my-aws functions from natural language descriptions using AI. It reads the project's conventions and generates compliant functions that follow the established patterns. 8 | 9 | ## 📋 Prerequisites 10 | 11 | - Install and configure [llm](https://github.com/simonw/llm) 12 | ```bash 13 | pip install llm 14 | ``` 15 | - Configure an LLM provider (e.g., OpenAI API key) 16 | ```bash 17 | llm keys set openai 18 | ``` 19 | 20 | ## 🚀 Usage 21 | 22 | ```bash 23 | source ~/.bash-my-aws/lib/extras/bmai 24 | bmai "list s3 buckets" 25 | ``` 26 | 27 | ## ✨ Features 28 | 29 | - 🗣️ Generates authentic bash-my-aws functions from natural language descriptions 30 | - 📘 Follows established conventions and patterns 31 | - 🔄 Creates functions that integrate with existing BMA commands 32 | - 💾 Saves generated functions for review 33 | 34 | ## 🔧 Installation 35 | 36 | The `bmai` command is included in the extras directory: 37 | 38 | ```bash 39 | source ~/.bash-my-aws/lib/extras/bmai 40 | ``` 41 | 42 | ## 💡 Examples 43 | 44 | Generate a command: 45 | 46 | ```bash 47 | # Generate a command to list S3 buckets 48 | $ bmai "list s3 buckets" 49 | s3-buckets() { 50 | # List S3 buckets in current region 51 | # 52 | # $ s3-buckets 53 | # example-bucket 2019-12-14 NEVER_UPDATED NO_STACK 54 | # another-bucket 2020-03-01 NEVER_UPDATED NO_STACK 55 | 56 | aws s3api list-buckets \ 57 | --output text \ 58 | --query " 59 | Buckets[].[ 60 | Name, 61 | CreationDate, 62 | join(',', ['NEVER_UPDATED']), 63 | join(',', ['NO_STACK']) 64 | ]" | 65 | columnise 66 | } 67 | ``` 68 | 69 | Generated functions are saved to: 70 | `~/.bash-my-aws/contrib/ai/slop/` 📁 71 | 72 | **Here's what the actual BMA command looks like:** 73 | 74 | ```bash 75 | buckets() { 76 | 77 | # List S3 Buckets 78 | # 79 | # $ buckets 80 | # web-assets 2019-12-20 08:24:38.182045 81 | # backups 2019-12-20 08:24:44.351215 82 | # archive 2019-12-20 08:24:57.567652 83 | 84 | local buckets=$(skim-stdin) 85 | local filters=$(__bma_read_filters $@) 86 | 87 | aws s3api list-buckets \ 88 | --output text \ 89 | --query " 90 | Buckets[${buckets:+?contains(['${buckets// /"','"}'], Name)}].[ 91 | Name, 92 | CreationDate 93 | ]" | 94 | grep -E -- "$filters" | 95 | column -t 96 | } 97 | ``` 98 | 99 | ## ⚙️ How It Works 100 | 101 | 1. 📝 Takes a natural language description as input 102 | 2. 📖 Reads the project's CONVENTIONS.md file 103 | 3. 🧠 Uses AI to generate a compliant function 104 | 4. 💾 Saves output to a file for review 105 | 5. 📺 Displays the generated function 106 | 107 | The generated functions follow BMA conventions including: 108 | 109 | - 🎯 Standard argument handling 110 | - 🔄 Integration with skim-stdin 111 | - 📊 Consistent output formatting 112 | - ⚠️ Proper error handling 113 | - 📚 Clear documentation and examples 114 | 115 | > 🗒️ Note: The example above shows the author's original response after realizing how simple and elegant the S3 bucket listing function could be while still following all conventions. 116 | 117 | -------------------------------------------------------------------------------- /docs/cachews-caching-for-awscli.md: -------------------------------------------------------------------------------- 1 | # Cachews - Caching for AWSCLI 2 | 3 | cachews is a zero-config caching proxy for awscli. 4 | 5 | Reducing request time by around 90% may also reduce: 6 | 7 | - cycle time when constructing commands or scripts that use awscli 8 | - your resistance to running a query across 500 accounts 9 | - how often you put the kettle on 10 | 11 | ![Cache of Cachews](images/cachews.png) 12 | 13 | It supports the following arguments (and ignores the rest): 14 | 15 | `--query` : JMESPath querys as found in awscli (and azcli) 16 | `--output` : `json` or `text` (tab separated values) 17 | 18 | ## Installation 19 | 20 | cachews comes included with bash-my-aws which is installed as follows: 21 | 22 | 1. Clone the repo 23 | 24 | ```Shell 25 | git clone https://github.com/bash-my-aws/bash-my-aws.git ${BMA_HOME:-$HOME/.bash-my-aws} 26 | ``` 27 | 28 | 2. Put the following in your shell's startup file: 29 | 30 | ```Shell 31 | export PATH="$PATH:${BMA_HOME:-$HOME/.bash-my-aws}/bin" 32 | source ${BMA_HOME:-$HOME/.bash-my-aws}/aliases 33 | 34 | # For ZSH users, uncomment the following two lines: 35 | # autoload -U +X compinit && compinit 36 | # autoload -U +X bashcompinit && bashcompinit 37 | 38 | source ${BMA_HOME:-$HOME/.bash-my-aws}/bash_completion.sh 39 | ``` 40 | 41 | 3. [Optional] Set CACHEWS_DIR environment variable. 42 | 43 | 44 | ## Usage 45 | 46 | Choose from two commands two suit your needs: 47 | 48 | - `cachews`: A drop in replacement for `aws` command 49 | - `cachews-replace`: Runs any command, replacing `aws` with `cachews` executable 50 | 51 | As a bonus, you can set `BMA_AWSCLI=cashews` to enable for all bash-my-aws commands. 52 | 53 | See below for real world examples. 54 | 55 | ### `cachews` - Replace `aws` command with `cachews` 56 | 57 | The simplest use case is to substitute `aws` for `cachews` in your command. 58 | 59 | Start a fresh cachedir for this demo: 60 | 61 | ```shell 62 | $ export CACHEWS_DIR="$(mktemp -d)" 63 | ``` 64 | 65 | AWS command takes ~1s: 66 | 67 | ```shell 68 | $ time aws ec2 describe-instances | wc -l 69 | 844 70 | 71 | real 0m1.113s 72 | user 0m0.823s 73 | sys 0m0.076s 74 | ``` 75 | 76 | cachews takes ~1s but caches the response: 77 | 78 | ```shell 79 | $ time cachews ec2 describe-instances | wc -l 80 | 844 81 | 82 | real 0m1.147s 83 | user 0m0.861s 84 | sys 0m0.088s 85 | ``` 86 | 87 | cachews takes ~0.08 seconds the second time: 88 | 89 | ```shell 90 | $ time cachews ec2 describe-instances | wc -l 91 | 844 92 | 93 | real 0m0.081s 94 | user 0m0.061s 95 | sys 0m0.023s 96 | ``` 97 | 98 | ### cachews-replace - avoids need to change code 99 | 100 | This could be a script with AWSCLI calls or even this crazy example. 101 | 102 | ```shell 103 | $ time cachews-replace aws ec2 describe-instances | wc -l 104 | 844 105 | 106 | real 0m0.083s 107 | user 0m0.065s 108 | sys 0m0.020s 109 | ``` 110 | 111 | ### bash-my-aws commands can use `$BMA_AWSCLI` env var 112 | 113 | The `bma` command exports a function called `aws` that calls the 114 | command in BMA_AWSCLI=cachews if one is set. 115 | 116 | ```shell 117 | $ time bma instances | wc -l 118 | 4 119 | 120 | real 0m1.200s 121 | user 0m0.834s 122 | sys 0m0.098s 123 | ``` 124 | 125 | ```shell 126 | $ time BMA_AWSCLI=cachews bma instances | wc -l 127 | 4 128 | 129 | real 0m0.099s 130 | user 0m0.079s 131 | sys 0m0.026s 132 | ``` 133 | 134 | bash-my-aws added `$BMA_AWSCLI` support back in 2020 to assist testing breaking 135 | changes in aws-cli-v2. Almost four years later when I wrote cachews it was a 136 | nice surprise to rediscover it. 137 | 138 | ``` 139 | commit 17376e63222033fef2a1005be6bccb0123263629 140 | Author: Mike Bailey 141 | Date: Sun Mar 8 12:25:00 2020 +1100 142 | 143 | Set $BMA_AWSCLI to specify which awscli to use 144 | 145 | Added while testing breaking changes in aws-cli-v2 146 | 147 | Also useful for testing with 148 | [localstack](https://github.com/localstack/localstack) 149 | 150 | e.g. `$ BMA_AWSCLI=awslocal stacks` 151 | 152 | Also, print AWSCLI version when BMA_DEBUG=true 153 | ``` 154 | -------------------------------------------------------------------------------- /docs/cloudformation-naming.md: -------------------------------------------------------------------------------- 1 | title: CloudFormation Naming Conventions - Bash-my-AWS 2 | description: Suggested naming conventions for CloudFormation stacks, 3 | templates and parameter files that enable use of some shortcuts 4 | in the Bash-my-AWS commands. 5 | 6 | ## Suggested stack/template/param file naming conventions 7 | 8 | bash-my-aws can take a lot of the effort out of creating and updating 9 | CloudFormation (CFN) stacks. Tab completion on remote stack names and 10 | even local file names is provided. 11 | 12 | Additionally, the create/update/diff commands can make life much easier 13 | if you follow some simple file naming conventions. 14 | 15 | *These are completely optional.* 16 | 17 | 18 | stack : token-env 19 | template: token.yml 20 | params : token-params-env.json or params/token-params-env.json 21 | 22 | Where: 23 | 24 | token : describes the resources (mywebsite, vpc, bastion, etc) 25 | env : environment descriptor (dev, test, prod, etc) 26 | 27 | Following these (entirely optional) conventions means bash-my-aws can 28 | infer template & params file from stack name. For example: 29 | 30 | $ stack-create mywebsite-test 31 | 32 | is equivalent (if files present) to: 33 | 34 | $ stack-create mywebsite-test mywebsite.yml mywebsite-params-test.json 35 | 36 | you could even achieve the same result with: 37 | 38 | $ stack-create mywebsite-params-test.json 39 | 40 | 41 | Other benefits include: 42 | 43 | * ease in locating stack for template (and vice versa) based on name 44 | * template and params files are listed together on filesystem 45 | * stack name env suffixes protect against accidents (wrong account error) 46 | * supports prodlike non-prod environments through using same template 47 | 48 | And don't forget, these naming conventions are completely optional. 49 | -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | # bash-my-aws development guide. 2 | 3 | There are three distinct classes of functions types within `bash-my-aws`; 4 | *query*, *detail*, and *action* functions. 5 | 6 | Query functions are responsible for requesting AWS return a list of resources 7 | by their unique identifier. That would mean that when querying EC2 instances, 8 | it should return a list of instance-ids. If we were to query CloudFormation, 9 | then stacks are what would be returned. 10 | 11 | Detail functions should provide you an output of one or more attributes. 12 | 13 | eg. instance-asg(), instance-state() 14 | 15 | Action functions should perform an action against a resource. An action for an 16 | EC2 instance may be something like *terminate* or *associate_eip*. 17 | 18 | e.g. stack-create(), instance-terminate(), asg-suspend() 19 | 20 | 21 | There are some great things about `bash-my-aws` which I would hate to lose. 22 | 23 | * It's easy to look at the code and learn how `awscli` works. 24 | * Simple tasks have simple commands. 25 | * It's really easy to extend. 26 | 27 | stdin should always be supported. You should be able to pipe one function into 28 | the next with ease. If an action function cannot be piped the output of a query 29 | function and have it work, without manipulation though other tools, then 30 | something is wrong. 31 | 32 | ## Namespacing 33 | 34 | We've decided to namespace functions by the resource they're concerned with. 35 | Yes, this may seem a bit like postgres naming their command createdb but that's 36 | OK. bash-my-aws reduces my keystrokes. We'll work something out if this becomes 37 | a problem. 38 | 39 | ``` 40 | stack-asgs stack-elbs stack-outputs stack-tags stack-validate 41 | stack-create stack-events stack-parameters stack-tail stacks 42 | stack-delete stack-failure stack-resources stack-template 43 | stack-diff stack-instances stack-status stack-update 44 | ``` 45 | 46 | ``` 47 | instance-asg instance-ssh instance-stop instance-volumes 48 | instance-console instance-ssh-details instance-tags instances 49 | instance-dns instance-stack instance-terminate 50 | instance-iam-profile instance-start instance-type 51 | instance-ip instance-state instance-userdata 52 | ``` 53 | 54 | ``` 55 | asg-capacity asg-max-size-set asg-resume asgs 56 | asg-desired-size-set asg-min-size-set asg-suspend 57 | asg-instances asg-processes_suspended asgard 58 | ``` 59 | 60 | 61 | 62 | ## Resources 63 | 64 | This is a partial list of resources, with possible alternative names in brackets. 65 | 66 | - asg (autoscaling_group) 67 | - bucket 68 | - elb (load_balancer) 69 | - instance 70 | - instance_type 71 | - rds (relational_database) 72 | - dynamodb (relational_database) 73 | - sg (security_group) 74 | - stack (cloudformation_stack) 75 | 76 | 77 | ## Query Functions 78 | 79 | Query functions are always namespaced under the plural of the resource. 80 | 81 | ### Default Query 82 | 83 | TODO: lets talk about the details. 84 | 85 | Some example usage of this function would be: 86 | 87 | $ 88 | resource_id1 attribute attribute 89 | resource_id2 attribute attribute 90 | resource_id3 attribute attribute 91 | 92 | 93 | ## Detail Functions 94 | 95 | Detail functions are always namespaced under the singular of the resource. 96 | 97 | Some example usage of this function would be: 98 | 99 | $ 100 | attribute1: value1 101 | attribute2: value2 102 | 103 | Some example responses: 104 | 105 | $ instance_security_groups i-abcd1234 106 | sg-00000001 i-abcd1234 107 | sg-00000002 i-abcd1234 108 | sg-00000003 i-abcd1234 109 | sg-00000004 i-abcd1234 110 | 111 | $ security_group_rules 112 | TODO: I'm not sure yet how to present a security group rule. 113 | 114 | $ instance_type i-abcd1234 115 | c3.large i-abcd1234 116 | 117 | $ instance_type_memory c3.large 118 | 3.75 c3.large 119 | 120 | 121 | 122 | ## Action Functions 123 | 124 | Action functions are always namespaced under the singular of the resource and are suffixed with the action they are responsible for undertaking. 125 | 126 | Some example usage of this function would be: 127 | 128 | $ _ 129 | 130 | 131 | ## Tests 132 | 133 | You can start the test suite by running the command `make test`. 134 | 135 | 136 | ## STDIN 137 | 138 | * The first word of each line must be a resource. 139 | * Additional information will be disregarded. 140 | -------------------------------------------------------------------------------- /docs/images/bma-02-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/images/bma-02-2.gif -------------------------------------------------------------------------------- /docs/images/cachews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/images/cachews.png -------------------------------------------------------------------------------- /docs/linuxconf2020.md: -------------------------------------------------------------------------------- 1 | title: Bash-my-AWS presentation video from LinuxConf 2020 2 | description: Video from Mike Bailey's presentation, 'Bash-my-AWS: CLI commands for managing AWS resources' at LCA 2020 3 | 4 | Mike Bailey introduces Bash-my-AWS with live demo at LinuxConf 2020. 5 | 6 | 7 | 8 | Mon 13 Jan 2020. [Linux Conf. Gold Coast, Australia](https://linux.conf.au/schedule/presentation/144/). 9 | -------------------------------------------------------------------------------- /docs/pipe-skimming.md: -------------------------------------------------------------------------------- 1 | title: Pipe-Skimming pattern for Unix CLI Tools - Bash-my-AWS 2 | description: Pipe-Skimming allows expressive, line oriented text to be 3 | piped to commands that skim only the resource identifiers from each line. 4 | 5 | Pipe-Skimming: Enhancing the UI of Unix CLI tools 6 | 7 | 8 | 9 | When text is piped to a command that implements pipe-skimming, it appends 10 | the first item from each line (STDIN) to its argument array (ARGV). 11 | 12 | This allows for expressive line oriented output to be piped to commands 13 | that will skim only the resource identifiers from each line. 14 | 15 | This makes exploring and traversing related resources from the command 16 | line a pleasure: 17 | 18 | $ stacks | grep nginx | stack-asgs | asg-instances | instance-state 19 | i-0e219fbee42347721 shutting-down 20 | 21 | Pipe-skimming is simple to implement within commands and doesn't require 22 | any changes to the command shell. 23 | 24 | 25 | ## How it Works 26 | 27 | The following examples show commands from [Bash-my-AWS](https://bash-my-aws.org/), 28 | the project from which this pattern was extracted. 29 | 30 | 31 | ### Usage Examples 32 | 33 | Here we list EC2 Instances running in an Amazon AWS Account: 34 | 35 | $ instances 36 | i-09d962a1d688bb3ec t3.nano running grafana-bma 2020-01-16T03:53:44.000Z 37 | i-083f73ad5a1895ba0 t3.small running huginn-bma 2020-01-16T03:54:24.000Z 38 | i-0e219fbee42347721 t3.nano running nginx-bma 2020-01-16T03:56:22.000Z 39 | 40 | 41 | Piping output from this command into `instance-asg` returns a list of 42 | AutoScaling Groups (ASGs) they belong to: 43 | 44 | $ instances | instance-asg 45 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX i-083f73ad5a1895ba0 46 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU i-0e219fbee42347721 47 | grafana-bma-AutoScalingGroup-1NXJHMJVZQVMB i-09d962a1d688bb3ec 48 | 49 | 50 | While functionally identical, the example above is far easier to type 51 | than this example using command arguments: 52 | 53 | $ instance-asg i-09d962a1d688bb3ec i-083f73ad5a1895ba0 i-0e219fbee42347721 54 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX i-083f73ad5a1895ba0 55 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU i-0e219fbee42347721 56 | grafana-bma-AutoScalingGroup-1NXJHMJVZQVMB i-09d962a1d688bb3ec 57 | 58 | 59 | We can continue adding commands to our pipeline: 60 | 61 | $ instances | instance-asg | asg-capacity 62 | grafana-bma-AutoScalingGroup-1NXJHMJVZQVMB 1 1 2 63 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX 1 1 2 64 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU 1 1 2 65 | 66 | 67 | ### Implementation in Bash-my-AWS 68 | 69 | The command `instance-asg` (a Bash function) appends the first item 70 | from each line of piped input on STDIN to its argument list: 71 | 72 | instance-asg() { 73 | 74 | # List autoscaling group membership of EC2 Instance(s) 75 | # 76 | # USAGE: instance-asg instance-id [instance-id] 77 | 78 | local instance_ids=$(skim-stdin "$@") 79 | [[ -z $instance_ids ]] && __bma_usage "instance-id [instance-id]" && return 1 80 | 81 | aws ec2 describe-instances \ 82 | --instance-ids $instance_ids \ 83 | --output text \ 84 | --query " 85 | Reservations[].Instances[][ 86 | { 87 | "AutoscalingGroupName": 88 | [Tags[?Key=='aws:autoscaling:groupName'].Value][0][0], 89 | "InstanceId": InstanceId 90 | } 91 | ][]" | 92 | columnise 93 | } 94 | 95 | 96 | This implementation uses a simple Bash function called `skim-stdin`: 97 | 98 | skim-stdin() { 99 | 100 | # Append first token from each line of STDIN to argument list 101 | # 102 | # Implementation of `pipe-skimming` pattern. 103 | # 104 | # Typical usage within Bash-my-AWS: 105 | # 106 | # - local asg_names=$(skim-stdin "$@") # Append to arg list 107 | # - local asg_names=$(skim-stdin) # Only draw from STDIN 108 | # 109 | # $ stacks | skim-stdin foo bar 110 | # foo bar huginn mastodon grafana 111 | # 112 | # $ stacks 113 | # huginn CREATE_COMPLETE 2020-01-11T06:18:46.905Z NEVER_UPDATED NOT_NESTED 114 | # mastodon CREATE_COMPLETE 2020-01-11T06:19:31.958Z NEVER_UPDATED NOT_NESTED 115 | # grafana CREATE_COMPLETE 2020-01-11T06:19:47.001Z NEVER_UPDATED NOT_NESTED 116 | 117 | ( 118 | printf -- "$*" # Print all args 119 | printf " " # Print a space 120 | [[ -t 0 ]] || awk 'ORS=" " { print $1 }' # Print first token of each line of STDIN 121 | ) | awk '{$1=$1;print}' # Trim leading/trailing spaces 122 | } 123 | 124 | 125 | Almost every command in [Bash-my-AWS](https://bash-my-aws.org) makes use of 126 | `skim-stdin` to accept resource identifiers via arguments and/or piped input on 127 | STDIN. 128 | 129 | AFAIK, this pattern has not previously been described. 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/assets/elb-asg-ec2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020-lightning/assets/elb-asg-ec2.png -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/assets/gccc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020-lightning/assets/gccc.jpg -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/assets/ph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020-lightning/assets/ph.jpg -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/assets/skim-stdin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020-lightning/assets/skim-stdin.png -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pipe-Skimming - LinuxConf 2020 5 | 6 | 18 | 19 | 20 | 56 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/script03: -------------------------------------------------------------------------------- 1 | Script started on 2020-01-17 13:06:15+10:00 [TERM="xterm-256color" TTY="/dev/pts/8" COLUMNS="90" LINES="22"] 2 | ]777;notify;Command completed;bma type skim-stdin \]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 3 | $  4 | $ instances 5 | 777;preexecœi-0588726918d6ee19b t3.small running huginn-bma 2020-01-17T01:57:33.000Z 6 | i-08ec93cf02239df3d t3.nano running nginx-bma 2020-01-17T01:57:40.000Z 7 | i-09b1a3bc8917b9ca0 t3.nano running grafana-bma 2020-01-17T01:57:42.000Z 8 | i-0120b019f373f8860 t3.small running huginn-bma 2020-01-17T02:40:14.000Z 9 | i-065bf156cdd2d6dc3 t3.nano running grafana-bma 2020-01-17T02:40:20.000Z 10 | i-078337da5262a5d5a t3.nano running nginx-bma 2020-01-17T02:40:28.000Z 11 | ]777;notify;Command completed;instances\]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 12 | $ instance-asg 13 | 777;preexecœUSAGE: instance-asg instance-id [instance-id] 14 | ]777;notify;Command completed;instance-asg\]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 15 | $ instance-asgs | instance-asg 16 | 777;preexecœgrafana-bma-AutoScalingGroup-1NXJHMJVZQVMB i-09b1a3bc8917b9ca0 17 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU i-08ec93cf02239df3d 18 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU i-078337da5262a5d5a 19 | grafana-bma-AutoScalingGroup-1NXJHMJVZQVMB i-065bf156cdd2d6dc3 20 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX i-0120b019f373f8860 21 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX i-0588726918d6ee19b 22 | ]777;notify;Command completed;instances | instance-asg\]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 23 | $ instances | instance-asg | asg- 24 | asg-capacity asg-max-size-set asg-scaling-activities 25 | asg-desired-size-set asg-min-size-set asg-stack 26 | asg-instances asg-processes_suspended asg-suspend 27 | asg-launch-configuration asg-resume 28 | 29 | $ instances | instance-asg | asg-capacity 30 | 777;preexecœgrafana-bma-AutoScalingGroup-1NXJHMJVZQVMB 1 2 2 31 | huginn-bma-AutoScalingGroup-QS7EQOT1G7OX 1 2 2 32 | nginx-bma-AutoScalingGroup-106KHAYHUSRHU 1 2 2 33 | ]777;notify;Command completed;instances | instance-asg | asg-capacity \]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 34 | $  35 | $ bma type instance-asg 36 | 777;preexecœinstance-asg is a function 37 | instance-asg () 38 | { 39 | local instance_ids=$(skim-stdin "$@"); 40 | [[ -z $instance_ids ]] && __bma_usage "instance-id [instance-id]" && return 1; 41 | aws ec2 describe-instances --instance-ids $instance_ids --output text --query " 42 | Reservations[].Instances[][ 43 | { 44 | "AutoscalingGroupName": 45 | [Tags[?Key=='aws:autoscaling:groupName'].Value][0][0], 46 | "InstanceId": InstanceId 47 | } 48 | ][]" | column -s' ' -t 49 | } 50 | ]777;notify;Command completed;bma type instance-asg\]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 51 | $ bma type skim-stdin 52 | 777;preexecœskim-stdin is a function 53 | skim-stdin () 54 | { 55 | echo $@ $([[ -t 0 ]] || awk 'ORS=" " { print $1 }') 56 | } 57 | ]777;notify;Command completed;bma type skim-stdin \]777;precmd\]0;m@localhost:~/.bash-my-aws\]7;file://localhost.localdomain/home/m/.bash-my-aws\ 58 | $ exit 59 | 60 | Script done on 2020-01-17 13:07:32+10:00 [COMMAND_EXIT_CODE="0"] 61 | -------------------------------------------------------------------------------- /docs/slides/lca2020-lightning/script03-timing: -------------------------------------------------------------------------------- 1 | 0.369481 153 2 | 0.000163 5 3 | 1.063417 12 4 | 0.785015 1 5 | 0.041692 1 6 | 0.131068 1 7 | 0.130326 1 8 | 0.082401 1 9 | 0.098469 1 10 | 0.115708 1 11 | 0.059148 1 12 | 0.199305 1 13 | 1.043864 2 14 | 0.000346 15 15 | 1.090803 474 16 | 0.005585 142 17 | 0.000033 3 18 | 0.000015 2 19 | 5.949352 1 20 | 0.054200 1 21 | 0.085777 1 22 | 0.157539 1 23 | 0.082871 1 24 | 0.130696 1 25 | 0.126670 1 26 | 0.077862 1 27 | 0.758478 1 28 | 0.556572 1 29 | 0.063002 1 30 | 0.087636 1 31 | 1.962162 2 32 | 0.000421 15 33 | 0.041817 47 34 | 0.006331 145 35 | 0.000092 5 36 | 5.285878 12 37 | 0.211459 8 38 | 0.957839 1 39 | 0.440574 1 40 | 0.321702 1 41 | 0.259987 1 42 | 0.029368 1 43 | 0.156103 1 44 | 0.123360 1 45 | 0.129010 1 46 | 0.122617 1 47 | 0.154131 1 48 | 0.064068 1 49 | 0.185980 1 50 | 0.804395 1 51 | 0.069904 1 52 | 0.072462 1 53 | 5.252453 2 54 | 1.000253 15 55 | 1.752400 390 56 | 0.005753 157 57 | 0.000040 3 58 | 0.000007 2 59 | 10.423892 24 60 | 5.669835 1 61 | 0.536342 1 62 | 0.271094 1 63 | 0.374355 1 64 | 0.062315 1 65 | 0.091461 1 66 | 0.606527 1 67 | 0.440605 1 68 | 0.484988 296 69 | 1.775263 1 70 | 0.061732 1 71 | 0.372612 7 72 | 1.343720 2 73 | 0.001413 15 74 | 2.831031 159 75 | 0.005721 173 76 | 0.000095 5 77 | 3.268316 12 78 | 2.354384 1 79 | 0.055674 1 80 | 0.113402 1 81 | 0.117561 1 82 | 0.279089 1 83 | 0.052716 1 84 | 0.113280 1 85 | 0.127368 1 86 | 0.146639 1 87 | 0.520760 1 88 | 0.042210 1 89 | 0.098005 1 90 | 0.128062 1 91 | 0.086665 1 92 | 0.277368 1 93 | 0.081706 1 94 | 0.059014 1 95 | 0.156667 1 96 | 0.566025 1 97 | 0.075684 1 98 | 0.184564 1 99 | 0.595689 2 100 | 0.000316 15 101 | 0.043583 492 102 | 0.005919 154 103 | 0.000086 5 104 | 61.177073 1 105 | 0.052595 1 106 | 0.148850 1 107 | 0.115137 1 108 | 0.321458 1 109 | 0.065252 1 110 | 0.076339 1 111 | 0.153379 1 112 | 0.126568 1 113 | 0.530923 1 114 | 0.083884 1 115 | 0.130456 1 116 | 0.126248 1 117 | 0.146988 7 118 | 1.254501 2 119 | 0.000452 15 120 | 0.042885 106 121 | 0.006173 153 122 | 0.000112 5 123 | 11.697056 6 124 | -------------------------------------------------------------------------------- /docs/slides/lca2020/assets/elb-asg-ec2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020/assets/elb-asg-ec2.png -------------------------------------------------------------------------------- /docs/slides/lca2020/assets/gccc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020/assets/gccc.jpg -------------------------------------------------------------------------------- /docs/slides/lca2020/assets/ph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbailey/bash-my-aws/d35235e63568ec3c7fa9070e2d6dc1537eb11968/docs/slides/lca2020/assets/ph.jpg -------------------------------------------------------------------------------- /docs/slides/lca2020/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bash-my-AWS - LinuxConf 2020 5 | 6 | 18 | 19 | 20 | 175 | 177 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/specs/feature-include-headers-in-output/header-comment-implementation.md: -------------------------------------------------------------------------------- 1 | # Header Comment Implementation for bash-my-aws 2 | 3 | ## Overview 4 | 5 | This document outlines the implementation of headers as comments for bash-my-aws resource listing functions. This approach maintains 100% backwards compatibility while improving usability. 6 | 7 | ## Core Implementation 8 | 9 | ### 1. Enhanced skim-stdin Function 10 | 11 | ```bash 12 | skim-stdin() { 13 | # Append first token from each line of STDIN to argument list 14 | # Now ignores comment lines starting with # 15 | 16 | local skimmed_stdin="$([[ -t 0 ]] || awk ' 17 | /^#/ { next } # Skip comment lines (headers) 18 | { print $1 } # Extract first field 19 | ' ORS=" ")" 20 | 21 | printf -- '%s %s' "$*" "$skimmed_stdin" | 22 | awk '{$1=$1;print}' # trim leading/trailing spaces 23 | } 24 | ``` 25 | 26 | ### 2. Header Output Helper 27 | 28 | ```bash 29 | # Add to shared-functions 30 | __bma_output_header() { 31 | local header="$1" 32 | 33 | # Determine if headers should be shown 34 | local show_headers="false" 35 | case "${BMA_HEADERS:-auto}" in 36 | always) show_headers="true" ;; 37 | never) show_headers="false" ;; 38 | auto) [[ -t 1 ]] && show_headers="true" ;; 39 | esac 40 | 41 | [[ "$show_headers" == "true" ]] && echo "# $header" 42 | } 43 | ``` 44 | 45 | ### 3. Updated Function Example 46 | 47 | ```bash 48 | instances() { 49 | # List EC2 Instances 50 | 51 | local instances=$(skim-stdin) 52 | local filters=$(__bma_read_filters $@) 53 | 54 | # Output header comment 55 | __bma_output_header "INSTANCE_ID AMI_ID TYPE STATE NAME LAUNCH_TIME AZ VPC" 56 | 57 | aws ec2 describe-instances \ 58 | ${instances/#/'--instance-ids '} \ 59 | --output text \ 60 | --query " 61 | Reservations[].Instances[][ 62 | InstanceId, 63 | ImageId, 64 | InstanceType, 65 | State.Name, 66 | [Tags[?Key=='Name'].Value][0][0], 67 | LaunchTime, 68 | Placement.AvailabilityZone, 69 | VpcId 70 | ]" | 71 | grep -E -- "$filters" | 72 | LC_ALL=C sort -t$'\t' -k 6 | 73 | columnise 74 | } 75 | ``` 76 | 77 | ## Usage Examples 78 | 79 | ### Default Behavior (auto mode) 80 | 81 | ```bash 82 | # Terminal: shows headers 83 | $ instances 84 | # INSTANCE_ID AMI_ID TYPE STATE NAME LAUNCH_TIME AZ VPC 85 | i-4e15ece1de1a3f869 ami-123456789012 t3.nano running nagios 2019-12-10T08:17:18.000Z ap-southeast-2a None 86 | 87 | # Pipe: no headers, skim-stdin ignores comment 88 | $ instances | instance-state 89 | i-4e15ece1de1a3f869 running 90 | ``` 91 | 92 | ### Forced Headers 93 | 94 | ```bash 95 | # Always show headers, even in pipes 96 | $ BMA_HEADERS=always instances | head -2 97 | # INSTANCE_ID AMI_ID TYPE STATE NAME LAUNCH_TIME AZ VPC 98 | i-4e15ece1de1a3f869 ami-123456789012 t3.nano running nagios 2019-12-10T08:17:18.000Z ap-southeast-2a None 99 | ``` 100 | 101 | ### No Headers 102 | 103 | ```bash 104 | # Never show headers, even in terminal 105 | $ BMA_HEADERS=never instances 106 | i-4e15ece1de1a3f869 ami-123456789012 t3.nano running nagios 2019-12-10T08:17:18.000Z ap-southeast-2a None 107 | ``` 108 | 109 | ## Implementation Checklist 110 | 111 | ### Phase 1: Core Changes 112 | - [ ] Update `skim-stdin` in `lib/shared-functions` 113 | - [ ] Add `__bma_output_header` helper function 114 | - [ ] Test backwards compatibility with existing scripts 115 | - [ ] Document BMA_HEADERS environment variable 116 | 117 | ### Phase 2: Update High-Value Functions 118 | - [ ] `instances()` - lib/instance-functions 119 | - [ ] `stacks()` - lib/stack-functions 120 | - [ ] `buckets()` - lib/s3-functions 121 | - [ ] `vpcs()` - lib/vpc-functions 122 | - [ ] `asgs()` - lib/asg-functions 123 | 124 | ### Phase 3: Complete Rollout 125 | - [ ] Update all remaining listing functions 126 | - [ ] Add header information to function documentation 127 | - [ ] Update README with header feature 128 | - [ ] Create migration guide for users 129 | 130 | ## Headers for Key Functions 131 | 132 | ```bash 133 | # instances 134 | INSTANCE_ID AMI_ID TYPE STATE NAME LAUNCH_TIME AZ VPC 135 | 136 | # stacks 137 | STACK_NAME STATUS CREATION_TIME LAST_UPDATED NESTED 138 | 139 | # buckets 140 | BUCKET_NAME CREATION_DATE 141 | 142 | # vpcs 143 | VPC_ID DEFAULT NAME CIDR STACK VERSION 144 | 145 | # asgs 146 | ASG_NAME NAME_TAG CREATED_TIME AVAILABILITY_ZONES 147 | 148 | # subnets 149 | SUBNET_ID VPC_ID AZ CIDR NAME 150 | 151 | # keypairs 152 | KEYPAIR_NAME FINGERPRINT 153 | 154 | # images 155 | IMAGE_ID CREATED NAME DESCRIPTION 156 | 157 | # volumes 158 | VOLUME_ID STATE SIZE TYPE CREATED NAME 159 | ``` 160 | 161 | ## Testing Strategy 162 | 163 | ### Backwards Compatibility Tests 164 | 165 | ```bash 166 | # Test 1: Existing pipe behavior unchanged 167 | instances | head -1 | grep -q "^i-" || echo "FAIL: First line should be instance ID" 168 | 169 | # Test 2: skim-stdin ignores comments 170 | echo -e "# HEADER\ni-12345" | skim-stdin | grep -q "i-12345" || echo "FAIL: Should extract instance ID" 171 | 172 | # Test 3: No headers in pipe by default 173 | instances | grep -q "^# INSTANCE_ID" && echo "FAIL: Headers shown in pipe" 174 | 175 | # Test 4: Headers shown in terminal 176 | BMA_HEADERS=always instances | grep -q "^# INSTANCE_ID" || echo "FAIL: Headers not shown" 177 | ``` 178 | 179 | ### User Acceptance Tests 180 | 181 | ```bash 182 | # Test 1: Headers improve readability 183 | instances # Should show column headers 184 | 185 | # Test 2: Piping still works 186 | instances | instance-state # Should not break 187 | 188 | # Test 3: File output includes headers for reference 189 | instances > /tmp/instances.txt 190 | head -1 /tmp/instances.txt # Should show header comment 191 | ``` 192 | 193 | ## Benefits 194 | 195 | 1. **Zero Breaking Changes** - Existing scripts continue to work 196 | 2. **Self-Documenting** - Headers explain column meanings 197 | 3. **Standard Unix Pattern** - Comments are universally understood 198 | 4. **Flexible Control** - Users can force headers on/off 199 | 5. **Clean Implementation** - Minimal code changes required 200 | 201 | ## Future Enhancements 202 | 203 | Once headers are implemented, we could add: 204 | 205 | 1. **Column Selection** 206 | ```bash 207 | instances --columns instance_id,name,state 208 | ``` 209 | 210 | 2. **Format Options** 211 | ```bash 212 | instances --format json 213 | instances --format csv 214 | ``` 215 | 216 | 3. **Schema Discovery** 217 | ```bash 218 | bma-schema instances 219 | ``` 220 | 221 | The comment-based header approach provides a solid foundation for these future features while solving the immediate usability need. -------------------------------------------------------------------------------- /docs/specs/feature-include-headers-in-output/header-line-challenge.md: -------------------------------------------------------------------------------- 1 | # Header Line Challenge for bash-my-aws Resource Listing Functions 2 | 3 | ## Background 4 | 5 | bash-my-aws is a popular 10+ year old project that provides CLI commands for managing AWS resources. The project follows consistent patterns for listing resources but currently does not include header lines in its output. 6 | 7 | ## Current Implementation 8 | 9 | ### Output Format 10 | - Functions output TSV (Tab-Separated Values) by default 11 | - The `columnise` function converts TSV to fixed-width columns when stdout is a terminal 12 | - No headers are included in the output 13 | 14 | ### Example Output 15 | ```bash 16 | $ instances 17 | i-4e15ece1de1a3f869 ami-123456789012 t3.nano running nagios 2019-12-10T08:17:18.000Z ap-southeast-2a None 18 | i-89cefa9403373d7a5 ami-123456789012 t3.nano running postgres1 2019-12-10T08:17:20.000Z ap-southeast-2a None 19 | ``` 20 | 21 | ## The Challenge 22 | 23 | We want to add header lines to resource listing functions to improve usability, but face several constraints: 24 | 25 | ### 1. **skim-stdin() Compatibility** 26 | The `skim-stdin()` function is fundamental to bash-my-aws's pipe-friendly design. It extracts the first token from each line of stdin and appends it to the argument list. Headers would be interpreted as resource IDs, breaking this pattern. 27 | 28 | Example of the problem: 29 | ```bash 30 | # If instances had headers: 31 | $ instances | instance-stop 32 | # Would try to stop an instance with ID "InstanceId" (the header) 33 | ``` 34 | 35 | ### 2. **Backwards Compatibility** 36 | - The project is over 10 years old with an established user base 37 | - Scripts and automation depend on the current output format 38 | - Breaking changes would impact many users 39 | 40 | ### 3. **Consistency Across Output Modes** 41 | - Output should be predictable whether piped or displayed in terminal 42 | - Showing headers only when stdout is a terminal would surprise users 43 | - Example: `instances > file.txt` would have different format than `instances` 44 | 45 | ### 4. **stderr is Not Suitable** 46 | - Sending headers to stderr was considered but rejected 47 | - stderr is for errors and diagnostics, not data formatting 48 | - Would complicate piping and redirection 49 | 50 | ## Requirements for a Solution 51 | 52 | 1. **Must not break skim-stdin()** - The pipe-skimming pattern must continue to work 53 | 2. **Backwards compatible** - Existing scripts must not break 54 | 3. **Consistent behavior** - Same output format regardless of terminal/pipe 55 | 4. **Clean implementation** - Should fit with bash-my-aws design philosophy 56 | 5. **Opt-in or smart detection** - Headers should not appear by default if they break existing usage 57 | 58 | ## Potential Solution Directions 59 | 60 | ### 1. **Metadata/Header Mode** 61 | - Add a flag or environment variable to enable headers 62 | - When enabled, functions could output headers before data 63 | - skim-stdin could be made header-aware 64 | 65 | ### 2. **Alternative Output Formats** 66 | - Support different output formats (json, yaml, table) 67 | - Headers would be natural in structured formats 68 | - Maintain TSV as default for compatibility 69 | 70 | ### 3. **Smart Header Detection** 71 | - Detect when output is being used by bash-my-aws functions 72 | - Skip headers when piping between bash-my-aws commands 73 | - Include headers for direct terminal output 74 | 75 | ### 4. **Header-aware skim-stdin** 76 | - Enhance skim-stdin to detect and skip header lines 77 | - Could use special markers or patterns 78 | - Maintains compatibility while adding functionality 79 | 80 | ## Questions to Explore 81 | 82 | 1. How do other CLI tools handle this problem? 83 | 2. What patterns exist for backwards-compatible enhancements? 84 | 3. Can we detect bash-my-aws function chains vs external usage? 85 | 4. What would be the minimal change for maximum benefit? 86 | 5. Could we use a progressive enhancement approach? 87 | 88 | ## Success Criteria 89 | 90 | A successful solution would: 91 | - Allow new users to see helpful headers 92 | - Not break any existing workflows 93 | - Be simple to implement and maintain 94 | - Feel natural within the bash-my-aws ecosystem 95 | - Potentially inspire similar improvements in other functions -------------------------------------------------------------------------------- /docs/specs/feature-include-headers-in-output/header-solutions-comparison.md: -------------------------------------------------------------------------------- 1 | # Header Line Solutions Comparison for bash-my-aws 2 | 3 | ## Executive Summary 4 | 5 | After extensive analysis, we've identified multiple approaches to add header lines to bash-my-aws while maintaining backwards compatibility. The solutions range from simple pattern matching to sophisticated metadata channels. 6 | 7 | ## Solution Comparison Matrix 8 | 9 | | Solution | Complexity | Backwards Compatible | User Experience | Implementation Effort | 10 | |----------|------------|---------------------|-----------------|---------------------| 11 | | **Smart Pattern Detection** | Medium | ✅ Excellent | Good | Medium | 12 | | **Environment Variables** | Low-Medium | ✅ Excellent | Good | Low | 13 | | **ANSI Escape Markers** | Medium | ✅ Excellent | Excellent | Medium | 14 | | **Metadata Side-Channel** | High | ✅ Excellent | Excellent | High | 15 | | **Context-Aware Enhancement** | High | ✅ Excellent | Excellent | High | 16 | | **Simple --no-headers Flag** | Low | ✅ Good | Good | Low | 17 | 18 | ## Detailed Solution Analysis 19 | 20 | ### 1. Smart Pattern Detection 21 | **Agent 1's Solution 3** 22 | 23 | ```bash 24 | # skim-stdin detects headers by pattern 25 | skim-stdin() { 26 | local skimmed_stdin="$([[ -t 0 ]] || awk ' 27 | NR == 1 && $1 ~ /^[A-Z][A-Z_]+$/ { next } 28 | $1 ~ /^(i-|vpc-|subnet-|rtb-|igw-|sg-|vol-|snap-|ami-)/ { print $1 } 29 | ' ORS=" ")" 30 | ... 31 | } 32 | ``` 33 | 34 | **Pros:** 35 | - No manual marking required 36 | - Works with minimal changes to existing functions 37 | - Progressive enhancement possible 38 | 39 | **Cons:** 40 | - Pattern matching may have edge cases 41 | - Requires careful tuning 42 | 43 | ### 2. ANSI Escape Sequence Headers 44 | **Agent 3's Solution 1** 45 | 46 | ```bash 47 | # Headers with invisible markers 48 | instances() { 49 | printf "\033[BMA-HEADER]INSTANCE_ID\tAMI_ID\t...\033[0m\n" 50 | # Normal output follows 51 | } 52 | ``` 53 | 54 | **Pros:** 55 | - Headers invisible to users but detectable by code 56 | - Clean terminal output 57 | - Minimal changes to skim-stdin 58 | 59 | **Cons:** 60 | - ANSI sequences might not work in all environments 61 | - Slightly magical/non-obvious 62 | 63 | ### 3. kubectl-Style Flag Approach 64 | **Agent 2's Research + Best Practices** 65 | 66 | ```bash 67 | # Global control via environment or flags 68 | export BMA_HEADERS=auto # auto/always/never 69 | 70 | instances() { 71 | local show_headers=$([[ -t 1 ]] && [[ "$BMA_HEADERS" != "never" ]]) 72 | [[ "$show_headers" == "true" ]] && echo "INSTANCE_ID AMI_ID ..." 73 | # Rest of function 74 | } 75 | ``` 76 | 77 | **Pros:** 78 | - Follows established CLI patterns 79 | - Simple to implement 80 | - User has explicit control 81 | 82 | **Cons:** 83 | - Requires updating all functions 84 | - No automatic detection in pipelines 85 | 86 | ### 4. Metadata Side-Channel 87 | **Agent 3's Solution 2** 88 | 89 | Uses separate file descriptor for schema information, allowing metadata to flow alongside data. 90 | 91 | **Pros:** 92 | - Most powerful and flexible 93 | - Enables rich tooling possibilities 94 | - Clean separation of concerns 95 | 96 | **Cons:** 97 | - Complex implementation 98 | - May be overkill for the problem 99 | - Learning curve for contributors 100 | 101 | ## Recommended Approach: Hybrid Progressive Enhancement 102 | 103 | Combine the best aspects of multiple solutions: 104 | 105 | ### Phase 1: Environment Variable Control (Quick Win) 106 | ```bash 107 | # Add to shared-functions 108 | bma_should_show_headers() { 109 | case "${BMA_HEADERS:-auto}" in 110 | always) echo "true" ;; 111 | never) echo "false" ;; 112 | auto) [[ -t 1 ]] && echo "true" || echo "false" ;; 113 | esac 114 | } 115 | 116 | # Update functions incrementally 117 | instances() { 118 | [[ $(bma_should_show_headers) == "true" ]] && \ 119 | echo "INSTANCE_ID AMI_ID TYPE STATE NAME LAUNCH_TIME AZ VPC" 120 | 121 | # Existing implementation unchanged 122 | ... 123 | } 124 | ``` 125 | 126 | ### Phase 2: Enhanced skim-stdin (Medium Term) 127 | ```bash 128 | # Make skim-stdin header-aware 129 | skim_stdin() { 130 | local skip_headers="${BMA_SKIP_HEADERS:-auto}" 131 | 132 | local skimmed_stdin="$([[ -t 0 ]] || awk -v skip="$skip_headers" ' 133 | BEGIN { header_seen = 0 } 134 | NR == 1 && skip != "false" && /^[A-Z][A-Z_]+/ { 135 | header_seen = 1 136 | next 137 | } 138 | { print $1 } 139 | ' ORS=" ")" 140 | 141 | printf -- '%s %s' "$*" "$skimmed_stdin" | awk '{$1=$1;print}' 142 | } 143 | ``` 144 | 145 | ### Phase 3: Metadata Commands (Long Term) 146 | ```bash 147 | # Add introspection commands 148 | bma-schema() { 149 | local cmd="$1" 150 | case "$cmd" in 151 | instances) echo "instance_id ami_id type state name launch_time az vpc" ;; 152 | stacks) echo "stack_name status creation_time last_updated nested" ;; 153 | # ... etc 154 | esac 155 | } 156 | 157 | # Future: JSON output mode 158 | instances-json() { 159 | BMA_OUTPUT_FORMAT=json instances "$@" 160 | } 161 | ``` 162 | 163 | ## Implementation Roadmap 164 | 165 | 1. **Week 1-2**: Implement environment variable control 166 | - Add `BMA_HEADERS` support 167 | - Update 2-3 high-value functions (instances, stacks, vpcs) 168 | - Test backwards compatibility 169 | 170 | 2. **Week 3-4**: Enhance skim-stdin 171 | - Add header detection logic 172 | - Test with piped commands 173 | - Document behavior 174 | 175 | 3. **Month 2**: Roll out to all functions 176 | - Update remaining functions 177 | - Add tests 178 | - Update documentation 179 | 180 | 4. **Future**: Advanced features 181 | - JSON output format 182 | - Schema introspection 183 | - Metadata side-channel (if needed) 184 | 185 | ## Success Metrics 186 | 187 | - ✅ No existing scripts break 188 | - ✅ New users see helpful headers by default 189 | - ✅ Power users can control behavior 190 | - ✅ Piping between commands works correctly 191 | - ✅ Implementation is maintainable 192 | 193 | ## Conclusion 194 | 195 | The hybrid approach provides immediate value while maintaining flexibility for future enhancements. Starting with environment variable control gives users choice today, while the roadmap provides a path to more sophisticated features without disrupting the existing ecosystem. -------------------------------------------------------------------------------- /docs/style.md: -------------------------------------------------------------------------------- 1 | bash-my-aws style guide 2 | ======================= 3 | 4 | * Always quote "$variables" 5 | * Only use parentheses around variables when not surrounded with whitespace (better way to phrase this?) 6 | * use ``$(date)`` rather than "``date``" 7 | * use `[[` and `]]` rather than `[`, `]`, or `test` 8 | 9 | * show errors with `__bma_error()` 10 | * show usage options with `__bma_usage()` 11 | * parse inputs with `__bma_read_inputs()` 12 | 13 | * regenerate and test the bash_completion script after adding new functions 14 | -------------------------------------------------------------------------------- /docs/tour.md: -------------------------------------------------------------------------------- 1 | # Tour 2 | 3 | Check this out! 4 | -------------------------------------------------------------------------------- /functions: -------------------------------------------------------------------------------- 1 | # DO NOT MANUALLY MODIFY THIS FILE. 2 | # Use 'scripts/build' to regenerate if required. 3 | 4 | __bma-using-aws-cli-v1 5 | __bma_error 6 | __bma_read_filters 7 | __bma_read_filters-az 8 | __bma_read_inputs 9 | __bma_read_stdin 10 | __bma_usage 11 | _bma_derive_params_from_stack_and_template 12 | _bma_derive_params_from_template 13 | _bma_derive_stack_from_params 14 | _bma_derive_stack_from_template 15 | _bma_derive_template_from_params 16 | _bma_derive_template_from_stack 17 | _bma_stack_args 18 | _bma_stack_capabilities 19 | _bma_stack_diff_params 20 | _bma_stack_diff_template 21 | _bma_stack_name_arg 22 | _bma_stack_params_arg 23 | _bma_stack_template_arg 24 | ad-app 25 | ad-app-owners 26 | ad-apps 27 | ad-group-members 28 | ad-groups 29 | ad-user-group-diff 30 | ad-user-groups 31 | ad-user-names 32 | ad-user-upns 33 | ad-users 34 | ad-users-graph 35 | afd-custom-domains 36 | afd-custom-domains-validation-request 37 | afd-endpoints 38 | afd-origin-groups 39 | afd-routes 40 | afd-waf-policies 41 | afd-waf-policy 42 | afd-waf-policy-rule-delete 43 | afd-waf-policy-rule-match-condition-values 44 | afd-waf-policy-rule-match-conditions 45 | afd-waf-policy-rules 46 | afds 47 | asg-capacity 48 | asg-desired-size-set 49 | asg-detach-instances 50 | asg-instances 51 | asg-launch-configuration 52 | asg-max-size-set 53 | asg-min-size-set 54 | asg-processes_suspended 55 | asg-resume 56 | asg-scaling-activities 57 | asg-stack 58 | asg-suspend 59 | asgs 60 | aws-account-alias 61 | aws-account-cost-explorer 62 | aws-account-cost-recommendations 63 | aws-account-each 64 | aws-account-id 65 | aws-accounts 66 | aws-panopticon 67 | az-account 68 | az-cache-item 69 | az-cache-item-delete 70 | az-cache-items 71 | az-user 72 | backup-jobs 73 | bucket-acls 74 | bucket-remove 75 | bucket-remove-force 76 | buckets 77 | cert-chain 78 | cert-delete 79 | cert-ificate 80 | cert-resource-record-valid 81 | cert-users 82 | cert-verify 83 | certs 84 | certs-arn 85 | cloudtrail-status 86 | cloudtrails 87 | columnise 88 | connector-group-apps 89 | connector-group-members 90 | connector-groups 91 | connectors 92 | debug 93 | deployment-delete-danger 94 | deployment-groups 95 | deployments-group 96 | distributions 97 | ecr-repositories 98 | ecr-repository-images 99 | elb-azs 100 | elb-dnsname 101 | elb-instances 102 | elb-stack 103 | elb-subnets 104 | elb-tag 105 | elb-tags 106 | elbs 107 | elbv2-azs 108 | elbv2-dnsname 109 | elbv2-subnets 110 | elbv2-target-groups 111 | elbv2s 112 | hosted-zone-ns-records 113 | hosted-zones 114 | iam-access-key-rotate 115 | iam-role-principal 116 | iam-roles 117 | iam-users 118 | image-deregister 119 | images 120 | instance-asg 121 | instance-az 122 | instance-console 123 | instance-dns 124 | instance-health-set-unhealthy 125 | instance-iam-profile 126 | instance-ip 127 | instance-profile 128 | instance-profile-role 129 | instance-rdp 130 | instance-ssh 131 | instance-ssh-details 132 | instance-ssm 133 | instance-ssm-platform-type 134 | instance-ssm-port-forward 135 | instance-stack 136 | instance-start 137 | instance-state 138 | instance-stop 139 | instance-stop-protection 140 | instance-stop-protection-disable 141 | instance-stop-protection-enable 142 | instance-subnet 143 | instance-tag 144 | instance-tag-create 145 | instance-tag-delete 146 | instance-tags 147 | instance-terminate 148 | instance-termination-protection 149 | instance-termination-protection-disable 150 | instance-termination-protection-enable 151 | instance-type 152 | instance-userdata 153 | instance-volumes 154 | instance-vpc 155 | instances 156 | keypair-create 157 | keypair-delete 158 | keypairs 159 | kms-alias-create 160 | kms-alias-delete 161 | kms-aliases 162 | kms-decrypt 163 | kms-encrypt 164 | kms-key-create 165 | kms-key-details 166 | kms-key-disable 167 | kms-key-enable 168 | kms-keys 169 | lambda-function-logs 170 | lambda-function-memory 171 | lambda-function-memory-set 172 | lambda-function-memory-step 173 | lambda-functions 174 | launch-configuration-asgs 175 | launch-configurations 176 | location 177 | location-each 178 | location-unset 179 | locations 180 | log-groups 181 | management-groups 182 | pcxs 183 | rds-db-clusters 184 | rds-db-instances 185 | region 186 | region-each 187 | regions 188 | resource-diff 189 | resource-export 190 | resource-group 191 | resource-group-export 192 | resource-group-unset 193 | resource-groups 194 | resource-show 195 | resourceids 196 | resources 197 | secrets 198 | service-principals 199 | skim-stdin 200 | skim-stdin-bma 201 | skim-stdin-tsv 202 | ssm-association-execution-targets 203 | ssm-association-executions 204 | ssm-associations 205 | ssm-automation-execution 206 | ssm-automation-execution-failures 207 | ssm-automation-executions 208 | ssm-automation-step-executions 209 | ssm-instances 210 | ssm-parameter-value 211 | ssm-parameters 212 | ssm-send-command 213 | ssm-send-command-windows 214 | stack-arn 215 | stack-asg-instances 216 | stack-asgs 217 | stack-cancel-update 218 | stack-create 219 | stack-delete 220 | stack-diff 221 | stack-elbs 222 | stack-events 223 | stack-exports 224 | stack-failure 225 | stack-instances 226 | stack-outputs 227 | stack-parameters 228 | stack-recreate 229 | stack-resources 230 | stack-status 231 | stack-tag 232 | stack-tags 233 | stack-tags-text 234 | stack-tail 235 | stack-template 236 | stack-template-changeset-latest 237 | stack-update 238 | stack-validate 239 | stacks 240 | sts-assume-role 241 | subnets 242 | subscription 243 | subscription-each 244 | subscription-unset 245 | subscriptions 246 | tag-keys 247 | tag-values 248 | target-group-targets 249 | target-groups 250 | vpc-az-count 251 | vpc-azs 252 | vpc-default-delete 253 | vpc-dhcp-options-ntp 254 | vpc-endpoint-services 255 | vpc-endpoints 256 | vpc-igw 257 | vpc-lambda-functions 258 | vpc-nat-gateways 259 | vpc-network-acls 260 | vpc-rds 261 | vpc-route-tables 262 | vpc-subnets 263 | vpcs 264 | -------------------------------------------------------------------------------- /lib/asg-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2046 3 | # shellcheck disable=SC2155 4 | # 5 | # asg-functions 6 | 7 | # -[ ] Is this useful? #task 8 | # -[ ] document #task 9 | asg-detach-instances() { 10 | 11 | # Detach all instances from asg(s) 12 | 13 | local asg_names=$(skim-stdin "$@") 14 | [[ -z $asg_names ]] && __bma_usage "asg_name [asg_name]" && return 1 15 | 16 | local asg_name 17 | for asg_name in $asg_names; do 18 | echo "This will suspend ${asg_name} and detach all instances:" 19 | asg-instances "$asg_name" 20 | local instance_ids=$(asg-instances "$asg_name" | awk '{print $1}') 21 | [ -t 0 ] || exec Opens web browser to AWS Cost Explorer with accounts selected 117 | 118 | local accounts_formatted="%22$(skim-stdin "$@" | sed 's/ /%22,%22/g')%22" 119 | 120 | local cmd_open="$(hash xdg-open &> /dev/null && echo 'xdg-open' || echo 'open')" 121 | 122 | $cmd_open "https://console.aws.amazon.com/cost-reports/home?#/custom?groupBy=None&hasBlended=false&hasAmortized=false&excludeDiscounts=true&excludeTaggedResources=false&chartStyle=Group&granularity=Monthly&reportType=CostUsage&isTemplate=false&filter=%5B%7B%22dimension%22:%22LinkedAccount%22,%22values%22:%5B$accounts_formatted%5D,%22include%22:true,%22children%22:null%7D%5D&usageAs=usageQuantity&forecastTimeRangeOption=None&&timeRangeOption=YearToDate" 123 | 124 | } 125 | 126 | # View a list of AWS_ACCOUNT_IDs in AWS Cost Recommendations 127 | 128 | aws-account-cost-recommendations(){ 129 | 130 | # Use with an AWS Organisations Master Account to open multiple accounts 131 | # in Cost Recommendations. 132 | # 133 | # $ grep non_prod AWS_ACCOUNTS | aws-account-each stacks FAILED 134 | # #=> Opens web browser to AWS Cost Recommendations with accounts selected 135 | 136 | local accounts_formatted="%22$(skim-stdin "$@" | sed 's/ /%22,%22/g')%22" 137 | 138 | local cmd_open="$(hash xdg-open &> /dev/null && echo 'xdg-open' || echo 'open')" 139 | 140 | $cmd_open "https://console.aws.amazon.com/cost-reports/home?#/recommendations/rightsizing?isLoading&service=AmazonEC2&rightsizingType=%5B%22Terminate%22,%22Modify%22%5D&filter=%5B%7B%22dimension%22:%22LINKED_ACCOUNT%22,%22values%22:%5B$accounts_formatted%5D,%22include%22:true%7D%5D" 141 | } 142 | -------------------------------------------------------------------------------- /lib/azure.azcli: -------------------------------------------------------------------------------- 1 | 2 | 3 | url='https://graph.microsoft.com/beta/users?${top_arg}${filter_arg}&select=userPrincipalName,displayName,onPremisesSyncEnabled,mail" 4 | 5 | az rest --method get --url 'https://graph.microsoft.com/beta/me/calendar/events' 6 | -------------------------------------------------------------------------------- /lib/backup-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # backup-functions 4 | 5 | backup-jobs() { 6 | 7 | # List Backup Jobs 8 | # 9 | # Lists backup jobs with state and date created. 10 | # 11 | # $ backup-jobs 12 | # X9B4Z0C5-R2E8-6G39-1M2P-7H8J6MPP9H8H COMPLETED 2023-08-27T20:00:00+10:00 13 | # Y7D1F2G1-X3B2-9H77-4N8R-2F9J4JZL8F2X COMPLETED 2023-08-27T20:00:00+10:00 14 | 15 | local filters=$(__bma_read_filters $@) 16 | 17 | aws backup list-backup-jobs \ 18 | --output text \ 19 | --query ' 20 | BackupJobs[].[ 21 | BackupJobId, 22 | State, 23 | CreationDate 24 | ]' | 25 | grep -E -- "$filters" | 26 | LC_ALL=C sort -k 3 | 27 | columnise 28 | } 29 | -------------------------------------------------------------------------------- /lib/cert-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # cert-functions 4 | 5 | certs() { 6 | 7 | # List ACM Certificates 8 | 9 | local cert_arns=$(skim-stdin) 10 | local filters=$(__bma_read_filters $@) 11 | 12 | local include_arn_bit='' 13 | [[ -n ${include_arn:-} ]] && include_arn_bit="CertificateArn," 14 | local retrieved_cert_arns=$( 15 | aws acm list-certificates \ 16 | --output text \ 17 | --query " 18 | CertificateSummaryList[${cert_arns:+?contains(['${cert_arns// /"','"}'], CertificateArn)}].[ 19 | CertificateArn 20 | ]" 21 | ) 22 | 23 | local cert_arn 24 | for cert_arn in $retrieved_cert_arns; do 25 | aws acm describe-certificate \ 26 | --certificate-arn "$cert_arn" \ 27 | --output text \ 28 | --query " 29 | Certificate.[ 30 | $include_arn_bit 31 | DomainName, 32 | Status, 33 | Type, 34 | length(InUseBy)==\`0\` && 'not-in-use' || 'in-use', 35 | NotBefore, 36 | NotAfter, 37 | join(',', [DomainValidationOptions[].ValidationMethod][]) 38 | ]" 39 | done | 40 | grep -E -- "$filters" | 41 | LC_ALL=C sort -b -k 5 | # sort by NotAfter 42 | column -t 43 | } 44 | 45 | certs-arn() { 46 | 47 | # Same as `certs` but with the ARN in first column 48 | 49 | local include_arn=true 50 | certs $@ 51 | } 52 | 53 | cert-users() { 54 | 55 | # List resources using ACM Cert(s) 56 | # 57 | # USAGE: cert-users cert-arn [cert-arn] 58 | 59 | local cert_arns=$(skim-stdin "$@") 60 | [[ -z "$cert_arns" ]] && __bma_usage "cert-arn [cert-arn]" && return 1 61 | 62 | local cert_arns 63 | for cert_arn in $cert_arns; do 64 | aws acm describe-certificate \ 65 | --certificate-arn "$cert_arn" \ 66 | --output text \ 67 | --query "Certificate.InUseBy[].[ 68 | @, 69 | \`$cert_arn\` 70 | ]" 71 | done | 72 | columnise 73 | } 74 | 75 | cert-delete() { 76 | 77 | # Delete ACM Cert(s) 78 | # 79 | # USAGE: cert-delete cert-arn [cert-arn] 80 | 81 | local cert_arns=$(skim-stdin "$@") 82 | [[ -z "$cert_arns" ]] && __bma_usage "cert-arn [cert-arn]" && return 1 83 | 84 | echo "You are about to delete the following certificates:" 85 | echo "$cert_arns" | tr " " "\n" 86 | [ -t 0 ] || exec 139 | 140 | [[ "$#" -lt 2 ]] && __bma_usage " [type]" && return 1 141 | 142 | local name="$1" 143 | local value="$2" 144 | local type="${3:-CNAME}" 145 | 146 | # Perform the DNS query 147 | local output=$(dig +noall +answer "$name" "$type") 148 | 149 | # Print the output of the DNS query 150 | echo "Query output:" 151 | echo "$output" 152 | 153 | # Check if the expected value is in the output 154 | if echo "$output" | grep -q "$value"; then 155 | echo "The DNS record is valid." 156 | return 0 157 | else 158 | echo "The DNS record is invalid or does not exist." 159 | return 1 160 | fi 161 | } 162 | 163 | 164 | # openssl verify -CAfile trusted-ca.crt -untrusted chain.crt cert.crt 165 | 166 | cert-verify() { 167 | 168 | # Verify ACM cert(s) 169 | # 170 | # USAGE: cert-chain cert-arn [cert-arn] 171 | 172 | local cert_arns=$(skim-stdin "$@") 173 | # Be quiet - makes it easier to scan with "cert-arns | cert_verify" 174 | # [[ -z "$cert_arns" ]] && __bma_usage "cert-arn [cert-arn]" && return 1 175 | 176 | local cert_arns 177 | for cert_arn in $cert_arns; do 178 | 179 | local cert_name=$(echo "$cert_arn" | bma certs | awk '{print $1}') 180 | 181 | # echo -n "$cert_name ($cert_arn): " 182 | echo -n "$cert_name : " 183 | 184 | local output=$( 185 | openssl verify \ 186 | -CAfile /etc/ssl/certs/ca-certificates.crt \ 187 | -untrusted <(cert-chain "$cert_arn") \ 188 | <(cert-ificate "$cert_arn") 2>&1 | 189 | sed 's#/dev/fd/[0-9]*: ##' 190 | ) 191 | [[ -n $output ]] && echo $output # lose the newlines 192 | done 193 | } 194 | -------------------------------------------------------------------------------- /lib/cloudfront-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # cloudfront-functions 4 | 5 | distributions() { 6 | 7 | # List Cloudfront Distributions 8 | 9 | aws cloudfront list-distributions \ 10 | --output "${BMA_OUTPUT_AWS:-${BMA_OUTPUT:-text}}" \ 11 | --query 'DistributionList.Items[].{ 12 | "ARN": ARN, 13 | "DomainName": DomainName, 14 | "Enabled": Enabled, 15 | "Comment": Comment 16 | }' 17 | } 18 | -------------------------------------------------------------------------------- /lib/cloudtrail-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # cloudtrail-functions 4 | 5 | cloudtrails() { 6 | 7 | # List Cloudtrails 8 | # 9 | # $ cloudtrails 10 | # failmode failmode-cloudtrail ap-southeast-2 IsMultiRegionTrail=true IncludeGlobalServiceEvents=true 11 | 12 | local cloudtrails=$(skim-stdin) 13 | local filters=$(__bma_read_filters "$@") 14 | 15 | aws cloudtrail describe-trails \ 16 | ${cloudtrails/#/'--trail-name-list '} \ 17 | --output text \ 18 | --query " 19 | trailList[].[ 20 | Name, 21 | S3BucketName, 22 | HomeRegion, 23 | join('=', [ 24 | 'IsMultiRegionTrail', 25 | to_string(IsMultiRegionTrail) 26 | ]), 27 | join('=', [ 28 | 'IncludeGlobalServiceEvents', 29 | to_string(IncludeGlobalServiceEvents) 30 | ]) 31 | ] " | 32 | grep -E -- "$filters" 33 | 34 | } 35 | 36 | cloudtrail-status() { 37 | 38 | # List logging status of Cloudtrails 39 | # 40 | # USAGE: cloudtrail-status cloudtrail [cloudtrail] 41 | 42 | local cloudtrails=$(skim-stdin "$@") 43 | [[ -z "$cloudtrails" ]] && __bma_usage "cloudtrail [cloudtrail]" && return 1 44 | 45 | local cloudtrail 46 | for cloudtrail in $cloudtrails; do 47 | aws cloudtrail get-trail-status \ 48 | --name "$cloudtrail" \ 49 | --output text \ 50 | --query "[ 51 | '$cloudtrail', 52 | join('=', ['IsLogging', to_string(IsLogging)]) 53 | ]" 54 | done | 55 | columnise 56 | } 57 | -------------------------------------------------------------------------------- /lib/cloudwatch-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # cloudwatch-functions 4 | 5 | cloudwatch-alarms(){ 6 | 7 | # List Cloudwatch Alarms 8 | # 9 | # USAGE: cloudwatch-alarms [filter] 10 | # 11 | # $ things 12 | # thing-1234567890123 Online Amazon Linux 2 192.168.1.10 server001.example.com 13 | # thing-1234567890124 Offline Amazon Linux 2 192.168.1.10 server001.example.com 14 | # thing-1234567890125 Online Amazon Linux 2 192.168.1.10 server001.example.com 15 | # 16 | # *Optionally provide a filter string for a `| grep` effect with tighter columisation:* 17 | # 18 | # $ things Online 19 | # i-1234567890123 Online Microsoft Windows Server 2019 Datacenter 68.0.11111 192.168.1.10 server001.example.com 20 | # i-1234567890124 Online Microsoft Windows Server 2022 Datacenter 68.0.11112 192.168.1.20 winserver002.example.com 21 | 22 | local alarms=$(skim-stdin) 23 | local filters=$(__bma_read_filters $@) 24 | 25 | local arg_filters="${alarms:+--alarm-names "${alarms}"}" 26 | 27 | aws cloudwatch describe-alarms \ 28 | $arg_filters \ 29 | --output text \ 30 | --query " 31 | MetricAlarms[].[ 32 | AlarmName, 33 | StateValue, 34 | ActionsEnabled, 35 | join(',', AlarmActions || \`[]\`) 36 | ]" \ 37 | | grep -E -- "$filters" \ 38 | | LC_ALL=C sort -t $'\t' -k 1 \ 39 | | columnise 40 | } 41 | 42 | cloudwatch-alarm-delete() { 43 | local alarms="$(skim-stdin "$@")" 44 | [[ -z $alarms ]] && __bma_usage "alarm_name [alarm_name]" && return 1 45 | 46 | alarm_names_json=$(printf "%s\n" "$alarms" | jq -R '.' | jq -crs '{"AlarmNames":.}') 47 | 48 | echo "You are about to delete the following Cloudwatch Alarms:" 49 | echo "$alarm_names_json" | jq . 50 | [ -t 0 ] || exec &2 "Set the following if using unsupported algorithm 'RC2-40-CBC': BMA_PKCS12_ARGS='-legacy'" && return 1 31 | 32 | cat "$pfx_file" | pkcs12-cert "$pfx_pass" > "$cert_file" 33 | cat "$pfx_file" | pkcs12-chain "$pfx_pass" > "$chain_file" 34 | 35 | # Verify certificate using CA file and chain 36 | openssl verify \ 37 | -CAfile "$cafile" \ 38 | -untrusted "$chain_file" \ 39 | "$cert_file" | 40 | sed "s#$cert_file#$pfx_file_basename#" 41 | } 42 | 43 | pkcs12-extract() { 44 | [[ $# -ne 1 ]] && echo "Usage: pkcs12-extract " >&2 && return 1 45 | [[ ! -f "$1" ]] && echo "Error: $1 is not a file" >&2 && return 1 46 | read -p "Please enter PFX password: " -sr pfx_pass 47 | pfx_file="$1" 48 | key_file=$( basename "$pfx_file" .pfx).key 49 | chain_file=$( basename "$pfx_file" .pfx).chain 50 | cert_file=$( basename "$pfx_file" .pfx).cert 51 | cat "$pfx_file" | pkcs12-key "$pfx_pass" > "$key_file" 52 | cat "$pfx_file" | pkcs12-chain "$pfx_pass" > "$chain_file" 53 | cat "$pfx_file" | pkcs12-cert "$pfx_pass" > "$cert_file" 54 | } 55 | 56 | pkcs12-key() { 57 | [[ $# -ne 1 ]] && echo "Usage: pkcs12-key " >&2 && return 1 58 | pass="${1:?}" 59 | if ! openssl pkcs12 $BMA_PKCS12_ARGS -nocerts -passin "pass:${pass:?}" 2>/dev/null; then 60 | echo "Error: Failed to extract key from PFX input" >&2 61 | return 1 62 | fi 63 | } 64 | 65 | pkcs12-chain() { 66 | [[ $# -ne 1 ]] && echo "Usage: pkcs12-chain " >&2 && return 1 67 | pass="${1:?}" 68 | if ! openssl pkcs12 $BMA_PKCS12_ARGS -cacerts -nokeys -passin "pass:${pass:?}" 2>/dev/null; then 69 | echo "Error: Failed to extract chain from PFX input" >&2 70 | return 1 71 | fi 72 | } 73 | 74 | pkcs12-cert() { 75 | [[ $# -ne 1 ]] && echo "Usage: pkcs12-cert " >&2 && return 1 76 | pass="${1:?}" 77 | if ! openssl pkcs12 $BMA_PKCS12_ARGS -clcerts -nokeys -passin "pass:${pass:?}" 2>/dev/null; then 78 | echo "Error: Failed to extract certificate from PFX input" >&2 79 | return 1 80 | fi 81 | } 82 | 83 | x509() { 84 | openssl x509 -text 85 | } 86 | -------------------------------------------------------------------------------- /lib/iam-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # iam-functions 4 | 5 | # [TIP] When a trusted Role is recreated, the trust is broken. 6 | # When the trust is broken, the friendly name is no longer used. 7 | # Thus, broken trust relationships can be listed with: 8 | # 9 | # `iam-roles | iam-role-principal | grep AROA` 10 | 11 | iam-roles() { 12 | 13 | # List IAM Roles 14 | # 15 | # $ iam-roles 16 | # config-role-ap-southeast-2 AROAI3QHAU3J2CDRNLQHD 2017-02-02T03:03:02Z 17 | # AWSBatchServiceRole AROAJJWRGUPTRXTV52TED 2017-03-09T05:31:39Z 18 | # ecsInstanceRole AROAJFQ3WMZXESGIKW5YD 2017-03-09T05:31:39Z 19 | 20 | local role_names=$(skim-stdin) 21 | local filters=$(__bma_read_filters $@) 22 | 23 | aws iam list-roles \ 24 | --output text \ 25 | --query " 26 | Roles[${role_names:+?contains(['${role_names// /"','"}'], RoleName)}].[ 27 | RoleName, 28 | RoleId, 29 | CreateDate 30 | ]" | 31 | grep -E -- "$filters" | 32 | LC_ALL=C sort -b -k 3 | 33 | columnise 34 | } 35 | 36 | iam-role-principal(){ 37 | 38 | # List role principal for IAM Role(s) 39 | # 40 | # USAGE: iam-role-principal role-name [role-name] 41 | 42 | local role_names=$(skim-stdin "$@") 43 | [[ -z "$role_names" ]] && __bma_usage "role-name [role-name]" && return 1 44 | 45 | aws iam list-roles \ 46 | --output text \ 47 | --query " 48 | Roles[?contains('$role_names', RoleName)].[ 49 | RoleName, 50 | AssumeRolePolicyDocument.Statement[0].Effect, 51 | AssumeRolePolicyDocument.Statement[0].Action, 52 | join('', keys(AssumeRolePolicyDocument.Statement[0].Principal)), 53 | join(',', values(AssumeRolePolicyDocument.Statement[0].Principal)[]) 54 | ]" | 55 | LC_ALL=C sort | 56 | columnise 57 | } 58 | 59 | iam-users() { 60 | 61 | # List IAM Users 62 | # 63 | # $ iam-users 64 | # config-role-ap-southeast-2 AROAI3QHAU3J2CDRNLQHD 2017-02-02T03:03:02Z 65 | # AWSBatchServiceRole AROAJJWRGUPTRXTV52TED 2017-03-09T05:31:39Z 66 | # ecsInstanceRole AROAJFQ3WMZXESGIKW5YD 2017-03-09T05:31:39Z 67 | 68 | local filters=$(__bma_read_filters $@) 69 | 70 | aws iam list-users \ 71 | --output text \ 72 | --query ' 73 | Users[].[ 74 | UserName, 75 | UserId, 76 | CreateDate 77 | ]' | 78 | grep -E -- "$filters" | 79 | LC_ALL=C sort -b -k 3 | 80 | columnise 81 | } 82 | 83 | iam-access-key-rotate() { 84 | local user_name="$1" 85 | 86 | # Check if user exists 87 | if ! aws iam get-user --user-name "$user_name" >/dev/null 2>&1; then 88 | echo "User does not exist." 89 | return 90 | fi 91 | 92 | read -p "Proceed with key rotation for user $user_name? (y/n): " confirm 93 | if [ "$confirm" != "y" ]; then 94 | echo "Key rotation cancelled." 95 | return 96 | fi 97 | 98 | # Get existing keys 99 | local keys=$(aws iam list-access-keys --user-name "$user_name") 100 | local active_keys=$(echo "$keys" | jq -r '.AccessKeyMetadata[] | select(.Status == "Active") | .AccessKeyId') 101 | local inactive_keys=$(echo "$keys" | jq -r '.AccessKeyMetadata[] | select(.Status == "Inactive") | .AccessKeyId') 102 | 103 | # Identify and remove inactive keys if exist 104 | if [ -n "$inactive_keys" ]; then 105 | IFS=$'\n' # Set Internal Field Separator to newline for the loop 106 | for key_to_remove in $inactive_keys; do 107 | read -p "Remove inactive key $key_to_remove for user $user_name? (y/n): " confirm 108 | if [ "$confirm" == "y" ]; then 109 | aws iam delete-access-key --user-name "$user_name" --access-key-id "$key_to_remove" 110 | fi 111 | done 112 | fi 113 | 114 | # Identify and remove inactive keys if exist 115 | if [ -n "$inactive_keys" ]; then 116 | while read -r key_to_remove; do 117 | read -p "Remove inactive key $key_to_remove for user $user_name? (y/n): " confirm 118 | [ "$confirm" == "y" ] && aws iam delete-access-key --user-name "$user_name" --access-key-id "$key_to_remove" 119 | done <<<"$inactive_keys" 120 | fi 121 | 122 | # If one or zero active keys, create a new key 123 | if [ $(echo "$active_keys" | wc -l) -lt 2 ]; then 124 | read -p "Create new key for user $user_name? (y/n): " confirm 125 | if [ "$confirm" == "y" ]; then 126 | local new_key=$(aws iam create-access-key --user-name "$user_name" | jq -r '.AccessKey') 127 | local access_key_id=$(echo "$new_key" | jq -r '.AccessKeyId') 128 | local secret_access_key=$(echo "$new_key" | jq -r '.SecretAccessKey') 129 | local date=$(date +%Y-%m-%d) 130 | 131 | # Create the secret in Secrets Manager 132 | aws secretsmanager create-secret \ 133 | --name "IAM-USER-$user_name-ACCESS-KEY-$access_key_id-$date" \ 134 | --description "Access Key Secret generated for IAM User Key rotation for user $user_name." \ 135 | --secret-string "{\"AccessKeyId\":\"$access_key_id\",\"SecretAccessKey\":\"$secret_access_key\"}" 136 | 137 | echo "New key created and stored in Secrets Manager." 138 | fi 139 | local secrets_name="IAM-USER-$user_name-ACCESS-KEY-$access_key_id-$date" 140 | local AWS_ACCOUNT_NAME=$(aws sts get-caller-identity --query Account --output text) 141 | 142 | echo "----------------------------------------" 143 | echo "COMMUNICATION TEMPLATE:" 144 | echo "----------------------------------------" 145 | echo "A IAM USER Access Key has been generated for the user $user_name." 146 | echo "This has been stored in Secrets Manager in the account $AWS_ACCOUNT_NAME." 147 | echo "The secret's name is $secrets_name." 148 | echo "----------------------------------------" 149 | else 150 | echo "Both keys are active. Contact the application owner to determine which key to remove." 151 | fi 152 | } 153 | -------------------------------------------------------------------------------- /lib/image-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # image-functions 4 | # 5 | # List Amazon Machine Images (AMIs) 6 | 7 | images() { 8 | 9 | # List EC2 AMI's 10 | # 11 | # Usage: images [owner] [image-id] [image-id]... 12 | # 13 | # owner defaults to `self` or can one or more of: 14 | # 15 | # - an AWS_ACCOUNT_ID (e.g. 1234567890) 16 | # - an AWS_OWNER_ALIAS (amazon, amazon-marketplace, microsoft) 17 | # 18 | # image_id can be one or more AMIs 19 | 20 | # 21 | # Trialing a different approach for grabbing resource ids from input. 22 | # As normal, you can pipe resource ids in as first token on each line. 23 | # We treat all args that don't start with ami- as owner identifiers. 24 | local inputs=$(skim-stdin "$@") 25 | local image_ids=$(echo -n "$inputs" | tr ' ' '\n' | grep ami- | tr '\n' ' ') 26 | local owners=$(echo -n "$inputs" | tr ' ' '\n' | grep -v ami- | tr '\n' ' ') 27 | 28 | # 29 | # Trialing a new pattern for output - putting the Name at the end. 30 | # This is more like the output of `ls -la` 31 | # 32 | # - Pro: Preceding fields tend to be of the same length 33 | # - Pro: Easier for eyes to scan final column for names(?) 34 | # - Con: Using this pattern for instances() would put name past 80 char point 35 | # - Con: Migrating instances() to this output is A Big Change (not made lightly) 36 | 37 | aws ec2 describe-images \ 38 | $([[ -n "$image_ids" ]] && echo --image-ids "${image_ids}") \ 39 | $([[ -n "$owners" || -z "$image_ids" ]] && echo --owners "${owners:-self}") \ 40 | --output text \ 41 | --query " 42 | sort_by(Images, &CreationDate)[].[ 43 | ImageId, 44 | CreationDate, 45 | OwnerId, 46 | Name 47 | ]" | 48 | columnise 49 | } 50 | 51 | image-deregister() { 52 | 53 | # Deregister AMI(s) 54 | # 55 | # USAGE: image-deregister image_id [image_id] 56 | 57 | local image_ids=$(skim-stdin "$@") 58 | [[ -z "${image_ids}" ]] && __bma_usage "image_id" && return 1 59 | 60 | local image_id 61 | for image_id in $image_ids; do 62 | aws ec2 deregister-image --image-id "$image_id" 63 | done 64 | } 65 | -------------------------------------------------------------------------------- /lib/keypair-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # keypair-functions 4 | # 5 | # List, create and delete EC2 SSH Keypairs 6 | 7 | keypairs() { 8 | 9 | # List EC2 SSH Keypairs in current Region 10 | # 11 | # $ keypairs 12 | # alice 8f:85:9a:1e:6c:76:29:34:37:45:de:7f:8d:f9:70:eb 13 | # bob 56:73:29:c2:ad:7b:6f:b6:f2:f3:b4:de:e4:2b:12:d4 14 | 15 | local keypairs=$(skim-stdin) 16 | local filters=$(__bma_read_filters $@) 17 | 18 | aws ec2 describe-key-pairs \ 19 | ${keypairs/#/'--key-names '} \ 20 | --query 'KeyPairs[].[KeyName, KeyFingerprint]' \ 21 | --output text | 22 | grep -E -- "$filters" | 23 | columnise 24 | } 25 | 26 | keypair-create() { 27 | 28 | # Create SSH Keypair on local machine and import public key into new EC2 Keypair. 29 | # 30 | # Provides benefits over AWS creating the keypair: 31 | # 32 | # - Amazon never has access to private key. 33 | # - Private key is protected with passphrase before being written to disk. 34 | # - Keys is written to ~/.ssh with correct file permissions. 35 | # - You control the SSH Key type (algorithm, length, etc). 36 | # 37 | # USAGE: keypair-create [key_name] [key_dir] 38 | # 39 | # $ keypair-create yet-another-keypair 40 | # Creating /home/m/.ssh/yet-another-keypair 41 | # Generating public/private rsa key pair. 42 | # Enter passphrase (empty for no passphrase): 43 | # Enter same passphrase again: 44 | # Your identification has been saved in /home/m/.ssh/yet-another-keypair. 45 | # Your public key has been saved in /home/m/.ssh/yet-another-keypair.pub. 46 | # The key fingerprint is: 47 | # SHA256:zIpbxLo7rpQvKyezOLATk96B1kSL0QP41q6x8tUrySk m@localhost.localdomain 48 | # The key's randomart image is: 49 | # +---[RSA 4096]----+ 50 | # |..o | 51 | # |.. + | 52 | # | .+.o | 53 | # | .oo.. o | 54 | # | o+. o S | 55 | # |=o.+.= . | 56 | # |+++==o+ | 57 | # |XoE+*+ . | 58 | # |o@+**+. | 59 | # +----[SHA256]-----+ 60 | # { 61 | # "KeyFingerprint": "21:82:f9:5b:79:d6:dc:0f:7b:79:43:7c:c5:34:6c:2d", 62 | # "KeyName": "yet-another-keypair" 63 | # } 64 | # 65 | # !!! Note 66 | # KeyPair Name defaults to "$(aws-account-alias)-$(region)" if none provided 67 | 68 | KEY_NAME=${1:-"$(aws-account-alias)-$(region)"} 69 | KEY_DIR=${2:-"$HOME/.ssh"} 70 | 71 | if aws ec2 describe-key-pairs --key-names "$KEY_NAME" &>/dev/null; then 72 | __bma_error "Keypair already exists: '$KEY_NAME'" 73 | return 1 74 | fi 75 | 76 | echo "Creating ${KEY_DIR}/${KEY_NAME}" 77 | ssh-keygen -t rsa -m PEM -b 4096 -o -a 100 -f "${KEY_DIR}/${KEY_NAME}" 78 | chmod 0600 "${KEY_DIR}/${KEY_NAME}" 79 | aws ec2 import-key-pair \ 80 | --key-name "$KEY_NAME" \ 81 | --public-key-material "fileb://${KEY_DIR}/${KEY_NAME}.pub" 82 | } 83 | 84 | keypair-delete() { 85 | 86 | # Delete EC2 SSH Keypairs by providing their names as arguments or via STDIN 87 | # 88 | # USAGE: keypair-delete key_name [key_name] 89 | # 90 | # $ keypair-delete alice bob 91 | # You are about to delete the following EC2 SSH KeyPairs: 92 | # alice 93 | # bob 94 | # Are you sure you want to continue? y 95 | # 96 | # $ keypairs | keypair-delete 97 | # You are about to delete the following EC2 SSH KeyPairs: 98 | # yet-another-keypair 99 | # Are you sure you want to continue? y 100 | 101 | local keypairs=$(skim-stdin "$@") 102 | [[ -z $keypairs ]] && __bma_usage "key_name [key_name]" && return 1 103 | 104 | echo "You are about to delete the following EC2 SSH KeyPairs:" 105 | echo "$keypairs" | tr ' ' "\n" | keypairs 106 | [ -t 0 ] || exec "$TMPFILE" 53 | elif [[ ! -t 0 ]]; then 54 | base64 --decode > "$TMPFILE" 55 | else 56 | __bma_usage "[ciphertext_file]" 57 | return 1 58 | fi 59 | 60 | aws kms decrypt \ 61 | --ciphertext-blob "$ciphertext" \ 62 | --output text \ 63 | --query Plaintext | base64 --decode 64 | 65 | rm -f "$TMPFILE" 66 | } 67 | 68 | kms-aliases() { 69 | 70 | # List KMS Aliases 71 | # 72 | # $ kms-aliases default 73 | # alias/default d714a175-db12-4574-8f27-aa071a1dfd8a arn:aws:kms:ap-southeast-2:089834043791:alias/default 74 | 75 | # TODO Accept alias_names from STDIN and filter in query 76 | local filters=$(__bma_read_filters $@) 77 | 78 | aws kms list-aliases \ 79 | --output text \ 80 | --query " 81 | Aliases[].[ 82 | AliasName, 83 | TargetKeyId, 84 | AliasArn 85 | ]" | 86 | grep -E -- "$filters" | 87 | columnise 88 | } 89 | 90 | kms-alias-create() { 91 | 92 | # Create alias for KMS Key 93 | # 94 | # USAGE: kms-alias-create alias_name key_id 95 | # 96 | # $ kms-keys | tail -1 97 | # d714a175-db12-4574-8f27-aa071a1dfd8a 98 | # 99 | # $ kms-keys | tail -1 | kms-alias-create alias/foobar 100 | # 101 | # $ kms-aliases foobar 102 | # alias/foobar d714a175-db12-4574-8f27-aa071a1dfd8a arn:aws:kms:ap-southeast-2:089834043791:alias/foobar 103 | 104 | local alias_name="$1" 105 | local key_id="$2" 106 | [[ -z "$alias_name" || -z "$key_id" ]] && __bma_usage "alias_name key_id" && return 1 107 | 108 | # prepend "alias/" to alias_name if missing 109 | aws kms create-alias \ 110 | --alias-name "$alias_name" \ 111 | --target-key-id "$key_id" 112 | } 113 | 114 | kms-alias-delete() { 115 | 116 | # Delete alias for KMS Key 117 | # 118 | # USAGE: kms-alias-delete alias_name [alias_name] 119 | # 120 | # $ kms-aliases foobar | kms-alias-delete 121 | # You are about to delete the following kms aliases: 122 | # alias/foobar 123 | # Are you sure you want to continue? y 124 | 125 | local alias_names=$(skim-stdin "$@") 126 | [[ -z "$alias_names" ]] && __bma_usage "alias_name [alias_name]" && return 1 127 | 128 | echo "You are about to delete the following kms aliases:" 129 | echo "$alias_names" | tr ' ' "\n" 130 | [ -t 0 ] || exec /dev/null | grep -- --table-right > /dev/null; then 19 | column_command='column --table --table-right 3,4' 20 | else 21 | column_command='column -t' 22 | fi 23 | 24 | aws lambda list-functions \ 25 | --output text \ 26 | --query " 27 | Functions[${function_names:+?contains(['${function_names// /"','"}'], FunctionName)}].[ 28 | FunctionName, 29 | LastModified, 30 | Runtime, 31 | MemorySize 32 | ]" | 33 | grep -E -- "$filters" | 34 | LC_ALL=C sort -b -k 2 | 35 | $column_command 36 | } 37 | 38 | lambda-function-memory(){ 39 | 40 | # List memorySize for lambda function(s) 41 | # 42 | # USAGE: lambda-function-memory function [function] 43 | 44 | local function_names=$(skim-stdin "$@") 45 | [[ -z "$function_names" ]] && __bma_usage "function [function]" && return 1 46 | 47 | local column_command 48 | if column --help 2>/dev/null | grep -- --table-right > /dev/null; then 49 | column_command='column --table --table-right 2' 50 | else 51 | column_command='column -t' 52 | fi 53 | 54 | local function_name 55 | for function_name in $function_names; do 56 | aws lambda get-function \ 57 | --function-name "$function_name" \ 58 | --output text \ 59 | --query 'Configuration.[FunctionName, MemorySize]' 60 | done | 61 | $column_command 62 | } 63 | 64 | 65 | lambda-function-memory-set(){ 66 | 67 | # Update memorySize for lambda function(s) 68 | # 69 | # USAGE: lambda-function-memory-set memory function [function] 70 | 71 | local memory=$1 72 | shift 1 73 | local usage_msg="memory function [function]" 74 | [[ -z "${memory}" ]] && __bma_usage "$usage_msg" && return 1 75 | 76 | local function_names=$(skim-stdin "$@") 77 | [[ -z "${function_names}" && -t 0 ]] && __bma_usage "$usage_msg" && return 1 78 | 79 | local function_name 80 | for function_name in $function_names; do 81 | aws lambda update-function-configuration \ 82 | --function-name "$function_name" \ 83 | --memory-size "$memory" \ 84 | --query '[MemorySize, FunctionName]' \ 85 | --output text 86 | done 87 | } 88 | 89 | 90 | lambda-function-memory-step(){ 91 | 92 | # Repeatedly update memorySize for lambda function(s) 93 | # 94 | # Useful for measuring impact of memory on cost/performance. 95 | # The function increases memorySize by 64KB every two minutes 96 | # until it reaches the value requested. There is a two minute 97 | # delay between increases to provide time to collect data from 98 | # function execution. 99 | # 100 | # USAGE: lambda-function-memory-step memory function [function] 101 | 102 | local sleep_period=120 103 | local memory_last=$1 104 | shift 1 105 | local usage_msg="memory function [function]" 106 | [[ -z "${memory_last}" ]] && __bma_usage $usage_msg && return 1 107 | # XXX check it's a valid memory size 108 | 109 | local function_names=$(skim-stdin "$@") 110 | [[ -z "${function_names}" && -t 0 ]] && __bma_usage $usage_msg && return 1 111 | 112 | echo "You are about to step memory for following functions:" 113 | echo "$function_names" | tr ' ' "\n" | lambda-functions 114 | [ -t 0 ] || exec /dev/null| grep -- --table-right > /dev/null; then 21 | column_command='column --table --table-right 3,4' 22 | else 23 | column_command='column -t' 24 | fi 25 | 26 | aws logs describe-log-groups \ 27 | --output text \ 28 | --query " 29 | logGroups[${log_group_names:+?contains(['${log_group_names// /"','"}'], logGroupName)}].[ 30 | logGroupName, 31 | creationTime, 32 | metricFilterCount, 33 | storedBytes 34 | ]" | 35 | grep -E -- "$filters" | 36 | LC_ALL=C sort --key 2 | 37 | $column_command 38 | } 39 | 40 | -------------------------------------------------------------------------------- /lib/rds-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2155 3 | # 4 | # rds-functions 5 | # 6 | # Amazon AWS RDS database instances & clusters 7 | 8 | # This is the second function refering to a type of `instance` 9 | # Perhaps EC2 `instance` functions should be aliased to `ec2-instances` 10 | # The original names would be deprecated but still supported for some time. 11 | 12 | rds-db-instances() { 13 | 14 | # List RDS Database Instances 15 | 16 | # Tip: Filter on whether DB is in cluster with: `awk -F'\t' '$6 != "None"'` 17 | 18 | local filters=$(__bma_read_filters "$@") 19 | 20 | aws rds describe-db-instances \ 21 | --output text \ 22 | --query "DBInstances[].[ 23 | DBInstanceIdentifier, 24 | Engine, 25 | EngineVersion, 26 | DBInstanceStatus, 27 | DBName, 28 | DBClusterIdentifier 29 | ]" | 30 | grep -E -- "$filters" | 31 | LC_ALL=C sort -b -k 1 | 32 | columnise 33 | } 34 | 35 | 36 | 37 | rds-db-clusters() { 38 | 39 | # List RDS Database Clusters 40 | 41 | local filters=$(__bma_read_filters "$@") 42 | 43 | aws rds describe-db-clusters \ 44 | --output text \ 45 | --query "DBClusters[].[ 46 | DBClusterIdentifier, 47 | Engine, 48 | EngineVersion, 49 | Status, 50 | ClusterCreateTime 51 | ]" | 52 | grep -E -- "$filters" | 53 | LC_ALL=C sort -b -k 5 | 54 | columnise 55 | } 56 | -------------------------------------------------------------------------------- /lib/region-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # region-functions 4 | 5 | # There is often confusion around the two environment variables: 6 | # 7 | # - AWS_DEFAULT_REGION 8 | # - AWS_REGION 9 | # 10 | # AWS_REGION takes precedence in AWSCLI and some other tools 11 | 12 | # Configuring environment variables for the AWS CLI 13 | # 14 | # https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html 15 | # 16 | # AWS_REGION 17 | # 18 | # The AWS SDK compatible environment variable that specifies the AWS Region to send the request to. 19 | # 20 | # If defined, this environment variable overrides the values in the environment variable AWS_DEFAULT_REGION and the profile setting region. You can override this environment variable by using the --region command line parameter. 21 | # 22 | # AWS_DEFAULT_REGION 23 | # 24 | # The Default region name identifies the AWS Region whose servers you want to 25 | # send your requests to by default. This is typically the Region closest to 26 | # you, but it can be any Region. For example, you can type us-west-2 to use US 27 | # West (Oregon). This is the Region that all later requests are sent to, unless 28 | # you specify otherwise in an individual command. 29 | # 30 | # Note: You must specify an AWS Region when using the AWS CLI, either explicitly 31 | # or by setting a default Region. For a list of the available Regions, see 32 | # Regions and Endpoints. The Region designators used by the AWS CLI are the 33 | # same names that you see in AWS Management Console URLs and service endpoints. 34 | # 35 | # If defined, this environment variable overrides the value for the profile 36 | # setting region. You can override this environment variable by using the 37 | # --region command line parameter and the AWS SDK compatible AWS_REGION 38 | # environment variable. 39 | 40 | regions() { 41 | 42 | # List regions 43 | # 44 | # The region() function must be sourced in order to update the 45 | # AWS_DEFAULT_REGION environment variable. This is because it 46 | # cannot update an environment variable when run as a subprocess. 47 | # 48 | # $ regions 49 | # ap-northeast-1 50 | # ap-northeast-2 51 | # ap-south-1 52 | # ap-southeast-1 53 | # ap-southeast-2 54 | # ... 55 | # us-west-2 56 | 57 | aws ec2 describe-regions \ 58 | --query "Regions[].[RegionName]" \ 59 | --output text | 60 | LC_ALL=C sort 61 | } 62 | 63 | 64 | region() { 65 | 66 | # Get/Set `$AWS_DEFAULT_REGION` shell environment variable 67 | # 68 | # $ region 69 | # us-east-1 70 | # 71 | # $ region ap-southeast-2 72 | # 73 | # $ region 74 | # ap-southeast-2 75 | 76 | local inputs=$(skim-stdin "$@") 77 | # XXX Check input is a valid region 78 | if [[ -z "$inputs" ]]; then 79 | echo "${AWS_DEFAULT_REGION:-'AWS_DEFAULT_REGION not set'}" 80 | else 81 | export AWS_DEFAULT_REGION="$inputs" 82 | # Update AWS_REGION only if already set. 83 | if [[ -n $AWS_REGION ]]; then 84 | export AWS_REGION="$AWS_DEFAULT_REGION" 85 | fi 86 | fi 87 | } 88 | 89 | region-each() { 90 | 91 | # Run a command in every region. 92 | # Any output lines will be appended with "#${REGION}". 93 | # 94 | # $ region-each stacks 95 | # example-ec2-ap-northeast-1 CREATE_COMPLETE 2011-05-23T15:47:44Z NEVER_UPDATED NOT_NESTED #ap-northeast-1 96 | # example-ec2-ap-northeast-2 CREATE_COMPLETE 2011-05-23T15:47:44Z NEVER_UPDATED NOT_NESTED #ap-northeast-2 97 | # ... 98 | # example-ec2-us-west-2 CREATE_COMPLETE 2011-05-23T15:47:44Z NEVER_UPDATED NOT_NESTED #us-west-2 99 | 100 | local region 101 | for region in $(regions); do 102 | ( 103 | export AWS_DEFAULT_REGION="${region}" 104 | export AWS_REGION="${region}" 105 | eval "$@" | sed "s/$/ #${region}/" 106 | ) 107 | done 108 | } 109 | 110 | -------------------------------------------------------------------------------- /lib/route53-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | hosted-zones(){ 4 | 5 | # List Route53 Hosted Zones 6 | # 7 | # $ hosted-zones 8 | # /hostedzone/Z3333333333333 5 NotPrivateZone bash-my-aws.org. 9 | # /hostedzone/Z5555555555555 2 NotPrivateZone bash-my-universe.com. 10 | # /hostedzone/Z4444444444444 3 NotPrivateZone bashmyaws.org. 11 | # /hostedzone/Z1111111111111 3 NotPrivateZone bash-my-aws.com. 12 | # /hostedzone/Z2222222222222 3 NotPrivateZone bashmyaws.com. 13 | 14 | local ids=$(skim-stdin) 15 | local filters=$(__bma_read_filters $@) 16 | 17 | aws route53 list-hosted-zones \ 18 | --output text \ 19 | --query " 20 | HostedZones[${ids:+?contains(['${ids// /"','"}'], Id)}].[ 21 | Id, 22 | ResourceRecordSetCount, 23 | (Config.PrivateZone && 'PrivateZone') || 'NotPrivateZone', 24 | Name 25 | ]" | 26 | sort -k 4 | 27 | grep -E -- "$filters" | 28 | columnise 29 | } 30 | 31 | hosted-zone-ns-records(){ 32 | 33 | # Generate NS records for delegating domain to AWS 34 | # 35 | # $ hosted-zones bash-my-aws.org 36 | # /hostedzone/ZJ6ZCG2UD6OKX 5 NotPrivateZone bash-my-aws.org. 37 | # 38 | # $ hosted-zones bash-my-aws.org | hosted-zone-ns-records 39 | # bash-my-aws.org. 300 IN NS ns-786.awsdns-34.net. 40 | # bash-my-aws.org. 300 IN NS ns-1549.awsdns-01.co.uk. 41 | # bash-my-aws.org. 300 IN NS ns-362.awsdns-45.com. 42 | # bash-my-aws.org. 300 IN NS ns-1464.awsdns-55.org. 43 | 44 | local inputs=$(skim-stdin "$@") 45 | local hosted_zone_id=$(echo "$inputs" | awk '{print $1}') 46 | [[ -z "$hosted_zone_id" ]] && __bma_usage "hosted-zone-id" && return 1 47 | 48 | local hosted_zone_name 49 | hosted_zone_name=$( 50 | aws route53 list-resource-record-sets \ 51 | --hosted-zone-id "$hosted_zone_id" \ 52 | --query "ResourceRecordSets[?Type=='NS'].Name" \ 53 | --output text 54 | ) 55 | 56 | aws route53 list-resource-record-sets \ 57 | --hosted-zone-id "$hosted_zone_id" \ 58 | --output text \ 59 | --query " 60 | ResourceRecordSets[?Type=='NS'].ResourceRecords[].[ 61 | '$hosted_zone_name 300 IN NS', 62 | Value 63 | ]" 64 | } 65 | -------------------------------------------------------------------------------- /lib/secretsmanager-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # secretsmanager-functions 4 | 5 | secrets() { 6 | aws secretsmanager list-secrets \ 7 | --output text \ 8 | --query 'SecretList[].[Name, Description, CreatedDate]' | 9 | columnise 10 | } 11 | -------------------------------------------------------------------------------- /lib/shared-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # internal-functions 4 | # 5 | # Used by bash-my-aws functions to work with stdin and arguments. 6 | 7 | 8 | skim-stdin() { 9 | 10 | # Append first token from each line of STDIN to argument list 11 | # 12 | # Implementation of `pipe-skimming` pattern. 13 | # 14 | # $ stacks | skim-stdin foo bar 15 | # foo bar huginn mastodon grafana 16 | # 17 | # $ stacks 18 | # huginn CREATE_COMPLETE 2020-01-11T06:18:46.905Z NEVER_UPDATED NOT_NESTED 19 | # mastodon CREATE_COMPLETE 2020-01-11T06:19:31.958Z NEVER_UPDATED NOT_NESTED 20 | # grafana CREATE_COMPLETE 2020-01-11T06:19:47.001Z NEVER_UPDATED NOT_NESTED 21 | # 22 | # Typical usage within Bash-my-AWS functions: 23 | # 24 | # local asg_names=$(skim-stdin "$@") # Append to arg list 25 | # local asg_names=$(skim-stdin) # Only draw from STDIN 26 | 27 | local skimmed_stdin="$([[ -t 0 ]] || awk 'ORS=" " { print $1 }')" 28 | 29 | printf -- '%s %s' "$*" "$skimmed_stdin" | 30 | awk '{$1=$1;print}' # trim leading/trailing spaces 31 | 32 | } 33 | 34 | columnise() { 35 | if [[ $BMA_COLUMNISE_ONLY_WHEN_TERMINAL_PRESENT == 'true' ]] && ! [[ -t 1 ]]; then 36 | cat 37 | else 38 | column -t -s $'\t' 39 | fi 40 | } 41 | 42 | __bma-using-aws-cli-v1() { 43 | aws --version | grep 'aws-cli/1' &>/dev/null 44 | } 45 | 46 | 47 | __bma_read_filters() { 48 | 49 | # Construct a string to be passed to `grep -E` 50 | # 51 | # $ __bma_read_filters foo bar baz 52 | # foo|bar|baz 53 | 54 | ( IFS=$'|'; printf -- "$*" ) 55 | } 56 | 57 | 58 | __bma_error() { 59 | echo "ERROR: $@" > /dev/stderr 60 | return 1 61 | } 62 | 63 | 64 | __bma_usage() { 65 | echo "USAGE: ${FUNCNAME[1]} $@" > /dev/stderr 66 | } 67 | 68 | 69 | # 70 | # The following two functions are deprecated 71 | # 72 | 73 | 74 | __bma_read_inputs() { 75 | # deprecated 76 | echo $(__bma_read_stdin) $@ | 77 | sed -E 's/\ +$//' | 78 | sed -E 's/^\ +//' 79 | } 80 | 81 | 82 | __bma_read_stdin() { 83 | # deprecated - use skim-stdin 84 | [[ -t 0 ]] || 85 | cat | 86 | awk '{ print $1 }' | 87 | tr '\n' ' ' | 88 | sed 's/\ $//' 89 | } 90 | 91 | -------------------------------------------------------------------------------- /lib/sts-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # sts-functions 4 | # 5 | # AWS Simple Token Service functions 6 | 7 | sts-assume-role() { 8 | 9 | # Assume an IAM Role 10 | # 11 | # USAGE: sts-assume-role role_arn 12 | 13 | local role_arn=$1 14 | local time_stamp=$(date +%s) 15 | [[ -z "${role_arn}" ]] && __bma_usage "role_arn" && return 1 16 | aws sts assume-role \ 17 | --role-arn "$role_arn" \ 18 | --role-session-name "$time_stamp" \ 19 | --duration-seconds 900 \ 20 | --output text \ 21 | --query 'Credentials.[ 22 | [join(`=`, [`AWS_ACCESS_KEY_ID`, AccessKeyId])], 23 | [join(`=`, [`AWS_SECRET_ACCESS_KEY`, SecretAccessKey])], 24 | [join(`=`, [`AWS_SESSION_TOKEN`, SessionToken])], 25 | [join(`=`, [`AWS_SECURITY_TOKEN`, SessionToken])] 26 | ]' 27 | } 28 | -------------------------------------------------------------------------------- /lib/tag-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # tag-functions 4 | 5 | tag-keys() { 6 | 7 | # List unique set of tag keys in AWS Account / Region 8 | # 9 | # USAGE: tag-keys 10 | 11 | aws resourcegroupstaggingapi get-tag-keys \ 12 | --output text \ 13 | --query 'TagKeys[].[@]' | 14 | sort 15 | } 16 | 17 | tag-values() { 18 | 19 | # List unique set of tag values for key in AWS Account / Region 20 | # 21 | # USAGE: tag-values key 22 | 23 | local key="${1:-}" 24 | [[ -z $key ]] && __bma_usage "key" && return 1 25 | aws resourcegroupstaggingapi get-tag-values \ 26 | --key "$key" \ 27 | --output text \ 28 | --query "TagValues[].['$key', @]" | 29 | sort 30 | } 31 | 32 | tag-split() { 33 | # Split AWS resource tags into one tag per line 34 | # Usage: bma instances | tail -1 | bma instance-tags | tag-split 35 | sed 's/ \([^= ]*=\)/\n\1/g' 36 | } 37 | -------------------------------------------------------------------------------- /lib/target-group-functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # target-group-functions 4 | # 5 | # Load balancer target groups 6 | 7 | target-groups() { 8 | 9 | # List EC2 ELBv2 target groups 10 | # 11 | # $ target-groups 12 | # bash-my-aws-nlb-tg TCP 22 vpc-04636ebe5573f6f65 instance bash-my-aws-nlb 13 | # bash-my-aws-alb-tg HTTP 443 vpc-04636ebe5573f6f65 instance bash-my-aws-alb 14 | 15 | local tg_names=$(skim-stdin) 16 | local filters=$(__bma_read_filters $@) 17 | 18 | aws elbv2 describe-target-groups \ 19 | ${tg_names/#/'--names '} \ 20 | --output text \ 21 | --query " 22 | TargetGroups[][ 23 | TargetGroupName, 24 | Protocol, 25 | Port, 26 | VpcId, 27 | TargetType, 28 | join(' ', LoadBalancerArns[]) 29 | ]" | 30 | sed 's,arn:[^/]*:loadbalancer/[^/]*/\([^/]*\)[^[:blank:]]*,\1,g' | 31 | grep -E -- "$filters" | 32 | LC_ALL=C sort -b | 33 | columnise 34 | } 35 | 36 | target-group-targets() { 37 | 38 | # List EC2 ELBv2 target group targets 39 | # Accepts Target Group names on stdin or as arguments 40 | # 41 | # $ target-group-targets bash-my-aws-nlb-tg 42 | # i-4e15ece1de1a3f869 443 healthy bash-my-aws-nlb-tg 43 | # i-89cefa9403373d7a5 443 unhealthy bash-my-aws-nlb-tg 44 | 45 | local tg_names=$(skim-stdin "$@") 46 | 47 | [[ -z $tg_names ]] && __bma_usage "tg-name [tg-name]" && return 1 48 | 49 | for tg_name in $tg_names; do 50 | local tg_arn=$(aws elbv2 describe-target-groups \ 51 | --names "$tg_name" \ 52 | --output text \ 53 | --query " 54 | TargetGroups[][ 55 | TargetGroupArn 56 | ] 57 | ") 58 | aws elbv2 describe-target-health \ 59 | --target-group-arn "$tg_arn" \ 60 | --output text \ 61 | --query " 62 | TargetHealthDescriptions[][ 63 | Target.Id, 64 | Target.Port, 65 | TargetHealth.State, 66 | TargetHealth.Description, 67 | '${tg_name}' 68 | ] 69 | " 70 | done | columnise 71 | } 72 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'Bash-my-AWS' 2 | site_description: 3 | Bash-my-AWS is a simple but powerful set of CLI commands for managing 4 | resources on Amazon Web Services. They harness the power of Amazon's AWSCLI, 5 | while abstracting away verbosity. 6 | site_author: 'Mike Bailey' 7 | site_url: 'https://bash-my-aws.org/' 8 | google_analytics: ['UA-154550647-1', 'bash-my-aws.org'] 9 | 10 | nav: 11 | - Home: index.md 12 | # - Tour: tour.md 13 | - Command Reference: command-reference.md 14 | - cachews (caching for awscli): cachews-caching-for-awscli.md 15 | - Pipe-Skimming: pipe-skimming.md 16 | - CloudFormation Naming: cloudformation-naming.md 17 | - LCA 2020 Video: linuxconf2020.md 18 | 19 | theme: 20 | name: material 21 | repo_name: 'bash-my-aws/bash-my-aws' 22 | repo_url: 'https://github.com/bash-my-aws/bash-my-aws' 23 | # extra: 24 | # social: 25 | # - type: 'github' 26 | # link: 'https://github.com/bash-my-aws' 27 | markdown_extensions: 28 | - admonition 29 | - codehilite: 30 | guess_lang: false 31 | - toc: 32 | permalink: true 33 | - meta 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | project_root="$(cd "$(dirname "$0")/.." && pwd)" 4 | aliases_destination="$project_root/aliases" 5 | funcs_destination="$project_root/functions" 6 | completion_destination="$project_root/bash_completion.sh" 7 | 8 | # Generate a file with all BMA function names (used in bash completion) 9 | 10 | # functions in shell before loading BMA functions 11 | funcs_before_bma=$(compgen -A function) 12 | 13 | # load all the functions from bash-my-aws 14 | for f in $project_root/lib/*-functions; do source "$f"; done 15 | 16 | # all function after loading BMA functions 17 | funcs_after_bma=$(compgen -A function) 18 | 19 | # Generate the functions file 20 | 21 | { 22 | echo "# DO NOT MANUALLY MODIFY THIS FILE." 23 | echo "# Use 'scripts/build' to regenerate if required." 24 | echo "" 25 | echo "${funcs_before_bma}" "${funcs_after_bma}" | 26 | tr ' ' '\n' | 27 | awk 'NF' | 28 | LC_ALL=C sort | 29 | uniq -u 30 | } > "$funcs_destination" 31 | 32 | 33 | # Generate the aliases file 34 | { 35 | echo "# DO NOT MANUALLY MODIFY THIS FILE." 36 | echo "# Use 'scripts/build' to regenerate if required." 37 | echo "" 38 | } > "$aliases_destination" 39 | 40 | # Don't create alias for these functions 41 | exclusions=('region') 42 | 43 | for fnc in $(echo "${funcs_before_bma}" "${funcs_after_bma}" "${exclusions}" | tr ' ' '\n' | LC_ALL=C sort | uniq -u); do 44 | echo "alias $fnc='\${BMA_HOME:-\$HOME/.bash-my-aws}/bin/bma $fnc'" >> "$aliases_destination" 45 | done; 46 | 47 | 48 | # functions to clone 49 | fncs_to_clone=('region') 50 | 51 | { 52 | echo 53 | echo "# We'll find a less suprising place for this in future" 54 | echo "# region() needs to be a function in order to let it" 55 | echo "# set AWS_DEFAULT_REGION in the current shell" 56 | } >> "$aliases_destination" 57 | 58 | for fnc_name in $fncs_to_clone; do 59 | function_body=$(type "$fnc_name" | tail -n +3) 60 | printf "function %s() %s" "$fnc_name" "$function_body" >> "$aliases_destination" 61 | done; 62 | 63 | echo "" >> "$aliases_destination" 64 | 65 | ${project_root}/scripts/build-completions > "$completion_destination" 66 | 67 | ${project_root}/scripts/build-docs 68 | 69 | 70 | -------------------------------------------------------------------------------- /scripts/build-completions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bma_path="$(cd "$(dirname "$0")/.." && pwd)" 4 | 5 | cat <&2 "Usage: $0 filename(s)" && exit 1 6 | 7 | for x in $@; do 8 | 9 | echo 10 | echo 11 | basename "$x" | sed -E 's/(.*)-functions/## \1-commands/g' 12 | 13 | cat "$x" | 14 | grep '(){\|() {\|^ #' | 15 | grep -v 'shellcheck\|TODO\|XXX' | 16 | grep -v '_bma' | 17 | sed -E 's/\(\) ?\{//g' | 18 | sed -E 's/^([^ ]+)/\n\n### \1\n/g' | 19 | sed -E 's/^ #( )?//g' 20 | 21 | done 22 | 23 | # grep '()\|^ #' | 24 | -------------------------------------------------------------------------------- /scripts/localstack-stack-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bma stack-create nagios cloudformation/ec2.yml 4 | bma stack-create postgres01 cloudformation/ec2.yml 5 | bma stack-create postgres02 cloudformation/ec2.yml 6 | bma stack-create prometheus-web cloudformation/ec2.yml 7 | -------------------------------------------------------------------------------- /scripts/localstack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # [localstack services/ports](https://github.com/localstack/localstack#overview) 4 | 5 | DEBUG=1 SERVICES="cloudformation,ec2,iam,KMS,kms,lambda,route53,sts,s3" localstack start --host & 6 | 7 | # Set aws-account-alias 8 | awslocal iam create-account-alias --account-alias demo-account 9 | -------------------------------------------------------------------------------- /test/bash-spec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #================================================================================== 3 | # BDD-style testing framework for bash scripts. 4 | # 5 | # expect variable [not] to_be value Compare scalar values for equality 6 | # expect variable [not] to_match regex Regex match 7 | # expect array [not] to_contain value Look for a value in an array 8 | # expect filename [not] to_exist Verify file existence 9 | # expect filename [not] to_have_mode modestring Verify file mode (permissions) 10 | # expect [not] to_be_true condition Verify exit mode as boolean 11 | # 12 | # Author: Dave Nicolette 13 | # Date: 29 Jul 2014 14 | # Modified by REA Group 2014 15 | #================================================================================== 16 | 17 | # XXX: should use mktemp for proper random file name -- (GM) 18 | result_file="$RANDOM" 19 | _passed_=0 20 | _failed_=0 21 | 22 | exec 6<&1 23 | exec > "$result_file" 24 | 25 | function output_results { 26 | exec 1>&6 6>&- 27 | local results="$(<$result_file)" 28 | rm -f -- "$result_file" 29 | local passes=$(printf '%s' "$results" | grep -F PASS | wc -l) 30 | local fails=$(printf '%s' "$results" | grep -F '**** FAIL' | wc -l ) 31 | printf '%s\n--SUMMARY\n%d PASSED\n%d FAILED\n' "$results" "$passes" "$fails" 32 | [[ ${fails:-1} -eq 0 ]] 33 | exit $? 34 | } 35 | 36 | function _array_contains_ { 37 | for elem in "${_actual_[@]}"; do 38 | [[ "$elem" == "$_expected_" ]] && return 0 39 | done 40 | return 1 41 | } 42 | 43 | function _negation_check_ { 44 | if [[ "$_negation_" == true ]]; then 45 | if [[ "$_pass_" == true ]]; then 46 | _pass_=false 47 | else 48 | _pass_=true 49 | fi 50 | fi 51 | if [[ "$_pass_" == true ]]; then 52 | (( _passed_+=1 )) 53 | pass 54 | else 55 | (( _failed_+=1 )) 56 | fail 57 | fi 58 | } 59 | 60 | function it { 61 | printf ' %s\n %s\n' "$1" "$2" 62 | } 63 | 64 | function describe { 65 | printf '%s\n%s\n' "$1" "$2" 66 | } 67 | 68 | function context { 69 | printf '%s\n%s\n' "$1" "$2" 70 | } 71 | 72 | function pass { 73 | echo " PASS" 74 | } 75 | 76 | function fail { 77 | echo "**** FAIL - expected:$( if [[ "$_negation_" == true ]]; then echo ' NOT'; fi; ) '$_expected_' | actual: '${_actual_[@]}'" 78 | } 79 | 80 | function expect { 81 | _expected_= 82 | _negation_=false 83 | declare -a _actual_ 84 | until [[ "${1:0:3}" == to_ || "$1" == not || -z "$1" ]]; do 85 | _actual_+=("$1") 86 | shift 87 | done 88 | "$@" 89 | } 90 | 91 | function not { 92 | _negation_=true 93 | "$@" 94 | } 95 | 96 | function to_be { 97 | _expected_="$1" 98 | _pass_=false 99 | [[ "${_actual_[0]}" == "$_expected_" ]] && _pass_=true 100 | _negation_check_ 101 | } 102 | 103 | function to_be_true { 104 | _expected_="$@ IS TRUE" 105 | _pass=false 106 | _actual_="$@ IS FALSE" 107 | if "$@"; then 108 | _pass_=true 109 | _actual_="$@ IS TRUE" 110 | fi 111 | _negation_check_ 112 | } 113 | 114 | function to_match { 115 | _expected_="$1" 116 | _pass_=false 117 | [[ "${_actual_[0]}" =~ $_expected_ ]] && _pass_=true 118 | _negation_check_ 119 | } 120 | 121 | function to_contain { 122 | _expected_="$1" 123 | _pass_=false 124 | _array_contains_ "$_expected_" "$_actual_" && _pass_=true 125 | _negation_check_ 126 | } 127 | 128 | function to_exist { 129 | _pass_=false 130 | _expected_="$_actual_ EXISTS" 131 | if [[ -e "${_actual_[0]}" ]]; then 132 | _pass_=true 133 | [[ "$_negation_" == true ]] && _expected_="$_actual_ EXISTS" 134 | else 135 | _actual_="File not found" 136 | fi 137 | _negation_check_ 138 | } 139 | 140 | function to_have_mode { 141 | _filename_="${_actual_[0]}" 142 | _expected_="$1" 143 | _pass_=false 144 | if [[ -e "$_filename_" ]]; then 145 | _fullname_="$_filename_" 146 | else 147 | _fullname_="$(which $_filename_)" 148 | fi 149 | if [[ -e "$_fullname_" ]]; then 150 | _os_="$(uname -s)" 151 | if [[ "$_os_" == Linux ]]; then 152 | _actual_="$(stat -c %A $_fullname_)" 153 | else 154 | _actual_="$(stat $_fullname_ | cut -f 3 -d ' ')" 155 | fi 156 | [[ "$_actual_" =~ "$_expected_" ]] && _pass_=true 157 | else 158 | echo "File not found: $_fullname_" 159 | fi 160 | _negation_check_ 161 | } 162 | 163 | TEMP="$(getopt -o h --long help \ 164 | -n 'javawrap' -- $@)" 165 | 166 | if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi 167 | 168 | eval set -- "$TEMP" 169 | 170 | while true; do 171 | case "$1" in 172 | -h | --help ) show_help; exit 0 ;; 173 | -- ) shift; break ;; 174 | * ) break ;; 175 | esac 176 | done 177 | 178 | trap output_results EXIT 179 | -------------------------------------------------------------------------------- /test/check-completions: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bma_path=$(cd $(dirname $0)/.. && pwd) 4 | 5 | # function in bma 6 | bma_funcs=$( 7 | cat "${bma_path}/functions" \ 8 | | grep -vE "^#" \ 9 | | grep -vE "^__bma" \ 10 | | grep -vE "^_bma" 11 | ) 12 | 13 | completions=$( 14 | cat "${bma_path}/bash_completion.sh" \ 15 | | grep -E "^complete -F" \ 16 | | awk '{print $4}' 17 | ) 18 | 19 | for func in $bma_funcs;do 20 | if [[ "${completions}" =~ "${func}" ]];then 21 | printf "\033[32m\xE2\x9C\x94 %s\033[0m\n" "${func}" 22 | else 23 | printf "\033[31m\xE2\x9D\x8C %s\033[0m\n" "${func}" 24 | fi 25 | done 26 | 27 | echo "Note that some functions should not have bash completions." 28 | echo "These are generally functions that require use to enter a" 29 | echo "value (such as desired number of instances) as first arg." 30 | -------------------------------------------------------------------------------- /test/commands: -------------------------------------------------------------------------------- 1 | cmd __bma_error 2 | cmd __bma_read_inputs 3 | cmd __bma_read_stdin 4 | cmd __bma_usage 5 | cmd _stack_capabilities 6 | cmd _stack_diff_params 7 | cmd _stack_diff_template 8 | cmd _stack_name_arg 9 | cmd _stack_params_arg 10 | cmd _stack_template_arg 11 | cmd asg-capacity 12 | cmd asg-capacity my-auto-scaling-group 13 | cmd asg-desired-size-set 14 | cmd asg-desired-size-set my-auto-scaling-group 123 15 | cmd asg-desired-size-set my-auto-scaling-group 123a 16 | cmd asg-desired-size-set my-auto-scaling-group a123 17 | cmd asg-instances 18 | cmd asg-instances my-auto-scaling-group 19 | cmd asg-max-size-set 20 | cmd asg-max-size-set my-auto-scaling-group 21 | cmd asg-max-size-set my-auto-scaling-group 123 22 | cmd asg-max-size-set my-auto-scaling-group 123a 23 | cmd asg-max-size-set my-auto-scaling-group a123 24 | cmd asg-max-size-set my-auto-scaling-group abc 25 | cmd asg-min-size-set 26 | cmd asg-min-size-set my-auto-scaling-group 27 | cmd asg-min-size-set my-auto-scaling-group 123 28 | cmd asg-min-size-set my-auto-scaling-group 123a 29 | cmd asg-min-size-set my-auto-scaling-group a123 30 | cmd asg-min-size-set my-auto-scaling-group abc 31 | cmd asg-processes_suspended 32 | cmd asg-processes_suspended my-auto-scaling-group 33 | cmd asg-resume 34 | cmd asg-resume my-auto-scaling-group 35 | cmd asg-scaling-activities 36 | cmd asg-scaling-activities my-auto-scaling-group 37 | cmd asg-stack 38 | cmd asg-stack asg-name1 asg-name2 39 | cmd asg-suspend 40 | cmd asg-suspend my-auto-scaling-group 41 | cmd asgs 42 | cmd aws-account-alias 43 | cmd bucket-acls 44 | cmd bucket-acls bucket1 bucket2 45 | cmd buckets 46 | cmd cloudtrails 47 | cmd elb-dnsname 48 | cmd elb-dnsname my-load-balancer 49 | cmd elb-instances 50 | cmd elb-instances my-load-balancer 51 | cmd elbs 52 | cmd instance-asg 53 | cmd instance-asg i-11111111 i-22222222 54 | cmd instance-az 55 | cmd instance-az i-11111111 i-22222222 56 | cmd instance-console 57 | cmd instance-console i-11111111 i-22222222 58 | cmd instance-dns 59 | cmd instance-dns i-11111111 i-22222222 60 | cmd instance-iam-profile 61 | cmd instance-iam-profile i-11111111 i-22222222 62 | cmd instance-ip 63 | cmd instance-ip i-11111111 i-22222222 64 | cmd instance-ssh 65 | cmd instance-ssh-details 66 | cmd instance-ssh-details i-11111111 67 | cmd instance-ssm 68 | cmd instance-stack 69 | cmd instance-stack i-11111111 i-22222222 70 | cmd instance-start 71 | cmd instance-start i-11111111 i-22222222 72 | cmd instance-state 73 | cmd instance-state i-11111111 i-22222222 74 | cmd instance-stop 75 | cmd instance-stop i-11111111 i-22222222 76 | cmd instance-tags 77 | cmd instance-tags i-11111111 i-22222222 78 | cmd instance-terminate 79 | cmd instance-terminate i-11111111 i-22222222 80 | cmd instance-type 81 | cmd instance-type i-11111111 i-22222222 82 | cmd instance-userdata 83 | cmd instance-userdata i-11111111 i-22222222 84 | cmd instance-volumes 85 | cmd instance-volumes i-11111111 i-22222222 86 | cmd instance-vpc 87 | cmd instance-vpc i-11111111 i-22222222 88 | cmd instances 89 | cmd region 90 | cmd region-each 91 | cmd regions 92 | cmd stack-asg-instances 93 | cmd stack-asg-instances my-stack 94 | cmd stack-asgs 95 | cmd stack-cancel-update 96 | cmd stack-cancel-update my-stack 97 | cmd stack-create 98 | cmd stack-create my-stack template-file parameters-file --capabilities=OPTIONAL_VALUE --role-arn=OPTIONAL_VALUE 99 | cmd stack-delete 100 | cmd stack-delete my-stack 101 | cmd stack-diff 102 | cmd stack-diff stack template-file 103 | cmd stack-elbs 104 | cmd stack-events 105 | cmd stack-exports 106 | cmd stack-failure 107 | cmd stack-failure my-stack 108 | cmd stack-instances 109 | cmd stack-outputs 110 | cmd stack-outputs my-stack 111 | cmd stack-parameters 112 | cmd stack-parameters my-stack 113 | cmd stack-recreate 114 | cmd stack-recreate my-stack 115 | cmd stack-resources 116 | cmd stack-resources my-stack 117 | cmd stack-status 118 | cmd stack-status my-stack 119 | cmd stack-tags 120 | cmd stack-tags my-stack 121 | cmd stack-tail 122 | cmd stack-tail my-stack 123 | cmd stack-template 124 | cmd stack-update 125 | cmd stack-update my-stack template-file parameters-file 126 | cmd stack-validate 127 | cmd stack-validate template-file 128 | cmd stacks 129 | cmd sts-assume-role 130 | cmd sts-assume-role role_arn 131 | cmd vpc-default-delete 132 | cmd vpc-dhcp-options-ntp 133 | cmd vpc-igw 134 | cmd vpc-igw vpc-11111111 vpc-22222222 135 | cmd vpc-lambda-functions 136 | cmd vpc-lambda-functions vpc-11111111 vpc-22222222 137 | cmd vpc-nat-gateways 138 | cmd vpc-nat-gateways vpc-11111111 vpc-22222222 139 | cmd vpc-network-acls 140 | cmd vpc-network-acls vpc-11111111 vpc-22222222 141 | cmd vpc-rds 142 | cmd vpc-rds vpc-11111111 vpc-22222222 143 | cmd vpc-route-tables 144 | cmd vpc-route-tables vpc-11111111 vpc-22222222 145 | cmd vpc-subnets 146 | cmd vpc-subnets vpc-11111111 vpc-22222222 147 | cmd vpcs 148 | -------------------------------------------------------------------------------- /test/exercise-functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for f in ${BMA_HOME:-$HOME/.bash-my-aws}/lib/*-functions; do source $f; done 4 | source "${BMA_HOME:-$HOME/.bash-my-aws}"/bin/bma 5 | 6 | # init function override for aws 7 | aws() { 8 | echo "aws $@" 9 | } 10 | 11 | # override to make output prettier 12 | base64() { 13 | cat 14 | } 15 | 16 | # Don't run this one 17 | stack-tail() { 18 | : 19 | } 20 | 21 | cmd() { 22 | echo "# Command: $@" 23 | $@ 24 | echo 25 | } 26 | 27 | source commands 28 | -------------------------------------------------------------------------------- /test/shared-spec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(dirname $0)/bash-spec.sh 3 | source $(dirname $0)/../lib/shared-functions 4 | 5 | describe "bma_usage:" "$( 6 | context "with a string" "$( 7 | expect "$(__bma_usage "something" 2>&1)" to_be "USAGE: main something" 8 | )" 9 | )" 10 | 11 | describe "bma_read_stdin:" "$( 12 | context "single word on a single line" "$( 13 | expect "$(echo "a" | __bma_read_stdin)" to_be "a" 14 | )" 15 | 16 | context "multi word on a single line" "$( 17 | expect "$(echo "a blah" | __bma_read_stdin)" to_be "a" 18 | )" 19 | 20 | context "single word on multi line" "$( 21 | expect "$(printf "a\nb" | __bma_read_stdin)" to_be "a b" 22 | )" 23 | 24 | context "multi word on a single line" "$( 25 | expect "$(printf "a blah\nb else\n" | __bma_read_stdin)" to_be "a b" 26 | )" 27 | )" 28 | 29 | describe "bma_read_inputs:" "$( 30 | context "empty" "$( 31 | val=$(__bma_read_stdin) 32 | expect "${val:-empty}" to_be "empty" 33 | )" 34 | 35 | context "with stdin" "$( 36 | val=$(echo "a blah" | __bma_read_inputs) 37 | expect "${val:-empty}" to_be "a" 38 | )" 39 | 40 | context "with argv" "$( 41 | val=$(__bma_read_inputs "argv") 42 | expect "${val:-empty}" to_be "argv" 43 | )" 44 | 45 | context "multi word on a single line" "$( 46 | val=$(printf "a blah\nb else\n" | __bma_read_inputs) 47 | expect "${val:-empty}" to_be "a b" 48 | )" 49 | )" 50 | -------------------------------------------------------------------------------- /test/stack-spec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(dirname $0)/bash-spec.sh 3 | source $(dirname $0)/../lib/stack-functions 4 | source $(dirname $0)/../lib/shared-functions 5 | 6 | describe "_bma_stack_name_arg:" "$( 7 | context "without an argument" "$( 8 | expect $(_bma_stack_name_arg) to_be "" 9 | )" 10 | 11 | context "with a string" "$( 12 | expect "$(_bma_stack_name_arg "argument")" to_be "argument" 13 | )" 14 | 15 | context "with a file extension" "$( 16 | expect "$(_bma_stack_name_arg "file.json")" to_be "file" 17 | )" 18 | 19 | context "with a full json path" "$( 20 | expect "$(_bma_stack_name_arg "/path/to/file.json")" to_be "file" 21 | )" 22 | 23 | context "with a yaml file" "$( 24 | expect "$(_bma_stack_name_arg "file.yaml")" to_be "file" 25 | )" 26 | 27 | context "with a yml file" "$( 28 | expect "$(_bma_stack_name_arg "file.yml")" to_be "file" 29 | )" 30 | 31 | context "with a full yaml path" "$( 32 | expect "$(_bma_stack_name_arg "/path/to/file.yaml")" to_be "file" 33 | )" 34 | 35 | context "with a full xml path" "$( 36 | expect "$(_bma_stack_name_arg "/path/to/file.xml")" to_be "file" 37 | )" 38 | )" 39 | 40 | describe "_bma_stack_template_arg:" "$( 41 | context "cannot find template without any details" "$( 42 | expect $(_bma_stack_template_arg) to_be "" 43 | )" 44 | 45 | context "cannot find template with only stack name" "$( 46 | expect $(_bma_stack_template_arg "stack") to_be "" 47 | )" 48 | 49 | context "cannot find template when it's gone" "$( 50 | expect $(_bma_stack_template_arg "stack" /file/is/gone) to_be "/file/is/gone" 51 | )" 52 | 53 | context "can find template when it exists" "$( 54 | cd ${TMPDIR} 55 | touch stack.json 56 | expect $(_bma_stack_template_arg "stack") to_be "stack.json" 57 | rm stack.json 58 | )" 59 | 60 | context "can find template when stack is hyphenated and it exists" "$( 61 | cd ${TMPDIR} 62 | touch stack.json 63 | expect $(_bma_stack_template_arg "stack-example") to_be "stack.json" 64 | rm stack.json 65 | )" 66 | 67 | context "can find template when it is provided" "$( 68 | tmpfile=$(mktemp -t bma.XXX) 69 | expect $(_bma_stack_template_arg "stack" "${tmpfile}") to_be "${tmpfile}" 70 | rm ${tmpfile} 71 | )" 72 | 73 | )" 74 | 75 | [[ -d cloudformation/params ]] || mkdir -p cloudformation/params 76 | 77 | 78 | # templates 79 | touch \ 80 | $(dirname $0)/cloudformation/great-app.json \ 81 | $(dirname $0)/cloudformation/great-app.yml \ 82 | $(dirname $0)/cloudformation/great-app.yaml \ 83 | 84 | # params 85 | 86 | [[ -d params ]] || mkdir params 87 | 88 | touch \ 89 | $(dirname $0)/cloudformation/great-app-params.json \ 90 | $(dirname $0)/cloudformation/great-app-params-staging.json \ 91 | $(dirname $0)/cloudformation/great-app-params-another-env.json \ 92 | $(dirname $0)/cloudformation/params/great-app-params.json \ 93 | $(dirname $0)/cloudformation/params/great-app-params-staging.json \ 94 | $(dirname $0)/cloudformation/params/great-app-params-another-env.json 95 | 96 | cd $(dirname $0)/cloudformation 97 | 98 | describe "_bma_stack_args:" "$( 99 | context "without an argument" "$( 100 | expect $(_bma_stack_args) to_be "" 101 | )" 102 | 103 | context "with a stack" "$( 104 | expect "$(_bma_stack_args great-app)" to_be "Resolved arguments: great-app ./great-app.json " 105 | )" 106 | 107 | context "with a template" "$( 108 | expect "$(_bma_stack_args great-app.yaml)" to_be "Resolved arguments: great-app great-app.yaml ./great-app-params.json" 109 | )" 110 | 111 | context "with a params file" "$( 112 | expect "$(_bma_stack_args params/great-app-params-staging.json)" to_be "Resolved arguments: great-app-staging ./great-app.json params/great-app-params-staging.json" 113 | )" 114 | 115 | )" 116 | 117 | cd - 118 | --------------------------------------------------------------------------------