From 16ca82713faf6678d4b7055130768541f86ea20c Mon Sep 17 00:00:00 2001 From: Ritu Sood Date: Tue, 25 Aug 2020 22:01:22 -0700 Subject: CLI code for EMCO Add cli emcoctl as a client utility for EMCO Issue-ID: MULTICLOUD-1065 Signed-off-by: Ritu Sood Change-Id: Ie1951910628469b5a7e75550b9daa34ba377d1a4 --- src/tools/emcoctl/cmd/apply.go | 62 ++++++++ src/tools/emcoctl/cmd/config.go | 80 +++++++++++ src/tools/emcoctl/cmd/delete.go | 57 ++++++++ src/tools/emcoctl/cmd/get.go | 40 ++++++ src/tools/emcoctl/cmd/getall.go | 39 +++++ src/tools/emcoctl/cmd/root.go | 85 +++++++++++ src/tools/emcoctl/cmd/utils.go | 305 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 src/tools/emcoctl/cmd/apply.go create mode 100644 src/tools/emcoctl/cmd/config.go create mode 100644 src/tools/emcoctl/cmd/delete.go create mode 100644 src/tools/emcoctl/cmd/get.go create mode 100644 src/tools/emcoctl/cmd/getall.go create mode 100644 src/tools/emcoctl/cmd/root.go create mode 100644 src/tools/emcoctl/cmd/utils.go (limited to 'src/tools/emcoctl/cmd') diff --git a/src/tools/emcoctl/cmd/apply.go b/src/tools/emcoctl/cmd/apply.go new file mode 100644 index 00000000..f451a614 --- /dev/null +++ b/src/tools/emcoctl/cmd/apply.go @@ -0,0 +1,62 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// applyCmd represents the apply command +var applyCmd = &cobra.Command{ + Use: "apply", + Short: "apply(Post) the resources from input file or url(with body) from command line", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("apply called") + c := NewRestClient() + if len(inputFiles) > 0 { + resources := readResources() + for _, res := range resources { + if res.file != "" { + err := c.RestClientMultipartPost(res.anchor, res.body, res.file) + if err != nil { + fmt.Println("Apply: ", res.anchor, "Error: ",err) + return + } + } else { + err := c.RestClientPost(res.anchor, res.body) + if err != nil { + fmt.Println("Apply: ", res.anchor, "Error: ",err) + return + } + } + } + } else if len(args) >= 1 { + fmt.Println(args[0]) + c.RestClientPost(args[0], []byte{}) + } else { + fmt.Println("Error: No args ") + } + }, +} + +func init() { + fmt.Println("INIT ") + rootCmd.AddCommand(applyCmd) + applyCmd.Flags().StringSliceVarP(&inputFiles, "filename", "f", []string{}, "Filename of the input file") + applyCmd.Flags().StringSliceVarP(&valuesFiles, "values", "v", []string{}, "Values to go with the file") +} diff --git a/src/tools/emcoctl/cmd/config.go b/src/tools/emcoctl/cmd/config.go new file mode 100644 index 00000000..c5e44660 --- /dev/null +++ b/src/tools/emcoctl/cmd/config.go @@ -0,0 +1,80 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "strconv" +) + +// Configurations exported +type EmcoConfigurations struct { + Orchestrator ControllerConfigurations + Clm ControllerConfigurations + Ncm ControllerConfigurations + Dcm ControllerConfigurations + Rsync ControllerConfigurations + OvnAction ControllerConfigurations +} + +// ControllerConfigurations exported +type ControllerConfigurations struct { + Port int + Host string +} + +const urlVersion string = "v2" +const urlPrefix string = "http://" +var Configurations EmcoConfigurations + +// GetOrchestratorURL Url for Orchestrator +func GetOrchestratorURL() string { + if Configurations.Orchestrator.Host == "" || Configurations.Orchestrator.Port == 0 { + panic("No Orchestrator Information in Config File") + } + return urlPrefix + Configurations.Orchestrator.Host + ":" + strconv.Itoa(Configurations.Orchestrator.Port) + "/" + urlVersion +} + +// GetClmURL Url for Clm +func GetClmURL() string { + if Configurations.Clm.Host == "" || Configurations.Clm.Port == 0 { + panic("No Clm Information in Config File") + } + return urlPrefix + Configurations.Clm.Host + ":" + strconv.Itoa(Configurations.Clm.Port) + "/" + urlVersion +} + +// GetNcmURL Url for Ncm +func GetNcmURL() string { + if Configurations.Ncm.Host == "" || Configurations.Ncm.Port == 0 { + panic("No Ncm Information in Config File") + } + return urlPrefix + Configurations.Ncm.Host + ":" + strconv.Itoa(Configurations.Ncm.Port) + "/" + urlVersion +} + +// GetDcmURL Url for Dcm +func GetDcmURL() string { + if Configurations.Dcm.Host == "" || Configurations.Dcm.Port == 0 { + panic("No Dcm Information in Config File") + } + return urlPrefix + Configurations.Dcm.Host + ":" + strconv.Itoa(Configurations.Dcm.Port) + "/" + urlVersion +} + +// GetOvnactionURL Url for Ovnaction +func GetOvnactionURL() string { + if Configurations.OvnAction.Host == "" || Configurations.OvnAction.Port == 0 { + panic("No Ovn Action Information in Config File") + } + return urlPrefix + Configurations.OvnAction.Host + ":" + strconv.Itoa(Configurations.OvnAction.Port) + "/" + urlVersion +} diff --git a/src/tools/emcoctl/cmd/delete.go b/src/tools/emcoctl/cmd/delete.go new file mode 100644 index 00000000..d6dbfe34 --- /dev/null +++ b/src/tools/emcoctl/cmd/delete.go @@ -0,0 +1,57 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// deleteCmd represents the delete command +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete resources in input file or commandline", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("delete called") + c := NewRestClient() + if len(inputFiles) > 0 { + resources := readResources() + for i := len(resources) - 1; i >= 0; i-- { + res := resources[i] + c.RestClientDelete(res.anchor, res.body) + } + } else if len(args) >= 1 { + fmt.Println(args[0]) + c.RestClientDelete(args[0], nil) + } + }, +} + +func init() { + rootCmd.AddCommand(deleteCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // deleteCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + deleteCmd.Flags().StringSliceVarP(&inputFiles, "filename", "f", []string{}, "Filename of the input file") +} diff --git a/src/tools/emcoctl/cmd/get.go b/src/tools/emcoctl/cmd/get.go new file mode 100644 index 00000000..2cc96dc4 --- /dev/null +++ b/src/tools/emcoctl/cmd/get.go @@ -0,0 +1,40 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// getCmd represents the get command +var getCmd = &cobra.Command{ + Use: "get", + Short: "Get the resource(s) based on the URL", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("get called") + c := NewRestClient() + if len(args) >= 1 { + fmt.Println(args[0]) + c.RestClientGet(args[0]) + } + }, +} + +func init() { + rootCmd.AddCommand(getCmd) +} diff --git a/src/tools/emcoctl/cmd/getall.go b/src/tools/emcoctl/cmd/getall.go new file mode 100644 index 00000000..329b2582 --- /dev/null +++ b/src/tools/emcoctl/cmd/getall.go @@ -0,0 +1,39 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// getallCmd represents the getall command +var getallCmd = &cobra.Command{ + Use: "getall", + Short: "Get all resources in the file provided", + Run: func(cmd *cobra.Command, args []string) { + resources := readResources() + c := NewRestClient() + for _, res := range resources { + c.RestClientGetAll(res.anchor) + } + }, +} + +func init() { + rootCmd.AddCommand(getallCmd) + // Here you will define your flags and configuration settings. + getallCmd.Flags().StringSliceVarP(&inputFiles, "filename", "f", []string{}, "Filename of the input file") +} diff --git a/src/tools/emcoctl/cmd/root.go b/src/tools/emcoctl/cmd/root.go new file mode 100644 index 00000000..4c1ac19f --- /dev/null +++ b/src/tools/emcoctl/cmd/root.go @@ -0,0 +1,85 @@ +/* +Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd +import ( + "fmt" + "github.com/spf13/cobra" + "os" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "emco", + Short: "Emcoctl - CLI for EMCO", + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) {fmt.Println("emcoctl -f file") }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println("Test") + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.emco.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + cfgFile = "emco-cfg.yaml" + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println(home) + // Search config in home directory with name ".emco" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".emco") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + err := viper.Unmarshal(&Configurations) + if err != nil { + fmt.Printf("Unable to decode into struct, %v", err) + } + } +} \ No newline at end of file diff --git a/src/tools/emcoctl/cmd/utils.go b/src/tools/emcoctl/cmd/utils.go new file mode 100644 index 00000000..34063eee --- /dev/null +++ b/src/tools/emcoctl/cmd/utils.go @@ -0,0 +1,305 @@ +/*Copyright © 2020 Intel Corp + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + neturl "net/url" + "os" + "strings" + + "github.com/go-resty/resty/v2" + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" + pkgerrors "github.com/pkg/errors" +) + +var inputFiles []string +var valuesFiles []string + +type ResourceContext struct { + Anchor string `json:"anchor" yaml:"anchor"` +} + +type Metadata struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + UserData1 string `yaml:"userData1,omitempty" json:"userData1,omitempty"` + UserData2 string `yaml:"userData2,omitempty" json:"userData2,omitempty"` +} + +type emcoRes struct { + Version string `yaml:"version" json:"version"` + Context ResourceContext `yaml:"resourceContext" json:"resourceContext"` + Meta Metadata `yaml:"metadata" json:"metadata"` + Spec map[string]interface{} `yaml:"spec,omitempty" json:"spec,omitempty"` + File string `yaml:"file,omitempty" json:"file,omitempty"` + Label string `yaml:"label-name,omitempty" json:"label-name,omitempty"` +} + +type emcoBody struct { + Meta Metadata `json:"metadata,omitempty"` + Label string `json:"label-name,omitempty"` + Spec map[string]interface{} `json:"spec,omitempty"` +} + +type emcoCompositeAppSpec struct { + Version string `json: "version"` +} + +type Resources struct { + anchor string + body []byte + file string +} +// RestyClient to use with CLI +type RestyClient struct { + client *resty.Client +} + +var Client RestyClient + +// NewRestClient returns a rest client +func NewRestClient() RestyClient { + // Create a Resty Client + Client.client = resty.New() + // Bearer Auth Token for all request + // Client.client.SetAuthToken() + // Registering global Error object structure for JSON/XML request + //Client.client.SetError(&Error{}) + return Client +} + +// readResources reads all the resources in the file provided +func readResources() []Resources { + // TODO: Remove Assumption only one file + // Open file and Parse to get all resources + var resources []Resources + f, err := os.Open(inputFiles[0]) + defer f.Close() + if err != nil { + fmt.Printf("Error %s reading file %s\n", err, inputFiles[0]) + return []Resources{} + } + if len(valuesFiles) > 0 { + //Apply template + } + + dec := yaml.NewDecoder(f) + // Iterate through all resources in the file + for { + var doc emcoRes + if dec.Decode(&doc) != nil { + break + } + body := &emcoBody{Meta: doc.Meta, Spec: doc.Spec, Label: doc.Label} + jsonBody, err := json.Marshal(body) + if err != nil { + return []Resources{} + } + var res Resources + if doc.File != "" { + res = Resources{anchor: doc.Context.Anchor, body: jsonBody, file: doc.File} + } else { + res = Resources{anchor: doc.Context.Anchor, body: jsonBody} + } + resources = append(resources, res) + } + return resources +} + +//RestClientPost to post to server no multipart +func (r RestyClient) RestClientPost(anchor string, body []byte) error { + + url, err := GetURL(anchor) + if err != nil { + return err + } + + // POST JSON string + resp, err := r.client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + Post(url) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("URL:", anchor, "Response Code:", resp.StatusCode(), "Response:", resp) + if (resp.StatusCode() >= 201 && resp.StatusCode() <= 299) { + return nil + } + return pkgerrors.Errorf("Server Post Error") +} + +//RestClientMultipartPost to post to server with multipart +func (r RestyClient) RestClientMultipartPost(anchor string, body []byte, file string) error { + url, err := GetURL(anchor) + if err != nil { + return err + } + + // Read file for multipart + f, err := ioutil.ReadFile(file) + if err != nil { + fmt.Printf("Error %s reading file %s\n", err, file) + return err + } + + // Multipart Post + formParams := neturl.Values{} + formParams.Add("metadata", string(body)) + resp, err := r.client.R(). + SetFileReader("file", "filename", bytes.NewReader(f)). + SetFormDataFromValues(formParams). + Post(url) + + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("URL:", anchor, "Response Code:", resp.StatusCode(), "Response:", resp) + if (resp.StatusCode() >= 201 && resp.StatusCode() <= 299) { + return nil + } + return pkgerrors.Errorf("Server Multipart Post Error") +} +// RestClientGetAll returns all resource in the input file +func (r RestyClient) RestClientGetAll(anchor string) error { + url, err := GetURL(anchor) + if err != nil { + return err + } + resp, err := r.client.R(). + Get(url) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("URL:", anchor, "Response Code:", resp.StatusCode(), "Response:", resp) + return nil +} +// RestClientGet gets resource +func (r RestyClient) RestClientGet(anchor string) error { + s := strings.Split(anchor, "/") + a := s[len(s)-2] + // Determine if multipart + if a == "apps" || a == "profiles" || a == "clusters" { + url, err := GetURL(anchor) + if err != nil { + return err + } + // Supports only getting metadata + resp, err := r.client.R(). + SetHeader("Accept", "application/json"). + Get(url) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("URL:", anchor, "Response Code:", resp.StatusCode(), "Response:", resp) + } else { + r.RestClientGetAll(anchor) + } + + return nil +} +// RestClientDelete calls rest delete command +func (r RestyClient) RestClientDelete(anchor string, body []byte) error { + var url string + + s := strings.Split(anchor, "/") + a := s[len(s)-1] + if a == "instantiate" { + // Change instantiate to destroy + s[len(s)-1] = "terminate" + anchor = strings.Join(s[:], "/") + fmt.Println("URL:", anchor) + return r.RestClientPost(anchor, []byte{}) + } else if a == "apply" { + // Change apply to terminate + s[len(s)-1] = "terminate" + anchor = strings.Join(s[:], "/") + fmt.Println("URL:", anchor) + return r.RestClientPost(anchor, []byte{}) + } else if a == "approve" || a == "status" { + // Approve and status doesn't have delete + return nil + } + var e emcoBody + err := json.Unmarshal(body, &e) + if err != nil { + fmt.Println(err) + return err + } + if e.Meta.Name != "" { + s := strings.Split(anchor, "/") + a := s[len(s)-1] + name := e.Meta.Name + anchor = anchor + "/" + name + if a == "composite-apps" { + var cav emcoCompositeAppSpec + err := mapstructure.Decode(e.Spec, &cav) + if err != nil { + fmt.Println("mapstruct error") + return err + } + anchor = anchor + "/" + cav.Version + } + } + url, err = GetURL(anchor) + if err != nil { + return err + } + resp, err := r.client.R(). + Delete(url) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("URL:", anchor, "Response Code:", resp.StatusCode()) + return nil +} +// GetURL reads the configuration file to get URL +func GetURL(anchor string) (string, error) { + var baseUrl string + s := strings.Split(anchor, "/") + if len(s) < 1 { + return "", fmt.Errorf("Invalid Anchor") + } + + switch s[0] { + case "cluster-providers": + if len(s) >= 5 && (s[4] == "networks" || s[4] == "provider-networks" || s[4] == "apply" || s[4] == "terminate") { + baseUrl = GetNcmURL() + } else { + baseUrl = GetClmURL() + } + case "controllers": + baseUrl = GetOrchestratorURL() + case "projects": + if len(s) >= 6 && s[5] == "network-controller-intent" { + baseUrl = GetOvnactionURL() + } else { + baseUrl = GetOrchestratorURL() + } + default: + return "", fmt.Errorf("Invalid Anchor") + } + return (baseUrl + "/" + anchor), nil +} -- cgit 1.2.3-korg