Ejemplo n.º 1
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)
Ejemplo n.º 2
0
    def test_main_nonzero_exit_on_error(self, m_patch, m_diff, m_get, m_k8s):
        """Simulate sane program invocation.

        This test verifies that the bootstrapping works and the correct
        `main_*` function will be called with the correct parameters. However,
        each of those `main_*` functions returns with an error which means
        `square.main` must return with a non-zero exit code.

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

        # Mock all calls to the K8s API.
        m_k8s.load_auto_config.return_value = config
        m_k8s.session.return_value = "client"
        m_k8s.version.return_value = (config, False)

        # Pretend all main functions return errors.
        m_get.return_value = (None, True)
        m_diff.return_value = (None, True)
        m_patch.return_value = (None, True)

        # Simulate all input options.
        for option in ["get", "diff", "patch"]:
            with mock.patch("sys.argv", ["square.py", option, "ns"]):
                assert square.main() == 1
Ejemplo n.º 3
0
    def test_main_valid_options(self, m_patch, m_diff, m_get, m_k8s):
        """Simulate sane program invocation.

        This test verifies that the bootstrapping works and the correct
        `main_*` function will be called with the correct parameters.

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

        # Mock all calls to the K8s API.
        m_k8s.load_auto_config.return_value = config
        m_k8s.session.return_value = "client"
        m_k8s.version.return_value = (config, False)

        # Pretend all main functions return success.
        m_get.return_value = (None, False)
        m_diff.return_value = (None, False)
        m_patch.return_value = (None, False)

        # Simulate all input options.
        for option in ["get", "diff", "patch"]:
            args = ("square.py", option, "deployment", "service", "--folder",
                    "myfolder")
            with mock.patch("sys.argv", args):
                square.main()

        # Every main function must have been called exactly once.
        args = config, "client", "myfolder", ["Deployment", "Service"], None
        m_get.assert_called_once_with(*args)
        m_diff.assert_called_once_with(*args)
        m_patch.assert_called_once_with(*args)
Ejemplo n.º 4
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)
Ejemplo n.º 5
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)
Ejemplo 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)
Ejemplo 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)
Ejemplo n.º 8
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)
Ejemplo n.º 9
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)
Ejemplo n.º 10
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)
Ejemplo n.º 11
0
    def test_main_invalid_option_in_main(self, m_cmd, m_k8s):
        """Simulate an option that `square` does not know about.

        This is a somewhat pathological test and exists primarily to close a
        harmless gap in the test coverage.

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

        # Mock all calls to the K8s API.
        m_k8s.load_auto_config.return_value = config
        m_k8s.session.return_value = "client"
        m_k8s.version.return_value = (config, False)

        # Pretend all main functions return errors.
        m_cmd.return_value = types.SimpleNamespace(
            verbosity=0,
            parser="invalid",
            kubeconfig="conf",
            ctx="ctx",
        )
        assert square.main() == 1
Ejemplo n.º 12
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)
Ejemplo n.º 13
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)
Ejemplo n.º 14
0
    def test_compile_plan_create_delete_ok(self):
        """Test a plan that creates and deletes resource, but not patches any.

        To do this, the local and server resources are all distinct. As a
        result, the returned plan must dictate that all local resources shall
        be created, all server resources deleted, and none patched.

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

        # Allocate arrays for the MetaManifests and resource URLs.
        meta = [None] * 5
        url = [None] * 5

        # Define Namespace "ns1" with 1 deployment.
        meta[0] = MetaManifest('v1', 'Namespace', None, 'ns1')
        meta[1] = MetaManifest('v1', 'Deployment', 'ns1', 'res_0')

        # Define Namespace "ns2" with 2 deployments.
        meta[2] = MetaManifest('v1', 'Namespace', None, 'ns2')
        meta[3] = MetaManifest('v1', 'Deployment', 'ns2', 'res_1')
        meta[4] = MetaManifest('v1', 'Deployment', 'ns2', 'res_2')

        # Determine the K8s resource urls for those that will be added.
        upb = urlpath
        url[0] = upb(config, meta[0].kind, meta[0].namespace)[0]
        url[1] = upb(config, meta[1].kind, meta[1].namespace)[0]

        # Determine the K8s resource URLs for those that will be deleted. They
        # are slightly different because DELETE requests expect a URL path that
        # ends with the resource, eg
        # "/api/v1/namespaces/ns2"
        # instead of
        # "/api/v1/namespaces".
        url[2] = upb(config, meta[2].kind,
                     meta[2].namespace)[0] + "/" + meta[2].name
        url[3] = upb(config, meta[3].kind,
                     meta[3].namespace)[0] + "/" + meta[3].name
        url[4] = upb(config, meta[4].kind,
                     meta[4].namespace)[0] + "/" + meta[4].name

        # Compile local and server manifests that have no resource overlap.
        # This will ensure that we have to create all the local resources,
        # delete all the server resources and path nothing.
        loc_man = {meta[0]: "0", meta[1]: "1"}
        srv_man = {meta[2]: "2", meta[3]: "3", meta[4]: "4"}

        # The resources require a manifest to specify the terms of deletion.
        # This is currently hard coded into the function.
        del_opts = {
            "apiVersion": "v1",
            "kind": "DeleteOptions",
            "gracePeriodSeconds": 0,
            "orphanDependents": False,
        }

        # Resources from local files must be created, resources on server must
        # be deleted.
        expected = DeploymentPlan(
            create=[
                DeltaCreate(meta[0], url[0], loc_man[meta[0]]),
                DeltaCreate(meta[1], url[1], loc_man[meta[1]]),
            ],
            patch=[],
            delete=[
                DeltaDelete(meta[2], url[2], del_opts),
                DeltaDelete(meta[3], url[3], del_opts),
                DeltaDelete(meta[4], url[4], del_opts),
            ],
        )
        assert square.compile_plan(config, loc_man,
                                   srv_man) == (expected, False)