Просмотр исходного кода

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 <[email protected]>

Guillaume Lours 7 месяцев назад
Родитель
Сommit
40f5786e68
3 измененных файлов с 198 добавлено и 7 удалено
  1. 72 4
      docs/examples/provider.go
  2. 67 2
      docs/extension.md
  3. 59 1
      pkg/compose/plugins.go

+ 72 - 4
docs/examples/provider.go

@@ -17,11 +17,13 @@
 package main
 package main
 
 
 import (
 import (
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"time"
 	"time"
 
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 )
 )
 
 
 func main() {
 func main() {
@@ -43,16 +45,27 @@ func composeCommand() *cobra.Command {
 		TraverseChildren: true,
 		TraverseChildren: true,
 	}
 	}
 	c.PersistentFlags().String("project-name", "", "compose project name") // unused
 	c.PersistentFlags().String("project-name", "", "compose project name") // unused
-	c.AddCommand(&cobra.Command{
+	upCmd := &cobra.Command{
 		Use:  "up",
 		Use:  "up",
 		Run:  up,
 		Run:  up,
 		Args: cobra.ExactArgs(1),
 		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",
 		Use:  "down",
 		Run:  down,
 		Run:  down,
 		Args: cobra.ExactArgs(1),
 		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
 	return c
 }
 }
 
 
@@ -72,3 +85,58 @@ func up(_ *cobra.Command, args []string) {
 func down(_ *cobra.Command, _ []string) {
 func down(_ *cobra.Command, _ []string) {
 	fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
 	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"`
+}

+ 67 - 2
docs/extension.md

@@ -20,6 +20,7 @@ the resource(s) needed to run a service.
       options:
       options:
         type: mysql
         type: mysql
         size: 256
         size: 256
+        name: myAwesomeCloudDB
 ```
 ```
 
 
 `provider.type` tells Compose the binary to run, which can be either:
 `provider.type` tells Compose the binary to run, which can be either:
@@ -104,8 +105,72 @@ into its runtime environment.
 ## Down lifecycle
 ## Down lifecycle
 
 
 `down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
 `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. 
+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
 ## Examples
 
 
-See [example](examples/provider.go) for illustration on implementing this API in a command line 
+See [example](examples/provider.go) for illustration on implementing this API in a command line 

+ 59 - 1
pkg/compose/plugins.go

@@ -17,6 +17,7 @@
 package compose
 package compose
 
 
 import (
 import (
+	"bytes"
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"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 {
 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
 	provider := *service.Provider
 
 
 	args := []string{"compose", "--project-name", project.Name, command}
 	args := []string{"compose", "--project-name", project.Name, command}
 	for k, v := range provider.Options {
 	for k, v := range provider.Options {
 		for _, value := range v {
 		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)
 	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()...)
 	cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
 	return cmd
 	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
+}