示例#1
0
    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
示例#2
0
    def test_compile_plan_patch_with_diff(self, config, k8sconfig):
        """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.

        """
        # 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 populate the expected
        # output structure below.
        patch, err = sq.make_patch(config, k8sconfig, loc_man[meta],
                                   srv_man[meta])
        assert not err
        diff_str, err = manio.diff(config, k8sconfig, 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=[])
        ret = sq.compile_plan(config, k8sconfig, loc_man, srv_man)
        assert ret == (expected, False)
示例#3
0
def compile_plan(
        config: Config,
        k8sconfig: K8sConfig,
        local: ServerManifests,
        server: ServerManifests) -> Tuple[DeploymentPlan, bool]:
    """Return the `DeploymentPlan` to transition K8s to the `local` state.

    The deployment plan is a named tuple. It specifies which resources to
    create, patch and delete to ensure that the state of K8s matches that
    specified in `local`.

    Inputs:
        config: Square configuration.
        k8sconfig: K8sConfig
        local: ServerManifests
            Should be output from `load_manifest` or `load`.
        server: ServerManifests
            Should be output from `manio.download`.

    Returns:
        DeploymentPlan

    """
    err_resp = (DeploymentPlan(tuple(), tuple(), tuple()), True)

    # Abort unless all local manifests reference valid K8s resource kinds.
    if any([k8s.resource(k8sconfig, meta)[1] for meta in local]):
        return err_resp

    # Replace the server resources fetched from the K8s preferred endpoint with
    # those fetched from the endpoint used by the local manifest.
    server, err = match_api_version(k8sconfig, local, server)
    assert not err

    # Apply the filters to all local and server manifests before we compute patches.
    server = {
        meta: manio.strip(k8sconfig, man, config.filters)
        for meta, man in server.items()
    }
    local = {
        meta: manio.strip(k8sconfig, man, config.filters)
        for meta, man in local.items()
    }

    # Abort if any of the manifests could not be stripped.
    err_srv = {_[2] for _ in server.values()}
    err_loc = {_[2] for _ in local.values()}
    if True in err_srv or True in err_loc:
        logit.error("Could not strip all manifests.")
        return err_resp

    # Unpack the stripped manifests (first element in the tuple returned from
    # `manio.strip`).
    server = {k: dotdict.undo(v[0]) for k, v in server.items()}
    local = {k: dotdict.undo(v[0]) for k, v in local.items()}

    # Partition the set of meta manifests into create/delete/patch groups.
    plan, err = partition_manifests(local, server)
    if err:
        logit.error("Could not partition the manifests for the plan.")
        return err_resp

    # Sanity check: the resources to patch *must* exist in both local and
    # server manifests. This is a bug if not.
    assert set(plan.patch).issubset(set(local.keys()))
    assert set(plan.patch).issubset(set(server.keys()))

    # For later: every DELETE request will have to pass along a `DeleteOptions`
    # manifest (see below).
    del_opts = {
        "apiVersion": "v1",
        "kind": "DeleteOptions",
        "gracePeriodSeconds": 0,
        "orphanDependents": False,
    }

    # Compile the Deltas to create the missing resources.
    create = []
    for delta in plan.create:
        # We only need the resource and namespace, not its name, because that
        # is how the POST request to create a resource works in K8s.
        # Ignore the error flag because the `strip` function we used above
        # already ensured the resource exists.
        resource, err = k8s.resource(k8sconfig, delta._replace(name=""))
        assert not err

        # Compile the Delta and add it to the list.
        create.append(DeltaCreate(delta, resource.url, local[delta]))

    # Compile the Deltas to delete the excess resources.
    delete = []
    for meta in plan.delete:
        # Resource URL. Ignore the error flag because the `strip` function
        # above already called `k8s.resource` and would have aborted on error.
        resource, err = k8s.resource(k8sconfig, meta)
        assert not err

        # Compile the Delta and add it to the list.
        delete.append(DeltaDelete(meta, resource.url, del_opts.copy()))

    # Iterate over each manifest that needs patching and determine the
    # necessary JSON Patch to transition K8s into the state specified in the
    # local manifests.
    patches = []
    for meta in plan.patch:
        # Compute textual diff (only useful for the user to study the diff).
        diff_str, err = manio.diff(config, k8sconfig, local[meta], server[meta])
        if err or diff_str is None:
            logit.error(f"Could not compute the diff for <{meta}>.")
            return err_resp

        # Compute the JSON patch that will change the K8s state to match the
        # one in the local files.
        patch, err = make_patch(config, k8sconfig, local[meta], server[meta])
        if err or patch is None:
            logit.error(f"Could not compute the patch for <{meta}>")
            return err_resp

        # Append the patch to the list of patches, unless it is empty.
        if len(patch.ops):
            patches.append(DeltaPatch(meta, diff_str, patch))

    # Assemble and return the deployment plan.
    return (DeploymentPlan(create, patches, delete), False)
示例#4
0
    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
示例#5
0
    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