예제 #1
0
    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 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
예제 #3
0
def parse_api_group(api_version, url,
                    resp) -> Tuple[List[K8sResource], Dict[str, str]]:
    """Compile the K8s API `resp` into a `K8sResource` tuples.

    The `resp` is the verbatim response from the K8s API group regarding the
    resources it provides. Here we compile those into `K8sResource` tuples iff
    they meet the criteria to be manageable by Square. These criteria are, most
    notably, the ability to create, get, patch and delete the resource.

    Also return a LUT to convert short names like "svc" into the proper resource
    kind "Service".

    """
    resources = resp["resources"]

    def valid(_res):
        """Return `True` if `res` describes a Square compatible resource."""
        # Convenience.
        name = _res["name"]
        verbs = list(sorted(_res["verbs"]))

        # Ignore resource like "services/status". We only care for "services".
        if "/" in name:
            logit.debug(
                f"Ignore resource <{name}>: has a slash ('/') in its name")
            return False

        # Square can only man age the resource if it can be read, modified and
        # deleted. Here we check if `res` has the respective verbs.
        minimal_verbs = {"create", "delete", "get", "list", "patch", "update"}
        if not minimal_verbs.issubset(set(verbs)):
            logit.debug(
                f"Ignore resource <{name}>: insufficient verbs: {verbs}")
            return False

        return True

    # Compile the K8s resource definition into a `K8sResource` structure if it
    # is compatible with Square (see `valid` helper above).
    group_urls: List[K8sResource]
    group_urls = [
        K8sResource(api_version, _["kind"], _["name"], _["namespaced"], url)
        for _ in resources if valid(_)
    ]

    # Compile LUT to translate short names into their proper resource
    # kind: Example short2kind = {"service":, "Service", "svc": "Service"}
    short2kind: Dict[str, str] = {}
    for res in resources:
        kind = res["kind"]

        short2kind[kind.lower()] = kind
        short2kind[res["name"]] = kind
        for short_name in res.get("shortNames", []):
            short2kind[short_name] = kind

    return (group_urls, short2kind)
예제 #4
0
    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)
예제 #5
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
예제 #6
0
    def test_compile_api_endpoints_integrated(self, integrationtest,
                                              k8sconfig):
        """Ask for all endpoints and perform some sanity checks.

        This test is about `compile_api_endpoints` but we only inspect the
        K8sConfig structure which must have been populated by
        `compile_api_endpoints` under the hood.

        """
        square.square.setup_logging(2)

        # This will call `compile_api_endpoints` internally to populate fields
        # in `k8sconfig`.
        config = self.k8sconfig(integrationtest, k8sconfig)

        # Sanity check.
        kinds = {
            # Standard resources that a v1.16 Kubernetes cluster always has.
            ('ClusterRole', 'rbac.authorization.k8s.io/v1'),
            ('ClusterRole', 'rbac.authorization.k8s.io/v1beta1'),
            ('ConfigMap', 'v1'),
            ('DaemonSet', 'apps/v1'),
            ('Deployment', 'apps/v1'),
            ('HorizontalPodAutoscaler', 'autoscaling/v1'),
            ('HorizontalPodAutoscaler', 'autoscaling/v2beta1'),
            ('HorizontalPodAutoscaler', 'autoscaling/v2beta2'),
            ('Pod', 'v1'),
            ('Service', 'v1'),
            ('ServiceAccount', 'v1'),

            # Our CustomCRD.
            ("DemoCRD", "mycrd.com/v1"),
        }
        assert kinds.issubset(set(config.apis.keys()))

        # Verify some standard resource types.
        assert config.apis[("Namespace", "v1")] == K8sResource(
            apiVersion="v1",
            kind="Namespace",
            name="namespaces",
            namespaced=False,
            url=f"{config.url}/api/v1",
        )
        hpa = "HorizontalPodAutoscaler"
        assert config.apis[(hpa, "autoscaling/v1")] == K8sResource(
            apiVersion="autoscaling/v1",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v1",
        )
        assert config.apis[(hpa, "autoscaling/v2beta1")] == K8sResource(
            apiVersion="autoscaling/v2beta1",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v2beta1",
        )
        assert config.apis[(hpa, "autoscaling/v2beta2")] == K8sResource(
            apiVersion="autoscaling/v2beta2",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v2beta2",
        )
        assert config.apis[("Pod", "v1")] == K8sResource(
            apiVersion="v1",
            kind="Pod",
            name="pods",
            namespaced=True,
            url=f"{config.url}/api/v1",
        )
        assert config.apis[("Deployment", "apps/v1")] == K8sResource(
            apiVersion="apps/v1",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        )
        assert config.apis[(
            "Ingress", "networking.k8s.io/v1beta1")] == K8sResource(
                apiVersion="networking.k8s.io/v1beta1",
                kind="Ingress",
                name="ingresses",
                namespaced=True,
                url=f"{config.url}/apis/networking.k8s.io/v1beta1",
            )

        # Verify our CRD.
        assert config.apis[("DemoCRD", "mycrd.com/v1")] == K8sResource(
            apiVersion="mycrd.com/v1",
            kind="DemoCRD",
            name="democrds",
            namespaced=True,
            url=f"{config.url}/apis/mycrd.com/v1",
        )

        # Verify default resource versions for a Deployment. This is specific
        # to Kubernetes 1.16.
        assert config.apis[("Deployment", "")].apiVersion == "apps/v1"

        # Ingress are still in beta in v1.16. However, they exist as both
        # `networking.k8s.io/v1beta1` and `extensions/v1beta1`. In that case,
        # the function would just return the last one in alphabetical order.
        assert config.apis[("Ingress", "")].apiVersion == "extensions/v1beta1"
예제 #7
0
    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
예제 #8
0
def resource(k8sconfig: K8sConfig,
             meta: MetaManifest) -> Tuple[K8sResource, bool]:
    """Return `K8sResource` object.

    That object will contain the full path to a resource, eg.
    https://1.2.3.4/api/v1/namespace/foo/services.

    Inputs:
        k8sconfig: K8sConfig
        meta: MetaManifest

    Returns:
        K8sResource

    """
    err_resp = (K8sResource("", "", "", False, ""), True)

    # Compile the lookup key for the resource, eg `("Service", "v1")`.
    if not meta.apiVersion:
        # Use the most recent version of the API if None was specified.
        candidates = [(kind, ver) for kind, ver in k8sconfig.apis
                      if kind == meta.kind]
        if len(candidates) == 0:
            logit.warning(f"Cannot determine API version for <{meta.kind}>")
            return err_resp
        candidates.sort()
        key = candidates.pop(0)
    else:
        key = (meta.kind, meta.apiVersion)

    # Retrieve the resource.
    try:
        resource = k8sconfig.apis[key]
    except KeyError:
        logit.error(f"Unsupported resource <{meta.kind}> {key}.")
        return err_resp

    # Void the "namespace" key for non-namespaced resources.
    if not resource.namespaced:
        meta = meta._replace(namespace=None)

    # Namespaces are special because they lack the `namespaces/` path prefix.
    if meta.kind == "Namespace":
        # Return the correct URL, depending on whether we want all namespaces
        # or a particular one.
        url = f"{resource.url}/namespaces"
        if meta.name:
            url += f"/{meta.name}"
        return resource._replace(url=url), False

    # Determine if the prefix for namespaced resources.
    if meta.namespace is None:
        namespace = ""
    else:
        # Namespace name must conform to K8s standards.
        match = re.match(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", meta.namespace)
        if match is None or match.group() != meta.namespace:
            logit.error(f"Invalid namespace name <{meta.namespace}>.")
            return err_resp
        namespace = f"namespaces/{meta.namespace}"

    # Sanity check: we cannot search for a namespaced resource by name in all
    # namespaces. Example: we cannot search for a Service `foo` in all
    # namespaces. We could only search for Service `foo` in namespace `bar`, or
    # all services in all namespaces.
    if resource.namespaced and meta.name and not meta.namespace:
        logit.error(
            f"Cannot search for {meta.kind} {meta.name} in {meta.namespace}")
        return err_resp

    # Create the full path to the resource depending on whether we have a
    # namespace and resource name. Here are all three possibilities:
    #  - /api/v1/namespaces/services
    #  - /api/v1/namespaces/my-namespace/services
    #  - /api/v1/namespaces/my-namespace/services/my-service
    path = f"{namespace}/{resource.name}" if namespace else resource.name
    path = f"{path}/{meta.name}" if meta.name else path

    # The concatenation above may have introduced `//`. Here we remove them.
    path = path.replace("//", "/")

    # Return the K8sResource with the correct URL.
    resource = resource._replace(url=f"{resource.url}/{path}")
    return resource, False
예제 #9
0
def k8s_apis(config: K8sConfig):
    return {
        ("ClusterRole", ""):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="ClusterRole",
            name="clusterroles",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("ClusterRole", "rbac.authorization.k8s.io/v1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="ClusterRole",
            name="clusterroles",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("ClusterRole", "rbac.authorization.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1beta1",
            kind="ClusterRole",
            name="clusterroles",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1beta1",
        ),
        ("ClusterRoleBinding", ""):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="ClusterRoleBinding",
            name="clusterrolebindings",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("ClusterRoleBinding", "rbac.authorization.k8s.io/v1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="ClusterRoleBinding",
            name="clusterrolebindings",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("ClusterRoleBinding", "rbac.authorization.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1beta1",
            kind="ClusterRoleBinding",
            name="clusterrolebindings",
            namespaced=False,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1beta1",
        ),
        ("ConfigMap", ""):
        K8sResource(
            apiVersion="v1",
            kind="ConfigMap",
            name="configmaps",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("ConfigMap", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="ConfigMap",
            name="configmaps",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("CronJob", ""):
        K8sResource(
            apiVersion="batch/v1beta1",
            kind="CronJob",
            name="cronjobs",
            namespaced=True,
            url=f"{config.url}/apis/batch/v1beta1",
        ),
        ("CronJob", "batch/v1beta1"):
        K8sResource(
            apiVersion="batch/v1beta1",
            kind="CronJob",
            name="cronjobs",
            namespaced=True,
            url=f"{config.url}/apis/batch/v1beta1",
        ),
        ("CustomResourceDefinition", ""):
        K8sResource(
            apiVersion="apiextensions.k8s.io/v1beta1",
            kind="CustomResourceDefinition",
            name="customresourcedefinitions",
            namespaced=False,
            url=f"{config.url}/apis/apiextensions.k8s.io/v1beta1",
        ),
        ("CustomResourceDefinition", "apiextensions.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="apiextensions.k8s.io/v1beta1",
            kind="CustomResourceDefinition",
            name="customresourcedefinitions",
            namespaced=False,
            url=f"{config.url}/apis/apiextensions.k8s.io/v1beta1",
        ),
        ("DaemonSet", ""):
        K8sResource(
            apiVersion="apps/v1",
            kind="DaemonSet",
            name="daemonsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("DaemonSet", "apps/v1"):
        K8sResource(
            apiVersion="apps/v1",
            kind="DaemonSet",
            name="daemonsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("DaemonSet", "apps/v1beta2"):
        K8sResource(
            apiVersion="apps/v1beta2",
            kind="DaemonSet",
            name="daemonsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1beta2",
        ),
        ("DaemonSet", "extensions/v1beta1"):
        K8sResource(
            apiVersion="extensions/v1beta1",
            kind="DaemonSet",
            name="daemonsets",
            namespaced=True,
            url=f"{config.url}/apis/extensions/v1beta1",
        ),
        ("DemoCRD", ""):
        K8sResource(
            apiVersion="mycrd.com/v1",
            kind="DemoCRD",
            name="democrds",
            namespaced=True,
            url=f"{config.url}/apis/mycrd.com/v1",
        ),
        ("DemoCRD", "mycrd.com/v1"):
        K8sResource(
            apiVersion="mycrd.com/v1",
            kind="DemoCRD",
            name="democrds",
            namespaced=True,
            url=f"{config.url}/apis/mycrd.com/v1",
        ),
        ("Deployment", ""):
        K8sResource(
            apiVersion="apps/v1",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("Deployment", "apps/v1"):
        K8sResource(
            apiVersion="apps/v1",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("Deployment", "apps/v1beta1"):
        K8sResource(
            apiVersion="apps/v1beta1",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1beta1",
        ),
        ("Deployment", "apps/v1beta2"):
        K8sResource(
            apiVersion="apps/v1beta2",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1beta2",
        ),
        ("Deployment", "extensions/v1beta1"):
        K8sResource(
            apiVersion="extensions/v1beta1",
            kind="Deployment",
            name="deployments",
            namespaced=True,
            url=f"{config.url}/apis/extensions/v1beta1",
        ),
        ("HorizontalPodAutoscaler", ""):
        K8sResource(
            apiVersion="autoscaling/v1",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v1",
        ),
        ("HorizontalPodAutoscaler", "autoscaling/v1"):
        K8sResource(
            apiVersion="autoscaling/v1",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v1",
        ),
        ("HorizontalPodAutoscaler", "autoscaling/v2beta1"):
        K8sResource(
            apiVersion="autoscaling/v2beta1",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v2beta1",
        ),
        ("HorizontalPodAutoscaler", "autoscaling/v2beta2"):
        K8sResource(
            apiVersion="autoscaling/v2beta2",
            kind="HorizontalPodAutoscaler",
            name="horizontalpodautoscalers",
            namespaced=True,
            url=f"{config.url}/apis/autoscaling/v2beta2",
        ),
        ("Ingress", ""):
        K8sResource(
            apiVersion="extensions/v1beta1",
            kind="Ingress",
            name="ingresses",
            namespaced=True,
            url=f"{config.url}/apis/extensions/v1beta1",
        ),
        ("Ingress", "extensions/v1beta1"):
        K8sResource(
            apiVersion="extensions/v1beta1",
            kind="Ingress",
            name="ingresses",
            namespaced=True,
            url=f"{config.url}/apis/extensions/v1beta1",
        ),
        ("Ingress", "networking.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="networking.k8s.io/v1beta1",
            kind="Ingress",
            name="ingresses",
            namespaced=True,
            url=f"{config.url}/apis/networking.k8s.io/v1beta1",
        ),
        ("Job", ""):
        K8sResource(
            apiVersion="batch/v1",
            kind="Job",
            name="jobs",
            namespaced=True,
            url=f"{config.url}/apis/batch/v1",
        ),
        ("Job", "batch/v1"):
        K8sResource(
            apiVersion="batch/v1",
            kind="Job",
            name="jobs",
            namespaced=True,
            url=f"{config.url}/apis/batch/v1",
        ),
        ("Namespace", ""):
        K8sResource(
            apiVersion="v1",
            kind="Namespace",
            name="namespaces",
            namespaced=False,
            url=f"{config.url}/api/v1",
        ),
        ("Namespace", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="Namespace",
            name="namespaces",
            namespaced=False,
            url=f"{config.url}/api/v1",
        ),
        ("PersistentVolume", ""):
        K8sResource(
            apiVersion="v1",
            kind="PersistentVolume",
            name="persistentvolumes",
            namespaced=False,
            url=f"{config.url}/api/v1",
        ),
        ("PersistentVolume", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="PersistentVolume",
            name="persistentvolumes",
            namespaced=False,
            url=f"{config.url}/api/v1",
        ),
        ("PersistentVolumeClaim", ""):
        K8sResource(
            apiVersion="v1",
            kind="PersistentVolumeClaim",
            name="persistentvolumeclaims",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("PersistentVolumeClaim", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="PersistentVolumeClaim",
            name="persistentvolumeclaims",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("Pod", ""):
        K8sResource(
            apiVersion="v1",
            kind="Pod",
            name="pods",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("Pod", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="Pod",
            name="pods",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("PodDisruptionBudget", ""):
        K8sResource(
            apiVersion="policy/v1beta1",
            kind="PodDisruptionBudget",
            name="poddisruptionbudgets",
            namespaced=True,
            url=f"{config.url}/apis/policy/v1beta1",
        ),
        ("PodDisruptionBudget", "policy/v1beta1"):
        K8sResource(
            apiVersion="policy/v1beta1",
            kind="PodDisruptionBudget",
            name="poddisruptionbudgets",
            namespaced=True,
            url=f"{config.url}/apis/policy/v1beta1",
        ),
        ("Role", ""):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="Role",
            name="roles",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("Role", "rbac.authorization.k8s.io/v1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="Role",
            name="roles",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("Role", "rbac.authorization.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1beta1",
            kind="Role",
            name="roles",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1beta1",
        ),
        ("RoleBinding", ""):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="RoleBinding",
            name="rolebindings",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("RoleBinding", "rbac.authorization.k8s.io/v1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1",
            kind="RoleBinding",
            name="rolebindings",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1",
        ),
        ("RoleBinding", "rbac.authorization.k8s.io/v1beta1"):
        K8sResource(
            apiVersion="rbac.authorization.k8s.io/v1beta1",
            kind="RoleBinding",
            name="rolebindings",
            namespaced=True,
            url=f"{config.url}/apis/rbac.authorization.k8s.io/v1beta1",
        ),
        ("Secret", ""):
        K8sResource(
            apiVersion="v1",
            kind="Secret",
            name="secrets",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("Secret", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="Secret",
            name="secrets",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("Service", ""):
        K8sResource(
            apiVersion="v1",
            kind="Service",
            name="services",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("Service", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="Service",
            name="services",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("ServiceAccount", ""):
        K8sResource(
            apiVersion="v1",
            kind="ServiceAccount",
            name="serviceaccounts",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("ServiceAccount", "v1"):
        K8sResource(
            apiVersion="v1",
            kind="ServiceAccount",
            name="serviceaccounts",
            namespaced=True,
            url=f"{config.url}/api/v1",
        ),
        ("StatefulSet", ""):
        K8sResource(
            apiVersion="apps/v1",
            kind="StatefulSet",
            name="statefulsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("StatefulSet", "apps/v1"):
        K8sResource(
            apiVersion="apps/v1",
            kind="StatefulSet",
            name="statefulsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1",
        ),
        ("StatefulSet", "apps/v1beta1"):
        K8sResource(
            apiVersion="apps/v1beta1",
            kind="StatefulSet",
            name="statefulsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1beta1",
        ),
        ("StatefulSet", "apps/v1beta2"):
        K8sResource(
            apiVersion="apps/v1beta2",
            kind="StatefulSet",
            name="statefulsets",
            namespaced=True,
            url=f"{config.url}/apis/apps/v1beta2",
        ),
    }