diff --git a/cmd/uncloud/build.go b/cmd/uncloud/build.go index 1421ab4e..eeacf056 100644 --- a/cmd/uncloud/build.go +++ b/cmd/uncloud/build.go @@ -98,6 +98,8 @@ func runBuild(ctx context.Context, uncli *cli.CLI, opts buildOptions) error { return fmt.Errorf("load compose file(s): %w", err) } + uncli.SetClusterContextIfUnset(compose.ClusterContext(project)) + servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps) if err != nil { return fmt.Errorf("determine services to build: %w", err) diff --git a/cmd/uncloud/deploy.go b/cmd/uncloud/deploy.go index 37e8aeba..6b516e8e 100644 --- a/cmd/uncloud/deploy.go +++ b/cmd/uncloud/deploy.go @@ -83,6 +83,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { return fmt.Errorf("load compose file(s): %w", err) } + uncli.SetClusterContextIfUnset(compose.ClusterContext(project)) + if len(opts.services) > 0 { // Includes service dependencies by default. This is the default docker compose behavior. project, err = project.WithSelectedServices(opts.services) diff --git a/cmd/uncloud/service/logs.go b/cmd/uncloud/service/logs.go index aac30c40..cd47b2c4 100644 --- a/cmd/uncloud/service/logs.go +++ b/cmd/uncloud/service/logs.go @@ -107,6 +107,9 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo if err != nil { return fmt.Errorf("load Compose file(s): %w", err) } + + uncli.SetClusterContextIfUnset(compose.ClusterContext(project)) + // View logs for all services, including disabled by inactive profiles. serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...) if len(serviceNames) == 0 { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index f3828088..31381299 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -561,6 +561,15 @@ func provisionOrConnectRemoteMachine( return machineClient, nil } +// SetClusterContextIfUnset sets the cluster context override only if no --context flag was used +// and no --connect direct connection is active. +func (cli *CLI) SetClusterContextIfUnset(name string) { + if name == "" || cli.contextOverride != "" || cli.conn != nil { + return + } + cli.contextOverride = name +} + // ProgressOut returns an output stream for progress writer. func (cli *CLI) ProgressOut() *streams.Out { return streams.NewOut(os.Stdout) diff --git a/pkg/client/compose/context.go b/pkg/client/compose/context.go new file mode 100644 index 00000000..be589963 --- /dev/null +++ b/pkg/client/compose/context.go @@ -0,0 +1,22 @@ +package compose + +import ( + "github.com/compose-spec/compose-go/v2/types" +) + +// ContextExtensionKey is the top-level Compose extension key for specifying the cluster context. +const ContextExtensionKey = "x-context" + +// ClusterContext extracts the x-context value from the project's top-level extensions. +// Returns an empty string if x-context is not set. +func ClusterContext(project *types.Project) string { + v, ok := project.Extensions[ContextExtensionKey] + if !ok { + return "" + } + s, ok := v.(string) + if !ok { + return "" + } + return s +} diff --git a/pkg/client/compose/context_test.go b/pkg/client/compose/context_test.go new file mode 100644 index 00000000..8570a937 --- /dev/null +++ b/pkg/client/compose/context_test.go @@ -0,0 +1,45 @@ +package compose + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClusterContext(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + { + name: "no x-context", + content: ` +services: + web: + image: nginx +`, + want: "", + }, + { + name: "x-context set", + content: ` +x-context: prod +services: + web: + image: nginx +`, + want: "prod", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + project, err := LoadProjectFromContent(context.Background(), tt.content) + require.NoError(t, err) + assert.Equal(t, tt.want, ClusterContext(project)) + }) + } +} diff --git a/website/docs/4-guides/1-deployments/1-deploy-app.md b/website/docs/4-guides/1-deployments/1-deploy-app.md index ee802ab6..225c9739 100644 --- a/website/docs/4-guides/1-deployments/1-deploy-app.md +++ b/website/docs/4-guides/1-deployments/1-deploy-app.md @@ -322,6 +322,24 @@ Choose unique names to avoid conflicts with services deployed from other Compose ::: +## Deploy to a specific cluster context + +If you manage multiple clusters, you can set `x-context` in your Compose file to make sure it always deploys to the +correct one. You won't need to remember to manually switch clusters with `uc ctx` or `--context`. + +```yaml title="compose.yaml" +x-context: prod + +services: + web: + image: myapp:latest +``` + +With this configuration, `uc deploy` and other commands using the Compose file will always target the `prod` context, +regardless of your currently active context. You can still override it with the `--context` flag if needed. + +See [`x-context`](../../8-compose-file-reference/1-support-matrix.md#x-context) for more details. + ## Use a different Compose file location If your Compose file has a different name or location, use the `-f/--file` flag to specify its path: diff --git a/website/docs/8-compose-file-reference/1-support-matrix.md b/website/docs/8-compose-file-reference/1-support-matrix.md index 000a52c1..5acf89af 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -72,6 +72,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea | External configs | ❌ Not supported | Not supported | | Short syntax | ❌ Not supported | Use long syntax only | | **Extensions** | | | +| `x-context` | ✅ Uncloud-specific | Cluster context override | | `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | | `x-machines` | ✅ Uncloud-specific | Machine placement constraints | | `x-ports` | ✅ Uncloud-specific | Service port publishing | @@ -86,6 +87,25 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea Uncloud provides several custom extensions to enhance the Compose experience: +### `x-context` + +Set the cluster context for all commands that use the Compose file, such as `deploy`, `build`, and `logs`. This is +useful when you manage multiple clusters and want to make sure a Compose file is always deployed to the right one. +No need to remember to manually switch clusters with `uc ctx` or `--context`. + +`x-context` is a top-level key, not a service-level attribute. + +```yaml +x-context: prod + +services: + web: + image: nginx +``` + +The `--context` and `--connect` flags take precedence over `x-context`. If you don't specify any of these, the current +context from your Uncloud config (`--uncloud-config`) is used. + ### `x-ports` Expose HTTP/HTTPS service ports via the Caddy reverse proxy, or bind TCP/UDP ports directly to the host: