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 }