package cmd import ( "bufio" "bytes" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "io" "math/rand" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "syscall" "text/template" "time" "unicode" "github.com/ghodss/yaml" "github.com/rancher/cli/cliclient" "github.com/rancher/cli/config" "github.com/rancher/norman/clientbase" ntypes "github.com/rancher/norman/types" "github.com/rancher/norman/types/convert" managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" "github.com/sirupsen/logrus" "github.com/urfave/cli" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/client-go/tools/clientcmd/api" ) const ( letters = "abcdefghijklmnopqrstuvwxyz0123456789" cfgFile = "cli2.json" kubeConfigKeyFormat = "%s-%s" defaultHTTPTimeout = time.Minute // Matches the default timeout of the Norman Api Client. ) var ( // ManagementResourceTypes lists the types we use the management client for ManagementResourceTypes = []string{"cluster", "node", "project"} // ProjectResourceTypes lists the types we use the cluster client for ProjectResourceTypes = []string{"secret", "namespacedSecret", "workload"} // ClusterResourceTypes lists the types we use the project client for ClusterResourceTypes = []string{"persistentVolume", "storageClass", "namespace"} formatFlag = cli.StringFlag{ Name: "format,o", Usage: "'json', 'yaml' or custom format", } quietFlag = cli.BoolFlag{ Name: "quiet,q", Usage: "Only display IDs or suppress help text", } ) type MemberData struct { Name string MemberType string AccessType string } type RoleTemplate struct { ID string Name string Description string } type RoleTemplateBinding struct { ID string Member string Role string Created string } func listAllRoles() []string { roles := []string{} roles = append(roles, ManagementResourceTypes...) roles = append(roles, ProjectResourceTypes...) roles = append(roles, ClusterResourceTypes...) return roles } func listRoles(ctx *cli.Context, context string) error { c, err := GetClient(ctx) if err != nil { return err } filter := defaultListOpts(ctx) filter.Filters["hidden"] = false filter.Filters["context"] = context templates, err := c.ManagementClient.RoleTemplate.List(filter) if err != nil { return err } writer := NewTableWriter([][]string{ {"ID", "ID"}, {"NAME", "Name"}, {"DESCRIPTION", "Description"}, }, ctx) defer writer.Close() for _, item := range templates.Data { writer.Write(&RoleTemplate{ ID: item.ID, Name: item.Name, Description: item.Description, }) } return writer.Err() } func listRoleTemplateBindings(writerConfig *TableWriterConfig, rtbs []RoleTemplateBinding) error { writer := NewTableWriterWithConfig([][]string{ {"BINDING-ID", "ID"}, {"MEMBER", "Member"}, {"ROLE", "Role"}, {"CREATED", "Created"}, }, writerConfig) defer writer.Close() for _, rtb := range rtbs { writer.Write(&rtb) } return writer.Err() } type principalGetter interface { ByID(id string) (*managementClient.Principal, error) } func getMemberNameFromPrincipal(principals principalGetter, principalID string) string { principal, err := principals.ByID(url.PathEscape(principalID)) if err != nil { principal = parsePrincipalID(principalID) } return fmt.Sprintf( "%s (%s %s)", principal.Name, cases.Title(language.Und).String(principal.Provider), cases.Title(language.Und).String(principal.PrincipalType), ) } func parsePrincipalID(principalID string) *managementClient.Principal { scheme, id, _ := strings.Cut(principalID, "://") provider, ptype, _ := strings.Cut(scheme, "_") if provider == "local" && ptype == "" { ptype = "user" } if ptype != "user" { ptype = "group" } return &managementClient.Principal{ Name: id, LoginName: id, Provider: provider, PrincipalType: ptype, } } func getKubeConfigForUser(ctx *cli.Context, user string) (*api.Config, error) { cf, err := loadConfig(ctx) if err != nil { return nil, err } focusedServer, err := cf.FocusedServer() if err != nil { return nil, err } kubeConfig := focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, focusedServer.FocusedCluster())] return kubeConfig, nil } func setKubeConfigForUser(ctx *cli.Context, user string, kubeConfig *api.Config) error { cf, err := loadConfig(ctx) if err != nil { return err } focusedServer, err := cf.FocusedServer() if err != nil { return err } if focusedServer.KubeConfigs == nil { focusedServer.KubeConfigs = make(map[string]*api.Config) } focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, focusedServer.FocusedCluster())] = kubeConfig return cf.Write() } func searchForMember(ctx *cli.Context, c *cliclient.MasterClient, name string) (*managementClient.Principal, error) { filter := defaultListOpts(ctx) filter.Filters["ID"] = "thisisnotathingIhope" // A collection is needed to get the action link pCollection, err := c.ManagementClient.Principal.List(filter) if err != nil { return nil, err } p := managementClient.SearchPrincipalsInput{ Name: name, } results, err := c.ManagementClient.Principal.CollectionActionSearch(pCollection, &p) if err != nil { return nil, err } dataLength := len(results.Data) switch { case dataLength == 0: return nil, fmt.Errorf("no results found for %q", name) case dataLength == 1: return &results.Data[0], nil case dataLength >= 10: results.Data = results.Data[:10] } var names []string for _, person := range results.Data { names = append(names, person.Name+fmt.Sprintf(" (%s)", person.PrincipalType)) } selection := selectFromList("Multiple results found:", names) return &results.Data[selection], nil } func loadAndVerifyCert(path string) (string, error) { caCert, err := os.ReadFile(path) if err != nil { return "", err } return verifyCert(caCert) } func verifyCert(caCert []byte) (string, error) { // replace the escaped version of the line break caCert = bytes.Replace(caCert, []byte(`\n`), []byte("\n"), -1) block, _ := pem.Decode(caCert) if nil == block { return "", errors.New("no cert was found") } parsedCert, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", err } if !parsedCert.IsCA { return "", errors.New("caCerts is not valid") } return string(caCert), nil } func GetConfigPath(ctx *cli.Context) string { // path will always be set by the global flag default path := ctx.GlobalString("config") return filepath.Join(path, cfgFile) } func loadConfig(ctx *cli.Context) (config.Config, error) { path := GetConfigPath(ctx) return config.LoadFromPath(path) } func lookupConfig(ctx *cli.Context) (*config.ServerConfig, error) { cf, err := loadConfig(ctx) if err != nil { return nil, err } cs, err := cf.FocusedServer() if err != nil { return nil, err } return cs, nil } func GetClient(ctx *cli.Context) (*cliclient.MasterClient, error) { cf, err := lookupConfig(ctx) if err != nil { return nil, err } mc, err := cliclient.NewMasterClient(cf) if err != nil { return nil, err } return mc, nil } // GetResourceType maps an incoming resource type to a valid one from the schema func GetResourceType(c *cliclient.MasterClient, resource string) (string, error) { if c.ManagementClient != nil { for key := range c.ManagementClient.APIBaseClient.Types { if strings.EqualFold(key, resource) { return key, nil } } } if c.ProjectClient != nil { for key := range c.ProjectClient.APIBaseClient.Types { if strings.EqualFold(key, resource) { return key, nil } } } if c.ClusterClient != nil { for key := range c.ClusterClient.APIBaseClient.Types { if strings.EqualFold(key, resource) { return key, nil } } } if c.CAPIClient != nil { for key := range c.CAPIClient.APIBaseClient.Types { lowerKey := strings.ToLower(key) if strings.HasPrefix(lowerKey, "cluster.x-k8s.io") && lowerKey == strings.ToLower(resource) { return key, nil } } } return "", fmt.Errorf("unknown resource type: %s", resource) } func Lookup(c *cliclient.MasterClient, name string, types ...string) (*ntypes.Resource, error) { var byName *ntypes.Resource for _, schemaType := range types { rt, err := GetResourceType(c, schemaType) if err != nil { logrus.Debugf("Error GetResourceType: %v", err) return nil, err } var schemaClient clientbase.APIBaseClientInterface // the schemaType dictates which client we need to use if c.CAPIClient != nil { if strings.HasPrefix(rt, "cluster.x-k8s.io") { schemaClient = c.CAPIClient } } if c.ManagementClient != nil { if _, ok := c.ManagementClient.APIBaseClient.Types[rt]; ok { schemaClient = c.ManagementClient } } if c.ProjectClient != nil { if _, ok := c.ProjectClient.APIBaseClient.Types[rt]; ok { schemaClient = c.ProjectClient } } if c.ClusterClient != nil { if _, ok := c.ClusterClient.APIBaseClient.Types[rt]; ok { schemaClient = c.ClusterClient } } // Attempt to get the resource by ID var resource ntypes.Resource if err := schemaClient.ByID(schemaType, name, &resource); !clientbase.IsNotFound(err) && err != nil { logrus.Debugf("Error schemaClient.ByID: %v", err) return nil, err } else if err == nil && resource.ID == name { return &resource, nil } // Resource was not found assuming the ID, check if it's the name of a resource var collection ntypes.ResourceCollection listOpts := &ntypes.ListOpts{ Filters: map[string]interface{}{ "name": name, "removed_null": 1, }, } if err := schemaClient.List(schemaType, listOpts, &collection); !clientbase.IsNotFound(err) && err != nil { logrus.Debugf("Error schemaClient.List: %v", err) return nil, err } if len(collection.Data) > 1 { ids := []string{} for _, data := range collection.Data { ids = append(ids, data.ID) } return nil, fmt.Errorf("Multiple resources of type %s found for name %s: %v", schemaType, name, ids) } // No matches for this schemaType, try the next one if len(collection.Data) == 0 { continue } if byName != nil { return nil, fmt.Errorf("Multiple resources named %s: %s:%s, %s:%s", name, collection.Data[0].Type, collection.Data[0].ID, byName.Type, byName.ID) } byName = &collection.Data[0] } if byName == nil { return nil, fmt.Errorf("Not found: %s", name) } return byName, nil } // RandomLetters returns a string with random letters of length n func RandomLetters(n int) string { b := make([]byte, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func appendTabDelim(buf *bytes.Buffer, value string) { if buf.Len() == 0 { buf.WriteString(value) } else { buf.WriteString("\t") buf.WriteString(value) } } func SimpleFormat(values [][]string) (string, string) { headerBuffer := bytes.Buffer{} valueBuffer := bytes.Buffer{} for _, v := range values { appendTabDelim(&headerBuffer, v[0]) if strings.Contains(v[1], "{{") { appendTabDelim(&valueBuffer, v[1]) } else { appendTabDelim(&valueBuffer, "{{."+v[1]+"}}") } } headerBuffer.WriteString("\n") valueBuffer.WriteString("\n") return headerBuffer.String(), valueBuffer.String() } func defaultAction(fn func(ctx *cli.Context) error) func(ctx *cli.Context) error { return func(ctx *cli.Context) error { if ctx.Bool("help") { return cli.ShowAppHelp(ctx) } return fn(ctx) } } func printTemplate(out io.Writer, templateContent string, obj interface{}) error { funcMap := map[string]interface{}{ "endpoint": FormatEndpoint, "ips": FormatIPAddresses, "json": FormatJSON, } tmpl, err := template.New("").Funcs(funcMap).Parse(templateContent) if err != nil { return err } return tmpl.Execute(out, obj) } func selectFromList(header string, choices []string) int { if header != "" { fmt.Println(header) } reader := bufio.NewReader(os.Stdin) selected := -1 for selected <= 0 || selected > len(choices) { for i, choice := range choices { fmt.Printf("[%d] %s\n", i+1, choice) } fmt.Print("Select: ") text, _ := reader.ReadString('\n') text = strings.TrimSpace(text) num, err := strconv.Atoi(text) if err == nil { selected = num } } return selected - 1 } func processExitCode(err error) error { if exitErr, ok := err.(*exec.ExitError); ok { if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { os.Exit(status.ExitStatus()) } } return err } func SplitOnColon(s string) []string { return strings.Split(s, ":") } func parseClusterAndProjectID(id string) (string, string, error) { // Validate id // Examples: // c-qmpbm:p-mm62v // c-qmpbm:project-mm62v // c-m-j2s7m6lq:p-mm62v // See https://github.com/rancher/rancher/issues/14400 if match, _ := regexp.MatchString("((local)|(c-[[:alnum:]]{5})|(c-m-[[:alnum:]]{8})):(p|project)-[[:alnum:]]{5}", id); match { parts := SplitOnColon(id) return parts[0], parts[1], nil } return "", "", fmt.Errorf("Unable to extract clusterid and projectid from [%s]", id) } // Return a JSON blob of the file at path func readFileReturnJSON(path string) ([]byte, error) { file, err := os.ReadFile(path) if err != nil { return []byte{}, err } // This is probably already JSON if true if hasPrefix(file, []byte("{")) { return file, nil } return yaml.YAMLToJSON(file) } // renameKeys renames the keys in a given map of arbitrary depth with a provided function for string keys. func renameKeys(input map[string]interface{}, f func(string) string) { for k, v := range input { delete(input, k) newKey := f(k) input[newKey] = v if innerMap, ok := v.(map[string]interface{}); ok { renameKeys(innerMap, f) } } } // convertSnakeCaseKeysToCamelCase takes a map and recursively transforms all snake_case keys into camelCase keys. func convertSnakeCaseKeysToCamelCase(input map[string]interface{}) { renameKeys(input, convert.ToJSONKey) } // Return true if the first non-whitespace bytes in buf is prefix. func hasPrefix(buf []byte, prefix []byte) bool { trim := bytes.TrimLeftFunc(buf, unicode.IsSpace) return bytes.HasPrefix(trim, prefix) } // getClusterNames maps cluster ID to name and defaults to ID if name is blank func getClusterNames(ctx *cli.Context, c *cliclient.MasterClient) (map[string]string, error) { clusterNames := make(map[string]string) clusterCollection, err := c.ManagementClient.Cluster.List(defaultListOpts(ctx)) if err != nil { return clusterNames, err } for _, cluster := range clusterCollection.Data { if cluster.Name == "" { clusterNames[cluster.ID] = cluster.ID } else { clusterNames[cluster.ID] = cluster.Name } } return clusterNames, nil } func getClusterName(cluster *managementClient.Cluster) string { if cluster.Name != "" { return cluster.Name } return cluster.ID } const humanTimeFormat = "02 Jan 2006 15:04:05 MST" func createdTimetoHuman(t string) (string, error) { parsedTime, err := time.Parse(time.RFC3339, t) if err != nil { return "", err } return parsedTime.Format(humanTimeFormat), nil } func ConfigDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(homeDir, ".rancher"), nil } func newHTTPClient(serverConfig *config.ServerConfig, tlsConfig *tls.Config) (*http.Client, error) { var proxy func(*http.Request) (*url.URL, error) if serverConfig.ProxyURL != "" { proxyURL, err := url.Parse(serverConfig.ProxyURL) if err != nil { return nil, fmt.Errorf("invalid proxy address %s: %w", serverConfig.ProxyURL, err) } proxy = http.ProxyURL(proxyURL) } else { proxy = http.ProxyFromEnvironment } tr := &http.Transport{ Proxy: proxy, } if tlsConfig != nil { tr.TLSClientConfig = tlsConfig } timeout := serverConfig.GetHTTPTimeout() if timeout == 0 { timeout = defaultHTTPTimeout } return &http.Client{ Transport: tr, Timeout: timeout, }, nil }