Skip to content

Commit 0121715

Browse files
committed
[patch] Fix connect()
1 parent 3529c9e commit 0121715

2 files changed

Lines changed: 127 additions & 74 deletions

File tree

src/mas/devops/ocp.py

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import logging
1212
import os
13-
import tempfile
13+
from pathlib import Path
1414
from time import sleep
1515

1616
from kubernetes import client, config
@@ -31,6 +31,7 @@ def connect(server: str, token: str, skipVerify: bool = False) -> bool:
3131
Connect to a target OpenShift Container Platform (OCP) cluster.
3232
3333
Configures Kubernetes client with the provided server URL and authentication token.
34+
Updates the default kubeconfig file with the new cluster context.
3435
3536
Parameters:
3637
server (str): The OpenShift cluster API server URL (e.g., "https://api.cluster.example.com:6443")
@@ -46,51 +47,77 @@ def connect(server: str, token: str, skipVerify: bool = False) -> bool:
4647
logger.info(f"Connect(server={server}, token=***)")
4748

4849
try:
49-
# Create kubeconfig structure
50-
kubeconfigDict = {
51-
"apiVersion": "v1",
52-
"kind": "Config",
53-
"clusters": [
54-
{
55-
"name": "my-cluster",
56-
"cluster": {
57-
"server": server,
58-
"insecure-skip-tls-verify": skipVerify,
59-
},
60-
}
61-
],
62-
"users": [
63-
{
64-
"name": "my-credentials",
65-
"user": {"token": token},
66-
}
67-
],
68-
"contexts": [
69-
{
70-
"name": "my-context",
71-
"context": {
72-
"cluster": "my-cluster",
73-
"user": "my-credentials",
74-
},
75-
}
76-
],
77-
"current-context": "my-context",
50+
# Determine kubeconfig path
51+
kubeconfig_path = os.environ.get("KUBECONFIG")
52+
if not kubeconfig_path:
53+
kubeconfig_path = os.path.join(Path.home(), ".kube", "config")
54+
55+
logger.debug(f"Using kubeconfig at {kubeconfig_path}")
56+
57+
# Load existing kubeconfig or create new one
58+
if os.path.exists(kubeconfig_path):
59+
with open(kubeconfig_path, "r") as f:
60+
kubeconfigDict = yaml.safe_load(f) or {}
61+
else:
62+
kubeconfigDict = {"apiVersion": "v1", "kind": "Config", "clusters": [], "users": [], "contexts": [], "current-context": ""}
63+
# Ensure directory exists
64+
os.makedirs(os.path.dirname(kubeconfig_path), exist_ok=True)
65+
66+
# Ensure required keys exist
67+
if "clusters" not in kubeconfigDict:
68+
kubeconfigDict["clusters"] = []
69+
if "users" not in kubeconfigDict:
70+
kubeconfigDict["users"] = []
71+
if "contexts" not in kubeconfigDict:
72+
kubeconfigDict["contexts"] = []
73+
74+
# Define cluster, user, and context names
75+
cluster_name = "mas-cluster"
76+
user_name = "mas-user"
77+
context_name = "mas-context"
78+
79+
# Update or add cluster
80+
cluster_entry = {
81+
"name": cluster_name,
82+
"cluster": {
83+
"server": server,
84+
"insecure-skip-tls-verify": skipVerify,
85+
},
86+
}
87+
# Remove existing cluster with same name
88+
kubeconfigDict["clusters"] = [c for c in kubeconfigDict["clusters"] if c.get("name") != cluster_name]
89+
kubeconfigDict["clusters"].append(cluster_entry)
90+
91+
# Update or add user
92+
user_entry = {"name": user_name, "user": {"token": token}}
93+
# Remove existing user with same name
94+
kubeconfigDict["users"] = [u for u in kubeconfigDict["users"] if u.get("name") != user_name]
95+
kubeconfigDict["users"].append(user_entry)
96+
97+
# Update or add context
98+
context_entry = {
99+
"name": context_name,
100+
"context": {
101+
"cluster": cluster_name,
102+
"user": user_name,
103+
},
78104
}
105+
# Remove existing context with same name
106+
kubeconfigDict["contexts"] = [c for c in kubeconfigDict["contexts"] if c.get("name") != context_name]
107+
kubeconfigDict["contexts"].append(context_entry)
79108

80-
# Write to temporary file
81-
with tempfile.NamedTemporaryFile(mode="w", suffix=".kubeconfig", delete=False) as tmpFile:
82-
tmpKubeconfigPath = tmpFile.name
83-
yaml.dump(kubeconfigDict, tmpFile)
109+
# Set current context
110+
kubeconfigDict["current-context"] = context_name
84111

85-
logger.debug(f"Created temporary kubeconfig at {tmpKubeconfigPath}")
112+
# Write updated kubeconfig
113+
with open(kubeconfig_path, "w") as f:
114+
yaml.dump(kubeconfigDict, f, default_flow_style=False)
86115

87-
# Load the configuration
88-
config.load_kube_config(config_file=tmpKubeconfigPath)
89-
logger.info("KubeConfig context changed to my-context")
116+
logger.debug(f"Updated kubeconfig at {kubeconfig_path}")
90117

91-
# Clean up temporary file
92-
os.unlink(tmpKubeconfigPath)
93-
logger.debug(f"Removed temporary kubeconfig {tmpKubeconfigPath}")
118+
# Load the configuration from the updated kubeconfig
119+
config.load_kube_config(config_file=kubeconfig_path)
120+
logger.info(f"KubeConfig context changed to {context_name}")
94121

95122
return True
96123

test/src/test_ocp_connect.py

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#
99
# *****************************************************************************
1010

11-
from unittest.mock import patch, MagicMock
11+
from unittest.mock import patch, mock_open
1212
from kubernetes.config.config_exception import ConfigException
1313

1414
from mas.devops.ocp import connect
@@ -18,15 +18,16 @@ class TestOcpConnect:
1818
"""Test suite for ocp.connect() function."""
1919

2020
@patch("mas.devops.ocp.config.load_kube_config")
21-
@patch("mas.devops.ocp.os.unlink")
22-
@patch("mas.devops.ocp.tempfile.NamedTemporaryFile")
21+
@patch("mas.devops.ocp.os.makedirs")
22+
@patch("mas.devops.ocp.os.path.exists")
23+
@patch("builtins.open", new_callable=mock_open)
24+
@patch("mas.devops.ocp.yaml.safe_load")
2325
@patch("mas.devops.ocp.yaml.dump")
24-
def test_connect_success(self, mock_yaml_dump, mock_tempfile, mock_unlink, mock_load_config):
26+
def test_connect_success(self, mock_yaml_dump, mock_yaml_load, mock_file, mock_exists, mock_makedirs, mock_load_config):
2527
"""Test successful connection to OCP cluster."""
26-
# Setup mock temporary file
27-
mock_file = MagicMock()
28-
mock_file.name = "/tmp/test.kubeconfig"
29-
mock_tempfile.return_value.__enter__.return_value = mock_file
28+
# Setup - kubeconfig exists with existing content
29+
mock_exists.return_value = True
30+
mock_yaml_load.return_value = {"apiVersion": "v1", "kind": "Config", "clusters": [], "users": [], "contexts": [], "current-context": ""}
3031

3132
# Execute
3233
result = connect(
@@ -38,19 +39,21 @@ def test_connect_success(self, mock_yaml_dump, mock_tempfile, mock_unlink, mock_
3839
# Verify
3940
assert result is True
4041
mock_yaml_dump.assert_called_once()
41-
mock_load_config.assert_called_once_with(config_file="/tmp/test.kubeconfig")
42-
mock_unlink.assert_called_once_with("/tmp/test.kubeconfig")
42+
mock_load_config.assert_called_once()
43+
# Verify the kubeconfig was written
44+
assert mock_file.call_count >= 2 # Once for read, once for write
4345

4446
@patch("mas.devops.ocp.config.load_kube_config")
45-
@patch("mas.devops.ocp.os.unlink")
46-
@patch("mas.devops.ocp.tempfile.NamedTemporaryFile")
47+
@patch("mas.devops.ocp.os.makedirs")
48+
@patch("mas.devops.ocp.os.path.exists")
49+
@patch("builtins.open", new_callable=mock_open)
50+
@patch("mas.devops.ocp.yaml.safe_load")
4751
@patch("mas.devops.ocp.yaml.dump")
48-
def test_connect_with_tls_skip(self, mock_yaml_dump, mock_tempfile, mock_unlink, mock_load_config):
52+
def test_connect_with_tls_skip(self, mock_yaml_dump, mock_yaml_load, mock_file, mock_exists, mock_makedirs, mock_load_config):
4953
"""Test connection with TLS verification skipped."""
50-
# Setup mock temporary file
51-
mock_file = MagicMock()
52-
mock_file.name = "/tmp/test.kubeconfig"
53-
mock_tempfile.return_value.__enter__.return_value = mock_file
54+
# Setup - kubeconfig exists
55+
mock_exists.return_value = True
56+
mock_yaml_load.return_value = {"apiVersion": "v1", "kind": "Config", "clusters": [], "users": [], "contexts": [], "current-context": ""}
5457

5558
# Execute
5659
result = connect(
@@ -66,15 +69,16 @@ def test_connect_with_tls_skip(self, mock_yaml_dump, mock_tempfile, mock_unlink,
6669
assert call_args["clusters"][0]["cluster"]["insecure-skip-tls-verify"] is True
6770

6871
@patch("mas.devops.ocp.config.load_kube_config")
69-
@patch("mas.devops.ocp.os.unlink")
70-
@patch("mas.devops.ocp.tempfile.NamedTemporaryFile")
72+
@patch("mas.devops.ocp.os.makedirs")
73+
@patch("mas.devops.ocp.os.path.exists")
74+
@patch("builtins.open", new_callable=mock_open)
75+
@patch("mas.devops.ocp.yaml.safe_load")
7176
@patch("mas.devops.ocp.yaml.dump")
72-
def test_connect_config_exception(self, mock_yaml_dump, mock_tempfile, mock_unlink, mock_load_config):
77+
def test_connect_config_exception(self, mock_yaml_dump, mock_yaml_load, mock_file, mock_exists, mock_makedirs, mock_load_config):
7378
"""Test connection failure with ConfigException."""
74-
# Setup mock temporary file
75-
mock_file = MagicMock()
76-
mock_file.name = "/tmp/test.kubeconfig"
77-
mock_tempfile.return_value.__enter__.return_value = mock_file
79+
# Setup - kubeconfig exists
80+
mock_exists.return_value = True
81+
mock_yaml_load.return_value = {"apiVersion": "v1", "kind": "Config", "clusters": [], "users": [], "contexts": [], "current-context": ""}
7882

7983
# Setup mock to raise ConfigException
8084
mock_load_config.side_effect = ConfigException("Invalid configuration")
@@ -87,18 +91,18 @@ def test_connect_config_exception(self, mock_yaml_dump, mock_tempfile, mock_unli
8791

8892
# Verify
8993
assert result is False
90-
mock_unlink.assert_not_called() # Should not clean up on error
9194

9295
@patch("mas.devops.ocp.config.load_kube_config")
93-
@patch("mas.devops.ocp.os.unlink")
94-
@patch("mas.devops.ocp.tempfile.NamedTemporaryFile")
96+
@patch("mas.devops.ocp.os.makedirs")
97+
@patch("mas.devops.ocp.os.path.exists")
98+
@patch("builtins.open", new_callable=mock_open)
99+
@patch("mas.devops.ocp.yaml.safe_load")
95100
@patch("mas.devops.ocp.yaml.dump")
96-
def test_connect_unexpected_exception(self, mock_yaml_dump, mock_tempfile, mock_unlink, mock_load_config):
101+
def test_connect_unexpected_exception(self, mock_yaml_dump, mock_yaml_load, mock_file, mock_exists, mock_makedirs, mock_load_config):
97102
"""Test connection failure with unexpected exception."""
98-
# Setup mock temporary file
99-
mock_file = MagicMock()
100-
mock_file.name = "/tmp/test.kubeconfig"
101-
mock_tempfile.return_value.__enter__.return_value = mock_file
103+
# Setup - kubeconfig exists
104+
mock_exists.return_value = True
105+
mock_yaml_load.return_value = {"apiVersion": "v1", "kind": "Config", "clusters": [], "users": [], "contexts": [], "current-context": ""}
102106

103107
# Setup mock to raise unexpected exception
104108
mock_load_config.side_effect = RuntimeError("Unexpected error")
@@ -111,4 +115,26 @@ def test_connect_unexpected_exception(self, mock_yaml_dump, mock_tempfile, mock_
111115

112116
# Verify
113117
assert result is False
114-
mock_unlink.assert_not_called() # Should not clean up on error
118+
119+
@patch("mas.devops.ocp.config.load_kube_config")
120+
@patch("mas.devops.ocp.os.makedirs")
121+
@patch("mas.devops.ocp.os.path.exists")
122+
@patch("builtins.open", new_callable=mock_open)
123+
@patch("mas.devops.ocp.yaml.dump")
124+
def test_connect_creates_new_kubeconfig(self, mock_yaml_dump, mock_file, mock_exists, mock_makedirs, mock_load_config):
125+
"""Test connection creates new kubeconfig when it doesn't exist."""
126+
# Setup - kubeconfig doesn't exist
127+
mock_exists.return_value = False
128+
129+
# Execute
130+
result = connect(
131+
server="https://api.test.example.com:6443",
132+
token="test-token-123",
133+
skipVerify=False,
134+
)
135+
136+
# Verify
137+
assert result is True
138+
mock_makedirs.assert_called_once() # Should create directory
139+
mock_yaml_dump.assert_called_once()
140+
mock_load_config.assert_called_once()

0 commit comments

Comments
 (0)