Skip to content

Commit b67b663

Browse files
authored
feat: implement runconfig export/import functionality (#1171) (#1183)
Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent fc736e7 commit b67b663

File tree

11 files changed

+529
-9
lines changed

11 files changed

+529
-9
lines changed

cmd/thv/app/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func NewRootCmd(enableUpdates bool) *cobra.Command {
4747
rootCmd.AddCommand(proxyCmd)
4848
rootCmd.AddCommand(restartCmd)
4949
rootCmd.AddCommand(serveCmd)
50+
rootCmd.AddCommand(newExportCmd())
5051
rootCmd.AddCommand(newVersionCmd())
5152
rootCmd.AddCommand(logsCommand())
5253
rootCmd.AddCommand(newSecretCommand())

cmd/thv/app/export.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/stacklok/toolhive/pkg/runner"
11+
)
12+
13+
func newExportCmd() *cobra.Command {
14+
return &cobra.Command{
15+
Use: "export <workload name> <path>",
16+
Short: "Export a workload's run configuration to a file",
17+
Long: `Export a workload's run configuration to a file for sharing or backup.
18+
19+
The exported configuration can be used with 'thv run --from-config <path>' to recreate
20+
the same workload with identical settings.
21+
22+
Examples:
23+
# Export a workload configuration to a file
24+
thv export my-server ./my-server-config.json
25+
26+
# Export to a specific directory
27+
thv export github-mcp /tmp/configs/github-config.json`,
28+
Args: cobra.ExactArgs(2),
29+
RunE: exportCmdFunc,
30+
}
31+
}
32+
33+
func exportCmdFunc(cmd *cobra.Command, args []string) error {
34+
ctx := cmd.Context()
35+
workloadName := args[0]
36+
outputPath := args[1]
37+
38+
// Load the saved run configuration
39+
runnerInstance, err := runner.LoadState(ctx, workloadName)
40+
if err != nil {
41+
return fmt.Errorf("failed to load run configuration for workload '%s': %w", workloadName, err)
42+
}
43+
44+
// Ensure the output directory exists
45+
outputDir := filepath.Dir(outputPath)
46+
if err := os.MkdirAll(outputDir, 0750); err != nil {
47+
return fmt.Errorf("failed to create output directory: %w", err)
48+
}
49+
50+
// Create the output file
51+
// #nosec G304 - outputPath is provided by the user as a command line argument for export functionality
52+
outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
53+
if err != nil {
54+
return fmt.Errorf("failed to create output file: %w", err)
55+
}
56+
defer outputFile.Close()
57+
58+
// Write the configuration to the file
59+
if err := runnerInstance.Config.WriteJSON(outputFile); err != nil {
60+
return fmt.Errorf("failed to write configuration to file: %w", err)
61+
}
62+
63+
fmt.Printf("Successfully exported run configuration for '%s' to '%s'\n", workloadName, outputPath)
64+
return nil
65+
}

cmd/thv/app/run.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app
22

33
import (
4+
"context"
45
"fmt"
56
"net"
67
"os"
@@ -10,6 +11,7 @@ import (
1011
"github.com/stacklok/toolhive/pkg/container"
1112
"github.com/stacklok/toolhive/pkg/groups"
1213
"github.com/stacklok/toolhive/pkg/logger"
14+
"github.com/stacklok/toolhive/pkg/runner"
1315
"github.com/stacklok/toolhive/pkg/workloads"
1416
)
1517

@@ -18,7 +20,7 @@ var runCmd = &cobra.Command{
1820
Short: "Run an MCP server",
1921
Long: `Run an MCP server with the specified name, image, or protocol scheme.
2022
21-
ToolHive supports three ways to run an MCP server:
23+
ToolHive supports four ways to run an MCP server:
2224
2325
1. From the registry:
2426
$ thv run server-name [-- args...]
@@ -39,9 +41,20 @@ ToolHive supports three ways to run an MCP server:
3941
or go (Golang). For Go, you can also specify local paths starting
4042
with './' or '../' to build and run local Go projects.
4143
44+
4. From an exported configuration:
45+
$ thv run --from-config <path>
46+
Runs an MCP server using a previously exported configuration file.
47+
4248
The container will be started with the specified transport mode and
4349
permission profile. Additional configuration can be provided via flags.`,
44-
Args: cobra.MinimumNArgs(1),
50+
Args: func(cmd *cobra.Command, args []string) error {
51+
// If --from-config is provided, no args are required
52+
if runFlags.FromConfig != "" {
53+
return nil
54+
}
55+
// Otherwise, require at least 1 argument
56+
return cobra.MinimumNArgs(1)(cmd, args)
57+
},
4558
RunE: runCmdFunc,
4659
// Ignore unknown flags to allow passing flags to the MCP server
4760
FParseErrWhitelist: cobra.FParseErrWhitelist{
@@ -67,9 +80,18 @@ func init() {
6780
func runCmdFunc(cmd *cobra.Command, args []string) error {
6881
ctx := cmd.Context()
6982

83+
// Check if we should load configuration from a file
84+
if runFlags.FromConfig != "" {
85+
return runFromConfigFile(ctx)
86+
}
87+
7088
// Get the name of the MCP server to run.
7189
// This may be a server name from the registry, a container image, or a protocol scheme.
72-
serverOrImage := args[0]
90+
// When using --from-config, no args are required
91+
var serverOrImage string
92+
if len(args) > 0 {
93+
serverOrImage = args[0]
94+
}
7395

7496
// Process command arguments using os.Args to find everything after --
7597
cmdArgs := parseCommandArguments(os.Args)
@@ -178,3 +200,43 @@ func ValidateAndNormaliseHostFlag(host string) (string, error) {
178200

179201
return "", fmt.Errorf("could not resolve host: %s", host)
180202
}
203+
204+
// runFromConfigFile loads a run configuration from a file and executes it
205+
func runFromConfigFile(ctx context.Context) error {
206+
// Open and read the configuration file
207+
configFile, err := os.Open(runFlags.FromConfig)
208+
if err != nil {
209+
return fmt.Errorf("failed to open configuration file '%s': %w", runFlags.FromConfig, err)
210+
}
211+
defer configFile.Close()
212+
213+
// Deserialize the configuration
214+
runConfig, err := runner.ReadJSON(configFile)
215+
if err != nil {
216+
return fmt.Errorf("failed to parse configuration file: %w", err)
217+
}
218+
219+
// Create container runtime
220+
rt, err := container.NewFactory().Create(ctx)
221+
if err != nil {
222+
return fmt.Errorf("failed to create container runtime: %v", err)
223+
}
224+
225+
// Set the runtime in the config
226+
runConfig.Deployer = rt
227+
228+
// Create workload manager
229+
workloadManager := workloads.NewManagerFromRuntime(rt)
230+
231+
// Run the workload based on foreground flag
232+
if runFlags.Foreground {
233+
err = workloadManager.RunWorkload(ctx, runConfig)
234+
} else {
235+
err = workloadManager.RunWorkloadDetached(ctx, runConfig)
236+
}
237+
if err != nil {
238+
return err
239+
}
240+
241+
return nil
242+
}

cmd/thv/app/run_flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ type RunFlags struct {
7070

7171
// Tools filter
7272
ToolsFilter []string
73+
74+
// Configuration import
75+
FromConfig string
7376
}
7477

7578
// AddRunFlags adds all the run flags to a command
@@ -155,6 +158,7 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) {
155158
nil,
156159
"Filter MCP server tools (comma-separated list of tool names)",
157160
)
161+
cmd.Flags().StringVar(&config.FromConfig, "from-config", "", "Load configuration from exported file")
158162
}
159163

160164
// BuildRunnerConfig creates a runner.RunConfig from the configuration

docs/cli/thv.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_export.md

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_run.md

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/docs.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)