Compare commits

..

71 commits

Author SHA1 Message Date
Conor McManus
225b116df0 Remove workspace var. can be replaced with internal variable terraform.workspace 2023-03-27 14:23:50 +02:00
Conor McManus
c97850e916 Remove shared creds file 2023-01-24 17:14:05 +01:00
Conor McManus
9491a890ab Fix issue with -e flag not generating file correctly 2023-01-24 16:50:12 +01:00
Conor McManus
4829fe8f54 Merge branch 'master' of gitlab.com:spengreb/atmos 2023-01-24 15:06:56 +01:00
Conor McManus
08d6f222bf Fix latest docker image not working 2023-01-24 15:06:49 +01:00
spengreb
590fd10b60 Merge branch 'fix-env-flag-overriding-creds-file' into 'master'
Fix check on env vars

See merge request spengreb/atmos!4
2023-01-24 13:46:54 +00:00
Conor McManus
57045656b3 Fix check on env vars 2023-01-24 14:44:49 +01:00
spengreb
473755b7e7 Will push latest docker version as well as terraform version 2022-12-01 16:11:00 +01:00
spengreb
aa45175a63 Will push latest docker version as well as terraform version 2022-12-01 16:02:14 +01:00
spengreb
53b0678d02 Merge branch 'add-ci-cd' into 'master'
Add ci cd

See merge request spengreb/atmos!3
2022-12-01 14:51:27 +00:00
spengreb
b130a75f87 Add ci cd 2022-12-01 14:51:27 +00:00
spengreb
e47b0cd446 docker will always get latest terraform version 2022-12-01 15:07:28 +01:00
Spengreb
a8a6bbfc30 Update tf version to 12.24 2020-09-24 15:47:45 +02:00
Conor
624181c0e6 more fixes for -e flag 2020-01-24 16:28:43 +01:00
Conor
ddcccde8f1 -e creds file fixes in docker 2020-01-24 16:23:36 +01:00
Conor
402db3a03f Dockerfile changes 2020-01-24 15:43:23 +01:00
Conor
a15e924235 remove shared creds as its not working 2020-01-24 15:33:33 +01:00
Conor
b11f87e709 Add check for -e flag for overriding aws creds file 2020-01-24 15:20:29 +01:00
Conor
b7f72bb67b Fix docker version 2020-01-24 14:12:49 +01:00
Conor
8e693dcbbb Update docker version number 2020-01-24 13:43:26 +01:00
Conor
9f4f6d0b89 Update tf version 2020-01-24 13:37:53 +01:00
conor
0b3f3068e4 Fix closing bracket 2019-11-13 12:04:41 +01:00
conor
d3f44bb7ec -e flag now uses default aws creds file location. This is because of issues around allowing the terraform backend chunk finding creds in a non-default location 2019-11-13 11:58:37 +01:00
conor
b815e3b526 Fixes for workspaces not found in credentials.py 2019-11-13 11:37:50 +01:00
conor
a72d1c1c01 Add exclusion to adding project prefix to default workspace 2019-11-13 11:34:51 +01:00
conor
53175c47a6 Update terraform version 2019-11-05 10:11:29 +01:00
Spengreb
08e8b90aee
Merge pull request #2 from simonArnold/separation-and-basic-integration-tests
Rough separation of modules and added top level tests to main logic method
2019-10-30 11:21:55 +01:00
Simon Arnold
ea9bc944c9 Rough separation of modules and added top level tests to main logic method 2019-10-29 21:32:35 +01:00
spengreb
052384ceec Revert back to replacing default credentials due to issues with getting the correct statefile 2019-08-30 16:22:52 +02:00
spengreb
41a422e0bc Add exception for env vars to use _ 2019-08-30 15:35:19 +02:00
spengreb
9fac2103c5 Change logic for checking if dir is a git dir. 2019-08-30 13:40:14 +02:00
Spengreb
d5062e63e9
Update README.md 2019-08-29 14:32:01 +02:00
conor
a7e03157b3 Change default workspace to default instead of qa 2019-08-19 15:39:47 +02:00
conor
58785c2c46 Fix for using _ vs - 2019-08-15 17:32:45 +02:00
conor
d5fb22ea8a add git http creds helper 2019-08-14 12:49:55 +02:00
conor
9664581b71 Add version tag 2019-08-06 13:36:18 +02:00
conor
b810b600dd Merged 2019-08-06 13:31:42 +02:00
conor
fdd14d9250 Update tf version 2019-08-06 13:27:59 +02:00
conor
126099b363 -p flag now works with creds file 2019-08-01 11:01:45 +02:00
conor
250f1b83d6 A little error handling 2019-07-29 15:01:15 +02:00
spengreb
85b1113280 Update readme 2019-07-26 11:58:27 +02:00
Spengreb
edca76ac24
Merge pull request #1 from Spengreb/release/1.1
Release/1.1
2019-07-17 10:40:56 +02:00
conor
b6d8d623df Add verbose mode and flag for project prefix in env vars 2019-07-17 10:15:21 +02:00
conor
8bff7443d5 Changed -p flag to be -n for neutralize for stoppping atmos from doing var appending 2019-07-17 09:34:14 +02:00
conor
dc5ba48990 Atmos will use its own credentials file 2019-07-16 17:12:34 +02:00
conor
6bcd68e410 Update -p flag description to be accurate 2019-07-16 13:09:32 +02:00
conor
e3f5688ee4 Flip the order of where tf commands happen. This appears to be more stable behaviour 2019-06-12 15:55:01 +02:00
spengreb
b9d3f5ca56 Merge branch 'master' of github.com:Spengreb/atmos 2019-06-12 11:58:40 +02:00
spengreb
9ebf7fbd35 Upgrade to terraform 12.1 2019-06-12 11:58:25 +02:00
conor
f12fd7b2f3 Master branch will try to get default.tfvars 2019-06-11 13:11:50 +02:00
Conor.McManus
c6880e40a7 Merge branch 'master' of github.com:Spengreb/atmos 2019-06-07 10:17:28 +02:00
Conor.McManus
38661f8f07 Add emoji 2019-06-07 10:17:17 +02:00
spengreb
f01338e3ba Terraform 0.12 now included in dockerfile 2019-06-04 09:23:28 +02:00
spengreb
669e3c160c yeo 2019-06-04 09:17:43 +02:00
spengreb
6279a80e6b remove tags 2019-06-04 09:03:48 +02:00
spengreb
1a411a52a4 Add docker latest tag 2019-06-04 08:52:32 +02:00
spengreb
f947820f54 Add automatic docker registry 2019-06-04 08:51:23 +02:00
Conor.McManus
0c0675857a Drone io badge 2019-05-27 13:32:44 +02:00
Conor.McManus
deb0294b61 Add ci stuff 2019-05-27 13:30:45 +02:00
Conor.McManus
1c8e828df6 Add drone ci file 2019-05-27 13:21:06 +02:00
Conor.McManus
288a8382a1 remove debug prints 2019-04-18 14:31:50 +02:00
Conor.McManus
0b432cd88a Add override to turn off automatically switching workspace 2019-04-17 11:44:14 +02:00
Conor.McManus
731f14eb62 Change -t to -e 2019-04-17 10:59:51 +02:00
Conor.McManus
ccccfda839 Change -t to -e 2019-04-17 10:59:30 +02:00
Conor.McManus
5985e6ffc9 Add workspace manager to lock git branch with workspace 2019-04-17 10:53:05 +02:00
Conor.McManus
360e946f6c Add git package to docker 2019-04-16 10:26:59 +02:00
Conor.McManus
fad6dfce79 Fix docker build 2019-04-15 16:51:21 +02:00
Conor.McManus
c36f461dc9 Fix docker build 2019-04-15 16:49:45 +02:00
Conor.McManus
a37c260264 Change OS base 2019-04-15 16:46:08 +02:00
Conor.McManus
49c7760afa Bug fix for templatingnot working 2019-04-15 16:38:32 +02:00
Conor.McManus
1b4ffc81c6 Remove unused import 2019-04-15 15:43:24 +02:00
10 changed files with 299 additions and 58 deletions

20
.drone.yml Normal file
View file

@ -0,0 +1,20 @@
kind: pipeline
name: default
steps:
- name: test
image: python
commands:
- ./atmos.py --help
- name: docker
image: plugins/docker
settings:
username:
from_secret: docker_user
password:
from_secret: docker_password
dockerfile: Dockerfile
repo: spengreb/atmos
tags:
- latest
- "0.12.20"

39
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,39 @@
stages:
- 🤞 test
- 🤞 test docker build
- 🚀 publish
test:
stage: 🤞 test
script:
- python3 -m unittest
test-docker-build:
stage: 🤞 test docker build
image: docker:latest
services:
- docker:dind
before_script:
- apk add jq curl
script:
- TERRAFORM_VERSION=$(curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version')
- docker build --pull -t "$CI_REGISTRY_IMAGE:$TERRAFORM_VERSION" .
except:
- master
publish:
stage: 🚀 publish
image: docker:latest
services:
- docker:dind
before_script:
- apk add jq curl
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- TERRAFORM_VERSION=$(curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version')
- docker build --pull -t "$CI_REGISTRY_IMAGE:$TERRAFORM_VERSION" .
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker push "$CI_REGISTRY_IMAGE:$TERRAFORM_VERSION"
- docker push "$CI_REGISTRY_IMAGE"
only:
- master

View file

@ -1,9 +1,12 @@
FROM alpine
FROM python:latest
RUN apk add python3
RUN wget -O /tmp/terraform.zip https://releases.hashicorp.com/terraform/0.11.13/terraform_0.11.13_linux_amd64.zip
RUN apt update && apt install -y jq
RUN wget -O /tmp/terraform.zip `echo "https://releases.hashicorp.com/terraform/$(curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version')/terraform_$(curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version')_linux_amd64.zip"`
RUN unzip /tmp/terraform.zip
RUN mv terraform /usr/bin/
COPY shared-creds /root/.aws/credentials
COPY atmos.py /usr/bin/atmos
COPY git-askpass-helper.sh /usr/bin/git-pass
RUN mkdir /atmos
COPY atmos.py credentials.py workspaces.py /atmos/
RUN ln -s /atmos/atmos.py /usr/bin/atmos

View file

@ -1,10 +1,24 @@
[![CircleCI](https://circleci.com/gh/Spengreb/atmos.svg?style=svg)](https://circleci.com/gh/Spengreb/atmos)
[![Build Status](https://cloud.drone.io/api/badges/Spengreb/atmos/status.svg)](https://cloud.drone.io/Spengreb/atmos)
# Terraform Atmosphere
Atmos is a thin wrapper for managing Terraform Workspaces easily. Using the workspace name it will select the correct .tfvar file, defaulting to a qa var file for any other workspace. This is primarily for pipelines but works just as well from the command line. It can process all terraform commands and parameters passing them on directly.
# Terraform Atmosphere :earth_africa:
Atmos is a thin wrapper for managing Terraform Workspaces easily. Using the workspace name it will select the correct .tfvar file, defaulting to a qa var file for any other workspace. This is primarily for pipelines but works just as well from the command line. It can process all terraform commands and parameters passing them on directly. Atmos will automatically switch workspaces per git branches if it discovers its in a git repository
# Quick Start
## Local Use
Atmos requires terraform to be installed on your system.
- Clone this atmos project
- Symlink atmos.py to your /usr/bin/ `$ ln -s $(pwd)/atmos.py /usr/bin/atmos`
- Set up your `~/.aws/credentials` to include a `[default]` stanza which is where your S3 backend storage is.
- Setup other stanzas in your credentials file for each environment you want. For example `[dev]` with your dev account IAM credentials
- You can also setup environment variables and use the -e flag. See below for more.
- Use `$ atmos apply/plan/destroy` to run terraform apply whilst maintaining environment context
## CI/CD
- Build the atmos image
- Use atmos as the build image in your CI/CD
- Include switching/creating terraform workspaces
@ -46,11 +60,11 @@ variable "workspace" {
}
```
This will make Terraform lookup AWS credentials from the `~/.aws/credentials` file using the workspace name as the stanza name. For example the credentials file would look like the shared-creds file in this repo.
This will make Terraform lookup AWS credentials from the `~/.aws/credentials` file using the workspace name as the stanza name.
## atmos -t
## atmos -e
Adding the `-t` flag to atmos will make it generate a new `~/.aws/credentials` file from environment variables. You must first include the `default` access key ID & secret access key like this:
Adding the `-e` flag to atmos will make it generate a new `~/.aws/credentials` file from environment variables. You must first include the `default` access key ID & secret access key like this:
```
DEFAULT_ACCESS_KEY_ID=id
@ -67,4 +81,26 @@ QA_ACCESS_KEY_ID=id
QA_SECRET_ACCESS_KEY=key
```
Note: Atmos will override your default credentials file as this functionality is for use in a docker container or in situations where you would rather use variables.
# atmos -m
Adding `-m` flag will set to manual mode. It will not try to automatically switch workspace per branch. It will adhere to whatever you last set the workspace to.
# atmos -p
Adding `-p` flag will set the project prefix when looking for credentials.
Example:
` $ atmos -e -p PROJ plan`
Will make atmos look for environment vars with the prefix 'VER' selecting the following env vars.
```
PROJ_DEV_ACCESS_KEY_ID
PROJ_DEV_SECRET_ACCESS_KEY
```
Note this also works on the `.aws/credentials` file
# atmos -v
Verbose output mode, will show the vars atmos has selected and some environment context

View file

@ -1,66 +1,58 @@
#!/usr/bin/env python3
import argparse, subprocess, shlex, sys, os, glob
from jinja2 import Environment, FileSystemLoader
import workspaces
import credentials
def main(argv):
parser = argparse.ArgumentParser(description='Control Terraform Workspaces.')
g = parser.add_mutually_exclusive_group()
g.add_argument("command", help="Send commands to terraform with workspace variable context", nargs='?', default=False)
parser.add_argument("-t", help="Template mode, gather shared-creds from environment variables (Dont use this flag if you dont want your ~/.aws/credentials replaced. This is for CI/CD", action='store_true', default=False)
parser.add_argument("-e", help="Gather shared-creds from environment variables. This is for CI/CD", action='store_true', default=False)
parser.add_argument("-m", help="Prevents workspace from changing with git branches automatically", action='store_true', default=False)
parser.add_argument("-n", help="Atmos will not add -var-file or -var args to terraform", action='store_true', default=False)
parser.add_argument("-p", "--project", help="Add a project prefix for env vars", nargs='?', default="")
parser.add_argument("-v", "--verbose", help="Debug mode", action="store_true", default=False)
args, params = parser.parse_known_args()
if args.command:
determine_actions(args, params)
def determine_actions(args, params):
workspace = get_env()
env_actions = ["plan", "apply", "destroy"] # Commands that require env context
aws_creds_file = "$HOME/.aws/credentials"
if (is_git_directory()) and not (args.m):
# if (args.e):
# aws_creds_file = aws_creds_file + "-atmos"
workspaces.workspace_manager()
workspace = workspaces.get_env()
workspace_vars = workspace
if (args.project) and workspace != 'default':
workspace = args.project + "-" + workspace
env_actions = ["init", "plan", "apply", "destroy"] # Commands that require env context
cmd = 'terraform {args}'.format(args=args.command)
if (args.command in env_actions) and not (args.n): # Append with env context
cmd = cmd + ' -var-file=vars/{env}.tfvars'.format(env=workspace_vars)
for param in params: # Pass terraform params directly through
cmd = cmd + ' ' + param
if (args.command in env_actions) and (workspace != "default"): # Append with env context
cmd = cmd + ' -var-file=vars/{env}.tfvars -var "workspace={env}"'.format(env=workspace)
if (args.e):
credentials.generate(args)
if (args.t):
generate_creds()
if (args.verbose):
print("Atmos will run: " + cmd)
print('Terraform {args} using env vars in {env}'.format(args=args.command, env=workspace_vars))
run_cmd(cmd)
print('Terraform {args} using env vars in {env}'.format(args=args.command, env=workspace))
def run_cmd(cmd):
with subprocess.Popen(shlex.split(cmd)) as proc:
exit # Start process but kill py program
def generate_creds():
current_workspace = get_env()
workspaces = ['default']
def is_git_directory():
return subprocess.call(['git', 'branch'], stderr=subprocess.STDOUT, stdout = open(os.devnull, 'w')) == 0
if current_workspace != 'default':
workspaces.append(current_workspace)
contents = ""
for workspace in workspaces:
contents = contents + "[{workspace}]\n".format(workspace=workspace)
contents = contents + "access_key_id=" + os.environ.get(workspace.upper() + '_ACCESS_KEY_ID') + "\n"
contents = contents + "secret_access_key=" + os.environ.get(workspace.upper() + '_SECRET_ACCESS_KEY') + "\n"
with open(os.path.expanduser('~/.aws/credentials'), 'w+') as f:
f.write(contents)
def get_valid_envs():
try:
# Use var files when present, otherwise default to qa
return [os.path.splitext(os.path.basename(x))[0] for x in glob.glob("vars/*.tfvars")]
except FileNotFoundError:
return False
def get_env():
try:
tf_env = open('.terraform/environment', 'r').read()
except:
return("default")
if str(tf_env) in get_valid_envs():
return(tf_env)
else:
return("qa")
if __name__ == "__main__":
main(sys.argv)
main(sys.argv)

53
credentials.py Normal file
View file

@ -0,0 +1,53 @@
import workspaces, sys, os
def generate(args):
current_workspace = workspaces.get_env()
workspaces_names = ['default']
aws_creds_dir = '~/.aws'
aws_creds_file = 'credentials'
aws_creds_full = aws_creds_dir + '/' + aws_creds_file
if os.path.isfile(os.path.expanduser(aws_creds_full)):
answer = input(f"[WARNING] File {aws_creds_full} already exists. Atmos will generate a new credentials file from your env vars. \nDo you want to override {aws_creds_full}? [y/N]")
if not answer or answer[0].lower() != 'y':
print("File not changed. This flag is for CI/CD only")
exit(1)
else:
if not os.path.isdir(os.path.expanduser(aws_creds_dir)):
os.makedirs(os.path.expanduser(aws_creds_dir))
if current_workspace != 'default':
workspaces_names.append(current_workspace)
project_name = ""
if (args.project):
delimeter = "_"
project_name = args.project.upper() + delimeter
contents = ""
for workspace in workspaces_names:
access_key_name = project_name + workspace.upper() + '_ACCESS_KEY_ID'
secret_key_name = project_name + workspace.upper() + '_SECRET_ACCESS_KEY'
if (args.verbose):
print(access_key_name)
print(secret_key_name)
if (workspace == 'default'):
contents = contents + "[{workspace}]\n".format(workspace=(workspace).lower())
else:
contents = contents + "[{workspace}]\n".format(workspace=(project_name.replace("_", "-") + workspace).lower())
try:
contents = contents + "aws_access_key_id=" + os.environ.get(access_key_name) + "\n"
except:
print("[ERROR]: Env Variable " + access_key_name + " not found.")
sys.exit(1)
try:
contents = contents + "aws_secret_access_key=" + os.environ.get(secret_key_name) + "\n"
except:
print("[ERROR]: Env Variable " + secret_key_name + " not found.")
sys.exit(1)
with open(os.path.expanduser(aws_creds_full), 'w+') as f:
f.write(contents)

3
git-askpass-helper.sh Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo ${GIT_PASSWORD}

View file

@ -1,7 +0,0 @@
[default]
[preprod]
[production]
[qa]

69
tests.py Normal file
View file

@ -0,0 +1,69 @@
import unittest
from unittest.mock import MagicMock, patch
import argparse
import atmos
class DetermineActionsTests(unittest.TestCase):
def setUp(self):
self.input_args = argparse.Namespace(command="mytestcommand", e=False, m=False, n=False, project="", verbose=False)
atmos.is_git_directory = MagicMock(return_value=False)
atmos.run_cmd = MagicMock()
def test_whenCalledWithNoAdditionalArgs_shouldRunTheTerraformCommand(self):
"""Simplest case where atmos is only called with a command and no further arguments"""
atmos.determine_actions(self.input_args, [])
atmos.run_cmd.assert_called_with("terraform mytestcommand")
def test_whenCalledWithParams_theyAreAppended(self):
"""Should append all params after the command"""
atmos.determine_actions(self.input_args, ["--myparam", "myvalue"])
atmos.run_cmd.assert_called_with("terraform mytestcommand --myparam myvalue")
@patch("workspaces.get_env")
def test_whenCalledWithInitCommand_shouldAppendVarsAndCreds(self, mocked_get_env):
"""Case where var-file, -var workpace=xyz is appended"""
self.input_args.command = "init"
mocked_get_env.return_value = "mytestenv"
atmos.determine_actions(self.input_args, [])
atmos.run_cmd.assert_called_with('terraform init -var-file=vars/mytestenv.tfvars -var "workspace=mytestenv"')
@patch("workspaces.get_env")
def test_whenCalledWithPlanCommand_shouldAppendVarsAndCreds(self, mocked_get_env):
"""Case where var-file, -var workpace=xyz is appended"""
self.input_args.command = "plan"
mocked_get_env.return_value = "mytestenv"
atmos.determine_actions(self.input_args, [])
atmos.run_cmd.assert_called_with('terraform plan -var-file=vars/mytestenv.tfvars -var "workspace=mytestenv"')
@patch("workspaces.get_env")
def test_whenCalledWithApplyCommand_shouldAppendVarsAndCreds(self, mocked_get_env):
"""Case where var-file, -var workpace=xyz is appended"""
self.input_args.command = "apply"
mocked_get_env.return_value = "mytestenv"
atmos.determine_actions(self.input_args, [])
atmos.run_cmd.assert_called_with('terraform apply -var-file=vars/mytestenv.tfvars -var "workspace=mytestenv"')
@patch("workspaces.get_env")
def test_whenCalledWithDestroyCommand_shouldAppendVarsAndCreds(self, mocked_get_env):
"""Case where var-file, -var workpace=xyz is appended"""
self.input_args.command = "destroy"
mocked_get_env.return_value = "mytestenv"
atmos.determine_actions(self.input_args, [])
atmos.run_cmd.assert_called_with('terraform destroy -var-file=vars/mytestenv.tfvars -var "workspace=mytestenv"')
@patch("workspaces.workspace_manager")
def test_whenInAGitRepo_andManualArgIsNotGiven_andEnvironmentArgIsNotGiven_shouldCallTheWorkspaceManager(self, mocked_workspace_manager):
atmos.is_git_directory.return_value = True
atmos.determine_actions(self.input_args, [])
mocked_workspace_manager.assert_called_once()
@patch("credentials.generate")
def test_whenEnvironmentArgIsGiven_shouldGenerateCredentials(self, mocked_generate):
self.input_args.e = True
atmos.determine_actions(self.input_args, [])
mocked_generate.assert_called_once()
if __name__ == '__main__':
unittest.main()

33
workspaces.py Normal file
View file

@ -0,0 +1,33 @@
import subprocess, os, glob
def workspace_manager():
branch = subprocess.getoutput("git rev-parse --abbrev-ref HEAD")
if branch == "master":
branch = "default"
else:
if branch not in get_valid_envs():
branch = "default"
if get_env() != branch:
print("[INFO]: Terraform workspace & git branch have diverged. Changing workspace to git branch...")
subprocess.call(["terraform", "workspace", "new", branch], stderr=subprocess.STDOUT, stdout=open(os.devnull, 'w'))
subprocess.call(["terraform", "workspace", "select", branch], stderr=subprocess.STDOUT, stdout=open(os.devnull, 'w'))
def get_valid_envs():
try:
# Use var files when present, otherwise default to default
return [os.path.splitext(os.path.basename(x))[0] for x in glob.glob("vars/*.tfvars")]
except FileNotFoundError:
return False
def get_env():
try:
tf_env = ""
with open('.terraform/environment', 'r') as f:
tf_env = f.readline()
except:
return("default")
if str(tf_env) in get_valid_envs():
return(tf_env)
else:
return("default")