diff --git a/Makefile b/Makefile index d9c05e0..7b49874 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,10 @@ GITHUB_REPO_HOST_AND_PATH := github.com/$(GITHUB_REPO_OWNER)/$(GITHUB_REPO_NAME) IMAGE_NAME := quay.io/nordstrom/kubelogin BUILD := build CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) -CURRENT_TAG := v0.0.52 +# NOTE: only patch level version is auto incremented to set a new version +# to specify a new version: $ make version CURRENT_TAG="v0.2.0" +LAST_TAG := $(shell git tag --list | tail -1) +CURRENT_TAG ?= $(shell echo $(LAST_TAG) | awk -F. '{print $$1"."$$2"."$$NF+1}') GOLANG_TOOLCHAIN_VERSION := 1.9.1 .PHONY: image/build image/push @@ -139,6 +142,7 @@ $(BUILD)/server/kubelogin-server-$(CURRENT_TAG)-%: cmd/server/*.go | $(BUILD)/se -e GOOS=$* \ golang:$(GOLANG_TOOLCHAIN_VERSION) \ go build -v -o /go/bin/$(@F) \ + -ldflags "-X main.version=$(CURRENT_TAG)" \ $(GITHUB_REPO_HOST_AND_PATH)/cmd/server/ $(BUILD)/cli/kubelogin-cli-$(CURRENT_TAG)-%: cmd/cli/*.go | $(BUILD)/cli @@ -149,6 +153,7 @@ $(BUILD)/cli/kubelogin-cli-$(CURRENT_TAG)-%: cmd/cli/*.go | $(BUILD)/cli -e GOOS=$* \ golang:$(GOLANG_TOOLCHAIN_VERSION) \ go build -v -o /go/bin/$(@F) \ + -ldflags "-X main.version=$(CURRENT_TAG)" \ $(GITHUB_REPO_HOST_AND_PATH)/cmd/cli/ \ .PHONY: test_app @@ -160,3 +165,8 @@ build $(BUILD)/server $(BUILD)/server/linux $(BUILD)/cli $(BUILD)/cli/mac $(BUIL clean: rm -rf build + +.PHONY: version +version: ${ARGS} + @echo "Last tag: $(LAST_TAG)" + @echo "Next tag: $(CURRENT_TAG)" diff --git a/cmd/cli/app.go b/cmd/cli/app.go new file mode 100644 index 0000000..a3d7742 --- /dev/null +++ b/cmd/cli/app.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os/exec" + + "github.com/pkg/errors" + yaml "gopkg.in/yaml.v2" +) + +type app struct { + filenameWithPath string + kubectlUser string + kubeloginAlias string + kubeloginServer string +} + +func (app *app) makeExchange(token string) error { + url := fmt.Sprintf("%s/exchange?token=%s", app.kubeloginServer, token) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("Unable to create request. %s", err) + return err + } + client := http.DefaultClient + res, err := client.Do(req) + if err != nil { + log.Printf("Unable to make request. %s", err) + return err + } + if res.StatusCode != http.StatusOK { + log.Fatalf("Failed to retrieve token from kubelogin server. Please try again or contact your administrator") + } + defer res.Body.Close() // nolint: errcheck + jwt, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Unable to read response body. %s", err) + return err + } + if err := app.configureKubectl(string(jwt)); err != nil { + log.Printf("Error when setting credentials: %v", err) + return err + } + return nil +} + +func (app *app) tokenHandler(w http.ResponseWriter, r *http.Request) { + token := r.FormValue("token") + if err := app.makeExchange(token); err != nil { + log.Fatalf("Could not exchange token for jwt %v", err) + } + fmt.Fprint(w, "You are now logged in! You can close this window") + doneChannel <- true +} + +func (app *app) configureKubectl(jwt string) error { + configCmd := exec.Command("kubectl", "config", "set-credentials", app.kubectlUser, "--token="+jwt) + return configCmd.Run() +} + +func (app *app) generateAuthURL() (string, string, error) { + portNum, err := findFreePort() + if err != nil { + log.Print("err, could not find an open port") + return "", "", err + } + + loginURL := fmt.Sprintf("%s/login?port=%s", app.kubeloginServer, portNum) + + return loginURL, portNum, nil +} + +func (app *app) getConfigSettings(alias string) error { + yamlFile, err := ioutil.ReadFile(app.filenameWithPath) + if err != nil { + return errors.Wrap(err, "failed to read config file for login use") + } + var config Config + if err := yaml.Unmarshal(yamlFile, &config); err != nil { + return errors.Wrap(err, "failed to unmarshal yaml file for login use") + } + + aliasConfig, ok := config.aliasSearch(alias) + if !ok { + return errors.New("Could not find specified alias, check spelling or use the config verb to create an alias") + } + app.kubectlUser = aliasConfig.KubectlUser + app.kubeloginServer = aliasConfig.BaseURL + return nil +} + +func (app *app) configureFile(kubeloginrcAlias string, loginServerURL *url.URL, kubectlUser string) error { + var config Config + aliasConfig := config.newAliasConfig(kubeloginrcAlias, loginServerURL.String(), kubectlUser) + yamlFile, err := ioutil.ReadFile(app.filenameWithPath) + if err != nil { + return config.createConfig(app.filenameWithPath, aliasConfig) // Either error or nil value + } + if err := yaml.Unmarshal(yamlFile, &config); err != nil { + return errors.Wrap(err, "failed to unmarshal yaml file") + } + foundAliasConfig, ok := config.aliasSearch(aliasFlag) + if !ok { + newConfig := config.newAliasConfig(kubeloginrcAlias, loginServerURL.String(), kubectlUser) + config.appendAlias(newConfig) + if err := config.writeToFile(app.filenameWithPath); err != nil { + log.Fatal(err) + } + log.Print("New Alias configured") + return nil + } + + return config.updateAlias(foundAliasConfig, loginServerURL, app.filenameWithPath) // Either error or nil value +} diff --git a/cmd/cli/config.go b/cmd/cli/config.go new file mode 100644 index 0000000..e13d268 --- /dev/null +++ b/cmd/cli/config.go @@ -0,0 +1,81 @@ +package main + +import ( + "io/ioutil" + "log" + "net/url" + "os" + + "github.com/pkg/errors" + yaml "gopkg.in/yaml.v2" +) + +// Config contains the array of aliases (AliasConfig) +type Config struct { + Aliases []*AliasConfig `yaml:"aliases"` +} + +func (config *Config) aliasSearch(alias string) (*AliasConfig, bool) { + for index, aliases := range config.Aliases { + if alias == aliases.Alias { + return config.Aliases[index], true + } + } + return nil, false +} + +func (config *Config) createConfig(onDiskFile string, aliasConfig AliasConfig) error { + log.Print("Couldn't find config file in root directory. Creating config file...") + _, e := os.Stat(onDiskFile) // Does config file exist? + if os.IsNotExist(e) { // Create file + fh, err := os.Create(onDiskFile) + if err != nil { + return errors.Wrap(err, "failed to create file in root directory") + } + _ = fh.Close() + } + + log.Print("Config file created, setting config values...") + config.Aliases = make([]*AliasConfig, 0) + config.appendAlias(aliasConfig) + if err := config.writeToFile(onDiskFile); err != nil { + log.Fatal(err) + } + log.Print("File configured") + return nil +} + +func (config *Config) newAliasConfig(kubeloginrcAlias, loginServerURL, kubectlUser string) AliasConfig { + newConfig := AliasConfig{ + BaseURL: loginServerURL, + Alias: kubeloginrcAlias, + KubectlUser: kubectlUser, + } + return newConfig +} + +func (config *Config) appendAlias(aliasConfig AliasConfig) { + config.Aliases = append(config.Aliases, &aliasConfig) +} + +func (config *Config) writeToFile(onDiskFile string) error { + marshaledYaml, err := yaml.Marshal(config) + if err != nil { + return errors.Wrap(err, "failed to marshal alias yaml") + } + if err := ioutil.WriteFile(onDiskFile, marshaledYaml, 0600); err != nil { + return errors.Wrap(err, "failed to write to kubeloginrc file with the alias") + } + log.Printf(string(marshaledYaml)) + return nil +} + +func (config *Config) updateAlias(aliasConfig *AliasConfig, loginServerURL *url.URL, onDiskFile string) error { + aliasConfig.KubectlUser = userFlag + aliasConfig.BaseURL = loginServerURL.String() + if err := config.writeToFile(onDiskFile); err != nil { + log.Fatal(err) + } + log.Print("Alias updated") + return nil +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index bbdd2b1..4d46859 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "io/ioutil" "log" "net" "net/http" @@ -15,32 +14,23 @@ import ( "runtime" "strings" "time" - - "github.com/pkg/errors" - - yaml "gopkg.in/yaml.v2" ) -type app struct { - filenameWithPath string - kubectlUser string - kubeloginAlias string - kubeloginServer string -} - var ( + testTest = false aliasFlag string userFlag string kubeloginServerBaseURL string + version string doneChannel chan bool usageMessage = `Kubelogin Usage: - + One time login: kubelogin login --server-url=https://kubelogin.example.com --kubectl-user=user - + Configure an alias (shortcut): kubelogin config --alias=example --server-url=https://kubelogin.example.com --kubectl-user=example_oidc - + Use an alias: kubelogin login example` ) @@ -52,9 +42,13 @@ type AliasConfig struct { KubectlUser string `yaml:"kubectl-user"` } -// Config contains the array of aliases (AliasConfig) -type Config struct { - Aliases []*AliasConfig `yaml:"aliases"` +// fortest checks at runtime if tests are running, if not we must have version. +func fortest() { + if !testTest { // testTest is set in main_test.go + panic("Kubelogin version is not set.") // Makefile must inject version string + } else { + version = "testing" + } } func findFreePort() (string, error) { @@ -71,61 +65,6 @@ func findFreePort() (string, error) { return portString, nil } -func (app *app) makeExchange(token string) error { - url := fmt.Sprintf("%s/exchange?token=%s", app.kubeloginServer, token) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf("Unable to create request. %s", err) - return err - } - client := http.DefaultClient - res, err := client.Do(req) - if err != nil { - log.Printf("Unable to make request. %s", err) - return err - } - if res.StatusCode != http.StatusOK { - log.Fatalf("Failed to retrieve token from kubelogin server. Please try again or contact your administrator") - } - defer res.Body.Close() // nolint: errcheck - jwt, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Unable to read response body. %s", err) - return err - } - if err := app.configureKubectl(string(jwt)); err != nil { - log.Printf("Error when setting credentials: %v", err) - return err - } - return nil -} - -func (app *app) tokenHandler(w http.ResponseWriter, r *http.Request) { - token := r.FormValue("token") - if err := app.makeExchange(token); err != nil { - log.Fatalf("Could not exchange token for jwt %v", err) - } - fmt.Fprint(w, "You are now logged in! You can close this window") - doneChannel <- true -} - -func (app *app) configureKubectl(jwt string) error { - configCmd := exec.Command("kubectl", "config", "set-credentials", app.kubectlUser, "--token="+jwt) - return configCmd.Run() -} - -func (app *app) generateAuthURL() (string, string, error) { - portNum, err := findFreePort() - if err != nil { - log.Print("err, could not find an open port") - return "", "", err - } - - loginURL := fmt.Sprintf("%s/login?port=%s", app.kubeloginServer, portNum) - - return loginURL, portNum, nil -} - func createMux(app app) *http.ServeMux { newMux := http.NewServeMux() newMux.HandleFunc("/exchange/", app.tokenHandler) @@ -179,164 +118,76 @@ func setFlags(command *flag.FlagSet, loginCmd bool) { command.StringVar(&userFlag, "kubectl-user", "kubelogin_user", "in kubectl config, username used to store credentials") command.StringVar(&kubeloginServerBaseURL, "server-url", "", "base URL of the kubelogin server, ex: https://kubelogin.example.com") } -func (app *app) getConfigSettings(alias string) error { - yamlFile, err := ioutil.ReadFile(app.filenameWithPath) - if err != nil { - return errors.Wrap(err, "failed to read config file for login use") - } - var config Config - if err := yaml.Unmarshal(yamlFile, &config); err != nil { - return errors.Wrap(err, "failed to unmarshal yaml file for login use") - } - - aliasConfig, ok := config.aliasSearch(alias) - if !ok { - return errors.New("Could not find specified alias, check spelling or use the config verb to create an alias") - } - app.kubectlUser = aliasConfig.KubectlUser - app.kubeloginServer = aliasConfig.BaseURL - return nil -} -func (config *Config) aliasSearch(alias string) (*AliasConfig, bool) { - for index, aliases := range config.Aliases { - if alias == aliases.Alias { - return config.Aliases[index], true +func login(app app) { + loginCommmand := flag.NewFlagSet("login", flag.ExitOnError) + setFlags(loginCommmand, true) + if !strings.HasPrefix(os.Args[2], "--") { + //use alias to extract needed information + if err := app.getConfigSettings(os.Args[2]); err != nil { + log.Fatal(err) + } + generateURLAndListenForServerResponse(app) + } else { + _ = loginCommmand.Parse(os.Args[2:]) + if loginCommmand.Parsed() { + if kubeloginServerBaseURL == "" { + log.Fatal("--server-url must be set!") + } + app.kubectlUser = userFlag + app.kubeloginServer = kubeloginServerBaseURL + generateURLAndListenForServerResponse(app) } } - return nil, false } -func (config *Config) createConfig(onDiskFile string, aliasConfig AliasConfig) error { - log.Print("Couldn't find config file in root directory. Creating config file...") - _, e := os.Stat(onDiskFile) // Does config file exist? - if os.IsNotExist(e) { // Create file - fh, err := os.Create(onDiskFile) +func config(app app) { + configCommand := flag.NewFlagSet("config", flag.ExitOnError) + setFlags(configCommand, false) + _ = configCommand.Parse(os.Args[2:]) + if configCommand.Parsed() { + if kubeloginServerBaseURL == "" { + log.Fatal("--server-url must be set!") + } + verifiedServerURL, err := url.ParseRequestURI(kubeloginServerBaseURL) if err != nil { - return errors.Wrap(err, "failed to create file in root directory") + log.Fatalf("Invalid URL given: %v | Err: %v", kubeloginServerBaseURL, err) } - _ = fh.Close() - } - - log.Print("Config file created, setting config values...") - config.Aliases = make([]*AliasConfig, 0) - config.appendAlias(aliasConfig) - if err := config.writeToFile(onDiskFile); err != nil { - log.Fatal(err) - } - log.Print("File configured") - return nil -} - -func (config *Config) newAliasConfig(kubeloginrcAlias, loginServerURL, kubectlUser string) AliasConfig { - newConfig := AliasConfig{ - BaseURL: loginServerURL, - Alias: kubeloginrcAlias, - KubectlUser: kubectlUser, - } - return newConfig -} - -func (config *Config) appendAlias(aliasConfig AliasConfig) { - config.Aliases = append(config.Aliases, &aliasConfig) -} -func (config *Config) writeToFile(onDiskFile string) error { - marshaledYaml, err := yaml.Marshal(config) - if err != nil { - return errors.Wrap(err, "failed to marshal alias yaml") - } - if err := ioutil.WriteFile(onDiskFile, marshaledYaml, 0600); err != nil { - return errors.Wrap(err, "failed to write to kubeloginrc file with the alias") - } - log.Printf(string(marshaledYaml)) - return nil -} - -func (config *Config) updateAlias(aliasConfig *AliasConfig, loginServerURL *url.URL, onDiskFile string) error { - aliasConfig.KubectlUser = userFlag - aliasConfig.BaseURL = loginServerURL.String() - if err := config.writeToFile(onDiskFile); err != nil { - log.Fatal(err) - } - log.Print("Alias updated") - return nil -} - -func (app *app) configureFile(kubeloginrcAlias string, loginServerURL *url.URL, kubectlUser string) error { - var config Config - aliasConfig := config.newAliasConfig(kubeloginrcAlias, loginServerURL.String(), kubectlUser) - yamlFile, err := ioutil.ReadFile(app.filenameWithPath) - if err != nil { - return config.createConfig(app.filenameWithPath, aliasConfig) // Either error or nil value - } - if err := yaml.Unmarshal(yamlFile, &config); err != nil { - return errors.Wrap(err, "failed to unmarshal yaml file") - } - foundAliasConfig, ok := config.aliasSearch(aliasFlag) - if !ok { - newConfig := config.newAliasConfig(kubeloginrcAlias, loginServerURL.String(), kubectlUser) - config.appendAlias(newConfig) - if err := config.writeToFile(app.filenameWithPath); err != nil { + if err := app.configureFile(aliasFlag, verifiedServerURL, userFlag); err != nil { log.Fatal(err) } - log.Print("New Alias configured") - return nil + os.Exit(0) } - - return config.updateAlias(foundAliasConfig, loginServerURL, app.filenameWithPath) // Either error or nil value } func main() { var app app - loginCommmand := flag.NewFlagSet("login", flag.ExitOnError) - setFlags(loginCommmand, true) - configCommand := flag.NewFlagSet("config", flag.ExitOnError) - setFlags(configCommand, false) + + fortest() // Check if test are run + user, err := user.Current() if err != nil { log.Fatalf("Could not determine current user of this system. Err: %v", err) } + + if os.Args[1] == "version" { + fmt.Println(version) + os.Exit(0) + } + app.filenameWithPath = path.Join(user.HomeDir, "/.kubeloginrc.yaml") + if len(os.Args) < 3 { fmt.Println(usageMessage) os.Exit(1) } + switch os.Args[1] { case "login": - if !strings.HasPrefix(os.Args[2], "--") { - //use alias to extract needed information - if err := app.getConfigSettings(os.Args[2]); err != nil { - log.Fatal(err) - } - generateURLAndListenForServerResponse(app) - } else { - _ = loginCommmand.Parse(os.Args[2:]) - if loginCommmand.Parsed() { - if kubeloginServerBaseURL == "" { - log.Fatal("--server-url must be set!") - } - app.kubectlUser = userFlag - app.kubeloginServer = kubeloginServerBaseURL - generateURLAndListenForServerResponse(app) - } - } + login(app) case "config": - _ = configCommand.Parse(os.Args[2:]) - if configCommand.Parsed() { - if kubeloginServerBaseURL == "" { - log.Fatal("--server-url must be set!") - } - verifiedServerURL, err := url.ParseRequestURI(kubeloginServerBaseURL) - if err != nil { - log.Fatalf("Invalid URL given: %v | Err: %v", kubeloginServerBaseURL, err) - } - - if err := app.configureFile(aliasFlag, verifiedServerURL, userFlag); err != nil { - log.Fatal(err) - } - os.Exit(0) - } + config(app) default: fmt.Println(usageMessage) os.Exit(1) diff --git a/cmd/cli/main_test.go b/cmd/cli/main_test.go index a1ff140..b8bb22f 100644 --- a/cmd/cli/main_test.go +++ b/cmd/cli/main_test.go @@ -11,6 +11,17 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func init() { + testTest = true +} + +func TestVersion(t *testing.T) { + fortest() + Convey("Version has to be set.", t, func() { + So(version, ShouldEqual, "testing") + }) +} + func TestFindFreePort(t *testing.T) { Convey("findFreePort", t, func() { Convey("should find a free port and return a port as a string if there is no error", func() { diff --git a/cmd/server/main.go b/cmd/server/main.go index 64bbbbd..52abb13 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -76,8 +76,19 @@ var ( Help: "tracks the duration of each response handler. classified by the request method", }, []string{"method"}) + version string + testTest = false ) +// fortest checks at runtime if tests are running, if not we must have version. +func fortest() { + if !testTest { // testTest is set in main_test.go + panic("Kubelogin version is not set.") // Makefile must inject version string + } else { + version = "testing" + } +} + // the config for oauth2, scopes contain info we want back from the auth server func (authClient *oidcClient) getOAuth2Config(scopes []string) *oauth2.Config { return &oauth2.Config{ diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 51494c3..b8a84a0 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -13,6 +13,17 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func init() { + testTest = true +} + +func TestVersion(t *testing.T) { + fortest() + Convey("Version has to be set.", t, func() { + So(version, ShouldEqual, "testing") + }) +} + func TestServerSpecs(t *testing.T) { Convey("Kubelogin Server", t, func() { redisTTL, _ := time.ParseDuration("10s")