github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/serialize.go (about)

     1  package k8s
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  
     9  	v1 "k8s.io/api/core/v1"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    12  	"k8s.io/apimachinery/pkg/runtime"
    13  	yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
    14  	"k8s.io/client-go/kubernetes/scheme"
    15  	yamlEncoder "sigs.k8s.io/yaml"
    16  )
    17  
    18  func ParseYAMLFromString(yaml string) ([]K8sEntity, error) {
    19  	buf := bytes.NewBuffer([]byte(yaml))
    20  	return ParseYAML(buf)
    21  }
    22  
    23  func decodeMetaList(list *metav1.List) ([]K8sEntity, error) {
    24  	result := make([]K8sEntity, 0, len(list.Items))
    25  	for _, item := range list.Items {
    26  		decoded, err := decodeRawExtension(item)
    27  		if err != nil {
    28  			return nil, err
    29  		}
    30  		result = append(result, decoded...)
    31  	}
    32  	return result, nil
    33  }
    34  
    35  func decodeList(list *v1.List) ([]K8sEntity, error) {
    36  	return decodeMetaList((*metav1.List)(list))
    37  }
    38  
    39  func decodeToRuntimeObj(ext runtime.RawExtension) (runtime.Object, error) {
    40  	ext.Raw = bytes.TrimSpace(ext.Raw)
    41  
    42  	// NOTE(nick): I LOL'd at the null check, but it's what kubectl does.
    43  	if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
    44  		return nil, nil
    45  	}
    46  
    47  	obj, _, decodeErr :=
    48  		scheme.Codecs.UniversalDeserializer().Decode(ext.Raw, nil, nil)
    49  	if decodeErr == nil {
    50  		return obj, nil
    51  	}
    52  
    53  	// decode as unstructured - if the _original_ decode error was due to it
    54  	// being a non-standard type, the unstructured object will be returned;
    55  	// otherwise, it'll be used to provide additional context to the error if
    56  	// possible
    57  	var unst unstructured.Unstructured
    58  	_, gvk, err :=
    59  		unstructured.UnstructuredJSONScheme.Decode(ext.Raw, nil, &unst)
    60  	if err != nil {
    61  		if gvk != nil && gvk.Kind != "" {
    62  			// add the kind if possible (decode will return it even on error
    63  			// if it was able to parse it out first); we don't have the name
    64  			// available since both structured + unstructured decodes failed
    65  			decodeErr = fmt.Errorf("decoding %s object: %w", gvk.Kind, decodeErr)
    66  		}
    67  		// ignore the unstructured error and instead use the original decode
    68  		// error, as it's more likely to be descriptive
    69  		return nil, decodeErr
    70  	}
    71  	obj = &unst
    72  
    73  	if runtime.IsNotRegisteredError(decodeErr) {
    74  		// not a built-in/known K8s type, but a valid apiserver object, so
    75  		// return the unstructured object
    76  		return obj, nil
    77  	}
    78  
    79  	kind := unst.GetKind()
    80  	if kind == "" {
    81  		kind = "Kubernetes object"
    82  	}
    83  	// add the kind and object name to the error
    84  	// example -> decoding Secret "foo": illegal base64 data at input byte 0
    85  	err = fmt.Errorf("decoding %s %q: %w", kind, unst.GetName(), decodeErr)
    86  	return nil, err
    87  }
    88  
    89  func decodeRawExtension(ext runtime.RawExtension) ([]K8sEntity, error) {
    90  	obj, err := decodeToRuntimeObj(ext)
    91  	if err != nil {
    92  		return nil, err
    93  	} else if obj == nil {
    94  		return nil, nil
    95  	}
    96  
    97  	// Check to see if this is a list, and we can decode the list elements.
    98  	list, isList := obj.(*v1.List)
    99  	if isList {
   100  		return decodeList(list)
   101  	}
   102  
   103  	metaList, isMetaList := obj.(*metav1.List)
   104  	if isMetaList {
   105  		return decodeMetaList(metaList)
   106  	}
   107  
   108  	return []K8sEntity{NewK8sEntity(obj)}, nil
   109  }
   110  
   111  // Parse the YAML into entities.
   112  // Loosely based on
   113  // https://github.com/kubernetes/cli-runtime/blob/d6a36215b15f83b94578f2ffce5d00447972e8ae/pkg/genericclioptions/resource/visitor.go#L583
   114  func ParseYAML(k8sYaml io.Reader) ([]K8sEntity, error) {
   115  	reader := bufio.NewReader(k8sYaml)
   116  	decoder := yamlDecoder.NewYAMLOrJSONDecoder(reader, 4096)
   117  
   118  	result := make([]K8sEntity, 0)
   119  	for {
   120  		ext := runtime.RawExtension{}
   121  		if err := decoder.Decode(&ext); err != nil {
   122  			if err == io.EOF {
   123  				break
   124  			}
   125  			return nil, err
   126  		}
   127  
   128  		entities, err := decodeRawExtension(ext)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		result = append(result, entities...)
   133  	}
   134  
   135  	return result, nil
   136  }
   137  
   138  // Serializes the provided K8s object as YAML to the given writer.
   139  //
   140  // By convention, all K8s objects contain ObjectMetadata, Spec, and Status.
   141  // This only serializes the metadata and spec, skipping the status.
   142  func serializeSpec(obj runtime.Object, w io.Writer) error {
   143  	json, err := specJSONIterator.Marshal(obj)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	data, err := yamlEncoder.JSONToYAML(json)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	_, err = w.Write(data)
   152  	return err
   153  }
   154  
   155  // Serializes the provided K8s objects as YAML.
   156  //
   157  // By convention, all K8s objects contain ObjectMetadata, Spec, and Status.
   158  // This only serializes the metadata and spec, skipping the status.
   159  func SerializeSpecYAML(decoded []K8sEntity) (string, error) {
   160  	buf, err := SerializeSpecYAMLToBuffer(decoded)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  	return buf.String(), nil
   165  }
   166  
   167  func SerializeSpecYAMLToBuffer(decoded []K8sEntity) (*bytes.Buffer, error) {
   168  	buf := bytes.NewBuffer(nil)
   169  	for i, obj := range decoded {
   170  		if i != 0 {
   171  			buf.Write([]byte("\n---\n"))
   172  		}
   173  		err := serializeSpec(obj.Obj, buf)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  	}
   178  	return buf, nil
   179  }