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:
parent
61e44da936
commit
40f5786e68
@ -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"`
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user