Exemplo n.º 1
0
    def test_compile_plan_patch_with_diff(self):
        """Test a plan that patches all resources.

        To do this, the local and server resources are identical. As a
        result, the returned plan must nominate all manifests for patching, and
        none to create and delete.

        """
        # Create vanilla `Config` instance.
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # Define a single resource.
        meta = MetaManifest('v1', 'Namespace', None, 'ns1')

        # Local and server manifests have the same resources but their
        # definition differs. This will ensure a non-empty patch in the plan.
        loc_man = {meta: make_manifest("Namespace", None, "ns1")}
        srv_man = {meta: make_manifest("Namespace", None, "ns1")}
        loc_man[meta]["metadata"]["labels"] = {"foo": "foo"}
        srv_man[meta]["metadata"]["labels"] = {"bar": "bar"}

        # Compute the JSON patch and textual diff to populated the expected
        # output structure below.
        patch, err = square.make_patch(config, loc_man[meta], srv_man[meta])
        assert not err
        diff_str, err = manio.diff(config, loc_man[meta], srv_man[meta])
        assert not err

        # Verify the test function returns the correct Patch and diff.
        expected = DeploymentPlan(create=[],
                                  patch=[DeltaPatch(meta, diff_str, patch)],
                                  delete=[])
        assert square.compile_plan(config, loc_man,
                                   srv_man) == (expected, False)
Exemplo n.º 2
0
    def test_make_patch_err(self):
        """Verify error cases with invalid or incompatible manifests."""
        valid_cfg = k8s.Config("url", "token", "cert", "client_cert", "1.10")
        invalid_cfg = k8s.Config("url", "token", "cert", "client_cert",
                                 "invalid")

        # Create two valid manifests, then stunt one in such a way that
        # `manio.strip` will reject it.
        kind, namespace, name = "Deployment", "namespace", "name"
        valid = make_manifest(kind, namespace, name)
        invalid = make_manifest(kind, namespace, name)
        del invalid["kind"]

        # Must handle errors from `manio.strip`.
        assert square.make_patch(valid_cfg, valid, invalid) == (None, True)
        assert square.make_patch(valid_cfg, invalid, valid) == (None, True)
        assert square.make_patch(valid_cfg, invalid, invalid) == (None, True)

        # Must handle `urlpath` errors.
        assert square.make_patch(invalid_cfg, valid, valid) == (None, True)

        # 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 square.make_patch(valid_cfg, valid_a, valid_b) == (None, True)
Exemplo n.º 3
0
    def test_compile_plan_patch_no_diff(self):
        """Test a plan that patches all resources.

        To do this, the local and server resources are identical. As a
        result, the returned plan must nominate all manifests for patching, and
        none to create and delete.

        """
        # Create vanilla `Config` instance.
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # Allocate arrays for the MetaManifests.
        meta = [None] * 4

        # Define two Namespace with 1 deployment each.
        meta[0] = MetaManifest('v1', 'Namespace', None, 'ns1')
        meta[1] = MetaManifest('v1', 'Deployment', 'ns1', 'res_0')
        meta[2] = MetaManifest('v1', 'Namespace', None, 'ns2')
        meta[3] = MetaManifest('v1', 'Deployment', 'ns2', 'res_1')

        # Determine the K8s resource URLs for patching. Those URLs must contain
        # the resource name as the last path element, eg "/api/v1/namespaces/ns1"
        url = [
            urlpath(config, _.kind, _.namespace)[0] + f"/{_.name}"
            for _ in meta
        ]

        # Local and server manifests are identical. The plan must therefore
        # only nominate patches but nothing to create or delete.
        loc_man = srv_man = {
            meta[0]: make_manifest("Namespace", None, "ns1"),
            meta[1]: make_manifest("Deployment", "ns1", "res_0"),
            meta[2]: make_manifest("Namespace", None, "ns2"),
            meta[3]: make_manifest("Deployment", "ns2", "res_1"),
        }
        expected = DeploymentPlan(create=[],
                                  patch=[
                                      DeltaPatch(meta[0], "",
                                                 JsonPatch(url[0], [])),
                                      DeltaPatch(meta[1], "",
                                                 JsonPatch(url[1], [])),
                                      DeltaPatch(meta[2], "",
                                                 JsonPatch(url[2], [])),
                                      DeltaPatch(meta[3], "",
                                                 JsonPatch(url[3], [])),
                                  ],
                                  delete=[])
        assert square.compile_plan(config, loc_man,
                                   srv_man) == (expected, False)
Exemplo n.º 4
0
    def test_print_deltas(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 square.print_deltas(plan) == (None, False)
Exemplo n.º 5
0
    def test_compile_plan_err(self, m_patch, m_diff, m_part):
        """Use mocks for the internal function calls to simulate errors."""
        # Create vanilla `Config` instance.
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # Define a single resource and valid dummy return value for
        # `square.partition_manifests`.
        meta = MetaManifest('v1', 'Namespace', None, 'ns1')
        plan = DeploymentPlan(create=[], patch=[meta], delete=[])

        # Local and server manifests have the same resources but their
        # definition differs. This will ensure a non-empty patch in the plan.
        loc_man = srv_man = {meta: make_manifest("Namespace", None, "ns1")}

        # Simulate an error in `compile_plan`.
        m_part.return_value = (None, True)
        assert square.compile_plan(config, loc_man, srv_man) == (None, True)

        # Simulate an error in `diff`.
        m_part.return_value = (plan, False)
        m_diff.return_value = (None, True)
        assert square.compile_plan(config, loc_man, srv_man) == (None, True)

        # Simulate an error in `make_patch`.
        m_part.return_value = (plan, False)
        m_diff.return_value = ("some string", False)
        m_patch.return_value = (None, True)
        assert square.compile_plan(config, loc_man, srv_man) == (None, True)
Exemplo n.º 6
0
    def test_make_patch_incompatible(self):
        """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".

        """
        # Setup.
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # Demo manifest.
        srv = make_manifest('Deployment', 'Namespace', 'name')

        # `apiVersion` must match.
        loc = copy.deepcopy(srv)
        loc['apiVersion'] = 'mismatch'
        assert square.make_patch(config, loc, srv) == (None, True)

        # `kind` must match.
        loc = copy.deepcopy(srv)
        loc['kind'] = 'Mismatch'
        assert square.make_patch(config, loc, srv) == (None, True)

        # `name` must match.
        loc = copy.deepcopy(srv)
        loc['metadata']['name'] = 'mismatch'
        assert square.make_patch(config, loc, srv) == (None, True)

        # `namespace` must match.
        loc = copy.deepcopy(srv)
        loc['metadata']['namespace'] = 'mismatch'
        assert square.make_patch(config, loc, srv) == (None, True)
Exemplo n.º 7
0
    def test_make_patch_error_urlpath(self, m_url):
        """Coverage gap: simulate `urlpath` error."""
        # Setup.
        kind, ns, name = "Deployment", "ns", "foo"
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # Simulate `urlpath` error.
        m_url.return_value = (None, True)

        # Test function must return with error.
        loc = srv = make_manifest(kind, ns, name)
        assert square.make_patch(config, loc, srv) == (None, True)
Exemplo n.º 8
0
    def test_make_patch_ok(self):
        """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.

        """
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # 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.
        expected = JsonPatch(
            url=urlpath(config, kind, namespace)[0] + f"/{name}",
            ops=[],
        )
        assert square.make_patch(config, loc, loc) == (expected, False)

        # The patch between `srv` and `loc` must remove the old label and add
        # the new one.
        expected = JsonPatch(url=urlpath(config, kind, namespace)[0] +
                             f"/{name}",
                             ops=[{
                                 'op': 'remove',
                                 'path': '/metadata/labels/old'
                             }, {
                                 'op': 'add',
                                 'path': '/metadata/labels/new',
                                 'value': 'new'
                             }])
        assert square.make_patch(config, loc, srv) == (expected, False)
Exemplo n.º 9
0
    def test_make_patch_empty(self):
        """Basic test: compute patch between two identical resources."""
        # Setup.
        kind, ns, name = 'Deployment', 'ns', 'foo'
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")

        # PATCH URLs require the resource name at the end of the request path.
        url = urlpath(config, kind, ns)[0] + f'/{name}'

        # The patch must be empty for identical manifests.
        loc = srv = make_manifest(kind, ns, name)
        data, err = square.make_patch(config, loc, srv)
        assert (data, err) == (JsonPatch(url, []), False)
        assert isinstance(data, JsonPatch)
Exemplo n.º 10
0
    def test_make_patch_special(self):
        """Namespace, ClusterRole(Bindings) etc are special.

        What makes them special is that they exist outside namespaces.
        Therefore, they will/must not contain a `metadata.Namespace` attribute
        and require special treatment in `make_patch`.

        """
        # Generic fixtures; values are irrelevant.
        config = types.SimpleNamespace(url='http://examples.com/',
                                       version="1.10")
        name = "foo"

        for kind in ["Namespace", "ClusterRole"]:
            # Determine the resource path so we can verify it later.
            url = urlpath(config, kind, None)[0] + f'/{name}'

            # The patch between two identical manifests must be empty but valid.
            loc = srv = make_manifest(kind, None, name)
            assert square.make_patch(config, loc, srv) == ((url, []), False)

            # Create two almost identical manifests, except the second one has
            # the forbidden `metadata.namespace` attribute. This must fail.
            loc = make_manifest(kind, None, name)
            srv = copy.deepcopy(loc)
            loc['metadata']['namespace'] = 'foo'
            data, err = square.make_patch(config, loc, srv)
            assert data is None and err is not None

            # Create two almost identical manifests, except the second one has
            # different `metadata.labels`. This must succeed.
            loc = make_manifest(kind, None, name)
            srv = copy.deepcopy(loc)
            loc['metadata']['labels'] = {"key": "value"}

            data, err = square.make_patch(config, loc, srv)
            assert err is False and len(data) > 0
Exemplo n.º 11
0
    def test_compile_plan_create_delete_err(self, m_part):
        """Simulate `urlpath` errors"""
        # Invalid configuration. We will use it to trigger an error in `urlpath`.
        cfg_invalid = k8s.Config("url", "token", "cert", "cert", "invalid")

        # Valid ManifestMeta and dummy manifest dict.
        meta = manio.make_meta(make_manifest("Deployment", "ns", "name"))
        man = {meta: None}

        # Pretend we only have to "create" resources, and then trigger the
        # `urlpath` error in its code path.
        m_part.return_value = (
            DeploymentPlan(create=[meta], patch=[], delete=[]),
            False,
        )
        assert square.compile_plan(cfg_invalid, man, man) == (None, True)

        # Pretend we only have to "delete" resources, and then trigger the
        # `urlpath` error in its code path.
        m_part.return_value = (
            DeploymentPlan(create=[], patch=[], delete=[meta]),
            False,
        )
        assert square.compile_plan(cfg_invalid, man, man) == (None, True)
Exemplo n.º 12
0
    def test_main_patch(self, m_delete, m_patch, m_post, m_plan, m_prun,
                        m_down, m_load):
        """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 client config and MetaManifest.
        config = k8s.Config("url", "token", "ca_cert", "client_cert", "1.10")
        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 deployment plan.
        plan = DeploymentPlan(
            create=[DeltaCreate(meta, "create_url", "create_man")],
            patch=[DeltaPatch(meta, "diff", patch)],
            delete=[DeltaDelete(meta, "delete_url", "delete_man")],
        )

        # Pretend that all K8s requests succeed.
        m_down.return_value = ({}, False)
        m_load.return_value = ({}, {}, False)
        m_prun.side_effect = ["local", "server"]
        m_plan.return_value = (plan, False)
        m_post.return_value = (None, False)
        m_patch.return_value = (None, False)
        m_delete.return_value = (None, False)

        # The arguments to the test function will always be the same in this test.
        args = config, "client", "folder", "kinds", "ns"

        # Update the K8s resources and verify that the test functions made the
        # corresponding calls to K8s.
        assert square.main_patch(*args) == (None, False)
        m_load.assert_called_once_with("folder")
        m_down.assert_called_once_with(config, "client", "kinds", "ns")
        m_plan.assert_called_once_with(config, "local", "server")
        m_post.assert_called_once_with("client", "create_url", "create_man")
        m_patch.assert_called_once_with("client", patch.url, patch.ops)
        m_delete.assert_called_once_with("client", "delete_url", "delete_man")

        # -----------------------------------------------------------------
        #                   Simulate Error Scenarios
        # -----------------------------------------------------------------
        # Make `delete` fail.
        m_prun.side_effect = ["local", "server"]
        m_delete.return_value = (None, True)
        assert square.main_patch(*args) == (None, True)

        # Make `patch` fail.
        m_prun.side_effect = ["local", "server"]
        m_patch.return_value = (None, True)
        assert square.main_patch(*args) == (None, True)

        # Make `post` fail.
        m_prun.side_effect = ["local", "server"]
        m_post.return_value = (None, True)
        assert square.main_patch(*args) == (None, True)

        # Make `compile_plan` fail.
        m_prun.side_effect = ["local", "server"]
        m_plan.return_value = (None, True)
        assert square.main_patch(*args) == (None, True)

        # Make `download_manifests` fail.
        m_down.return_value = (None, True)
        assert square.main_patch(*args) == (None, True)

        # Make `load` fail.
        m_load.return_value = (None, None, True)
        assert square.main_patch(*args) == (None, True)
Exemplo n.º 13
0
    def test_prune(self):
        """Prune a list of manifests based on namespace and resource."""
        # Convenience.
        mm, prune = manio.make_meta, square.prune

        # The list of manifests we will use in this test.
        man = {
            mm(make_manifest("Namespace", None, "ns0")):
            "ns0",
            mm(make_manifest("Namespace", None, "ns1")):
            "ns1",
            mm(make_manifest("Deployment", "ns0", "d_ns0")):
            "d_ns0",
            mm(make_manifest("Deployment", "ns1", "d_ns1")):
            "d_ns1",
            mm(make_manifest("Service", "ns0", "s_ns0")):
            "s_ns0",
            mm(make_manifest("Service", "ns1", "s_ns1")):
            "s_ns1",
            mm(make_manifest("Secret", "ns1", "valid")):
            "valid-secret",

            # K8s creates those automatically and we will not touch it.
            mm(make_manifest("Secret", "ns1", "default-token-12345")):
            "ignore",
        }

        # Ask for the Service manifests in all namespaces.
        expected = {
            mm(make_manifest("Service", "ns0", "s_ns0")): "s_ns0",
            mm(make_manifest("Service", "ns1", "s_ns1")): "s_ns1",
        }
        assert prune(man, ["Service"], None) == expected
        assert prune(man, ["Service"], ["ns0", "ns1"]) == expected

        # Ask for the Namespace-, Deployment and Secret manifests in "ns1"
        # only. This one must contain the "valid" secret but not the
        # "default-token-12345" secret.
        expected = {
            mm(make_manifest("Namespace", None, "ns1")): "ns1",
            mm(make_manifest("Deployment", "ns1", "d_ns1")): "d_ns1",
            mm(make_manifest("Secret", "ns1", "valid")): "valid-secret",
        }
        assert prune(man, ["Namespace", "Deployment", "Secret"],
                     ["ns1"]) == expected