Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/tree/aws/acm/acm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

type CertificateManager struct {
Certificates []Certificate
Certificates []Certificate `tree:"certificates"`
}

func (cm *CertificateManager) AddCertificateAuthorities(cas *acmpca.PCACertificateAuthority) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/tree/aws/acmpca/certificate_authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import (
type CertificateAuthority struct {
resource.Resource `tree:"-"`

UsageMode value.String
MonthlyRequests value.Int
UsageMode value.String `tree:"usage_mode"`
MonthlyRequests value.Int `tree:"monthly_requests"`
}
6 changes: 3 additions & 3 deletions pkg/tree/aws/ec2/ec2.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package ec2

type EC2 struct {
Instances []Instance `tree:"instance"`
InstanceStates []InstanceStateMapping `tree:"instance_state"`
LaunchTemplates []LaunchTemplate `tree:"launch_template"`
Instances []Instance `tree:"instances"`
InstanceStates []InstanceStateMapping `tree:"instance_states"`
LaunchTemplates []LaunchTemplate `tree:"launch_templates"`
}

func (ec2 *EC2) PostProcess() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/tree/aws/ec2/launch_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ type LaunchTemplate struct {
}

type NetworkInterface struct {
AssociatePublicIPAddress value.Bool
DeviceIndex value.Int
AssociatePublicIPAddress value.Bool `tree:"associate_public_ip_address"`
DeviceIndex value.Int `tree:"device_index"`
}
2 changes: 1 addition & 1 deletion pkg/tree/proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestFromProtoAttributes(t *testing.T) {
"ec2": {
Resources: []*prototree.Resource{
{
Type: "instance",
Type: "instances",
Attributes: &prototree.ValueObject{
Entries: map[string]*prototree.Value{
"instance_type": {Value: &prototree.Value_StringValue{StringValue: string("t3.micro")}},
Expand Down
6 changes: 3 additions & 3 deletions pkg/tree/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ type Definition struct {
type Tags []Tag

type Tag struct {
Key value.String
Value value.String
IsDefault bool
Key value.String `tree:"key"`
Value value.String `tree:"value"`
IsDefault bool `tree:"-"` // not needed for our purposes
}

func (t Tags) Get(name string) (value.String, bool) {
Expand Down
74 changes: 74 additions & 0 deletions pkg/tree/tree_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tree

import (
"fmt"
"reflect"
"testing"

"github.com/infracost/go-proto/pkg/tree/aws"
Expand Down Expand Up @@ -105,3 +107,75 @@ func TestToResourcesMixed(t *testing.T) {
assert.Equal(t, "i-111", resources[0].GetBase().ID)
assert.Equal(t, "unsupported-1", resources[1].GetBase().ID)
}

func TestAllFieldsHaveTreeTag(t *testing.T) {
var problems []string
seen := make(map[reflect.Type]bool)
checkTreeTags(reflect.TypeOf(Tree{}), "Tree", &problems, seen)
for _, p := range problems {
t.Error(p)
}
}

const treePkgPrefix = "github.com/infracost/go-proto/pkg/tree"

var valuePkgPath = reflect.TypeOf(value.Value[string]{}).PkgPath()

func isTreePackage(pkgPath string) bool {
return pkgPath == treePkgPrefix || len(pkgPath) > len(treePkgPrefix) && pkgPath[len(treePkgPrefix)] == '/'
}

func deref(typ reflect.Type) reflect.Type {
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
return typ
}

func checkTreeTags(typ reflect.Type, path string, problems *[]string, seen map[reflect.Type]bool) {
typ = deref(typ)
if typ.Kind() != reflect.Struct || seen[typ] {
return
}
seen[typ] = true

tagValues := make(map[string]string) // tag value -> field name
for i := range typ.NumField() {
field := typ.Field(i)
if !field.IsExported() {
continue
}
fieldPath := fmt.Sprintf("%s.%s", path, field.Name)

tag, hasTag := field.Tag.Lookup("tree")
if !hasTag {
*problems = append(*problems, fmt.Sprintf("missing tree tag: %s", fieldPath))
continue
}
if tag == "-" {
continue
}

// check for duplicate tag values within the same struct
if prev, exists := tagValues[tag]; exists {
*problems = append(*problems, fmt.Sprintf("duplicate tree tag %q: %s.%s and %s.%s", tag, path, prev, path, field.Name))
} else {
tagValues[tag] = field.Name
}

// resolve element type for slices/pointers
ft := deref(field.Type)
if ft.Kind() == reflect.Slice {
ft = deref(ft.Elem())
}

// stop at value package types
if ft.PkgPath() == valuePkgPath {
continue
}
// only recurse into structs within the tree package
if ft.Kind() == reflect.Struct && isTreePackage(ft.PkgPath()) {
checkTreeTags(ft, fieldPath, problems, seen)
}
}
}
Loading