jac-scale 0.1.1__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.
- jac_scale/__init__.py +0 -0
- jac_scale/abstractions/config/app_config.jac +30 -0
- jac_scale/abstractions/config/base_config.jac +26 -0
- jac_scale/abstractions/database_provider.jac +51 -0
- jac_scale/abstractions/deployment_target.jac +64 -0
- jac_scale/abstractions/image_registry.jac +54 -0
- jac_scale/abstractions/logger.jac +20 -0
- jac_scale/abstractions/models/deployment_result.jac +27 -0
- jac_scale/abstractions/models/resource_status.jac +38 -0
- jac_scale/config_loader.jac +31 -0
- jac_scale/context.jac +14 -0
- jac_scale/factories/database_factory.jac +43 -0
- jac_scale/factories/deployment_factory.jac +43 -0
- jac_scale/factories/registry_factory.jac +32 -0
- jac_scale/factories/utility_factory.jac +34 -0
- jac_scale/impl/config_loader.impl.jac +131 -0
- jac_scale/impl/context.impl.jac +24 -0
- jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
- jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
- jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
- jac_scale/impl/serve.impl.jac +1785 -0
- jac_scale/jserver/__init__.py +0 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
- jac_scale/jserver/impl/jserver.impl.jac +79 -0
- jac_scale/jserver/jfast_api.jac +162 -0
- jac_scale/jserver/jserver.jac +101 -0
- jac_scale/memory_hierarchy.jac +138 -0
- jac_scale/plugin.jac +218 -0
- jac_scale/plugin_config.jac +175 -0
- jac_scale/providers/database/kubernetes_mongo.jac +137 -0
- jac_scale/providers/database/kubernetes_redis.jac +110 -0
- jac_scale/providers/registry/dockerhub.jac +64 -0
- jac_scale/serve.jac +118 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
- jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
- jac_scale/tests/__init__.py +0 -0
- jac_scale/tests/conftest.py +29 -0
- jac_scale/tests/fixtures/test_api.jac +159 -0
- jac_scale/tests/fixtures/todo_app.jac +68 -0
- jac_scale/tests/test_abstractions.py +88 -0
- jac_scale/tests/test_deploy_k8s.py +265 -0
- jac_scale/tests/test_examples.py +484 -0
- jac_scale/tests/test_factories.py +149 -0
- jac_scale/tests/test_file_upload.py +444 -0
- jac_scale/tests/test_k8s_utils.py +156 -0
- jac_scale/tests/test_memory_hierarchy.py +247 -0
- jac_scale/tests/test_serve.py +1835 -0
- jac_scale/tests/test_sso.py +711 -0
- jac_scale/utilities/loggers/standard_logger.jac +40 -0
- jac_scale/utils.jac +16 -0
- jac_scale-0.1.1.dist-info/METADATA +658 -0
- jac_scale-0.1.1.dist-info/RECORD +57 -0
- jac_scale-0.1.1.dist-info/WHEEL +5 -0
- jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
- jac_scale-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Test API fixture for jac-scale serve tests."""
|
|
2
|
+
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Test Functions
|
|
5
|
+
# ============================================================================
|
|
6
|
+
def add_numbers(
|
|
7
|
+
a: int, b: int
|
|
8
|
+
) -> int {
|
|
9
|
+
return a + b;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def greet(name: str = "World") -> str {
|
|
13
|
+
return "Hello, " + name + "!";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def multiply(x: int, y: int) -> int {
|
|
17
|
+
return x * y;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# Test Data Models
|
|
22
|
+
# ============================================================================
|
|
23
|
+
node Task {
|
|
24
|
+
has title: str;
|
|
25
|
+
has priority: int = 1;
|
|
26
|
+
has completed: bool = False;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
node TaskList {
|
|
30
|
+
has name: str = "My Tasks";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# Test Walkers
|
|
35
|
+
# ============================================================================
|
|
36
|
+
walker CreateTask {
|
|
37
|
+
has title: str;
|
|
38
|
+
has priority: int = 1;
|
|
39
|
+
|
|
40
|
+
can create with `root entry {
|
|
41
|
+
new_task = here ++> Task(title=self.title, priority=self.priority);
|
|
42
|
+
report new_task ;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
walker ListTasks {
|
|
47
|
+
can list with `root entry {
|
|
48
|
+
tasks = [-->];
|
|
49
|
+
report tasks ;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
walker CompleteTask {
|
|
54
|
+
has title: str;
|
|
55
|
+
|
|
56
|
+
can complete with `root entry {
|
|
57
|
+
for task in [-->] {
|
|
58
|
+
if task.title == self.title {
|
|
59
|
+
visit task;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
can mark_done with Task entry {
|
|
65
|
+
here.completed = True;
|
|
66
|
+
report here ;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
walker GetTask {
|
|
71
|
+
can get with Task entry {
|
|
72
|
+
report here ;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
walker : priv PrivateCreateTask {
|
|
77
|
+
has title: str;
|
|
78
|
+
has priority: int = 1;
|
|
79
|
+
|
|
80
|
+
can create with `root entry {
|
|
81
|
+
new_task = here ++> Task(title=self.title, priority=self.priority);
|
|
82
|
+
report {"message": "Private task created", "task": new_task} ;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
walker : pub PublicInfo {
|
|
87
|
+
can get_info with `root entry {
|
|
88
|
+
report {"message": "This is a public endpoint", "auth_required": False} ;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Async Walker Test
|
|
93
|
+
import asyncio;
|
|
94
|
+
|
|
95
|
+
"""Async walker that creates a task with simulated async delay."""
|
|
96
|
+
async walker AsyncCreateTask {
|
|
97
|
+
has title: str;
|
|
98
|
+
has delay_ms: int = 100;
|
|
99
|
+
|
|
100
|
+
async can create with `root entry {
|
|
101
|
+
report {"status": "started", "title": self.title};
|
|
102
|
+
# Simulate async operation (e.g., external API call, DB operation)
|
|
103
|
+
delay_seconds = float(self.delay_ms) / 1000.0;
|
|
104
|
+
await asyncio.sleep(delay_seconds);
|
|
105
|
+
report {"status": "after_async_wait"};
|
|
106
|
+
new_task = here ++> Task(title=self.title, priority=1);
|
|
107
|
+
report {"status": "completed", "task": new_task};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# ============================================================================
|
|
112
|
+
# File Upload Walkers
|
|
113
|
+
# ============================================================================
|
|
114
|
+
import from fastapi { UploadFile }
|
|
115
|
+
|
|
116
|
+
"""Walker that handles single file upload."""
|
|
117
|
+
walker UploadSingleFile {
|
|
118
|
+
has myfile: UploadFile;
|
|
119
|
+
has description: str = "";
|
|
120
|
+
|
|
121
|
+
can process with `root entry {
|
|
122
|
+
report {
|
|
123
|
+
"filename": self.myfile.filename,
|
|
124
|
+
"content_type": self.myfile.content_type,
|
|
125
|
+
"size": self.myfile.size,
|
|
126
|
+
"description": self.description
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
"""Walker with file upload that requires authentication (private by default)."""
|
|
132
|
+
walker SecureFileUpload {
|
|
133
|
+
has document: UploadFile;
|
|
134
|
+
has notes: str = "";
|
|
135
|
+
|
|
136
|
+
can process with `root entry {
|
|
137
|
+
report {
|
|
138
|
+
"filename": self.document.filename,
|
|
139
|
+
"content_type": self.document.content_type,
|
|
140
|
+
"size": self.document.size,
|
|
141
|
+
"notes": self.notes,
|
|
142
|
+
"authenticated": True
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
"""Public walker with file upload (no auth required)."""
|
|
148
|
+
walker : pub PublicFileUpload {
|
|
149
|
+
has attachment: UploadFile;
|
|
150
|
+
|
|
151
|
+
can process with `root entry {
|
|
152
|
+
report {
|
|
153
|
+
"filename": self.attachment.filename,
|
|
154
|
+
"content_type": self.attachment.content_type,
|
|
155
|
+
"size": self.attachment.size,
|
|
156
|
+
"public": True
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
node Task {
|
|
2
|
+
has id: int,
|
|
3
|
+
title: str,
|
|
4
|
+
completed: bool = False;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
walker CreateTask {
|
|
8
|
+
has id: int,
|
|
9
|
+
title: str;
|
|
10
|
+
|
|
11
|
+
can create with `root entry {
|
|
12
|
+
new_task = here ++> Task(id=self.id, title=self.title);
|
|
13
|
+
report new_task ;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
walker GetAllTasks {
|
|
18
|
+
can read with `root entry{
|
|
19
|
+
tasks = [-->];
|
|
20
|
+
report tasks;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
walker GetTask {
|
|
25
|
+
has id: int;
|
|
26
|
+
|
|
27
|
+
can get with `root entry{
|
|
28
|
+
for task in [-->]{
|
|
29
|
+
if (self.id == task.id){
|
|
30
|
+
report task;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
walker CompleteTask {
|
|
37
|
+
has id: int;
|
|
38
|
+
|
|
39
|
+
can complete with `root entry {
|
|
40
|
+
for task in [-->Task] {
|
|
41
|
+
if (self.id == task.id){
|
|
42
|
+
visit task;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
can mark_complete with Task entry {
|
|
48
|
+
here.completed = True;
|
|
49
|
+
report here ; # here is the visited task
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
walker DeleteTask {
|
|
54
|
+
has id: int;
|
|
55
|
+
|
|
56
|
+
can visit_node with `root entry {
|
|
57
|
+
for task in [-->] {
|
|
58
|
+
if (self.id == task.id){
|
|
59
|
+
visit task;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
can delete with `root entry {
|
|
65
|
+
del here;
|
|
66
|
+
report {"deleted": True, "id": self.id};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tests for abstraction base classes."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
# Note: These imports will work once the Jac files are compiled to Python
|
|
6
|
+
try:
|
|
7
|
+
from ..abstractions.config.app_config import AppConfig
|
|
8
|
+
from ..abstractions.models.deployment_result import DeploymentResult
|
|
9
|
+
from ..abstractions.models.resource_status import (
|
|
10
|
+
ResourceStatus,
|
|
11
|
+
ResourceStatusInfo,
|
|
12
|
+
)
|
|
13
|
+
except ImportError:
|
|
14
|
+
# If Jac files aren't compiled yet, skip these tests
|
|
15
|
+
pytest.skip("Jac modules not compiled", allow_module_level=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_deployment_result_success():
|
|
19
|
+
"""Test DeploymentResult with success."""
|
|
20
|
+
result = DeploymentResult(
|
|
21
|
+
success=True,
|
|
22
|
+
service_url="http://localhost:8000",
|
|
23
|
+
message="Deployment successful",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert result.success is True
|
|
27
|
+
assert result.is_successful() is True
|
|
28
|
+
assert result.service_url == "http://localhost:8000"
|
|
29
|
+
assert result.message == "Deployment successful"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_deployment_result_failure():
|
|
33
|
+
"""Test DeploymentResult with failure."""
|
|
34
|
+
result = DeploymentResult(
|
|
35
|
+
success=False,
|
|
36
|
+
message="Deployment failed",
|
|
37
|
+
details={"error": "Connection timeout"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert result.success is False
|
|
41
|
+
assert result.is_successful() is False
|
|
42
|
+
assert result.message == "Deployment failed"
|
|
43
|
+
assert "error" in result.details
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_resource_status_info_ready():
|
|
47
|
+
"""Test ResourceStatusInfo when resource is ready."""
|
|
48
|
+
status = ResourceStatusInfo(
|
|
49
|
+
status=ResourceStatus.RUNNING,
|
|
50
|
+
replicas=3,
|
|
51
|
+
ready_replicas=3,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert status.is_ready() is True
|
|
55
|
+
assert status.replicas == 3
|
|
56
|
+
assert status.ready_replicas == 3
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_resource_status_info_not_ready():
|
|
60
|
+
"""Test ResourceStatusInfo when resource is not ready."""
|
|
61
|
+
status = ResourceStatusInfo(
|
|
62
|
+
status=ResourceStatus.PENDING,
|
|
63
|
+
replicas=3,
|
|
64
|
+
ready_replicas=1,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert status.is_ready() is False
|
|
68
|
+
assert status.replicas == 3
|
|
69
|
+
assert status.ready_replicas == 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_resource_status_enum():
|
|
73
|
+
"""Test ResourceStatus enum values."""
|
|
74
|
+
assert ResourceStatus.UNKNOWN == "unknown"
|
|
75
|
+
assert ResourceStatus.PENDING == "pending"
|
|
76
|
+
assert ResourceStatus.RUNNING == "running"
|
|
77
|
+
assert ResourceStatus.FAILED == "failed"
|
|
78
|
+
assert ResourceStatus.STOPPED == "stopped"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_app_config_get_code_path():
|
|
82
|
+
"""Test AppConfig.get_code_path() method."""
|
|
83
|
+
app_config = AppConfig(code_folder="/path/to/code")
|
|
84
|
+
|
|
85
|
+
path = app_config.get_code_path()
|
|
86
|
+
|
|
87
|
+
assert str(path) == "/path/to/code"
|
|
88
|
+
assert hasattr(path, "exists") # Should be a Path object
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Tests for Kubernetes deployment using new factory-based architecture."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from kubernetes import client, config
|
|
9
|
+
from kubernetes.client.exceptions import ApiException
|
|
10
|
+
|
|
11
|
+
from ..abstractions.config.app_config import AppConfig
|
|
12
|
+
from ..config_loader import get_scale_config
|
|
13
|
+
from ..factories.deployment_factory import DeploymentTargetFactory
|
|
14
|
+
from ..factories.utility_factory import UtilityFactory
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _request_with_retry(
|
|
18
|
+
method: str,
|
|
19
|
+
url: str,
|
|
20
|
+
json: dict[str, Any] | None = None,
|
|
21
|
+
timeout: int = 10,
|
|
22
|
+
max_retries: int = 60,
|
|
23
|
+
retry_interval: float = 2.0,
|
|
24
|
+
) -> requests.Response:
|
|
25
|
+
"""Make an HTTP request with retry logic for 503 responses.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
method: HTTP method (GET, POST, etc.)
|
|
29
|
+
url: The URL to request
|
|
30
|
+
json: JSON payload for the request
|
|
31
|
+
timeout: Request timeout in seconds
|
|
32
|
+
max_retries: Maximum number of retries for 503 responses
|
|
33
|
+
retry_interval: Time to wait between retries in seconds
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Response object
|
|
37
|
+
"""
|
|
38
|
+
response = None
|
|
39
|
+
for attempt in range(max_retries):
|
|
40
|
+
response = requests.request(
|
|
41
|
+
method=method,
|
|
42
|
+
url=url,
|
|
43
|
+
json=json,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if response.status_code == 503:
|
|
48
|
+
print(
|
|
49
|
+
f"[DEBUG] {url} returned 503, retrying ({attempt + 1}/{max_retries})..."
|
|
50
|
+
)
|
|
51
|
+
time.sleep(retry_interval)
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
# Return last response even if it was 503
|
|
57
|
+
assert response is not None, "No response received"
|
|
58
|
+
return response
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_deploy_all_in_one():
|
|
62
|
+
"""
|
|
63
|
+
Test deployment using the new factory-based architecture.
|
|
64
|
+
Deploys the all-in-one app found in jac client examples against a live Kubernetes cluster.
|
|
65
|
+
Validates deployment, services, sends HTTP request, and tests cleanup.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# Load kubeconfig and initialize client
|
|
69
|
+
config.load_kube_config()
|
|
70
|
+
apps_v1 = client.AppsV1Api()
|
|
71
|
+
core_v1 = client.CoreV1Api()
|
|
72
|
+
|
|
73
|
+
namespace = "all-in-one"
|
|
74
|
+
app_name = namespace
|
|
75
|
+
|
|
76
|
+
# Set environment
|
|
77
|
+
os.environ.update({"APP_NAME": app_name, "K8s_NAMESPACE": namespace})
|
|
78
|
+
|
|
79
|
+
# Resolve the absolute path to the todo app folder
|
|
80
|
+
test_dir = os.path.dirname(os.path.abspath(__file__))
|
|
81
|
+
todo_app_path = os.path.join(
|
|
82
|
+
test_dir, "../../../jac-client/jac_client/examples/all-in-one"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get configuration
|
|
86
|
+
scale_config = get_scale_config()
|
|
87
|
+
target_config = scale_config.get_kubernetes_config()
|
|
88
|
+
target_config["app_name"] = app_name
|
|
89
|
+
target_config["namespace"] = namespace
|
|
90
|
+
|
|
91
|
+
# Create logger
|
|
92
|
+
logger = UtilityFactory.create_logger("standard")
|
|
93
|
+
|
|
94
|
+
# Create deployment target using factory
|
|
95
|
+
deployment_target = DeploymentTargetFactory.create(
|
|
96
|
+
"kubernetes", target_config, logger
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create app config
|
|
100
|
+
app_config = AppConfig(
|
|
101
|
+
code_folder=todo_app_path,
|
|
102
|
+
file_name="main.jac",
|
|
103
|
+
build=False,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Deploy using new architecture
|
|
107
|
+
result = deployment_target.deploy(app_config)
|
|
108
|
+
|
|
109
|
+
assert result.success is True
|
|
110
|
+
print(f"✓ Deployment successful: {result.message}")
|
|
111
|
+
|
|
112
|
+
# Wait a moment for services to stabilize
|
|
113
|
+
time.sleep(5)
|
|
114
|
+
|
|
115
|
+
# Validate the main deployment exists
|
|
116
|
+
deployment = apps_v1.read_namespaced_deployment(name=app_name, namespace=namespace)
|
|
117
|
+
assert deployment.metadata.name == app_name
|
|
118
|
+
assert deployment.spec.replicas == 1
|
|
119
|
+
|
|
120
|
+
# Validate main service
|
|
121
|
+
service = core_v1.read_namespaced_service(
|
|
122
|
+
name=f"{app_name}-service", namespace=namespace
|
|
123
|
+
)
|
|
124
|
+
assert service.spec.type == "NodePort"
|
|
125
|
+
node_port = service.spec.ports[0].node_port
|
|
126
|
+
print(f"✓ Service is exposed on NodePort: {node_port}")
|
|
127
|
+
|
|
128
|
+
# Validate MongoDB StatefulSet and Service
|
|
129
|
+
mongodb_stateful = apps_v1.read_namespaced_stateful_set(
|
|
130
|
+
name=f"{app_name}-mongodb", namespace=namespace
|
|
131
|
+
)
|
|
132
|
+
assert mongodb_stateful.metadata.name == f"{app_name}-mongodb"
|
|
133
|
+
assert mongodb_stateful.spec.service_name == f"{app_name}-mongodb-service"
|
|
134
|
+
|
|
135
|
+
mongodb_service = core_v1.read_namespaced_service(
|
|
136
|
+
name=f"{app_name}-mongodb-service", namespace=namespace
|
|
137
|
+
)
|
|
138
|
+
assert mongodb_service.spec.ports[0].port == 27017
|
|
139
|
+
|
|
140
|
+
# Validate Redis Deployment and Service
|
|
141
|
+
redis_deploy = apps_v1.read_namespaced_deployment(
|
|
142
|
+
name=f"{app_name}-redis", namespace=namespace
|
|
143
|
+
)
|
|
144
|
+
assert redis_deploy.metadata.name == f"{app_name}-redis"
|
|
145
|
+
|
|
146
|
+
redis_service = core_v1.read_namespaced_service(
|
|
147
|
+
name=f"{app_name}-redis-service", namespace=namespace
|
|
148
|
+
)
|
|
149
|
+
assert redis_service.spec.ports[0].port == 6379
|
|
150
|
+
|
|
151
|
+
# Test get_status
|
|
152
|
+
status = deployment_target.get_status(app_name)
|
|
153
|
+
assert status is not None
|
|
154
|
+
assert status.replicas >= 0
|
|
155
|
+
print(
|
|
156
|
+
f"✓ Deployment status: {status.status.value}, replicas: {status.replicas}/{status.ready_replicas}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Send POST request to create a todo (with retry for 503)
|
|
160
|
+
url = f"http://localhost:{node_port}/walker/create_todo"
|
|
161
|
+
payload = {"text": "first-task"}
|
|
162
|
+
response = _request_with_retry("POST", url, json=payload, timeout=10)
|
|
163
|
+
assert response.status_code == 200
|
|
164
|
+
print(f"✓ Successfully created todo at {url}")
|
|
165
|
+
print(f" Response: {response.json()}")
|
|
166
|
+
|
|
167
|
+
url = f"http://localhost:{node_port}/cl/app"
|
|
168
|
+
response = _request_with_retry("GET", url, timeout=100)
|
|
169
|
+
print(f"Response status code for app page: {response.status_code}")
|
|
170
|
+
assert response.status_code == 200
|
|
171
|
+
print(f"✓ Successfully reached app page at {url}")
|
|
172
|
+
|
|
173
|
+
# Cleanup using new architecture
|
|
174
|
+
deployment_target.destroy(app_name)
|
|
175
|
+
time.sleep(60) # Wait for deletion to propagate
|
|
176
|
+
|
|
177
|
+
# Verify cleanup - resources should no longer exist
|
|
178
|
+
try:
|
|
179
|
+
apps_v1.read_namespaced_deployment(app_name, namespace=namespace)
|
|
180
|
+
raise AssertionError("Deployment should have been deleted")
|
|
181
|
+
except ApiException as e:
|
|
182
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
core_v1.read_namespaced_service(f"{app_name}-service", namespace=namespace)
|
|
186
|
+
raise AssertionError("Service should have been deleted")
|
|
187
|
+
except ApiException as e:
|
|
188
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
apps_v1.read_namespaced_stateful_set(f"{app_name}-mongodb", namespace=namespace)
|
|
192
|
+
raise AssertionError("MongoDB StatefulSet should have been deleted")
|
|
193
|
+
except ApiException as e:
|
|
194
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
core_v1.read_namespaced_service(
|
|
198
|
+
f"{app_name}-mongodb-service", namespace=namespace
|
|
199
|
+
)
|
|
200
|
+
raise AssertionError("MongoDB Service should have been deleted")
|
|
201
|
+
except ApiException as e:
|
|
202
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
apps_v1.read_namespaced_deployment(f"{app_name}-redis", namespace=namespace)
|
|
206
|
+
raise AssertionError("Redis Deployment should have been deleted")
|
|
207
|
+
except ApiException as e:
|
|
208
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
core_v1.read_namespaced_service(
|
|
212
|
+
f"{app_name}-redis-service", namespace=namespace
|
|
213
|
+
)
|
|
214
|
+
raise AssertionError("Redis Service should have been deleted")
|
|
215
|
+
except ApiException as e:
|
|
216
|
+
assert e.status == 404, f"Expected 404, got {e.status}"
|
|
217
|
+
|
|
218
|
+
# Verify PVC cleanup
|
|
219
|
+
pvcs = core_v1.list_namespaced_persistent_volume_claim(namespace=namespace)
|
|
220
|
+
for pvc in pvcs.items:
|
|
221
|
+
assert not pvc.metadata.name.startswith(app_name), (
|
|
222
|
+
f"PVC '{pvc.metadata.name}' should have been deleted"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
print("✓ Cleanup verification complete - all resources properly deleted")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_deployment_target_methods():
|
|
229
|
+
"""Test individual methods of KubernetesTarget."""
|
|
230
|
+
# Load kubeconfig
|
|
231
|
+
config.load_kube_config()
|
|
232
|
+
|
|
233
|
+
namespace = "test-methods"
|
|
234
|
+
app_name = "test-methods-app"
|
|
235
|
+
|
|
236
|
+
# Set environment
|
|
237
|
+
os.environ.update({"APP_NAME": app_name, "K8s_NAMESPACE": namespace})
|
|
238
|
+
|
|
239
|
+
# Get configuration
|
|
240
|
+
scale_config = get_scale_config()
|
|
241
|
+
target_config = scale_config.get_kubernetes_config()
|
|
242
|
+
target_config["app_name"] = app_name
|
|
243
|
+
target_config["namespace"] = namespace
|
|
244
|
+
|
|
245
|
+
# Create deployment target
|
|
246
|
+
logger = UtilityFactory.create_logger("standard")
|
|
247
|
+
deployment_target = DeploymentTargetFactory.create(
|
|
248
|
+
"kubernetes", target_config, logger
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Test get_service_url (before deployment, should return None or handle gracefully)
|
|
252
|
+
service_url = deployment_target.get_service_url(app_name)
|
|
253
|
+
# Service URL may be None if service doesn't exist yet
|
|
254
|
+
assert service_url is None or isinstance(service_url, str)
|
|
255
|
+
|
|
256
|
+
# Test get_status (before deployment, should handle gracefully)
|
|
257
|
+
try:
|
|
258
|
+
status = deployment_target.get_status(app_name)
|
|
259
|
+
# Should return UNKNOWN or handle the error gracefully
|
|
260
|
+
assert status is not None
|
|
261
|
+
except Exception:
|
|
262
|
+
# It's okay if it raises an exception for non-existent deployment
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
print("✓ Deployment target methods tested")
|