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  }