github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/kubernetes/manifest/manifest.go (about)

     1  package manifest
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"text/template"
     8  
     9  	"github.com/Masterminds/sprig/v3"
    10  	"github.com/pkg/errors"
    11  	"github.com/stretchr/objx"
    12  	yaml "gopkg.in/yaml.v2"
    13  )
    14  
    15  // Manifest represents a Kubernetes API object. The fields `apiVersion` and
    16  // `kind` are required, `metadata.name` should be present as well
    17  type Manifest map[string]interface{}
    18  
    19  // New creates a new Manifest
    20  func New(raw map[string]interface{}) (Manifest, error) {
    21  	m := Manifest(raw)
    22  	if err := m.Verify(); err != nil {
    23  		return nil, err
    24  	}
    25  	return m, nil
    26  }
    27  
    28  // NewFromObj creates a new Manifest from an objx.Map
    29  func NewFromObj(raw objx.Map) (Manifest, error) {
    30  	return New(map[string]interface{}(raw))
    31  }
    32  
    33  // String returns the Manifest in yaml representation
    34  func (m Manifest) String() string {
    35  	y, err := yaml.Marshal(m)
    36  	if err != nil {
    37  		// this should never go wrong in normal operations
    38  		panic(errors.Wrap(err, "formatting manifest"))
    39  	}
    40  	return string(y)
    41  }
    42  
    43  var (
    44  	ErrInvalidStr = fmt.Errorf("missing or not of string type")
    45  	ErrInvalidMap = fmt.Errorf("missing or not an object")
    46  )
    47  
    48  // Verify checks whether the manifest is correctly structured
    49  func (m Manifest) Verify() error {
    50  	o := m2o(m)
    51  	fields := make(map[string]error)
    52  
    53  	if !o.Get("kind").IsStr() {
    54  		fields["kind"] = ErrInvalidStr
    55  	}
    56  	if !o.Get("apiVersion").IsStr() {
    57  		fields["apiVersion"] = ErrInvalidStr
    58  	}
    59  
    60  	// Lists don't have `metadata`
    61  	if !m.IsList() {
    62  		if !o.Get("metadata").IsMSI() {
    63  			fields["metadata"] = ErrInvalidMap
    64  		}
    65  		if !o.Get("metadata.name").IsStr() && !o.Get("metadata.generateName").IsStr() {
    66  			fields["metadata.name"] = ErrInvalidStr
    67  		}
    68  
    69  		if err := verifyMSS(o.Get("metadata.labels").Data()); err != nil {
    70  			fields["metadata.labels"] = err
    71  		}
    72  		if err := verifyMSS(o.Get("metadata.annotations").Data()); err != nil {
    73  			fields["metadata.annotations"] = err
    74  		}
    75  	}
    76  
    77  	if len(fields) == 0 {
    78  		return nil
    79  	}
    80  
    81  	return &SchemaError{
    82  		Fields:   fields,
    83  		Manifest: m,
    84  	}
    85  }
    86  
    87  // verifyMSS checks that ptr is either nil or a string map
    88  func verifyMSS(ptr interface{}) error {
    89  	if ptr == nil {
    90  		return nil
    91  	}
    92  
    93  	switch t := ptr.(type) {
    94  	case map[string]string:
    95  		return nil
    96  	case map[string]interface{}:
    97  		for k, v := range t {
    98  			if _, ok := v.(string); !ok {
    99  				return fmt.Errorf("contains non-string field '%s' of type '%T'", k, v)
   100  			}
   101  		}
   102  		return nil
   103  	default:
   104  		return fmt.Errorf("must be object, but got '%T' instead", ptr)
   105  	}
   106  }
   107  
   108  // IsList returns whether the manifest is a List type, containing other
   109  // manifests as children. Code based on
   110  // https://github.com/kubernetes/apimachinery/blob/61490fe38e784592212b24b9878306b09be45ab0/pkg/apis/meta/v1/unstructured/unstructured.go#L54
   111  func (m Manifest) IsList() bool {
   112  	items, ok := m["items"]
   113  	if !ok {
   114  		return false
   115  	}
   116  	_, ok = items.([]interface{})
   117  	return ok
   118  }
   119  
   120  // Items returns list items if the manifest is of List type
   121  func (m Manifest) Items() (List, error) {
   122  	if !m.IsList() {
   123  		return nil, fmt.Errorf("attempt to unwrap non-list object '%s' of kind '%s'", m.Metadata().Name(), m.Kind())
   124  	}
   125  
   126  	// This is safe, IsList() asserts this
   127  	items := m["items"].([]interface{})
   128  	list := make(List, 0, len(items))
   129  	for _, i := range items {
   130  		child, ok := i.(map[string]interface{})
   131  		if !ok {
   132  			return nil, fmt.Errorf("unwrapped list item is not an object, but '%T'", child)
   133  		}
   134  
   135  		m := Manifest(child)
   136  		list = append(list, m)
   137  	}
   138  
   139  	return list, nil
   140  }
   141  
   142  // Kind returns the kind of the API object
   143  func (m Manifest) Kind() string {
   144  	return m["kind"].(string)
   145  }
   146  
   147  // KindName returns kind and metadata.name in the `<kind>/<name>` format
   148  func (m Manifest) KindName() string {
   149  	return fmt.Sprintf("%s/%s",
   150  		m.Kind(),
   151  		m.Metadata().Name(),
   152  	)
   153  }
   154  
   155  // APIVersion returns the version of the API this object uses
   156  func (m Manifest) APIVersion() string {
   157  	return m["apiVersion"].(string)
   158  }
   159  
   160  // Metadata returns the metadata of this object
   161  func (m Manifest) Metadata() Metadata {
   162  	if m["metadata"] == nil {
   163  		m["metadata"] = make(map[string]interface{})
   164  	}
   165  	return Metadata(m["metadata"].(map[string]interface{}))
   166  }
   167  
   168  // UnmarshalJSON validates the Manifest during json parsing
   169  func (m *Manifest) UnmarshalJSON(data []byte) error {
   170  	type tmp Manifest
   171  	var t tmp
   172  	if err := json.Unmarshal(data, &t); err != nil {
   173  		return err
   174  	}
   175  	*m = Manifest(t)
   176  	return m.Verify()
   177  }
   178  
   179  // UnmarshalYAML validates the Manifest during yaml parsing
   180  func (m *Manifest) UnmarshalYAML(unmarshal func(interface{}) error) error {
   181  	var tmp map[string]interface{}
   182  	if err := unmarshal(&tmp); err != nil {
   183  		return err
   184  	}
   185  
   186  	data, err := json.Marshal(tmp)
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	return json.Unmarshal(data, m)
   192  }
   193  
   194  // Metadata is the metadata object from the Manifest
   195  type Metadata map[string]interface{}
   196  
   197  // Name of the manifest
   198  func (m Metadata) Name() string {
   199  	name, ok := m["name"]
   200  	if !ok {
   201  		return ""
   202  	}
   203  	return name.(string)
   204  }
   205  
   206  // HasNamespace returns whether the manifest has a namespace set
   207  func (m Metadata) HasNamespace() bool {
   208  	return m2o(m).Get("namespace").IsStr()
   209  }
   210  
   211  // Namespace of the manifest
   212  func (m Metadata) Namespace() string {
   213  	if !m.HasNamespace() {
   214  		return ""
   215  	}
   216  	return m["namespace"].(string)
   217  }
   218  
   219  func (m Metadata) UID() string {
   220  	uid, ok := m["uid"].(string)
   221  	if !ok {
   222  		return ""
   223  	}
   224  	return uid
   225  }
   226  
   227  // Labels of the manifest
   228  func (m Metadata) Labels() map[string]interface{} {
   229  	return safeMSI(m, "labels")
   230  }
   231  
   232  // Annotations of the manifest
   233  func (m Metadata) Annotations() map[string]interface{} {
   234  	return safeMSI(m, "annotations")
   235  }
   236  
   237  // Managed fields of the manifest
   238  func (m Metadata) ManagedFields() []interface{} {
   239  	items, ok := m["managedFields"]
   240  	if !ok {
   241  		return make([]interface{}, 0)
   242  	}
   243  	list, ok := items.([]interface{})
   244  	if !ok {
   245  		return make([]interface{}, 0)
   246  	}
   247  	return list
   248  }
   249  
   250  func safeMSI(m map[string]interface{}, key string) map[string]interface{} {
   251  	switch t := m[key].(type) {
   252  	case map[string]interface{}:
   253  		return t
   254  	default:
   255  		m[key] = make(map[string]interface{})
   256  		return m[key].(map[string]interface{})
   257  	}
   258  }
   259  
   260  // List of individual Manifests
   261  type List []Manifest
   262  
   263  // String returns the List as a yaml stream. In case of an error, it is
   264  // returned as a string instead.
   265  func (m List) String() string {
   266  	buf := bytes.Buffer{}
   267  	enc := yaml.NewEncoder(&buf)
   268  
   269  	for _, d := range m {
   270  		if err := enc.Encode(d); err != nil {
   271  			// This should never happen in normal operations
   272  			panic(errors.Wrap(err, "formatting manifests"))
   273  		}
   274  	}
   275  
   276  	return buf.String()
   277  }
   278  
   279  func (m List) Namespaces() []string {
   280  	namespaces := map[string]struct{}{}
   281  	for _, manifest := range m {
   282  		if namespace := manifest.Metadata().Namespace(); namespace != "" {
   283  			namespaces[namespace] = struct{}{}
   284  		}
   285  	}
   286  	keys := []string{}
   287  	for k := range namespaces {
   288  		keys = append(keys, k)
   289  	}
   290  	return keys
   291  }
   292  
   293  func m2o(m interface{}) objx.Map {
   294  	switch mm := m.(type) {
   295  	case Metadata:
   296  		return objx.New(map[string]interface{}(mm))
   297  	case Manifest:
   298  		return objx.New(map[string]interface{}(mm))
   299  	}
   300  	return nil
   301  }
   302  
   303  // DefaultNameFormat to use when no nameFormat is supplied
   304  const DefaultNameFormat = `{{ print .kind "_" .metadata.name | snakecase }}`
   305  
   306  func ListAsMap(list List, nameFormat string) (map[string]interface{}, error) {
   307  	if nameFormat == "" {
   308  		nameFormat = DefaultNameFormat
   309  	}
   310  
   311  	tmpl, err := template.New("").
   312  		Funcs(sprig.TxtFuncMap()).
   313  		Parse(nameFormat)
   314  	if err != nil {
   315  		return nil, fmt.Errorf("parsing name format: %w", err)
   316  	}
   317  
   318  	out := make(map[string]interface{})
   319  	for _, m := range list {
   320  		var buf bytes.Buffer
   321  		if err := tmpl.Execute(&buf, m); err != nil {
   322  			return nil, err
   323  		}
   324  		name := buf.String()
   325  
   326  		if _, ok := out[name]; ok {
   327  			return nil, ErrorDuplicateName{name: name, format: nameFormat}
   328  		}
   329  		out[name] = map[string]interface{}(m)
   330  	}
   331  
   332  	return out, nil
   333  }