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.
- daktari/__init__.py +1 -1
- daktari/checks/__init__.py +0 -0
- daktari/checks/android.py +26 -0
- daktari/checks/aws.py +34 -0
- daktari/checks/certs.py +34 -0
- daktari/checks/conan.py +96 -0
- daktari/checks/direnv.py +59 -0
- daktari/checks/docker.py +50 -0
- daktari/checks/files.py +72 -0
- daktari/checks/flutter.py +46 -0
- daktari/checks/git.py +198 -0
- daktari/checks/google.py +113 -0
- daktari/checks/intellij_idea.py +248 -0
- daktari/checks/java.py +104 -0
- daktari/checks/kubernetes.py +154 -0
- daktari/checks/misc.py +302 -0
- daktari/checks/nodejs.py +92 -0
- daktari/checks/onepassword.py +105 -0
- daktari/checks/python.py +16 -0
- daktari/checks/ssh.py +33 -0
- daktari/checks/terraform.py +70 -0
- daktari/checks/test_certs.py +32 -0
- daktari/checks/test_intellij_idea.py +66 -0
- daktari/checks/test_java.py +88 -0
- daktari/checks/test_misc.py +18 -0
- daktari/checks/test_onepassword.py +31 -0
- daktari/checks/test_ssh.py +22 -0
- daktari/checks/test_yarn.py +86 -0
- daktari/checks/xml.py +33 -0
- daktari/checks/yarn.py +156 -0
- {daktari-0.0.254.dist-info → daktari-0.0.255.dist-info}/METADATA +2 -2
- daktari-0.0.255.dist-info/RECORD +64 -0
- daktari-0.0.254.dist-info/RECORD +0 -35
- {daktari-0.0.254.dist-info → daktari-0.0.255.dist-info}/LICENSE.txt +0 -0
- {daktari-0.0.254.dist-info → daktari-0.0.255.dist-info}/WHEEL +0 -0
- {daktari-0.0.254.dist-info → daktari-0.0.255.dist-info}/entry_points.txt +0 -0
- {daktari-0.0.254.dist-info → daktari-0.0.255.dist-info}/top_level.txt +0 -0
daktari/checks/google.py
ADDED
|
@@ -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")
|