...

Source file src/edge-infra.dev/pkg/edge/edgecli/flagutil/bulkTerminalParser.go

Documentation: edge-infra.dev/pkg/edge/edgecli/flagutil

     1  package flagutil
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  
    11  	"edge-infra.dev/pkg/edge/api/graph/model"
    12  	"edge-infra.dev/pkg/lib/networkvalidator"
    13  
    14  	goyaml "gopkg.in/yaml.v3"
    15  	"sigs.k8s.io/yaml"
    16  )
    17  
    18  type IPVersion string
    19  
    20  const (
    21  	IPv4 IPVersion = "v4"
    22  	IPv6 IPVersion = "v6"
    23  )
    24  
    25  type IPAddress struct {
    26  	Version IPVersion
    27  	Address net.IP
    28  }
    29  
    30  func ParseTerminalConfigFile(filepath string) ([]*model.TerminalCreateInput, error) {
    31  	contents, err := os.ReadFile(filepath)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  
    36  	// The gqlgen-ed model types are only JSON-serializable, not YAML, so must convert
    37  	// YAML to JSON first, then JSON to model types. Unfortunately, the YAML-to-JSON
    38  	// conversion succeeds in all but the most pathological cases, so if an invalid
    39  	// YAML input file is provided, we will most likely receive a JSON error, not a
    40  	// YAML error. The libraries in question, despite being the de-facto standard
    41  	// for Go, provide woeful error handling... Without being able to configure
    42  	// gqlgen to allow direct YAML unmarshalling (not possible), these are the best
    43  	// error messages we can provide.
    44  	data, err := yaml.YAMLToJSONStrict(contents)
    45  	if err != nil {
    46  		errmsg := ""
    47  		switch yamlError := err.(type) {
    48  		case *goyaml.TypeError:
    49  			errmsg = fmt.Sprintf("Failed to unmarshal field(s) in YAML input file: %v", yamlError)
    50  		default:
    51  			errmsg = fmt.Sprintf("Invalid YAML input file: %v", yamlError)
    52  		}
    53  		return nil, errors.New(errmsg)
    54  	}
    55  
    56  	terminals, err := decodeIntoCreateTerminalStruct(data)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	// Extra validation performed here as API will allow empty class, while CLI should not
    62  	if err = validateTerminals(terminals); err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	return terminals, nil
    67  }
    68  
    69  func decodeIntoCreateTerminalStruct(data []byte) ([]*model.TerminalCreateInput, error) {
    70  	type diskCreateDup struct {
    71  		IncludeDisk *bool   `json:"includeDisk"`
    72  		ExpectEmpty *bool   `json:"expectEmpty"`
    73  		DevicePath  *string `json:"devicePath"`
    74  		UsePart     *bool   `json:"usePart"`
    75  	}
    76  	type terminalCreateDup struct {
    77  		model.TerminalCreateInput
    78  		DiskDup []*diskCreateDup `json:"disks"`
    79  	}
    80  	var terminalsDup []*terminalCreateDup
    81  
    82  	decoder := json.NewDecoder(bytes.NewReader(data))
    83  	decoder.DisallowUnknownFields()
    84  
    85  	err := decoder.Decode(&terminalsDup)
    86  	if err != nil {
    87  		errmsg := ""
    88  		switch jsonError := err.(type) {
    89  		case *json.InvalidUnmarshalError:
    90  			// E.g. passed null pointer
    91  			errmsg = fmt.Sprintf("JSON data (converted from YAML input) not valid for unmarshalling: %v", err)
    92  		case *json.SyntaxError:
    93  			errmsg = fmt.Sprintf("Syntax error in JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err)
    94  		case *json.UnmarshalTypeError:
    95  			errmsg = fmt.Sprintf("Failed to unmarshal JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err)
    96  		default:
    97  			errmsg = fmt.Sprintf("Encountered unknown error when unmarshalling JSON data (converted from YAML input): %v", jsonError)
    98  		}
    99  		return nil, errors.New(errmsg)
   100  	}
   101  
   102  	var terminals []*model.TerminalCreateInput
   103  	for _, terminalDup := range terminalsDup {
   104  		terminal := terminalDup.TerminalCreateInput
   105  
   106  		disks := []*model.TerminalDiskCreateInput{}
   107  		for _, diskDup := range terminalDup.DiskDup {
   108  			if diskDup.DevicePath == nil {
   109  				return nil, fmt.Errorf("cannot create a terminal disk without devicePath")
   110  			}
   111  			if diskDup.IncludeDisk == nil {
   112  				diskDup.IncludeDisk = func(b bool) *bool { return &b }(false)
   113  			}
   114  			if diskDup.ExpectEmpty == nil {
   115  				diskDup.ExpectEmpty = func(b bool) *bool { return &b }(true)
   116  			}
   117  			if diskDup.UsePart == nil {
   118  				diskDup.UsePart = func(b bool) *bool { return &b }(false)
   119  			}
   120  
   121  			disk := model.TerminalDiskCreateInput{DevicePath: *diskDup.DevicePath, ExpectEmpty: *diskDup.ExpectEmpty, IncludeDisk: *diskDup.IncludeDisk, UsePart: *diskDup.UsePart}
   122  			disks = append(disks, &disk)
   123  		}
   124  
   125  		terminal.Disks = disks
   126  		terminals = append(terminals, &terminal)
   127  	}
   128  
   129  	return terminals, nil
   130  }
   131  
   132  func validateTerminals(terminals []*model.TerminalCreateInput) error {
   133  	for idx, terminal := range terminals {
   134  		if !isValidTerminalClass(terminal.Class) {
   135  			return fmt.Errorf("terminal %v has invalid or missing class field - must be one of '%v' or '%v'", idx, model.TerminalClassTypeServer, model.TerminalClassTypeTouchpoint)
   136  		}
   137  		if terminal.Hostname != nil && networkvalidator.IsValidHostname(*terminal.Hostname) != nil {
   138  			return fmt.Errorf("terminal %v has invalid hostname field", idx)
   139  		}
   140  	}
   141  	return nil
   142  }
   143  

View as plain text