Skip to content

Commit 1525d18

Browse files
committed
feat: add cluster context support in Compose with x-context extension
1 parent 8ae38cf commit 1525d18

File tree

8 files changed

+121
-0
lines changed

8 files changed

+121
-0
lines changed

cmd/uncloud/build.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ func runBuild(ctx context.Context, uncli *cli.CLI, opts buildOptions) error {
9898
return fmt.Errorf("load compose file(s): %w", err)
9999
}
100100

101+
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
102+
101103
servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps)
102104
if err != nil {
103105
return fmt.Errorf("determine services to build: %w", err)

cmd/uncloud/deploy.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
8383
return fmt.Errorf("load compose file(s): %w", err)
8484
}
8585

86+
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
87+
8688
if len(opts.services) > 0 {
8789
// Includes service dependencies by default. This is the default docker compose behavior.
8890
project, err = project.WithSelectedServices(opts.services)

cmd/uncloud/service/logs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
107107
if err != nil {
108108
return fmt.Errorf("load Compose file(s): %w", err)
109109
}
110+
111+
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
112+
110113
// View logs for all services, including disabled by inactive profiles.
111114
serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...)
112115
if len(serviceNames) == 0 {

internal/cli/cli.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,15 @@ func provisionOrConnectRemoteMachine(
561561
return machineClient, nil
562562
}
563563

564+
// SetClusterContextIfUnset sets the cluster context override only if no --context flag was used
565+
// and no --connect direct connection is active.
566+
func (cli *CLI) SetClusterContextIfUnset(name string) {
567+
if name == "" || cli.contextOverride != "" || cli.conn != nil {
568+
return
569+
}
570+
cli.contextOverride = name
571+
}
572+
564573
// ProgressOut returns an output stream for progress writer.
565574
func (cli *CLI) ProgressOut() *streams.Out {
566575
return streams.NewOut(os.Stdout)

pkg/client/compose/context.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package compose
2+
3+
import (
4+
"github.com/compose-spec/compose-go/v2/types"
5+
)
6+
7+
// ContextExtensionKey is the top-level Compose extension key for specifying the cluster context.
8+
const ContextExtensionKey = "x-context"
9+
10+
// ClusterContext extracts the x-context value from the project's top-level extensions.
11+
// Returns an empty string if x-context is not set.
12+
func ClusterContext(project *types.Project) string {
13+
v, ok := project.Extensions[ContextExtensionKey]
14+
if !ok {
15+
return ""
16+
}
17+
s, ok := v.(string)
18+
if !ok {
19+
return ""
20+
}
21+
return s
22+
}

pkg/client/compose/context_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package compose
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestClusterContext(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
content string
15+
want string
16+
}{
17+
{
18+
name: "no x-context",
19+
content: `
20+
services:
21+
web:
22+
image: nginx
23+
`,
24+
want: "",
25+
},
26+
{
27+
name: "x-context set",
28+
content: `
29+
x-context: prod
30+
services:
31+
web:
32+
image: nginx
33+
`,
34+
want: "prod",
35+
},
36+
}
37+
38+
for _, tt := range tests {
39+
t.Run(tt.name, func(t *testing.T) {
40+
project, err := LoadProjectFromContent(context.Background(), tt.content)
41+
require.NoError(t, err)
42+
assert.Equal(t, tt.want, ClusterContext(project))
43+
})
44+
}
45+
}

website/docs/4-guides/1-deployments/1-deploy-app.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,24 @@ Choose unique names to avoid conflicts with services deployed from other Compose
322322

323323
:::
324324

325+
## Deploy to a specific cluster context
326+
327+
If you manage multiple clusters, you can set `x-context` in your Compose file to make sure it always deploys to the
328+
correct one. You won't need to remember to manually switch clusters with `uc ctx` or `--context`.
329+
330+
```yaml title="compose.yaml"
331+
x-context: prod
332+
333+
services:
334+
web:
335+
image: myapp:latest
336+
```
337+
338+
With this configuration, `uc deploy` and other commands using the Compose file will always target the `prod` context,
339+
regardless of your currently active context. You can still override it with the `--context` flag if needed.
340+
341+
See [`x-context`](../../8-compose-file-reference/1-support-matrix.md#x-context) for more details.
342+
325343
## Use a different Compose file location
326344

327345
If your Compose file has a different name or location, use the `-f/--file` flag to specify its path:

website/docs/8-compose-file-reference/1-support-matrix.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
7272
| External configs | ❌ Not supported | Not supported |
7373
| Short syntax | ❌ Not supported | Use long syntax only |
7474
| **Extensions** | | |
75+
| `x-context` | ✅ Uncloud-specific | Cluster context override |
7576
| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration |
7677
| `x-machines` | ✅ Uncloud-specific | Machine placement constraints |
7778
| `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
8687

8788
Uncloud provides several custom extensions to enhance the Compose experience:
8889

90+
### `x-context`
91+
92+
Set the cluster context for all commands that use the Compose file, such as `deploy`, `build`, and `logs`. This is
93+
useful when you manage multiple clusters and want to make sure a Compose file is always deployed to the right one.
94+
No need to remember to manually switch clusters with `uc ctx` or `--context`.
95+
96+
`x-context` is a top-level key, not a service-level attribute.
97+
98+
```yaml
99+
x-context: prod
100+
101+
services:
102+
web:
103+
image: nginx
104+
```
105+
106+
The `--context` and `--connect` flags take precedence over `x-context`. If you don't specify any of these, the current
107+
context from your Uncloud config (`--uncloud-config`) is used.
108+
89109
### `x-ports`
90110

91111
Expose HTTP/HTTPS service ports via the Caddy reverse proxy, or bind TCP/UDP ports directly to the host:

0 commit comments

Comments
 (0)