diff --git a/docs/examples/provider.go b/docs/examples/provider.go index 6223a0f24..e500e8d5c 100644 --- a/docs/examples/provider.go +++ b/docs/examples/provider.go @@ -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"` +} diff --git a/docs/extension.md b/docs/extension.md index f0d5cb9e9..8991b63ca 100644 --- a/docs/extension.md +++ b/docs/extension.md @@ -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: @@ -104,8 +105,72 @@ into its runtime environment. ## Down lifecycle `down` lifecycle is equivalent to `up` with the ` compose --project-name down ` command. -The provider is responsible for releasing all resources associated with the service. +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 \ No newline at end of file +See [example](examples/provider.go) for illustration on implementing this API in a command line diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index bbf2c2ff6..092fb8dd0 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -17,6 +17,7 @@ package compose import ( + "bytes" "context" "encoding/json" "errors" @@ -161,12 +162,23 @@ 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 { - args = append(args, fmt.Sprintf("--%s=%s", k, value)) + if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok { + args = append(args, fmt.Sprintf("--%s=%s", k, value)) + } } } args = append(args, service.Name) @@ -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 +}