diff --git a/pkg/cmd/cli/cmd/project.go b/pkg/cmd/cli/cmd/project.go index dd6bc11dfd24..6c5ee46a06f7 100644 --- a/pkg/cmd/cli/cmd/project.go +++ b/pkg/cmd/cli/cmd/project.go @@ -1,17 +1,25 @@ package cmd import ( + "bytes" "fmt" "io" "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/fields" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/golang/glog" - "github.com/openshift/origin/pkg/cmd/cli/config" + + "github.com/openshift/origin/pkg/client" + cliconfig "github.com/openshift/origin/pkg/cmd/cli/config" "github.com/openshift/origin/pkg/cmd/util/clientcmd" + "github.com/openshift/origin/pkg/project/api" + + "github.com/golang/glog" "github.com/spf13/cobra" ) @@ -36,6 +44,7 @@ func NewCmdProject(f *clientcmd.Factory, out io.Writer) *cobra.Command { oClient, _, err := f.Clients() checkErr(err) + // No argument provided, we will just print info if argsLength == 0 { currentContext := rawCfg.Contexts[rawCfg.CurrentContext] currentProject := currentContext.Namespace @@ -44,70 +53,114 @@ func NewCmdProject(f *clientcmd.Factory, out io.Writer) *cobra.Command { _, 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) + glog.Fatalf("The project %q specified in your config does not exist.", currentProject) + } + if clientcmd.IsForbidden(err) { + glog.Fatalf("You do not have rights to view project %q.", currentProject) } checkErr(err) } - fmt.Printf("Using project '%v'.\n", currentProject) + if rawCfg.CurrentContext != currentProject { + fmt.Printf("Using project %q from context named %q on server %q.\n", currentProject, rawCfg.CurrentContext, clientCfg.Host) + } else { + fmt.Printf("Using project %q on server %q.\n", currentProject, clientCfg.Host) + } } else { - fmt.Printf("No specific project in use.\n") + fmt.Printf("No project has been set. Pass a project name to make that the default.\n") } return - } - projectName := args[0] + // We have an argument that can be either a context or project + argument := args[0] + + configStore, err := loadConfigStore(cmd) + checkErr(err) + config := configStore.Config + + contextInUse := "" + namespaceInUse := "" + + // Check if argument is an existing context, if so just set it as the context in use. + // If not a context then we will try to handle it as a project. + if context, ok := config.Contexts[argument]; ok { + contextInUse = argument + namespaceInUse = context.Namespace + + config.CurrentContext = argument - project, err := oClient.Projects().Get(projectName) - if err != nil { - if errors.IsNotFound(err) { - glog.Fatalf("Unable to find a project with name '%v'.", projectName) + } else { + project, err := oClient.Projects().Get(argument) + if err != nil { + if isNotFound, isForbidden := errors.IsNotFound(err), clientcmd.IsForbidden(err); isNotFound || isForbidden { + msg := "" + + if isNotFound { + msg = fmt.Sprintf("A project named %q does not exist on server %q.", argument, clientCfg.Host) + } else { + msg = fmt.Sprintf("You do not have rights to view project %q on server %q.", argument, clientCfg.Host) + } + + projects, err := getProjects(oClient) + if err == nil { + msg += "\nYour projects are:" + for _, project := range projects { + msg += "\n" + project.Name + } + } + + if hasMultipleServers(config) { + msg += "\nTo see projects on another server, pass '--server='." + } + + glog.Fatal(msg) + } + + checkErr(err) } - checkErr(err) - } - pathFromFlag := cmdutil.GetFlagString(cmd, config.OpenShiftConfigFlagName) + // If a context exists, 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] - configStore, err := config.LoadFrom(pathFromFlag) - if err != nil { - configStore, err = config.LoadWithLoadingRules() - checkErr(err) - } - checkErr(err) + if namespace == project.Name && clusterAndAuthEquality(clientCfg, cluster, authInfo) { + exists = true + config.CurrentContext = k - config := configStore.Config + contextInUse = k + namespaceInUse = namespace - // 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 + 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] + // Otherwise create a new context, reusing the cluster and auth info + if !exists { + currentCtx := rawCfg.CurrentContext + newCtx := clientcmdapi.NewContext() newCtx.Namespace = project.Name - newCtx.AuthInfo = ctx.AuthInfo - newCtx.Cluster = ctx.Cluster - config.Contexts[fmt.Sprint(util.NewUUID())] = *newCtx + + newCtx.AuthInfo = rawCfg.Contexts[currentCtx].AuthInfo + newCtx.Cluster = rawCfg.Contexts[currentCtx].Cluster + + existingContexIdentifiers := &util.StringSet{} + for key := range rawCfg.Contexts { + existingContexIdentifiers.Insert(key) + } + + newCtxName := cliconfig.GenerateContextIdentifier(newCtx.Namespace, newCtx.Cluster, "", existingContexIdentifiers) + + config.Contexts[newCtxName] = *newCtx + config.CurrentContext = newCtxName + + contextInUse = newCtxName + namespaceInUse = project.Name } } @@ -115,8 +168,60 @@ func NewCmdProject(f *clientcmd.Factory, out io.Writer) *cobra.Command { glog.Fatalf("Error saving project information in the config: %v.", err) } - fmt.Printf("Now using project '%v'.\n", project.Name) + if contextInUse != namespaceInUse { + fmt.Printf("Now using project %q from context named %q on server %q.\n", namespaceInUse, contextInUse, clientCfg.Host) + } else { + fmt.Printf("Now using project %q on server %q.\n", namespaceInUse, clientCfg.Host) + } }, } return cmd } + +func getProjects(oClient *client.Client) ([]api.Project, error) { + projects, err := oClient.Projects().List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, err + } + return projects.Items, nil +} + +func loadConfigStore(cmd *cobra.Command) (*cliconfig.ConfigStore, error) { + pathFromFlag := cmdutil.GetFlagString(cmd, cliconfig.OpenShiftConfigFlagName) + + configStore, err := cliconfig.LoadFrom(pathFromFlag) + if err != nil { + configStore, err = cliconfig.LoadWithLoadingRules() + if err != nil { + return nil, err + } + } + + return configStore, err +} + +func clusterAndAuthEquality(clientCfg *kclient.Config, cluster clientcmdapi.Cluster, authInfo clientcmdapi.AuthInfo) bool { + return cluster.Server == clientCfg.Host && + cluster.InsecureSkipTLSVerify == clientCfg.Insecure && + cluster.CertificateAuthority == clientCfg.CAFile && + bytes.Equal(cluster.CertificateAuthorityData, clientCfg.CAData) && + authInfo.Token == clientCfg.BearerToken && + authInfo.ClientCertificate == clientCfg.TLSClientConfig.CertFile && + bytes.Equal(authInfo.ClientCertificateData, clientCfg.TLSClientConfig.CertData) && + authInfo.ClientKey == clientCfg.TLSClientConfig.KeyFile && + bytes.Equal(authInfo.ClientKeyData, clientCfg.TLSClientConfig.KeyData) +} + +// TODO these kind of funcs could be moved to some kind of clientcmd util +func hasMultipleServers(config *clientcmdapi.Config) bool { + server := "" + for _, cluster := range config.Clusters { + if len(server) == 0 { + server = cluster.Server + } + if server != cluster.Server { + return true + } + } + return false +} diff --git a/pkg/cmd/cli/config/smart_merge.go b/pkg/cmd/cli/config/smart_merge.go index 400dd4592c0f..5f6ddd50fced 100644 --- a/pkg/cmd/cli/config/smart_merge.go +++ b/pkg/cmd/cli/config/smart_merge.go @@ -2,12 +2,18 @@ package config import ( "fmt" + "net" + "net/url" "reflect" + "regexp" clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" ) +var invalidSafeStringSep = regexp.MustCompile(`[.:/]`) + // MergeConfig takes a haystack to look for existing stanzas in (probably the merged config), a config object to modify (probably // either the local or envvar config), and the new additions to merge in. It tries to find equivalents for the addition inside of the // haystack and uses the mapping to avoid creating additional stanzas with duplicate information. It either locates or original @@ -73,7 +79,7 @@ func MergeConfig(haystack, toModify, addition clientcmdapi.Config) (*clientcmdap continue } - uniqueName := getUniqueName(actualContext.Cluster+"-"+actualContext.AuthInfo, existingContextNames) + uniqueName := GenerateContextIdentifier(actualContext.Namespace, actualContext.Cluster, actualContext.AuthInfo, existingContextNames) requestedContextNamesToActualContextNames[requestedKey] = uniqueName ret.Contexts[uniqueName] = *actualContext } @@ -142,6 +148,19 @@ func getMapKeys(theMap reflect.Value) (*util.StringSet, error) { } func getUniqueName(basename string, existingNames *util.StringSet) string { + if parsedUrl, err := url.Parse(basename); err != nil { + if host, port, err := net.SplitHostPort(parsedUrl.Host); err != nil { + if !existingNames.Has(host) { + return host + } + if id := host + "-" + port; !existingNames.Has(id) { + return id + } + } + } + + basename = invalidSafeStringSep.ReplaceAllString(basename, "-") + if !existingNames.Has(basename) { return basename } @@ -155,3 +174,68 @@ func getUniqueName(basename string, existingNames *util.StringSet) string { return string(util.NewUUID()) } + +// Generates the best context identifier possible based on the information it gets. +func GenerateContextIdentifier(namespace string, cluster string, authInfo string, existingContextIdentifiers *util.StringSet) string { + ctx := "" + + // try to use plain namespace + if len(namespace) > 0 { + ctx += namespace + + if !existingContextIdentifiers.Has(ctx) { + return ctx + } + } + + // tries appending "-host" or "-host-port" + if len(cluster) > 0 { + if parsedUrl, err := url.Parse(cluster); err != nil { + if host, port, err := net.SplitHostPort(parsedUrl.Host); err != nil { + if len(ctx) > 0 { + ctx += "-" + } + ctx += host + if !existingContextIdentifiers.Has(ctx) { + return ctx + } + + ctx += "-" + port + if !existingContextIdentifiers.Has(ctx) { + return ctx + } + + } else { + if len(ctx) > 0 { + ctx += "-" + } + ctx += "-" + parsedUrl.Host + if !existingContextIdentifiers.Has(ctx) { + return ctx + } + } + } + } + + // tries appending "-username" + if len(authInfo) > 0 { + if len(ctx) > 0 { + ctx += "-" + } + ctx += authInfo + + if !existingContextIdentifiers.Has(ctx) { + return ctx + } + } + + // append an integer + for i := 0; i < 100; i++ { + if trialName := fmt.Sprintf("%v-%d", ctx, i); !existingContextIdentifiers.Has(trialName) { + return trialName + } + } + + glog.Fatalf("Unable to generate a context identifier. Please provide a context using the '--context=' flag.") + return "" +}