istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/crd/conversion.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package crd 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "io" 22 "reflect" 23 24 "github.com/hashicorp/go-multierror" 25 "gopkg.in/yaml.v2" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 kubeyaml "k8s.io/apimachinery/pkg/util/yaml" 28 29 "istio.io/istio/pkg/config" 30 "istio.io/istio/pkg/config/schema/collections" 31 "istio.io/istio/pkg/config/schema/resource" 32 "istio.io/istio/pkg/log" 33 ) 34 35 // FromJSON converts a canonical JSON to a proto message 36 func FromJSON(s resource.Schema, js string) (config.Spec, error) { 37 c, err := s.NewInstance() 38 if err != nil { 39 return nil, err 40 } 41 if err = config.ApplyJSON(c, js); err != nil { 42 return nil, err 43 } 44 return c, nil 45 } 46 47 func StatusJSONFromMap(schema resource.Schema, jsonMap *json.RawMessage) (config.Status, error) { 48 if jsonMap == nil { 49 return nil, nil 50 } 51 js, err := json.Marshal(jsonMap) 52 if err != nil { 53 return nil, err 54 } 55 status, err := schema.Status() 56 if err != nil { 57 return nil, err 58 } 59 err = json.Unmarshal(js, status) 60 if err != nil { 61 return nil, err 62 } 63 return status, nil 64 } 65 66 // FromYAML converts a canonical YAML to a proto message 67 func FromYAML(s resource.Schema, yml string) (config.Spec, error) { 68 c, err := s.NewInstance() 69 if err != nil { 70 return nil, err 71 } 72 if err = config.ApplyYAML(c, yml); err != nil { 73 return nil, err 74 } 75 return c, nil 76 } 77 78 // FromJSONMap converts from a generic map to a proto message using canonical JSON encoding 79 // JSON encoding is specified here: https://developers.google.com/protocol-buffers/docs/proto3#json 80 func FromJSONMap(s resource.Schema, data any) (config.Spec, error) { 81 // Marshal to YAML bytes 82 str, err := yaml.Marshal(data) 83 if err != nil { 84 return nil, err 85 } 86 out, err := FromYAML(s, string(str)) 87 if err != nil { 88 return nil, multierror.Prefix(err, fmt.Sprintf("YAML decoding error: %v", string(str))) 89 } 90 return out, nil 91 } 92 93 // ConvertObject converts an IstioObject k8s-style object to the internal configuration model. 94 func ConvertObject(schema resource.Schema, object IstioObject, domain string) (*config.Config, error) { 95 js, err := json.Marshal(object.GetSpec()) 96 if err != nil { 97 return nil, err 98 } 99 spec, err := FromJSON(schema, string(js)) 100 if err != nil { 101 return nil, err 102 } 103 status, err := StatusJSONFromMap(schema, object.GetStatus()) 104 if err != nil { 105 log.Errorf("could not get istio status from map %v, err %v", object.GetStatus(), err) 106 } 107 meta := object.GetObjectMeta() 108 109 return &config.Config{ 110 Meta: config.Meta{ 111 GroupVersionKind: schema.GroupVersionKind(), 112 Name: meta.Name, 113 Namespace: meta.Namespace, 114 Domain: domain, 115 Labels: meta.Labels, 116 Annotations: meta.Annotations, 117 ResourceVersion: meta.ResourceVersion, 118 CreationTimestamp: meta.CreationTimestamp.Time, 119 }, 120 Spec: spec, 121 Status: status, 122 }, nil 123 } 124 125 // ConvertConfig translates Istio config to k8s config JSON 126 func ConvertConfig(cfg config.Config) (IstioObject, error) { 127 spec, err := config.ToRaw(cfg.Spec) 128 if err != nil { 129 return nil, err 130 } 131 var status *json.RawMessage 132 if cfg.Status != nil { 133 s, err := config.ToRaw(cfg.Status) 134 if err != nil { 135 return nil, err 136 } 137 // Probably a bit overkill, but this ensures we marshal a pointer to an empty object (&empty{}) as nil 138 if !bytes.Equal(s, []byte("{}")) { 139 status = &s 140 } 141 } 142 namespace := cfg.Namespace 143 if namespace == "" { 144 namespace = metav1.NamespaceDefault 145 } 146 return &IstioKind{ 147 TypeMeta: metav1.TypeMeta{ 148 Kind: cfg.GroupVersionKind.Kind, 149 APIVersion: cfg.GroupVersionKind.Group + "/" + cfg.GroupVersionKind.Version, 150 }, 151 ObjectMeta: metav1.ObjectMeta{ 152 Name: cfg.Name, 153 Namespace: namespace, 154 ResourceVersion: cfg.ResourceVersion, 155 Labels: cfg.Labels, 156 Annotations: cfg.Annotations, 157 CreationTimestamp: metav1.NewTime(cfg.CreationTimestamp), 158 }, 159 Spec: spec, 160 Status: status, 161 }, nil 162 } 163 164 // TODO - add special cases for type-to-kind and kind-to-type 165 // conversions with initial-isms. Consider adding additional type 166 // information to the abstract model and/or elevating k8s 167 // representation to first-class type to avoid extra conversions. 168 169 func parseInputsImpl(inputs string, withValidate bool) ([]config.Config, []IstioKind, error) { 170 var varr []config.Config 171 var others []IstioKind 172 reader := bytes.NewReader([]byte(inputs)) 173 empty := IstioKind{} 174 175 // We store configs as a YaML stream; there may be more than one decoder. 176 yamlDecoder := kubeyaml.NewYAMLOrJSONDecoder(reader, 512*1024) 177 for { 178 obj := IstioKind{} 179 err := yamlDecoder.Decode(&obj) 180 if err == io.EOF { 181 break 182 } 183 if err != nil { 184 return nil, nil, fmt.Errorf("cannot parse proto message: %v", err) 185 } 186 if reflect.DeepEqual(obj, empty) { 187 continue 188 } 189 190 gvk := obj.GroupVersionKind() 191 s, exists := collections.PilotGatewayAPI().FindByGroupVersionAliasesKind(resource.FromKubernetesGVK(&gvk)) 192 if !exists { 193 log.Debugf("unrecognized type %v", obj.Kind) 194 others = append(others, obj) 195 continue 196 } 197 198 cfg, err := ConvertObject(s, &obj, "") 199 if err != nil { 200 return nil, nil, fmt.Errorf("cannot parse proto message for %v: %v", obj.Name, err) 201 } 202 203 if withValidate { 204 if _, err := s.ValidateConfig(*cfg); err != nil { 205 return nil, nil, fmt.Errorf("configuration is invalid: %v", err) 206 } 207 } 208 209 varr = append(varr, *cfg) 210 } 211 212 return varr, others, nil 213 } 214 215 // ParseInputs reads multiple documents from `kubectl` output and checks with 216 // the schema. It also returns the list of unrecognized kinds as the second 217 // response. 218 // 219 // NOTE: This function only decodes a subset of the complete k8s 220 // ObjectMeta as identified by the fields in model.Meta. This 221 // would typically only be a problem if a user dumps an configuration 222 // object with kubectl and then re-ingests it. 223 func ParseInputs(inputs string) ([]config.Config, []IstioKind, error) { 224 return parseInputsImpl(inputs, true) 225 }