Skip to content

Commit

Permalink
Merge branch 'implement-tag-cmd' into 'master'
Browse files Browse the repository at this point in the history
Implement tag/untag cmds

See merge request mistio/mist-cli!159
  • Loading branch information
d-mo committed Jun 24, 2022
2 parents 9962340 + a179d14 commit 4fe2767
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,5 +462,9 @@ func main() {
// Add metering command
cli.Root.AddCommand(meterCmd())

cli.Root.AddCommand(tagCmd())

cli.Root.AddCommand(untagCmd())

cli.Root.Execute()
}
251 changes: 251 additions & 0 deletions tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package main

import (
"encoding/json"
"os"
"strings"

"github.com/jmespath/go-jmespath"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
trie "github.com/v-pap/trie"
"gitlab.ops.mist.io/mistio/openapi-cli-generator/cli"
"gopkg.in/h2non/gentleman.v2"
)

var taggableResources []string = []string{
"cloud",
"cluster",
"key",
"machine",
"network",
"rule",
"schedule",
"script",
"secret",
"volume",
"zone",
}

var resourceListControllersMap map[string]func(params *viper.Viper) (*gentleman.Response, map[string]interface{}, cli.CLIOutputOptions, error) = map[string]func(params *viper.Viper) (*gentleman.Response, map[string]interface{}, cli.CLIOutputOptions, error){
"cloud": MistApiV2ListClouds,
"cluster": MistApiV2ListClusters,
"key": MistApiV2ListKeys,
"machine": MistApiV2ListMachines,
"network": MistApiV2ListNetworks,
"rule": MistApiV2ListRules,
"schedule": MistApiV2ListSchedules,
"script": MistApiV2ListScripts,
"secret": MistApiV2ListSecrets,
"volume": MistApiV2ListVolumes,
"zone": MistApiV2ListZones,
}

var resourceGetControllersMap map[string]func(param string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, cli.CLIOutputOptions, error) = map[string]func(param string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, cli.CLIOutputOptions, error){
"cloud": MistApiV2GetCloud,
"cluster": MistApiV2GetCluster,
"key": MistApiV2GetKey,
"machine": MistApiV2GetMachine,
"network": MistApiV2GetNetwork,
"rule": MistApiV2GetRule,
"schedule": MistApiV2GetSchedule,
"script": MistApiV2GetScript,
"secret": MistApiV2GetSecret,
"volume": MistApiV2GetVolume,
"zone": MistApiV2GetZone,
}

type KeyValuePair struct {
Key string `json:"key"`
Value string `json:"value"`
}

type Resource struct {
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
}

type Operation struct {
Operation string `json:"operation"`
Tags []KeyValuePair `json:"tags"`
Resources []Resource `json:"resources"`
}

type tagResourceBody struct {
Operations []Operation `json:"operations"`
}

func tagValidArgsFunction(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
resourceType := strings.Fields(cmd.Use)[0]
params := viper.New()
params.Set("only", "name")
var decoded interface{}
_, decoded, _, err := resourceListControllersMap[resourceType](params)
if err != nil {
logger.Fatalf("Error calling operation: %s", err.Error())
}
data, _ := jmespath.Search("data[].name", decoded)
j, _ := json.Marshal(data)
str := strings.Replace(strings.Replace(strings.Replace(string(j[:]), "[", "", -1), "]", "", -1), " ", "\\ ", -1)
return strings.Split(str, ","), cobra.ShellCompDirectiveNoFileComp
}

func tagRun(cmd *cobra.Command, args []string, params *viper.Viper, tagOperation string) {
resourceType := strings.Fields(cmd.Use)[0]
resourceNames := args[:len(args)-1]
stringTags := args[len(args)-1]
resources := []Resource{}
for _, resourceName := range resourceNames {
_, decodedResource, _, err := resourceGetControllersMap[resourceType](resourceName, params)
rawResourceID, _ := jmespath.Search("data.id", decodedResource)
resourceID, ok := rawResourceID.(string)
if !ok {
logger.Fatalf("Error parsing resource: %s", err.Error())
}
resources = append(resources, Resource{ResourceType: resourceType + "s", ResourceID: resourceID})
}
tags := []KeyValuePair{}
for _, stringTag := range strings.Split(stringTags, ",") {
splittedTag := strings.Split(stringTag, "=")
kv := KeyValuePair{}
kv.Key = splittedTag[0]
if len(splittedTag) > 1 {
kv.Value = splittedTag[1]
}
tags = append(tags, kv)
}
operations := []Operation{{Operation: tagOperation, Tags: tags, Resources: resources}}
body := tagResourceBody{Operations: operations}
rawBody, err := json.Marshal(body)
if err != nil {
logger.Fatalf("Error marshalling tags: %s", err.Error())
}
_, decodedTag, outputOptions, err := MistApiV2TagResources(params, string(rawBody))
if err != nil {
logger.Fatalf("Error calling operation: %s", err.Error())
}

if err := cli.Formatter.Format(decodedTag, params, outputOptions); err != nil {
logger.Fatalf("Formatting failed: %s", err.Error())
}

}

func calculateAliasesMap(terms []string) map[string][]string {
termsTrie := trie.New()
aliasesMap := make(map[string][]string)
for _, term := range terms {
termsTrie.Insert(term)
}
for _, term := range terms {
suffix, ok := termsTrie.FindLongestUniqueSuffix(term)
if !ok {
continue
}
aliasesMap[term] = calculateAliases(term, suffix)
}
return aliasesMap
}

func tagCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tag",
Short: "Tag resource",
}
cmd.SetErr(os.Stderr)
aliasesMap := calculateAliasesMap(taggableResources)
for _, resource := range taggableResources {
params := viper.New()
cmdResource := &cobra.Command{
Use: resource + " RESOURCE... TAGS",
Short: "Tag " + resource,
Aliases: aliasesMap[resource],
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return tagValidArgsFunction(cmd, args, toComplete)
},
Run: func(cmd *cobra.Command, args []string) {
tagRun(cmd, args, params, "add")
},
}
cmd.AddCommand(cmdResource)
}
return cmd
}

func untagCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "untag",
Short: "Untag resource",
}
cmd.SetErr(os.Stderr)
aliasesMap := calculateAliasesMap(taggableResources)
for _, resource := range taggableResources {
params := viper.New()
cmdResource := &cobra.Command{
Use: resource + " RESOURCE... TAGS",
Short: "Untag " + resource,
Aliases: aliasesMap[resource],
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return tagValidArgsFunction(cmd, args, toComplete)
},
Run: func(cmd *cobra.Command, args []string) {
tagRun(cmd, args, params, "remove")
},
}
cmd.AddCommand(cmdResource)
}
return cmd
}

// MistApiV2TagResources Tag Resources
func MistApiV2TagResources(params *viper.Viper, body string) (*gentleman.Response, interface{}, cli.CLIOutputOptions, error) {
handlerPath := "tag-resources"
if mistApiV2Subcommand {
handlerPath = "Mist CLI " + handlerPath
}

err := setContext()
if err != nil {
return nil, nil, cli.CLIOutputOptions{}, err
}

server, err := getServer()
if err != nil {
return nil, nil, cli.CLIOutputOptions{}, err
}

url := server + "/api/v2/tags"

req := cli.Client.Post().URL(url)

if body != "" {
req = req.AddHeader("Content-Type", "application/json").BodyString(body)
}

cli.HandleBefore(handlerPath, params, req)

resp, err := req.Do()
if err != nil {
return resp, nil, cli.CLIOutputOptions{}, errors.Wrap(err, "Request failed")
}

var decoded interface{}

if resp.StatusCode < 400 {
if err := cli.UnmarshalResponse(resp, &decoded); err != nil {
return resp, nil, cli.CLIOutputOptions{}, errors.Wrap(err, "Unmarshalling response failed")
}
} else {
return resp, nil, cli.CLIOutputOptions{}, errors.Errorf("HTTP %d: %s", resp.StatusCode, resp.String())
}

after := cli.HandleAfter(handlerPath, params, resp, decoded)
if after != nil {
decoded = after
}

return resp, decoded, cli.CLIOutputOptions{[]string{}, []string{}, []string{}, []string{}, map[string]string{}}, nil
}

0 comments on commit 4fe2767

Please sign in to comment.