diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader.go index a75ea25e9f93..b7c4896efca4 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader.go @@ -31,32 +31,25 @@ import ( ) const ( - RecommendedConfigPathFlag = "kubeconfig" - RecommendedConfigPathEnvVar = "KUBECONFIG" + RecommendedConfigPathFlag = "kubeconfig" + RecommendedConfigPathEnvVar = "KUBECONFIG" + RecommendedConfigPathInCurrentDir = ".kubeconfig" + RecommendedConfigPathInHomeDir = ".kube/.kubeconfig" ) -// ClientConfigLoadingRules is a struct that calls our specific locations that are used for merging together a Config +// ClientConfigLoadingRules is a collection of file paths that are used for merging together a Config type ClientConfigLoadingRules struct { - CommandLinePath string - EnvVarPath string - CurrentDirectoryPath string - HomeDirectoryPath string + CommandLinePath string + FilePriority []string } -// NewClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to -// use this constructor -func NewClientConfigLoadingRules() *ClientConfigLoadingRules { - return &ClientConfigLoadingRules{ - CurrentDirectoryPath: ".kubeconfig", - HomeDirectoryPath: os.Getenv("HOME") + "/.kube/.kubeconfig", - } +// NewClientConfigLoadingRules returns a ClientConfigLoadingRules object with a list of paths in priority order. +// You are not required to use this constructor +func NewClientConfigLoadingRules(filepriority []string) *ClientConfigLoadingRules { + return &ClientConfigLoadingRules{"", filepriority} } -// Load takes the loading rules and merges together a Config object based on following order. -// 1. CommandLinePath -// 2. EnvVarPath -// 3. CurrentDirectoryPath -// 4. HomeDirectoryPath +// Load takes the loading rules and merges together a Config object based on the provided file priority. // A missing CommandLinePath file produces an error. Empty filenames or other missing files are ignored. // Read errors or files with non-deserializable content produce errors. // The first file to set a particular map key wins and map key's value is never changed. @@ -77,7 +70,7 @@ func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) { } } - kubeConfigFiles := []string{rules.CommandLinePath, rules.EnvVarPath, rules.CurrentDirectoryPath, rules.HomeDirectoryPath} + kubeConfigFiles := append([]string{rules.CommandLinePath}, rules.FilePriority...) // first merge all of our maps mapConfig := clientcmdapi.NewConfig() diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go index 29f6ea29d330..58ab78760f9e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go @@ -312,9 +312,14 @@ func (f *Factory) ClientMapperForCommand(cmd *cobra.Command) resource.ClientMapp // 3. If the command line specifies one and the auth info specifies another, honor the command line technique. // 2. Use default values and potentially prompt for auth information func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { - loadingRules := clientcmd.NewClientConfigLoadingRules() - loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + loadPriority := []string{ + os.Getenv(clientcmd.RecommendedConfigPathEnvVar), + clientcmd.RecommendedConfigPathInCurrentDir, + fmt.Sprintf("%v/%v", os.Getenv("HOME"), clientcmd.RecommendedConfigPathInHomeDir), + } + + loadingRules := clientcmd.NewClientConfigLoadingRules(loadPriority) + flags.StringVar(&loadingRules.CommandLinePath, clientcmd.RecommendedConfigPathFlag, "", "Path to the kubeconfig file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} flagNames := clientcmd.RecommendedConfigOverrideFlags("") diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go index 58016b741a7c..96a379f670d3 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go @@ -111,8 +111,12 @@ func (o viewOptions) validate() error { func (o *viewOptions) getStartingConfig() (*clientcmdapi.Config, string, error) { switch { case o.merge.Value(): - loadingRules := clientcmd.NewClientConfigLoadingRules() - loadingRules.EnvVarPath = os.Getenv("KUBECONFIG") + loadPriority := []string{ + os.Getenv(clientcmd.RecommendedConfigPathEnvVar), + clientcmd.RecommendedConfigPathInCurrentDir, + fmt.Sprintf("%v/%v", os.Getenv("HOME"), clientcmd.RecommendedConfigPathInHomeDir), + } + loadingRules := clientcmd.NewClientConfigLoadingRules(loadPriority) loadingRules.CommandLinePath = o.pathOptions.specifiedFile overrides := &clientcmd.ConfigOverrides{} diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 6b4fcb5e5a50..4c541dc47a9a 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -45,7 +45,8 @@ TEMP_DIR=${USE_TEMP:-$(mktemp -d /tmp/openshift-cmd.XXXX)} ETCD_DATA_DIR="${TEMP_DIR}/etcd" VOLUME_DIR="${TEMP_DIR}/volumes" CERT_DIR="${TEMP_DIR}/certs" -mkdir -p "${ETCD_DATA_DIR}" "${VOLUME_DIR}" "${CERT_DIR}" +CONFIG_DIR="${TEMP_DIR}/configs" +mkdir -p "${ETCD_DATA_DIR}" "${VOLUME_DIR}" "${CERT_DIR}" "${CONFIG_DIR}" # handle profiling defaults profile="${OPENSHIFT_PROFILE-}" @@ -83,14 +84,6 @@ wait_for_url "http://${API_HOST}:${KUBELET_PORT}/healthz" "kubelet: " 0.25 80 wait_for_url "${API_SCHEME}://${API_HOST}:${API_PORT}/healthz" "apiserver: " 0.25 80 wait_for_url "${API_SCHEME}://${API_HOST}:${API_PORT}/api/v1beta1/minions/127.0.0.1" "apiserver(minions): " 0.25 80 -# Set KUBERNETES_MASTER for osc -export KUBERNETES_MASTER="${API_SCHEME}://${API_HOST}:${API_PORT}" -if [[ "${API_SCHEME}" == "https" ]]; then - # Make osc use ${CERT_DIR}/admin/.kubeconfig, and ignore anything in the running user's $HOME dir - export HOME="${CERT_DIR}/admin" - export KUBECONFIG="${CERT_DIR}/admin/.kubeconfig" -fi - # profile the cli commands export OPENSHIFT_PROFILE="${CLI_PROFILE-}" @@ -98,6 +91,49 @@ export OPENSHIFT_PROFILE="${CLI_PROFILE-}" # Begin tests # +# test client not configured +[ "$(osc get services 2>&1 | grep 'no server found')" ] + +# Set KUBERNETES_MASTER for osc from now on +export KUBERNETES_MASTER="${API_SCHEME}://${API_HOST}:${API_PORT}" + +# Set certificates for osc from now on +if [[ "${API_SCHEME}" == "https" ]]; then + # test bad certificate + [ "$(osc get services 2>&1 | grep 'certificate signed by unknown authority')" ] + + # ignore anything in the running user's $HOME dir + export HOME="${CERT_DIR}/admin" +fi + +# test config files from the --config flag +osc get services --config="${CERT_DIR}/admin/.kubeconfig" + +# test config files from env vars +OPENSHIFTCONFIG="${CERT_DIR}/admin/.kubeconfig" osc get services +KUBECONFIG="${CERT_DIR}/admin/.kubeconfig" osc get services + +# test config files in the current directory +TEMP_PWD=`pwd` +pushd ${CONFIG_DIR} >/dev/null + cp ${CERT_DIR}/admin/.kubeconfig .openshiftconfig + ${TEMP_PWD}/${GO_OUT}/osc get services + mv .openshiftconfig .kubeconfig + ${TEMP_PWD}/${GO_OUT}/osc get services +popd + +# test config files in the home directory +mv ${CONFIG_DIR} ${HOME}/.kube +osc get services +mkdir -p ${HOME}/.config +mv ${HOME}/.kube ${HOME}/.config/openshift +mv ${HOME}/.config/openshift/.kubeconfig ${HOME}/.config/openshift/.config +osc get services +echo "config files: ok" +export OPENSHIFTCONFIG="${HOME}/.config/openshift/.config" + +# from this point every command will use config from the OPENSHIFTCONFIG env var + osc get templates osc create -f examples/sample-app/application-template-dockerbuild.json osc get templates @@ -273,14 +309,14 @@ osc describe policybinding master -n recreated-project | grep anypassword:create echo "ex new-project: ok" [ ! "$(openshift ex router | grep 'does not exist')"] -[ "$(openshift ex router -o yaml --credentials="${KUBECONFIG}" | grep 'openshift/origin-haproxy-')" ] -openshift ex router --create --credentials="${KUBECONFIG}" +[ "$(openshift ex router -o yaml --credentials="${OPENSHIFTCONFIG}" | grep 'openshift/origin-haproxy-')" ] +openshift ex router --create --credentials="${OPENSHIFTCONFIG}" [ "$(openshift ex router | grep 'service exists')" ] echo "ex router: ok" [ ! "$(openshift ex registry | grep 'does not exist')"] -[ "$(openshift ex registry -o yaml --credentials="${KUBECONFIG}" | grep 'openshift/origin-docker-registry')" ] -openshift ex registry --create --credentials="${KUBECONFIG}" +[ "$(openshift ex registry -o yaml --credentials="${OPENSHIFTCONFIG}" | grep 'openshift/origin-docker-registry')" ] +openshift ex registry --create --credentials="${OPENSHIFTCONFIG}" [ "$(openshift ex registry | grep 'service exists')" ] echo "ex registry: ok" diff --git a/pkg/cmd/cli/cli.go b/pkg/cmd/cli/cli.go index 7ec6b7de4482..dafc77c89e55 100644 --- a/pkg/cmd/cli/cli.go +++ b/pkg/cmd/cli/cli.go @@ -56,11 +56,13 @@ func NewCommandCLI(name, fullName string) *cobra.Command { } f := clientcmd.New(cmds.PersistentFlags()) + in := os.Stdin out := os.Stdout cmds.SetUsageTemplate(templates.CliUsageTemplate()) cmds.SetHelpTemplate(templates.CliHelpTemplate()) + cmds.AddCommand(cmd.NewCmdLogin(f, in, out)) cmds.AddCommand(cmd.NewCmdNewApplication(fullName, f, out)) cmds.AddCommand(cmd.NewCmdStartBuild(fullName, f, out)) cmds.AddCommand(cmd.NewCmdCancelBuild(fullName, f, out)) @@ -76,6 +78,7 @@ func NewCommandCLI(name, fullName string) *cobra.Command { cmds.AddCommand(cmd.NewCmdLog(fullName, f, out)) cmds.AddCommand(f.NewCmdProxy(out)) cmds.AddCommand(kubecmd.NewCmdNamespace(out)) + cmds.AddCommand(cmd.NewCmdProject(f, out)) cmds.AddCommand(cmd.NewCmdOptions(f, out)) cmds.AddCommand(version.NewVersionCommand(fullName)) diff --git a/pkg/cmd/cli/cmd/login.go b/pkg/cmd/cli/cmd/login.go new file mode 100644 index 000000000000..59168fe35f70 --- /dev/null +++ b/pkg/cmd/cli/cmd/login.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/openshift/origin/pkg/cmd/cli/config" + osclientcmd "github.com/openshift/origin/pkg/cmd/util/clientcmd" +) + +const longDescription = `Logs in to the OpenShift server and save the session +information to a config file that will be used by every subsequent command. + +First-time users of the OpenShift client must run this command to configure the server, +establish a session against it and save it to a configuration file, usually in the +user's home directory. + +The information required to login, like username and password or a session token, and +the server details, can be provided through flags. If not provided, the command will +prompt for user input if needed. +` + +func NewCmdLogin(f *osclientcmd.Factory, reader io.Reader, out io.Writer) *cobra.Command { + options := &LoginOptions{} + + cmds := &cobra.Command{ + Use: "login [--username=] [--password=] [--server=] [--context=] [--certificate-authority=]", + Short: "Logs in and save the configuration", + Long: longDescription, + Run: func(cmd *cobra.Command, args []string) { + options.Reader = reader + options.ClientConfig = f.OpenShiftClientConfig + + checkErr(options.GatherInfo()) + + forcePath := cmdutil.GetFlagString(cmd, config.OpenShiftConfigFlagName) + options.PathToSaveConfig = forcePath + + newFileCreated, err := options.SaveConfig() + checkErr(err) + + if newFileCreated { + fmt.Println("Welcome to OpenShift v3! Use 'osc --help' for a list of commands available.") + } + }, + } + + // TODO flags below should be DE-REGISTERED from the persistent flags and kept only here. + // Login is the only command that can negotiate a session token against the auth server. + cmds.Flags().StringVarP(&options.Username, "username", "u", "", "Username, will prompt if not provided") + cmds.Flags().StringVarP(&options.Password, "password", "p", "", "Password, will prompt if not provided") + return cmds +} diff --git a/pkg/cmd/cli/cmd/loginoptions.go b/pkg/cmd/cli/cmd/loginoptions.go new file mode 100644 index 000000000000..5e98b0fc44fc --- /dev/null +++ b/pkg/cmd/cli/cmd/loginoptions.go @@ -0,0 +1,336 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + kclientcmd "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/cli/config" + "github.com/openshift/origin/pkg/cmd/util" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" + "github.com/openshift/origin/pkg/cmd/util/tokencmd" + "github.com/openshift/origin/pkg/user/api" +) + +const defaultClusterURL = "https://localhost:8443" + +// Helper for the login and setup process, gathers all information required for a +// successful login and eventual update of config files. +// Depending on the Reader present it can be interactive, asking for terminal input in +// case of any missing information. +// Notice that some methods mutate this object so it should not be reused. The Config +// provided as a pointer will also mutate (handle new auth tokens, etc). +type LoginOptions struct { + // flags and printing helpers + Username string + Password string + Project string + + // infra + ClientConfig kclientcmd.ClientConfig + Config *kclient.Config + Reader io.Reader + + // flow controllers + gatherServerInfo bool + gatherAuthInfo bool + gatherProjectInfo bool + + // Optional, if provided will only try to save in it + PathToSaveConfig string +} + +// Gather all required information in a comprehensive order. +func (o *LoginOptions) GatherInfo() error { + if err := o.GatherServerInfo(); err != nil { + return err + } + if err := o.GatherAuthInfo(); err != nil { + return err + } + if err := o.GatherProjectInfo(); err != nil { + return err + } + return nil +} + +// Makes sure it has all the needed information about the server we are connecting to, +// particularly the host address and certificate information. For every information not +// present ask for interactive user input. Will also ping the server to make sure we can +// connect to it, and if any problem is found (e.g. certificate issues), ask the user about +// connecting insecurely. +func (o *LoginOptions) GatherServerInfo() error { + // we need to have a server to talk to + + if util.IsTerminal(o.Reader) { + for !o.serverProvided() { + defaultServer := defaultClusterURL + promptMsg := fmt.Sprintf("Please provide the server URL or just to use '%v': ", defaultServer) + + server := util.PromptForStringWithDefault(o.Reader, defaultServer, promptMsg) + kclientcmd.DefaultCluster = clientcmdapi.Cluster{Server: server} + } + } + + // we know the server we are expected to use + + clientCfg, err := o.ClientConfig.ClientConfig() + if err != nil { + return err + } + + // ping to check if server is reachable + + osClient, err := client.New(clientCfg) + if err != nil { + return err + } + + result := osClient.Get().AbsPath("/osapi").Do() + if result.Error() != nil { + // certificate issue, prompt user for insecure connection + + if clientcmd.IsCertificateAuthorityUnknown(result.Error()) { + fmt.Println("The server uses a certificate signed by unknown authority. You can bypass the certificate check but it will make all connections insecure.") + + clientCfg.Insecure = util.PromptForBool(os.Stdin, "Use insecure connections (strongly discouraged)? [y/N] ") + if !clientCfg.Insecure { + return fmt.Errorf(clientcmd.GetPrettyMessageFor(result.Error())) + } + } + } + + // we have all info we need, now we can have a proper Config + + o.Config = clientCfg + + o.gatherServerInfo = true + return nil +} + +// Negotiate a bearer token with the auth server, or try to reuse one based on the +// information already present. In case of any missing information, ask for user input +// (usually username and password, interactive depending on the Reader). +func (o *LoginOptions) GatherAuthInfo() error { + if err := o.assertGatheredServerInfo(); err != nil { + return err + } + + if me, err := o.Whoami(); err == nil && (!o.usernameProvided() || (o.usernameProvided() && o.Username == usernameFromUser(me))) { + o.Username = usernameFromUser(me) + fmt.Printf("Already logged into '%v' as '%v'.\n", o.Config.Host, o.Username) + + } else { + // if not, we need to log in again + + o.Config.BearerToken = "" + token, err := tokencmd.RequestToken(o.Config, o.Reader, o.Username, o.Password) + if err != nil { + return err + } + o.Config.BearerToken = token + + me, err := o.Whoami() + if err != nil { + return err + } + o.Username = usernameFromUser(me) + fmt.Printf("Logged into '%v' as '%v'.\n", o.Config.Host, o.Username) + } + + // TODO investigate about the safety and intent of the proposal below + // if trying to log in an user that's not the currently logged in, try to reuse an existing token + + // if o.usernameProvided() { + // glog.V(5).Infof("Checking existing authentication info for '%v'...\n", o.Username) + + // for _, ctx := range rawCfg.Contexts { + // authInfo := rawCfg.AuthInfos[ctx.AuthInfo] + // clusterInfo := rawCfg.Clusters[ctx.Cluster] + + // if ctx.AuthInfo == o.Username && clusterInfo.Server == o.Server && len(authInfo.Token) > 0 { // only token for now + // glog.V(5).Infof("Authentication exists for '%v' on '%v', trying to use it...\n", o.Server, o.Username) + + // o.Config.BearerToken = authInfo.Token + + // if me, err := whoami(o.Config); err == nil && usernameFromUser(me) == o.Username { + // o.Username = usernameFromUser(me) + // return nil + // } + + // glog.V(5).Infof("Token %v no longer valid for '%v', can't use it\n", authInfo.Token, o.Username) + // } + // } + // } + + o.gatherAuthInfo = true + return nil +} + +// Discover the projects available for the stabilished session and take one to use. It +// fails in case of no existing projects, and print out useful information in case of +// multiple projects. +func (o *LoginOptions) GatherProjectInfo() error { + if err := o.assertGatheredAuthInfo(); err != nil { + return err + } + + oClient, err := client.New(o.Config) + if err != nil { + return err + } + + projects, err := oClient.Projects().List(labels.Everything(), labels.Everything()) + if err != nil { + return err + } + + projectsItems := projects.Items + + switch len(projectsItems) { + case 0: + me, err := o.Whoami() + if err != nil { + return err + } + // TODO most users will not be allowed to run the suggested commands below, so we should check it and/or + // have a server endpoint that allows an admin to describe to users how to request projects + fmt.Printf(`You don't have any project. +To create a new project, run 'openshift ex new-project --admin=%s'. +To be added as an admin to an existing project, run 'openshift ex policy add-user admin %s -n '. +`, me.Name, me.Name) + + case 1: + o.Project = projectsItems[0].Name + fmt.Printf("Using project '%v'.\n", o.Project) + + default: + projects := []string{} + + for _, project := range projectsItems { + projects = append(projects, project.Name) + } + + namespace, err := o.ClientConfig.Namespace() + if err != nil { + return err + } + + if current, err := oClient.Projects().Get(namespace); err != nil { + if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) { + o.Project = projects[0] + } else { + return err + } + } else { + o.Project = current.Name + } + + if n := len(projects); n > 10 { + projects = projects[:10] + fmt.Printf("You have %d projects, displaying only the first 10. To view all your projects run 'osc get projects'.\n", n) + } + var sortedProjects sort.StringSlice = projects + sortedProjects.Sort() + fmt.Printf("Your projects are: %v. You can switch between them at any time using 'osc project '.\n", strings.Join(projects, ", ")) + fmt.Printf("Using project '%v'.\n", o.Project) + } + + o.gatherProjectInfo = true + return nil +} + +// Save all the information present in this helper to a config file. An explicit config +// file path can be provided, if not use the established conventions about config +// loading rules. Will create a new config file if one can't be found at all. Will only +// succeed if all required info is present. +func (o *LoginOptions) SaveConfig() (created bool, err error) { + if len(o.Username) == 0 { + return false, fmt.Errorf("Insufficient data to merge configuration.") + } + + var configStore *config.ConfigStore + + if len(o.PathToSaveConfig) > 0 { + configStore, err = config.LoadFrom(o.PathToSaveConfig) + if err != nil { + return created, err + } + } else { + configStore, err = config.LoadWithLoadingRules() + if err != nil { + configStore, err = config.CreateEmpty() + if err != nil { + return created, err + } + created = true + } + } + + rawCfg, err := o.ClientConfig.RawConfig() + if err != nil { + return created, err + } + return created, configStore.SaveToFile(o.Username, o.Project, o.Config, rawCfg) +} + +func (o *LoginOptions) Whoami() (*api.User, error) { + oClient, err := client.New(o.Config) + if err != nil { + return nil, err + } + + me, err := oClient.Users().Get("~") + if err != nil { + return nil, err + } + + return me, nil +} + +func (o *LoginOptions) assertGatheredServerInfo() error { + if !o.gatherServerInfo { + return fmt.Errorf("Must gather server info first.") + } + return nil +} + +func (o *LoginOptions) assertGatheredAuthInfo() error { + if !o.gatherAuthInfo { + return fmt.Errorf("Must gather auth info first.") + } + return nil +} + +func (o *LoginOptions) assertGatheredProjectInfo() error { + if !o.gatherProjectInfo { + return fmt.Errorf("Must gather project info first.") + } + return nil +} + +func (o *LoginOptions) usernameProvided() bool { + return len(o.Username) > 0 +} + +func (o *LoginOptions) passwordProvided() bool { + return len(o.Password) > 0 +} + +func (o *LoginOptions) serverProvided() bool { + _, err := o.ClientConfig.ClientConfig() + return err == nil || !clientcmd.IsNoServerFound(err) +} + +func usernameFromUser(user *api.User) string { + return strings.Split(user.Name, ":")[1] +} diff --git a/pkg/cmd/cli/cmd/project.go b/pkg/cmd/cli/cmd/project.go new file mode 100644 index 000000000000..5efa0c572ee3 --- /dev/null +++ b/pkg/cmd/cli/cmd/project.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + kclientcmd "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" + "github.com/openshift/origin/pkg/cmd/cli/config" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" + "github.com/spf13/cobra" +) + +func NewCmdProject(f *clientcmd.Factory, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "project ", + Short: "switch to another project", + Long: `Switch to another project and make it the default in your configuration.`, + Run: func(cmd *cobra.Command, args []string) { + argsLength := len(args) + + if argsLength > 1 { + glog.Fatal("Only one argument is supported (project name).") + } + + rawCfg, err := f.OpenShiftClientConfig.RawConfig() + checkErr(err) + + clientCfg, err := f.OpenShiftClientConfig.ClientConfig() + checkErr(err) + + oClient, _, err := f.Clients(cmd) + checkErr(err) + + if argsLength == 0 { + currentContext := rawCfg.Contexts[rawCfg.CurrentContext] + currentProject := currentContext.Namespace + + _, err := oClient.Projects().Get(currentProject) + if err != nil { + if errors.IsNotFound(err) { + glog.Fatalf("The project '%v' specified in your config does not exist or you do not have rights to view it.", currentProject) + } + checkErr(err) + } + + fmt.Printf("Using project '%v'.\n", currentProject) + return + + } + + projectName := args[0] + + project, err := oClient.Projects().Get(projectName) + if err != nil { + if errors.IsNotFound(err) { + glog.Fatalf("Unable to find a project with name '%v'.", projectName) + } + checkErr(err) + } + + pathFromFlag := cmdutil.GetFlagString(cmd, config.OpenShiftConfigFlagName) + + configStore, err := config.LoadFrom(pathFromFlag) + if err != nil { + configStore, err = config.LoadWithLoadingRules() + checkErr(err) + } + checkErr(err) + + config := configStore.Config + + // check if context exists in the file I'm going to save + // if so just set it as the current one + exists := false + for k, ctx := range config.Contexts { + namespace := ctx.Namespace + cluster := config.Clusters[ctx.Cluster] + authInfo := config.AuthInfos[ctx.AuthInfo] + + if namespace == project.Name && cluster.Server == clientCfg.Host && authInfo.Token == clientCfg.BearerToken { + exists = true + config.CurrentContext = k + break + } + } + + // otherwise use the current context if it's in the file I'm going to save, + // or create a new one if it's not + if !exists { + currentCtx := rawCfg.CurrentContext + if ctx, ok := config.Contexts[currentCtx]; ok { + ctx.Namespace = project.Name + config.Contexts[currentCtx] = ctx + } else { + ctx = rawCfg.Contexts[currentCtx] + newCtx := clientcmdapi.NewContext() + newCtx.Namespace = project.Name + newCtx.AuthInfo = ctx.AuthInfo + newCtx.Cluster = ctx.Cluster + config.Contexts[fmt.Sprint(util.NewUUID())] = *newCtx + } + } + + if err = kclientcmd.WriteToFile(*config, configStore.Path); err != nil { + glog.Fatalf("Error saving project information in the config: %v.", err) + } + + fmt.Printf("Now using project '%v'.\n", project.Name) + }, + } + return cmd +} diff --git a/pkg/cmd/cli/config/loader.go b/pkg/cmd/cli/config/loader.go new file mode 100644 index 000000000000..a186cc9d175b --- /dev/null +++ b/pkg/cmd/cli/config/loader.go @@ -0,0 +1,54 @@ +package config + +import ( + "os" + "path" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" +) + +const ( + OpenShiftConfigPathEnvVar = "OPENSHIFTCONFIG" + OpenShiftConfigFlagName = "config" + OpenShiftConfigFileName = ".openshiftconfig" + OpenShiftConfigHomeDir = ".config/openshift" + OpenShiftConfigHomeFileName = ".config" + OpenShiftConfigHomeDirFileName = OpenShiftConfigHomeDir + "/" + OpenShiftConfigHomeFileName + + KubeConfigPathEnvVar = clientcmd.RecommendedConfigPathEnvVar + KubeConfigFileName = ".kubeconfig" + KubeConfigHomeDir = ".kube" +) + +// Set up the rules and priorities for loading config files. +func NewOpenShiftClientConfigLoadingRules() *clientcmd.ClientConfigLoadingRules { + return clientcmd.NewClientConfigLoadingRules(FullClientConfigFilePriority()) +} + +// File priority loading rules for OpenShift. +// 1. OPENSHIFTCONFIG env var +// 2. .openshiftconfig file in current directory +// 3. ~/.config/openshift/.config file +func OpenShiftClientConfigFilePriority() []string { + return []string{ + os.Getenv(OpenShiftConfigPathEnvVar), + OpenShiftConfigFileName, + path.Join(os.Getenv("HOME"), OpenShiftConfigHomeDirFileName), + } +} + +// File priority loading rules for Kube. +// 1. KUBECONFIG env var +// 2. .kubeconfig file in current directory +// 3. ~/.kube/.kubeconfig file +func KubeClientConfigFilePriority() []string { + return []string{ + os.Getenv(KubeConfigPathEnvVar), + KubeConfigFileName, + path.Join(os.Getenv("HOME"), KubeConfigHomeDir, KubeConfigFileName), + } +} + +func FullClientConfigFilePriority() []string { + return append(OpenShiftClientConfigFilePriority(), KubeClientConfigFilePriority()...) +} diff --git a/pkg/cmd/experimental/login/smart_merge.go b/pkg/cmd/cli/config/smart_merge.go similarity index 93% rename from pkg/cmd/experimental/login/smart_merge.go rename to pkg/cmd/cli/config/smart_merge.go index e6b0e2449184..400dd4592c0f 100644 --- a/pkg/cmd/experimental/login/smart_merge.go +++ b/pkg/cmd/cli/config/smart_merge.go @@ -1,4 +1,4 @@ -package login +package config import ( "fmt" @@ -140,3 +140,18 @@ func getMapKeys(theMap reflect.Value) (*util.StringSet, error) { return ret, nil } + +func getUniqueName(basename string, existingNames *util.StringSet) string { + if !existingNames.Has(basename) { + return basename + } + + for i := 0; i < 100; i++ { + trialName := fmt.Sprintf("%v-%d", basename, i) + if !existingNames.Has(trialName) { + return trialName + } + } + + return string(util.NewUUID()) +} diff --git a/pkg/cmd/cli/config/store.go b/pkg/cmd/cli/config/store.go new file mode 100644 index 000000000000..89a25d660682 --- /dev/null +++ b/pkg/cmd/cli/config/store.go @@ -0,0 +1,159 @@ +package config + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/golang/glog" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + + "github.com/openshift/origin/pkg/cmd/flagtypes" +) + +const ( + fromKube = "fromkube" + fromOpenShift = "fromopenshift" +) + +// A ConfigStore is the representation of a config from one individual config file. Can be used +// to persist configs by being explicit about the file to save. +type ConfigStore struct { + Config *clientcmdapi.Config + Path string + providerEngine string +} + +func (c *ConfigStore) FromOpenShift() bool { + return c.providerEngine == fromOpenShift +} + +func (c *ConfigStore) FromKube() bool { + return c.providerEngine == fromKube +} + +// Load a ConfigStore from the explicit path to a config file provided as argument +// Error if not found. +func LoadFrom(path string) (*ConfigStore, error) { + data, err := ioutil.ReadFile(path) + if err == nil { + config, err := clientcmd.Load(data) + if err != nil { + return nil, err + } + return &ConfigStore{config, path, fromOpenShift}, nil + } + + return nil, fmt.Errorf("Unable to load config file from '%v': %v", path, err.Error()) +} + +// Load a ConfigStore using the priority conventions declared by the ClientConfigLoadingRules. +// Error if none can be found. +func LoadWithLoadingRules() (store *ConfigStore, err error) { + loadingRules := map[string][]string{ + fromOpenShift: OpenShiftClientConfigFilePriority(), + fromKube: KubeClientConfigFilePriority(), + } + + for source, priorities := range loadingRules { + for _, path := range priorities { + data, err := ioutil.ReadFile(path) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("Unable to load config file from %v: %v", path, err.Error()) + } + if err == nil { + config, err := clientcmd.Load(data) + if err != nil { + return store, err + } + return &ConfigStore{config, path, source}, nil + } + } + } + + return nil, fmt.Errorf("Unable to load a config file from any of the expected locations.") +} + +// Create a new file to store configs and returns the ConfigStore that represents it. +func CreateEmpty() (*ConfigStore, error) { + configPathToCreateIfNotFound := fmt.Sprintf("%v/%v", os.Getenv("HOME"), OpenShiftConfigHomeDirFileName) + + glog.V(3).Infof("A new config will be created at: %v ", configPathToCreateIfNotFound) + + newConfig := clientcmdapi.NewConfig() + + if err := os.MkdirAll(fmt.Sprintf("%v/%v", os.Getenv("HOME"), OpenShiftConfigHomeDir), 0755); err != nil { + return nil, fmt.Errorf("Tried to create a new config file but failed while creating directory %v: %v", OpenShiftConfigHomeDirFileName, err) + } + glog.V(5).Infof("Created directory %v", "~/"+OpenShiftConfigHomeDir) + + if err := clientcmd.WriteToFile(*newConfig, configPathToCreateIfNotFound); err != nil { + return nil, fmt.Errorf("Tried to create a new config file but failed with: %v", err) + } + glog.V(5).Infof("Created file %v", configPathToCreateIfNotFound) + + data, err := ioutil.ReadFile(configPathToCreateIfNotFound) + if err != nil { + return nil, err + } + + config, err := clientcmd.Load(data) + if err != nil { + return nil, err + } + + return &ConfigStore{config, configPathToCreateIfNotFound, fromOpenShift}, nil +} + +// Save the provided config attributes to this ConfigStore. +func (c *ConfigStore) SaveToFile(credentialsName string, namespace string, clientCfg *client.Config, rawCfg clientcmdapi.Config) error { + glog.V(4).Infof("Trying to merge and update %v config to '%v'...", c.providerEngine, c.Path) + + config := clientcmdapi.NewConfig() + + credentials := clientcmdapi.NewAuthInfo() + credentials.Token = clientCfg.BearerToken + credentials.ClientCertificate = clientCfg.TLSClientConfig.CertFile + credentials.ClientCertificateData = clientCfg.TLSClientConfig.CertData + credentials.ClientKey = clientCfg.TLSClientConfig.KeyFile + credentials.ClientKeyData = clientCfg.TLSClientConfig.KeyData + if len(credentialsName) == 0 { + credentialsName = "osc-login" + } + config.AuthInfos[credentialsName] = *credentials + + serverAddr := flagtypes.Addr{Value: clientCfg.Host}.Default() + clusterName := fmt.Sprintf("%v:%v", serverAddr.Host, serverAddr.Port) + cluster := clientcmdapi.NewCluster() + cluster.Server = clientCfg.Host + cluster.CertificateAuthority = clientCfg.CAFile + cluster.CertificateAuthorityData = clientCfg.CAData + cluster.InsecureSkipTLSVerify = clientCfg.Insecure + config.Clusters[clusterName] = *cluster + + contextName := clusterName + "-" + credentialsName + context := clientcmdapi.NewContext() + context.Cluster = clusterName + context.AuthInfo = credentialsName + context.Namespace = namespace + config.Contexts[contextName] = *context + config.CurrentContext = contextName + + configToModify := c.Config + + configToWrite, err := MergeConfig(rawCfg, *configToModify, *config) + if err != nil { + return err + } + + // TODO need to handle file not writable (probably create a copy) + err = clientcmd.WriteToFile(*configToWrite, c.Path) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cmd/experimental/login/login.go b/pkg/cmd/experimental/login/login.go deleted file mode 100644 index 785e1212d423..000000000000 --- a/pkg/cmd/experimental/login/login.go +++ /dev/null @@ -1,178 +0,0 @@ -package login - -import ( - "fmt" - "os" - - "github.com/golang/glog" - "github.com/spf13/cobra" - - kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" - clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" - kcmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - - "github.com/openshift/origin/pkg/client" - "github.com/openshift/origin/pkg/cmd/flagtypes" - osclientcmd "github.com/openshift/origin/pkg/cmd/util/clientcmd" - "github.com/openshift/origin/pkg/cmd/util/tokencmd" -) - -func NewCmdLogin(f *osclientcmd.Factory, parentName, name string) *cobra.Command { - cmds := &cobra.Command{ - Use: name, - Short: "Logs in and returns a session token", - Long: `Logs in to the OpenShift server and prints out a session token. - -Username and password can be provided through flags, the command will -prompt for user input if not provided. -`, - Run: func(cmd *cobra.Command, args []string) { - clientCfg, err := f.OpenShiftClientConfig.ClientConfig() - if err != nil { - glog.Fatalf("%v\n", err) - } - - username := "" - - // check to see if we're already signed in. If so, simply make sure that .kubeconfig has that information - if userFullName, err := whoami(clientCfg); err == nil { - if err := updateKubeconfigFile(userFullName, clientCfg.BearerToken, f.OpenShiftClientConfig); err != nil { - glog.Fatalf("%v\n", err) - } - username = userFullName - - } else { - usernameFlag := kcmdutil.GetFlagString(cmd, "username") - passwordFlag := kcmdutil.GetFlagString(cmd, "password") - - accessToken, err := tokencmd.RequestToken(clientCfg, os.Stdin, usernameFlag, passwordFlag) - if err != nil { - glog.Fatalf("%v\n", err) - } - - clientCfg.BearerToken = accessToken - - if userFullName, err := whoami(clientCfg); err == nil { - err = updateKubeconfigFile(userFullName, accessToken, f.OpenShiftClientConfig) - if err != nil { - glog.Fatalf("%v\n", err) - } else { - username = userFullName - } - } else { - glog.Fatalf("%v\n", err) - } - } - - fmt.Printf("Logged into %v as %v\n", clientCfg.Host, username) - }, - } - - cmds.Flags().StringP("username", "u", "", "Username, will prompt if not provided") - cmds.Flags().StringP("password", "p", "", "Password, will prompt if not provided") - return cmds -} - -func whoami(clientCfg *kclient.Config) (string, error) { - osClient, err := client.New(clientCfg) - if err != nil { - return "", err - } - - me, err := osClient.Users().Get("~") - if err != nil { - return "", err - } - - return me.FullName, nil -} - -func updateKubeconfigFile(username, token string, clientCfg clientcmd.ClientConfig) error { - rawMergedConfig, err := clientCfg.RawConfig() - if err != nil { - return err - } - clientConfig, err := clientCfg.ClientConfig() - if err != nil { - return err - } - namespace, err := clientCfg.Namespace() - if err != nil { - return err - } - - config := clientcmdapi.NewConfig() - - credentialsName := username - if len(credentialsName) == 0 { - credentialsName = "osc-login" - } - credentials := clientcmdapi.NewAuthInfo() - credentials.Token = token - config.AuthInfos[credentialsName] = *credentials - - serverAddr := flagtypes.Addr{Value: clientConfig.Host}.Default() - clusterName := fmt.Sprintf("%v:%v", serverAddr.Host, serverAddr.Port) - cluster := clientcmdapi.NewCluster() - cluster.Server = clientConfig.Host - cluster.InsecureSkipTLSVerify = clientConfig.Insecure - cluster.CertificateAuthority = clientConfig.CAFile - config.Clusters[clusterName] = *cluster - - contextName := clusterName + "-" + credentialsName - context := clientcmdapi.NewContext() - context.Cluster = clusterName - context.AuthInfo = credentialsName - context.Namespace = namespace - config.Contexts[contextName] = *context - - config.CurrentContext = contextName - - configToModify, err := getConfigFromFile(".kubeconfig") - if err != nil { - return err - } - - configToWrite, err := MergeConfig(rawMergedConfig, *configToModify, *config) - if err != nil { - return err - } - err = clientcmd.WriteToFile(*configToWrite, ".kubeconfig") - if err != nil { - return err - } - - return nil - -} - -func getConfigFromFile(filename string) (*clientcmdapi.Config, error) { - var err error - config, err := clientcmd.LoadFromFile(filename) - if err != nil && !os.IsNotExist(err) { - return nil, err - } - - if config == nil { - config = clientcmdapi.NewConfig() - } - - return config, nil -} - -func getUniqueName(basename string, existingNames *util.StringSet) string { - if !existingNames.Has(basename) { - return basename - } - - for i := 0; i < 100; i++ { - trialName := fmt.Sprintf("%v-%d", basename, i) - if !existingNames.Has(trialName) { - return trialName - } - } - - return string(util.NewUUID()) -} diff --git a/pkg/cmd/experimental/registry/registry.go b/pkg/cmd/experimental/registry/registry.go index 338cad01b204..2c5d767f2745 100644 --- a/pkg/cmd/experimental/registry/registry.go +++ b/pkg/cmd/experimental/registry/registry.go @@ -157,7 +157,8 @@ func NewCmdRegistry(f *clientcmd.Factory, parentName, name string, out io.Writer if len(cfg.Credentials) == 0 { glog.Fatalf("You must specify a .kubeconfig file path containing credentials for connecting the registry to the master with --credentials") } - credentials, err := (&kclientcmd.ClientConfigLoadingRules{CommandLinePath: cfg.Credentials}).Load() + clientConfigLoadingRules := &kclientcmd.ClientConfigLoadingRules{cfg.Credentials, []string{}} + credentials, err := clientConfigLoadingRules.Load() if err != nil { glog.Fatalf("The provided credentials %q could not be loaded: %v", cfg.Credentials, err) } diff --git a/pkg/cmd/experimental/router/router.go b/pkg/cmd/experimental/router/router.go index 3700af6220c6..16b746784c5c 100644 --- a/pkg/cmd/experimental/router/router.go +++ b/pkg/cmd/experimental/router/router.go @@ -21,7 +21,6 @@ import ( configcmd "github.com/openshift/origin/pkg/config/cmd" dapi "github.com/openshift/origin/pkg/deploy/api" "github.com/openshift/origin/pkg/generate/app" - //imageapi "github.com/openshift/origin/pkg/image/api" ) const longDesc = ` @@ -148,7 +147,9 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out io.Writer) if len(cfg.Credentials) == 0 { glog.Fatalf("You must specify a .kubeconfig file path containing credentials for connecting the router to the master with --credentials") } - credentials, err := (&kclientcmd.ClientConfigLoadingRules{CommandLinePath: cfg.Credentials}).Load() + + clientConfigLoadingRules := &kclientcmd.ClientConfigLoadingRules{cfg.Credentials, []string{}} + credentials, err := clientConfigLoadingRules.Load() if err != nil { glog.Fatalf("The provided credentials %q could not be loaded: %v", cfg.Credentials, err) } diff --git a/pkg/cmd/openshift/openshift.go b/pkg/cmd/openshift/openshift.go index d1456ece6099..bee62d15306b 100644 --- a/pkg/cmd/openshift/openshift.go +++ b/pkg/cmd/openshift/openshift.go @@ -9,7 +9,6 @@ import ( "github.com/openshift/origin/pkg/cmd/cli" "github.com/openshift/origin/pkg/cmd/experimental/config" "github.com/openshift/origin/pkg/cmd/experimental/generate" - "github.com/openshift/origin/pkg/cmd/experimental/login" "github.com/openshift/origin/pkg/cmd/experimental/policy" "github.com/openshift/origin/pkg/cmd/experimental/project" exregistry "github.com/openshift/origin/pkg/cmd/experimental/registry" @@ -122,7 +121,6 @@ func newExperimentalCommand(parentName, name string) *cobra.Command { experimental.AddCommand(tokens.NewCmdTokens(f, subName, "tokens")) experimental.AddCommand(policy.NewCommandPolicy(f, subName, "policy")) experimental.AddCommand(generate.NewCmdGenerate(f, subName, "generate")) - experimental.AddCommand(login.NewCmdLogin(f, subName, "login")) experimental.AddCommand(exrouter.NewCmdRouter(f, subName, "router", os.Stdout)) experimental.AddCommand(exregistry.NewCmdRegistry(f, subName, "registry", os.Stdout)) return experimental diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 505a0ac03526..d8735acf2e02 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -14,7 +14,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/coreos/go-systemd/daemon" "github.com/golang/glog" - "github.com/openshift/origin/pkg/cmd/server/etcd" "github.com/openshift/origin/pkg/cmd/server/kubernetes" "github.com/openshift/origin/pkg/cmd/server/origin" diff --git a/pkg/cmd/util/clientcmd/client.go b/pkg/cmd/util/clientcmd/client.go new file mode 100644 index 000000000000..b49ca3089f6e --- /dev/null +++ b/pkg/cmd/util/clientcmd/client.go @@ -0,0 +1,29 @@ +package clientcmd + +import ( + "net/http" + + "github.com/golang/glog" +) + +const ( + unauthorizedErrorMessage = `Your session has expired. Use the following command to log in again: + osc login` +) + +type statusHandlerClient struct { + delegate *http.Client +} + +func (client *statusHandlerClient) Do(req *http.Request) (*http.Response, error) { + resp, err := client.delegate.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusUnauthorized { + glog.Fatal(unauthorizedErrorMessage) + } + + return resp, err +} diff --git a/pkg/cmd/util/clientcmd/errors.go b/pkg/cmd/util/clientcmd/errors.go new file mode 100644 index 000000000000..8dc0ed0bd516 --- /dev/null +++ b/pkg/cmd/util/clientcmd/errors.go @@ -0,0 +1,54 @@ +package clientcmd + +import "strings" + +const ( + unknownReason = 0 + noServerFoundReason = 1 + certificateAuthorityUnknownReason = 2 + + certificateAuthorityUnknownMsg = "The server uses a certificate signed by unknown authority. You may need to use the --certificate-authority flag to provide the path to a certificate file for the certificate authority, or --insecure-skip-tls-verify to bypass the certificate check and use insecure connections." + notConfiguredMsg = `OpenShift is not configured. You need to run the login command in order to create a default config for your server and credentials: + osc login +You can also run this command again providing the path to a config file directly, either through the --config flag of the OPENSHIFTCONFIG environment variable. +` +) + +func GetPrettyMessageFor(err error) string { + if err == nil { + return "" + } + + reason := detectReason(err) + + switch reason { + case noServerFoundReason: + return notConfiguredMsg + + case certificateAuthorityUnknownReason: + return certificateAuthorityUnknownMsg + } + + return err.Error() +} + +func IsNoServerFound(err error) bool { + return detectReason(err) == noServerFoundReason +} + +func IsCertificateAuthorityUnknown(err error) bool { + return detectReason(err) == certificateAuthorityUnknownReason +} + +func detectReason(err error) int { + if err != nil { + switch { + case strings.Contains(err.Error(), "certificate signed by unknown authority"): + return certificateAuthorityUnknownReason + + case strings.Contains(err.Error(), "no server found for"): + return noServerFoundReason + } + } + return unknownReason +} diff --git a/pkg/cmd/util/clientcmd/factory.go b/pkg/cmd/util/clientcmd/factory.go index 6caafcbdce69..3114c4096f10 100644 --- a/pkg/cmd/util/clientcmd/factory.go +++ b/pkg/cmd/util/clientcmd/factory.go @@ -2,7 +2,7 @@ package clientcmd import ( "fmt" - "os" + "net/http" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" @@ -17,34 +17,39 @@ import ( "github.com/openshift/origin/pkg/api/latest" "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/cli/config" "github.com/openshift/origin/pkg/cmd/cli/describe" ) -const defaultClusterURL = "https://localhost:8443" - // NewFactory creates a default Factory for commands that should share identical server // connection behavior. Most commands should use this method to get a factory. func New(flags *pflag.FlagSet) *Factory { - // Override global default to https and port 8443 - clientcmd.DefaultCluster.Server = defaultClusterURL + // Override global default to "" so we force the client to ask for user input + // TODO refactor this usptream: + // DefaultCluster should not be a global + // A call to ClientConfig() should always return the best clientCfg possible + // even if an error was returned, and let the caller decide what to do + clientcmd.DefaultCluster.Server = "" // TODO: there should be two client configs, one for OpenShift, and one for Kubernetes clientConfig := DefaultClientConfig(flags) f := NewFactory(clientConfig) f.BindFlags(flags) + return f } // Copy of kubectl/cmd/DefaultClientConfig, using NewNonInteractiveDeferredLoadingClientConfig func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { - loadingRules := clientcmd.NewClientConfigLoadingRules() - loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + loadingRules := config.NewOpenShiftClientConfigLoadingRules() + + flags.StringVar(&loadingRules.CommandLinePath, config.OpenShiftConfigFlagName, "", "Path to the config file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} overrideFlags := clientcmd.RecommendedConfigOverrideFlags("") overrideFlags.ContextOverrideFlags.NamespaceShort = "n" clientcmd.BindOverrideFlags(overrides, flags, overrideFlags) + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) return clientConfig @@ -66,70 +71,79 @@ func NewFactory(clientConfig clientcmd.ClientConfig) *Factory { return mapper, api.Scheme } - // Save original RESTClient function - kRESTClientFunc := w.Factory.RESTClient w.RESTClient = func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.RESTClient, error) { + oClient, kClient, err := w.Clients(cmd) + if err != nil { + return nil, fmt.Errorf("unable to create client %s: %v", mapping.Kind, err) + } + if latest.OriginKind(mapping.Kind, mapping.APIVersion) { - cfg, err := w.OpenShiftClientConfig.ClientConfig() - if err != nil { - return nil, fmt.Errorf("unable to find client config %s: %v", mapping.Kind, err) - } - cli, err := client.New(cfg) - if err != nil { - return nil, fmt.Errorf("unable to create client %s: %v", mapping.Kind, err) - } - return cli.RESTClient, nil + return oClient.RESTClient, nil + } else { + return kClient.RESTClient, nil } - return kRESTClientFunc(cmd, mapping) } - // Save original Describer function - kDescriberFunc := w.Factory.Describer w.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + oClient, kClient, err := w.Clients(cmd) + if err != nil { + return nil, fmt.Errorf("unable to create client %s: %v", mapping.Kind, err) + } + + cfg, err := w.OpenShiftClientConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("unable to describe %s: %v", mapping.Kind, err) + } + if latest.OriginKind(mapping.Kind, mapping.APIVersion) { - cfg, err := w.OpenShiftClientConfig.ClientConfig() - if err != nil { - return nil, fmt.Errorf("unable to describe %s: %v", mapping.Kind, err) - } - cli, err := client.New(cfg) - if err != nil { - return nil, fmt.Errorf("unable to describe %s: %v", mapping.Kind, err) - } - kubeClient, err := kclient.New(cfg) - if err != nil { - return nil, fmt.Errorf("unable to describe %s: %v", mapping.Kind, err) - } - describer, ok := describe.DescriberFor(mapping.Kind, cli, kubeClient, cfg.Host) + describer, ok := describe.DescriberFor(mapping.Kind, oClient, kClient, cfg.Host) if !ok { return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) } return describer, nil } - return kDescriberFunc(cmd, mapping) + return w.Factory.Describer(cmd, mapping) } w.Printer = func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return describe.NewHumanReadablePrinter(noHeaders), nil } + w.DefaultNamespace = func(cmd *cobra.Command) (string, error) { + return w.OpenShiftClientConfig.Namespace() + } + return w } // Clients returns an OpenShift and Kubernetes client. func (f *Factory) Clients(cmd *cobra.Command) (*client.Client, *kclient.Client, error) { - os, err := f.OpenShiftClientConfig.ClientConfig() + cfg, err := f.OpenShiftClientConfig.ClientConfig() + if err != nil { + return nil, nil, err + } + + transport, err := kclient.TransportFor(cfg) if err != nil { return nil, nil, err } - oc, err := client.New(os) + httpClient := &http.Client{ + Transport: transport, + } + + oClient, err := client.New(cfg) if err != nil { return nil, nil, err } - kc, err := f.Client(cmd) + kClient, err := kclient.New(cfg) if err != nil { return nil, nil, err } - return oc, kc, nil + + oClient.Client = &statusHandlerClient{httpClient} + kClient.Client = &statusHandlerClient{httpClient} + + return oClient, kClient, nil } // ShortcutExpander is a RESTMapper that can be used for OpenShift resources. diff --git a/pkg/cmd/util/clientconfig.go b/pkg/cmd/util/clientconfig.go index b741536a74be..5519c8a3a756 100644 --- a/pkg/cmd/util/clientconfig.go +++ b/pkg/cmd/util/clientconfig.go @@ -1,18 +1,16 @@ package util import ( - "os" - "github.com/spf13/pflag" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + "github.com/openshift/origin/pkg/cmd/cli/config" ) -// Copy of kubectl/cmd/DefaultClientConfig, using NewNonInteractiveDeferredLoadingClientConfig func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { - loadingRules := clientcmd.NewClientConfigLoadingRules() - loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + loadingRules := config.NewOpenShiftClientConfigLoadingRules() + + flags.StringVar(&loadingRules.CommandLinePath, config.OpenShiftConfigFlagName, "", "Path to the config file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} overrideFlags := clientcmd.RecommendedConfigOverrideFlags("") diff --git a/pkg/cmd/util/terminal.go b/pkg/cmd/util/terminal.go index 10dfe6562009..06e2baaef021 100644 --- a/pkg/cmd/util/terminal.go +++ b/pkg/cmd/util/terminal.go @@ -1,20 +1,23 @@ package util import ( + "bufio" "fmt" "io" "os" + "strings" "github.com/docker/docker/pkg/term" "github.com/golang/glog" ) +// Takes an io.Reader and prompt for user input if it's a terminal, returning the result. func PromptForString(r io.Reader, format string, a ...interface{}) string { fmt.Printf(format, a...) return readInput(r) } -// TODO not tested on other platforms +// Prompt for user input by disabling echo in terminal, useful for password prompt. func PromptForPasswordString(r io.Reader, format string, a ...interface{}) string { if file, ok := r.(*os.File); ok { inFd := file.Fd() @@ -37,18 +40,50 @@ func PromptForPasswordString(r io.Reader, format string, a ...interface{}) strin fmt.Printf("\n") return input - } else { - glog.V(3).Infof("Stdin is not a terminal") - return PromptForString(r, format, a...) } - } else { - glog.V(3).Infof("Unable to use a TTY") + glog.V(3).Infof("Stdin is not a terminal") return PromptForString(r, format, a...) } + return PromptForString(r, format, a...) +} + +// Prompt for user input of a boolean value. The accepted values are: +// yes, y, true, t, 1 (not case sensitive) +// no, n, false, f, 0 (not case sensitive) +// A valid answer is mandatory so it will keep asking until an answer is provided. +func PromptForBool(r io.Reader, format string, a ...interface{}) bool { + str := PromptForString(r, format, a...) + switch strings.ToLower(str) { + case "1", "t", "true", "y", "yes": + return true + case "0", "f", "false", "n", "no": + return false + } + fmt.Println("Please enter 'yes' or 'no'.") + return PromptForBool(r, format, a...) +} + +// Prompt for user input but take a default in case nothing is provided. +func PromptForStringWithDefault(r io.Reader, def string, format string, a ...interface{}) string { + s := PromptForString(r, format, a...) + if len(s) == 0 { + return def + } + return s } func readInput(r io.Reader) string { + if IsTerminal(r) { + reader := bufio.NewReader(r) + result, _ := reader.ReadString('\n') + return strings.TrimSuffix(result, "\n") + } var result string fmt.Fscan(r, &result) return result } + +func IsTerminal(r io.Reader) bool { + file, ok := r.(*os.File) + return ok && term.IsTerminal(file.Fd()) +} diff --git a/pkg/cmd/util/tokencmd/challenging_client.go b/pkg/cmd/util/tokencmd/challenging_client.go index 9c8b449d7207..e8c3bc164b93 100644 --- a/pkg/cmd/util/tokencmd/challenging_client.go +++ b/pkg/cmd/util/tokencmd/challenging_client.go @@ -40,7 +40,7 @@ func (client *challengingClient) Do(req *http.Request) (*http.Response, error) { missingUsername := len(username) == 0 missingPassword := len(password) == 0 - if (missingUsername || missingPassword) && (client.reader != nil) { + if (missingUsername || missingPassword) && client.reader != nil { fmt.Printf("Authenticate for \"%v\"\n", realm) if missingUsername { username = util.PromptForString(client.reader, "Username: ") diff --git a/pkg/cmd/util/tokencmd/request_token.go b/pkg/cmd/util/tokencmd/request_token.go index 7f43cc26b857..d56119a66683 100644 --- a/pkg/cmd/util/tokencmd/request_token.go +++ b/pkg/cmd/util/tokencmd/request_token.go @@ -7,10 +7,10 @@ import ( "regexp" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/golang/glog" - - "github.com/openshift/origin/pkg/auth/server/tokenrequest" "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/oauth/server/osinserver" + + server "github.com/openshift/origin/pkg/cmd/server/origin" ) const accessTokenRedirectPattern = `#access_token=([\w]+)&` @@ -45,15 +45,10 @@ func RequestToken(clientCfg *kclient.Config, reader io.Reader, defaultUsername s osClient.Client = &challengingClient{httpClient, reader, defaultUsername, defaultPassword} - result := osClient.Get().AbsPath("oauth", "authorize").Param("response_type", "token").Param("client_id", "openshift-challenging-client").Do() + result := osClient.Get().AbsPath(server.OpenShiftOAuthAPIPrefix, osinserver.AuthorizePath).Param("response_type", "token").Param("client_id", "openshift-challenging-client").Do() if len(tokenGetter.accessToken) == 0 { - if result.Error() != nil { - glog.Errorf("Error making server request: %v", result.Error()) - } - - requestTokenURL := clientCfg.Host + "/oauth" /* clean up after auth.go dies */ + tokenrequest.RequestTokenEndpoint - return "", errors.New("Unable to get token. Try visiting " + requestTokenURL + " for a new token.") + return "", result.Error() } return tokenGetter.accessToken, nil diff --git a/test/integration/login_test.go b/test/integration/login_test.go new file mode 100644 index 000000000000..03404b89eb79 --- /dev/null +++ b/test/integration/login_test.go @@ -0,0 +1,147 @@ +// +build integration,!no-etcd + +package integration + +import ( + "os" + "testing" + + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/cli/cmd" + newproject "github.com/openshift/origin/pkg/cmd/experimental/project" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" + "github.com/openshift/origin/pkg/user/api" + "github.com/spf13/pflag" +) + +func init() { + requireEtcd() +} + +func TestLogin(t *testing.T) { + startConfig, err := StartTestMaster() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + openshiftClient, openshiftClientConfig, err := startConfig.GetOpenshiftClient() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // empty config, should display message + loginOptions := newLoginOptions("", "", "", "", false) + err = loginOptions.GatherServerInfo() + if err == nil { + t.Errorf("Raw login should error out") + } + + username := "joe" + password := "pass" + project := "the-singularity-is-near" + server := openshiftClientConfig.Host + + loginOptions = newLoginOptions(server, username, password, "", true) + + if err = loginOptions.GatherServerInfo(); err != nil { + t.Fatalf("Error trying to determine server info: ", err) + } + + if err = loginOptions.GatherAuthInfo(); err != nil { + t.Fatalf("Error trying to determine auth info: ", err) + } + + me, err := loginOptions.Whoami() + if err != nil { + t.Errorf("unexpected error: ", err) + } + if me.Name != "anypassword:"+username { + t.Fatalf("Unexpected user after authentication: %v", me.Name) + } + + newProjectOptions := &newproject.NewProjectOptions{ + Client: openshiftClient, + ProjectName: project, + AdminRole: "admin", + MasterPolicyNamespace: "master", + AdminUser: "anypassword:" + username, + } + if err := newProjectOptions.Run(); err != nil { + t.Fatalf("unexpected error, a project is required to continue: ", err) + } + + oClient, _ := client.New(loginOptions.Config) + p, err := oClient.Projects().Get(project) + if err != nil { + t.Errorf("unexpected error: ", err) + } + + if p.Name != project { + t.Fatalf("Got the unexpected project: %v", p.Name) + } + + // TODO Commented because of incorrectly hitting cache when listing projects. + // Should be enabled again when cache eviction is properly fixed. + + // err = loginOptions.GatherProjectInfo() + // if err != nil { + // t.Fatalf("unexpected error: ", err) + // } + + // if loginOptions.Project != project { + // t.Fatalf("Expected project %v but got %v", project, loginOptions.Project) + // } + + // configFile, err := ioutil.TempFile("", "openshiftconfig") + // if err != nil { + // t.Fatalf("unexpected error: %v", err) + // } + // defer os.Remove(configFile.Name()) + + // if _, err = loginOptions.SaveConfig(configFile.Name()); err != nil { + // t.Fatalf("unexpected error: ", err) + // } +} + +func newLoginOptions(server string, username string, password string, context string, insecure bool) *cmd.LoginOptions { + flagset := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) + factory := clientcmd.New(flagset) + + flags := []string{} + + if len(server) > 0 { + flags = append(flags, "--server="+server) + } + if len(context) > 0 { + flags = append(flags, "--context="+context) + } + if insecure { + flags = append(flags, "--insecure-skip-tls-verify") + } + + flagset.Parse(flags) + + loginOptions := &cmd.LoginOptions{ + ClientConfig: factory.OpenShiftClientConfig, + Reader: os.Stdin, + Username: username, + Password: password, + } + + return loginOptions +} + +func whoami(clientCfg *kclient.Config) (*api.User, error) { + oClient, err := client.New(clientCfg) + if err != nil { + return nil, err + } + + me, err := oClient.Users().Get("~") + if err != nil { + return nil, err + } + + return me, nil +}