github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/kubernetes/client/resources.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "os" 8 "regexp" 9 "strings" 10 11 "github.com/pkg/errors" 12 13 "github.com/grafana/tanka/pkg/kubernetes/manifest" 14 ) 15 16 // Resources the Kubernetes API knows 17 type Resources []Resource 18 19 // Namespaced returns whether a resource is namespace-specific or cluster-wide 20 func (r Resources) Namespaced(m manifest.Manifest) bool { 21 for _, res := range r { 22 if m.Kind() == res.Kind { 23 return res.Namespaced 24 } 25 } 26 27 return false 28 } 29 30 // Resource is a Kubernetes API Resource 31 type Resource struct { 32 APIGroup string `json:"APIGROUP"` 33 APIVersion string `json:"APIVERSION"` 34 Kind string `json:"KIND"` 35 Name string `json:"NAME"` 36 Namespaced bool `json:"NAMESPACED,string"` 37 Shortnames string `json:"SHORTNAMES"` 38 Verbs string `json:"VERBS"` 39 Categories string `json:"CATEGORIES"` 40 } 41 42 func (r Resource) FQN() string { 43 apiGroup := "" 44 if r.APIGroup != "" { 45 // this is only set in kubectl v1.18 and earlier 46 apiGroup = r.APIGroup 47 } else if pos := strings.Index(r.APIVersion, "/"); pos > 0 { 48 apiGroup = r.APIVersion[0:pos] 49 } 50 return strings.TrimSuffix(r.Name+"."+apiGroup, ".") 51 } 52 53 // Resources returns all API resources known to the server 54 func (k Kubectl) Resources() (Resources, error) { 55 cmd := k.ctl("api-resources", "--cached", "--output=wide") 56 var out bytes.Buffer 57 cmd.Stdout = &out 58 cmd.Stderr = os.Stderr 59 60 if err := cmd.Run(); err != nil { 61 return nil, err 62 } 63 64 var res Resources 65 if err := UnmarshalTable(out.String(), &res); err != nil { 66 return nil, errors.Wrap(err, "parsing table") 67 } 68 69 return res, nil 70 } 71 72 // UnmarshalTable unmarshals a raw CLI table into ptr. `json` package is used 73 // for getting the dat into the ptr, `json:` struct tags can be used. 74 func UnmarshalTable(raw string, ptr interface{}) error { 75 raw = strings.TrimSpace(raw) 76 77 lines := strings.Split(raw, "\n") 78 if len(lines) == 0 { 79 return ErrorNoHeader 80 } 81 82 headerStr := lines[0] 83 // headers are ALL-CAPS 84 if !regexp.MustCompile(`^[A-Z\s]+$`).MatchString(headerStr) { 85 return ErrorNoHeader 86 } 87 88 lines = lines[1:] 89 90 spc := regexp.MustCompile(`[A-Z]+\s*`) 91 header := spc.FindAllString(headerStr, -1) 92 93 tbl := make([]map[string]string, 0, len(lines)) 94 for _, l := range lines { 95 elems := splitRow(l, header) 96 if len(elems) != len(header) { 97 return ErrorElementsMismatch{Header: len(header), Row: len(elems)} 98 } 99 100 row := make(map[string]string) 101 for i, e := range elems { 102 key := strings.TrimSpace(header[i]) 103 row[key] = strings.TrimSpace(e) 104 } 105 tbl = append(tbl, row) 106 } 107 108 j, err := json.Marshal(tbl) 109 if err != nil { 110 return err 111 } 112 113 return json.Unmarshal(j, ptr) 114 } 115 116 // ErrorNoHeader occurs when the table lacks an ALL-CAPS header 117 var ErrorNoHeader = fmt.Errorf("table has no header") 118 119 // ErrorElementsMismatch occurs when a row has a different count of elements 120 // than it's header 121 type ErrorElementsMismatch struct { 122 Header, Row int 123 } 124 125 func (e ErrorElementsMismatch) Error() string { 126 return fmt.Sprintf("header and row have different element count: %v != %v", e.Header, e.Row) 127 } 128 129 func splitRow(s string, header []string) (elems []string) { 130 pos := 0 131 for i, h := range header { 132 if i == len(header)-1 { 133 elems = append(elems, s[pos:]) 134 continue 135 } 136 137 lim := len(h) 138 endPos := pos + lim 139 if endPos >= len(s) { 140 endPos = len(s) 141 } 142 143 elems = append(elems, s[pos:endPos]) 144 pos = endPos 145 } 146 return elems 147 }