1+ # Licensed to the Apache Software Foundation (ASF) under one
2+ # or more contributor license agreements. See the NOTICE file
3+ # distributed with this work for additional information
4+ # regarding copyright ownership. The ASF licenses this file
5+ # to you under the Apache License, Version 2.0 (the
6+ # "License"); you may not use this file except in compliance
7+ # with the License. You may obtain a copy of the License at
8+ #
9+ # http://www.apache.org/licenses/LICENSE-2.0
10+ #
11+ # Unless required by applicable law or agreed to in writing,
12+ # software distributed under the License is distributed on an
13+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+ # KIND, either express or implied. See the License for the
15+ # specific language governing permissions and limitations
16+ # under the License.
17+ #
18+ # this module wraps kubectl
19+ import nuvolaris.testutil as tu
20+ import nuvolaris.template as tpl
21+ import subprocess
22+ import json
23+ import logging
24+ import yaml
25+
26+
27+ output = ""
28+ error = ""
29+ returncode = -1
30+
31+ dry_run = False
32+
33+ mocker = tu.MockKube()
34+
35+ # execute kubectl commands
36+ # default namespace is nuvolaris, you can change with keyword arg namespace
37+ # default output is text
38+ # if you specify jsonpath it will filter and parse the json output
39+ # returns exceptions if errors
40+ def kubectl(*args, namespace="nuvolaris", input=None, jsonpath=None, debugresult=True, timeout=None):
41+ # support for mocked requests
42+ mres = mocker.invoke(*args)
43+ if mres:
44+ mocker.save(input)
45+ return mres
46+
47+ cmd = namespace and ["kubectl", "-n", namespace] or ["kubectl"]
48+ cmd += list(args)
49+ if jsonpath:
50+ cmd += ["-o", "jsonpath-as-json=%s" % jsonpath]
51+
52+ # if is a string, convert input in bytes
53+ try: input = input.encode('utf-8')
54+ except: pass
55+
56+ # executing
57+ logging.debug(cmd)
58+ res = subprocess.run(cmd, capture_output=True, input=input, timeout=timeout)
59+
60+ global returncode, output, error
61+ returncode = res.returncode
62+ output = res.stdout.decode()
63+ error = res.stderr.decode()
64+
65+ if res.returncode == 0:
66+ if jsonpath:
67+ try:
68+ parsed = json.loads(output)
69+ if debugresult:
70+ logging.debug("result: %s", json.dumps(parsed, indent=2))
71+ return parsed
72+ except Exception as e:
73+ logging.info(output)
74+ logging.info(e)
75+ return e
76+ else:
77+ return output
78+ logging.info(f"Error: kubectl f{cmd} input='{input}' output='{output}' error='{error}'")
79+ raise Exception(error)
80+
81+ # create a configmap from keyword arguments
82+ def configMap(name, **kwargs):
83+ """
84+ >>> import nuvolaris.kube as kube, nuvolaris.testutil as tu
85+ >>> tu.grep(kube.configMap("hello", value="world"), "kind:|name:|value:", sort=True)
86+ kind: ConfigMap
87+ name: hello
88+ value: world
89+ >>> tu.grep(kube.configMap("hello", **{"file.js":"function", "file.py": "def"}), "file.", sort=True)
90+ file.js: function
91+ file.py: def
92+ """
93+ out = yaml.safe_load("""apiVersion: v1
94+ kind: ConfigMap
95+ metadata:
96+ name: %s
97+ data: {}
98+ """% name)
99+ for key, value in kwargs.items():
100+ out['data'][key] = value
101+ return yaml.dump(out)
102+
103+ # delete an object
104+ def delete(obj, namespace="nuvolaris"):
105+ # tested with apply
106+ if not isinstance(obj, str):
107+ obj = json.dumps(obj)
108+ return kubectl("delete", "-f", "-", namespace=namespace, input=obj)
109+
110+ # shortcut
111+ def ctl(arg, jsonpath='{@}', flatten=False):
112+ import flatdict, json
113+ data = kubectl(*arg.split(), jsonpath=jsonpath)
114+ if flatten:
115+ return dict(flatdict.FlatterDict(data, delimiter="."))
116+ return data
117+
118+ # apply an object
119+ def apply(obj, namespace="nuvolaris"):
120+ if not isinstance(obj, str):
121+ obj = yaml.dump(obj, default_flow_style=False, sort_keys=False)
122+ return kubectl("apply", "-f", "-", namespace=namespace, input=obj)
123+
124+ # apply an expanded template
125+ def applyTemplate(name, data, namespace="nuvolaris"):
126+ obj = tpl.expand_template(name, data)
127+ return kubectl("apply", "-f", "-", namespace=namespace, input=obj)
128+
129+ # delete an expanded template
130+ def deleteTemplate(name, data, namespace="nuvolaris"):
131+ obj = tpl.expand_template(name, data)
132+ return kubectl("delete", "-f", "-", namespace=namespace, input=obj)
133+
134+ def get(name, namespace="nuvolaris"):
135+ try:
136+ return json.loads(kubectl("get", name, "-ojson", namespace=namespace))
137+ except:
138+ return None
139+
140+ def get_pods(selector, namespace="nuvolaris"):
141+ """
142+ filter the existing pods using the given selector expression. (ex name=mongodb-kubernetes-operator)
143+ """
144+ try:
145+ return json.loads(kubectl("get", "pods", f"--selector={selector}","-ojson",namespace=namespace))
146+ except:
147+ return None
148+
149+ def wait(name, condition, timeout="600s", namespace="nuvolaris"):
150+ try:
151+ return kubectl("wait", name, f"--for={condition}", f"--timeout={timeout}",namespace=namespace)
152+ except:
153+ return None
154+
155+ # patch an object
156+ def patch(name, data, namespace="nuvolaris", tpe="merge"):
157+ if not type(data) == str:
158+ data = json.dumps(data)
159+ res = kubectl("patch", name, "--type", tpe, "-p", data)
160+ return res
161+
162+ def scale_sts(name, replicas, namespace="nuvolaris"):
163+ try:
164+ return kubectl("scale", name, f"--replicas={replicas}" ,namespace=namespace)
165+ except:
166+ return None
167+
168+ # rollout the specified element. Normally used for DeamonSet or StatefulSet
169+ def rollout(name, namespace="nuvolaris"):
170+ try:
171+ return kubectl("rollout", "restart", name, namespace=namespace)
172+ except:
173+ return None
174+
175+ def detect_kind():
176+ try:
177+ is_kind = kubectl("get","node/nuvolaris-control-plane",
178+ namespace=None,
179+ jsonpath='{.metadata.labels.nuvolaris\\.io/kube}')
180+ return is_kind and "kind" in is_kind
181+ except:
182+ return False
0 commit comments