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
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
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)
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)
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
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"
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
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
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", ), }