package edgecliutils import ( "bytes" "encoding/json" "errors" "fmt" "net" "os" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/lib/networkvalidator" goyaml "gopkg.in/yaml.v3" "sigs.k8s.io/yaml" ) type IPVersion string const ( IPv4 IPVersion = "v4" IPv6 IPVersion = "v6" ) type IPAddress struct { Version IPVersion Address net.IP } func ParseTerminalConfigFile(filepath string) ([]*model.TerminalCreateInput, error) { contents, err := os.ReadFile(filepath) if err != nil { return nil, err } // The gqlgen-ed model types are only JSON-serializable, not YAML, so must convert // YAML to JSON first, then JSON to model types. Unfortunately, the YAML-to-JSON // conversion succeeds in all but the most pathological cases, so if an invalid // YAML input file is provided, we will most likely receive a JSON error, not a // YAML error. The libraries in question, despite being the de-facto standard // for Go, provide woeful error handling... Without being able to configure // gqlgen to allow direct YAML unmarshalling (not possible), these are the best // error messages we can provide. data, err := yaml.YAMLToJSONStrict(contents) if err != nil { errmsg := "" switch yamlError := err.(type) { case *goyaml.TypeError: errmsg = fmt.Sprintf("Failed to unmarshal field(s) in YAML input file: %v", yamlError) default: errmsg = fmt.Sprintf("Invalid YAML input file: %v", yamlError) } return nil, errors.New(errmsg) } terminals, err := decodeIntoCreateTerminalStruct(data) if err != nil { return nil, err } // Extra validation performed here as API will allow empty class, while CLI should not if err = validateTerminals(terminals); err != nil { return nil, err } return terminals, nil } func decodeIntoCreateTerminalStruct(data []byte) ([]*model.TerminalCreateInput, error) { type diskCreateDup struct { IncludeDisk *bool `json:"includeDisk"` ExpectEmpty *bool `json:"expectEmpty"` DevicePath *string `json:"devicePath"` } type terminalCreateDup struct { model.TerminalCreateInput DiskDup []*diskCreateDup `json:"disks"` } var terminalsDup []*terminalCreateDup decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() err := decoder.Decode(&terminalsDup) if err != nil { errmsg := "" switch jsonError := err.(type) { case *json.InvalidUnmarshalError: // E.g. passed null pointer errmsg = fmt.Sprintf("JSON data (converted from YAML input) not valid for unmarshalling: %v", err) case *json.SyntaxError: errmsg = fmt.Sprintf("Syntax error in JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err) case *json.UnmarshalTypeError: errmsg = fmt.Sprintf("Failed to unmarshal JSON data (converted from YAML input) at offset %v: %v", int(jsonError.Offset), err) default: errmsg = fmt.Sprintf("Encountered unknown error when unmarshalling JSON data (converted from YAML input): %v", jsonError) } return nil, errors.New(errmsg) } var terminals []*model.TerminalCreateInput for _, terminalDup := range terminalsDup { terminal := terminalDup.TerminalCreateInput disks := []*model.TerminalDiskCreateInput{} for _, diskDup := range terminalDup.DiskDup { errMsg := "cannot create a terminal disk without" if diskDup.DevicePath == nil { return nil, fmt.Errorf("%s devicePath", errMsg) } if diskDup.ExpectEmpty == nil { return nil, fmt.Errorf("%s expectEmpty", errMsg) } if diskDup.IncludeDisk == nil { return nil, fmt.Errorf("%s includeDisk", errMsg) } disk := model.TerminalDiskCreateInput{DevicePath: *diskDup.DevicePath, ExpectEmpty: *diskDup.ExpectEmpty, IncludeDisk: *diskDup.IncludeDisk} disks = append(disks, &disk) } terminal.Disks = disks terminals = append(terminals, &terminal) } return terminals, nil } func validateTerminals(terminals []*model.TerminalCreateInput) error { for idx, terminal := range terminals { if !isValidTerminalClass(terminal.Class) { return fmt.Errorf("terminal %v has invalid or missing class field - must be one of '%v' or '%v'", idx, model.TerminalClassTypeServer, model.TerminalClassTypeTouchpoint) } if terminal.Hostname != nil && networkvalidator.IsValidHostname(*terminal.Hostname) != nil { return fmt.Errorf("terminal %v has invalid hostname field", idx) } } return nil }