diff --git a/.gitignore b/.gitignore index 5e74d1f..d9dd8a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ *.egg-info build/ +*.pyc diff --git a/bin/last_deploy.py b/bin/last_deploy.py new file mode 100644 index 0000000..dca74ab --- /dev/null +++ b/bin/last_deploy.py @@ -0,0 +1,42 @@ +import os +from datetime import datetime, timedelta + +from cad.datasources.jenkins import api +from cad.datasources.jenkins.queries import JenkinsLastSuccessfulParameterizedBuildQuery +from cad.engine.executor import Executor +from cad.heuristics.deploy import Deploy +from cad.heuristics.evaulators import SingleValueThresholdEvaluator + +deploy_playbook = Deploy( + last_deploy=SingleValueThresholdEvaluator( + name='LastDeploy < 24 hours!', + query=JenkinsLastSuccessfulParameterizedBuildQuery( + client=api.Client( + username=os.environ['JENKINS_USERNAME'], + password=os.environ['JENKINS_PASSWORD'], + job_build_url=os.environ['JENKINS_JOB_BUILD_URL'] + ), + match={ + 'ENV': os.environ['DEPLOY_ENV'], + 'REGION': os.environ['DEPLOY_REGION'], + 'SERVICE': os.environ['DEPLOY_SERVICE'] + }, + ), + comparator=lambda x: x >= datetime.now() - timedelta(hours=24) + ) +) + +if __name__ == '__main__': + import logging + import sys + + root = logging.getLogger() + root.setLevel(logging.DEBUG) + + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + root.addHandler(handler) + + Executor(deploy_playbook).run() diff --git a/cad/datasources/__init__.py b/cad/datasources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cad/datasources/jenkins/__init__.py b/cad/datasources/jenkins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cad/datasources/jenkins/api.py b/cad/datasources/jenkins/api.py new file mode 100644 index 0000000..0c482b8 --- /dev/null +++ b/cad/datasources/jenkins/api.py @@ -0,0 +1,45 @@ +from datetime import datetime + +import requests + + +BUILD_SUCCESS = 'SUCCESS' +PARAMETERS_ACTION = 'hudson.model.ParametersAction' + + +class Build: + def __init__(self, data): + self.data = data + + def is_success(self): + return self.data['result'] == BUILD_SUCCESS + + def params(self): + # can a job have multiple params???? + for action in self.data.get('actions', []): + if action['_class'] == PARAMETERS_ACTION: + return {p['name']:p['value'] for p in action.get('parameters', [])} + return {} + + def datetime(self): + return datetime.fromtimestamp(self.data['timestamp'] / 1000) + + +class Client: + def __init__(self, username, password, job_build_url, requests=None): + self.username = username + self.password = password + self.job_build_url = job_build_url + self.requests = requests if requests is None else requests + + def builds(self): + query = { + 'tree': 'builds[actions[parameters[name,value]],number,status,timestamp,id,result,parameter]' + } + resp = requests.get( + self.job_build_url, + params=query, + auth=(self.username, self.password) + ) + assert resp.status_code == 200, resp.status_code + return [Build(b) for b in resp.json().get('builds')] diff --git a/cad/datasources/jenkins/queries.py b/cad/datasources/jenkins/queries.py new file mode 100644 index 0000000..985868e --- /dev/null +++ b/cad/datasources/jenkins/queries.py @@ -0,0 +1,27 @@ +import logging + +logger = logging.getLogger(__name__) + + +class JenkinsLastSuccessfulParameterizedBuildQuery: + def __init__(self, client, match={}): + self.c = client + self.match = match + + def result(self): + builds = self.c.builds() + result = [] + for build in builds: + if build.is_success(): + # check to see if this build matches the match dict + # get all keys from match + build_params = build.params() + params_to_check = {k:build_params[k] for k in self.match.keys()} + if self.match == params_to_check: + logger.debug({'match': params_to_check}) + result = [build.datetime()] + break + + logger.debug({'result': result}) + return result + diff --git a/cad/engine/executor.py b/cad/engine/executor.py index 327a18c..6321729 100644 --- a/cad/engine/executor.py +++ b/cad/engine/executor.py @@ -1,6 +1,6 @@ import logging -from cad.heuristics.nodes import End +from cad.heuristics.nodes import End, Alert ROOT_NODE = 0 EDGE_TO = 1 diff --git a/tests/__init__.py b/tests/__init__.py index 9c9b589..365428f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,4 +8,4 @@ handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) -root.addHandler(handler) \ No newline at end of file +root.addHandler(handler) diff --git a/tests/datasources/__init__.py b/tests/datasources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/datasources/jenkins/__init__.py b/tests/datasources/jenkins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/datasources/jenkins/test_client.py b/tests/datasources/jenkins/test_client.py new file mode 100644 index 0000000..e32ba3e --- /dev/null +++ b/tests/datasources/jenkins/test_client.py @@ -0,0 +1,75 @@ +import unittest + +from cad.datasources.jenkins.api import Build + + +class APIClientTestCase(unittest.TestCase): + + def test_client_invalid_status_code(self): + self.fail() + + def test_client_returns_builds(self): + self.fail() + + +class BuildTestCase(unittest.TestCase): + + def test_is_success_success(self): + self.assertTrue( + Build( + data={ + 'id': '34', + 'number': 34, + 'result': 'SUCCESS', + 'timestamp': 1536267817556 + } + ) + ) + + def test_build_params_no_actions(self): + self.fail() + + def test_build_params_no_parameters(self): + self.fail() + + def test_build_params_has_params(self): + self.assertEqual( + { + 'AMI': '', + 'ENV': 'staging', + 'REGION': 'us-west-2', + 'SERVICE': 'my-service' + }, + Build( + data={ + '_class': 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + 'actions': [ + { + '_class': 'hudson.model.ParametersAction', + 'parameters': [ + { + '_class': 'hudson.model.StringParameterValue', + 'name': 'ENV', + 'value': 'staging' + }, + { + '_class': 'hudson.model.StringParameterValue', + 'name': 'REGION', + 'value': 'us-west-2' + }, + { + '_class': 'hudson.model.StringParameterValue', + 'name': 'SERVICE', + 'value': 'my-service' + }, + { + '_class': 'hudson.model.StringParameterValue', + 'name': 'AMI', + 'value': '' + } + ] + } + ] + } + ).params() + ) diff --git a/tests/datasources/jenkins/test_queries.py b/tests/datasources/jenkins/test_queries.py new file mode 100644 index 0000000..486ccfa --- /dev/null +++ b/tests/datasources/jenkins/test_queries.py @@ -0,0 +1,50 @@ +import unittest +from datetime import datetime +from unittest.mock import MagicMock + +from cad.datasources.jenkins.api import Build +from cad.datasources.jenkins.queries import JenkinsLastSuccessfulParameterizedBuildQuery + + +class JenkinsLastSuccessfulParameterizedBuildQueryTestCase(unittest.TestCase): + def test_result_builds_no_success(self): + self.fail() + + def test_result_builds_no_match(self): + self.fail() + + def test_result_builds_match(self): + client = MagicMock() + client.builds.return_value = [ + Build( + data={ + 'id': '34', + 'number': 34, + 'result': 'SUCCESS', + 'timestamp': 1536267817556, + '_class': 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + 'actions': [ + { + '_class': 'hudson.model.ParametersAction', + 'parameters': [ + { + '_class': 'hudson.model.StringParameterValue', + 'name': 'SERVICE', + 'value': 'test-service' + }, + ] + } + ] + + } + ) + ] + self.assertEqual( + [datetime(2018, 9, 6, 17, 3, 37, 556000)], + JenkinsLastSuccessfulParameterizedBuildQuery( + client=client, + match={ + 'SERVICE': 'test-service' + } + ).result() + )