def test_failing_call(self): kubectl = Kubectl() cmd = ['ls', '-l', '/does/probably/not/exist'] with pytest.raises(KubectlCallFailed) as error: kubectl._call(cmd) assert error.value.args[0].endswith(b'No such file or directory\n')
def test_default_command(self): kubectl = Kubectl() default_cmd = kubectl._make_command() expected = ['kubectl', 'get', 'pods'] assert default_cmd == expected
def test_list_deployments(self, mock_call): expected = ['get', 'deployments', '-o', 'json'] expected_call = ['kubectl', '--namespace', 'test-space'] expected_call.extend(expected) kubectl = Kubectl() kubectl.namespace = 'test-space' kubectl.list_deployments() mock_call.assert_called_once_with(expected_call)
def test_get_deployment(self, mock_call): name = 'test-deployment' expected = ['get', 'deployment', name, '-o', 'json'] expected_call = ['kubectl', '--namespace', 'test-space'] expected_call.extend(expected) kubectl = Kubectl() kubectl.namespace = 'test-space' kubectl.get_deployment(name) mock_call.assert_called_once_with(expected_call)
def test_list_deployments_by_selector(self, mock_call): expected = ['get', 'deployments', '--selector', 'servicegroup=twyla,mylabel=myvalue', '-o', 'json'] expected_call = ['kubectl', '--namespace', 'test-space'] expected_call.extend(expected) kubectl = Kubectl() kubectl.namespace = 'test-space' kubectl.list_deployments(selectors={'servicegroup': 'twyla', 'mylabel': 'myvalue'}) mock_call.assert_called_once_with(expected_call)
def cluster_info(dump_to: str, group: str, namespace: str): kubectl = Kubectl() kubectl.namespace = namespace state = kubectl.list_deployments(selectors={'servicegroup': group}) print_cluster_info(state) if dump_to is not None: deployable = scrub_cluster_info(state) with open(dump_to, mode='w') as fd: fd.write(yaml.dump(deployable, default_flow_style=False))
def test_call(self, mock_json, mock_subprocess): mock_pipe = mock.MagicMock() mock_subprocess.PIPE = mock_pipe kubectl = Kubectl() cmd = ['kubectl', 'get', 'pods'] kubectl._call(cmd) mock_subprocess.run.assert_called_once_with( cmd, stdout=mock_pipe, stderr=mock_pipe)
def test_namespaced_command(self): kubectl = Kubectl() kubectl.namespace = 'twyla' default_cmd = kubectl._make_command() expected = ['kubectl', '--namespace', 'twyla', 'get', 'pods'] assert default_cmd == expected
def test_apply(self, mock_json, mock_subprocess): mock_pipe = mock.MagicMock() mock_subprocess.PIPE = mock_pipe kubectl = Kubectl() file_name = 'deployment.yml' expected = ['kubectl', 'apply', '-f', file_name] kubectl.apply(file_name) mock_subprocess.run.assert_called_once_with( expected, stdout=mock_pipe, stderr=mock_pipe)
def __init__(self, namespace: str, deployment_name: str, printer: Callable[[str], int], error_printer: Callable[[str], int], deployment_template: str=None, variants: List[str]=None): self.printer = printer self.error_printer = error_printer self.deployment_name = deployment_name self.deployment_template = deployment_template or 'deployment.yml' self.kubectl = Kubectl() self.kubectl.namespace = namespace self.variants = variants or []
def apply(from_file: str): # Load the deployments from file and get the current count of replicas in # the target cluster for each of the deployments. Then update the replicas # to match the target cluster. Save the file and pass on to kubectl apply. with open(from_file) as fd: content = fd.read() kube_list = yaml.load(content) kubectl = Kubectl() kubectl.update_replicas(kube_list) with open(from_file, mode='w') as fd: fd.write(yaml.dump(kube_list, default_flow_style=False)) try: lines = kubectl.apply(from_file) for line in lines.split('\n'): prompt(line) except KubectlCallFailed as e: error_prompt(str(e))
def test_make_selector_args(self): kubectl = Kubectl() res = kubectl._make_selector_args(None) assert res == [] res = kubectl._make_selector_args({}) assert res == [] res = kubectl._make_selector_args({'one': 'val'}) assert res == ['--selector', 'one=val'] res = kubectl._make_selector_args({'one': 'val', 'two': 'val2'}) assert res == ['--selector', 'one=val,two=val2']
def test_update_replicas_no_remote(self, mock_call): kube_list = json.loads(''' { "apiVersion": "v1", "items": [ { "apiVersion": "extensions/v1beta1", "kind": "Deployment", "metadata": { "labels": { "app": "test-service-one", "servicegroup": "twyla" }, "name": "test-service-one", "namespace": "twyla" }, "spec": { "replicas": 2, "selector": { "matchLabels": { "app": "test-service-one", "name": "test-service-one" } }, "strategy": { "rollingUpdate": { "maxSurge": "0%", "maxUnavailable": "100%" }, "type": "RollingUpdate" }, "template": { "metadata": { "creationTimestamp": null, "labels": { "app": "test-service-one", "name": "test-service-one" } }, "spec": { "containers": [ { "env": [ { "name": "TWYLA_CLUSTER_NAME", "valueFrom": { "configMapKeyRef": { "key": "cluster-name", "name": "cluster-vars" } } } ], "image": "twyla.azurecr.io/test-service-one:cc0cd960", "imagePullPolicy": "Always", "name": "test-service-one", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File" } ], "dnsPolicy": "ClusterFirst", "imagePullSecrets": [ { "name": "twyla-registry-login" } ], "restartPolicy": "Always", "schedulerName": "default-scheduler", "securityContext": {}, "terminationGracePeriodSeconds": 30 } } } }, { "apiVersion": "extensions/v1beta1", "kind": "Deployment", "metadata": { "labels": { "app": "test-service-two", "servicegroup": "twyla" }, "name": "test-service-two", "namespace": "twyla" }, "spec": { "progressDeadlineSeconds": 600, "replicas": 1, "revisionHistoryLimit": 2, "selector": { "matchLabels": { "app": "test-service-two" } }, "strategy": { "rollingUpdate": { "maxSurge": 1, "maxUnavailable": 1 }, "type": "RollingUpdate" }, "template": { "metadata": { "creationTimestamp": null, "labels": { "app": "test-service-two", "name": "test-service-two" } }, "spec": { "containers": [ { "env": [ { "name": "TWYLA_CLUSTER_NAME", "valueFrom": { "configMapKeyRef": { "key": "cluster-name", "name": "cluster-vars" } } } ], "image": "twyla.azurecr.io/test-service-two:c42d0ebe", "imagePullPolicy": "Always", "name": "test-service-two", "ports": [ { "containerPort": 5000, "protocol": "TCP" } ], "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File" } ], "dnsPolicy": "ClusterFirst", "imagePullSecrets": [ { "name": "twyla-registry-login" } ], "restartPolicy": "Always", "schedulerName": "default-scheduler", "securityContext": {}, "terminationGracePeriodSeconds": 30 } } } } ], "kind": "List", "metadata": { "resourceVersion": "", "selfLink": "" } } ''') dep1 = json.loads('''{ "apiVersion":"extensions/v1beta1", "kind":"Deployment", "metadata":{ "annotations":{ "deployment.kubernetes.io/revision":"89", "kubectl.kubernetes.io/last-applied-configuration":"{\\"apiVersion\\":\\"extensions/v1beta1\\",\\"kind\\":\\"Deployment\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"app\\":\\"test-service-one\\",\\"servicegroup\\":\\"twyla\\"},\\"name\\":\\"test-service-one\\",\\"namespace\\":\\"twyla\\"},\\"spec\\":{\\"progressDeadlineSeconds\\":600,\\"replicas\\":1,\\"revisionHistoryLimit\\":2,\\"selector\\":{\\"matchLabels\\":{\\"app\\":\\"test-service-one\\"}},\\"strategy\\":{\\"rollingUpdate\\":{\\"maxSurge\\":\\"50%\\",\\"maxUnavailable\\":\\"50%\\"},\\"type\\":\\"RollingUpdate\\"},\\"template\\":{\\"metadata\\":{\\"creationTimestamp\\":null,\\"labels\\":{\\"app\\":\\"test-service-one\\",\\"name\\":\\"test-service-one\\"}},\\"spec\\":{\\"containers\\":[{\\"env\\":[{\\"name\\":\\"TWYLA_CLUSTER_NAME\\",\\"valueFrom\\":{\\"configMapKeyRef\\":{\\"key\\":\\"cluster-name\\",\\"name\\":\\"cluster-vars\\"}}},{\\"name\\":\\"TWYLA_DOCUMENT_STORE_URI\\",\\"valueFrom\\":{\\"secretKeyRef\\":{\\"key\\":\\"twyla_document_store_string\\",\\"name\\":\\"document-store-secrets\\"}}}],\\"image\\":\\"twyla.azurecr.io/test-service-one:356dcef4\\",\\"imagePullPolicy\\":\\"Always\\",\\"name\\":\\"test-service-one\\",\\"resources\\":{},\\"terminationMessagePath\\":\\"/dev/termination-log\\",\\"terminationMessagePolicy\\":\\"File\\"}],\\"dnsPolicy\\":\\"ClusterFirst\\",\\"imagePullSecrets\\":[{\\"name\\":\\"twyla-registry-login\\"}],\\"restartPolicy\\":\\"Always\\",\\"schedulerName\\":\\"default-scheduler\\",\\"securityContext\\":{},\\"terminationGracePeriodSeconds\\":30}}}}\\n" }, "creationTimestamp":"2017-10-16T14:55:37Z", "generation":95, "labels":{ "app":"test-service-one", "servicegroup":"twyla" }, "name":"test-service-one", "namespace":"twyla", "resourceVersion":"16810663", "selfLink":"/apis/extensions/v1beta1/namespaces/twyla/deployments/test-service-one", "uid":"120abf54-b282-11e7-b58f-000d3a2bee3e" }, "spec":{ "progressDeadlineSeconds":600, "replicas":1, "revisionHistoryLimit":2, "selector":{ "matchLabels":{ "app":"test-service-one" } }, "strategy":{ "rollingUpdate":{ "maxSurge":"50%", "maxUnavailable":"50%" }, "type":"RollingUpdate" }, "template":{ "metadata":{ "creationTimestamp":null, "labels":{ "app":"test-service-one", "name":"test-service-one" } }, "spec":{ "containers":[ { "env":[ { "name":"TWYLA_CLUSTER_NAME", "valueFrom":{ "configMapKeyRef":{ "key":"cluster-name", "name":"cluster-vars" } } }, { "name":"TWYLA_DOCUMENT_STORE_URI", "valueFrom":{ "secretKeyRef":{ "key":"twyla_document_store_string", "name":"document-store-secrets" } } } ], "image":"twyla.azurecr.io/test-service-one:356dcef4", "imagePullPolicy":"Always", "name":"test-service-one", "resources":{ }, "terminationMessagePath":"/dev/termination-log", "terminationMessagePolicy":"File" } ], "dnsPolicy":"ClusterFirst", "imagePullSecrets":[ { "name":"twyla-registry-login" } ], "restartPolicy":"Always", "schedulerName":"default-scheduler", "securityContext":{ }, "terminationGracePeriodSeconds":30 } } }, "status":{ "availableReplicas":1, "conditions":[ { "lastTransitionTime":"2018-01-08T16:38:21Z", "lastUpdateTime":"2018-02-13T18:47:02Z", "message":"ReplicaSet \\"test-service-one-3114667387\\" has successfully progressed.", "reason":"NewReplicaSetAvailable", "status":"True", "type":"Progressing" }, { "lastTransitionTime":"2018-02-14T16:36:34Z", "lastUpdateTime":"2018-02-14T16:36:34Z", "message":"Deployment has minimum availability.", "reason":"MinimumReplicasAvailable", "status":"True", "type":"Available" } ], "observedGeneration":95, "readyReplicas":1, "replicas":1, "updatedReplicas":1 } }''') mock_call.side_effect = [ dep1, KubectlCallFailed ] # before (this makes the test more obvious) assert kube_list['items'][0]['spec']['replicas'] == 2 assert kube_list['items'][1]['spec']['replicas'] == 1 kubectl = Kubectl() kubectl.update_replicas(kube_list) # after assert kube_list['items'][0]['spec']['replicas'] == 1 assert kube_list['items'][1]['spec']['replicas'] == 1
class Kube: def __init__(self, namespace: str, deployment_name: str, printer: Callable[[str], int], error_printer: Callable[[str], int], deployment_template: str=None, variants: List[str]=None): self.printer = printer self.error_printer = error_printer self.deployment_name = deployment_name self.deployment_template = deployment_template or 'deployment.yml' self.kubectl = Kubectl() self.kubectl.namespace = namespace self.variants = variants or [] def get_remote_deployment(self): return self.kubectl.get_deployment(self.deployment_name) def apply(self, tag: str): # Load the deployment definition file_name = self.render_template(tag) output = self.kubectl.apply(file_name) for line in output.split('\n'): if line is not '': self.printer(line) def info(self): try: deployment = self.get_remote_deployment() self.print_deployment_info( 'Current {}'.format(self.deployment_name), deployment) except KubectlCallFailed as e: self.error_printer(self.exception(e)) def exception(self, e): return e.args[0].decode('utf8').strip() def print_deployment_info( self, title: str, deployment): info_template = Template(''' {{ meta.title }}: {% for c in deployment.spec.template.spec.containers %} name: {{ c.name }} image: {{ c.image }} {% endfor %} replicas: {{ deployment.status.readyReplicas }}/{{ deployment.status.replicas -}} ''') rendered = info_template.render(meta={'name': self.deployment_name, 'title': title}, deployment=deployment) for line in rendered.split('\n'): if line: self.printer(line) return def render_template(self, tag: str): jinja = Environment(loader=FileSystemLoader('./')) template = jinja.get_template(self.deployment_template) replicas = None try: deployment = self.get_remote_deployment() replicas = deployment['spec']['replicas'] # make sure to use the same number of replicas as remote to honor # scaling if deployment.get('status'): replicas = deployment['status']['replicas'] except KubectlCallFailed as e: self.error_printer(self.exception(e)) data = { 'image': tag, 'name': self.deployment_name, 'namespace': self.kubectl.namespace, 'replicas': replicas, 'variants': self.variants } rendered = template.render(data=data) tmp_file = tempfile.NamedTemporaryFile(delete=False) tmp_file.write(rendered.encode('utf8')) return tmp_file.name