diff --git a/MANIFEST.in b/MANIFEST.in index b1fc69e..e69de29 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +0,0 @@ -include VERSION \ No newline at end of file diff --git a/Makefile b/Makefile index 3457783..5988fe9 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ DOCKERFILE := etc/docker/Dockerfile IMAGE_OWNER := rehive IMAGE_NAME := tesserarius IMAGE_BASE := alpine -IMAGE_VERSION := $(shell cat VERSION) +IMAGE_VERSION := $(shell python -c "import tesserarius; print(tesserarius.__version__);") IMAGE_TAG := $(IMAGE_OWNER)/$(IMAGE_NAME):$(IMAGE_VERSION) CONTAINER_NAME := tessie diff --git a/VERSION b/VERSION deleted file mode 100644 index 77d6f4c..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.0 diff --git a/etc/tesserarius/tesserarius.yaml b/etc/tesserarius/tesserarius.yaml index 7a0c12b..7fd93db 100644 --- a/etc/tesserarius/tesserarius.yaml +++ b/etc/tesserarius/tesserarius.yaml @@ -6,5 +6,13 @@ staging: kubernetes: cluster: staging namespace: tesserarius-staging +extensions: serviceAccount: - name: extensions-developer \ No newline at end of file + name: extensions-team-developer + displayName: "Extensions Team Developer" + description: "Service account for the Extensions Team Developer" +platform: + serviceAccount: + name: platform-developer + displayName: "Platform Developer" + description: "Service Account for the Platform Developer" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d8055e0..304a083 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ google-api-python-client +invoke==0.18.1 +python-dotenv==0.10.2 +PyYAML==5.1 +semver==2.8.1 diff --git a/setup.py b/setup.py index 8004c84..aa8e5b1 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,19 @@ from setuptools import find_packages, setup +import tesserarius + here = path.abspath(path.dirname(__file__)) + +def parse_requirements(filename): + """ + Load requirements from a pip requirements file + """ + lineiter = (line.strip() for line in open(filename)) + return [line for line in lineiter if line and not line.startswith("#")] + + # Get the long description from the README file with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() @@ -23,24 +34,25 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version=open('VERSION').read().strip(), + version=tesserarius.__version__, # version=version_string, # version='1.0.6', description=( - "CLI that makes it easy to build and deploy an application to k8s."), + "CLI application that make it easier to perform DevOps on " \ + "Kubernetes and GCloud "), long_description=long_description, # The project's main homepage. - url='https://github.com/kidynamit/tesserarius', + url=tesserarius.__url__, # Author details - author='Mwangi', - author_email='mwangi@rehive.com', + author=tesserarius.__author__, + author_email=tesserarius.__email__, # Choose your license - license='MIT License', + license=tesserarius.__license__, # See https://pypi.python.org/pypi?:action=list_classifiers classifiers=[ @@ -68,7 +80,7 @@ # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.7', ], # What does your project relate to? @@ -96,13 +108,7 @@ # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html - install_requires=[ - 'invoke<=0.18.1,>=0.15', - 'pyyaml', - 'python-dotenv>=0.5.1', - 'semver', - ], - + install_requires= parse_requirements("requirements.txt"), # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: diff --git a/tesserarius/__init__.py b/tesserarius/__init__.py index 14d3526..2e4a535 100644 --- a/tesserarius/__init__.py +++ b/tesserarius/__init__.py @@ -1,4 +1,6 @@ -from invoke import Collection - -from .tasks import * -from .service_accounts import * +from invoke import Collection, task +__version__ = '0.0.1' +__url__ = 'https://github.com/rehive/tesserarius', +__author__ = 'Mwangi' +__email__ = 'info@rehive.com' +__license__ = 'MIT License' diff --git a/tesserarius/extensions/__init__.py b/tesserarius/extensions/__init__.py new file mode 100644 index 0000000..6e3a6f8 --- /dev/null +++ b/tesserarius/extensions/__init__.py @@ -0,0 +1,5 @@ +from tesserarius import Collection +from tesserarius.extensions.serviceaccount import collection as sa_collection + +collection = Collection("extensions") +collection.add_collection(sa_collection, "serviceaccount") diff --git a/tesserarius/extensions/serviceaccount.py b/tesserarius/extensions/serviceaccount.py new file mode 100644 index 0000000..3f96944 --- /dev/null +++ b/tesserarius/extensions/serviceaccount.py @@ -0,0 +1,81 @@ +from tesserarius import task, Collection +from tesserarius.serviceaccount import BaseServiceAccount, BASE_NAME_PATTERN +from tesserarius.utils import get_gcloud_wide_flags, get_settings + + +class ExtensionsServiceAccount(BaseServiceAccount): + project_id = "rehive-services" + + + def __init__(self, + name=None, + display_name=None, + description=None, + base=None): + """ + Checks if self.name has the correct naming convention + + platform- + + short name for the service account describing its purpose. + pattern is defined in the tesserarius.serviceaccount + + Example: platform-image_store, platform-patroni_wale + """ + name_pattern = r"extensions-[a-z]+-" + BASE_NAME_PATTERN + if name is not None and display_name is not None: + super().__init__(name=name, + display_name=display_name, + description=description, + name_pattern=name_pattern) + # discard base object + base = None + + if base is not None and isinstance(base, BaseServiceAccount): + super().__init__(name=base.name, + display_name=base.display_name, + description=base.description, + name_pattern=name_pattern) + else: + raise ServiceAccountCreateError( + "Invalid arguments provided to create obj.") + + self._check_name() + + + @staticmethod + def create_obj(project="extensions"): + return ExtensionsServiceAccount(base=BaseServiceAccount.create_obj(project)) + + +@task +def create(ctx): + ''' + Creates an IAM GCloud Service Account on rehive-services + ''' + sa = ExtensionsServiceAccount.create_obj() + sa.create(ctx) + + +@task +def update(ctx): + ''' + Updates an IAM GCloud Service Account on rehive-services + ''' + sa = ExtensionsServiceAccount.create_obj() + sa.update(ctx) + + +@task +def delete(ctx): + ''' + an IAM GCloud Service Account on rehive-services + ''' + sa = ExtensionsServiceAccount.create_obj() + sa.delete(ctx) + +collection = Collection("serviceaccount") +collection.add_task(create, "create") +collection.add_task(update, "update") +collection.add_task(delete, "delete") +# collection.add_task(authorize_serviceaccount, "auth") diff --git a/tesserarius/main.py b/tesserarius/main.py index afd2d8c..a9d5b0b 100644 --- a/tesserarius/main.py +++ b/tesserarius/main.py @@ -1,7 +1,9 @@ import pkg_resources from invoke import Argument, Collection, Program -import tesserarius +from tesserarius.tasks import namespace +from tesserarius.extensions import collection as extensions_collection +from tesserarius.platform import collection as platform_collection class MainProgram(Program): @@ -12,9 +14,5 @@ def core_args(self): ] return core_args + extra_args -namespace = Collection() -namespace.add_collection(tesserarius.tasks.cluster) -namespace.add_collection(tesserarius.service_accounts.collection) - version = pkg_resources.get_distribution("tesserarius").version program = MainProgram(namespace=namespace, version=version) diff --git a/tesserarius/platform/__init__.py b/tesserarius/platform/__init__.py new file mode 100644 index 0000000..69d8b2e --- /dev/null +++ b/tesserarius/platform/__init__.py @@ -0,0 +1,5 @@ +from tesserarius import Collection +from tesserarius.platform.serviceaccount import collection as sa_collection + +collection = Collection("platform") +collection.add_collection(sa_collection, "serviceaccount") diff --git a/tesserarius/platform/serviceaccount.py b/tesserarius/platform/serviceaccount.py new file mode 100644 index 0000000..436a1af --- /dev/null +++ b/tesserarius/platform/serviceaccount.py @@ -0,0 +1,81 @@ +from tesserarius import task, Collection +from tesserarius.serviceaccount import BaseServiceAccount, BASE_NAME_PATTERN +from tesserarius.utils import get_gcloud_wide_flags, get_settings + + +class PlatformServiceAccount(BaseServiceAccount): + project_id = "rehive-core" + + + def __init__(self, + name=None, + display_name=None, + description=None, + base=None): + """ + Checks if self.name has the correct naming convention + + extensions-- + + short name for the service account describing its purpose. + pattern is defined in the tesserarius.serviceaccount + + Example: extensions-product-image_store, extensions-product-patroni_wale + """ + name_pattern = r"platform-" + BASE_NAME_PATTERN + if name is not None and display_name is not None: + super().__init__(name=name, + display_name=display_name, + description=description, + name_pattern=name_pattern) + # discard base object + base = None + + if base is not None and isinstance(base, BaseServiceAccount): + super().__init__(name=base.name, + display_name=base.display_name, + description=base.description, + name_pattern=name_pattern) + else: + raise ServiceAccountCreateError( + "Invalid arguments provided to create obj.") + + self._check_name() + + + @staticmethod + def create_obj(project="platform"): + return PlatformServiceAccount(base=BaseServiceAccount.create_obj(project)) + + +@task +def create(ctx): + ''' + Creates an IAM GCloud Service Account on rehive-core + ''' + sa = PlatformServiceAccount.create_obj() + sa.create(ctx) + + +@task +def update(ctx): + ''' + Updates an IAM GCloud Service Account on rehive-core + ''' + sa = PlatformServiceAccount.create_obj() + sa.update(ctx) + + +@task +def delete(ctx): + ''' + an IAM GCloud Service Account on rehive-core + ''' + sa = PlatformServiceAccount.create_obj() + sa.delete(ctx) + +collection = Collection("serviceaccount") +collection.add_task(create, "create") +collection.add_task(update, "update") +collection.add_task(delete, "delete") +# collection.add_task(authorize_serviceaccount, "auth") diff --git a/tesserarius/service_accounts.py b/tesserarius/service_accounts.py deleted file mode 100644 index 1f97e04..0000000 --- a/tesserarius/service_accounts.py +++ /dev/null @@ -1,49 +0,0 @@ -from tesserarius.utils import get_gcloud_wide_flags, get_settings -from invoke import task, Collection -from invoke.exceptions import ParseError - - -@task -def create_serviceaccount(ctx, config): - ''' - Creates an IAM GCloud Service Account - ''' - settings_dict = get_settings() - config_dict = settings_dict[config] - command = "gcloud iam service-accounts create {name}" - - try: - command += " --display-name {display_name}".format( - display_name=config_dict['serviceAccount']['displayName']) - except KeyError: - pass - - command += get_gcloud_wide_flags(config_dict, allow_type=False) - ctx.run(command.format( - name=config_dict['serviceAccount']['name']), - echo=True) - - -@task -def authorize_serviceaccount(ctx, config): - ''' - Creates an IAM GCloud Service Account - ''' - sa = ctx["serviceAccount"] - settings_dict = get_settings() - config_dict = settings_dict[config] - command = "gcloud auth activate-service-account"\ - " {name}@{project}.iam.gserviceaccount.com"\ - " --key-file={keyfile}" - - command += get_gcloud_wide_flags(config_dict, allow_type=False) - - ctx.run(command.format( - name=config_dict['serviceAccount']['name'], - project=config_dict['gcloud']['project'], - key_file=config_dict['serviceAccount']['keyFile']), - echo=True) - -collection = Collection("serviceaccount") -collection.add_task(create_serviceaccount, "create") -# collection.add_task(authorize_serviceaccount, "auth") diff --git a/tesserarius/serviceaccount.py b/tesserarius/serviceaccount.py new file mode 100644 index 0000000..9add7ea --- /dev/null +++ b/tesserarius/serviceaccount.py @@ -0,0 +1,172 @@ +from re import match + +from tesserarius.utils import get_gcloud_wide_flags, get_settings +from invoke.exceptions import Failure, UnexpectedExit +from tesserarius.utils import get_error_stream as terr, get_out_stream as tout + +BASE_NAME_PATTERN = r"[a-z_]+" + +class ServiceAccountValidationError(Exception): + pass + + +class ServiceAccountCreateError(Exception): + pass + + +class BaseServiceAccount(): + project_id = None + name = None + emailaddress = None + display_name = "" + description = "" + name_pattern = None + + created = False + + def __init__(self, + name=None, + display_name=None, + description=None, + name_pattern=None): + self.name_pattern = name_pattern + self.name = name + self.display_name = display_name + self.description = description + + if name_pattern is None: + self.name_pattern = BASE_NAME_PATTERN + + + def __str__(self): + return "account_name: {name}, \ + display_name: {display_name}, \ + project_id: {project_id}, \ + description: {description}".format( + name=self.name, + display_name=self.display_name, + project_id=self.project_id, + description=self.description + ) + + + def _check_name(self): + """ + Checks if self.name has the correct naming convention + + + + short name for the service account describing its purpose. + Example: image_store, patroni_wale, walebackups + """ + if not match(r"^{}$".format(self.name_pattern), self.name): + raise ServiceAccountValidationError("Invalid account name.") + + + def get_emailaddress(self): + self.emailaddress = "{name}@{project_id}" \ + ".iam.gserviceaccount.com".format( + name=self.name, project_id=self.project_id) + return self.emailaddress + + + def create(self, ctx): + ''' + Creates an IAM GCloud Service Account + ''' + print("Creating service account '{name}' ... ".format(name=self.name), + end="") + command = "gcloud alpha iam service-accounts create {name}" \ + " --display-name \"{display_name}\"" \ + " --description \"{description}\"" \ + " --verbosity debug " \ + " --project {project_id}" + + if not self.created: + try: + result = ctx.run(command.format( + name=self.name, + display_name=self.display_name, + description=self.description, + project_id=self.project_id), + echo=False,out_stream=tout(), err_stream=terr()) + self.get_emailaddress() + self.created = True + print("SUCCESS!") + except (Failure, UnexpectedExit,): + self.emailaddress = None + print("FAILED! [serviceaccount can't be created]'") + + elif self.created: + print("FAILED! [serviceaccount has already been created]'") + + + def update(self, ctx): + ''' + Updates an IAM GCloud Service Account + ''' + print("Updating service account '{name}' ... ".format(name=self.name), + end="") + self.get_emailaddress() + command = "gcloud alpha iam service-accounts update {emailaddress}" \ + " --display-name \"{display_name}\"" \ + " --description \"{description}\"" \ + " --verbosity debug " \ + " --project {project_id}" + + try: + result = ctx.run(command.format( + emailaddress=self.emailaddress, + display_name=self.display_name, + description=self.description, + project_id=self.project_id), + echo=False,out_stream=tout(), err_stream=terr()) + self.get_emailaddress() + print("SUCCESS!") + except (Failure, UnexpectedExit,): + self.emailaddress = None + print("FAILED! [serviceaccount can't be updated]'") + + + def delete(self, ctx): + ''' + Deletes an IAM GCloud Service Account + ''' + print("Deleting service account '{name}' ... ".format(name=self.name), + end="") + self.get_emailaddress() + command = "gcloud alpha iam service-accounts delete {emailaddress}" \ + " --verbosity debug " \ + " --project {project_id}" + + try: + result = ctx.run(command.format( + emailaddress=self.emailaddress, + project_id=self.project_id), + echo=False,out_stream=tout(), err_stream=terr()) + self.get_emailaddress() + print("SUCCESS!") + except (Failure, UnexpectedExit,): + self.emailaddress = None + print("FAILED! [serviceaccount can't be deleted]'") + + + @staticmethod + def create_obj(project): + settings_dict = get_settings() + try: + project_dict = settings_dict[project] + except KeyError: + raise ServiceAccountCreateError( + "Config '{project}' not found.".format(project=project)) + + try: + return BaseServiceAccount( + name=project_dict['serviceAccount']['name'], + description=project_dict['serviceAccount']['description'], + display_name=project_dict['serviceAccount']['displayName']) + + except KeyError: + raise ServiceAccountCreateError( + "Config '{project}' has invalid keys.".format(project=project)) + diff --git a/tesserarius/tasks.py b/tesserarius/tasks.py index bebc2d6..adea8e0 100644 --- a/tesserarius/tasks.py +++ b/tesserarius/tasks.py @@ -1,6 +1,9 @@ -from tesserarius.utils import get_gcloud_wide_flags, get_settings -from invoke import task, Collection from invoke.exceptions import ParseError +from tesserarius import Collection, task +from tesserarius.utils import get_gcloud_wide_flags, get_settings +from tesserarius.extensions import collection as extensions_collection +from tesserarius.platform import collection as platform_collection + @task def set_cluster(ctx, config): @@ -14,6 +17,10 @@ def set_cluster(ctx, config): cluster=config_dict['kubernetes']['cluster']), echo=True) +collection = Collection("cluster") +collection.add_task(set_cluster, "set") -cluster = Collection("cluster") -cluster.add_task(set_cluster, "set") +namespace = Collection() +namespace.add_collection(extensions_collection) +namespace.add_collection(platform_collection) +namespace.add_collection(collection) diff --git a/tesserarius/utils.py b/tesserarius/utils.py index b5b385b..3e94afc 100644 --- a/tesserarius/utils.py +++ b/tesserarius/utils.py @@ -27,11 +27,22 @@ def format_yaml(template, config): return formatted +def get_error_stream(): + path = 'var/tesserarius/error.log' + return open(path, 'a+') + + +def get_out_stream(): + path = 'var/tesserarius/out.log' + return open(path, 'a+') + + def get_settings(): ''' Import project settings ''' - with open('etc/tesserarius/tesserarius.yaml', 'r') as stream: + path = 'etc/tesserarius/tesserarius.yaml' + with open(path, 'r') as stream: settings_dict = yaml.load(stream, Loader=Loader) return settings_dict diff --git a/test/serviceaccount.py b/test/serviceaccount.py new file mode 100644 index 0000000..50c116a --- /dev/null +++ b/test/serviceaccount.py @@ -0,0 +1,20 @@ +from tesserarius.extensions.serviceaccount import ExtensionsServiceAccount as esa +from tesserarius.platform.serviceaccount import PlatformServiceAccount as psa + + +def extensions_create_obj(): + sa = esa.create_obj() + + +def platform_create_obj(): + sa = psa.create_obj() + + +def main(): + extensions_create_obj() + platform_create_obj() + + +if __name__ == "__main__": + main() +