Coverage for mcpgateway / tools / builder / pipeline.py: 100%
39 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/tools/builder/pipeline.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Teryl Taylor
7Abstract base class for MCP Stack deployment implementations.
9This module defines the CICDModule interface that all deployment implementations
10must implement. It provides a common API for building, deploying, and managing
11MCP Gateway stacks with external plugin servers.
13The base class implements shared functionality (validation) while requiring
14subclasses to implement deployment-specific logic (build, deploy, etc.).
16Design Pattern:
17 Strategy Pattern - Different implementations (Dagger vs Python) can be
18 swapped transparently via the DeployFactory.
20Example:
21 >>> from mcpgateway.tools.builder.factory import DeployFactory
22 >>> deployer, mode = DeployFactory.create_deployer("dagger", verbose=False)
23 ⚠ Dagger not installed. Using plain python.
24 >>> # Validate configuration (output varies by config)
25 >>> # deployer.validate("mcp-stack.yaml")
26 >>> # Async methods must be called with await (see method examples below)
27"""
29# Standard
30from abc import ABC, abstractmethod
31from pathlib import Path
32from typing import Optional
34# Third-Party
35from pydantic import ValidationError
36from rich.console import Console
37import yaml
39# First-Party
40from mcpgateway.tools.builder.schema import MCPStackConfig
42# Shared console instance for consistent output formatting
43console = Console()
46class CICDModule(ABC):
47 """Abstract base class for MCP Stack deployment implementations.
49 This class defines the interface that all deployment implementations must
50 implement. It provides common initialization and validation logic while
51 deferring implementation-specific details to subclasses.
53 Attributes:
54 verbose (bool): Enable verbose output during operations
55 console (Console): Rich console for formatted output
57 Implementations:
58 - MCPStackDagger: High-performance implementation using Dagger SDK
59 - MCPStackPython: Fallback implementation using plain Python + Docker/Podman
61 Examples:
62 >>> # Test that CICDModule is abstract
63 >>> from abc import ABC
64 >>> issubclass(CICDModule, ABC)
65 True
67 >>> # Test initialization with defaults
68 >>> class TestDeployer(CICDModule):
69 ... async def build(self, config_file: str, **kwargs) -> None:
70 ... pass
71 ... async def generate_certificates(self, config_file: str) -> None:
72 ... pass
73 ... async def deploy(self, config_file: str, **kwargs) -> None:
74 ... pass
75 ... async def verify(self, config_file: str, **kwargs) -> None:
76 ... pass
77 ... async def destroy(self, config_file: str) -> None:
78 ... pass
79 ... def generate_manifests(self, config_file: str, **kwargs) -> Path:
80 ... return Path(".")
81 >>> deployer = TestDeployer()
82 >>> deployer.verbose
83 False
85 >>> # Test initialization with verbose=True
86 >>> verbose_deployer = TestDeployer(verbose=True)
87 >>> verbose_deployer.verbose
88 True
90 >>> # Test that console is available
91 >>> hasattr(deployer, 'console')
92 True
93 """
95 def __init__(self, verbose: bool = False):
96 """Initialize the deployment module.
98 Args:
99 verbose: Enable verbose output during all operations
101 Examples:
102 >>> # Cannot instantiate abstract class directly
103 >>> try:
104 ... CICDModule()
105 ... except TypeError as e:
106 ... "abstract" in str(e).lower()
107 True
108 """
109 self.verbose = verbose
110 self.console = console
112 def validate(self, config_file: str) -> None:
113 """Validate mcp-stack.yaml configuration using Pydantic schemas.
115 This method provides comprehensive validation of the MCP stack configuration
116 using Pydantic models defined in schema.py. It validates:
117 - Required sections (deployment, gateway, plugins)
118 - Deployment type (kubernetes or compose)
119 - Gateway image specification
120 - Plugin configurations (name, repo/image, etc.)
121 - Custom business rules (unique names, valid combinations)
123 Args:
124 config_file: Path to mcp-stack.yaml configuration file
126 Raises:
127 ValueError: If configuration is invalid, with formatted error details
128 ValidationError: If Pydantic schema validation fails
129 FileNotFoundError: If config_file does not exist
131 Examples:
132 >>> import tempfile
133 >>> import yaml
134 >>> from pathlib import Path
135 >>> # Create a test deployer
136 >>> class TestDeployer(CICDModule):
137 ... async def build(self, config_file: str, **kwargs) -> None:
138 ... pass
139 ... async def generate_certificates(self, config_file: str) -> None:
140 ... pass
141 ... async def deploy(self, config_file: str, **kwargs) -> None:
142 ... pass
143 ... async def verify(self, config_file: str, **kwargs) -> None:
144 ... pass
145 ... async def destroy(self, config_file: str) -> None:
146 ... pass
147 ... def generate_manifests(self, config_file: str, **kwargs) -> Path:
148 ... return Path(".")
149 >>> deployer = TestDeployer(verbose=False)
151 >>> # Test with valid minimal config
152 >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
153 ... config = {
154 ... 'deployment': {'type': 'compose'},
155 ... 'gateway': {'image': 'test:latest'},
156 ... 'plugins': []
157 ... }
158 ... yaml.dump(config, f)
159 ... config_path = f.name
160 >>> deployer.validate(config_path)
161 >>> import os
162 >>> os.unlink(config_path)
164 >>> # Test with missing file
165 >>> try:
166 ... deployer.validate("/nonexistent/config.yaml")
167 ... except FileNotFoundError as e:
168 ... "config.yaml" in str(e)
169 True
171 >>> # Test with invalid config (missing required fields)
172 >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
173 ... bad_config = {'deployment': {'type': 'compose'}}
174 ... yaml.dump(bad_config, f)
175 ... bad_path = f.name
176 >>> try:
177 ... deployer.validate(bad_path)
178 ... except ValueError as e:
179 ... "validation failed" in str(e).lower()
180 True
181 >>> os.unlink(bad_path)
182 """
183 if self.verbose:
184 self.console.print(f"[blue]Validating {config_file}...[/blue]")
186 # Load YAML configuration
187 with open(config_file, "r") as f:
188 config_dict = yaml.safe_load(f)
190 # Validate using Pydantic schema
191 try:
192 # Local
194 MCPStackConfig(**config_dict)
195 except ValidationError as e:
196 # Format validation errors for better readability
197 error_msg = "Configuration validation failed:\n"
198 for error in e.errors():
199 # Join the error location path (e.g., plugins -> 0 -> name)
200 loc = " -> ".join(str(x) for x in error["loc"])
201 error_msg += f" • {loc}: {error['msg']}\n"
202 raise ValueError(error_msg) from e
204 if self.verbose:
205 self.console.print("[green]✓ Configuration valid[/green]")
207 @abstractmethod
208 async def build(self, config_file: str, plugins_only: bool = False, specific_plugins: Optional[list[str]] = None, no_cache: bool = False, copy_env_templates: bool = False) -> None:
209 """Build container images for plugins and/or gateway.
211 Subclasses must implement this to build Docker/Podman images from
212 Git repositories or use pre-built images.
214 Args:
215 config_file: Path to mcp-stack.yaml
216 plugins_only: Only build plugins, skip gateway
217 specific_plugins: List of specific plugin names to build (optional)
218 no_cache: Disable build cache for fresh builds
219 copy_env_templates: Copy .env.template files from cloned repos
221 Raises:
222 RuntimeError: If build fails
223 ValueError: If plugin configuration is invalid
225 Example:
226 # await deployer.build("mcp-stack.yaml", plugins_only=True)
227 # ✓ Built OPAPluginFilter
228 # ✓ Built LLMGuardPlugin
229 """
231 @abstractmethod
232 async def generate_certificates(self, config_file: str) -> None:
233 """Generate mTLS certificates for gateway and plugins.
235 Creates a certificate authority (CA) and issues certificates for:
236 - Gateway (client certificates for connecting to plugins)
237 - Each plugin (server certificates for accepting connections)
239 Certificates are stored in the paths defined in the config's
240 certificates section (default: ./certs/mcp/).
242 Args:
243 config_file: Path to mcp-stack.yaml
245 Raises:
246 RuntimeError: If certificate generation fails
247 FileNotFoundError: If required tools (openssl) are not available
249 Example:
250 # await deployer.generate_certificates("mcp-stack.yaml")
251 # ✓ Certificates generated
252 """
254 @abstractmethod
255 async def deploy(self, config_file: str, dry_run: bool = False, skip_build: bool = False, skip_certs: bool = False) -> None:
256 """Deploy the MCP stack to Kubernetes or Docker Compose.
258 This is the main deployment method that orchestrates:
259 1. Building containers (unless skip_build=True)
260 2. Generating mTLS certificates (unless skip_certs=True or mTLS disabled)
261 3. Generating manifests (Kubernetes YAML or docker-compose.yaml)
262 4. Applying the deployment (unless dry_run=True)
264 Args:
265 config_file: Path to mcp-stack.yaml
266 dry_run: Generate manifests without actually deploying
267 skip_build: Skip building containers (use existing images)
268 skip_certs: Skip certificate generation (use existing certs)
270 Raises:
271 RuntimeError: If deployment fails at any stage
272 ValueError: If configuration is invalid
274 Example:
275 # Full deployment
276 # await deployer.deploy("mcp-stack.yaml")
277 # ✓ Build complete
278 # ✓ Certificates generated
279 # ✓ Deployment complete
281 # Dry run (generate manifests only)
282 # await deployer.deploy("mcp-stack.yaml", dry_run=True)
283 # ✓ Dry-run complete (no changes made)
284 """
286 @abstractmethod
287 async def verify(self, config_file: str, wait: bool = False, timeout: int = 300) -> None:
288 """Verify deployment health and readiness.
290 Checks that all deployed services are healthy and ready:
291 - Kubernetes: Checks pod status, optionally waits for Ready
292 - Docker Compose: Checks container status
294 Args:
295 config_file: Path to mcp-stack.yaml
296 wait: Wait for deployment to become ready
297 timeout: Maximum time to wait in seconds (default: 300)
299 Raises:
300 RuntimeError: If verification fails or timeout is reached
301 TimeoutError: If wait=True and deployment doesn't become ready
303 Example:
304 # Quick health check
305 # await deployer.verify("mcp-stack.yaml")
306 # NAME READY STATUS RESTARTS AGE
307 # mcpgateway-xxx 1/1 Running 0 2m
308 # mcp-plugin-opa-xxx 1/1 Running 0 2m
310 # Wait for ready state
311 # await deployer.verify("mcp-stack.yaml", wait=True, timeout=600)
312 # ✓ Deployment healthy
313 """
315 @abstractmethod
316 async def destroy(self, config_file: str) -> None:
317 """Destroy the deployed MCP stack.
319 Removes all deployed resources:
320 - Kubernetes: Deletes all resources in the namespace
321 - Docker Compose: Stops and removes containers, networks, volumes
323 WARNING: This is destructive and cannot be undone!
325 Args:
326 config_file: Path to mcp-stack.yaml
328 Raises:
329 RuntimeError: If destruction fails
331 Example:
332 # await deployer.destroy("mcp-stack.yaml")
333 # ✓ Deployment destroyed
334 """
336 @abstractmethod
337 def generate_manifests(self, config_file: str, output_dir: Optional[str] = None) -> Path:
338 """Generate deployment manifests (Kubernetes YAML or docker-compose.yaml).
340 Creates deployment manifests based on configuration:
341 - Kubernetes: Generates Deployment, Service, ConfigMap, Secret YAML files
342 - Docker Compose: Generates docker-compose.yaml with all services
344 Also generates:
345 - plugins-config.yaml: Plugin manager configuration for gateway
346 - Environment files: .env files for each service
348 Args:
349 config_file: Path to mcp-stack.yaml
350 output_dir: Output directory for manifests (default: ./deploy/manifests)
352 Returns:
353 Path: Directory containing generated manifests
355 Raises:
356 ValueError: If configuration is invalid
357 OSError: If output directory cannot be created
359 Example:
360 # manifests_path = deployer.generate_manifests("mcp-stack.yaml")
361 # print(f"Manifests generated in: {manifests_path}")
362 # Manifests generated in: /path/to/deploy/manifests
364 # Custom output directory
365 # deployer.generate_manifests("mcp-stack.yaml", output_dir="./my-manifests")
366 # ✓ Manifests generated: ./my-manifests
367 """