Skip to content

Commit 69dce04

Browse files
authored
Complete feature: mcpd config export (#106)
* Complete export functionality for mcpd config Completes the partially implemented 'mcpd config export' command by adding support for config file processing alongside the existing runtime file handling. The export now generates two output files: 1. Portable execution context (secrets.prod.toml) with templated variable references 2. Environment contract (.env) with placeholder mappings for CI/CD systems Key improvements: - Processes both config and runtime files (was runtime-only before) - Supports all server configuration types (env vars, args, bool args) - Generates consistent placeholder naming: MCPD__{SERVER_NAME}__{VAR_NAME} - Handles server names with hyphens (converts to underscores in placeholders) - Validates required configuration fields - Provides clear success/error messaging Fixes the bug where only runtime configuration was being exported. * Add comprehensive testdata for export command testing Adds structured testdata files for testing export functionality across different server configurations: - basic_export: Server with all config types (env, args, bool args) - multiple_servers: Multiple servers with different requirements - github_server_hyphens: Server name with hyphens (tests name conversion) - minimal_server_required_only: Server with required fields only Each testdata directory contains: - config.test.toml: Input server configuration - secrets.test.toml: Input runtime secrets - context.test.toml: Expected portable execution context output - contract.test.env: Expected environment contract output These files enable comprehensive integration testing of the export functionality without relying on inline test data. * Fix export bugs and add comprehensive testing Fixes critical bugs discovered during manual testing: 1. Missing required config fields in AggregateConfigs function - Added RequiredEnvVars, RequiredValueArgs, RequiredBoolArgs copying 2. Missing environment variables in contract file output - Created envVarsToContract helper function - Added proper environment variable placeholder extraction 3. Inconsistent .env file ordering - Fixed lexicographical sorting using slices.Sort 4. Empty placeholder validation issues - Added strings.TrimSpace and empty string filtering Additional improvements: - Added comprehensive unit tests for envVarsToContract function - Enhanced error handling and validation - Improved placeholder name consistency across all output formats These fixes ensure the export functionality works correctly for all supported configuration scenarios. * Add comprehensive test suite for export command Implements a full test suite for the export functionality with: Integration Tests: - Tests all server configuration scenarios using testdata files - Verifies both context and contract output file generation - Covers edge cases like server names with hyphens - Tests required-only configurations (no runtime secrets) Error Tests: - Tests malformed configuration files - Tests missing server configurations - Verifies proper error messages and no output file creation Unit Tests: - Tests dotenv file writing with various data scenarios - Tests lexicographical sorting and newline escaping Test Infrastructure: - dataPathsForTest: Helper for consistent testdata file path management - overrideFlagsForTest: Helper for clean global flag override with automatic cleanup - Comprehensive constants for test file naming consistency - Proper use of t.Helper() and t.Cleanup() patterns
1 parent 7772a10 commit 69dce04

25 files changed

+1902
-321
lines changed

cmd/config/export/export.go

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package export
22

33
import (
44
"fmt"
5+
"os"
6+
"slices"
7+
"strings"
58

69
"github.com/spf13/cobra"
710

@@ -10,6 +13,7 @@ import (
1013
"github.com/mozilla-ai/mcpd/v2/internal/config"
1114
"github.com/mozilla-ai/mcpd/v2/internal/context"
1215
"github.com/mozilla-ai/mcpd/v2/internal/flags"
16+
"github.com/mozilla-ai/mcpd/v2/internal/runtime"
1317
)
1418

1519
type Cmd struct {
@@ -82,44 +86,60 @@ func (c *Cmd) longDescription() string {
8286
}
8387

8488
func (c *Cmd) run(cmd *cobra.Command, _ []string) error {
85-
contextPath := c.ContextOutput
86-
// contractPath := c.ContractOutput
87-
88-
if err := exportPortableExecutionContext(c.ctxLoader, flags.RuntimeFile, contextPath); err != nil {
89+
if err := c.handleExport(); err != nil {
8990
return err
9091
}
9192

92-
if _, err := fmt.Fprintf(
93-
cmd.OutOrStdout(),
94-
"✓ Portable Execution Context exported: %s\n", contextPath,
95-
); err != nil {
96-
return err
93+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✓ Environment Contract exported: %s\n", c.ContractOutput)
94+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✓ Portable Execution Context exported: %s\n", c.ContextOutput)
95+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✓ Export completed successfully!\n")
96+
97+
return nil
98+
}
99+
100+
func (c *Cmd) handleExport() error {
101+
cfg, err := c.cfgLoader.Load(flags.ConfigFile)
102+
if err != nil {
103+
return fmt.Errorf("failed to load config: %w", err)
104+
}
105+
106+
rtCtx, err := c.ctxLoader.Load(flags.RuntimeFile)
107+
if err != nil {
108+
return fmt.Errorf("failed to load execution context config: %w", err)
97109
}
98110

99-
// Export 'Environment Contract'
100-
// TODO: export to contractPath based on format
101-
// fmt.Fprintf(cmd.OutOrStdout(), "✓ Environment Contract exported: %s\n", contractPath)
111+
servers, err := runtime.AggregateConfigs(cfg, rtCtx)
112+
if err != nil {
113+
return err
114+
}
102115

103-
if _, err := fmt.Fprintf(
104-
cmd.OutOrStdout(),
105-
"✓ Export completed successfully!\n",
106-
); err != nil {
116+
contract, err := servers.Export(c.ContextOutput)
117+
if err != nil {
107118
return err
108119
}
109120

110-
return nil
121+
// TODO: Support other 'c.Format's
122+
return writeDotenvFile(c.ContractOutput, contract)
111123
}
112124

113-
func exportPortableExecutionContext(loader context.Loader, src string, dest string) error {
114-
mod, err := loader.Load(src)
115-
if err != nil {
116-
return fmt.Errorf("failed to load execution context config: %w", err)
117-
}
125+
func writeDotenvFile(path string, data map[string]string) error {
126+
var b strings.Builder
118127

119-
exp, ok := mod.(context.Exporter)
120-
if !ok {
121-
return fmt.Errorf("execution context config does not support exporting")
128+
// Get keys and sort them lexicographically
129+
keys := make([]string, 0, len(data))
130+
for k := range data {
131+
keys = append(keys, k)
132+
}
133+
slices.Sort(keys)
134+
135+
// Write entries in sorted order
136+
for _, k := range keys {
137+
v := data[k]
138+
escaped := strings.ReplaceAll(v, "\n", "\\n")
139+
if _, err := fmt.Fprintf(&b, "%s=%s\n", k, escaped); err != nil {
140+
return err
141+
}
122142
}
123143

124-
return exp.Export(dest)
144+
return os.WriteFile(path, []byte(b.String()), 0o644)
125145
}

0 commit comments

Comments
 (0)