Skip to content

Commit 98de80a

Browse files
committed
Refactor kubeconfig: provider/consumer split, tests, fixes
- Add kubeconfig_generator.py and kubeconfig_helpers.py - provider-kubeconfig: fix namespace create bug, add -p permissionfile - Unit tests for build_kubeconfig_dict, CLI parity, provider/consumer split - Integration tests for provider and consumer equivalence (skipped without cluster)
1 parent cba0fcc commit 98de80a

File tree

5 files changed

+752
-4
lines changed

5 files changed

+752
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
*~
2+
__pycache__
23
venv
34
provider-kubeconfig.log
5+
kubeconfig-generator.log
46
bak
57
vendor
68
operator-deployer/artifacts/deployment/operator-deployer

kubeconfig_generator.py

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Kubeconfig generator: creates provider and consumer kubeconfigs.
4+
Use provider-kubeconfig.py for backward compatibility
5+
"""
6+
import argparse
7+
import json
8+
import os
9+
import sys
10+
11+
from logging.config import dictConfig
12+
13+
from kubeconfig_helpers import (
14+
create_role_rolebinding,
15+
extract_token_and_build_kubeconfig,
16+
generate_kubeconfig,
17+
run_command,
18+
)
19+
20+
dictConfig({
21+
"version": 1,
22+
"formatters": {"default": {"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"}},
23+
"handlers": {
24+
"file.handler": {
25+
"class": "logging.handlers.RotatingFileHandler",
26+
"filename": "kubeconfig-generator.log",
27+
"maxBytes": 10000000,
28+
"backupCount": 5,
29+
"level": "DEBUG",
30+
},
31+
},
32+
"root": {"level": "INFO", "handlers": ["file.handler"]},
33+
})
34+
35+
36+
# --- Provider RBAC ---
37+
38+
def apply_provider_rbac(sa, namespace, kubeconfig, run_cmd=None):
39+
"""Apply ClusterRole and ClusterRoleBinding for provider (full platform operator permissions)."""
40+
run = run_cmd or run_command
41+
42+
all_resources = []
43+
rule_list = [
44+
{"apiGroups": ["*", ""], "resources": ["*"], "verbs": ["get", "watch", "list"]},
45+
{
46+
"apiGroups": ["workflows.kubeplus"],
47+
"resources": ["resourcecompositions", "resourcemonitors", "resourcepolicies", "resourceevents"],
48+
"verbs": ["get", "watch", "list", "create", "delete", "update", "patch"],
49+
},
50+
{
51+
"apiGroups": ["rbac.authorization.k8s.io"],
52+
"resources": ["clusterroles", "clusterrolebindings", "roles", "rolebindings"],
53+
"verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"],
54+
},
55+
{"apiGroups": [""], "resources": ["pods/portforward"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch"]},
56+
{"apiGroups": ["platformapi.kubeplus"], "resources": ["*"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch"]},
57+
{"apiGroups": [""], "resources": ["secrets", "serviceaccounts", "configmaps", "events", "persistentvolumeclaims", "serviceaccounts/token", "services", "services/proxy", "endpoints"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"]},
58+
{"apiGroups": [""], "resources": ["namespaces"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch"]},
59+
{
60+
"apiGroups": ["apps"],
61+
"resources": ["deployments", "daemonsets", "deployments/rollback", "deployments/scale", "replicasets", "replicasets/scale", "statefulsets", "statefulsets/scale"],
62+
"verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"],
63+
},
64+
{"apiGroups": [""], "resources": ["users", "groups", "serviceaccounts"], "verbs": ["impersonate"]},
65+
{
66+
"apiGroups": [""],
67+
"resources": ["pods", "pods/attach", "pods/exec", "pods/portforward", "pods/proxy", "pods/eviction", "replicationcontrollers", "replicationcontrollers/scale"],
68+
"verbs": ["get", "list", "create", "update", "delete", "watch", "patch", "deletecollection"],
69+
},
70+
{"apiGroups": ["admissionregistration.k8s.io"], "resources": ["mutatingwebhookconfigurations"], "verbs": ["get", "create", "delete", "update"]},
71+
{"apiGroups": ["apiextensions.k8s.io"], "resources": ["customresourcedefinitions"], "verbs": ["get", "create", "delete", "update", "patch"]},
72+
{
73+
"apiGroups": ["certificates.k8s.io"],
74+
"resources": ["signers"],
75+
"resourceNames": ["kubernetes.io/legacy-unknown", "kubernetes.io/kubelet-serving", "kubernetes.io/kube-apiserver-client", "cloudark.io/kubeplus"],
76+
"verbs": ["get", "create", "delete", "update", "patch", "approve"],
77+
},
78+
{"apiGroups": ["*"], "resources": ["*"], "verbs": ["get"]},
79+
{"apiGroups": ["certificates.k8s.io"], "resources": ["certificatesigningrequests", "certificatesigningrequests/approval"], "verbs": ["create", "delete", "update", "patch"]},
80+
{
81+
"apiGroups": ["extensions"],
82+
"resources": ["deployments", "daemonsets", "deployments/rollback", "deployments/scale", "replicasets", "replicasets/scale", "replicationcontrollers/scale", "ingresses", "networkpolicies"],
83+
"verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"],
84+
},
85+
{"apiGroups": ["networking.k8s.io"], "resources": ["ingresses", "networkpolicies"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"]},
86+
{"apiGroups": ["authorization.k8s.io"], "resources": ["localsubjectaccessreviews"], "verbs": ["create"]},
87+
{"apiGroups": ["autoscaling"], "resources": ["horizontalpodautoscalers"], "verbs": ["create", "delete", "deletecollection", "patch", "update"]},
88+
{"apiGroups": ["batch"], "resources": ["cronjobs", "jobs"], "verbs": ["create", "delete", "deletecollection", "patch", "update"]},
89+
{"apiGroups": ["policy"], "resources": ["poddisruptionbudgets"], "verbs": ["create", "delete", "deletecollection", "patch", "update"]},
90+
{"apiGroups": [""], "resources": ["resourcequotas"], "verbs": ["create", "delete", "deletecollection", "patch", "update"]},
91+
{"apiGroups": [""], "resources": ["persistentvolumes", "persistentvolumeclaims"], "verbs": ["get", "watch", "list", "create", "delete", "update", "patch"]},
92+
]
93+
94+
for r in rule_list:
95+
all_resources.extend(r.get("resources", []))
96+
97+
role = {"apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRole", "metadata": {"name": sa, "namespace": namespace}, "rules": rule_list}
98+
create_role_rolebinding(role, sa + "-role.yaml", kubeconfig, run_cmd)
99+
100+
role_binding = {
101+
"apiVersion": "rbac.authorization.k8s.io/v1",
102+
"kind": "ClusterRoleBinding",
103+
"metadata": {"name": sa, "namespace": namespace},
104+
"subjects": [{"kind": "ServiceAccount", "name": sa, "namespace": namespace, "apiGroup": ""}],
105+
"roleRef": {"kind": "ClusterRole", "name": sa, "apiGroup": "rbac.authorization.k8s.io"},
106+
}
107+
create_role_rolebinding(role_binding, sa + "-rolebinding.yaml", kubeconfig, run_cmd)
108+
109+
cfg_map_filename = sa + "-perms.txt"
110+
all_resources = sorted(list(set(all_resources)))
111+
with open(cfg_map_filename, "w", encoding="utf-8") as fp:
112+
fp.write(str(all_resources))
113+
run(
114+
"kubectl create configmap " + sa + "-perms -n " + namespace
115+
+ " --from-file=" + cfg_map_filename + kubeconfig
116+
)
117+
118+
119+
def update_provider_rbac(permission_file, sa, namespace, kubeconfig, run_cmd=None):
120+
"""Add permissions from JSON file to provider (update command)."""
121+
run = run_cmd or run_command
122+
123+
with open(permission_file, "r", encoding="utf-8") as fp:
124+
perms_data = json.load(fp)
125+
perms = perms_data["perms"]
126+
rule_list = []
127+
new_resources = []
128+
129+
for api_group, res_actions in perms.items():
130+
for res in res_actions:
131+
for resource, verbs in res.items():
132+
if resource not in new_resources:
133+
new_resources.append(resource.strip())
134+
rule_group = {}
135+
if api_group == "non-apigroup":
136+
if "nonResourceURL" in resource:
137+
parts = resource.split("nonResourceURL::")
138+
rule_group["nonResourceURLs"] = [parts[0].strip()]
139+
rule_group["verbs"] = verbs
140+
else:
141+
rule_group["apiGroups"] = [api_group]
142+
rule_group["verbs"] = verbs
143+
if "resourceName" in resource:
144+
parts = resource.split("/resourceName::")
145+
rule_group["resources"] = [parts[0].strip()]
146+
rule_group["resourceNames"] = [parts[1].strip()]
147+
else:
148+
rule_group["resources"] = [resource]
149+
rule_list.append(rule_group)
150+
151+
role = {"apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRole", "metadata": {"name": sa + "-update", "namespace": namespace}, "rules": rule_list}
152+
create_role_rolebinding(role, sa + "-update-role.yaml", kubeconfig, run_cmd)
153+
154+
role_binding = {
155+
"apiVersion": "rbac.authorization.k8s.io/v1",
156+
"kind": "ClusterRoleBinding",
157+
"metadata": {"name": sa + "-update", "namespace": namespace},
158+
"subjects": [{"kind": "ServiceAccount", "name": sa, "namespace": namespace, "apiGroup": ""}],
159+
"roleRef": {"kind": "ClusterRole", "name": sa + "-update", "apiGroup": "rbac.authorization.k8s.io"},
160+
}
161+
create_role_rolebinding(role_binding, sa + "-update-rolebinding.yaml", kubeconfig, run_cmd)
162+
163+
cfg_map_name = sa + "-perms"
164+
cfg_map_filename = sa + "-perms.txt"
165+
cmd = "kubectl get configmap " + cfg_map_name + " -o json -n " + namespace + kubeconfig
166+
out, _ = run(cmd)
167+
kubeplus_perms = []
168+
if out:
169+
json_op = json.loads(out)
170+
perms_str = json_op.get("data", {}).get(cfg_map_filename, "")
171+
for p in perms_str.replace("'", "").replace("[", "").replace("]", "").split(","):
172+
p = p.strip()
173+
if p:
174+
kubeplus_perms.append(p)
175+
new_resources.extend(kubeplus_perms)
176+
177+
run("kubectl delete configmap " + cfg_map_name + " -n " + namespace + kubeconfig)
178+
new_resources = sorted(list(set(new_resources)))
179+
with open(cfg_map_filename, "w", encoding="utf-8") as fp:
180+
fp.write(str(new_resources))
181+
run(
182+
"kubectl create configmap " + cfg_map_name + " -n " + namespace
183+
+ " --from-file=" + cfg_map_filename + kubeconfig
184+
)
185+
186+
187+
# --- Consumer RBAC ---
188+
189+
def apply_consumer_rbac(sa, namespace, kubeconfig, run_cmd=None):
190+
"""Apply ClusterRole and ClusterRoleBinding for consumer (read + apps + impersonate + portforward)."""
191+
run = run_cmd or run_command
192+
193+
role = {
194+
"apiVersion": "rbac.authorization.k8s.io/v1",
195+
"kind": "ClusterRole",
196+
"metadata": {"name": sa, "namespace": namespace},
197+
"rules": [
198+
{"apiGroups": ["*", ""], "resources": ["*"], "verbs": ["get", "watch", "list"]},
199+
{
200+
"apiGroups": ["apps"],
201+
"resources": [
202+
"deployments", "daemonsets", "deployments/rollback", "deployments/scale",
203+
"replicasets", "replicasets/scale", "statefulsets", "statefulsets/scale",
204+
],
205+
"verbs": ["get", "watch", "list", "create", "delete", "update", "patch", "deletecollection"],
206+
},
207+
{"apiGroups": [""], "resources": ["users", "groups", "serviceaccounts"], "verbs": ["impersonate"]},
208+
{"apiGroups": [""], "resources": ["pods/portforward"], "verbs": ["create", "get"]},
209+
],
210+
}
211+
create_role_rolebinding(role, sa + "-role-impersonate.yaml", kubeconfig, run_cmd)
212+
213+
role_binding = {
214+
"apiVersion": "rbac.authorization.k8s.io/v1",
215+
"kind": "ClusterRoleBinding",
216+
"metadata": {"name": sa, "namespace": namespace},
217+
"subjects": [{"kind": "ServiceAccount", "name": sa, "namespace": namespace, "apiGroup": ""}],
218+
"roleRef": {"kind": "ClusterRole", "name": sa, "apiGroup": "rbac.authorization.k8s.io"},
219+
}
220+
create_role_rolebinding(role_binding, sa + "-rolebinding-impersonate.yaml", kubeconfig, run_cmd)
221+
222+
cfg_map_filename = sa + "-perms.txt"
223+
all_resources = ["*", "deployments", "daemonsets", "pods/portforward", "users", "groups", "serviceaccounts"]
224+
all_resources = sorted(list(set(all_resources)))
225+
with open(cfg_map_filename, "w", encoding="utf-8") as fp:
226+
fp.write(str(all_resources))
227+
run(
228+
"kubectl create configmap " + sa + "-perms -n " + namespace
229+
+ " --from-file=" + cfg_map_filename + kubeconfig
230+
)
231+
232+
233+
# --- Main CLI ---
234+
235+
def main():
236+
"""Parse args and dispatch to create, delete, update, or extract."""
237+
kubeconfig_path = os.path.join(os.getenv("HOME", ""), ".kube", "config")
238+
parser = argparse.ArgumentParser(
239+
description="Generate provider or consumer kubeconfig for KubePlus."
240+
)
241+
parser.add_argument("action", help="command", choices=["create", "delete", "update", "extract"])
242+
parser.add_argument(
243+
"namespace",
244+
help="Namespace in which the ServiceAccount will be created. "
245+
"For provider: typically the KubePlus install namespace (e.g. default). "
246+
"For consumer (-c): the namespace where the consumer SA lives.",
247+
)
248+
parser.add_argument(
249+
"-k", "--kubeconfig",
250+
help="Path to kubeconfig for executing steps. Default: ~/.kube/config",
251+
)
252+
parser.add_argument(
253+
"-s", "--apiserverurl",
254+
help="API Server URL for the generated kubeconfig. Use "
255+
"kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' to retrieve it.",
256+
)
257+
parser.add_argument(
258+
"-f", "--filename",
259+
help="Output filename. Default: kubeplus-saas-provider.json (or <consumer>.json with -c)",
260+
)
261+
parser.add_argument(
262+
"-x", "--clustername",
263+
help="Cluster name for context and cluster in the generated kubeconfig file.",
264+
)
265+
permission_help = "Permissions file - use with update command. "
266+
permission_help += "JSON structure: {perms:{<apiGroup>:[{resource|resource/resourceName::<name>:[verbs]},...]}}"
267+
parser.add_argument("-p", "--permissionfile", help=permission_help)
268+
parser.add_argument(
269+
"-c", "--consumer",
270+
help="Generate consumer kubeconfig. Use consumer name as value (e.g. -c team1).",
271+
)
272+
pargs = parser.parse_args()
273+
274+
action = pargs.action
275+
namespace = pargs.namespace
276+
277+
if pargs.kubeconfig:
278+
kubeconfig_path = pargs.kubeconfig
279+
kubeconfig_string = " --kubeconfig=" + kubeconfig_path
280+
281+
api_server_ip = pargs.apiserverurl or ""
282+
permission_file = pargs.permissionfile or ""
283+
cluster_name = pargs.clustername or ""
284+
285+
if action == "update" and not permission_file:
286+
print("Permission file missing. Please provide -p/--permissionfile.")
287+
print(permission_help)
288+
sys.exit(1)
289+
290+
sa = "kubeplus-saas-provider"
291+
if pargs.consumer:
292+
sa = pargs.consumer
293+
294+
filename = pargs.filename or sa
295+
if not filename.endswith(".json"):
296+
filename += ".json"
297+
298+
if action == "create":
299+
if permission_file:
300+
print("Permissions file should be used with update command.")
301+
sys.exit(1)
302+
out, err = run_command("kubectl get ns " + namespace + kubeconfig_string)
303+
if "not found" in (out or "") or "not found" in (err or ""):
304+
run_command("kubectl create ns " + namespace + kubeconfig_string)
305+
run_command("kubectl label --overwrite=true ns " + namespace + " managedby=kubeplus " + kubeconfig_string)
306+
307+
if sa == "kubeplus-saas-provider":
308+
generate_kubeconfig(sa, namespace, filename, api_server_ip, kubeconfig_string, cluster_name or None)
309+
apply_provider_rbac(sa, namespace, kubeconfig_string)
310+
print("Provider kubeconfig created: " + filename)
311+
else:
312+
generate_kubeconfig(sa, namespace, filename, api_server_ip, kubeconfig_string, cluster_name or None)
313+
apply_consumer_rbac(sa, namespace, kubeconfig_string)
314+
print("Consumer kubeconfig created: " + filename)
315+
316+
elif action == "extract":
317+
extract_token_and_build_kubeconfig(
318+
sa, namespace, filename, api_server_ip, kubeconfig_string,
319+
cluster_name or None
320+
)
321+
print("Kubeconfig extracted: " + filename)
322+
323+
elif action == "update":
324+
update_provider_rbac(permission_file, sa, namespace, kubeconfig_string)
325+
print("Kubeconfig permissions updated: " + filename)
326+
327+
elif action == "delete":
328+
cwd = os.getcwd()
329+
run_command("kubectl delete sa " + sa + " -n " + namespace + kubeconfig_string)
330+
run_command("kubectl delete configmap " + sa + " -n " + namespace + kubeconfig_string)
331+
run_command("kubectl delete clusterrole " + sa + kubeconfig_string)
332+
run_command("kubectl delete clusterrolebinding " + sa + kubeconfig_string)
333+
run_command("kubectl delete clusterrole " + sa + "-update" + kubeconfig_string)
334+
run_command("kubectl delete clusterrolebinding " + sa + "-update" + kubeconfig_string)
335+
run_command("kubectl delete configmap " + sa + "-perms -n " + namespace + kubeconfig_string)
336+
for f in [
337+
sa + "-secret.yaml", filename, sa + "-role.yaml", sa + "-update-role.yaml",
338+
sa + "-rolebinding.yaml", sa + "-role-impersonate.yaml",
339+
sa + "-rolebinding-impersonate.yaml", sa + "-update-rolebinding.yaml",
340+
sa + "-perms.txt", sa + "-perms-update.txt",
341+
]:
342+
path = os.path.join(cwd, f)
343+
if os.path.exists(path):
344+
try:
345+
os.remove(path)
346+
except OSError:
347+
pass
348+
349+
350+
if __name__ == "__main__":
351+
main()

0 commit comments

Comments
 (0)