Skip to content

Commit b2259b4

Browse files
committed
add support of metadata subcommand for provider services
This command will let Compose and external tooling know about which parameters should be passed to the Compose plugin Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
1 parent 27e90a3 commit b2259b4

File tree

3 files changed

+203
-12
lines changed

3 files changed

+203
-12
lines changed

docs/examples/provider.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package main
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
2122
"os"
2223
"time"
2324

2425
"github.com/spf13/cobra"
26+
"github.com/spf13/pflag"
2527
)
2628

2729
func main() {
@@ -43,16 +45,27 @@ func composeCommand() *cobra.Command {
4345
TraverseChildren: true,
4446
}
4547
c.PersistentFlags().String("project-name", "", "compose project name") // unused
46-
c.AddCommand(&cobra.Command{
48+
upCmd := &cobra.Command{
4749
Use: "up",
4850
Run: up,
4951
Args: cobra.ExactArgs(1),
50-
})
51-
c.AddCommand(&cobra.Command{
52+
}
53+
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
54+
_ = upCmd.MarkFlagRequired("type")
55+
upCmd.Flags().Int("size", 10, "Database size in GB")
56+
upCmd.Flags().String("name", "", "Name of the database to be created")
57+
_ = upCmd.MarkFlagRequired("name")
58+
59+
downCmd := &cobra.Command{
5260
Use: "down",
5361
Run: down,
5462
Args: cobra.ExactArgs(1),
55-
})
63+
}
64+
downCmd.Flags().String("name", "", "Name of the database to be deleted")
65+
_ = downCmd.MarkFlagRequired("name")
66+
67+
c.AddCommand(upCmd, downCmd)
68+
c.AddCommand(metadataCommand(upCmd, downCmd))
5669
return c
5770
}
5871

@@ -72,3 +85,58 @@ func up(_ *cobra.Command, args []string) {
7285
func down(_ *cobra.Command, _ []string) {
7386
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
7487
}
88+
89+
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
90+
return &cobra.Command{
91+
Use: "metadata",
92+
Run: func(cmd *cobra.Command, _ []string) {
93+
metadata(upCmd, downCmd)
94+
},
95+
Args: cobra.NoArgs,
96+
}
97+
}
98+
99+
func metadata(upCmd, downCmd *cobra.Command) {
100+
metadata := ProviderMetadata{}
101+
metadata.Description = "Manage services on AwesomeCloud"
102+
metadata.Up = commandParameters(upCmd)
103+
metadata.Down = commandParameters(downCmd)
104+
jsonMetadata, err := json.Marshal(metadata)
105+
if err != nil {
106+
panic(err)
107+
}
108+
fmt.Println(string(jsonMetadata))
109+
}
110+
111+
func commandParameters(cmd *cobra.Command) CommandMetadata {
112+
cmdMetadata := CommandMetadata{}
113+
cmd.Flags().VisitAll(func(f *pflag.Flag) {
114+
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
115+
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
116+
Name: f.Name,
117+
Description: f.Usage,
118+
Required: isRequired,
119+
Type: f.Value.Type(),
120+
Default: f.DefValue,
121+
})
122+
})
123+
return cmdMetadata
124+
}
125+
126+
type ProviderMetadata struct {
127+
Description string `json:"description"`
128+
Up CommandMetadata `json:"up"`
129+
Down CommandMetadata `json:"down"`
130+
}
131+
132+
type CommandMetadata struct {
133+
Parameters []Metadata `json:"parameters"`
134+
}
135+
136+
type Metadata struct {
137+
Name string `json:"name"`
138+
Description string `json:"description"`
139+
Required bool `json:"required"`
140+
Type string `json:"type"`
141+
Default string `json:"default,omitempty"`
142+
}

docs/extension.md

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# About
22

33
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
4-
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
5-
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
4+
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
5+
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
66
many other runtimes, typically cloud services or services natively provided by host.
77

88
The Compose extensibility model has been designed to extend the `service` support to runtimes accessible through
@@ -20,6 +20,7 @@ the resource(s) needed to run a service.
2020
options:
2121
type: mysql
2222
size: 256
23+
name: myAwesomeCloudDB
2324
```
2425
2526
`provider.type` tells Compose the binary to run, which can be either:
@@ -41,7 +42,7 @@ awesomecloud compose --project-name <NAME> up --type=mysql --size=256 "database"
4142
```
4243

4344
> __Note:__ `project-name` _should_ be used by the provider to tag resources
44-
> set for project, so that later execution with `down` subcommand releases
45+
> set for project, so that later execution with `down` subcommand releases
4546
> all allocated resources set for the project.
4647

4748
## Communication with Compose
@@ -76,12 +77,12 @@ sequenceDiagram
7677

7778
## Connection to a service managed by a provider
7879

79-
A service in the Compose application can declare dependency on a service managed by an external provider:
80+
A service in the Compose application can declare dependency on a service managed by an external provider:
8081

8182
```yaml
8283
services:
8384
app:
84-
image: myapp
85+
image: myapp
8586
depends_on:
8687
- database
8788
@@ -104,8 +105,72 @@ into its runtime environment.
104105
## Down lifecycle
105106

106107
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
107-
The provider is responsible for releasing all resources associated with the service.
108+
The provider is responsible for releasing all resources associated with the service.
109+
110+
## Provide metadata about options
111+
112+
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
113+
114+
The `metadata` subcommand takes no parameters and returns a JSON structure on the `stdout` channel that describes the parameters accepted by both the `up` and `down` commands, including whether each parameter is mandatory or optional.
115+
116+
```console
117+
awesomecloud compose metadata
118+
```
119+
120+
The expected JSON output format is:
121+
```json
122+
{
123+
"description": "Manage services on AwesomeCloud",
124+
"up": {
125+
"parameters": [
126+
{
127+
"name": "type",
128+
"description": "Database type (mysql, postgres, etc.)",
129+
"required": true,
130+
"type": "string"
131+
},
132+
{
133+
"name": "size",
134+
"description": "Database size in GB",
135+
"required": false,
136+
"type": "integer",
137+
"default": "10"
138+
},
139+
{
140+
"name": "name",
141+
"description": "Name of the database to be created",
142+
"required": true,
143+
"type": "string"
144+
}
145+
]
146+
},
147+
"down": {
148+
"parameters": [
149+
{
150+
"name": "name",
151+
"description": "Name of the database to be removed",
152+
"required": true,
153+
"type": "string"
154+
}
155+
]
156+
}
157+
}
158+
```
159+
The top elements are:
160+
- `description`: Human-readable description of the provider
161+
- `up`: Object describing the parameters accepted by the `up` command
162+
- `down`: Object describing the parameters accepted by the `down` command
163+
164+
And for each command parameter, you should include the following properties:
165+
- `name`: The parameter name (without `--` prefix)
166+
- `description`: Human-readable description of the parameter
167+
- `required`: Boolean indicating if the parameter is mandatory
168+
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
169+
- `default`: Default value (optional, only for non-required parameters)
170+
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
171+
172+
This metadata allows Compose and other tools to understand the provider's interface and provide better user experience, such as validation, auto-completion, and documentation generation.
108173

109174
## Examples
110175

111-
See [example](examples/provider.go) for illustration on implementing this API in a command line
176+
See [example](examples/provider.go) for illustration on implementing this API in a command line

pkg/compose/plugins.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package compose
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"encoding/json"
2223
"errors"
@@ -161,12 +162,23 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
161162
}
162163

163164
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) *exec.Cmd {
165+
cmdOptionsMetadata := s.getPluginMetadata(path)
166+
var currentCommandMetadata CommandMetadata
167+
switch command {
168+
case "up":
169+
currentCommandMetadata = cmdOptionsMetadata.Up
170+
case "down":
171+
currentCommandMetadata = cmdOptionsMetadata.Down
172+
}
173+
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
164174
provider := *service.Provider
165175

166176
args := []string{"compose", "--project-name", project.Name, command}
167177
for k, v := range provider.Options {
168178
for _, value := range v {
169-
args = append(args, fmt.Sprintf("--%s=%s", k, value))
179+
if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok {
180+
args = append(args, fmt.Sprintf("--%s=%s", k, value))
181+
}
170182
}
171183
}
172184
args = append(args, service.Name)
@@ -198,3 +210,49 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
198210
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
199211
return cmd
200212
}
213+
214+
func (s *composeService) getPluginMetadata(path string) ProviderMetadata {
215+
cmd := exec.Command(path, "compose", "metadata")
216+
stdout := &bytes.Buffer{}
217+
cmd.Stdout = stdout
218+
219+
if err := cmd.Run(); err != nil {
220+
logrus.Debugf("failed to start plugin metadata command: %v", err)
221+
return ProviderMetadata{}
222+
}
223+
224+
var metadata ProviderMetadata
225+
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
226+
output, _ := io.ReadAll(stdout)
227+
logrus.Debugf("failed to decode plugin metadata: %v - %s", err, output)
228+
return ProviderMetadata{}
229+
}
230+
return metadata
231+
}
232+
233+
type ProviderMetadata struct {
234+
Description string `json:"description"`
235+
Up CommandMetadata `json:"up"`
236+
Down CommandMetadata `json:"down"`
237+
}
238+
239+
type CommandMetadata struct {
240+
Parameters []ParametersMetadata `json:"parameters"`
241+
}
242+
243+
type ParametersMetadata struct {
244+
Name string `json:"name"`
245+
Description string `json:"description"`
246+
Required bool `json:"required"`
247+
Type string `json:"type"`
248+
Default string `json:"default,omitempty"`
249+
}
250+
251+
func (c CommandMetadata) GetParameter(paramName string) (ParametersMetadata, bool) {
252+
for _, p := range c.Parameters {
253+
if p.Name == paramName {
254+
return p, true
255+
}
256+
}
257+
return ParametersMetadata{}, false
258+
}

0 commit comments

Comments
 (0)