def test_make_patch_ok(self, config, k8sconfig): """Compute patch between two manifests. This test function first verifies that the patch between two identical manifests is empty. The second used two manifests that have different labels. This must produce two patch operations, one to remove the old label and one to add the new ones. """ # Two valid manifests. kind, namespace, name = "Deployment", "namespace", "name" srv = make_manifest(kind, namespace, name) loc = make_manifest(kind, namespace, name) srv["metadata"]["labels"] = {"old": "old"} loc["metadata"]["labels"] = {"new": "new"} # The Patch between two identical manifests must be a No-Op. res, err = resource(k8sconfig, MetaManifest("apps/v1", kind, namespace, name)) assert not err expected = JsonPatch(url=res.url, ops=[]) assert sq.make_patch(config, k8sconfig, loc, loc) == (expected, False) # The patch between `srv` and `loc` must remove the old label and add # the new one. expected = JsonPatch(url=res.url, ops=[{ 'op': 'remove', 'path': '/metadata/labels/old' }, { 'op': 'add', 'path': '/metadata/labels/new', 'value': 'new' }]) assert sq.make_patch(config, k8sconfig, loc, srv) == (expected, False)
def make_patch( config: Config, k8sconfig: K8sConfig, local: dict, server: dict) -> Tuple[JsonPatch, bool]: """Return JSON patch to transition `server` to `local`. Inputs: config: Square configuration. k8sconfig: K8sConfig local: dict Usually on fo the dict manifests returned by `load_manifest`. server: dict Usually on fo the dict manifests returned by `manio.download`. Returns: Patch: the JSON patch and human readable diff in a `Patch` tuple. """ # Convenience. loc, srv = local, server meta = manio.make_meta(local) # Log the manifest info for which we will try to compute a patch. man_id = f"{meta.kind}: {meta.namespace}/{meta.name}" logit.debug(f"Making patch for {man_id}") del meta # Sanity checks: abort if the manifests do not specify the same resource. try: res_srv, err_srv = k8s.resource(k8sconfig, manio.make_meta(srv)) res_loc, err_loc = k8s.resource(k8sconfig, manio.make_meta(loc)) assert err_srv is err_loc is False assert res_srv == res_loc except AssertionError: # Log the invalid manifests and return with an error. keys = ("apiVersion", "kind", "metadata") loc_tmp = {k: loc[k] for k in keys} srv_tmp = {k: srv[k] for k in keys} logit.error( "Cannot compute JSON patch for incompatible manifests. " f"Local: <{loc_tmp}> Server: <{srv_tmp}>" ) return (JsonPatch("", []), True) # Compute JSON patch. patch = jsonpatch.make_patch(srv, loc) patch = json.loads(patch.to_string()) # Return the patch. return (JsonPatch(res_srv.url, patch), False)
def test_show_plan(self): """Just verify it runs. There is nothing really to tests here because the function only prints strings to the terminal. Therefore, we will merely ensure that all code paths run without error. """ meta = manio.make_meta(make_manifest("Deployment", "ns", "name")) patch = JsonPatch( url="url", ops=[{ 'op': 'remove', 'path': '/metadata/labels/old' }, { 'op': 'add', 'path': '/metadata/labels/new', 'value': 'new' }], ) plan = DeploymentPlan( create=[DeltaCreate(meta, "url", "manifest")], patch=[ DeltaPatch(meta, "", patch), DeltaPatch(meta, " normal\n+ add\n- remove", patch) ], delete=[DeltaDelete(meta, "url", "manifest")], ) assert sq.show_plan(plan) is False
def test_make_patch_incompatible(self, config, k8sconfig): """Must not try to compute diffs for incompatible manifests. For instance, refuse to compute a patch when one manifest has kind "Namespace" and the other "Deployment". The same is true for "apiVersion", "metadata.name" and "metadata.namespace". """ # Demo Deployment manifest. srv = make_manifest('Deployment', 'Namespace', 'name') err_resp = (JsonPatch("", []), True) # `apiVersion` must match. loc = copy.deepcopy(srv) loc['apiVersion'] = 'mismatch' assert sq.make_patch(config, k8sconfig, loc, srv) == err_resp # `kind` must match. loc = copy.deepcopy(srv) loc['kind'] = 'Mismatch' assert sq.make_patch(config, k8sconfig, loc, srv) == err_resp # `name` must match. loc = copy.deepcopy(srv) loc['metadata']['name'] = 'mismatch' assert sq.make_patch(config, k8sconfig, loc, srv) == err_resp # `namespace` must match. loc = copy.deepcopy(srv) loc['metadata']['namespace'] = 'mismatch' assert sq.make_patch(config, k8sconfig, loc, srv) == err_resp
def test_make_patch_error_resource(self, m_url, config, k8sconfig): """Coverage gap: simulate `resource` error.""" # Simulate `resource` error. m_url.return_value = (None, True) # Test function must return with error. loc = srv = make_manifest("Deployment", "ns", "foo") assert sq.make_patch(config, k8sconfig, loc, srv) == (JsonPatch("", []), True)
def test_make_patch_empty(self, config, k8sconfig): """Basic test: compute patch between two identical resources.""" # Setup. kind, ns, name = 'Deployment', 'ns', 'foo' # PATCH URLs require the resource name at the end of the request path. url = resource(k8sconfig, MetaManifest("apps/v1", kind, ns, name))[0].url # The patch must be empty for identical manifests. loc = srv = make_manifest(kind, ns, name) data, err = sq.make_patch(config, k8sconfig, loc, srv) assert (data, err) == (JsonPatch(url, []), False) assert isinstance(data, JsonPatch)
def test_make_patch_err(self, config, k8sconfig): """Verify error cases with invalid or incompatible manifests.""" err_resp = (JsonPatch("", []), True) kind, namespace, name = "Deployment", "namespace", "name" valid = make_manifest(kind, namespace, name) # Must handle `resource` errors. with mock.patch.object(sq.k8s, "resource") as m_url: m_url.return_value = (None, True) assert sq.make_patch(config, k8sconfig, valid, valid) == err_resp # Must handle incompatible manifests, ie manifests that do not belong # to the same resource. valid_a = make_manifest(kind, namespace, "bar") valid_b = make_manifest(kind, namespace, "foo") assert sq.make_patch(config, k8sconfig, valid_a, valid_b) == err_resp
def test_apply_plan(self, m_delete, m_apply, m_post, config, kube_creds): """Simulate a successful resource update (add, patch, delete). To this end, create a valid (mocked) deployment plan, mock out all calls, and verify that all the necessary calls are made. The second part of the test simulates errors. This is not a separate test because it shares virtually all the boiler plate code. """ # Valid MetaManifest. meta = manio.make_meta(make_manifest("Deployment", "ns", "name")) # Valid Patch. patch = JsonPatch( url="patch_url", ops=[ { 'op': 'remove', 'path': '/metadata/labels/old' }, { 'op': 'add', 'path': '/metadata/labels/new', 'value': 'new' }, ], ) # Valid non-empty deployment plan. plan = DeploymentPlan( create=[DeltaCreate(meta, "create_url", "create_man")], patch=[DeltaPatch(meta, "diff", patch)], delete=[DeltaDelete(meta, "delete_url", "delete_man")], ) def reset_mocks(): m_post.reset_mock() m_apply.reset_mock() m_delete.reset_mock() # Pretend that all K8s requests succeed. m_post.return_value = (None, False) m_apply.return_value = (None, False) m_delete.return_value = (None, False) # Update the K8s resources and verify that the test functions made the # corresponding calls to K8s. reset_mocks() assert sq.apply_plan(config, plan) is False m_post.assert_called_once_with("k8s_client", "create_url", "create_man") m_apply.assert_called_once_with("k8s_client", patch.url, patch.ops) m_delete.assert_called_once_with("k8s_client", "delete_url", "delete_man") # ----------------------------------------------------------------- # Simulate An Empty Plan # ----------------------------------------------------------------- # Repeat the test and ensure the function does not even ask for # confirmation if the plan is empty. reset_mocks() empty_plan = DeploymentPlan(create=[], patch=[], delete=[]) # Call test function and verify that it did not try to apply # the empty plan. assert sq.apply_plan(config, empty_plan) is False assert not m_post.called assert not m_apply.called assert not m_delete.called # ----------------------------------------------------------------- # Simulate Error Scenarios # ----------------------------------------------------------------- reset_mocks() # Make `delete` fail. m_delete.return_value = (None, True) assert sq.apply_plan(config, plan) is True # Make `patch` fail. m_apply.return_value = (None, True) assert sq.apply_plan(config, plan) is True # Make `post` fail. m_post.return_value = (None, True) assert sq.apply_plan(config, plan) is True
def test_apply_plan(self, m_apply, m_plan, config): """Simulate a successful resource update (add, patch delete). To this end, create a valid (mocked) deployment plan, mock out all calls, and verify that all the necessary calls are made. The second part of the test simulates errors. This is not a separate test because it shares virtually all the boiler plate code. """ fun = main.apply_plan # ----------------------------------------------------------------- # Simulate A Non-Empty Plan # ----------------------------------------------------------------- # Valid Patch. patch = JsonPatch( url="patch_url", ops=[ {'op': 'remove', 'path': '/metadata/labels/old'}, {'op': 'add', 'path': '/metadata/labels/new', 'value': 'new'}, ], ) # Valid non-empty deployment plan. meta = manio.make_meta(make_manifest("Deployment", "ns", "name")) plan = DeploymentPlan( create=[DeltaCreate(meta, "create_url", "create_man")], patch=[DeltaPatch(meta, "diff", patch)], delete=[DeltaDelete(meta, "delete_url", "delete_man")], ) # Simulate a none empty plan and successful application of that plan. m_plan.return_value = (plan, False) m_apply.return_value = False # Function must not apply the plan without the user's confirmation. with mock.patch.object(main, 'input', lambda _: "no"): assert fun(config, "yes") is True assert not m_apply.called # Function must apply the plan if the user confirms it. with mock.patch.object(main, 'input', lambda _: "yes"): assert fun(config, "yes") is False m_apply.assert_called_once_with(config, plan) # Repeat with disabled security question. m_apply.reset_mock() assert fun(config, None) is False m_apply.assert_called_once_with(config, plan) # ----------------------------------------------------------------- # Simulate An Empty Plan # ----------------------------------------------------------------- # Function must not even ask for confirmation if the plan is empty. m_apply.reset_mock() m_plan.return_value = (DeploymentPlan(create=[], patch=[], delete=[]), False) with mock.patch.object(main, 'input', lambda _: "yes"): assert fun(config, "yes") is False assert not m_apply.called # ----------------------------------------------------------------- # Simulate Error Scenarios # ----------------------------------------------------------------- # Make `apply_plan` fail. m_plan.return_value = (plan, False) m_apply.return_value = (None, True) assert fun(config, None) is True