Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions to clean up the cloudknot config file #250

Merged
merged 25 commits into from
Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f5a8662
Add ability to prune stacks
richford Sep 4, 2020
d23347a
Add logger info statement about removing stack from config
richford Sep 4, 2020
949c290
Switch to correct profile and region before checking stack status
richford Sep 4, 2020
214290c
Add functions to prune ECR repos and old batch jobs
richford Sep 4, 2020
d872c94
Remove trailing white space and unused variable name
richford Sep 4, 2020
d6a55fb
Reinsert necessary deleted boto response in cloudknot.py
richford Sep 4, 2020
8c3a0e8
Remove whitespace
richford Sep 4, 2020
5c355fd
WIP: prune docker images
richford Sep 4, 2020
2bc665f
Prune images
richford Sep 5, 2020
771a434
Add unit tests for config.py functions. Still need tests for prune_im…
richford Sep 7, 2020
97207db
Fix unused local variable
richford Sep 7, 2020
3e84b65
Mock in the test_get_tags function
richford Sep 7, 2020
e79f511
Use mock aws credentials in test_prune_stacks and test_prune_repos
richford Sep 7, 2020
bfb173a
Use nosec comments for fake AWS credentials
richford Sep 7, 2020
8b9949e
Remove aws_credentials fixture from test_prune_stacks
richford Sep 8, 2020
4133732
Try good ol' print statement debugging
richford Sep 8, 2020
0ceec20
Fix set_profile to accomodate fallback option from get_profile
richford Sep 8, 2020
f2b2302
Use fallback profile in set_region too
richford Sep 8, 2020
d4be639
Use codacy coverage uploader in github action
richford Sep 8, 2020
db1df8c
Use coveralls instead
richford Sep 8, 2020
3e84f29
Remove leading dot-slash in github action
richford Sep 8, 2020
3fc598e
Use coveralls CLI since the github action expects a .lcov file
richford Sep 9, 2020
81a23e2
Add github token to environment for coveralls in github action
richford Sep 9, 2020
fbb1a4c
Maybe we don't need a COVERALLS_REPO_TOKEN at all
richford Sep 9, 2020
e2bde0e
Update coverage badge in README.md
richford Sep 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ branch = True
source = cloudknot/*
include = cloudknot/*
omit = */setup.py
cloudknot/commands/*.py
cloudknot/due.py
cloudknot/config.py
cloudknot/version.py
cloudknot/_meta.py
cloudknot/data/*/*/*.py
Expand Down
21 changes: 16 additions & 5 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install software
run: |
python -m pip install coveralls --use-feature=2020-resolver
python -m pip install --upgrade pip --use-feature=2020-resolver
python -m pip install .[dev] --use-feature=2020-resolver
- name: Configure
Expand All @@ -34,9 +35,19 @@ jobs:
- name: Test
run: |
make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
- name: Coveralls Parallel
run: |
coveralls
env:
COVERALLS_PARALLEL: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

finish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: .coverage
name: codecov-cloudknot
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ lint: flake

test:
# Unit testing using pytest
py.test --pyargs cloudknot --cov-report term-missing --cov=cloudknot
pytest --pyargs cloudknot --cov-report term-missing --cov=cloudknot

devtest:
# Unit testing with the -x option, aborts testing after first failure
# Useful for development when tests are long
py.test -x --pyargs cloudknot --cov-report term-missing --cov=cloudknot
pytest -x --pyargs cloudknot --cov-report term-missing --cov=cloudknot

clean: clean-build clean-pyc ## remove all build, test, coverage and Python artifacts

Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
![Build Status](https://github.com/nrdg/cloudknot/workflows/build/badge.svg)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/4a5d0c767bfd4f0eae820c24df1ce2a8)](https://www.codacy.com/gh/nrdg/cloudknot?utm_source=github.com&utm_medium=referral&utm_content=nrdg/cloudknot&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/d6fa3a18646f4a8089c7c897819d0342)](https://www.codacy.com/manual/richford/cloudknot?utm_source=github.com&utm_medium=referral&utm_content=nrdg/cloudknot&utm_campaign=Badge_Coverage)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Build Status](https://github.com/nrdg/cloudknot/workflows/build/badge.svg)](https://github.com/nrdg/cloudknot/workflows/build/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nrdg/cloudknot/badge.svg?branch=master)](https://coveralls.io/github/nrdg/cloudknot?branch=master)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/4a5d0c767bfd4f0eae820c24df1ce2a8)](https://www.codacy.com/gh/nrdg/cloudknot?utm_source=github.com&utm_medium=referral&utm_content=nrdg/cloudknot&utm_campaign=Badge_Grade)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![DOI](https://zenodo.org/badge/102051437.svg)](https://zenodo.org/badge/latestdoi/102051437)

# cloudknot
Expand Down
4 changes: 2 additions & 2 deletions cloudknot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

try:
__version__ = version(__name__)
except PackageNotFoundError:
except PackageNotFoundError: # pragma: nocover
# package is not installed
pass

Expand Down Expand Up @@ -59,7 +59,7 @@
pre_existing = e.errno == errno.EEXIST and os.path.isdir(logdir)
if pre_existing:
pass
else:
else: # pragma: nocover
raise e

handler = logging.FileHandler(logpath, mode="w")
Expand Down
19 changes: 14 additions & 5 deletions cloudknot/aws/base_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ def get_tags(name, additional_tags=None):
if additional_tags is not None:
if isinstance(additional_tags, list):
if not all(
[set(item.keys) == set(["Key", "Value"]) for item in additional_tags]
[set(item.keys()) == set(["Key", "Value"]) for item in additional_tags]
):
raise ValueError(
"If additional_tags is a list, it must be a list of "
"dictionaries of the form {'Key': key_val, 'Value': "
"value_val}."
)
tag_list = additional_tags
tag_list += additional_tags
elif isinstance(additional_tags, dict):
if "Key" in additional_tags.keys() or "Value" in additional_tags.keys():
raise ValueError(
Expand Down Expand Up @@ -578,7 +578,10 @@ def set_region(region="us-east-1"):
# throughout the package
max_pool = clients["iam"].meta.config.max_pool_connections
boto_config = botocore.config.Config(max_pool_connections=max_pool)
session = boto3.Session(profile_name=get_profile(fallback=None))
profile_name = get_profile(fallback=None)
session = boto3.Session(
profile_name=profile_name if profile_name != "from-env" else None
)
clients["batch"] = session.client(
"batch", region_name=region, config=boto_config
)
Expand All @@ -591,6 +594,8 @@ def set_region(region="us-east-1"):
clients["iam"] = session.client("iam", region_name=region, config=boto_config)
clients["s3"] = session.client("s3", region_name=region, config=boto_config)

mod_logger.debug("Set region to {region:s}".format(region=region))


@registered
def list_profiles():
Expand Down Expand Up @@ -724,7 +729,7 @@ def set_profile(profile_name):
"""
profile_info = list_profiles()

if profile_name not in profile_info.profile_names:
if not (profile_name in profile_info.profile_names or profile_name == "from-env"):
raise CloudknotInputError(
"The profile you specified does not exist in either the AWS "
"config file at {conf:s} or the AWS shared credentials file at "
Expand All @@ -750,7 +755,9 @@ def set_profile(profile_name):
# throughout the package
max_pool = clients["iam"].meta.config.max_pool_connections
boto_config = botocore.config.Config(max_pool_connections=max_pool)
session = boto3.Session(profile_name=profile_name)
session = boto3.Session(
profile_name=profile_name if profile_name != "from-env" else None
)
clients["batch"] = session.client(
"batch", region_name=get_region(), config=boto_config
)
Expand All @@ -773,6 +780,8 @@ def set_profile(profile_name):
"s3", region_name=get_region(), config=boto_config
)

mod_logger.debug("Set profile to {profile:s}".format(profile=profile_name))


#: module-level dictionary of boto3 clients for IAM, EC2, Batch, ECR, ECS, S3.
clients = {
Expand Down
16 changes: 16 additions & 0 deletions cloudknot/aws/ecr.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ def registered(fn):
mod_logger = logging.getLogger(__name__)


def _get_repo_info_from_uri(repo_uri):
# Get all repositories
repositories = clients["ecr"].describe_repositories(maxResults=500)["repositories"]

_repo_uri = repo_uri.split(":")[0]
# Filter by matching on repo_uri
matching_repo = [
repo for repo in repositories if repo["repositoryUri"] == _repo_uri
][0]

return {
"registry_id": matching_repo["registryId"],
"repo_name": matching_repo["repositoryName"],
}


# noinspection PyPropertyAccess,PyAttributeOutsideInit
@registered
class DockerRepo(NamedObject):
Expand Down
101 changes: 13 additions & 88 deletions cloudknot/cloudknot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from concurrent.futures import ThreadPoolExecutor

from . import aws
from .config import get_config_file, rlock
from .config import get_config_file, rlock, is_valid_stack
from . import dockerimage

__all__ = []
Expand Down Expand Up @@ -129,54 +129,13 @@ def __init__(

self._stack_id = config.get(self._pars_name, "stack-id")

try:
response = aws.clients["cloudformation"].describe_stacks(
StackName=self._stack_id
)
except aws.clients["cloudformation"].exceptions.ClientError as e:
error_code = e.response.get("Error").get("Message")
no_stack_code = "Stack with id {0:s} does not exist" "".format(
self._stack_id
)
if error_code == no_stack_code:
# Remove this section from the config file
with rlock:
config.read(get_config_file())
config.remove_section(self._pars_name)
with open(get_config_file(), "w") as f:
config.write(f)
raise aws.ResourceDoesNotExistException(
"Cloudknot found this PARS in its config file, but "
"the PARS stack that you requested does not exist on "
"AWS. Cloudknot has deleted this PARS from the config "
"file, so you may be able to create a new one simply "
"by re-running your previous command.",
self._stack_id,
)
else: # pragma: nocover
raise e

no_stack = len(response.get("Stacks")) == 0 or response.get("Stacks")[0][
"StackStatus"
] in [
"CREATE_FAILED",
"ROLLBACK_COMPLETE",
"ROLLBACK_IN_PROGRESS",
"ROLLBACK_FAILED",
"DELETE_IN_PROGRESS",
"DELETE_FAILED",
"DELETE_COMPLETE",
"UPDATE_ROLLBACK_FAILED",
]

if no_stack:
if not is_valid_stack(self._stack_id):
# Remove this section from the config file
with rlock:
config.read(get_config_file())
config.remove_section(self._pars_name)
with open(get_config_file(), "w") as f:
config.write(f)

raise aws.ResourceDoesNotExistException(
"Cloudknot found this PARS in its config file, but "
"the PARS stack that you requested does not exist on "
Expand All @@ -186,6 +145,9 @@ def __init__(
self._stack_id,
)

response = aws.clients["cloudformation"].describe_stacks(
StackName=self._stack_id
)
outs = response.get("Stacks")[0]["Outputs"]

self._batch_service_role = _stack_out("BatchServiceRole", outs)
Expand Down Expand Up @@ -1005,61 +967,24 @@ def __init__(

self._stack_id = config.get(self._knot_name, "stack-id")

try:
response = aws.clients["cloudformation"].describe_stacks(
StackName=self._stack_id
)
except aws.clients["cloudformation"].exceptions.ClientError as e:
error_code = e.response.get("Error").get("Message")
no_stack_code = "Stack with id {0:s} does not exist" "".format(
self._stack_id
)
if error_code == no_stack_code:
# Remove this section from the config file
with rlock:
config.read(get_config_file())
config.remove_section(self._knot_name)
with open(get_config_file(), "w") as f:
config.write(f)
raise aws.ResourceDoesNotExistException(
"The Knot cloudformation stack that you requested "
"does not exist. Cloudknot has deleted this Knot from "
"the config file, so you may be able to create a new "
"one simply by re-running your previous command.",
self._stack_id,
)
else:
raise e

no_stack = len(response.get("Stacks")) == 0 or response.get("Stacks")[0][
"StackStatus"
] in [
"CREATE_FAILED",
"ROLLBACK_COMPLETE",
"ROLLBACK_IN_PROGRESS",
"ROLLBACK_FAILED",
"DELETE_IN_PROGRESS",
"DELETE_FAILED",
"DELETE_COMPLETE",
"UPDATE_ROLLBACK_FAILED",
]

if no_stack:
if not is_valid_stack(self._stack_id):
# Remove this section from the config file
with rlock:
config.read(get_config_file())
config.remove_section(self._knot_name)
with open(get_config_file(), "w") as f:
config.write(f)

raise aws.ResourceDoesNotExistException(
"The Knot cloudformation stack that you requested does "
"not exist. Cloudknot has deleted this Knot from the "
"config file, so you may be able to create a new one "
"simply by re-running your previous command.",
"The Knot cloudformation stack that you requested "
"does not exist. Cloudknot has deleted this Knot from "
"the config file, so you may be able to create a new "
"one simply by re-running your previous command.",
self._stack_id,
)

response = aws.clients["cloudformation"].describe_stacks(
StackName=self._stack_id
)
outs = response.get("Stacks")[0]["Outputs"]

job_def_arn = _stack_out("JobDefinition", outs)
Expand Down
Loading