Browse Source

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 6 months ago
parent
commit
40f5786e68
3 changed files with 198 additions and 7 deletions
  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
 
 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"`
+}

+ 67 - 2
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 `<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
 
-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
 
 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
+}