vantage6 5.0.0a37__py3-none-any.whl → 5.0.0a38__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 vantage6 might be problematic. Click here for more details.

Files changed (43) hide show
  1. vantage6/cli/algostore/attach.py +28 -3
  2. vantage6/cli/algostore/list.py +2 -2
  3. vantage6/cli/algostore/start.py +11 -3
  4. vantage6/cli/algostore/version.py +62 -0
  5. vantage6/cli/auth/attach.py +1 -1
  6. vantage6/cli/auth/list.py +2 -2
  7. vantage6/cli/auth/remove.py +58 -0
  8. vantage6/cli/auth/start.py +12 -8
  9. vantage6/cli/cli.py +2 -0
  10. vantage6/cli/common/attach.py +114 -0
  11. vantage6/cli/common/decorator.py +5 -3
  12. vantage6/cli/common/list.py +68 -0
  13. vantage6/cli/common/remove.py +18 -0
  14. vantage6/cli/common/stop.py +3 -9
  15. vantage6/cli/common/utils.py +44 -76
  16. vantage6/cli/common/version.py +82 -0
  17. vantage6/cli/context/__init__.py +3 -0
  18. vantage6/cli/context/algorithm_store.py +2 -2
  19. vantage6/cli/node/attach.py +27 -3
  20. vantage6/cli/node/list.py +3 -44
  21. vantage6/cli/node/start.py +11 -3
  22. vantage6/cli/node/stop.py +13 -15
  23. vantage6/cli/node/version.py +96 -33
  24. vantage6/cli/sandbox/config/base.py +10 -2
  25. vantage6/cli/sandbox/config/core.py +3 -3
  26. vantage6/cli/sandbox/config/node.py +2 -5
  27. vantage6/cli/sandbox/remove.py +17 -35
  28. vantage6/cli/sandbox/start.py +8 -0
  29. vantage6/cli/sandbox/stop.py +1 -1
  30. vantage6/cli/server/attach.py +28 -3
  31. vantage6/cli/server/list.py +2 -2
  32. vantage6/cli/server/start.py +11 -3
  33. vantage6/cli/server/version.py +31 -18
  34. vantage6/cli/template/algo_store_config.j2 +3 -0
  35. vantage6/cli/template/node_config.j2 +2 -0
  36. vantage6/cli/use/context.py +8 -1
  37. vantage6/cli/use/namespace.py +10 -7
  38. vantage6/cli/utils_kubernetes.py +270 -0
  39. {vantage6-5.0.0a37.dist-info → vantage6-5.0.0a38.dist-info}/METADATA +3 -3
  40. {vantage6-5.0.0a37.dist-info → vantage6-5.0.0a38.dist-info}/RECORD +43 -38
  41. /vantage6/cli/node/{task_cleanup/__init__.py → common/task_cleanup.py} +0 -0
  42. {vantage6-5.0.0a37.dist-info → vantage6-5.0.0a38.dist-info}/WHEEL +0 -0
  43. {vantage6-5.0.0a37.dist-info → vantage6-5.0.0a38.dist-info}/entry_points.txt +0 -0
@@ -214,7 +214,7 @@ class NodeSandboxConfigManager(BaseSandboxConfigManager):
214
214
  config_name = f"{self.server_name}-{node_name}"
215
215
 
216
216
  path_to_data_dir = self._create_and_get_data_dir(
217
- InstanceType.NODE, is_data_folder=True
217
+ InstanceType.NODE, is_data_folder=True, node_name=node_name
218
218
  )
219
219
 
220
220
  # delete old node config if it exists
@@ -262,7 +262,7 @@ class NodeSandboxConfigManager(BaseSandboxConfigManager):
262
262
  "image": (
263
263
  self.node_image
264
264
  # TODO v5+ update
265
- or "harbor2.vantage6.ai/infrastructure/node:5.0.0a36"
265
+ or "harbor2.vantage6.ai/infrastructure/node:5.0.0a37"
266
266
  ),
267
267
  "logging": {
268
268
  "level": "DEBUG",
@@ -272,9 +272,6 @@ class NodeSandboxConfigManager(BaseSandboxConfigManager):
272
272
  f"http://vantage6-{self.server_name}-auth-user-auth-keycloak."
273
273
  f"{self.namespace}.svc.cluster.local"
274
274
  ),
275
- "dev": {
276
- "task_dir_extension": str(path_to_data_dir),
277
- },
278
275
  "persistence": {
279
276
  "tasks": {
280
277
  "hostPath": str(path_to_data_dir),
@@ -4,19 +4,18 @@ from shutil import rmtree
4
4
 
5
5
  import click
6
6
 
7
- from vantage6.common import error, info
7
+ from vantage6.common import error, warning
8
8
  from vantage6.common.globals import InstanceType
9
9
 
10
+ from vantage6.cli.auth.remove import auth_remove
10
11
  from vantage6.cli.common.remove import execute_remove
11
12
  from vantage6.cli.configuration_create import select_configuration_questionnaire
12
13
  from vantage6.cli.context import get_context
13
14
  from vantage6.cli.context.algorithm_store import AlgorithmStoreContext
14
15
  from vantage6.cli.context.auth import AuthContext
15
16
  from vantage6.cli.context.node import NodeContext
16
- from vantage6.cli.context.server import ServerContext
17
17
  from vantage6.cli.globals import InfraComponentName
18
18
  from vantage6.cli.server.remove import cli_server_remove
19
- from vantage6.cli.utils import remove_file
20
19
 
21
20
 
22
21
  @click.command()
@@ -60,28 +59,6 @@ def cli_sandbox_remove(
60
59
 
61
60
  ctx = get_context(InstanceType.SERVER, name, system_folders=False, is_sandbox=True)
62
61
 
63
- # remove the server
64
- # Note that this also checks if the server is running. Therefore, it is prevented
65
- # that a running sandbox is removed.
66
- for handler in itertools.chain(ctx.log.handlers, ctx.log.root.handlers):
67
- handler.close()
68
- click_ctx.invoke(
69
- cli_server_remove, ctx=ctx, name=name, system_folders=False, force=True
70
- )
71
-
72
- # removing the server import config
73
- info("Deleting demo import config file")
74
- server_configs = ServerContext.instance_folders(
75
- InstanceType.SERVER, ctx.name, system_folders=False
76
- )
77
- import_config_to_del = Path(server_configs["dev"]) / f"{ctx.name}.yaml"
78
- remove_file(import_config_to_del, "import_configuration")
79
-
80
- # also remove the server folder
81
- server_folder = server_configs["data"]
82
- if server_folder.is_dir():
83
- rmtree(server_folder)
84
-
85
62
  # remove the store folder
86
63
  store_configs = AlgorithmStoreContext.instance_folders(
87
64
  InstanceType.ALGORITHM_STORE,
@@ -117,20 +94,13 @@ def cli_sandbox_remove(
117
94
  if auth_folder.is_dir():
118
95
  rmtree(auth_folder)
119
96
 
120
- # remove the auth config file
97
+ # remove the auth service
121
98
  auth_ctx = AuthContext(
122
99
  instance_name=f"{ctx.name}-auth",
123
100
  system_folders=False,
124
101
  is_sandbox=True,
125
102
  )
126
- execute_remove(
127
- auth_ctx,
128
- InstanceType.AUTH,
129
- InfraComponentName.AUTH,
130
- f"{ctx.name}-auth",
131
- system_folders=False,
132
- force=True,
133
- )
103
+ auth_remove(auth_ctx, f"{ctx.name}-auth", system_folders=False, force=True)
134
104
 
135
105
  # remove the nodes
136
106
  NodeContext.LOGGING_ENABLED = False
@@ -168,6 +138,18 @@ def cli_sandbox_remove(
168
138
 
169
139
  # remove data files attached to the network
170
140
  data_dirs_nodes = NodeContext.instance_folders("node", "", False)["dev"]
171
- rmtree(Path(data_dirs_nodes / ctx.name))
141
+ try:
142
+ rmtree(Path(data_dirs_nodes / ctx.name))
143
+ except Exception as e:
144
+ warning(f"Failed to delete data directory {data_dirs_nodes / ctx.name}: {e}")
172
145
 
146
+ # remove the server last - if anything goes wrong, the server is still there so the
147
+ # user can still retry the removal.
148
+ # Note that this also checks if the server is running. Therefore, it is prevented
149
+ # that a running sandbox is removed.
150
+ for handler in itertools.chain(ctx.log.handlers, ctx.log.root.handlers):
151
+ handler.close()
152
+ click_ctx.invoke(
153
+ cli_server_remove, ctx=ctx, name=name, system_folders=False, force=True
154
+ )
173
155
  # TODO remove the right data in the custom data directory if it is provided
@@ -44,6 +44,12 @@ from vantage6.cli.server.start import cli_server_start
44
44
  help="Generate this number of nodes in the development network. Only used if "
45
45
  "--re-initialize flag is provided.",
46
46
  )
47
+ @click.option(
48
+ "--node-image",
49
+ type=str,
50
+ default=None,
51
+ help="Node image to use. Only used if --re-initialize flag is provided.",
52
+ )
47
53
  @click.option(
48
54
  "--extra-node-config",
49
55
  type=click.Path("rb"),
@@ -80,6 +86,7 @@ def cli_sandbox_start(
80
86
  local_chart_dir: Path | None,
81
87
  re_initialize: bool,
82
88
  num_nodes: int,
89
+ node_image: str | None,
83
90
  extra_node_config: Path | None,
84
91
  add_dataset: tuple[str, Path] | None,
85
92
  custom_data_dir: Path | None,
@@ -101,6 +108,7 @@ def cli_sandbox_start(
101
108
  namespace=namespace,
102
109
  num_nodes=num_nodes,
103
110
  initialize=re_initialize,
111
+ node_image=node_image,
104
112
  extra_node_config=extra_node_config,
105
113
  add_dataset=add_dataset,
106
114
  custom_data_dir=custom_data_dir,
@@ -83,7 +83,7 @@ def cli_sandbox_stop(
83
83
  is_sandbox=True,
84
84
  )
85
85
 
86
- # TODO: stop the auth service
86
+ # stop the auth service
87
87
  cmd = [
88
88
  "v6",
89
89
  "auth",
@@ -2,13 +2,38 @@ import click
2
2
 
3
3
  from vantage6.common import info
4
4
 
5
- from vantage6.cli.common.utils import attach_logs
5
+ from vantage6.cli.common.attach import attach_logs
6
+ from vantage6.cli.context import InstanceType
7
+ from vantage6.cli.globals import InfraComponentName
6
8
 
7
9
 
8
10
  @click.command()
9
- def cli_server_attach() -> None:
11
+ @click.option("-n", "--name", default=None, help="Name of the configuration")
12
+ @click.option("--system", "system_folders", flag_value=True, help="Use system folders")
13
+ @click.option("--user", "system_folders", flag_value=False, help="Use user folders")
14
+ @click.option("--context", default=None, help="Kubernetes context to use")
15
+ @click.option("--namespace", default=None, help="Kubernetes namespace to use")
16
+ @click.option(
17
+ "--sandbox", "is_sandbox", flag_value=True, help="Attach to a sandbox environment"
18
+ )
19
+ def cli_server_attach(
20
+ name: str | None,
21
+ system_folders: bool,
22
+ context: str,
23
+ namespace: str,
24
+ is_sandbox: bool,
25
+ ) -> None:
10
26
  """
11
27
  Show the server logs in the current console.
12
28
  """
13
29
  info("Attaching to server logs...")
14
- attach_logs("app=vantage6-server", "component=vantage6-server")
30
+ attach_logs(
31
+ name,
32
+ instance_type=InstanceType.SERVER,
33
+ infra_component=InfraComponentName.SERVER,
34
+ system_folders=system_folders,
35
+ context=context,
36
+ namespace=namespace,
37
+ is_sandbox=is_sandbox,
38
+ additional_labels="component=vantage6-server",
39
+ )
@@ -2,7 +2,7 @@ import click
2
2
 
3
3
  from vantage6.common.globals import InstanceType
4
4
 
5
- from vantage6.cli.common.utils import get_server_configuration_list
5
+ from vantage6.cli.common.list import get_configuration_list
6
6
 
7
7
 
8
8
  @click.command()
@@ -10,4 +10,4 @@ def cli_server_configuration_list() -> None:
10
10
  """
11
11
  Print the available server configurations.
12
12
  """
13
- get_server_configuration_list(InstanceType.SERVER)
13
+ get_configuration_list(InstanceType.SERVER)
@@ -3,6 +3,7 @@ import click
3
3
  from vantage6.common import info
4
4
  from vantage6.common.globals import InstanceType, Ports
5
5
 
6
+ from vantage6.cli.common.attach import attach_logs
6
7
  from vantage6.cli.common.decorator import click_insert_context
7
8
  from vantage6.cli.common.start import (
8
9
  helm_install,
@@ -10,12 +11,11 @@ from vantage6.cli.common.start import (
10
11
  start_port_forward,
11
12
  )
12
13
  from vantage6.cli.common.utils import (
13
- attach_logs,
14
14
  create_directory_if_not_exists,
15
15
  select_context_and_namespace,
16
16
  )
17
17
  from vantage6.cli.context.server import ServerContext
18
- from vantage6.cli.globals import ChartName
18
+ from vantage6.cli.globals import ChartName, InfraComponentName
19
19
 
20
20
 
21
21
  @click.command()
@@ -95,4 +95,12 @@ def cli_server_start(
95
95
  )
96
96
 
97
97
  if attach:
98
- attach_logs("app=vantage6-server", "component=vantage6-server")
98
+ attach_logs(
99
+ name,
100
+ instance_type=InstanceType.SERVER,
101
+ infra_component=InfraComponentName.SERVER,
102
+ system_folders=system_folders,
103
+ context=context,
104
+ namespace=namespace,
105
+ is_sandbox=ctx.is_sandbox,
106
+ )
@@ -1,12 +1,11 @@
1
1
  import click
2
- import docker
2
+ import requests
3
3
 
4
- from vantage6.common import error
5
- from vantage6.common.docker.addons import check_docker_running
4
+ from vantage6.common import error, info
6
5
  from vantage6.common.globals import InstanceType
7
6
 
8
7
  from vantage6.cli import __version__
9
- from vantage6.cli.common.utils import get_running_servers, get_server_name
8
+ from vantage6.cli.common.version import get_and_select_ctx
10
9
  from vantage6.cli.globals import DEFAULT_SERVER_SYSTEM_FOLDERS
11
10
 
12
11
 
@@ -16,22 +15,36 @@ from vantage6.cli.globals import DEFAULT_SERVER_SYSTEM_FOLDERS
16
15
  @click.option(
17
16
  "--user", "system_folders", flag_value=False, default=DEFAULT_SERVER_SYSTEM_FOLDERS
18
17
  )
19
- def cli_server_version(name: str, system_folders: bool) -> None:
18
+ @click.option("--context", default=None, help="Kubernetes context to use")
19
+ @click.option("--namespace", default=None, help="Kubernetes namespace to use")
20
+ @click.option(
21
+ "--sandbox", "is_sandbox", flag_value=True, help="Is this a sandbox environment?"
22
+ )
23
+ def cli_server_version(
24
+ name: str, system_folders: bool, context: str, namespace: str, is_sandbox: bool
25
+ ) -> None:
20
26
  """
21
27
  Print the version of the vantage6 server.
22
28
  """
23
- check_docker_running()
24
- client = docker.from_env()
25
-
26
- running_server_names = get_running_servers(client, InstanceType.SERVER)
27
-
28
- name = get_server_name(
29
- name, system_folders, running_server_names, InstanceType.SERVER
29
+ ctx = get_and_select_ctx(
30
+ InstanceType.SERVER, name, system_folders, context, namespace, is_sandbox
30
31
  )
32
+ server_config = ctx.config.get("server", {})
33
+ base_url = server_config.get("baseUrl", "")
34
+ api_path = server_config.get("apiPath", "")
35
+ if not base_url:
36
+ error("No base URL found in server configuration.")
37
+ return
38
+ if not api_path:
39
+ error("No API path found in server configuration.")
40
+ return
41
+
42
+ response = requests.get(f"{base_url}{api_path}/version")
43
+ if response.status_code != 200:
44
+ error("Failed to get server version.")
45
+ return
46
+ server_version = response.json().get("version", "")
31
47
 
32
- if name in running_server_names:
33
- container = client.containers.get(name)
34
- version = container.exec_run(cmd="vserver-local version", stdout=True)
35
- click.echo({"server": version.output.decode("utf-8"), "cli": __version__})
36
- else:
37
- error(f"Server {name} is not running! Cannot provide version...")
48
+ info("")
49
+ info(f"Server version: {server_version}")
50
+ info(f"CLI version: {__version__}")
@@ -102,6 +102,9 @@ store:
102
102
  port: 7602
103
103
  {% endif %}
104
104
 
105
+ # The port to expose the store on in the cluster
106
+ port: {{ store.port | default(7602) }}
107
+
105
108
  logging:
106
109
  # Controls the logging output level. Could be one of the following
107
110
  # levels: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET
@@ -79,6 +79,8 @@ node:
79
79
  name: engineio.server
80
80
  - level: warning
81
81
  name: sqlalchemy.engine
82
+ - level: warning
83
+ name: kubernetes.client.rest
82
84
  {% endif %}
83
85
 
84
86
  {% if node.encryption is defined %}
@@ -1,11 +1,12 @@
1
1
  import click
2
2
  import questionary
3
- from kubernetes import config
3
+ from kubernetes import client, config
4
4
 
5
5
  from vantage6.common import error
6
6
 
7
7
  from vantage6.cli.config import CliConfig
8
8
  from vantage6.cli.utils import switch_context_and_namespace
9
+ from vantage6.cli.utils_kubernetes import configure_kubernetes_client_for_microk8s
9
10
 
10
11
 
11
12
  @click.command()
@@ -14,6 +15,12 @@ def cli_use_context(context: str):
14
15
  """
15
16
  Set which Kubernetes context to use.
16
17
  """
18
+ # Configure for MicroK8s if needed
19
+ config.load_kube_config()
20
+ cfg = client.Configuration.get_default_copy()
21
+ configure_kubernetes_client_for_microk8s(cfg)
22
+ client.Configuration.set_default(cfg)
23
+
17
24
  # Get available contexts
18
25
  contexts, active_context = config.list_kube_config_contexts()
19
26
  context_names = [ctx["name"] for ctx in contexts]
@@ -1,11 +1,14 @@
1
1
  import click
2
2
  import questionary
3
- from kubernetes import client, config
3
+ from kubernetes import client
4
4
 
5
5
  from vantage6.common import error
6
6
 
7
7
  from vantage6.cli.config import CliConfig
8
8
  from vantage6.cli.utils import switch_context_and_namespace
9
+ from vantage6.cli.utils_kubernetes import (
10
+ get_core_api_with_ssl_handling,
11
+ )
9
12
 
10
13
 
11
14
  @click.command()
@@ -16,15 +19,15 @@ def cli_use_namespace(namespace: str):
16
19
 
17
20
  The namespace will be created if it does not exist.
18
21
  """
19
- # Load the active context configuration
20
- config.load_kube_config()
22
+ # Configure for MicroK8s if needed
23
+ core_api, _ = get_core_api_with_ssl_handling()
21
24
 
22
25
  try:
23
- v1 = client.CoreV1Api()
24
- namespace_list = v1.list_namespace()
26
+ namespace_list = core_api.list_namespace()
25
27
  except Exception:
26
28
  error(
27
- "Failed to connect to Kubernetes cluster. Check if the cluster is running and reachable."
29
+ "Failed to connect to Kubernetes cluster. Check if the cluster is running "
30
+ "and reachable."
28
31
  )
29
32
  return
30
33
 
@@ -46,7 +49,7 @@ def cli_use_namespace(namespace: str):
46
49
  namespace_body = client.V1Namespace(
47
50
  metadata=client.V1ObjectMeta(name=namespace)
48
51
  )
49
- v1.create_namespace(namespace_body)
52
+ core_api.create_namespace(namespace_body)
50
53
 
51
54
  # Switch to the selected namespace for current context
52
55
  switch_context_and_namespace(namespace=namespace)
@@ -0,0 +1,270 @@
1
+ """
2
+ Kubernetes utility functions for Vantage6 CLI.
3
+
4
+ This module provides utilities for handling Kubernetes client configuration,
5
+ especially for MicroK8s environments that may have SSL certificate issues.
6
+
7
+ The issue is that the python kubernetes client does not automatically use the
8
+ certificate from microk8s, so it is manually configured in this module. Note that this
9
+ is only an issue for the CLI and not for running nodes/servers/stores/etc., because
10
+ those are started using the `kubectl` command, which includes the microk8scertificate
11
+ automatically.
12
+ """
13
+
14
+ import base64
15
+ import ssl
16
+ from pathlib import Path
17
+
18
+ from kubernetes import client, config
19
+ from kubernetes.config.config_exception import ConfigException
20
+
21
+ from vantage6.common import warning
22
+
23
+
24
+ def configure_kubernetes_client_for_microk8s(cfg: client.Configuration) -> None:
25
+ """
26
+ Configure the Kubernetes client to handle MicroK8s SSL certificate issues.
27
+
28
+ This function detects if we're using MicroK8s and configures the client
29
+ to handle SSL certificates appropriately for both development and production.
30
+
31
+ Parameters
32
+ ----------
33
+ cfg : client.Configuration
34
+ The Kubernetes client configuration to configure
35
+ """
36
+ # Check if we're using MicroK8s by looking at the current context
37
+ if is_microk8s_context():
38
+ _configure_microk8s_ssl(cfg)
39
+
40
+
41
+ def _configure_microk8s_ssl(cfg: client.Configuration) -> None:
42
+ """
43
+ Configure SSL settings for MicroK8s.
44
+
45
+ This function handles MicroK8s SSL certificates in a secure way by:
46
+ 1. First trying to use the MicroK8s certificate directly
47
+ 2. If that fails, falling back to a more lenient but still secure approach
48
+ """
49
+ try:
50
+ # Try to get the MicroK8s certificate and use it properly
51
+ if cert_path := _get_microk8s_certificate_path():
52
+ _configure_with_certificate(cert_path, cfg)
53
+ else:
54
+ warning(
55
+ "MicroK8s certificate not found. You may run into errors when using "
56
+ "the CLI."
57
+ )
58
+
59
+ except Exception as e:
60
+ warning(f"Could not configure MicroK8s SSL settings: {e}")
61
+ warning("You may run into errors when using the CLI.")
62
+
63
+
64
+ def _get_microk8s_certificate_path() -> Path | None:
65
+ """
66
+ Get the path to the MicroK8s certificate.
67
+
68
+ Returns
69
+ -------
70
+ Path | None
71
+ Path to the MicroK8s certificate if found, None otherwise
72
+ """
73
+ # Common MicroK8s certificate locations
74
+ possible_paths = [
75
+ Path.home() / ".kube" / "microk8s.crt",
76
+ Path("/var/snap/microk8s/current/certs/ca.crt"),
77
+ Path("/var/snap/microk8s/current/certs/server.crt"),
78
+ ]
79
+
80
+ for cert_path in possible_paths:
81
+ if cert_path.exists():
82
+ return cert_path
83
+
84
+ return None
85
+
86
+
87
+ def _configure_with_certificate(cert_path: Path, cfg: client.Configuration) -> None:
88
+ """
89
+ Configure the Kubernetes client to use a specific certificate.
90
+
91
+ Parameters
92
+ ----------
93
+ cert_path : Path
94
+ Path to the certificate file
95
+ """
96
+ try:
97
+ # Validate the certificate before using it
98
+ if not _validate_certificate(cert_path):
99
+ warning(
100
+ f"Certificate {cert_path} appears to be invalid. You may run into "
101
+ "errors when using the CLI."
102
+ )
103
+ return
104
+
105
+ cfg.verify_ssl = True
106
+ cfg.ssl_ca_cert = str(cert_path)
107
+
108
+ # Apply the configuration to the default client
109
+ client.Configuration.set_default(cfg)
110
+
111
+ except Exception as e:
112
+ warning(f"Failed to configure with certificate {cert_path}: {e}")
113
+ warning("You may run into errors when using the CLI.")
114
+
115
+
116
+ def _validate_certificate(cert_path: Path) -> bool:
117
+ """
118
+ Validate that a certificate file is readable and appears to be a valid certificate.
119
+
120
+ Parameters
121
+ ----------
122
+ cert_path : Path
123
+ Path to the certificate file
124
+
125
+ Returns
126
+ -------
127
+ bool
128
+ True if the certificate appears valid, False otherwise
129
+ """
130
+ try:
131
+ # Check if the file exists and is readable
132
+ if not cert_path.exists() or not cert_path.is_file():
133
+ return False
134
+
135
+ # Try to read the certificate content
136
+ with open(cert_path, "rb") as f:
137
+ cert_data = f.read()
138
+
139
+ # Basic validation: check if it looks like a PEM certificate
140
+ if b"-----BEGIN CERTIFICATE-----" not in cert_data:
141
+ return False
142
+
143
+ # Try to parse the certificate with Python's ssl module
144
+ if _validate_certificate_with_ssl(cert_data):
145
+ return True
146
+
147
+ # If we can't parse it with ssl module, at least check it has the right
148
+ # structure
149
+ return (
150
+ b"-----BEGIN CERTIFICATE-----" in cert_data
151
+ and b"-----END CERTIFICATE-----" in cert_data
152
+ )
153
+
154
+ except Exception as e:
155
+ warning(f"Certificate validation failed: {e}")
156
+ return False
157
+
158
+
159
+ def _validate_certificate_with_ssl(cert_data: bytes) -> bool:
160
+ """
161
+ Validate a certificate with the ssl module.
162
+
163
+ Parameters
164
+ ----------
165
+ cert_data : bytes
166
+ The certificate data
167
+
168
+ Returns
169
+ -------
170
+ bool
171
+ True if the certificate appears valid, False otherwise
172
+ """
173
+ try:
174
+ # Extract the base64 part between BEGIN and END
175
+ start_text = b"-----BEGIN CERTIFICATE-----"
176
+ len_start_text = len(start_text)
177
+ start = cert_data.find(start_text)
178
+ end = cert_data.find(b"-----END CERTIFICATE-----")
179
+ if start != -1 and end != -1:
180
+ cert_b64 = (
181
+ cert_data[start + len_start_text : end]
182
+ .replace(b"\n", b"")
183
+ .replace(b"\r", b"")
184
+ )
185
+ cert_der = base64.b64decode(cert_b64)
186
+ ssl.DER_cert_to_PEM_cert(cert_der)
187
+ return True
188
+ except Exception:
189
+ pass
190
+ return False
191
+
192
+
193
+ def load_kubernetes_config_with_fallback() -> bool:
194
+ """
195
+ Load Kubernetes configuration with fallback for development environments.
196
+
197
+ This function tries to load the Kubernetes configuration and handles
198
+ common issues like SSL certificate problems in development environments.
199
+
200
+ Returns
201
+ -------
202
+ bool
203
+ True if configuration was loaded successfully, False otherwise
204
+ """
205
+ # Try to load in-cluster config first (for when running inside Kubernetes)
206
+ try:
207
+ config.load_incluster_config()
208
+ return True
209
+ except ConfigException:
210
+ pass
211
+
212
+ # Fallback to kubeconfig
213
+ try:
214
+ # Load kubeconfig into default config, then adjust CA if MicroK8s
215
+ config.load_kube_config()
216
+ cfg = client.Configuration.get_default_copy()
217
+ configure_kubernetes_client_for_microk8s(cfg)
218
+ client.Configuration.set_default(cfg)
219
+
220
+ return True
221
+ except ConfigException as exc:
222
+ warning(f"Failed to load Kubernetes configuration: {exc}")
223
+ return False
224
+
225
+
226
+ def create_kubernetes_apis_with_ssl_handling() -> tuple[
227
+ client.CoreV1Api, client.BatchV1Api
228
+ ]:
229
+ """
230
+ Create Kubernetes API clients with SSL handling for development environments.
231
+
232
+ Returns
233
+ -------
234
+ tuple[client.CoreV1Api, client.BatchV1Api]
235
+ Tuple of CoreV1Api and BatchV1Api clients
236
+ """
237
+ # Load configuration with fallback handling
238
+ if not load_kubernetes_config_with_fallback():
239
+ raise RuntimeError("Failed to load Kubernetes configuration")
240
+
241
+ # Create API clients
242
+ core_api = client.CoreV1Api()
243
+ batch_api = client.BatchV1Api()
244
+
245
+ return core_api, batch_api
246
+
247
+
248
+ def get_core_api_with_ssl_handling() -> client.CoreV1Api:
249
+ """
250
+ Get the CoreV1Api client with SSL handling for development environments.
251
+ """
252
+ core_api, _ = create_kubernetes_apis_with_ssl_handling()
253
+ return core_api
254
+
255
+
256
+ def is_microk8s_context() -> bool:
257
+ """
258
+ Check if the current Kubernetes context is MicroK8s.
259
+
260
+ Returns
261
+ -------
262
+ bool
263
+ True if using MicroK8s context, False otherwise
264
+ """
265
+ try:
266
+ _, active_context = config.list_kube_config_contexts()
267
+ current_context = active_context.get("name", "") if active_context else ""
268
+ return "microk8s" in current_context.lower()
269
+ except Exception:
270
+ return False