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>
This commit is contained in:
Guillaume Lours 2025-06-03 18:20:09 +02:00 committed by Nicolas De loof
parent 61e44da936
commit 40f5786e68
3 changed files with 198 additions and 7 deletions

View File

@ -17,11 +17,13 @@
package main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func main() {
@ -43,16 +45,27 @@ func composeCommand() *cobra.Command {
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
c.AddCommand(&cobra.Command{
upCmd := &cobra.Command{
Use: "up",
Run: up,
Args: cobra.ExactArgs(1),
})
c.AddCommand(&cobra.Command{
}
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().Int("size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
downCmd := &cobra.Command{
Use: "down",
Run: down,
Args: cobra.ExactArgs(1),
})
}
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
return c
}
@ -72,3 +85,58 @@ func up(_ *cobra.Command, args []string) {
func down(_ *cobra.Command, _ []string) {
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
}
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "metadata",
Run: func(cmd *cobra.Command, _ []string) {
metadata(upCmd, downCmd)
},
Args: cobra.NoArgs,
}
}
func metadata(upCmd, downCmd *cobra.Command) {
metadata := ProviderMetadata{}
metadata.Description = "Manage services on AwesomeCloud"
metadata.Up = commandParameters(upCmd)
metadata.Down = commandParameters(downCmd)
jsonMetadata, err := json.Marshal(metadata)
if err != nil {
panic(err)
}
fmt.Println(string(jsonMetadata))
}
func commandParameters(cmd *cobra.Command) CommandMetadata {
cmdMetadata := CommandMetadata{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
Name: f.Name,
Description: f.Usage,
Required: isRequired,
Type: f.Value.Type(),
Default: f.DefValue,
})
})
return cmdMetadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []Metadata `json:"parameters"`
}
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}

View File

@ -20,6 +20,7 @@ the resource(s) needed to run a service.
options:
type: mysql
size: 256
name: myAwesomeCloudDB
```
`provider.type` tells Compose the binary to run, which can be either:
@ -106,6 +107,70 @@ into its runtime environment.
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
The provider is responsible for releasing all resources associated with the service.
## Provide metadata about options
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
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.
```console
awesomecloud compose metadata
```
The expected JSON output format is:
```json
{
"description": "Manage services on AwesomeCloud",
"up": {
"parameters": [
{
"name": "type",
"description": "Database type (mysql, postgres, etc.)",
"required": true,
"type": "string"
},
{
"name": "size",
"description": "Database size in GB",
"required": false,
"type": "integer",
"default": "10"
},
{
"name": "name",
"description": "Name of the database to be created",
"required": true,
"type": "string"
}
]
},
"down": {
"parameters": [
{
"name": "name",
"description": "Name of the database to be removed",
"required": true,
"type": "string"
}
]
}
}
```
The top elements are:
- `description`: Human-readable description of the provider
- `up`: Object describing the parameters accepted by the `up` command
- `down`: Object describing the parameters accepted by the `down` command
And for each command parameter, you should include the following properties:
- `name`: The parameter name (without `--` prefix)
- `description`: Human-readable description of the parameter
- `required`: Boolean indicating if the parameter is mandatory
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
- `default`: Default value (optional, only for non-required parameters)
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
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.
## Examples
See [example](examples/provider.go) for illustration on implementing this API in a command line

View File

@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -161,14 +162,25 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
}
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) *exec.Cmd {
cmdOptionsMetadata := s.getPluginMetadata(path)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
currentCommandMetadata = cmdOptionsMetadata.Up
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
provider := *service.Provider
args := []string{"compose", "--project-name", project.Name, command}
for k, v := range provider.Options {
for _, value := range v {
if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok {
args = append(args, fmt.Sprintf("--%s=%s", k, value))
}
}
}
args = append(args, service.Name)
cmd := exec.CommandContext(ctx, path, args...)
@ -198,3 +210,49 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd
}
func (s *composeService) getPluginMetadata(path string) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
logrus.Debugf("failed to start plugin metadata command: %v", err)
return ProviderMetadata{}
}
var metadata ProviderMetadata
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
output, _ := io.ReadAll(stdout)
logrus.Debugf("failed to decode plugin metadata: %v - %s", err, output)
return ProviderMetadata{}
}
return metadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []ParametersMetadata `json:"parameters"`
}
type ParametersMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
func (c CommandMetadata) GetParameter(paramName string) (ParametersMetadata, bool) {
for _, p := range c.Parameters {
if p.Name == paramName {
return p, true
}
}
return ParametersMetadata{}, false
}