From e74db36301dd7e27d1f584549317d071fdcb6f19 Mon Sep 17 00:00:00 2001 From: ocobleseqx Date: Fri, 10 Nov 2023 20:38:16 +0100 Subject: [PATCH] update to metal-go and add all required fields and validations for Device Signed-off-by: ocobleseqx --- internal/outputs/terraform/device.tf.gotmpl | 35 +++-- internal/outputs/terraform/format.go | 28 ++-- internal/outputs/terraform/utils.go | 151 ++++++++++++++++++++ 3 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 internal/outputs/terraform/utils.go diff --git a/internal/outputs/terraform/device.tf.gotmpl b/internal/outputs/terraform/device.tf.gotmpl index 47ab07e6..f4a5c359 100644 --- a/internal/outputs/terraform/device.tf.gotmpl +++ b/internal/outputs/terraform/device.tf.gotmpl @@ -1,11 +1,26 @@ -# terraform import equinix_metal_device.{{.Hostname}} {{.ID}} -resource "equinix_metal_device" "{{.Hostname}}" { - plan = "{{.Plan.Slug}}" - hostname = "{{.Hostname}}" - billing_cycle = "{{.BillingCycle}}" - metro = "{{.Metro.Code}}" - operating_system = "{{.OS.Slug}}" - project_id = "{{.Project.ID}}" - - tags = {{.Tags}} +# terraform import equinix_metal_device.example {{.Id}} +resource "equinix_metal_device" "example" { + always_pxe = {{.AlwaysPxe}} + billing_cycle = {{.BillingCycle}} + custom_data = {{.Customdata | nullIfNilOrEmpty}} + description = {{.Description | nullIfNilOrEmpty}} + force_detach_volumes = false +{{- if .HardwareReservation }} + hardware_reservation_id = {{.HardwareReservation.Id }} +{{ else }} + hardware_reservation_id = null +{{- end }} + hostname = {{.Hostname}} + ipxe_script_url = {{.IpxeScriptUrl}} + metro = {{.Metro.Code}} + operating_system = {{.OperatingSystem.Slug}} + plan = {{.Plan.Slug}} + project_id = {{ hrefToID .Project.Href}} + project_ssh_key_ids = null + storage = {{.Storage | nullIfNilOrEmpty}} + tags = {{.Tags}} + termination_time = {{.TerminationTime | nullIfNilOrEmpty}} + user_data = {{.Userdata | nullIfNilOrEmpty}} # sensitive + user_ssh_key_ids = null + wait_for_reservation_deprovision = false } diff --git a/internal/outputs/terraform/format.go b/internal/outputs/terraform/format.go index dd2d8a18..73e3db99 100644 --- a/internal/outputs/terraform/format.go +++ b/internal/outputs/terraform/format.go @@ -3,10 +3,11 @@ package terraform import ( "bytes" _ "embed" + "fmt" + "html" "html/template" "path" - - "github.com/packethost/packngo" + metal "github.com/equinix-labs/metal-go/metal/v1" ) var ( @@ -23,21 +24,29 @@ func many(s string) string { func Marshal(i interface{}) ([]byte, error) { f := "" - switch i.(type) { - case *packngo.Device: + + switch v := i.(type) { + case *metal.Device: + fmt.Printf("single device") f = deviceFormat - case []packngo.Device: + case []metal.Device: + fmt.Printf("devices") f = many(deviceFormat) - case *packngo.Project: + case *metal.Project: f = projectFormat - case []packngo.Project: + case []metal.Project: f = many(projectFormat) + default: + return nil, fmt.Errorf("%v is not compatible with terraform output", v) } + addQuotesToString(i) + tmpl, err := template.New("terraform").Funcs(template.FuncMap{ "hrefToID": func(href string) string { - return path.Base(href) + return fmt.Sprintf("\"%s", path.Base(href)) }, + "nullIfNilOrEmpty": nullIfNilOrEmpty, }).Parse(f) if err != nil { return nil, err @@ -47,5 +56,6 @@ func Marshal(i interface{}) ([]byte, error) { if err != nil { return nil, err } - return buf.Bytes(), nil + result := html.UnescapeString(buf.String()) + return []byte(result), nil } diff --git a/internal/outputs/terraform/utils.go b/internal/outputs/terraform/utils.go new file mode 100644 index 00000000..d629c720 --- /dev/null +++ b/internal/outputs/terraform/utils.go @@ -0,0 +1,151 @@ +package terraform + +import ( + "fmt" + "reflect" + "time" +) + +func addQuotesToString(v interface{}) { + val := reflect.ValueOf(v) + + switch val.Kind() { + case reflect.Ptr: + val = val.Elem() + if val.Kind() != reflect.Struct { + return + } + + if val.Type() == reflect.TypeOf(new(string)) { + oldValue := val.Elem().String() + newValue := fmt.Sprintf(`"%s"`, oldValue) + val.Elem().SetString(newValue) + return + } + case reflect.String: + oldValue := val.String() + newValue := fmt.Sprintf(`"%s"`, oldValue) + val.SetString(newValue) + return + case reflect.Slice: + for i := 0; i < val.Len(); i++ { + elem := val.Index(i) + if elem.Kind() == reflect.Struct || (elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.Struct) { + addQuotesToString(elem.Interface()) + } + } + return + case reflect.Map: + for _, key := range val.MapKeys() { + elem := val.MapIndex(key) + if elem.Kind() == reflect.Struct || (elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.Struct) { + addQuotesToString(elem.Interface()) + } + } + return + default: + return + } + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + + switch field.Kind() { + case reflect.String: + oldValue := field.String() + newValue := fmt.Sprintf(`"%s"`, oldValue) + field.SetString(newValue) + case reflect.Ptr: + if field.IsNil() { + continue + } + // Check if the pointer is to a string + if field.Type().Elem() == reflect.TypeOf("") { + oldValue := field.Elem().String() + newValue := fmt.Sprintf(`"%s"`, oldValue) + field.Elem().SetString(newValue) + } else { + // Exclude *time.Time from recursion + if field.Type() != reflect.TypeOf(&time.Time{}) { + addQuotesToString(field.Interface()) + } + } + case reflect.Struct: + // Exclude time.Time from recursion + if field.Type() != reflect.TypeOf(time.Time{}) { + addQuotesToString(field.Interface()) + } + } + } +} + +func nullIfNilOrEmpty(v interface{}) interface{} { + if v == nil { + return "null" + } + + // Use reflection to check if the value is an empty value (e.g., empty string, empty slice, or empty map) + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.String: + if val.String() == "\"\"" { + return "null" + } + case reflect.Array, reflect.Slice, reflect.Map: + if val.Len() == 0 { + return "null" + } + case reflect.Ptr: + if val.IsNil() || val.IsZero() { + return "null" + } + + elem := val.Elem() + + // Check if it's a pointer to a string + if elem.Kind() == reflect.String && elem.String() == "\"\"" { + return "null" + } + + switch elem.Kind() { + case reflect.Struct: + if isPointerStructEmpty(elem) { + return "null" + } + case reflect.Array, reflect.Slice: + if elem.Len() == 0 { + return "null" + } + case reflect.Map: + if elem.Len() == 0 { + return "null" + } + } + } + + return v +} + +func isPointerStructEmpty(structVal reflect.Value) bool { + // Iterate through the struct fields + for i := 0; i < structVal.NumField(); i++ { + field := structVal.Field(i) + + // You can define custom logic to determine if a field is empty + // For example, check if a string field is empty, or if a slice/map field is empty + switch field.Kind() { + case reflect.String: + if field.String() != "" { + return false + } + case reflect.Slice, reflect.Map: + if field.Len() > 0 { + return false + } + // Add more cases for other field types as needed + } + } + + // All fields are empty + return true +}