コード例 #1
0
ファイル: test_k8s.py プロジェクト: olitheolix/square
    def test_resource_clusterrole(self, integrationtest, k8sconfig):
        """Verify with a ClusterRole resource.

        NOTE: this test is tailored to Kubernetes v1.16.

        """
        # Fixtures.
        config = self.k8sconfig(integrationtest, k8sconfig)
        MM = MetaManifest

        # Tuples of API version that we ask for (if any), and what the final
        # K8sResource element will contain.
        api_versions = [
            # We expect to get the version we asked for.
            ("rbac.authorization.k8s.io/v1", "rbac.authorization.k8s.io/v1"),

            # Function must automatically determine the latest version of the resource.
            ("", "rbac.authorization.k8s.io/v1"),
        ]

        # Ask for K8s resources.
        for src, expected in api_versions:
            # All ClusterRoles.
            res, err = k8s.resource(config, MM(src, "ClusterRole", None, None))
            assert not err
            assert res == K8sResource(
                apiVersion=expected,
                kind="ClusterRole",
                name="clusterroles",
                namespaced=False,
                url=f"{config.url}/apis/{expected}/clusterroles",
            )

            # All ClusterRoles in a particular namespace -> same as above
            # because the namespace is ignored for non-namespaced resources.
            assert k8s.resource(config, MM(src, "ClusterRole", "ns",
                                           None)) == (res, err)

            # A particular ClusterRole.
            res, err = k8s.resource(config, MM(src, "ClusterRole", None,
                                               "name"))
            assert not err
            assert res == K8sResource(
                apiVersion=expected,
                kind="ClusterRole",
                name="clusterroles",
                namespaced=False,
                url=f"{config.url}/apis/{expected}/clusterroles/name",
            )

            # A particular ClusterRole in a particular namespace -> Same as above
            # because the namespace is ignored for non-namespaced resources.
            assert k8s.resource(config, MM(src, "ClusterRole", "ns",
                                           "name")) == (res, err)  # noqa
コード例 #2
0
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)
コード例 #3
0
    def test_resource_service(self, integrationtest, k8sconfig):
        """Verify with a Service resource.

        NOTE: this test is tailored to Kubernetes v1.16.

        """
        # Fixtures.
        k8sconfig = self.k8sconfig(integrationtest, k8sconfig)
        err_resp = (K8sResource("", "", "", False, ""), True)

        # Tuples of API version that we ask for (if any), and what the final
        # K8sResource element will contain.
        api_versions = [
            # We expect to get the version we asked for.
            ("v1", "v1"),

            # Function must automatically determine the latest version of the resource.
            ("", "v1"),
        ]

        for src, expected in api_versions:
            # A particular Service in a particular namespace.
            res, err = k8s.resource(k8sconfig, MetaManifest(src, "Service", "ns", "name"))
            assert not err
            assert res == K8sResource(
                apiVersion=expected, kind="Service", name="services", namespaced=True,
                url=f"{k8sconfig.url}/api/v1/namespaces/ns/services/name",
            )

            # All Services in all namespaces.
            res, err = k8s.resource(k8sconfig, MetaManifest(src, "Service", None, None))
            assert not err
            assert res == K8sResource(
                apiVersion=expected, kind="Service", name="services", namespaced=True,
                url=f"{k8sconfig.url}/api/v1/services",
            )

            # All Services in a particular namespace.
            res, err = k8s.resource(k8sconfig, MetaManifest(src, "Service", "ns", ""))
            assert not err
            assert res == K8sResource(
                apiVersion=expected, kind="Service", name="services", namespaced=True,
                url=f"{k8sconfig.url}/api/v1/namespaces/ns/services",
            )

            # A particular Service in all namespaces -> Invalid.
            MM = MetaManifest
            assert k8s.resource(k8sconfig, MM(src, "Service", None, "name")) == err_resp
コード例 #4
0
ファイル: test_square.py プロジェクト: mmingorance-dh/square
    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)
コード例 #5
0
ファイル: test_square.py プロジェクト: mmingorance-dh/square
    def test_make_patch_special(self, config, k8sconfig):
        """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`.

        """
        for kind in ["Namespace", "ClusterRole"]:
            meta = manio.make_meta(make_manifest(kind, None, "name"))

            # Determine the resource path so we can verify it later.
            url = resource(k8sconfig, meta)[0].url

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

            # 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 = sq.make_patch(config, k8sconfig, loc, srv)
            assert err is False and len(data) > 0
コード例 #6
0
    def test_resource_err(self, integrationtest, k8sconfig):
        """Test various error scenarios."""
        # Fixtures.
        config = self.k8sconfig(integrationtest, k8sconfig)
        err_resp = (K8sResource("", "", "", False, ""), True)
        MM = MetaManifest

        # Sanity check: ask for a valid StatefulSet.
        _, err = k8s.resource(config, MM("apps/v1", "StatefulSet", "ns", "name"))
        assert not err

        # Ask for a StatefulSet on a bogus API endpoint.
        assert k8s.resource(config, MM("bogus", "StatefulSet", "ns", "name")) == err_resp

        # Ask for a bogus K8s kind.
        assert k8s.resource(config, MM("v1", "Bogus", "ns", "name")) == err_resp
        assert k8s.resource(config, MM("", "Bogus", "ns", "name")) == err_resp
コード例 #7
0
ファイル: test_k8s.py プロジェクト: olitheolix/square
    def test_resource_namespace(self, integrationtest, k8sconfig):
        """Verify with a Namespace resource.

        This one is a special case because it is not namespaced but Square's
        MetaManifest may specify a `namespace` for them, which refers to their
        actual name. This is a necessary implementation detail to properly
        support the selectors.

        """
        # Fixtures.
        config = self.k8sconfig(integrationtest, k8sconfig)
        MM = MetaManifest

        for src in ["", "v1"]:
            # A particular Namespace.
            res, err = k8s.resource(config, MM(src, "Namespace", None, "name"))
            assert not err
            assert res == K8sResource(
                apiVersion="v1",
                kind="Namespace",
                name="namespaces",
                namespaced=False,
                url=f"{config.url}/api/v1/namespaces/name",
            )

            # A particular Namespace in a particular namespace -> Invalid.
            assert k8s.resource(config, MM(src, "Namespace", "ns",
                                           "name")) == (res, err)

            # All Namespaces.
            res, err = k8s.resource(config, MM(src, "Namespace", None, None))
            assert not err
            assert res == K8sResource(
                apiVersion="v1",
                kind="Namespace",
                name="namespaces",
                namespaced=False,
                url=f"{config.url}/api/v1/namespaces",
            )

            # Same as above because the "namespace" argument is ignored for Namespaces.
            assert k8s.resource(config, MM(src, "Namespace", "name",
                                           "")) == (res, err)
コード例 #8
0
ファイル: test_square.py プロジェクト: mmingorance-dh/square
    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)
コード例 #9
0
ファイル: test_k8s.py プロジェクト: olitheolix/square
    def test_resource_statefulset(self, integrationtest, k8sconfig):
        """Verify with a HorizontalPodAutoscaler resource.

        This resource is available under three different API endpoints
        (v1, v2beta1 and v2beta2).

        NOTE: this test is tailored to Kubernetes v1.16.

        """
        config = self.k8sconfig(integrationtest, k8sconfig)
        MM = MetaManifest
        err_resp = (K8sResource("", "", "", False, ""), True)

        # Tuples of API version that we ask for (if any), and what the final
        # K8sResource element will contain.
        api_versions = [
            # We expect to get the version we asked for.
            ("autoscaling/v1", "autoscaling/v1"),
            ("autoscaling/v2beta1", "autoscaling/v2beta1"),
            ("autoscaling/v2beta2", "autoscaling/v2beta2"),

            # Function must automatically determine the latest version of the resource.
            ("", "autoscaling/v1"),
        ]

        # Convenience.
        kind = "HorizontalPodAutoscaler"
        name = kind.lower() + "s"

        for src, expected in api_versions:
            print(src)
            # A particular StatefulSet in a particular namespace.
            res, err = k8s.resource(config, MM(src, kind, "ns", "name"))
            assert not err
            assert res == K8sResource(
                apiVersion=expected,
                kind=kind,
                name=name,
                namespaced=True,
                url=f"{config.url}/apis/{expected}/namespaces/ns/{name}/name",
            )

            # All StatefulSets in all namespaces.
            res, err = k8s.resource(config, MM(src, kind, None, None))
            assert not err
            assert res == K8sResource(
                apiVersion=expected,
                kind=kind,
                name=name,
                namespaced=True,
                url=f"{config.url}/apis/{expected}/{name}",
            )

            # All StatefulSets in a particular namespace.
            res, err = k8s.resource(config, MM(src, kind, "ns", ""))
            assert not err
            assert res == K8sResource(
                apiVersion=expected,
                kind=kind,
                name=name,
                namespaced=True,
                url=f"{config.url}/apis/{expected}/namespaces/ns/{name}",
            )

            # A particular StatefulSet in all namespaces -> Invalid.
            assert k8s.resource(config, MM(src, kind, None,
                                           "name")) == err_resp
コード例 #10
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)
コード例 #11
0
def match_api_version(
        k8sconfig: K8sConfig,
        local: ServerManifests,
        server: ServerManifests) -> Tuple[ServerManifests, bool]:
    """Fetch the manifests from the endpoints defined in the local manifest.

    If a local manifest uses a different value for `apiVersion` then we need to
    re-fetch those manifests from K8s via that endpoint. This function does
    just that.

    This function returns `server` verbatim if there is no overlap with
    `server` and `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:
        `server` but possibly with some entries re-fetched from the same K8s
        endpoint that the equivalent resource in `local` specifies.

    """
    # Avoid side effects.
    server = copy.deepcopy(server)

    # Find the resources that exist in local manifests and on K8s. The
    # resources are identical if their MetaManifest are identical save for the
    # `apiVersion` field.
    mm_loc = {meta._replace(apiVersion=""): meta for meta in local}
    mm_srv = {meta._replace(apiVersion=""): meta for meta in server}
    meta_overlap = set(mm_loc.keys()) & set(mm_srv.keys())

    # Iterate over all the resources that exist on both the server and locally,
    # even though they may use different API versions.
    to_download: List[MetaManifest] = []
    for meta in meta_overlap:
        # Lookup the full MetaManifest for the local and server resource.
        # NOTE: meta_{loc,srv} are identical except possibly for the `apiVersion` field.
        meta_loc = mm_loc[meta]
        meta_srv = mm_srv[meta]

        # Do nothing if the `apiVersions` match because we can already compute a
        # plan for it. However, if the `apiVersions` differ then we will
        # replace entry in `server` with the one fetched from the correct K8s
        # endpoint (see next section).
        if meta_loc != meta_srv:
            del server[meta_srv]
            to_download.append(meta_loc)
            logit.info(
                f"Using non-default {meta.kind.upper()} endpoint "
                f"<{meta_loc.apiVersion}>"
            )

    # Re-fetch the resources we already got but this time from the correct endpoint.
    for meta in to_download:
        # Construct the correct K8sResource.
        resource, err = k8s.resource(k8sconfig, meta)
        assert not err

        # Download all resources of the current kind.
        meta, manifest, err = manio.download_single(k8sconfig, resource)
        assert not err

        # Add the resource to the `server` dict. This will have been one of
        # those we deleted a few lines earlier.
        server[meta] = manifest

    return server, False
コード例 #12
0
ファイル: test_square.py プロジェクト: mmingorance-dh/square
    def test_compile_plan_create_delete_ok(self, config, k8sconfig):
        """Test a plan that creates and deletes resource, but not patch 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.

        """
        # Local: defines Namespace "ns1" with 1 deployment.
        meta = [
            MetaManifest('v1', 'Namespace', None, 'ns1'),
            MetaManifest('apps/v1', 'Deployment', 'ns1', 'res_0'),

            # Server: has a Namespace "ns2" with 2 deployments.
            MetaManifest('v1', 'Namespace', None, 'ns2'),
            MetaManifest('apps/v1', 'Deployment', 'ns2', 'res_1'),
            MetaManifest('apps/v1', 'Deployment', 'ns2', 'res_2'),
        ]

        # Determine the K8sResource for all involved resources. Also verify
        # that all resources specify a valid API group.
        res = [resource(k8sconfig, _._replace(name="")) for _ in meta]
        assert not any([_[1] for _ in res])
        res = [_[0] for _ in res]

        # Compile local and server manifests. Their resources have no overlap.
        # This will ensure that we have to create all the local resources,
        # delete all the server resources, and patch nothing.
        loc_man = {
            _: make_manifest(_.kind, _.namespace, _.name)
            for _ in meta[:2]
        }
        srv_man = {
            _: make_manifest(_.kind, _.namespace, _.name)
            for _ in meta[2:]
        }

        # 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 declared in local files must be created and server resources deleted.
        expected = DeploymentPlan(
            create=[
                DeltaCreate(meta[0], res[0].url, loc_man[meta[0]]),
                DeltaCreate(meta[1], res[1].url, loc_man[meta[1]]),
            ],
            patch=[],
            delete=[
                DeltaDelete(meta[2], res[2].url + "/" + meta[2].name,
                            del_opts),
                DeltaDelete(meta[3], res[3].url + "/" + meta[3].name,
                            del_opts),
                DeltaDelete(meta[4], res[4].url + "/" + meta[4].name,
                            del_opts),
            ],
        )
        ret, err = sq.compile_plan(config, k8sconfig, loc_man, srv_man)
        assert ret.create == expected.create
        assert (ret, err) == (expected, False)