Skip to content

Commit 0782d84

Browse files
committed
Merge branch 'release/2.0.0'
2 parents 598158d + f73ed0f commit 0782d84

8 files changed

Lines changed: 214 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- "**"
7+
pull_request:
8+
9+
jobs:
10+
lint:
11+
name: Lint (Python 3.12)
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
cache: pip
23+
cache-dependency-path: |
24+
setup.py
25+
setup.cfg
26+
27+
- name: Install linters
28+
run: |
29+
python -m pip install --upgrade pip
30+
python -m pip install black flake8
31+
32+
- name: Lint (flake8)
33+
run: flake8 .
34+
35+
- name: Format check (black)
36+
run: black --check .
37+
38+
test:
39+
name: Test (Python ${{ matrix.python-version }})
40+
runs-on: ubuntu-latest
41+
strategy:
42+
fail-fast: false
43+
matrix:
44+
python-version:
45+
- "3.9"
46+
- "3.10"
47+
- "3.11"
48+
- "3.12"
49+
50+
steps:
51+
- name: Checkout
52+
uses: actions/checkout@v4
53+
54+
- name: Set up Python
55+
uses: actions/setup-python@v5
56+
with:
57+
python-version: ${{ matrix.python-version }}
58+
cache: pip
59+
cache-dependency-path: |
60+
setup.py
61+
setup.cfg
62+
63+
- name: Install dependencies
64+
run: |
65+
python -m pip install --upgrade pip
66+
python -m pip install -e '.[test]'
67+
68+
- name: Run tests
69+
run: python -m unittest

.github/workflows/release.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Release to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "*"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
id-token: write
12+
13+
jobs:
14+
release:
15+
name: Release
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.11"
26+
cache: pip
27+
cache-dependency-path: |
28+
setup.py
29+
setup.cfg
30+
31+
- name: Build distributions
32+
run: |
33+
python -m pip install --upgrade pip
34+
python -m pip install build
35+
python -m build --sdist --wheel
36+
37+
- name: Publish to PyPI
38+
uses: pypa/gh-action-pypi-publish@release/v1

.travis.yml

Lines changed: 0 additions & 25 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.0.0
2+
3+
- Upgrade to boto3
4+
- Switch to GitHub Actions
5+
- Remove TravisCI
6+
- Test upto Python 3.12
7+
18
## 1.0.0
29

310
- Added Python 3 support (only Python 3.7 tested).

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
# majorkirby [![Build Status](https://travis-ci.org/azavea/majorkirby.svg)](https://travis-ci.org/azavea/majorkirby)
1+
# majorkirby [![CI](https://github.com/azavea/majorkirby/actions/workflows/ci.yml/badge.svg)](https://github.com/azavea/majorkirby/actions/workflows/ci.yml)
22

33
Puts CloudFormation stacks into motion.
44

5+
This project uses `boto3` for AWS CloudFormation.
6+
57
## Testing
68

79
There are two ways to run the built-in test suite. One is intended to be run locally, while the other is setup to run within a Docker container.
810

911
### Local
1012

11-
The local tests require a working installation of Python 3:
13+
The local tests require a working installation of Python 3.
14+
15+
Install dependencies (including test extras) and run the tests:
16+
17+
```bash
18+
$ python3 -m pip install -e '.[test]'
19+
$ python3 -m unittest
20+
```
21+
22+
If you prefer `uv`:
23+
24+
```bash
25+
$ uv pip install -e '.[test]'
26+
$ uv run python -m unittest
27+
```
28+
29+
The legacy setuptools test runner is still supported:
1230

1331
```bash
1432
$ python3 setup.py test
@@ -20,5 +38,12 @@ The Docker setup builds an image with Python 3 installed, along with all of this
2038

2139
```bash
2240
$ docker-compose build majorkirby
41+
$ docker-compose run majorkirby python -m pip install -e '.[test]'
42+
$ docker-compose run majorkirby python -m unittest
43+
```
44+
45+
If you prefer to keep using the legacy setuptools test runner:
46+
47+
```bash
2348
$ docker-compose run majorkirby python setup.py test
2449
```

majorkirby/majorkirby.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import json
1010
import logging
1111

12-
from boto import cloudformation
13-
from boto.exception import BotoServerError
12+
import boto3
13+
from botocore.exceptions import ClientError
1414

1515
from troposphere import Template, Ref, Output, Tags
1616

@@ -114,14 +114,49 @@ def __init__(self, **kwargs):
114114
self.should_run = False
115115
self.input_wiring = {}
116116
self.last_heartbeat_id = None
117-
self.boto_conn = None
117+
self._cfn_client = None
118118
self.stack_outputs = {}
119119
self.extra_outputs = {}
120120
self.stack_name = self.get_stack_name()
121121
self.aws_region = kwargs.get("aws_region", "us-east-1")
122122
self.aws_profile = kwargs.get("aws_profile", None)
123123
self.stack = None
124124

125+
def _get_cfn_client(self):
126+
if self._cfn_client is not None:
127+
return self._cfn_client
128+
129+
session = boto3.session.Session(
130+
profile_name=self.aws_profile, region_name=self.aws_region
131+
)
132+
self._cfn_client = session.client("cloudformation")
133+
return self._cfn_client
134+
135+
@staticmethod
136+
def _to_cfn_parameters(parameters):
137+
return [
138+
{"ParameterKey": key, "ParameterValue": str(value)}
139+
for key, value in parameters
140+
]
141+
142+
@staticmethod
143+
def _to_cfn_tags(tags):
144+
return [{"Key": key, "Value": str(value)} for key, value in tags.items()]
145+
146+
def _describe_stack(self):
147+
cfn = self._get_cfn_client()
148+
try:
149+
response = cfn.describe_stacks(StackName=self.stack_name)
150+
return response["Stacks"][0]
151+
except ClientError as e:
152+
error = e.response.get("Error", {})
153+
if error.get("Code") != "ValidationError":
154+
raise
155+
message = (error.get("Message") or "").lower()
156+
if "does not exist" not in message:
157+
raise
158+
return None
159+
125160
def connect_from(self, stack, name=None):
126161
"""
127162
Connects a node's outputs to this node's inputs.
@@ -227,7 +262,10 @@ def set_up_stack(self):
227262
"""
228263
This method should be overridden to set up the stack.
229264
"""
230-
self.add_version("2010-09-09")
265+
if hasattr(self, "set_version"):
266+
self.set_version("2010-09-09")
267+
else:
268+
self.add_version("2010-09-09")
231269

232270
def add_parameter(self, parameter, source=None):
233271
"""
@@ -316,25 +354,25 @@ def _launch_cfn(self):
316354
Sets up stack and launches it.
317355
"""
318356
self.set_up_stack()
319-
self.boto_conn = cloudformation.connect_to_region(
320-
region_name=self.aws_region, profile_name=self.aws_profile
321-
)
357+
cfn = self._get_cfn_client()
322358
parameters = []
323359
for param, input_name in list(self.input_wiring.items()):
324360
try:
325361
parameters.append((param, self.get_input(input_name)))
326362
except MKInputError:
327363
pass
328-
# check to see if stack exists
329-
try:
330-
self.stack = self.boto_conn.describe_stacks(self.stack_name)[0]
331-
except BotoServerError:
332-
# it would be great if we could more granularly check the error
333-
self.boto_conn.create_stack(
334-
self.stack_name,
335-
tags=self.get_raw_tags(),
336-
template_body=self.to_json(),
337-
parameters=parameters,
364+
365+
cfn_parameters = self._to_cfn_parameters(parameters)
366+
cfn_tags = self._to_cfn_tags(self.get_raw_tags())
367+
368+
# Check to see if stack exists.
369+
self.stack = self._describe_stack()
370+
if self.stack is None:
371+
cfn.create_stack(
372+
StackName=self.stack_name,
373+
Tags=cfn_tags,
374+
TemplateBody=self.to_json(),
375+
Parameters=cfn_parameters,
338376
)
339377
self.logger.info("Stack %s created", self.stack_name)
340378

@@ -343,17 +381,20 @@ def _check_cfn(self):
343381
Checks the status of the stack
344382
"""
345383

346-
self.stack = self.boto_conn.describe_stacks(self.stack_name)[0]
347-
self.logger.debug("%s %s", self.stack_name, self.stack.stack_status)
348-
return self.stack.stack_status
384+
self.stack = self._describe_stack()
385+
if self.stack is None:
386+
raise MKNoSuchStackError
387+
status = self.stack.get("StackStatus")
388+
self.logger.debug("%s %s", self.stack_name, status)
389+
return status
349390

350391
def _assign_outputs(self):
351392
"""
352-
Moves keys and values from boto output objects into dict.
393+
Moves keys and values from CloudFormation outputs into a dict.
353394
"""
354395
self.stack_outputs = {}
355-
for output in self.stack.outputs:
356-
self.stack_outputs[output.key] = output.value
396+
for output in self.stack.get("Outputs", []) or []:
397+
self.stack_outputs[output["OutputKey"]] = output["OutputValue"]
357398
self._custom_output_transform()
358399

359400
def _custom_output_transform(self):

setup.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010
) as f:
1111
long_description = f.read()
1212

13-
tests_require = ["moto >=0.4.1"]
13+
tests_require = ["moto[cloudformation] >=4.0.0"]
1414

1515
setup(
1616
name="majorkirby",
17-
version="1.0.0",
17+
version="2.0.0",
1818
description="Puts CloudFormation stacks into motion.",
1919
author="Sharp Hall",
2020
author_email="shall@azavea.com",
2121
keywords="aws cloudformation",
2222
packages=find_packages(exclude=["tests"]),
23-
install_requires=["troposphere>=0.7.2", "boto>=2.38.0"],
23+
install_requires=["troposphere>=0.7.2", "boto3>=1.9.0"],
2424
extras_require={"dev": [], "test": tests_require},
2525
test_suite="tests",
2626
tests_require=tests_require,
@@ -29,6 +29,9 @@
2929
"Intended Audience :: Developers",
3030
"License :: OSI Approved :: Apache Software License",
3131
"Programming Language :: Python :: 3",
32-
"Programming Language :: Python :: 3.7",
32+
"Programming Language :: Python :: 3.9",
33+
"Programming Language :: Python :: 3.10",
34+
"Programming Language :: Python :: 3.11",
35+
"Programming Language :: Python :: 3.12",
3336
],
3437
)

tests/test_stack_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from majorkirby import GlobalConfigNode, StackNode
44
from troposphere import Output
5-
from moto import mock_cloudformation_deprecated
5+
from moto import mock_aws
66

77

88
class FirstStackNode(StackNode):
@@ -22,7 +22,7 @@ def set_up_stack(self):
2222

2323

2424
class TestStackNode(unittest.TestCase):
25-
@mock_cloudformation_deprecated
25+
@mock_aws
2626
def test_stack_threading(self):
2727
global_config = GlobalConfigNode(**{"test": "joker"})
2828

0 commit comments

Comments
 (0)