daktari 0.0.254__py3-none-any.whl → 0.0.255__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of daktari might be problematic. Click here for more details.

@@ -0,0 +1,113 @@
1
+ import json
2
+ import logging
3
+ import os.path
4
+ from json import JSONDecodeError
5
+ from typing import Optional
6
+
7
+ from daktari.check import Check, CheckResult
8
+ from daktari.command_utils import can_run_command
9
+ from daktari.file_utils import file_exists
10
+ from daktari.os import OS
11
+ from daktari.version_utils import get_simple_cli_version
12
+
13
+
14
+ class GoogleCloudSdkInstalled(Check):
15
+ name = "google.cloudSdkInstalled"
16
+
17
+ suggestions = {
18
+ OS.OS_X: """<cmd>brew install --cask google-cloud-sdk</cmd>
19
+
20
+ Then, add the gcloud components to your PATH.
21
+
22
+ For bash users, add this to ~/.bashrc:
23
+ source "$(brew --prefix)/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.bash.inc"
24
+
25
+ For zsh users, add this to ~/.zhsrc:
26
+ source "$(brew --prefix)/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc\" """,
27
+ OS.UBUNTU: "<cmd>sudo snap install google-cloud-sdk --classic</cmd>",
28
+ OS.GENERIC: "Install gcloud: https://cloud.google.com/sdk/docs/quickstart",
29
+ }
30
+
31
+ def check(self) -> CheckResult:
32
+ return self.verify(can_run_command("gcloud --version"), "Google Cloud SDK is <not/> installed and on $PATH")
33
+
34
+
35
+ class CloudSqlProxyInstalled(Check):
36
+ name = "google.cloudSqlProxyInstalled"
37
+
38
+ def __init__(self, required_version: Optional[str] = None, recommended_version: Optional[str] = None):
39
+ self.required_version = required_version
40
+ self.recommended_version = recommended_version
41
+
42
+ suggestions = {
43
+ OS.GENERIC: "Install Cloud SQL Proxy: <cmd>gcloud components install cloud_sql_proxy</cmd>",
44
+ }
45
+
46
+ def check(self) -> CheckResult:
47
+ installed_version = get_simple_cli_version("cloud_sql_proxy")
48
+ return self.validate_semver_expression(
49
+ "cloud_sql_proxy", installed_version, self.required_version, self.recommended_version
50
+ )
51
+
52
+
53
+ class GkeGcloudAuthPluginInstalled(Check):
54
+ name = "google.gkeGcloudAuthPluginInstalled"
55
+ depends_on = [GoogleCloudSdkInstalled]
56
+
57
+ suggestions = {
58
+ OS.UBUNTU: "<cmd>sudo apt-get install google-cloud-sdk-gke-gcloud-auth-plugin</cmd>",
59
+ OS.GENERIC: "<cmd>gcloud components install gke-gcloud-auth-plugin</cmd>",
60
+ }
61
+
62
+ def check(self) -> CheckResult:
63
+ return self.verify(can_run_command("gke-gcloud-auth-plugin --version "), "GKE auth plugin is <not/> installed")
64
+
65
+
66
+ class DockerGoogleCloudAuthConfigured(Check):
67
+ name = "google.dockerGCloudAuthConfigured"
68
+ depends_on = [GoogleCloudSdkInstalled]
69
+
70
+ def __init__(self, cloud_project, region, registry):
71
+ self.registry = registry
72
+ self.suggestions = {
73
+ OS.GENERIC: f"""
74
+ Setup gcloud authentication and docker credential helper for gcloud.
75
+ The following commands will open your browser and ask you to login and approve.
76
+ Run:
77
+ <cmd>rm -r ~/.config/gcloud</cmd>
78
+ <cmd>gcloud auth login</cmd>
79
+ <cmd>gcloud config set project {cloud_project}</cmd>
80
+ <cmd>gcloud config set --quiet compute/zone {region}</cmd>
81
+ <cmd>gcloud auth application-default login</cmd>
82
+ <cmd>gcloud auth configure-docker {registry}</cmd>
83
+ """
84
+ }
85
+
86
+ def check(self) -> CheckResult:
87
+ # Logged in with gcloud
88
+ google_config_path = os.path.expanduser("~/.config/gcloud/application_default_credentials.json")
89
+ if not file_exists(google_config_path):
90
+ return self.failed(f"{google_config_path} does not exist")
91
+
92
+ if not can_run_command("gcloud auth application-default print-access-token"):
93
+ return self.failed("Application Default Credentials are not correctly set up")
94
+
95
+ # Docker configured correctly
96
+ docker_config_path = os.path.expanduser("~/.docker/config.json")
97
+ if not file_exists(docker_config_path):
98
+ return self.failed(f"{docker_config_path} does not exist")
99
+
100
+ try:
101
+ with open(docker_config_path, "rb") as docker_config_file:
102
+ docker_config = json.load(docker_config_file)
103
+ except IOError:
104
+ logging.error(f"Exception reading {docker_config_path}", exc_info=True)
105
+ return self.failed(f"Failed to read {docker_config_path}")
106
+ except JSONDecodeError:
107
+ logging.error(f"Exception parsing {docker_config_path}", exc_info=True)
108
+ return self.failed(f"Failed to parse {docker_config_path}")
109
+
110
+ if docker_config.get("credHelpers", {}).get(self.registry) != "gcloud":
111
+ return self.failed("docker gcloud auth for {self.registry} not configured")
112
+
113
+ return self.passed("docker gcloud auth configured")
@@ -0,0 +1,248 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from json.decoder import JSONDecodeError
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from xml.etree.ElementTree import Element
8
+
9
+ import dpath.util
10
+ from semver import VersionInfo
11
+
12
+ from daktari.check import Check, CheckResult
13
+ from daktari.checks.files import FilesExist
14
+ from daktari.checks.xml import XmlFileXPathCheck
15
+ from daktari.command_utils import get_stdout
16
+ from daktari.os import OS, detect_os
17
+ from daktari.version_utils import try_parse_semver, sanitise_version_string
18
+
19
+ BUNDLE_ID_INTELLIJ_IDEA = "com.jetbrains.intellij"
20
+ BUNDLE_ID_INTELLIJ_IDEA_CE = "com.jetbrains.intellij.ce"
21
+
22
+ SNAP_NAME_INTELLIJ_IDEA = "intellij-idea-ultimate"
23
+ SNAP_NAME_INTELLIJ_IDEA_CE = "intellij-idea-community"
24
+
25
+
26
+ def locate_intellij_idea_mac():
27
+ from AppKit import NSWorkspace
28
+
29
+ for bundle_id in (BUNDLE_ID_INTELLIJ_IDEA, BUNDLE_ID_INTELLIJ_IDEA_CE):
30
+ url = NSWorkspace.sharedWorkspace().URLForApplicationWithBundleIdentifier_(bundle_id)
31
+ if url is not None:
32
+ logging.debug(f"IntelliJ IDEA location (via NSWorkspace): {url}")
33
+ return url
34
+
35
+ logging.debug("Could not find IntelliJ IDEA (via NSWorkspace)")
36
+ return None
37
+
38
+
39
+ def get_intellij_idea_version_mac() -> Optional[VersionInfo]:
40
+ intellij_url = locate_intellij_idea_mac()
41
+ if intellij_url is None:
42
+ return None
43
+ else:
44
+ from Foundation import NSBundle
45
+
46
+ version_str = NSBundle.bundleWithURL_(intellij_url).objectForInfoDictionaryKey_("CFBundleShortVersionString")
47
+
48
+ version_str = sanitise_version_string(version_str)
49
+ version = try_parse_semver(version_str)
50
+
51
+ logging.debug(f"IntelliJ IDEA version (via NSBundle): {version} ({version_str})")
52
+ return version
53
+
54
+
55
+ def get_intellij_idea_version_snap() -> Optional[VersionInfo]:
56
+ if not Path("/run/snapd.socket").is_socket():
57
+ logging.debug("/run/snapd.socket does not exist, not querying snapd")
58
+ return None
59
+
60
+ from requests_unixsocket import Session
61
+
62
+ session = Session()
63
+ snaps_req = session.get(
64
+ f"http+unix://%2Frun%2Fsnapd.socket/v2/snaps?snaps={SNAP_NAME_INTELLIJ_IDEA},{SNAP_NAME_INTELLIJ_IDEA_CE}"
65
+ )
66
+ snaps_info = snaps_req.json()
67
+ logging.debug(f"response from snapd: {snaps_info}")
68
+ version_str = dpath.util.get(snaps_info, "/result/0/version", default=None)
69
+ logging.debug(f"raw snapd version: {version_str}")
70
+ if not isinstance(version_str, str):
71
+ return None
72
+ version_str = sanitise_version_string(version_str)
73
+
74
+ version = try_parse_semver(version_str)
75
+ logging.debug(f"IntelliJ IDEA version (via snapd): {version}")
76
+ return version
77
+
78
+
79
+ def get_intellij_idea_version_tarball() -> Optional[VersionInfo]:
80
+ idea_bin_path = get_stdout(["sh", "-c", "which idea.sh"])
81
+ if idea_bin_path is None:
82
+ return None
83
+
84
+ product_info_path = os.path.join(os.path.dirname(idea_bin_path), "..", "product-info.json")
85
+ return get_intellij_version_from_product_info(product_info_path)
86
+
87
+
88
+ def get_intellij_idea_toolbox_version() -> Optional[VersionInfo]:
89
+ idea_bin_path = get_stdout(["sh", "-c", "which idea"])
90
+ if idea_bin_path is None:
91
+ return None
92
+
93
+ apps_dir = os.path.join(os.path.dirname(idea_bin_path), "..", "apps")
94
+ toolbox_apps = os.listdir(apps_dir)
95
+ logging.debug(f"Toolbox apps found: {toolbox_apps}", exc_info=True)
96
+ intellij_installs = [app for app in toolbox_apps if "intellij-idea" in app]
97
+ if len(intellij_installs) == 0:
98
+ logging.debug("No IntelliJ IDEA installs found")
99
+ return None
100
+
101
+ product_info_path = os.path.join(apps_dir, intellij_installs[0], "product-info.json")
102
+ return get_intellij_version_from_product_info(product_info_path)
103
+
104
+
105
+ def get_intellij_version_from_product_info(product_info_path: str) -> Optional[VersionInfo]:
106
+ try:
107
+ with open(product_info_path, "rb") as product_info_file:
108
+ product_info = json.load(product_info_file)
109
+ except IOError:
110
+ logging.debug("Failed to read IntelliJ IDEA product-info.json", exc_info=True)
111
+ return None
112
+ except JSONDecodeError:
113
+ logging.debug("Failed to parse IntelliJ IDEA product-info.json", exc_info=True)
114
+ return None
115
+
116
+ version_str = product_info.get("version", None)
117
+ version = try_parse_semver(version_str)
118
+ logging.debug(f"IntelliJ IDEA version (via product-info.json): {version}")
119
+ return version
120
+
121
+
122
+ def get_intellij_idea_version() -> Optional[VersionInfo]:
123
+ os = detect_os()
124
+ if os == OS.OS_X:
125
+ return get_intellij_idea_version_mac()
126
+ elif os == OS.UBUNTU:
127
+ return (
128
+ get_intellij_idea_version_snap()
129
+ or get_intellij_idea_version_tarball()
130
+ or get_intellij_idea_toolbox_version()
131
+ )
132
+ else:
133
+ return get_intellij_idea_version_tarball() or get_intellij_idea_toolbox_version()
134
+
135
+
136
+ class IntelliJIdeaInstalled(Check):
137
+ name = "intellij.installed"
138
+
139
+ suggestions = {OS.GENERIC: "Install IntelliJ Ultimate: https://www.jetbrains.com/idea/download/"}
140
+
141
+ def __init__(self, required_version: Optional[str] = None, recommended_version: Optional[str] = None):
142
+ self.required_version = required_version
143
+ self.recommended_version = recommended_version
144
+
145
+ def check(self) -> CheckResult:
146
+ intellij_version = get_intellij_idea_version()
147
+ return self.validate_semver_expression(
148
+ "IntelliJ IDEA", intellij_version, self.required_version, self.recommended_version
149
+ )
150
+
151
+
152
+ class IntelliJProjectImported(FilesExist):
153
+ name = "intellij.projectImported"
154
+ file_paths = [".idea/workspace.xml"]
155
+ pass_fail_message = "Project <not/> imported into IntelliJ"
156
+ depends_on = [IntelliJIdeaInstalled]
157
+ suggestions = {
158
+ OS.GENERIC: """
159
+ From the IntelliJ start screen, click 'Open or Import' and choose the repository root directory
160
+ """
161
+ }
162
+
163
+
164
+ class IntelliJNodePackageManagerConfigured(XmlFileXPathCheck):
165
+ name = "intellij.nodePackageManagerConfigured"
166
+ file_path = ".idea/workspace.xml"
167
+ xpath_query = "./component[@name='PropertiesComponent']"
168
+ depends_on = [IntelliJProjectImported]
169
+
170
+ def __init__(self, package_manager_path: str):
171
+ self.package_manager_path = package_manager_path
172
+ self.pass_fail_message = f"IntelliJ package manager has <not/> been set to {package_manager_path}"
173
+
174
+ self.suggestions = {
175
+ OS.GENERIC: f"""
176
+ Follow the steps to configure {self.package_manager_path} as your package manager:
177
+ https://www.jetbrains.com/help/idea/installing-and-removing-external-software-using-node-package-manager.html#ws_npm_yarn_configure_package_manager
178
+ """
179
+ }
180
+
181
+ def validate_query_result(self, result):
182
+ key_json = None if result is None else json.loads(result.text)
183
+ logging.debug(f"Raw properties json: {key_json}")
184
+ current_package_manager = str(key_json["keyToString"]["nodejs_package_manager_path"])
185
+ logging.debug(f"IntelliJ node package manager set to: {current_package_manager}")
186
+ return current_package_manager.__contains__(self.package_manager_path)
187
+
188
+
189
+ class IntelliJTypescriptCompilerPathConfigured(XmlFileXPathCheck):
190
+ name = "intellij.typescriptCompilerPathConfigured"
191
+ file_path = ".idea/compiler.xml"
192
+ xpath_query = "./component[@name='TypeScriptCompiler']/option[@name='typeScriptServiceDirectory']"
193
+ depends_on = [IntelliJProjectImported]
194
+
195
+ def __init__(self, typescript_compiler_path: str):
196
+ self.typescript_compiler_path = typescript_compiler_path
197
+ resolved_path = typescript_compiler_path.replace("$PROJECT_DIR$", os.getcwd())
198
+ self.pass_fail_message = f"IntelliJ typescript compiler path has <not/> been set to {resolved_path}"
199
+
200
+ self.suggestions = {
201
+ OS.GENERIC: f"""
202
+ Follow the steps to set {resolved_path} as your typescript compiler path:
203
+ https://www.jetbrains.com/help/idea/typescript-support.html#ws_ts_use_ts_service_checkbox
204
+ """
205
+ }
206
+
207
+ def validate_query_result(self, result):
208
+ if result is None:
209
+ self.pass_fail_message = "IntelliJ typescript compiler path is <not/> set"
210
+ return False
211
+ current_typescript_compiler_path = result.get("value")
212
+ logging.debug(f"IntelliJ typescript compiler set to: {current_typescript_compiler_path}")
213
+ return current_typescript_compiler_path == self.typescript_compiler_path
214
+
215
+
216
+ class IntelliJProjectSdkJavaVersion(XmlFileXPathCheck):
217
+ name = "intellij.jdkVersionConfigured"
218
+ file_path = ".idea/misc.xml"
219
+ xpath_query = "./component[@name='ProjectRootManager']"
220
+ depends_on = [IntelliJProjectImported]
221
+
222
+ def __init__(self, jdk_version: int):
223
+ self.jdk_version = jdk_version
224
+
225
+ self.suggestions = {
226
+ OS.GENERIC: f"""
227
+ Follow the steps to configure JDK {jdk_version}:
228
+ https://www.jetbrains.com/help/idea/sdk.html#change-project-sdk
229
+ """
230
+ }
231
+
232
+ def validate_query_result(self, result: Optional[Element]):
233
+ if result is None:
234
+ self.pass_fail_message = "IntelliJ Project SDK is not set"
235
+ return False
236
+
237
+ try:
238
+ jdk_type = result.attrib["project-jdk-type"]
239
+ if jdk_type != "JavaSDK":
240
+ self.pass_fail_message = f"IntelliJ Project SDK is not a Java JDK: {jdk_type}"
241
+ return False
242
+ except KeyError:
243
+ self.pass_fail_message = "IntelliJ Project SDK is not a Java JDK"
244
+ return False
245
+
246
+ language_level = result.attrib["languageLevel"]
247
+ self.pass_fail_message = f"IntelliJ Project SDK is <not/> set to Java {self.jdk_version}: {language_level}"
248
+ return language_level == f"JDK_{self.jdk_version}"
daktari/checks/java.py ADDED
@@ -0,0 +1,104 @@
1
+ import logging
2
+ import re
3
+ from typing import Optional
4
+
5
+ from semver import VersionInfo
6
+
7
+ from daktari.check import Check, CheckResult
8
+ from daktari.command_utils import get_stderr, run_command
9
+ from daktari.os import OS
10
+
11
+ java_version_pattern = re.compile('^.*version "(.*?)".*$', re.MULTILINE)
12
+ javac_version_pattern = re.compile("^javac (.*)$", re.MULTILINE)
13
+
14
+ one_dot_pattern = re.compile("1\\.([0-9]+)")
15
+ other_pattern = re.compile("([0-9]+)")
16
+
17
+
18
+ def get_java_version() -> Optional[VersionInfo]:
19
+ version_output = get_stderr("java -version")
20
+ return parse_java_version_output(version_output)
21
+
22
+
23
+ def get_jdk_version() -> Optional[VersionInfo]:
24
+ try:
25
+ version_output = run_command("javac -version")
26
+ except Exception:
27
+ return None
28
+
29
+ return parse_javac_version_output(version_output.stdout + version_output.stderr)
30
+
31
+
32
+ def parse_java_version_output(version_output: Optional[str]) -> Optional[VersionInfo]:
33
+ if version_output:
34
+ match = java_version_pattern.search(version_output)
35
+ if match:
36
+ version_string = match.group(1)
37
+ logging.debug(f"Java version string: {version_string}")
38
+ return parse_java_version_string(version_string)
39
+ return None
40
+
41
+
42
+ def parse_javac_version_output(version_output: Optional[str]) -> Optional[VersionInfo]:
43
+ if version_output:
44
+ match = javac_version_pattern.search(version_output)
45
+ if match:
46
+ version_string = match.group(1)
47
+ logging.debug(f"JDK version string: {version_string}")
48
+ return parse_java_version_string(version_string)
49
+ return None
50
+
51
+
52
+ def parse_java_version_string(version_string: str) -> Optional[VersionInfo]:
53
+ try:
54
+ return VersionInfo.parse(version_string)
55
+ except ValueError:
56
+ return parse_alternative_java_version_numbers(version_string)
57
+
58
+
59
+ def parse_alternative_java_version_numbers(version_string: str) -> Optional[VersionInfo]:
60
+ one_dot_match = one_dot_pattern.search(version_string)
61
+ if one_dot_match:
62
+ return VersionInfo(int(one_dot_match.group(1)))
63
+ other_pattern_match = other_pattern.search(version_string)
64
+ if other_pattern_match:
65
+ return VersionInfo(int(other_pattern_match.group(1)))
66
+ return None
67
+
68
+
69
+ class JavaVersion(Check):
70
+ name = "java.version"
71
+
72
+ def __init__(
73
+ self,
74
+ required_version: Optional[str] = None,
75
+ recommended_version: Optional[str] = None,
76
+ java_instructions: Optional[str] = None,
77
+ ):
78
+ self.required_version = required_version
79
+ self.recommended_version = recommended_version
80
+
81
+ java_instructions = f"\n\n{java_instructions}" if java_instructions else ""
82
+ self.suggestions = {OS.GENERIC: f"""Install Java{java_instructions}"""}
83
+
84
+ def check(self) -> CheckResult:
85
+ java_version = get_java_version()
86
+ logging.info(f"Java version: {java_version}")
87
+ return self.validate_semver_expression("Java", java_version, self.required_version, self.recommended_version)
88
+
89
+
90
+ class JdkVersion(Check):
91
+ name = "jdk.version"
92
+
93
+ def __init__(self, required_version: str, recommended_version: Optional[str] = None):
94
+ self.required_version = required_version
95
+ self.recommended_version = recommended_version
96
+
97
+ suggestions = {
98
+ OS.GENERIC: "Install Java JDK",
99
+ }
100
+
101
+ def check(self) -> CheckResult:
102
+ jdk_version = get_jdk_version()
103
+ logging.info(f"JDK version: {jdk_version}")
104
+ return self.validate_semver_expression("JDK", jdk_version, self.required_version, self.recommended_version)
@@ -0,0 +1,154 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ from typing import Optional, List
5
+
6
+ from semver import VersionInfo
7
+
8
+ from daktari.check import Check, CheckResult
9
+ from daktari.command_utils import get_stdout, can_run_command
10
+ from daktari.os import OS
11
+ from daktari.version_utils import try_parse_semver
12
+ from daktari.checks.google import GkeGcloudAuthPluginInstalled
13
+
14
+
15
+ class KubectlInstalled(Check):
16
+ def __init__(self, required_version: Optional[str] = None, recommended_version: Optional[str] = None):
17
+ self.required_version = required_version
18
+ self.recommended_version = recommended_version
19
+ self.name = "kubectl.installed"
20
+ self.suggestions = {
21
+ OS.OS_X: "<cmd>brew install kubectl</cmd>",
22
+ OS.UBUNTU: "<cmd>sudo snap install kubectl --classic</cmd>",
23
+ OS.GENERIC: "Install kubectl: https://kubernetes.io/docs/tasks/tools/#kubectl",
24
+ }
25
+
26
+ def check(self) -> CheckResult:
27
+ installed_version = get_kubectl_version()
28
+ return self.validate_semver_expression(
29
+ "Kubectl", installed_version, self.required_version, self.recommended_version
30
+ )
31
+
32
+
33
+ version_pattern = re.compile("Client Version: v(.*)")
34
+
35
+
36
+ def get_kubectl_version() -> Optional[VersionInfo]:
37
+ raw_version = get_stdout("kubectl version --client=true --short")
38
+ # Handle deprecation of --short
39
+ if raw_version is None:
40
+ raw_version = get_stdout("kubectl version --client=true")
41
+
42
+ if raw_version:
43
+ match = version_pattern.search(raw_version)
44
+ if match:
45
+ version = try_parse_semver(match.group(1))
46
+ logging.debug(f"Kubectl version: {version}")
47
+ return version
48
+ return None
49
+
50
+
51
+ class KubectlContextExists(Check):
52
+ depends_on = [GkeGcloudAuthPluginInstalled]
53
+
54
+ def __init__(self, context_name: str, provision_command: str = ""):
55
+ self.context_name = context_name
56
+ self.name = f"kubectl.contextExists.{context_name}"
57
+ self.suggestions = {OS.GENERIC: provision_command}
58
+
59
+ def check(self) -> CheckResult:
60
+ output = get_stdout("kubectl config get-contexts")
61
+ passed = bool(output and self.context_name in output)
62
+ if not passed:
63
+ return self.failed(f"{self.context_name} is <not/> configured for the current user")
64
+
65
+ can_connect = can_run_command(f"kubectl get ns --context {self.context_name}")
66
+ return self.verify(can_connect, f"Could <not/> connect to context {self.context_name}")
67
+
68
+
69
+ class KubectlNoExtraneousContexts(Check):
70
+ def __init__(self, expected_contexts: List[str]):
71
+ self.expected_contexts = expected_contexts
72
+ self.name = "kubectl.noExtraneousContexts"
73
+
74
+ def check(self) -> CheckResult:
75
+ output = get_stdout("kubectl config get-contexts -o name")
76
+ if not output:
77
+ return self.failed("Failed to list kubectl contexts")
78
+
79
+ contexts = output.splitlines()
80
+ extraneous_contexts = list(filter(lambda context: context not in self.expected_contexts, contexts))
81
+ if extraneous_contexts:
82
+ suggestion = "\n".join(
83
+ f"<cmd>kubectl config delete-context {context}</cmd>" for context in extraneous_contexts
84
+ )
85
+ self.suggestions = {
86
+ OS.GENERIC: suggestion,
87
+ }
88
+ return self.passed_with_warning(f"{len(extraneous_contexts)} extraneous kubectl context(s) found")
89
+
90
+ return self.passed("No extraneous kubectl contexts found")
91
+
92
+
93
+ class HelmInstalled(Check):
94
+ name = "helm.installed"
95
+
96
+ def __init__(self, required_version: Optional[str] = None, recommended_version: Optional[str] = None):
97
+ self.required_version = required_version
98
+ self.recommended_version = recommended_version
99
+ self.suggestions = {
100
+ OS.OS_X: "<cmd>brew install helm</cmd>",
101
+ OS.UBUNTU: "<cmd>sudo snap install helm --classic</cmd>",
102
+ OS.GENERIC: "Install Helm: https://helm.sh/docs/intro/install/",
103
+ }
104
+
105
+ def check(self) -> CheckResult:
106
+ installed_version = get_helm_version()
107
+ return self.validate_semver_expression(
108
+ "Helm", installed_version, self.required_version, self.recommended_version
109
+ )
110
+
111
+
112
+ helm_version_pattern = re.compile("v([0-9\\.]+)")
113
+
114
+
115
+ def get_helm_version() -> Optional[VersionInfo]:
116
+ raw_version = get_stdout("helm version --short")
117
+ if raw_version:
118
+ match = helm_version_pattern.search(raw_version)
119
+ if match:
120
+ version = try_parse_semver(match.group(1))
121
+ logging.debug(f"Helm Version: {version}")
122
+ return version
123
+ return None
124
+
125
+
126
+ class HelmRepoExists(Check):
127
+ depends_on = [HelmInstalled]
128
+
129
+ def __init__(self, repo_name: str, repo_url: str):
130
+ self.repo_name = repo_name
131
+ self.repo_url = repo_url.strip("/")
132
+ self.name = f"helm.repoExists.{repo_name}"
133
+ self.suggestions = {
134
+ OS.GENERIC: f"<cmd>helm repo add {repo_name} {repo_url} --force-update</cmd>",
135
+ }
136
+
137
+ def check(self) -> CheckResult:
138
+ output = get_stdout("helm repo list -o json")
139
+ if not output:
140
+ return self.failed("No helm repos appear to be configured for the current user.")
141
+ repo_json = json.loads(output)
142
+ repo = next(filter(lambda repo_details: repo_details.get("name") == self.repo_name, repo_json), None)
143
+ if repo is None:
144
+ return self.failed(f"{self.repo_name} is not configured for the current user")
145
+
146
+ installed_url = repo["url"].strip("/")
147
+ logging.debug(f"{self.repo_name} helm repo is installed with URL {installed_url}.")
148
+
149
+ if installed_url != self.repo_url:
150
+ return self.failed(
151
+ f"{self.repo_name} is configured to use the wrong URL. Expected {self.repo_url}, got {installed_url}"
152
+ )
153
+
154
+ return self.passed(f"{self.repo_name} is configured for the current user")