Skip to content

Commit

Permalink
update to metal-go and add all required fields and validations for De…
Browse files Browse the repository at this point in the history
…vice

Signed-off-by: ocobleseqx <oscar.cobles@eu.equinix.com>
  • Loading branch information
ocobles authored and cprivitere committed Jan 29, 2024
1 parent 58abbc6 commit e0da308
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 19 deletions.
35 changes: 25 additions & 10 deletions internal/outputs/terraform/device.tf.gotmpl
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 19 additions & 9 deletions internal/outputs/terraform/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / docs (1.19)

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:

Check failure on line 10 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / e2e-test

no required module provides package github.com/equinix-labs/metal-go/metal/v1; to add it:
)

var (
Expand All @@ -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:

Check failure on line 29 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / lint

previous case (typecheck)
fmt.Printf("single device")
f = deviceFormat
case []packngo.Device:
case []metal.Device:

Check failure on line 32 in internal/outputs/terraform/format.go

View workflow job for this annotation

GitHub Actions / lint

previous case (typecheck)
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
Expand All @@ -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
}
151 changes: 151 additions & 0 deletions internal/outputs/terraform/utils.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit e0da308

Please sign in to comment.