...

Source file src/edge-infra.dev/pkg/edge/edgeadmin/edgecliutils/bulkTerminalParser.go

Documentation: edge-infra.dev/pkg/edge/edgeadmin/edgecliutils

     1  package edgecliutils
     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  	}
    75  	type terminalCreateDup struct {
    76  		model.TerminalCreateInput
    77  		DiskDup []*diskCreateDup `json:"disks"`
    78  	}
    79  	var terminalsDup []*terminalCreateDup
    80  
    81  	decoder := json.NewDecoder(bytes.NewReader(data))
    82  	decoder.DisallowUnknownFields()
    83  
    84  	err := decoder.Decode(&terminalsDup)
    85  	if err != nil {
    86  		errmsg := ""
    87  		switch jsonError := err.(type) {
    88  		case *json.InvalidUnmarshalError:
    89  			// E.g. passed null pointer
    90  			errmsg = fmt.Sprintf("JSON data (converted from YAML input) not valid for unmarshalling: %v", err)
    91  		case *json.SyntaxError:
    92  			errmsg = fmt.Sprintf("Syntax error in JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err)
    93  		case *json.UnmarshalTypeError:
    94  			errmsg = fmt.Sprintf("Failed to unmarshal JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err)
    95  		default:
    96  			errmsg = fmt.Sprintf("Encountered unknown error when unmarshalling JSON data (converted from YAML input): %v", jsonError)
    97  		}
    98  		return nil, errors.New(errmsg)
    99  	}
   100  
   101  	var terminals []*model.TerminalCreateInput
   102  	for _, terminalDup := range terminalsDup {
   103  		terminal := terminalDup.TerminalCreateInput
   104  
   105  		disks := []*model.TerminalDiskCreateInput{}
   106  		for _, diskDup := range terminalDup.DiskDup {
   107  			errMsg := "cannot create a terminal disk without"
   108  			if diskDup.DevicePath == nil {
   109  				return nil, fmt.Errorf("%s devicePath", errMsg)
   110  			}
   111  			if diskDup.ExpectEmpty == nil {
   112  				return nil, fmt.Errorf("%s expectEmpty", errMsg)
   113  			}
   114  			if diskDup.IncludeDisk == nil {
   115  				return nil, fmt.Errorf("%s includeDisk", errMsg)
   116  			}
   117  
   118  			disk := model.TerminalDiskCreateInput{DevicePath: *diskDup.DevicePath, ExpectEmpty: *diskDup.ExpectEmpty, IncludeDisk: *diskDup.IncludeDisk}
   119  			disks = append(disks, &disk)
   120  		}
   121  
   122  		terminal.Disks = disks
   123  		terminals = append(terminals, &terminal)
   124  	}
   125  
   126  	return terminals, nil
   127  }
   128  
   129  func validateTerminals(terminals []*model.TerminalCreateInput) error {
   130  	for idx, terminal := range terminals {
   131  		if !isValidTerminalClass(terminal.Class) {
   132  			return fmt.Errorf("terminal %v has invalid or missing class field - must be one of '%v' or '%v'", idx, model.TerminalClassTypeServer, model.TerminalClassTypeTouchpoint)
   133  		}
   134  		if terminal.Hostname != nil && networkvalidator.IsValidHostname(*terminal.Hostname) != nil {
   135  			return fmt.Errorf("terminal %v has invalid hostname field", idx)
   136  		}
   137  	}
   138  	return nil
   139  }
   140  

View as plain text