github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/convert/yaml.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package convert
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"reflect"
    27  
    28  	"google.golang.org/protobuf/encoding/protojson"
    29  	"google.golang.org/protobuf/proto"
    30  	"sigs.k8s.io/yaml"
    31  
    32  	"github.com/zntrio/harp/v2/pkg/sdk/types"
    33  )
    34  
    35  var maxPayloadSize = int64(25 << 20) // 25MB
    36  
    37  // YAMLtoJSON reads a given reader in order to extract a JSON representation.
    38  func YAMLtoJSON(r io.Reader) (io.Reader, error) {
    39  	// Check arguments
    40  	if types.IsNil(r) {
    41  		return nil, fmt.Errorf("reader is nil")
    42  	}
    43  
    44  	// Drain the reader
    45  	jsonReader, err := loadFromYAML(r)
    46  	if err != nil {
    47  		return nil, fmt.Errorf("unable to parse YAML input: %w", err)
    48  	}
    49  
    50  	// No error
    51  	return jsonReader, nil
    52  }
    53  
    54  // PBtoYAML converts a protobuf object to a YAML representation.
    55  func PBtoYAML(msg proto.Message) ([]byte, error) {
    56  	// Check arguments
    57  	if types.IsNil(msg) {
    58  		return nil, fmt.Errorf("msg is nil")
    59  	}
    60  
    61  	// Encode protbuf message as JSON
    62  	pb, err := protojson.Marshal(msg)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("unable to encode protbuf message to JSON: %w", err)
    65  	}
    66  
    67  	// Decode input as JSON
    68  	var jsonObj interface{}
    69  	if errDecode := yaml.Unmarshal(pb, &jsonObj); errDecode != nil {
    70  		return nil, fmt.Errorf("unable to decode JSON input: %w", errDecode)
    71  	}
    72  
    73  	// Marshal as YAML
    74  	out, errEncode := yaml.Marshal(jsonObj)
    75  	if errEncode != nil {
    76  		return nil, fmt.Errorf("unable to produce YAML output: %w", errEncode)
    77  	}
    78  
    79  	// No error
    80  	return out, nil
    81  }
    82  
    83  // -----------------------------------------------------------------------------
    84  
    85  // loadFromYAML reads YAML definition and returns the PB struct.
    86  //
    87  // Protobuf doesn't contain YAML struct tags and json one are not symetric
    88  // to protobuf. We need to export YAML as JSON, and then read JSON to Protobuf
    89  // as done in k8s yaml loader.
    90  func loadFromYAML(r io.Reader) (io.Reader, error) {
    91  	// Check arguments
    92  	if types.IsNil(r) {
    93  		return nil, fmt.Errorf("reader is nil")
    94  	}
    95  
    96  	// Drain input reader
    97  	in, err := io.ReadAll(io.LimitReader(r, maxPayloadSize))
    98  	if err != nil && !errors.Is(err, io.EOF) {
    99  		return nil, fmt.Errorf("unable to drain input reader: %w", err)
   100  	}
   101  
   102  	// Decode as YAML any object
   103  	var specBody interface{}
   104  	if errYaml := yaml.Unmarshal(in, &specBody); errYaml != nil {
   105  		return nil, fmt.Errorf("unable to decode spec as YAML: %w", err)
   106  	}
   107  
   108  	// Convert map[interface{}]interface{} to a JSON serializable struct
   109  	specBody, err = convertMapStringInterface(specBody)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("unable to prepare spec to json transformation: %w", err)
   112  	}
   113  
   114  	// Marshal as json
   115  	jsonData, err := json.Marshal(specBody)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("unable ot marshal spec as JSON: %w", err)
   118  	}
   119  
   120  	// No error
   121  	return bytes.NewReader(jsonData), nil
   122  }
   123  
   124  // Converts map[interface{}]interface{} into map[string]interface{} for json.Marshaler.
   125  func convertMapStringInterface(val interface{}) (interface{}, error) {
   126  	switch items := val.(type) {
   127  	case map[interface{}]interface{}:
   128  		result := map[string]interface{}{}
   129  		for k, v := range items {
   130  			key, ok := k.(string)
   131  			if !ok {
   132  				return nil, fmt.Errorf("TypeError: value %s (type `%s') can't be assigned to type 'string'", k, reflect.TypeOf(k))
   133  			}
   134  			value, err := convertMapStringInterface(v)
   135  			if err != nil {
   136  				return nil, fmt.Errorf("unable to convert map[string] to map[interface{}]: %w", err)
   137  			}
   138  			result[key] = value
   139  		}
   140  		return result, nil
   141  	case []interface{}:
   142  		for k, v := range items {
   143  			value, err := convertMapStringInterface(v)
   144  			if err != nil {
   145  				return nil, fmt.Errorf("unable to convert map[string] to map[interface{}]: %w", err)
   146  			}
   147  			items[k] = value
   148  		}
   149  	}
   150  	return val, nil
   151  }