diff --git a/.gitignore b/.gitignore index ce6a29da5..065e2030c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ obj .vscode *.swp + +.idea +.DS_Store diff --git a/README.md b/README.md index b8c135a4a..a78e6283c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ The main commands supported by the CLI are: * `faas-cli deploy` - deploys the functions into a local or remote OpenFaaS gateway * `faas-cli remove` - removes the functions from a local or remote OpenFaaS gateway * `faas-cli invoke` - invokes the functions and reads from STDIN for the body of the request +* `faas-cli login` - stores basic auth credentials for OpenFaaS gateway (supports multiple gateways) +* `faas-cli logout` - removes basic auth credentials fora given gateway Help for all of the commands supported by the CLI can be found by running: @@ -51,7 +53,7 @@ You can chose between using a [programming language template](https://github.com **Templates** -Command: `faas-cli new FUNCTION_NAME --lang python/node/go//ruby/Dockerfile/etc` +Command: `faas-cli new FUNCTION_NAME --lang python/node/go/ruby/Dockerfile/etc` In your YAML you can also specify `lang: node/python/go/csharp/ruby` diff --git a/commands/login.go b/commands/login.go new file mode 100644 index 000000000..277b4dc20 --- /dev/null +++ b/commands/login.go @@ -0,0 +1,141 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package commands + +import ( + "crypto/tls" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + "time" + + "github.com/openfaas/faas-cli/config" + "github.com/spf13/cobra" +) + +var ( + username string + password string + passwordStdin bool +) + +func init() { + loginCmd.Flags().StringVar(&gateway, "gateway", defaultGateway, "Gateway URI") + loginCmd.Flags().StringVarP(&username, "username", "u", "", "Gateway username") + loginCmd.Flags().StringVarP(&password, "password", "p", "", "Gateway password") + loginCmd.Flags().BoolVar(&passwordStdin, "password-stdin", false, "Reads the gateway password from stdin") + + faasCmd.AddCommand(loginCmd) +} + +var loginCmd = &cobra.Command{ + Use: `login [--username USERNAME] [--password PASSWORD] [--gateway GATEWAY_URL]`, + Short: "Log in to OpenFaaS gateway", + Long: "Log in to OpenFaaS gateway.\nIf no gateway is specified, the default local one will be used.", + Example: ` faas-cli login -u user -p password --gateway http://localhost:8080 + cat ~/faas_pass.txt | faas-cli login -u user --password-stdin --gateway https://openfaas.mydomain.com`, + RunE: runLogin, +} + +func runLogin(cmd *cobra.Command, args []string) error { + + if len(username) == 0 { + return errors.New("must provide --username or -u") + } + + if len(password) > 0 { + fmt.Println("WARNING! Using --password is insecure, consider using: cat ~/faas_pass.txt | faas-cli login -u user --password-stdin") + if passwordStdin { + return errors.New("--password and --password-stdin are mutually exclusive") + } + + if len(username) == 0 { + return errors.New("must provide --username with --password") + } + } + + if passwordStdin { + if len(username) == 0 { + return errors.New("must provide --username with --password-stdin") + } + + passwordStdin, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + + password = strings.TrimSpace(string(passwordStdin)) + } + + password = strings.TrimSpace(password) + if len(password) == 0 { + return errors.New("must provide a non-empty password via --password or --password-stdin") + } + + fmt.Println("Calling the OpenFaaS server to validate the credentials...") + gateway = strings.TrimRight(strings.TrimSpace(gateway), "/") + if err := validateLogin(gateway, username, password); err != nil { + return err + } + + if err := config.UpdateAuthConfig(gateway, username, password); err != nil { + return err + } + + user, _, err := config.LookupAuthConfig(gateway) + if err != nil { + return err + } + fmt.Println("credentials saved for", user, gateway) + + return nil +} + +func validateLogin(url string, user string, pass string) error { + // TODO: provide --insecure flag for this + tr := &http.Transport{ + DisableKeepAlives: true, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{ + Transport: tr, + Timeout: time.Duration(5 * time.Second), + } + + // TODO: implement ping in the gateway API and call that + gatewayUrl := strings.TrimRight(url, "/") + req, _ := http.NewRequest("GET", gateway+"/system/functions", nil) + req.SetBasicAuth(user, pass) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + return fmt.Errorf("cannot connect to OpenFaaS on URL: %s", gatewayUrl) + } + + if res.Body != nil { + defer res.Body.Close() + } + + if res.TLS == nil { + fmt.Println("WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates.") + } + + switch res.StatusCode { + case http.StatusOK: + return nil + case http.StatusUnauthorized: + return errors.New("unable to login, either username or password is incorrect") + default: + bytesOut, err := ioutil.ReadAll(res.Body) + if err == nil { + return fmt.Errorf("server returned unexpected status code: %d - %s", res.StatusCode, string(bytesOut)) + } + } + + return nil +} diff --git a/commands/logout.go b/commands/logout.go new file mode 100644 index 000000000..8b497016f --- /dev/null +++ b/commands/logout.go @@ -0,0 +1,42 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package commands + +import ( + "errors" + "fmt" + "strings" + + "github.com/openfaas/faas-cli/config" + "github.com/spf13/cobra" +) + +func init() { + logoutCmd.Flags().StringVar(&gateway, "gateway", defaultGateway, "Gateway URI") + + faasCmd.AddCommand(logoutCmd) +} + +var logoutCmd = &cobra.Command{ + Use: `logout [--gateway GATEWAY_URL]`, + Short: "Log out from OpenFaaS gateway", + Long: "Log out from OpenFaaS gateway.\nIf no gateway is specified, the default local one will be used.", + Example: ` faas-cli logout --gateway https://openfaas.mydomain.com`, + RunE: runLogout, +} + +func runLogout(cmd *cobra.Command, args []string) error { + if len(gateway) == 0 { + return errors.New("gateway cannot be an empty string") + } + + gateway = strings.TrimRight(strings.TrimSpace(gateway), "/") + err := config.RemoveAuthConfig(gateway) + if err != nil { + return err + } + fmt.Println("credentials removed for", gateway) + + return nil +} diff --git a/config/file.go b/config/file.go new file mode 100644 index 000000000..21200a89d --- /dev/null +++ b/config/file.go @@ -0,0 +1,282 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package config + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/mitchellh/go-homedir" + "gopkg.in/yaml.v2" +) + +var ( + DefaultDir = "~/.openfaas" + // using .yml to avoid collision with openfaas-bitbar config + DefaultFile = "config.yml" +) + +type ConfigFile struct { + AuthConfigs []AuthConfig `yaml:"auths"` + FilePath string `yaml:"-"` +} + +type AuthConfig struct { + Gateway string `yaml:"gateway,omitempty"` + Auth string `yaml:"auth,omitempty"` + Token string `yaml:"token,omitempty"` +} + +// New initializes a config file for the given file path +func New(filePath string) (*ConfigFile, error) { + if filePath == "" { + return nil, errors.New("can't create config with empty filePath") + } + conf := &ConfigFile{ + AuthConfigs: make([]AuthConfig, 0), + FilePath: filePath, + } + + return conf, nil +} + +// EnsureFile creates the root dir and config file +func EnsureFile() (string, error) { + dirPath, err := homedir.Expand(DefaultDir) + if err != nil { + return "", err + } + + filePath := path.Clean(filepath.Join(dirPath, DefaultFile)) + if err := os.MkdirAll(filepath.Dir(filePath), 0700); err != nil { + return "", err + } + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return "", err + } + defer file.Close() + } + + return filePath, nil +} + +// FileExists returns true if the config file is located at the default path +func fileExists() bool { + dirPath, err := homedir.Expand(DefaultDir) + if err != nil { + return false + } + + filePath := path.Clean(filepath.Join(dirPath, DefaultFile)) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return false + } + + return true +} + +// Save writes the config to disk +func (configFile *ConfigFile) save() error { + file, err := os.OpenFile(configFile.FilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer file.Close() + + data, err := yaml.Marshal(configFile) + if err != nil { + return err + } + + _, err = file.Write(data) + return err +} + +// Load reads the yml file from disk +func (configFile *ConfigFile) load() error { + conf := &ConfigFile{} + + if _, err := os.Stat(configFile.FilePath); os.IsNotExist(err) { + return errors.New("can't load config from non existent filePath") + } + + data, err := ioutil.ReadFile(configFile.FilePath) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, conf); err != nil { + return err + } + + if len(conf.AuthConfigs) > 0 { + configFile.AuthConfigs = conf.AuthConfigs + } + return nil +} + +// EncodeAuth encodes the username and password strings to base64 +func EncodeAuth(username string, password string) string { + input := username + ":" + password + msg := []byte(input) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) + base64.StdEncoding.Encode(encoded, msg) + return string(encoded) +} + +// DecodeAuth decodes the input string from base64 to username and password +func DecodeAuth(input string) (string, string, error) { + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "", "", err + } + arr := strings.SplitN(string(decoded), ":", 2) + if len(arr) != 2 { + return "", "", errors.New("invalid auth config file") + } + return arr[0], arr[1], nil +} + +// UpdateAuthConfig creates or updates the username and password for a given gateway +func UpdateAuthConfig(gateway string, username string, password string) error { + _, err := url.ParseRequestURI(gateway) + if err != nil || len(gateway) < 1 { + return errors.New("invalid gateway URL") + } + + if len(username) < 1 { + return errors.New("username can't be an empty string") + } + + if len(password) < 1 { + return errors.New("password can't be an empty string") + } + + configPath, err := EnsureFile() + if err != nil { + return err + } + + cfg, err := New(configPath) + if err != nil { + return err + } + + if err := cfg.load(); err != nil { + return err + } + + auth := AuthConfig{ + Gateway: gateway, + Auth: "basic", + Token: EncodeAuth(username, password), + } + + index := -1 + for i, v := range cfg.AuthConfigs { + if gateway == v.Gateway { + index = i + break + } + } + + if index == -1 { + cfg.AuthConfigs = append(cfg.AuthConfigs, auth) + } else { + cfg.AuthConfigs[index] = auth + } + + if err := cfg.save(); err != nil { + return err + } + + return nil +} + +// LookupAuthConfig returns the username and password for a given gateway +func LookupAuthConfig(gateway string) (string, string, error) { + if !fileExists() { + return "", "", errors.New("config file not found") + } + + configPath, err := EnsureFile() + if err != nil { + return "", "", err + } + + cfg, err := New(configPath) + if err != nil { + return "", "", err + } + + if err := cfg.load(); err != nil { + return "", "", err + } + + for _, v := range cfg.AuthConfigs { + if gateway == v.Gateway { + user, pass, err := DecodeAuth(v.Token) + if err != nil { + return "", "", err + } + return user, pass, nil + } + } + + return "", "", fmt.Errorf("no auth config found for %s", gateway) +} + +// RemoveAuthConfig deletes the username and password for a given gateway +func RemoveAuthConfig(gateway string) error { + if !fileExists() { + return errors.New("config file not found") + } + + configPath, err := EnsureFile() + if err != nil { + return err + } + + cfg, err := New(configPath) + if err != nil { + return err + } + + if err := cfg.load(); err != nil { + return err + } + + index := -1 + for i, v := range cfg.AuthConfigs { + if gateway == v.Gateway { + index = i + break + } + } + + if index > -1 { + cfg.AuthConfigs = removeAuthByIndex(cfg.AuthConfigs, index) + if err := cfg.save(); err != nil { + return err + } + } else { + return fmt.Errorf("gateway %s not found in config", gateway) + } + + return nil +} + +func removeAuthByIndex(s []AuthConfig, index int) []AuthConfig { + return append(s[:index], s[index+1:]...) +} diff --git a/config/file_test.go b/config/file_test.go new file mode 100644 index 000000000..87d199040 --- /dev/null +++ b/config/file_test.go @@ -0,0 +1,219 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package config + +import ( + "io/ioutil" + "os" + "regexp" + "strings" + "testing" +) + +func Test_LookupAuthConfig_WithNoConfigFile(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test1.yml" + _, _, err := LookupAuthConfig("http://openfaas.test1") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:config file not found)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_UpdateAuthConfig_Insert(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test2.yml" + u := "admin" + p := "some pass" + gatewayURL := strings.TrimRight("http://openfaas.test/", "/") + UpdateAuthConfig(gatewayURL, u, p) + + user, pass, err := LookupAuthConfig(gatewayURL) + if err != nil { + t.Errorf("got error %s", err.Error()) + } + + if user != u || pass != p { + t.Errorf("got user %s and pass %s, expected %s %s", user, pass, u, p) + } +} + +func Test_UpdateAuthConfig_Update(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test3.yml" + u := "admin" + p := "pass" + gatewayURL := strings.TrimRight("http://openfaas.test/", "/") + UpdateAuthConfig(gatewayURL, u, p) + + user, pass, err := LookupAuthConfig(gatewayURL) + if err != nil { + t.Errorf("got error %s", err.Error()) + } + + if user != u || pass != p { + t.Errorf("got user %s and pass %s, expected %s %s", user, pass, u, p) + } + + u = "admin2" + p = "pass2" + UpdateAuthConfig(gatewayURL, u, p) + + user, pass, err = LookupAuthConfig(gatewayURL) + if err != nil { + t.Errorf("got error %s", err.Error()) + } + + if user != u || pass != p { + t.Errorf("got user %s and pass %s, expected %s %s", user, pass, u, p) + } +} + +func Test_UpdateAuthConfig_InvaidGatewayURL(t *testing.T) { + gateway := "http//test.test" + err := UpdateAuthConfig(gateway, "a", "b") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:invalid gateway)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_UpdateAuthConfig_EmptyGatewayURL(t *testing.T) { + gateway := "" + err := UpdateAuthConfig(gateway, "a", "b") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:invalid gateway)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_UpdateAuthConfig_EmptyUsername(t *testing.T) { + err := UpdateAuthConfig("http://test.test", "", "b") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:username)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_UpdateAuthConfig_EmptyPassword(t *testing.T) { + err := UpdateAuthConfig("http://test.test", "a", "") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:password)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_New_NoFile(t *testing.T) { + _, err := New("") + if err == nil { + t.Error("expected to fail on empty file path") + } +} + +func Test_EnsureFile(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test6.yml" + cfg, err := EnsureFile() + if err != nil { + t.Error(err.Error()) + } + if _, err := os.Stat(cfg); os.IsNotExist(err) { + t.Errorf("expected config at %s", cfg) + } +} + +func Test_EncodeAuth(t *testing.T) { + token := EncodeAuth("admin", "admin") + if token != "YWRtaW46YWRtaW4=" { + t.Errorf("Token not matched: %s", token) + } +} + +func Test_DecodeAuth(t *testing.T) { + u, p, err := DecodeAuth("YWRtaW46YWRtaW4=") + if err != nil || u != "admin" || p != "admin" { + t.Errorf("invalid base64 decoding") + } +} + +func Test_RemoveAuthConfig(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test7.yml" + + u := "admin" + p := "pass" + gatewayURL := strings.TrimRight("http://openfaas.test/", "/") + UpdateAuthConfig(gatewayURL, u, p) + + gatewayURL2 := strings.TrimRight("http://openfaas.test2/", "/") + UpdateAuthConfig(gatewayURL2, u, p) + + err := RemoveAuthConfig(gatewayURL) + if err != nil { + t.Errorf("got error %s", err.Error()) + } + + _, _, err = LookupAuthConfig(gatewayURL) + if err == nil { + t.Fatal("Error was not returned") + } + r := regexp.MustCompile(`(?m:no auth config found)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_RemoveAuthConfig_WithNoConfigFile(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test8.yml" + err := RemoveAuthConfig("http://openfaas.test1") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:config file not found)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} + +func Test_RemoveAuthConfig_WithUnknownGateway(t *testing.T) { + DefaultDir, _ = ioutil.TempDir("", "faas-cli-file-test") + DefaultFile = "test9.yml" + + u := "admin" + p := "pass" + gatewayURL := strings.TrimRight("http://openfaas.test/", "/") + UpdateAuthConfig(gatewayURL, u, p) + + err := RemoveAuthConfig("http://openfaas.test1") + if err == nil { + t.Errorf("Error was not returned") + } + + r := regexp.MustCompile(`(?m:gateway)`) + if !r.MatchString(err.Error()) { + t.Errorf("Error not matched: %s", err.Error()) + } +} diff --git a/proxy/auth.go b/proxy/auth.go new file mode 100644 index 000000000..b1a64b7a1 --- /dev/null +++ b/proxy/auth.go @@ -0,0 +1,21 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package proxy + +import ( + "net/http" + + "github.com/openfaas/faas-cli/config" +) + +//SetAuth sets basic auth for the given gateway +func SetAuth(req *http.Request, gateway string) { + username, password, err := config.LookupAuthConfig(gateway) + if err != nil { + // no auth info found + return + } + + req.SetBasicAuth(username, password) +} diff --git a/proxy/auth_test.go b/proxy/auth_test.go new file mode 100644 index 000000000..594da0bba --- /dev/null +++ b/proxy/auth_test.go @@ -0,0 +1,47 @@ +// Copyright (c) OpenFaaS Project 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package proxy + +import ( + "net/http" + "strings" + "testing" + + "io/ioutil" + + "github.com/openfaas/faas-cli/config" +) + +func Test_SetAuth_AuthorizationHeader(t *testing.T) { + //setup store + config.DefaultDir, _ = ioutil.TempDir("", "faas-cli-auth-test") + config.DefaultFile = "authtest1.yml" + basicAuthURL := strings.TrimRight("http://openfaas.test/", "/") + openURL := "http://openfaas.test/" + config.UpdateAuthConfig(basicAuthURL, "Aladdin", "open sesame") + + req, _ := http.NewRequest("GET", openURL, nil) + SetAuth(req, basicAuthURL) + header := req.Header.Get("Authorization") + expected := "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + if header != expected { + t.Errorf("got header %q, want %q", header, expected) + } +} + +func Test_SetAuth_SkipAuthorization(t *testing.T) { + //setup store + config.DefaultDir, _ = ioutil.TempDir("", "faas-cli-auth-test") + config.DefaultFile = "authtest2.yml" + basicAuthURL := strings.TrimRight("http://openfaas.test/", "/") + openURL := "http://openfaas.test2/" + config.UpdateAuthConfig(basicAuthURL, "Aladdin", "open sesame") + + req, _ := http.NewRequest("GET", openURL, nil) + SetAuth(req, "http://openfaas.test2") + header := req.Header.Get("Authorization") + if header != "" { + t.Errorf("got header %q, want none", header) + } +} diff --git a/proxy/delete.go b/proxy/delete.go index fda4fe157..66fcc09a3 100644 --- a/proxy/delete.go +++ b/proxy/delete.go @@ -28,6 +28,7 @@ func DeleteFunction(gateway string, functionName string) error { return err } req.Header.Set("Content-Type", "application/json") + SetAuth(req, gateway) delRes, delErr := c.Do(req) if delErr != nil { @@ -40,10 +41,12 @@ func DeleteFunction(gateway string, functionName string) error { } switch delRes.StatusCode { - case 200, 201, 202: + case http.StatusOK, http.StatusCreated, http.StatusAccepted: fmt.Println("Removing old function.") - case 404: + case http.StatusNotFound: fmt.Println("No existing function to remove") + case http.StatusUnauthorized: + fmt.Println("unauthorized access, run \"faas-cli login\" to setup authentication for this server") default: var bodyReadErr error bytesOut, bodyReadErr := ioutil.ReadAll(delRes.Body) diff --git a/proxy/deploy.go b/proxy/deploy.go index f0efc7cfc..743c2248e 100644 --- a/proxy/deploy.go +++ b/proxy/deploy.go @@ -12,8 +12,6 @@ import ( "strings" "time" - "os" - "github.com/openfaas/faas/gateway/requests" ) @@ -41,11 +39,7 @@ func DeployFunction(fprocess string, gateway string, functionName string, image gateway = strings.TrimRight(gateway, "/") if replace { - if deleteError := DeleteFunction(gateway, functionName); deleteError != nil { - fmt.Printf("Error while deleting function, so skipping deployment. %s\n", deleteError) - os.Exit(-1) - return - } + DeleteFunction(gateway, functionName) } req := requests.CreateFunctionRequest{ @@ -74,13 +68,13 @@ func DeployFunction(fprocess string, gateway string, functionName string, image var err error request, err = http.NewRequest(method, gateway+"/system/functions", reader) + SetAuth(request, gateway) if err != nil { fmt.Println(err) return } res, err := client.Do(request) - if err != nil { fmt.Println("Is FaaS deployed? Do you need to specify the --gateway flag?") fmt.Println(err) @@ -92,7 +86,7 @@ func DeployFunction(fprocess string, gateway string, functionName string, image } switch res.StatusCode { - case 200, 201, 202: + case http.StatusOK, http.StatusCreated, http.StatusAccepted: if update { fmt.Println("Updated.") } else { @@ -101,6 +95,8 @@ func DeployFunction(fprocess string, gateway string, functionName string, image deployedURL := fmt.Sprintf("URL: %s/function/%s\n", gateway, functionName) fmt.Println(deployedURL) + case http.StatusUnauthorized: + fmt.Println("unauthorized access, run \"faas-cli login\" to setup authentication for this server") default: bytesOut, err := ioutil.ReadAll(res.Body) if err == nil { diff --git a/proxy/invoke.go b/proxy/invoke.go index 43d081c4d..6e2e442f6 100644 --- a/proxy/invoke.go +++ b/proxy/invoke.go @@ -5,6 +5,7 @@ package proxy import ( "bytes" + "errors" "fmt" "io/ioutil" "net/http" @@ -26,7 +27,6 @@ func InvokeFunction(gateway string, name string, bytesIn *[]byte, contentType st qs, qsErr := buildQueryString(query) if qsErr != nil { return nil, qsErr - } gatewayURL := gateway + "/function/" + name + qs @@ -39,6 +39,7 @@ func InvokeFunction(gateway string, name string, bytesIn *[]byte, contentType st } req.Header.Add("Content-Type", contentType) + SetAuth(req, gateway) res, err := client.Do(req) @@ -53,13 +54,14 @@ func InvokeFunction(gateway string, name string, bytesIn *[]byte, contentType st } switch res.StatusCode { - case 200: + case http.StatusOK: var readErr error resBytes, readErr = ioutil.ReadAll(res.Body) if readErr != nil { return nil, fmt.Errorf("cannot read result from OpenFaaS on URL: %s %s", gateway, readErr) } - + case http.StatusUnauthorized: + return nil, errors.New("unauthorized access, run \"faas-cli login\" to setup authentication for this server") default: bytesOut, err := ioutil.ReadAll(res.Body) if err == nil { diff --git a/proxy/list.go b/proxy/list.go index 82fad16d4..257f1a3a8 100644 --- a/proxy/list.go +++ b/proxy/list.go @@ -5,6 +5,7 @@ package proxy import ( "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -24,11 +25,13 @@ func ListFunctions(gateway string) ([]requests.Function, error) { client := MakeHTTPClient(&timeout) getRequest, err := http.NewRequest(http.MethodGet, gateway+"/system/functions", nil) + SetAuth(getRequest, gateway) if err != nil { fmt.Println() fmt.Println(err) return nil, fmt.Errorf("cannot connect to OpenFaaS on URL: %s", gateway) } + res, err := client.Do(getRequest) if err != nil { fmt.Println() @@ -41,7 +44,7 @@ func ListFunctions(gateway string) ([]requests.Function, error) { } switch res.StatusCode { - case 200: + case http.StatusOK: bytesOut, err := ioutil.ReadAll(res.Body) if err != nil { @@ -51,7 +54,8 @@ func ListFunctions(gateway string) ([]requests.Function, error) { if jsonErr != nil { return nil, fmt.Errorf("cannot parse result from OpenFaaS on URL: %s\n%s", gateway, jsonErr.Error()) } - + case http.StatusUnauthorized: + return nil, errors.New("unauthorized access, run \"faas-cli login\" to setup authentication for this server") default: bytesOut, err := ioutil.ReadAll(res.Body) if err == nil { diff --git a/vendor.conf b/vendor.conf index 0a2953b4d..ea63443d8 100644 --- a/vendor.conf +++ b/vendor.conf @@ -4,4 +4,5 @@ github.com/spf13/cobra 2df9a531813370438a4d79bfc33e21f58063ed87 github.com/spf13/pflag e57e3eeb33f795204c1ca35f56c44f83227c6e6 github.com/ryanuber/go-glob 256dc444b735e061061cf46c809487313d5b0065 github.com/morikuni/aec 39771216ff4c63d11f5e604076f9c45e8be1067b -github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 \ No newline at end of file +github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +github.com/mitchellh/go-homedir b8bc1bf767474819792c23f32d8286a45736f1c6 diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 000000000..f9c841a51 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md new file mode 100644 index 000000000..d70706d5b --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/README.md @@ -0,0 +1,14 @@ +# go-homedir + +This is a Go library for detecting the user's home directory without +the use of cgo, so the library can be used in cross-compilation environments. + +Usage is incredibly simple, just call `homedir.Dir()` to get the home directory +for a user, and `homedir.Expand()` to expand the `~` in a path to the home +directory. + +**Why not just use `os/user`?** The built-in `os/user` package requires +cgo on Darwin systems. This means that any Go code that uses that package +cannot cross compile. But 99% of the time the use for `os/user` is just to +retrieve the home directory, which we can do for the current user without +cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go new file mode 100644 index 000000000..47e1f9ef8 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir.go @@ -0,0 +1,137 @@ +package homedir + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" +) + +// DisableCache will disable caching of the home directory. Caching is enabled +// by default. +var DisableCache bool + +var homedirCache string +var cacheLock sync.RWMutex + +// Dir returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Dir() (string, error) { + if !DisableCache { + cacheLock.RLock() + cached := homedirCache + cacheLock.RUnlock() + if cached != "" { + return cached, nil + } + } + + cacheLock.Lock() + defer cacheLock.Unlock() + + var result string + var err error + if runtime.GOOS == "windows" { + result, err = dirWindows() + } else { + // Unix-like system, so just assume Unix + result, err = dirUnix() + } + + if err != nil { + return "", err + } + homedirCache = result + return result, nil +} + +// Expand expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~`, the path is +// returned as-is. +func Expand(path string) (string, error) { + if len(path) == 0 { + return path, nil + } + + if path[0] != '~' { + return path, nil + } + + if len(path) > 1 && path[1] != '/' && path[1] != '\\' { + return "", errors.New("cannot expand user-specific home dir") + } + + dir, err := Dir() + if err != nil { + return "", err + } + + return filepath.Join(dir, path[1:]), nil +} + +func dirUnix() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + // If that fails, try getent + var stdout bytes.Buffer + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if err != exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd = exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func dirWindows() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + home = os.Getenv("USERPROFILE") + } + if home == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") + } + + return home, nil +}