From 4a7fb922b47bed633964852880ea0e4a78c7a8ac Mon Sep 17 00:00:00 2001 From: "Diaz de Arcaya Serrano, Josu" <josu.diazdearcaya@tecnalia.com> Date: Fri, 19 May 2023 09:04:09 +0200 Subject: [PATCH] y3 --- iem-api/.python-version | 2 +- iem-api/Dockerfile | 24 ++-- iem-api/certs/config | 3 + iem-api/certs/id_rsa | 27 ++++ iem-api/certs/id_rsa.pub | 1 + iem-api/db/.gitignore | 1 + iem-api/logging.ini | 31 ++++ iem-api/main.py | 62 ++++---- iem-api/src/core/engine.py | 64 +++++---- iem-api/src/core/iem.py | 191 +++++++++++++------------ iem-api/src/core/persistence.py | 84 +---------- iem-api/src/core/utils.py | 32 ++++- iem-api/src/resources/id_iem | 38 +++++ iem-api/src/resources/id_iem.pub | 1 + iem-api/tests/__init__.py | 0 iem-api/tests/it/__init__.py | 0 iem-api/tests/it/test_it_iem.py | 83 +++++++++++ iem-api/tests/resources/aws.zip | Bin 0 -> 1248 bytes iem-api/tests/resources/docker.zip | Bin 0 -> 5400 bytes iem-api/tests/resources/dummy.zip | Bin 0 -> 3700 bytes iem-api/tests/resources/openstack.zip | Bin 0 -> 3735 bytes iem-api/tests/unit/__init__.py | 0 iem-api/tests/unit/test_iem.py | 31 ++++ iem-api/tests/unit/test_main.py | 106 ++++++++++++++ iem-api/tests/unit/test_persistence.py | 32 +++++ 25 files changed, 571 insertions(+), 242 deletions(-) create mode 100644 iem-api/certs/config create mode 100644 iem-api/certs/id_rsa create mode 100644 iem-api/certs/id_rsa.pub create mode 100644 iem-api/db/.gitignore create mode 100644 iem-api/logging.ini create mode 100644 iem-api/src/resources/id_iem create mode 100644 iem-api/src/resources/id_iem.pub create mode 100644 iem-api/tests/__init__.py create mode 100644 iem-api/tests/it/__init__.py create mode 100644 iem-api/tests/it/test_it_iem.py create mode 100644 iem-api/tests/resources/aws.zip create mode 100644 iem-api/tests/resources/docker.zip create mode 100644 iem-api/tests/resources/dummy.zip create mode 100644 iem-api/tests/resources/openstack.zip create mode 100644 iem-api/tests/unit/__init__.py create mode 100644 iem-api/tests/unit/test_iem.py create mode 100644 iem-api/tests/unit/test_main.py create mode 100644 iem-api/tests/unit/test_persistence.py diff --git a/iem-api/.python-version b/iem-api/.python-version index f69abe4..0a59033 100644 --- a/iem-api/.python-version +++ b/iem-api/.python-version @@ -1 +1 @@ -3.9.7 +3.9.10 diff --git a/iem-api/Dockerfile b/iem-api/Dockerfile index 635043b..eb07ab2 100644 --- a/iem-api/Dockerfile +++ b/iem-api/Dockerfile @@ -1,17 +1,16 @@ FROM hashicorp/terraform:1.1.4 -ARG API_KEY +COPY requirements.txt /tmp/requirements.txt +RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3=3.9.16-r0 gnupg +RUN pip3 install -r /tmp/requirements.txt +# install docker stack +RUN apk add docker docker-compose -ENV API_KEY=$API_KEY +ENV API_KEY=changeme ENV IEM_HOME=/opt/iem/ +ENV DOCKERIZED=true COPY src/resources/ansible.cfg /etc/ansible/ansible.cfg -COPY requirements.txt /tmp/requirements.txt -COPY src ${IEM_HOME}src -COPY main.py ${IEM_HOME}main.py - -RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3=3.9.13-r1 gnupg -RUN pip3 install -r /tmp/requirements.txt # RUN adduser -h ${IEM_HOME} -S -D iem COPY certs/config ${IEM_HOME}.ssh/config @@ -28,7 +27,14 @@ RUN ansible-galaxy collection install community.general COPY roles.yml /tmp/roles.yml RUN ansible-galaxy install -r /tmp/roles.yml +RUN mkdir -p ${IEM_HOME}db && \ + mkdir -p ${IEM_HOME}deployments + +COPY src ${IEM_HOME}src +COPY main.py ${IEM_HOME}main.py +COPY logging.ini ${IEM_HOME}logging.ini + ENTRYPOINT ["/usr/bin/env"] WORKDIR ${IEM_HOME} -CMD /usr/bin/uvicorn main:app --host 0.0.0.0 +CMD /usr/bin/uvicorn main:app --host 0.0.0.0 --log-level info EXPOSE 8000 diff --git a/iem-api/certs/config b/iem-api/certs/config new file mode 100644 index 0000000..a3dd79d --- /dev/null +++ b/iem-api/certs/config @@ -0,0 +1,3 @@ +Host * + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null \ No newline at end of file diff --git a/iem-api/certs/id_rsa b/iem-api/certs/id_rsa new file mode 100644 index 0000000..86a197b --- /dev/null +++ b/iem-api/certs/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1FrTNE42EgZr9WJNMtvpKFHYhPUJ4lzEp83EM0jYY3TyjmIe +ThMuqMLAHCk22fl4a8PttucggJ5ZWKhcJh623/y8AybJcmqZgq9a41Q609dmirf0 +7frCl+6zL8Mqy2Le2BD4eRADcq11s8r8Ys6J+EBPHQgEnK9CeZLSc/WFRlVr4bOD +s0bEouDxjTAMYjYcpsCwqYgGdIXI9WWsnt3RvcEe8CaiTqoyDN8ZtgkG6MweSrTQ +js8ySHO6o25cOoF7aT9Ihhf32I+KUanNIOvk3RAw2z1FK5xkFbbqMggZqz7rJn3M +sn2dDiCQi2CWox2OYXV/jJKLC3UFuOX64fS9cwIDAQABAoIBAQCs69Tm1/Vx0ibh +aA4DJ06C1bsh8cP9v5soJgfp1xzWSGooBcA1xasOI6B6jhkrgNlNr/uIIEe4VLne +1yJKrGIwnUagrloGQMYGxDKXwYQx80p+FXRuwe7p96eUcjIL8tQSUCd1tdOI87VQ +FjBVaWiybfO+aUQQLytLgoK7iKfhb7vO+9F+ZK1iDjBDNxFuiOM5zoeWOI7boYkD +2yXIkwoBePS2rosbPLa649sVakKex2WhQdUFst4Zba2RhnWQBXUY44LvEK5TzScF +FyYphPOUSplbzzM2+fuOna91NIWmJyHmf15lj7X9kC66XFIZMlvapksB8stEpDiA +4al3IdBJAoGBAPPuM3xkr/kQYYn7E42fgpmINZ78V48gMLhpyUOabZtF8inMyMPB +q7kfHns8Vx0ET8grSNr3qwDDV82lwvGqRCFChASMdQRR9LanydkDSeqpuZyQtVlt +A/65YUdcNY7Vy+M+fRh5Srh/6qcO3beLeLWXbJ4RHBP/OEmHuF4mLfgVAoGBAN7c +qdxTOhXPvOU69Bs5rJdfo6qBI1Yg8MCGctsUwPFpw1kW773ROOPa6XI6D74Dsdg8 +ypZ+IC3pRVtx61Xi3NOwxWNTTG+dyUgTSFz+WKjywXZXeHIbWngiFqk8JFYQWPzk +6YaJk4tZhk2YuNNaCCYRgQqyWv8coEurRlMXZHlnAoGBALcJwdaQ0z8oXJimL4jw +7ZX5kIrpPWanuAdZUe4Jfj+qX8mf4fKKbCowQLYmlBOw/ZDtcfDlMYsUCdnFjZ+7 +rP3sJJYpM1F3khJRm3PdNOUCUMY8C+i7lejZADcE6SdyJFkztbjcowYI7nJHBHZL +ENvqcVW27wPOWlVKozz6lzn1AoGALVwmaoS6DtRwcwuzwZLUkR7TNhIAujgMKHN1 +DyhDOR+4tfpYI39hH+dfmnM83wTrfsKozUawkAepqToflySMo72X/2Zl6VXpMPVT +xjGyo/h87fRRvI/asxblG9702luLcTW6XjrEQBmhn0uVWtc5T15CsIWqxb/y1FPx +BVp+hcMCgYAlJXbjzjbbDoIOCsXPSPe9voBL8zVwp0aNuvQcuB/vCt1n1c1DWuPr +AGMy/fRwY0Znag+ODMuulm7RgXUQy6ifJHiz9cKVGg/mGifaJSjgC+1AI9HFlij3 +asM5CueU0gK974rDxQkwmIWpRH57+kf6s8tGDrPPvqX9S4p3oxFlTw== +-----END RSA PRIVATE KEY----- diff --git a/iem-api/certs/id_rsa.pub b/iem-api/certs/id_rsa.pub new file mode 100644 index 0000000..245caaf --- /dev/null +++ b/iem-api/certs/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUWtM0TjYSBmv1Yk0y2+koUdiE9QniXMSnzcQzSNhjdPKOYh5OEy6owsAcKTbZ+Xhrw+225yCAnllYqFwmHrbf/LwDJslyapmCr1rjVDrT12aKt/Tt+sKX7rMvwyrLYt7YEPh5EANyrXWzyvxizon4QE8dCAScr0J5ktJz9YVGVWvhs4OzRsSi4PGNMAxiNhymwLCpiAZ0hcj1Zaye3dG9wR7wJqJOqjIM3xm2CQbozB5KtNCOzzJIc7qjblw6gXtpP0iGF/fYj4pRqc0g6+TdEDDbPUUrnGQVtuoyCBmrPusmfcyyfZ0OIJCLYJajHY5hdX+MkosLdQW45frh9L1z josu@WKM0092A diff --git a/iem-api/db/.gitignore b/iem-api/db/.gitignore new file mode 100644 index 0000000..8d96d78 --- /dev/null +++ b/iem-api/db/.gitignore @@ -0,0 +1 @@ +iem.db diff --git a/iem-api/logging.ini b/iem-api/logging.ini new file mode 100644 index 0000000..7a1ca11 --- /dev/null +++ b/iem-api/logging.ini @@ -0,0 +1,31 @@ +[loggers] +keys=root,src,uvicorn + +[handlers] +keys=stream_handler + +[formatters] +keys=formatter + +[logger_root] +level=INFO +handlers=stream_handler + +[logger_src] +level=INFO +handlers=stream_handler +qualname=src +propagate=0 + +[logger_uvicorn] +level=INFO +handlers=stream_handler +qualname=uvicorn +propagate=0 + +[handler_stream_handler] +class=StreamHandler +formatter=formatter + +[formatter_formatter] +format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/iem-api/main.py b/iem-api/main.py index 3fb84f4..01bf7c0 100755 --- a/iem-api/main.py +++ b/iem-api/main.py @@ -1,33 +1,36 @@ +#!/usr/bin/env python3 + import json import logging -import os +from typing import List -from fastapi import FastAPI, BackgroundTasks, status, Security, Depends, HTTPException +import uvicorn +from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Security, status from fastapi.openapi.utils import get_openapi -from fastapi.security.api_key import APIKeyHeader, APIKey -from typing import List +from fastapi.security.api_key import APIKey, APIKeyHeader from src.core.iem import Iem -from src.core.persistence import Sqlite +from src.core.persistence import Persistence from src.core.utils import ( BaseResponse, - DeploymentResponse, - DeploymentRequest, DeleteDeploymentRequest, + DeploymentRequest, + DeploymentResponse, + SelfHealingRequest, ) -LOGGER = logging.getLogger("iem") - api_key_header = APIKeyHeader(name="x-api-key", auto_error=False) app = FastAPI( - title="IaC Execution Manager", version="0.1.15", description="IaC Execution Manager" + title="IaC Execution Manager", version="1.0.0", description="IaC Execution Manager" ) +logging.config.fileConfig("logging.ini") + async def get_api_key(api_key_query: str = Security(api_key_header)): - if Sqlite().valid_api_key(api_key_query=api_key_query): + if Persistence().valid_api_key(api_key_query=api_key_query): return api_key_query else: raise HTTPException( @@ -37,7 +40,7 @@ async def get_api_key(api_key_query: str = Security(api_key_header)): @app.get("/", tags=["greeting"]) -async def read_root(api_key: APIKey = Depends(get_api_key)): +async def read_root(_: APIKey = Depends(get_api_key)): return { "message": "Hello from the IaC Execution Manager!", "version": app.version, @@ -48,11 +51,7 @@ async def read_root(api_key: APIKey = Depends(get_api_key)): @app.get("/deployments/", response_model=List[DeploymentResponse], tags=["deployments"]) async def read_status( - start: int = 0, - count: int = 25, - start_date: str = "1970-01-01", - end_date: str = "2100-01-01", - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): all_deployments = Iem(credentials=None).get_all_deployments() return list(all_deployments) @@ -65,9 +64,7 @@ async def read_status( ) async def read_status_deployment( deployment_id: str, - start: int = 0, - count: int = 1, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): deployment = Iem().get_deployment(deployment_id=deployment_id) return list(deployment) @@ -82,11 +79,10 @@ async def read_status_deployment( async def deploy( d: DeploymentRequest, background_tasks: BackgroundTasks, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): - logging.warning(d) i = Iem(credentials=d.credentials) - background_tasks.add_task(i.deploy, d.deployment_id, d.repository, d.commit) + background_tasks.add_task(i.deploy, d.deployment_id, d.bundle.base64) return BaseResponse(message="Deployment Request Created") @@ -99,15 +95,29 @@ async def deploy( async def undeploy( d: DeleteDeploymentRequest, background_tasks: BackgroundTasks, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): - logging.warning(d) i = Iem(credentials=d.credentials) background_tasks.add_task(i.destroy, d.deployment_id) return BaseResponse(message="Undeployment Request Created") -if os.getenv("STAGE") == "dev": +@app.post( + "/self-healing/{strategy}", + status_code=status.HTTP_201_CREATED, + response_model=BaseResponse, + tags=["deployments"], +) +async def deploy( + d: SelfHealingRequest, + background_tasks: BackgroundTasks, + _: APIKey = Depends(get_api_key), +): + return BaseResponse(message="Deployment Request Created") + + +if __name__ == "__main__": + uvicorn.run("main:app", host="127.0.0.1", port=8000, log_level="info") with open("../openapi.json", "w") as f: json.dump( get_openapi( diff --git a/iem-api/src/core/engine.py b/iem-api/src/core/engine.py index 08f25cb..6eac90b 100644 --- a/iem-api/src/core/engine.py +++ b/iem-api/src/core/engine.py @@ -1,12 +1,13 @@ import logging import os import subprocess - +import time from abc import ABC, abstractmethod -from jinja2 import Template from subprocess import CalledProcessError -LOGGER = logging.getLogger("iem") +from jinja2 import Template + +LOGGER = logging.getLogger(__name__) class Factory: @@ -58,15 +59,16 @@ class Terraform(Engine): ) output = subprocess.run( ["terraform", "apply", "-auto-approve"], - check=True, cwd=self._repo_path, env=self._env, - # capture_output=True, + capture_output=True, ) + LOGGER.info(output.stdout.decode("utf-8")) + output.check_returncode() return "CREATED", output.stdout, output.stderr except CalledProcessError as e: LOGGER.exception(e) - return "ERROR", None, None + raise e def destroy(self): try: @@ -75,12 +77,14 @@ class Terraform(Engine): check=True, cwd=self._repo_path, env=self._env, - # capture_output=True, + capture_output=True, ) + LOGGER.info(output.stdout.decode("utf-8")) + output.check_returncode() return "DESTROYED", output.stdout, output.stderr except CalledProcessError as e: LOGGER.exception(e) - return "ERROR", None, None + raise e def output(self): try: @@ -120,26 +124,30 @@ class Ansible(Engine): def apply(self): LOGGER.info("About to apply ansible") - try: - output = subprocess.run( - ["ansible", "all", "-i", "inventory", "-m", "wait_for_connection"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) - LOGGER.info("All hosts in the inventory are reachable.") - output = subprocess.run( - ["ansible-playbook", "-i", "inventory", "main.yml"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) - return "CREATED", output.stdout, output.stderr - except CalledProcessError as e: - LOGGER.exception(e.output) - raise e + for i in range(2): + try: + output = subprocess.run( + ["ansible", "all", "-i", "inventory", "-m", "wait_for_connection"], + check=True, + cwd=self._repo_path, + env=self._env, + capture_output=True, + ) + LOGGER.info("All hosts in the inventory are reachable.") + output = subprocess.run( + ["ansible-playbook", "-i", "inventory", "main.yml"], + check=True, + cwd=self._repo_path, + env=self._env, + capture_output=True, + ) + LOGGER.info(output.stdout.decode("utf-8")) + return "CREATED", output.stdout, output.stderr + except CalledProcessError as e: + LOGGER.exception(e.output) + if i == 1: + raise e + time.sleep(10) def destroy(self): LOGGER.info("Nothing to be seen here.") diff --git a/iem-api/src/core/iem.py b/iem-api/src/core/iem.py index e4ad215..d5a40dc 100644 --- a/iem-api/src/core/iem.py +++ b/iem-api/src/core/iem.py @@ -1,54 +1,39 @@ -import git +import base64 +import binascii import json import logging import os -import subprocess +import shutil +from io import BytesIO +from subprocess import CalledProcessError +from zipfile import BadZipFile, ZipFile -from git import GitCommandError, InvalidGitRepositoryError from omegaconf import OmegaConf -from subprocess import CalledProcessError from src.core.engine import Factory -from src.core.persistence import Sqlite -from src.core.utils import DeploymentResponse, Credentials +from src.core.persistence import Persistence +from src.core.utils import Credentials, DeploymentResponse -LOGGER = logging.getLogger("iem") +LOGGER = logging.getLogger(__name__) class Iem: def __init__(self, credentials: Credentials = None): - logging.basicConfig( - level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s" - ) - self._credentials = credentials - # if credentials.aws: - # self._aws_access_key_id = credentials.aws.access_key_id - # self._aws_secret_access_key = credentials.aws.secret_access_key - # if credentials.openstack: - # self._os_username = credentials.openstack.user_name - # self._os_password = credentials.openstack.password - # self._os_auth_url = credentials.openstack.auth_url - # self._os_project_name = credentials.openstack.project_name + self._path_deployments = f"{os.environ['IEM_HOME']}deployments/" - # Check IEM_HOME variable - if os.getenv("IEM_HOME") is None: - LOGGER.error("Please define IEM_HOME environment variable.") - exit(-1) - self._iem_home = os.getenv("IEM_HOME") - - self._persistence = Sqlite() + self._persistence = Persistence() def get_all_deployments(self): rows = self._persistence.get_all_deployments() for r in rows: d = DeploymentResponse( - status_time=r[0], - deployment_id=r[1], - status=r[2], - stdout=r[3], - stderr=r[4], + status_time=r.status_time, + deployment_id=r.deployment_id, + status=r.status, + stdout=r.stdout, + stderr=r.stderr, ) yield d @@ -56,54 +41,87 @@ class Iem: row = self._persistence.get_deployment(deployment_id=deployment_id) if row: d = DeploymentResponse( - status_time=row[0], - deployment_id=row[1], - status=row[2], - stdout=row[3], - stderr=row[4], + status_time=row.status_time, + deployment_id=row.deployment_id, + status=row.status, + stdout=row.stdout, + stderr=row.stderr, ) yield d - def deploy(self, deployment_id: str, repository: str, commit: str): + def _get_env(self, deployment_id: str, credentials: Credentials) -> dict: + my_env = os.environ.copy() + if credentials.aws: + my_env["AWS_ACCESS_KEY_ID"] = credentials.aws.access_key_id + my_env["AWS_SECRET_ACCESS_KEY"] = credentials.aws.secret_access_key + if credentials.openstack: + my_env["OS_USERNAME"] = credentials.openstack.user_name + my_env["OS_PASSWORD"] = credentials.openstack.password + my_env["OS_AUTH_URL"] = credentials.openstack.auth_url + my_env["OS_PROJECT_NAME"] = credentials.openstack.project_name + if credentials.azure: + my_env["ARM_CLIENT_ID"] = credentials.azure.arm_client_id + my_env["ARM_CLIENT_SECRET"] = credentials.azure.arm_client_secret + my_env["ARM_SUBSCRIPTION_ID"] = credentials.azure.arm_subscription_id + my_env["ARM_TENANT_ID"] = credentials.azure.arm_tenant_id + if credentials.vmware: + my_env["VSPHERE_USER"] = credentials.vmware.user_name + my_env["VSPHERE_PASSWORD"] = credentials.vmware.password + my_env["VSPHERE_SERVER"] = credentials.vmware.server + my_env[ + "VSPHERE_ALLOW_UNVERIFIED_SSL" + ] = credentials.vmware.allow_unverified_ssl + + my_env["DEPLOYMENT_ID"] = deployment_id + + return my_env + + def _apply_stage(self, my_env: dict, repo_path: str, stage: str): + conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml") + + self.validate(env=my_env, io=conf.input) + + LOGGER.info(f"About to run stage: {stage}") + my_eng = Factory().get_engine(conf.engine)( + repo_path=f"{repo_path}/{stage}", my_env=my_env + ) + status, stdout, stderr = my_eng.apply() + + if conf.output: + output = my_eng.output() + self.update_env(output=output, env=my_env, io=conf.output) + self.validate(env=my_env, io=conf.output) + + return status, stdout, stderr + + def deploy(self, deployment_id: str, bundle: str): function_name = "deploy" - LOGGER.info(f"Running {function_name} method") + LOGGER.info(f"Running {function_name} method.") self._persistence.insert_deployment( deployment_id=deployment_id, status="STARTED", stdout=None, stderr=None ) + repo_path = f"{self._path_deployments}{deployment_id}" + if not os.path.exists(repo_path): + os.makedirs(repo_path) + try: - repo_path = f"{self._iem_home}{deployment_id}" - self.get_repo(repo_path=repo_path, repository=repository, commit=commit) - - LOGGER.info(f"About to deploy project {repo_path}") - my_env = os.environ.copy() - if self._credentials.aws: - my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id - my_env[ - "AWS_SECRET_ACCESS_KEY" - ] = self._credentials.aws.secret_access_key - if self._credentials.openstack: - my_env["OS_USERNAME"] = self._credentials.openstack.user_name - my_env["OS_PASSWORD"] = self._credentials.openstack.password - my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url - my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name + LOGGER.info(f"Decompressing base64 bundle.") + bundle_decode = base64.b64decode(bundle) + ZipFile(BytesIO(bundle_decode)).extractall(repo_path) + LOGGER.info(f"Reading credentials.") + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) + + LOGGER.info(f"About to deploy project {repo_path}.") project_conf = OmegaConf.load(f"{repo_path}/config.yaml") for stage in project_conf.iac: - conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml") - - self.validate(env=my_env, io=conf.input) - - my_eng = Factory().get_engine(conf.engine)( - repo_path=f"{repo_path}/{stage}", my_env=my_env + status, stdout, stderr = self._apply_stage( + my_env=my_env, repo_path=repo_path, stage=stage ) - status, stdout, stderr = my_eng.apply() - - if conf.output: - output = my_eng.output() - self.update_env(output=output, env=my_env, io=conf.output) - self.validate(env=my_env, io=conf.output) self._persistence.insert_deployment( deployment_id=deployment_id, @@ -111,12 +129,14 @@ class Iem: stdout=stdout, stderr=stderr, ) + LOGGER.info(f"Deployment completed for project {repo_path}") except ( + binascii.Error, + BadZipFile, CalledProcessError, - GitCommandError, FileNotFoundError, - InvalidGitRepositoryError, NameError, + KeyError, ) as e: LOGGER.exception(e) self._persistence.insert_deployment( @@ -130,17 +150,11 @@ class Iem: function_name = "destroy" LOGGER.info(f"Running {function_name} method") - repo_path = f"{self._iem_home}{deployment_id}" + repo_path = f"{self._path_deployments}{deployment_id}" - my_env = os.environ.copy() - if self._credentials.aws: - my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id - my_env["AWS_SECRET_ACCESS_KEY"] = self._credentials.aws.secret_access_key - if self._credentials.openstack: - my_env["OS_USERNAME"] = self._credentials.openstack.user_name - my_env["OS_PASSWORD"] = self._credentials.openstack.password - my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url - my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) LOGGER.info(f"About to destroy project {repo_path}") project_conf = OmegaConf.load(f"{repo_path}/config.yaml") @@ -153,6 +167,11 @@ class Iem: ) status, stdout, stderr = my_eng.destroy() + if "/home" in repo_path: + LOGGER.exception(f"Cannot delete {repo_path}") + elif os.getenv("DOCKERIZED") == "true": + shutil.rmtree(repo_path) + self._persistence.insert_deployment( deployment_id=deployment_id, status=status, @@ -173,21 +192,3 @@ class Iem: if variable not in env: LOGGER.error(f"Variable {variable} not found in environment.") raise NameError - - def get_repo(self, repo_path: str, repository: str, commit: str): - LOGGER.info( - f"About to download the project {repo_path} with repository {repository}" - ) - if not os.path.isdir(repo_path): - os.makedirs(repo_path) - repo = git.Repo.clone_from( - url=repository, to_path=repo_path, no_checkout=True - ) - else: - repo = git.Repo(repo_path) - - repo.git.checkout(commit) - - for submodule in repo.submodules: - submodule.update(init=True) - submodule.module().git.checkout() diff --git a/iem-api/src/core/persistence.py b/iem-api/src/core/persistence.py index 2f4979e..80ada9c 100644 --- a/iem-api/src/core/persistence.py +++ b/iem-api/src/core/persistence.py @@ -1,86 +1,16 @@ +import logging import os -import sqlite3 - -# from dataclasses import dataclass from datetime import datetime + from ratelimiter import RateLimiter +from sqlalchemy import Column, DateTime, Integer, String, create_engine, desc, select +from sqlalchemy.orm import Session, declarative_base -from sqlalchemy import create_engine -from sqlalchemy import Column -from sqlalchemy import DateTime, Integer, String -from sqlalchemy import select, desc -from sqlalchemy.orm import declarative_base, Session +LOGGER = logging.getLogger(__name__) Base = declarative_base() -class Sqlite: - def __init__(self): - self._db_name = "iem.db" - - # create database if it does not exist - if not os.path.isfile(self._db_name): - self.__create_database() - - def __create_database(self): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """CREATE TABLE deployments (id INTEGER PRIMARY KEY AUTOINCREMENT, - status_time DATETIME DEFAULT CURRENT_TIMESTAMP, - deployment_id TEXT NOT NULL, status TEXT NOT NULL, stdout TEXT, stderr TEXT);""" - conn.execute(sql) - - def insert_deployment( - self, deployment_id: str, status: str, stdout: str, stderr: str - ): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """INSERT INTO deployments (deployment_id, status, stdout, stderr) VALUES (?,?,?,?)""" - - cursor = conn.cursor() - cursor.execute( - sql, - ( - deployment_id, - status, - stdout, - stderr, - ), - ) - conn.commit() - - def get_deployment(self, deployment_id: str): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """SELECT status_time, deployment_id, status, stdout, stderr FROM deployments - WHERE deployment_id=? ORDER BY id DESC LIMIT 1""" - - cursor = conn.cursor() - cursor.execute(sql, (deployment_id,)) - row = cursor.fetchone() - - return row - - def get_all_deployments(self): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """SELECT status_time, deployment_id, status, stdout, stderr - FROM deployments ORDER BY id DESC LIMIT 25 OFFSET 0""" - - cursor = conn.cursor() - cursor.execute(sql) - rows = cursor.fetchall() - - return rows - - @RateLimiter(max_calls=10, period=1) - def valid_api_key(self, api_key_query: str): - if api_key_query == os.getenv("API_KEY"): - return True - else: - return False - - class Deployment(Base): __tablename__ = "deployments" @@ -96,8 +26,8 @@ class Deployment(Base): class Persistence: - def __init__(self): - self._engine = create_engine("sqlite:///:memory:", future=True) + def __init__(self, engine_url="sqlite:///db/iem.db"): + self._engine = create_engine(url=engine_url, future=True) Base.metadata.create_all(self._engine) def insert_deployment( diff --git a/iem-api/src/core/utils.py b/iem-api/src/core/utils.py index 5ec5df2..683afb2 100644 --- a/iem-api/src/core/utils.py +++ b/iem-api/src/core/utils.py @@ -1,10 +1,7 @@ -import logging - from datetime import datetime -from pydantic import BaseModel from typing import Dict, Optional -LOGGER = logging.getLogger("iem") +from pydantic import BaseModel class BaseResponse(BaseModel): @@ -42,16 +39,39 @@ class Openstack(BaseModel): user_domain_name: Optional[str] +class Docker(BaseModel): + server: str + user_name: str + password: str + + +class Vmware(BaseModel): + user_name: str + password: str + server: str + allow_unverified_ssl: Optional[str] + + class Credentials(BaseModel): aws: Optional[Aws] = None azure: Optional[Azure] = None openstack: Optional[Openstack] = None + vmware: Optional[Vmware] = None + docker: Optional[Docker] = None + + +class Bundle(BaseModel): + base64: str class DeploymentRequest(BaseModel): deployment_id: str - repository: str - commit: str + credentials: Credentials + bundle: Bundle + + +class SelfHealingRequest(BaseModel): + deployment_id: str credentials: Credentials diff --git a/iem-api/src/resources/id_iem b/iem-api/src/resources/id_iem new file mode 100644 index 0000000..2f5c307 --- /dev/null +++ b/iem-api/src/resources/id_iem @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/iem-api/src/resources/id_iem.pub b/iem-api/src/resources/id_iem.pub new file mode 100644 index 0000000..c4368a5 --- /dev/null +++ b/iem-api/src/resources/id_iem.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDV3VeYPD1WKQpq2PztNUQ0IAnm4JCmCPJpCLcem9kO3DvNbVBN5Q0JF0nfqgc8b1wP7uBH1b3G8jaX7Zsq6CpNu4OvXKw/EBxO3pdiAx6v0mRZMLBx2qcBd1TB90sxQ+juY6//06c52OgSjQ3Cey8a8/GKdtTMxRKh+rAuAch2WjcZBmTZPe+9E3fOhibHg5UA3sLhFcAPuEuEQRbTDBSsCqdCqHXfIe4NiLqJYVLOHkHuHfvTRaLROh7zmf20FmIP7s1gYU3GpDKRgZeuo2ZM9791N98B0JgSnepbx2/MDuG2WvAFwHIK7WSNVxGuxdvXspUWfDGam/G4ZGuxzaxVHtwYgSyxNoAUXcv82GS/MNLXqpfapvPR/LGcWAEcg1FgzjkC8y4MBZ6tQDowAYr/BEnUI7xmtIm0ixBWdd2zGtyMoSbvqHN0DSsRM+YDkD4XMcfd9Kp9kiIWHl1N9RSeV3D1JNfDGNGAE1qym25Ni0zWNbSyEUxpLkXj4T2wEDM= diff --git a/iem-api/tests/__init__.py b/iem-api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/it/__init__.py b/iem-api/tests/it/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/it/test_it_iem.py b/iem-api/tests/it/test_it_iem.py new file mode 100644 index 0000000..eb545cb --- /dev/null +++ b/iem-api/tests/it/test_it_iem.py @@ -0,0 +1,83 @@ +import base64 +import logging +import os +import unittest +import uuid + +from src.core.iem import Iem +from src.core.utils import Aws, Credentials, Openstack + +LOGGER = logging.getLogger(__name__) + + +class TestIem(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestIem, self).__init__(*args, **kwargs) + + # Check IEM_HOME variable + self._iem_home = os.environ["IEM_HOME"] + + @unittest.skipUnless(os.getenv("AWS"), "Define AWS variable to execute") + def test_deploy_destroy_aws(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + aws=Aws( + access_key_id=os.environ["AWS_ACCESS_KEY_ID"], + secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"], + ) + ) + ) + with open("tests/resources/aws.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + @unittest.skipUnless( + os.getenv("INTEGRATION"), "Define INTEGRATION variable to execute" + ) + def test_deploy_destroy_openstack(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + openstack=Openstack( + user_name=os.environ["OS_USERNAME"], + password=os.environ["OS_PASSWORD"], + auth_url=os.environ["OS_AUTH_URL"], + project_name=os.environ["OS_PROJECT_NAME"], + ) + ) + ) + with open("tests/resources/openstack.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + @unittest.skipUnless( + os.getenv("INTEGRATION"), "Define INTEGRATION variable to execute" + ) + def test_deploy_destroy_docker(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + openstack=Openstack( + user_name=os.environ["OS_USERNAME"], + password=os.environ["OS_PASSWORD"], + auth_url=os.environ["OS_AUTH_URL"], + project_name=os.environ["OS_PROJECT_NAME"], + ) + ) + ) + with open("tests/resources/docker.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) diff --git a/iem-api/tests/resources/aws.zip b/iem-api/tests/resources/aws.zip new file mode 100644 index 0000000000000000000000000000000000000000..0cc3d7449f463683c68ed584cc500b8d1a9d4014 GIT binary patch literal 1248 zcmWIWW@h1H00A%O@G!5g-~2KR3=AO5%^<^&oS&DLnXXrvn41$C!pXoaBl<mAUi5o% zX$3a}Bg+eB1_m&ptE;QPm6@1q#igL2t5A|!RFs&OUzE$Gr>Dn-upq%XJPc$J40AEa zFu>L7gUxpl{hkcMXy${=5Mf|o;9$u0j0}#R#2lK(z`)SOz`!7aVn%*xNkM5zv0h0U z*tEG3Wr<aot~Kp7<T`91!1BBL=pyzR6Yhl;Ogv{V&{cAf-A!oM<Y1QnwrZW*Wy_Y8 ztiJm|Wp%5}P3`Tyw~u<4RtQgsNS>S(WaPGEr+rxGj`JLvreUs?hs2JR?0z(5$K&m- zTaD|a1>;`%Tm2Ds{=2JB=&j2aMugi%y&{8!o2G?aVqjnhW@KOxMsa&`PG)Le3C!uY zBFhrbVmjS*f@9wy10L7!maR`~B|})wq&?$^S!mSht-RH@Fi1ekF#R~^+71!TU;lsK zEKCy2YUcTn&d{=cvv}EAm50w{y>*Nj?kfM$$#0NI+sLyj!F}UrvlCyI3da1IE%wVG z?f-xAvf00Wl+9DSzuZt;&LCWLo!8p;me(!vGT#YLYt)mRa@8`dyl>?yL*bU`ibe}R zCi|E~xg9vM=<W7=4PH4rcSWs`Xz%LX*1K)5w&&aJGH!AUcbO~~JX7iF?1Ryq8CQHu ze(F4MarP{^^L?S$Yqr9lcexS4^Tjzlj8CT3!iRx@A(nxGK@>H3&{ATw*!SdgF^rUW z`Rdis2dB<xYv^tadJq(FA+&&pzpuQctc;hh?|WHq&-aqXZnoW4>^s{OJd2)ApVnY{ zt#IkHrRmR>Mrlr2A||T3?5X(EqL(jU7BVsfcr!A|G2_Y{65wRa$iTp`r4htL$ttXn ztb&n@5e8yr28e+S3<3;)9TOQCP_hOq12fia0WuF)#(<ayao|!E^N_O%D+4nqn_zV* z#AM85i)?ZtBeF}85-}?SGbj<`F&Hz#kqvHOLNORQ`dJy6LD7$4G%Fh@B3Kz%8P+p0 JFi10lcmQ#daJ>Kk literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/docker.zip b/iem-api/tests/resources/docker.zip new file mode 100644 index 0000000000000000000000000000000000000000..888efb0b9697997713ed9d800eb25fcf8bf989c9 GIT binary patch literal 5400 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
lX&%#s*9Qw2m!z=FbU*l0<6}|H zspf>He}8qS99=!%#_B+_+y3J^vkz9Uy;fu_!C0pK!1&dUvLEqG4q_7(?lW85Uh;}n z-BIE88p)RH6{>3{CaEt-o0j@zdPkVo4g0M-Cua%EufA*49&=}LahGY__N286yWO=` zs%7^kgdOmcvfjWJb!>YOCr<@O5Yv&;+q_{RlkSO%%cWhP#_9A!VZtqsQv3U_?gh1^ zoHkqZNvEQs*jmH-O#Rh4rk|fKidge(zrjtL2cL^9k8l5!J}JsPXSuIv?)&I#CNBGh z`9qH{`53TV?2)DH;>M#FxP!%(H2Qq`$k}=!GnxO$WRFXdoyse9Qs$;yKc0OzBW<E! zin-)!m(Y}{cO^DV3_E#_ef{NKmOlIkc&>et^NuPjnN<C!T<mLpaLop(B(HgAqmwvZ zSD#h2GRnME(X;j4S#Q4iT{jwMADwut>8k0hYrmDgUsZm6@{a0N*C>r2AuDg@u+6uL zn$+fDIk~BM--0P}-&n49NjKi8m?FRAXVnhZTb5t<KE9RB-*R~y$J&XHT<_JE1|>Y$ zu}OWd=c>u4Z^<m2vc8+OHeN4$$=<NN*(z_?rIJs4tp2yG;Qk-hmgAxS-Yz)vZNs|g zbF4}wXEP_vFR4HDzW%Y<>gbhPPh4}I#oN{VDrPOdWEUd6xSh|`y54xk3iFh&K1*h# ziDlI_&bhzhP4fSrje?(QLUshV2B@SOnA!;l{QeVTVH+M4KF=UFyzq@$%?Z)xo>N(V z9(p_RQOuOZ84o`j?dEy^Sfq4r=k1e?iSdvB&su0~qvK$1|8HUc;xC=se=EN)Ut%}2 z#W2ft(NXn>$Aw=^+AioNG2L!i)?KIm?8sape%rmDN=lU9t?b%1;kDg+XXPu$^7w_q z_o@B1YE0hl`7-<3mynCj*E<*U^}A<fv`+l<MPu!befKZjdNcp7!0)}C$9A8s49XTS zc#u-~DyfmfIr%Qr>$cl&MVGv{zBFaqyiV_a?cX0SnPcMj?ce$9$Jgzz4qtsfTVLbr znpV&4&mQ)D{POm2ukFQRWos?h@&Z{kW((7VHn!<I%?!9Ky54`^>S%vgx6$SKQspRF zPfo$<a`#@Xc~;l$eXs0a+>CRnHW4io-{kf7pI^8)(q{qdBf(iu|BL2qtuk#CTlwGm zPLXA!Scg5En|TYOzJPRuTs{VUY-eC#@L^<N05u;Fr9^&dNkM6e9^rCgZm%~NlOa#r zcU#vP%pxW4Bb{^t50%WiB;HX`ep_%(q(;;2{~v3QW$11co1yZ=>7ZenU$5}7s&9`9 zo=IPmlU3Ux-Qc#*XpVz&|K8abUtaoe(>75h(fU?tzIj9CjQ6utdgH#=+e$xN?4h*n zsrt7^Q@dKS<Acw=N>SLoIR3uuJFklg<v$JV83Vi-nM9az_fi?4U`r#2fnKzO`l_H- z28hO}Eno&??}mbm1L41p6`(E=X`ND-(U90cZp?wYr66NLcuV79bfYmEcz8Og$Uy_D z8ju@apw1)63J~7X*uuzw+O0-wjDaixHN>#=gi$QP(nkbY0K!`unV8TlL5~bPJw_B; zu=MaiHh}P!##QLHfU7+s`g;f~uy-Ut#)I%*$7L)G43LHpq{#v5PT=fTAYvL@#{y&^ z2ybb8jW85FTtM9mP$C7<I1?!c1Ga7f$ZQba(%8!icQi&C0(A~R7Jz6x7GP_<gA51Z zEsY**a0}2<4=Ck;j0e$pjK|hS2N?~*TN)1_j7N!4JS}$Q7{%5|1{n;(e;wzr!`%*P z=Yktapr$g;CNm;lu{D}O#)9yc#^>lpLsAYYO=x6)U~2(`ECS)bj_n)_B(;YTp1{^3 z2AK)MTN=-!n2Vl1@wAT-PKT6q=&e(b@gTgVaTzDF)4@?pxDG-$9=%!v84bc)8qe?| b8&7m47vRmx1~Nd9L69Mije%haABYD4)j5y@ literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/dummy.zip b/iem-api/tests/resources/dummy.zip new file mode 100644 index 0000000000000000000000000000000000000000..20d2bc346232701a6a840e0834e7709fdc67bab3 GIT binary patch literal 3700 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 R2JkTOFodx%FmQki7y#hT4>te+ literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/openstack.zip b/iem-api/tests/resources/openstack.zip new file mode 100644 index 0000000000000000000000000000000000000000..7d2d24ddd670766af89958f489b8cfceff66c99a GIT binary patch literal 3735 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 HaDjLLJSJi( literal 0 HcmV?d00001 diff --git a/iem-api/tests/unit/__init__.py b/iem-api/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/unit/test_iem.py b/iem-api/tests/unit/test_iem.py new file mode 100644 index 0000000..97fa04d --- /dev/null +++ b/iem-api/tests/unit/test_iem.py @@ -0,0 +1,31 @@ +import logging +import os +import unittest +import uuid + +from src.core.iem import Iem +from src.core.utils import Aws, Credentials, Openstack + +LOGGER = logging.getLogger(__name__) + + +class TestIem(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestIem, self).__init__(*args, **kwargs) + + # Check IEM_HOME variable + self._iem_home = os.environ["IEM_HOME"] + + def test_get_all_deployments(self): + i = Iem() + all_deployments = i.get_all_deployments() + self.assertGreaterEqual(len(list(all_deployments)), 0) + + def test_get_deployment_ok(self): + i = Iem() + i.get_deployment(deployment_id="deployment2") + + def test_get_deployment_not_ok(self): + i = Iem() + deployment_id = str(uuid.uuid4()) + i.get_deployment(deployment_id=deployment_id) diff --git a/iem-api/tests/unit/test_main.py b/iem-api/tests/unit/test_main.py new file mode 100644 index 0000000..7d05433 --- /dev/null +++ b/iem-api/tests/unit/test_main.py @@ -0,0 +1,106 @@ +import base64 +import os +import unittest +import uuid +from unittest.mock import Mock, patch + +from fastapi.testclient import TestClient + +from main import app + + +class TestMain(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestMain, self).__init__(*args, **kwargs) + + self.client = TestClient(app) + + # Check API_KEY variable + self._api_key = os.environ["API_KEY"] + + def test_root_no_apikey(self): + response = self.client.get("/") + assert response.status_code == 403 + + def test_root(self): + response = self.client.get("/", headers={"x-api-key": self._api_key}) + assert response.status_code == 200 + + def test_get_all_deployments(self): + response = self.client.get( + f"/deployments/", headers={"x-api-key": self._api_key} + ) + assert response.status_code == 200 + + def test_get_one_deployment(self): + response = self.client.get( + f"/deployments/25/", headers={"x-api-key": self._api_key} + ) + assert response.status_code == 200 + + @patch("subprocess.run") + def test_deployments(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": str(uuid.uuid4()), + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + @patch("subprocess.run") + def test_undeploy(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + + deployment_id = str(uuid.uuid4()) + + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + response = self.client.post( + f"/undeploy/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": {}, + }, + ) + assert response.status_code == 202 diff --git a/iem-api/tests/unit/test_persistence.py b/iem-api/tests/unit/test_persistence.py new file mode 100644 index 0000000..6683e8b --- /dev/null +++ b/iem-api/tests/unit/test_persistence.py @@ -0,0 +1,32 @@ +import unittest +import uuid + +from src.core.persistence import Persistence + + +class TestPersistence(unittest.TestCase): + @classmethod + def setUpClass(self): + self.engine_url = "sqlite:///:memory:" + + def test_insert_deployment(self): + Persistence(engine_url=self.engine_url).insert_deployment( + "deployment1", "STARTED", "stdout", "stderr" + ) + + def test_get_deployment(self): + p = Persistence(engine_url=self.engine_url) + p.insert_deployment("deployment2", "STOPPED", "stdout", "stderr") + p.insert_deployment("deployment2", "STARTED", "stdout", "stderr") + d = p.get_deployment("deployment2") + self.assertEqual(d.status, "STARTED") + + def test_get_deployment_not_ok(self): + row = Persistence(engine_url=self.engine_url).get_deployment(str(uuid.uuid4())) + self.assertIsNone(row) + + def test_get_all_deployments(self): + persistence = Persistence(engine_url=self.engine_url) + persistence.insert_deployment("deployment3", "STARTED", "stdout", "stderr") + rows = persistence.get_all_deployments() + self.assertGreaterEqual(len(list(rows)), 1) -- GitLab