agentex-sdk 0.1.0a6__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.
- agentex/__init__.py +103 -0
- agentex/_base_client.py +1992 -0
- agentex/_client.py +506 -0
- agentex/_compat.py +219 -0
- agentex/_constants.py +14 -0
- agentex/_exceptions.py +108 -0
- agentex/_files.py +123 -0
- agentex/_models.py +829 -0
- agentex/_qs.py +150 -0
- agentex/_resource.py +43 -0
- agentex/_response.py +830 -0
- agentex/_streaming.py +333 -0
- agentex/_types.py +219 -0
- agentex/_utils/__init__.py +57 -0
- agentex/_utils/_logs.py +25 -0
- agentex/_utils/_proxy.py +65 -0
- agentex/_utils/_reflection.py +42 -0
- agentex/_utils/_resources_proxy.py +24 -0
- agentex/_utils/_streams.py +12 -0
- agentex/_utils/_sync.py +86 -0
- agentex/_utils/_transform.py +447 -0
- agentex/_utils/_typing.py +151 -0
- agentex/_utils/_utils.py +422 -0
- agentex/_version.py +4 -0
- agentex/lib/.keep +4 -0
- agentex/lib/__init__.py +0 -0
- agentex/lib/adk/__init__.py +41 -0
- agentex/lib/adk/_modules/__init__.py +0 -0
- agentex/lib/adk/_modules/acp.py +247 -0
- agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
- agentex/lib/adk/_modules/agents.py +77 -0
- agentex/lib/adk/_modules/events.py +141 -0
- agentex/lib/adk/_modules/messages.py +285 -0
- agentex/lib/adk/_modules/state.py +291 -0
- agentex/lib/adk/_modules/streaming.py +75 -0
- agentex/lib/adk/_modules/tasks.py +124 -0
- agentex/lib/adk/_modules/tracing.py +194 -0
- agentex/lib/adk/providers/__init__.py +9 -0
- agentex/lib/adk/providers/_modules/__init__.py +0 -0
- agentex/lib/adk/providers/_modules/litellm.py +232 -0
- agentex/lib/adk/providers/_modules/openai.py +416 -0
- agentex/lib/adk/providers/_modules/sgp.py +85 -0
- agentex/lib/adk/utils/__init__.py +5 -0
- agentex/lib/adk/utils/_modules/__init__.py +0 -0
- agentex/lib/adk/utils/_modules/templating.py +94 -0
- agentex/lib/cli/__init__.py +0 -0
- agentex/lib/cli/commands/__init__.py +0 -0
- agentex/lib/cli/commands/agents.py +328 -0
- agentex/lib/cli/commands/init.py +227 -0
- agentex/lib/cli/commands/main.py +33 -0
- agentex/lib/cli/commands/secrets.py +169 -0
- agentex/lib/cli/commands/tasks.py +118 -0
- agentex/lib/cli/commands/uv.py +133 -0
- agentex/lib/cli/handlers/__init__.py +0 -0
- agentex/lib/cli/handlers/agent_handlers.py +160 -0
- agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
- agentex/lib/cli/handlers/deploy_handlers.py +351 -0
- agentex/lib/cli/handlers/run_handlers.py +452 -0
- agentex/lib/cli/handlers/secret_handlers.py +670 -0
- agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/default/README.md.j2 +193 -0
- agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
- agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/sync/README.md.j2 +293 -0
- agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
- agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
- agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
- agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
- agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
- agentex/lib/cli/utils/cli_utils.py +14 -0
- agentex/lib/cli/utils/credential_utils.py +103 -0
- agentex/lib/cli/utils/exceptions.py +6 -0
- agentex/lib/cli/utils/kubectl_utils.py +135 -0
- agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
- agentex/lib/core/__init__.py +0 -0
- agentex/lib/core/adapters/__init__.py +0 -0
- agentex/lib/core/adapters/llm/__init__.py +1 -0
- agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
- agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
- agentex/lib/core/adapters/llm/port.py +24 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
- agentex/lib/core/adapters/streams/port.py +50 -0
- agentex/lib/core/clients/__init__.py +1 -0
- agentex/lib/core/clients/temporal/__init__.py +0 -0
- agentex/lib/core/clients/temporal/temporal_client.py +181 -0
- agentex/lib/core/clients/temporal/types.py +47 -0
- agentex/lib/core/clients/temporal/utils.py +56 -0
- agentex/lib/core/services/__init__.py +0 -0
- agentex/lib/core/services/adk/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/acp.py +210 -0
- agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
- agentex/lib/core/services/adk/agents.py +43 -0
- agentex/lib/core/services/adk/events.py +61 -0
- agentex/lib/core/services/adk/messages.py +164 -0
- agentex/lib/core/services/adk/providers/__init__.py +0 -0
- agentex/lib/core/services/adk/providers/litellm.py +256 -0
- agentex/lib/core/services/adk/providers/openai.py +723 -0
- agentex/lib/core/services/adk/providers/sgp.py +99 -0
- agentex/lib/core/services/adk/state.py +120 -0
- agentex/lib/core/services/adk/streaming.py +262 -0
- agentex/lib/core/services/adk/tasks.py +69 -0
- agentex/lib/core/services/adk/tracing.py +36 -0
- agentex/lib/core/services/adk/utils/__init__.py +0 -0
- agentex/lib/core/services/adk/utils/templating.py +58 -0
- agentex/lib/core/temporal/__init__.py +0 -0
- agentex/lib/core/temporal/activities/__init__.py +207 -0
- agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
- agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
- agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
- agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
- agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
- agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
- agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
- agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
- agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
- agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
- agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
- agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
- agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
- agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
- agentex/lib/core/temporal/services/__init__.py +0 -0
- agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
- agentex/lib/core/temporal/types/__init__.py +0 -0
- agentex/lib/core/temporal/types/workflow.py +5 -0
- agentex/lib/core/temporal/workers/__init__.py +0 -0
- agentex/lib/core/temporal/workers/worker.py +162 -0
- agentex/lib/core/temporal/workflows/workflow.py +26 -0
- agentex/lib/core/tracing/__init__.py +5 -0
- agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
- agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
- agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
- agentex/lib/core/tracing/trace.py +311 -0
- agentex/lib/core/tracing/tracer.py +70 -0
- agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
- agentex/lib/environment_variables.py +87 -0
- agentex/lib/py.typed +0 -0
- agentex/lib/sdk/__init__.py +0 -0
- agentex/lib/sdk/config/__init__.py +0 -0
- agentex/lib/sdk/config/agent_config.py +61 -0
- agentex/lib/sdk/config/agent_manifest.py +219 -0
- agentex/lib/sdk/config/build_config.py +35 -0
- agentex/lib/sdk/config/deployment_config.py +117 -0
- agentex/lib/sdk/config/local_development_config.py +56 -0
- agentex/lib/sdk/config/project_config.py +103 -0
- agentex/lib/sdk/fastacp/__init__.py +3 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
- agentex/lib/sdk/fastacp/fastacp.py +74 -0
- agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
- agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
- agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
- agentex/lib/sdk/fastacp/tests/README.md +297 -0
- agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
- agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
- agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
- agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
- agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
- agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
- agentex/lib/sdk/state_machine/__init__.py +6 -0
- agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
- agentex/lib/sdk/state_machine/state.py +10 -0
- agentex/lib/sdk/state_machine/state_machine.py +189 -0
- agentex/lib/sdk/state_machine/state_workflow.py +16 -0
- agentex/lib/sdk/utils/__init__.py +0 -0
- agentex/lib/sdk/utils/messages.py +223 -0
- agentex/lib/types/__init__.py +0 -0
- agentex/lib/types/acp.py +94 -0
- agentex/lib/types/agent_configs.py +79 -0
- agentex/lib/types/agent_results.py +29 -0
- agentex/lib/types/credentials.py +34 -0
- agentex/lib/types/fastacp.py +61 -0
- agentex/lib/types/files.py +13 -0
- agentex/lib/types/json_rpc.py +49 -0
- agentex/lib/types/llm_messages.py +354 -0
- agentex/lib/types/task_message_updates.py +171 -0
- agentex/lib/types/tracing.py +34 -0
- agentex/lib/utils/__init__.py +0 -0
- agentex/lib/utils/completions.py +131 -0
- agentex/lib/utils/console.py +14 -0
- agentex/lib/utils/io.py +29 -0
- agentex/lib/utils/iterables.py +14 -0
- agentex/lib/utils/json_schema.py +23 -0
- agentex/lib/utils/logging.py +31 -0
- agentex/lib/utils/mcp.py +17 -0
- agentex/lib/utils/model_utils.py +46 -0
- agentex/lib/utils/parsing.py +15 -0
- agentex/lib/utils/regex.py +6 -0
- agentex/lib/utils/temporal.py +13 -0
- agentex/py.typed +0 -0
- agentex/resources/__init__.py +103 -0
- agentex/resources/agents.py +707 -0
- agentex/resources/events.py +294 -0
- agentex/resources/messages/__init__.py +33 -0
- agentex/resources/messages/batch.py +271 -0
- agentex/resources/messages/messages.py +492 -0
- agentex/resources/spans.py +557 -0
- agentex/resources/states.py +544 -0
- agentex/resources/tasks.py +615 -0
- agentex/resources/tracker.py +384 -0
- agentex/types/__init__.py +56 -0
- agentex/types/acp_type.py +7 -0
- agentex/types/agent.py +29 -0
- agentex/types/agent_list_params.py +13 -0
- agentex/types/agent_list_response.py +10 -0
- agentex/types/agent_rpc_by_name_params.py +21 -0
- agentex/types/agent_rpc_params.py +51 -0
- agentex/types/agent_rpc_params1.py +21 -0
- agentex/types/agent_rpc_response.py +20 -0
- agentex/types/agent_rpc_result.py +90 -0
- agentex/types/agent_task_tracker.py +34 -0
- agentex/types/data_content.py +30 -0
- agentex/types/data_content_param.py +31 -0
- agentex/types/data_delta.py +14 -0
- agentex/types/event.py +29 -0
- agentex/types/event_list_params.py +22 -0
- agentex/types/event_list_response.py +10 -0
- agentex/types/message_author.py +7 -0
- agentex/types/message_create_params.py +18 -0
- agentex/types/message_list_params.py +14 -0
- agentex/types/message_list_response.py +10 -0
- agentex/types/message_style.py +7 -0
- agentex/types/message_update_params.py +18 -0
- agentex/types/messages/__init__.py +8 -0
- agentex/types/messages/batch_create_params.py +16 -0
- agentex/types/messages/batch_create_response.py +10 -0
- agentex/types/messages/batch_update_params.py +16 -0
- agentex/types/messages/batch_update_response.py +10 -0
- agentex/types/shared/__init__.py +3 -0
- agentex/types/shared/task_message_update.py +83 -0
- agentex/types/span.py +36 -0
- agentex/types/span_create_params.py +40 -0
- agentex/types/span_list_params.py +12 -0
- agentex/types/span_list_response.py +10 -0
- agentex/types/span_update_params.py +37 -0
- agentex/types/state.py +25 -0
- agentex/types/state_create_params.py +16 -0
- agentex/types/state_list_params.py +16 -0
- agentex/types/state_list_response.py +10 -0
- agentex/types/state_update_params.py +16 -0
- agentex/types/task.py +23 -0
- agentex/types/task_delete_by_name_response.py +8 -0
- agentex/types/task_delete_response.py +8 -0
- agentex/types/task_list_response.py +10 -0
- agentex/types/task_message.py +33 -0
- agentex/types/task_message_content.py +16 -0
- agentex/types/task_message_content_param.py +17 -0
- agentex/types/task_message_delta.py +16 -0
- agentex/types/text_content.py +53 -0
- agentex/types/text_content_param.py +54 -0
- agentex/types/text_delta.py +14 -0
- agentex/types/tool_request_content.py +36 -0
- agentex/types/tool_request_content_param.py +37 -0
- agentex/types/tool_request_delta.py +18 -0
- agentex/types/tool_response_content.py +36 -0
- agentex/types/tool_response_content_param.py +36 -0
- agentex/types/tool_response_delta.py +18 -0
- agentex/types/tracker_list_params.py +16 -0
- agentex/types/tracker_list_response.py +10 -0
- agentex/types/tracker_update_params.py +19 -0
- agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
- agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
- agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
- agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
- agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,670 @@
|
|
1
|
+
import base64
|
2
|
+
import json
|
3
|
+
from collections import defaultdict
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
import questionary
|
8
|
+
import typer
|
9
|
+
import yaml
|
10
|
+
from kubernetes.client.rest import ApiException
|
11
|
+
from rich.console import Console
|
12
|
+
|
13
|
+
from agentex.lib.cli.utils.cli_utils import handle_questionary_cancellation
|
14
|
+
from agentex.lib.cli.utils.kubectl_utils import get_k8s_client
|
15
|
+
from agentex.lib.cli.utils.kubernetes_secrets_utils import (
|
16
|
+
KUBERNETES_SECRET_TO_MANIFEST_KEY,
|
17
|
+
KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON,
|
18
|
+
KUBERNETES_SECRET_TYPE_OPAQUE,
|
19
|
+
VALID_SECRET_TYPES,
|
20
|
+
create_image_pull_secret_with_data,
|
21
|
+
create_secret_with_data,
|
22
|
+
get_secret_data,
|
23
|
+
update_image_pull_secret_with_data,
|
24
|
+
update_secret_with_data,
|
25
|
+
)
|
26
|
+
from agentex.lib.sdk.config.agent_config import AgentConfig
|
27
|
+
from agentex.lib.sdk.config.agent_manifest import AgentManifest
|
28
|
+
from agentex.lib.sdk.config.deployment_config import (
|
29
|
+
DeploymentConfig,
|
30
|
+
ImagePullSecretConfig,
|
31
|
+
InjectedSecretsValues,
|
32
|
+
)
|
33
|
+
from agentex.lib.types.credentials import CredentialMapping
|
34
|
+
from agentex.lib.utils.logging import make_logger
|
35
|
+
|
36
|
+
logger = make_logger(__name__)
|
37
|
+
console = Console()
|
38
|
+
|
39
|
+
|
40
|
+
# TODO: parse this into a Pydantic model.
|
41
|
+
def load_values_file(values_path: str) -> dict[str, dict[str, str]]:
|
42
|
+
"""Load and parse the values file (YAML/JSON)"""
|
43
|
+
try:
|
44
|
+
path = Path(values_path)
|
45
|
+
content = path.read_text()
|
46
|
+
|
47
|
+
if path.suffix.lower() in [".yaml", ".yml"]:
|
48
|
+
data = yaml.safe_load(content)
|
49
|
+
elif path.suffix.lower() == ".json":
|
50
|
+
data = json.loads(content)
|
51
|
+
else:
|
52
|
+
# Try YAML first, then JSON
|
53
|
+
try:
|
54
|
+
data = yaml.safe_load(content)
|
55
|
+
except yaml.YAMLError:
|
56
|
+
data = json.loads(content)
|
57
|
+
return InjectedSecretsValues.model_validate(data).model_dump()
|
58
|
+
|
59
|
+
except Exception as e:
|
60
|
+
raise RuntimeError(
|
61
|
+
f"Failed to load values file '{values_path}': {str(e)}"
|
62
|
+
) from e
|
63
|
+
|
64
|
+
|
65
|
+
def interactive_secret_input(secret_name: str, secret_key: str) -> str:
|
66
|
+
"""Prompt user for secret value with appropriate input method"""
|
67
|
+
console.print(
|
68
|
+
f"\n[bold]Enter value for secret '[cyan]{secret_name}[/cyan]' key '[cyan]{secret_key}[/cyan]':[/bold]"
|
69
|
+
)
|
70
|
+
|
71
|
+
input_type = questionary.select(
|
72
|
+
"What type of value is this?",
|
73
|
+
choices=[
|
74
|
+
"Simple text",
|
75
|
+
"Sensitive/password (hidden input)",
|
76
|
+
"Multi-line text",
|
77
|
+
"JSON/YAML content",
|
78
|
+
"Read from file",
|
79
|
+
],
|
80
|
+
).ask()
|
81
|
+
|
82
|
+
input_type = handle_questionary_cancellation(input_type, "secret input")
|
83
|
+
|
84
|
+
if input_type == "Sensitive/password (hidden input)":
|
85
|
+
result = questionary.password("Enter value (input will be hidden):").ask()
|
86
|
+
return handle_questionary_cancellation(result, "password input")
|
87
|
+
|
88
|
+
elif input_type == "Multi-line text":
|
89
|
+
console.print(
|
90
|
+
"[yellow]Enter multi-line text (press Ctrl+D when finished):[/yellow]"
|
91
|
+
)
|
92
|
+
lines = []
|
93
|
+
try:
|
94
|
+
while True:
|
95
|
+
line = input()
|
96
|
+
lines.append(line)
|
97
|
+
except EOFError:
|
98
|
+
pass
|
99
|
+
except KeyboardInterrupt:
|
100
|
+
console.print("[yellow]Multi-line input cancelled by user[/yellow]")
|
101
|
+
raise typer.Exit(0) # noqa
|
102
|
+
return "\n".join(lines)
|
103
|
+
|
104
|
+
elif input_type == "JSON/YAML content":
|
105
|
+
value = questionary.text("Enter JSON/YAML content:").ask()
|
106
|
+
value = handle_questionary_cancellation(value, "JSON/YAML input")
|
107
|
+
# Validate JSON/YAML format
|
108
|
+
try:
|
109
|
+
json.loads(value)
|
110
|
+
except json.JSONDecodeError:
|
111
|
+
try:
|
112
|
+
yaml.safe_load(value)
|
113
|
+
except yaml.YAMLError:
|
114
|
+
console.print(
|
115
|
+
"[yellow]Warning: Content doesn't appear to be valid JSON or YAML[/yellow]"
|
116
|
+
)
|
117
|
+
return value
|
118
|
+
|
119
|
+
elif input_type == "Read from file":
|
120
|
+
file_path = questionary.path("Enter file path:").ask()
|
121
|
+
file_path = handle_questionary_cancellation(file_path, "file path input")
|
122
|
+
try:
|
123
|
+
return Path(file_path).read_text().strip()
|
124
|
+
except Exception as e:
|
125
|
+
console.print(f"[red]Error reading file: {e}[/red]")
|
126
|
+
manual_value = questionary.text("Enter value manually:").ask()
|
127
|
+
return handle_questionary_cancellation(manual_value, "manual value input")
|
128
|
+
|
129
|
+
else: # Simple text
|
130
|
+
result = questionary.text("Enter value:").ask()
|
131
|
+
return handle_questionary_cancellation(result, "text input")
|
132
|
+
|
133
|
+
|
134
|
+
def get_secret(name: str, namespace: str, context: str | None = None) -> dict:
|
135
|
+
"""Get details about a secret"""
|
136
|
+
v1 = get_k8s_client(context)
|
137
|
+
|
138
|
+
try:
|
139
|
+
secret = v1.read_namespaced_secret(name=name, namespace=namespace)
|
140
|
+
return {
|
141
|
+
"name": secret.metadata.name,
|
142
|
+
"namespace": namespace,
|
143
|
+
"created": secret.metadata.creation_timestamp.isoformat(),
|
144
|
+
"exists": True,
|
145
|
+
}
|
146
|
+
except ApiException as e:
|
147
|
+
if e.status == 404:
|
148
|
+
console.print(
|
149
|
+
f"[red]Error: Secret '{name}' not found in namespace '{namespace}'[/red]"
|
150
|
+
)
|
151
|
+
return {"name": name, "namespace": namespace, "exists": False}
|
152
|
+
raise RuntimeError(f"Failed to get secret: {str(e)}") from e
|
153
|
+
|
154
|
+
|
155
|
+
def delete_secret(name: str, namespace: str, context: str | None = None) -> None:
|
156
|
+
"""Delete a secret"""
|
157
|
+
v1 = get_k8s_client(context)
|
158
|
+
|
159
|
+
try:
|
160
|
+
v1.delete_namespaced_secret(name=name, namespace=namespace)
|
161
|
+
console.print(
|
162
|
+
f"[green]Deleted secret '{name}' from namespace '{namespace}'[/green]"
|
163
|
+
)
|
164
|
+
except ApiException as e:
|
165
|
+
if e.status == 404:
|
166
|
+
console.print(
|
167
|
+
f"[red]Error: Secret '{name}' not found in namespace '{namespace}'[/red]"
|
168
|
+
)
|
169
|
+
else:
|
170
|
+
console.print(f"[red]Error deleting secret: {e.reason}[/red]")
|
171
|
+
raise RuntimeError(f"Failed to delete secret: {str(e)}") from e
|
172
|
+
|
173
|
+
|
174
|
+
def get_kubernetes_secrets_by_type(
|
175
|
+
namespace: str, context: str | None = None
|
176
|
+
) -> dict[str, list[dict]]:
|
177
|
+
"""List metadata about secrets in the namespace"""
|
178
|
+
v1 = get_k8s_client(context)
|
179
|
+
|
180
|
+
try:
|
181
|
+
secrets = v1.list_namespaced_secret(namespace=namespace)
|
182
|
+
secret_type_to_secret = defaultdict(list)
|
183
|
+
for secret in secrets.items:
|
184
|
+
if secret.type in VALID_SECRET_TYPES:
|
185
|
+
secret_type_to_secret[secret.type].append(
|
186
|
+
{
|
187
|
+
"name": secret.metadata.name,
|
188
|
+
"namespace": namespace,
|
189
|
+
"created": secret.metadata.creation_timestamp.isoformat(),
|
190
|
+
}
|
191
|
+
)
|
192
|
+
|
193
|
+
return secret_type_to_secret
|
194
|
+
except ApiException as e:
|
195
|
+
console.print(
|
196
|
+
f"[red]Error listing secrets in namespace '{namespace}': {e.reason}[/red]"
|
197
|
+
)
|
198
|
+
raise RuntimeError(f"Failed to list secrets: {str(e)}") from e
|
199
|
+
|
200
|
+
# NOTE: This corresponds with KUBERNETES_SECRET_TYPE_OPAQUE
|
201
|
+
|
202
|
+
|
203
|
+
def sync_user_defined_secrets(
|
204
|
+
manifest_obj: AgentManifest,
|
205
|
+
found_secrets: list[dict],
|
206
|
+
values_data: dict[str, Any],
|
207
|
+
cluster: str,
|
208
|
+
namespace: str,
|
209
|
+
interactive: bool,
|
210
|
+
changes: dict[str, list[str]],
|
211
|
+
) -> None:
|
212
|
+
"""Sync user defined secrets between manifest, cluster, and values file"""
|
213
|
+
console.print(
|
214
|
+
f"[bold]Syncing user defined secrets to cluster: {cluster} namespace: {namespace}[/bold]"
|
215
|
+
)
|
216
|
+
|
217
|
+
# Get the secrets from the cluster using the specified namespace and cluster context
|
218
|
+
cluster_secret_names = {secret["name"] for secret in found_secrets}
|
219
|
+
# Get the secrets from the manifest
|
220
|
+
agent_config: AgentConfig = manifest_obj.agent
|
221
|
+
manifest_credentials: list[CredentialMapping] = agent_config.credentials or []
|
222
|
+
|
223
|
+
if not manifest_credentials:
|
224
|
+
console.print("[yellow]No credentials found in manifest[/yellow]")
|
225
|
+
return
|
226
|
+
|
227
|
+
# Build required secrets map from manifest
|
228
|
+
required_secrets = {} # {secret_name: {secret_key: env_var_name}}
|
229
|
+
for cred in manifest_credentials:
|
230
|
+
if cred.secret_name not in required_secrets:
|
231
|
+
required_secrets[cred.secret_name] = {}
|
232
|
+
required_secrets[cred.secret_name][cred.secret_key] = cred.env_var_name
|
233
|
+
|
234
|
+
# Process each required secret
|
235
|
+
for secret_name, required_keys in required_secrets.items():
|
236
|
+
current_secret_data = get_secret_data(secret_name, namespace, cluster)
|
237
|
+
new_secret_data = {}
|
238
|
+
secret_needs_update = False
|
239
|
+
|
240
|
+
# Process each required key in this secret
|
241
|
+
for secret_key, _ in required_keys.items():
|
242
|
+
current_value = current_secret_data.get(secret_key)
|
243
|
+
|
244
|
+
# Get the new value
|
245
|
+
if (
|
246
|
+
values_data
|
247
|
+
and secret_name in values_data
|
248
|
+
and secret_key in values_data[secret_name]
|
249
|
+
):
|
250
|
+
new_value = values_data[secret_name][secret_key]
|
251
|
+
elif interactive:
|
252
|
+
if current_value:
|
253
|
+
console.print(
|
254
|
+
f"[blue]Secret '{secret_name}' key '{secret_key}' already exists[/blue]"
|
255
|
+
)
|
256
|
+
update_choice = questionary.select(
|
257
|
+
"What would you like to do?",
|
258
|
+
choices=[
|
259
|
+
"Keep current value",
|
260
|
+
"Update with new value",
|
261
|
+
"Show current value",
|
262
|
+
],
|
263
|
+
).ask()
|
264
|
+
update_choice = handle_questionary_cancellation(
|
265
|
+
update_choice, "secret update choice"
|
266
|
+
)
|
267
|
+
|
268
|
+
if update_choice == "Show current value":
|
269
|
+
console.print(f"Current value: [dim]{current_value}[/dim]")
|
270
|
+
update_choice = questionary.select(
|
271
|
+
"What would you like to do?",
|
272
|
+
choices=["Keep current value", "Update with new value"],
|
273
|
+
).ask()
|
274
|
+
update_choice = handle_questionary_cancellation(
|
275
|
+
update_choice, "secret update choice"
|
276
|
+
)
|
277
|
+
|
278
|
+
if update_choice == "Update with new value":
|
279
|
+
new_value = interactive_secret_input(secret_name, secret_key)
|
280
|
+
else:
|
281
|
+
new_value = current_value
|
282
|
+
else:
|
283
|
+
console.print(
|
284
|
+
f"[yellow]Secret '{secret_name}' key '{secret_key}' does not exist[/yellow]"
|
285
|
+
)
|
286
|
+
new_value = interactive_secret_input(secret_name, secret_key)
|
287
|
+
else:
|
288
|
+
raise RuntimeError(
|
289
|
+
f"No value provided for secret '{secret_name}' key '{secret_key}'. Provide values file or use interactive mode."
|
290
|
+
)
|
291
|
+
|
292
|
+
# Must be a string because kubernetes always expects a
|
293
|
+
new_value = str(new_value)
|
294
|
+
new_secret_data[secret_key] = new_value
|
295
|
+
|
296
|
+
# Check if value changed
|
297
|
+
if current_value != new_value:
|
298
|
+
secret_needs_update = True
|
299
|
+
else:
|
300
|
+
changes["noop"].append(
|
301
|
+
f"Secret '{secret_name}' key '{secret_key}' is up to date"
|
302
|
+
)
|
303
|
+
|
304
|
+
# Determine action needed
|
305
|
+
if secret_name not in cluster_secret_names:
|
306
|
+
changes["create"].append(
|
307
|
+
f"Create secret '{secret_name}' with keys: {list(required_keys.keys())}"
|
308
|
+
)
|
309
|
+
create_secret_with_data(secret_name, new_secret_data, namespace, cluster)
|
310
|
+
elif secret_needs_update:
|
311
|
+
changes["update"].append(f"Update secret '{secret_name}' (values changed)")
|
312
|
+
update_secret_with_data(secret_name, new_secret_data, namespace, cluster)
|
313
|
+
|
314
|
+
# Handle orphaned secrets (in cluster but not in manifest)
|
315
|
+
orphaned_secrets = cluster_secret_names - set(required_secrets.keys())
|
316
|
+
if orphaned_secrets:
|
317
|
+
console.print(
|
318
|
+
f"\n[yellow]Warning: Found {len(orphaned_secrets)} secrets in cluster not defined in manifest:[/yellow]"
|
319
|
+
)
|
320
|
+
for secret in orphaned_secrets:
|
321
|
+
console.print(f" - {secret}")
|
322
|
+
|
323
|
+
|
324
|
+
def create_dockerconfigjson_string(
|
325
|
+
registry: str, username: str, password: str, email: str | None = None
|
326
|
+
) -> str:
|
327
|
+
"""Create raw dockerconfigjson string data for use with Kubernetes string_data field"""
|
328
|
+
# Create the auth field (base64 encoded username:password)
|
329
|
+
auth_string = f"{username}:{password}"
|
330
|
+
auth_b64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
331
|
+
|
332
|
+
# Build the auth entry
|
333
|
+
auth_entry = {"username": username, "password": password, "auth": auth_b64}
|
334
|
+
|
335
|
+
# Only include email if provided
|
336
|
+
if email:
|
337
|
+
auth_entry["email"] = email
|
338
|
+
|
339
|
+
# Create the full dockerconfig structure
|
340
|
+
docker_config = {"auths": {registry: auth_entry}}
|
341
|
+
|
342
|
+
# Return raw JSON string (Kubernetes will handle base64 encoding when using string_data)
|
343
|
+
return json.dumps(docker_config)
|
344
|
+
|
345
|
+
|
346
|
+
def parse_dockerconfigjson_data(input_data: str) -> dict[str, dict[str, str]]:
|
347
|
+
"""Parse existing dockerconfigjson data to extract registry credentials"""
|
348
|
+
try:
|
349
|
+
# Decode base64
|
350
|
+
config = json.loads(input_data)
|
351
|
+
|
352
|
+
# Extract auths section
|
353
|
+
auths = config.get("auths", {})
|
354
|
+
|
355
|
+
# Convert to comparable format: {registry: {username, password, email}}
|
356
|
+
parsed_auths = {}
|
357
|
+
for registry, auth_data in auths.items():
|
358
|
+
# Try to decode the base64 auth field first
|
359
|
+
username = ""
|
360
|
+
password = ""
|
361
|
+
if "auth" in auth_data:
|
362
|
+
try:
|
363
|
+
auth_b64 = auth_data["auth"]
|
364
|
+
username_password = base64.b64decode(auth_b64).decode("utf-8")
|
365
|
+
if ":" in username_password:
|
366
|
+
username, password = username_password.split(":", 1)
|
367
|
+
except Exception:
|
368
|
+
pass
|
369
|
+
|
370
|
+
# Fall back to direct username/password fields if auth decode failed
|
371
|
+
if not username:
|
372
|
+
username = auth_data.get("username", "")
|
373
|
+
if not password:
|
374
|
+
password = auth_data.get("password", "")
|
375
|
+
|
376
|
+
parsed_auths[registry] = {
|
377
|
+
"username": username,
|
378
|
+
"password": password,
|
379
|
+
"email": auth_data.get("email", ""),
|
380
|
+
}
|
381
|
+
|
382
|
+
return parsed_auths
|
383
|
+
except Exception:
|
384
|
+
return {} # If parsing fails, assume empty/invalid
|
385
|
+
|
386
|
+
|
387
|
+
def credentials_changed(
|
388
|
+
current_auths: dict[str, dict[str, str]],
|
389
|
+
new_registry: str,
|
390
|
+
new_username: str,
|
391
|
+
new_password: str,
|
392
|
+
new_email: str = "",
|
393
|
+
) -> bool:
|
394
|
+
"""Check if credentials have actually changed"""
|
395
|
+
|
396
|
+
# If registry doesn't exist in current, it's a change
|
397
|
+
if new_registry not in current_auths:
|
398
|
+
return True
|
399
|
+
|
400
|
+
current_creds = current_auths[new_registry]
|
401
|
+
# Compare each field
|
402
|
+
if (
|
403
|
+
current_creds.get("username", "") != new_username
|
404
|
+
or current_creds.get("password", "") != new_password
|
405
|
+
or current_creds.get("email", "") != (new_email or "")
|
406
|
+
):
|
407
|
+
return True
|
408
|
+
else:
|
409
|
+
return False # No changes detected
|
410
|
+
|
411
|
+
|
412
|
+
def interactive_image_pull_secret_input(secret_name: str) -> dict[str, str]:
|
413
|
+
"""Prompt user for image pull secret values"""
|
414
|
+
console.print(
|
415
|
+
f"\n[bold]Configure image pull secret '[cyan]{secret_name}[/cyan]':[/bold]"
|
416
|
+
)
|
417
|
+
|
418
|
+
registry = questionary.text(
|
419
|
+
"Registry URL (e.g., docker.io, gcr.io, your-registry.com):",
|
420
|
+
default="docker.io",
|
421
|
+
).ask()
|
422
|
+
registry = handle_questionary_cancellation(registry, "registry input")
|
423
|
+
|
424
|
+
username = questionary.text("Username:").ask()
|
425
|
+
username = handle_questionary_cancellation(username, "username input")
|
426
|
+
|
427
|
+
password = questionary.password("Password (input will be hidden):").ask()
|
428
|
+
password = handle_questionary_cancellation(password, "password input")
|
429
|
+
|
430
|
+
email_choice = questionary.confirm(
|
431
|
+
"Do you want to include an email address? (optional)"
|
432
|
+
).ask()
|
433
|
+
email_choice = handle_questionary_cancellation(email_choice, "email choice")
|
434
|
+
email = ""
|
435
|
+
if email_choice:
|
436
|
+
email = questionary.text("Email address:").ask() or ""
|
437
|
+
if email is None: # Handle None from questionary
|
438
|
+
email = ""
|
439
|
+
|
440
|
+
return {
|
441
|
+
"registry": registry,
|
442
|
+
"username": username,
|
443
|
+
"password": password,
|
444
|
+
"email": email,
|
445
|
+
}
|
446
|
+
|
447
|
+
|
448
|
+
def sync_image_pull_secrets(
|
449
|
+
manifest_obj: AgentManifest,
|
450
|
+
found_dockerconfigjson_secrets: list[dict],
|
451
|
+
values_data: dict[str, Any],
|
452
|
+
cluster: str,
|
453
|
+
namespace: str,
|
454
|
+
interactive: bool,
|
455
|
+
changes: dict[str, list[str]],
|
456
|
+
) -> None:
|
457
|
+
"""Sync image pull secrets between manifest, cluster, and values file"""
|
458
|
+
console.print(
|
459
|
+
f"[bold]Syncing image pull secrets to cluster: {cluster} namespace: {namespace}[/bold]"
|
460
|
+
)
|
461
|
+
|
462
|
+
# Get the secrets of type KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON
|
463
|
+
cluster_dockerconfigjson_secret_names = {
|
464
|
+
secret["name"] for secret in found_dockerconfigjson_secrets
|
465
|
+
}
|
466
|
+
|
467
|
+
# Get the secrets from the manifest
|
468
|
+
deployment_config: DeploymentConfig = manifest_obj.deployment
|
469
|
+
manifest_image_pull_secrets: list[ImagePullSecretConfig] = (
|
470
|
+
deployment_config.imagePullSecrets or []
|
471
|
+
)
|
472
|
+
|
473
|
+
if not manifest_image_pull_secrets:
|
474
|
+
logger.info("No image pull secrets found in manifest")
|
475
|
+
return
|
476
|
+
|
477
|
+
# Get image pull secrets from values data
|
478
|
+
image_pull_values = values_data
|
479
|
+
|
480
|
+
# Process each required image pull secret
|
481
|
+
for pull_secret in manifest_image_pull_secrets:
|
482
|
+
secret_name = pull_secret.name
|
483
|
+
current_secret_data = get_secret_data(secret_name, namespace, cluster)
|
484
|
+
|
485
|
+
# Get new values
|
486
|
+
new_registry = ""
|
487
|
+
new_username = ""
|
488
|
+
new_password = ""
|
489
|
+
new_email = ""
|
490
|
+
|
491
|
+
if secret_name in image_pull_values:
|
492
|
+
# Get values from values file
|
493
|
+
secret_config = image_pull_values[secret_name]
|
494
|
+
new_registry = secret_config.get("registry", "")
|
495
|
+
new_username = secret_config.get("username", "")
|
496
|
+
new_password = secret_config.get("password", "")
|
497
|
+
new_email = secret_config.get("email", "")
|
498
|
+
|
499
|
+
if not new_registry or not new_username or not new_password:
|
500
|
+
raise RuntimeError(
|
501
|
+
f"Incomplete image pull secret configuration for '{secret_name}'. "
|
502
|
+
f"Required: registry, username, password. Optional: email"
|
503
|
+
)
|
504
|
+
elif interactive:
|
505
|
+
# Get values interactively
|
506
|
+
if secret_name in cluster_dockerconfigjson_secret_names:
|
507
|
+
console.print(
|
508
|
+
f"[blue]Image pull secret '{secret_name}' already exists[/blue]"
|
509
|
+
)
|
510
|
+
update_choice = questionary.select(
|
511
|
+
"What would you like to do?",
|
512
|
+
choices=["Keep current credentials", "Update with new credentials"],
|
513
|
+
).ask()
|
514
|
+
update_choice = handle_questionary_cancellation(
|
515
|
+
update_choice, "image pull secret update choice"
|
516
|
+
)
|
517
|
+
|
518
|
+
if update_choice == "Keep current credentials":
|
519
|
+
continue # Skip this secret
|
520
|
+
|
521
|
+
console.print(
|
522
|
+
f"[yellow]Image pull secret '{secret_name}' needs configuration[/yellow]"
|
523
|
+
)
|
524
|
+
creds = interactive_image_pull_secret_input(secret_name)
|
525
|
+
new_registry = creds["registry"]
|
526
|
+
new_username = creds["username"]
|
527
|
+
new_password = creds["password"]
|
528
|
+
new_email = creds["email"]
|
529
|
+
else:
|
530
|
+
raise RuntimeError(
|
531
|
+
f"No configuration provided for image pull secret '{secret_name}'. "
|
532
|
+
f"Provide values file or use interactive mode."
|
533
|
+
)
|
534
|
+
|
535
|
+
# Check if update is needed
|
536
|
+
secret_needs_update = False
|
537
|
+
action = ""
|
538
|
+
|
539
|
+
if secret_name not in cluster_dockerconfigjson_secret_names:
|
540
|
+
# Secret doesn't exist, needs creation
|
541
|
+
secret_needs_update = True
|
542
|
+
action = "create"
|
543
|
+
else:
|
544
|
+
# Secret exists, check if values changed
|
545
|
+
current_dockerconfig = current_secret_data.get(".dockerconfigjson", {})
|
546
|
+
current_auths = parse_dockerconfigjson_data(current_dockerconfig)
|
547
|
+
if credentials_changed(
|
548
|
+
current_auths, new_registry, new_username, new_password, new_email
|
549
|
+
):
|
550
|
+
secret_needs_update = True
|
551
|
+
action = "update"
|
552
|
+
else:
|
553
|
+
changes["noop"].append(
|
554
|
+
f"Secret '{secret_name}' key '{secret_name}' is up to date"
|
555
|
+
)
|
556
|
+
|
557
|
+
# Only perform action if update is needed
|
558
|
+
if secret_needs_update:
|
559
|
+
dockerconfig_string = create_dockerconfigjson_string(
|
560
|
+
new_registry, new_username, new_password, new_email
|
561
|
+
)
|
562
|
+
secret_data = {".dockerconfigjson": dockerconfig_string}
|
563
|
+
|
564
|
+
if action == "create":
|
565
|
+
changes[action].append(
|
566
|
+
f"Create image pull secret '{secret_name}' for registry '{new_registry}'"
|
567
|
+
)
|
568
|
+
create_image_pull_secret_with_data(
|
569
|
+
secret_name, secret_data, namespace, cluster
|
570
|
+
)
|
571
|
+
elif action == "update":
|
572
|
+
changes[action].append(
|
573
|
+
f"Update image pull secret '{secret_name}' (credentials changed)"
|
574
|
+
)
|
575
|
+
update_image_pull_secret_with_data(
|
576
|
+
secret_name, secret_data, namespace, cluster
|
577
|
+
)
|
578
|
+
|
579
|
+
|
580
|
+
def print_changes_summary(change_type: str, changes: dict[str, list[str]]) -> None:
|
581
|
+
# Show summary
|
582
|
+
console.print(f"\n[bold]Sync Summary for {change_type}:[/bold]")
|
583
|
+
if changes["create"]:
|
584
|
+
console.print("[green]Created:[/green]")
|
585
|
+
for change in changes["create"]:
|
586
|
+
console.print(f" ✓ {change}")
|
587
|
+
|
588
|
+
if changes["update"]:
|
589
|
+
console.print("[yellow]Updated:[/yellow]")
|
590
|
+
for change in changes["update"]:
|
591
|
+
console.print(f" ⚠ {change}")
|
592
|
+
|
593
|
+
if changes["noop"]:
|
594
|
+
console.print("[yellow]No changes:[/yellow]")
|
595
|
+
for change in changes["noop"]:
|
596
|
+
console.print(f" ✓ {change}")
|
597
|
+
del changes["noop"]
|
598
|
+
|
599
|
+
if not any(changes.values()):
|
600
|
+
console.print(
|
601
|
+
f"[green]✓ All secrets are already in sync for {change_type}[/green]"
|
602
|
+
)
|
603
|
+
|
604
|
+
console.print("")
|
605
|
+
|
606
|
+
|
607
|
+
def sync_secrets(
|
608
|
+
manifest_obj: AgentManifest,
|
609
|
+
cluster: str,
|
610
|
+
namespace: str,
|
611
|
+
interactive: bool,
|
612
|
+
values_path: str | None,
|
613
|
+
) -> None:
|
614
|
+
"""Sync secrets between manifest, cluster, and values file"""
|
615
|
+
logger.info(f"Syncing secrets to cluster: {cluster} namespace: {namespace}")
|
616
|
+
|
617
|
+
# Load values from file if provided
|
618
|
+
values_data = {}
|
619
|
+
if values_path:
|
620
|
+
try:
|
621
|
+
# TODO: Convert this to a pydantic model to validate the values file
|
622
|
+
values_data = load_values_file(values_path)
|
623
|
+
console.print(f"[green]Loaded values from {values_path}[/green]")
|
624
|
+
except Exception as e:
|
625
|
+
console.print(f"[red]Error loading values file: {e}[/red]")
|
626
|
+
raise
|
627
|
+
|
628
|
+
# Get the secrets from the cluster using the specified namespace and cluster context
|
629
|
+
cluster_secrets_by_type = get_kubernetes_secrets_by_type(
|
630
|
+
namespace=namespace, context=cluster
|
631
|
+
)
|
632
|
+
|
633
|
+
# Track changes for summary
|
634
|
+
changes = {"create": [], "update": [], "noop": []}
|
635
|
+
|
636
|
+
sync_user_defined_secrets(
|
637
|
+
manifest_obj,
|
638
|
+
cluster_secrets_by_type[KUBERNETES_SECRET_TYPE_OPAQUE],
|
639
|
+
values_data.get(
|
640
|
+
KUBERNETES_SECRET_TO_MANIFEST_KEY[KUBERNETES_SECRET_TYPE_OPAQUE], {}
|
641
|
+
),
|
642
|
+
cluster,
|
643
|
+
namespace,
|
644
|
+
interactive,
|
645
|
+
changes,
|
646
|
+
)
|
647
|
+
|
648
|
+
print_changes_summary("User Defined Secrets", changes)
|
649
|
+
|
650
|
+
# Track changes for summary
|
651
|
+
changes = {"create": [], "update": [], "noop": []}
|
652
|
+
|
653
|
+
sync_image_pull_secrets(
|
654
|
+
manifest_obj,
|
655
|
+
cluster_secrets_by_type[KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON],
|
656
|
+
values_data.get(
|
657
|
+
KUBERNETES_SECRET_TO_MANIFEST_KEY[KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON],
|
658
|
+
{},
|
659
|
+
),
|
660
|
+
cluster,
|
661
|
+
namespace,
|
662
|
+
interactive,
|
663
|
+
changes,
|
664
|
+
)
|
665
|
+
|
666
|
+
print_changes_summary("Image Pull Secrets", changes)
|
667
|
+
|
668
|
+
console.print(
|
669
|
+
f"\n[green]Secret sync completed for cluster '{cluster}' namespace '{namespace}'[/green]"
|
670
|
+
)
|