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  }