Repository: klothoplatform/klotho
Pull request #571 (2023-05-04 00:05:19)

move the "are all resources tested?" logic
This is largely a lift-and-shift, with a bit of extra logic to make sure that we check the caller's package in the new `FindAllResources` func. Most of the "interesting" logic unchanged from before.

This will let us reuse the "are all resources tested?" logic for other "leaf" plugins besides the current IaC one. In particular, we'll be able to use it in the visualizer plugin.

> **Note**:
> The list of `resources.*` resources is now in a prod-scoped file in the `resources` package. This will let us reuse it in that visualizer test.

### Standard checks

- **Unit tests**: all unit tests still pass
- **Docs**: n/a
- **Backwards compatibility**: no issues



Patch:
diff --git a/pkg/provider/aws/resources/list_all.go b/pkg/provider/aws/resources/list_all.go
new file mode 100644
index 00000000..aa154236
--- /dev/null
+++ b/pkg/provider/aws/resources/list_all.go
@@ -0,0 +1,64 @@
+package resources
+
+import (
+	"github.com/klothoplatform/klotho/pkg/core"
+	"github.com/klothoplatform/klotho/pkg/infra/kubernetes"
+)
+
+func ListAll() []core.Resource {
+	return []core.Resource{
+		&AccountId{},
+		&ApiDeployment{},
+		&ApiIntegration{},
+		&ApiMethod{},
+		&ApiResource{},
+		&ApiStage{},
+		&AvailabilityZones{},
+		&CloudfrontDistribution{},
+		&DynamodbTable{},
+		&EcrImage{},
+		&EcrRepository{},
+		&EksAddon{},
+		&EksCluster{},
+		&EksFargateProfile{},
+		&EksNodeGroup{},
+		&ElasticIp{},
+		&ElasticacheCluster{},
+		&ElasticacheSubnetgroup{},
+		&IamPolicy{},
+		&IamRole{},
+		&InternetGateway{},
+		&LambdaFunction{},
+		&LambdaPermission{},
+		&Listener{},
+		&LoadBalancer{},
+		&LogGroup{},
+		&NatGateway{},
+		&OpenIdConnectProvider{},
+		&OriginAccessIdentity{},
+		&PrivateDnsNamespace{},
+		&RdsInstance{},
+		&RdsProxyTargetGroup{},
+		&RdsProxy{},
+		&RdsSubnetGroup{},
+		&Region{},
+		&RestApi{},
+		&RouteTable{},
+		&S3BucketPolicy{},
+		&S3Bucket{},
+		&S3Object{},
+		&SecretVersion{},
+		&Secret{},
+		&SecurityGroup{},
+		&Subnet{},
+		&TargetGroup{},
+		&VpcEndpoint{},
+		&VpcLink{},
+		&Vpc{},
+		&kubernetes.HelmChart{},
+		&kubernetes.HelmChart{},
+		&kubernetes.Kubeconfig{},
+		&kubernetes.KustomizeDirectory{},
+		&kubernetes.Manifest{},
+	}
+}


Test patch:
diff --git a/pkg/core/coretesting/all_resources_list.go b/pkg/core/coretesting/all_resources_list.go
new file mode 100644
index 00000000..922280db
--- /dev/null
+++ b/pkg/core/coretesting/all_resources_list.go
@@ -0,0 +1,186 @@
+package coretesting
+
+import (
+	"fmt"
+	"go/types"
+	"reflect"
+	"runtime"
+	"strings"
+
+	"github.com/klothoplatform/klotho/pkg/core"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/tools/go/packages"
+)
+
+type (
+	Methods struct {
+		// signatures is the set of all methods declared on a type. Each signature follows the general format:
+		//
+		//	<name> func(<args>) <return type>
+		//
+		// The args do not include the receiver type. For example:
+		//
+		//	KlothoConstructRef func() []github.com/klothoplatform/klotho/pkg/core.AnnotationKey
+		signatures  map[string]struct{}
+		isInterface bool
+	}
+
+	TypeRef struct {
+		Pkg  string
+		Name string
+	}
+)
+
+// FindAllResources returns the TypeRefs for each of the resources you pass in, as well as for any other resources in
+// the same-or-descendent packages from those resources, and also any resources in the caller's package (or
+// descendents).
+//
+// For example, let's say you had:
+//
+//	└─ pkg/core/resources/
+//	   ├─ aws/
+//	   │  ├╴ResourceA
+//	   │  ├╴ResourceB
+//	   │  └─ subpackage/
+//	   │     └─ ResourceC
+//	   └─ gcp/
+//	      └─ ResourceD
+//
+// If you invoked FindAllResources with { &ResourceA{} }, this would return TypeRefs for:
+//   - ResourceA (because it was in the input)
+//   - ResourceB (because it's in the same package as A)
+//   - ResourceC (because it was in a subpackage of A)
+//   - but *not* for ResourceD
+//
+// If there are any problems, this function will return nil, but also trigger a failure on the provided
+// [assert.Assertions].
+func FindAllResources(a *assert.Assertions, from []core.Resource) []TypeRef {
+	// Find caller's package
+	pc, _, _, ok := runtime.Caller(1)
+	if !a.Truef(ok, "couldn't get caller info") {
+		return nil
+	}
+	// callerFunc is something like "github.com/klothoplatform/klotho/pkg/infra/iac2.TestKnownTemplates.func2"
+	// we want to extract "github.com/klothoplatform/klotho/pkg/infra/iac2"
+	callerFunc := runtime.FuncForPC(pc).Name()
+	callerFuncLastSlash := strings.LastIndexByte(callerFunc, '/')
+	callerFuncDotAfterLastSlash := strings.IndexByte(callerFunc[callerFuncLastSlash:], '.') + callerFuncLastSlash
+	callerPkg := callerFunc[:callerFuncDotAfterLastSlash]
+
+	// Find the methods for core.Resource
+	var t2 reflect.Type = reflect.TypeOf((*core.Resource)(nil)).Elem()
+	coreResourceRef := TypeRef{
+		Pkg:  t2.PkgPath(),
+		Name: t2.Name(),
+	}
+	coreTypes, err := getTypesInPackage(coreResourceRef.Pkg)
+	if !a.NoError(err) {
+		return nil
+	}
+	coreResourceType := coreTypes[coreResourceRef]
+	if !a.NotEmptyf(coreResourceType, `couldn't find %v!`, coreResourceRef) {
+		return nil
+	}
+
+	// Find all packages in our "from" list"
+	packageNames := make(map[string]struct{})
+	packageNames[callerPkg] = struct{}{}
+	for _, res := range from {
+		resType := reflect.TypeOf(res)
+		for resType.Kind() == reflect.Pointer {
+			resType = resType.Elem()
+		}
+		packageNames[resType.PkgPath()] = struct{}{}
+	}
+
+	// Within those packages, find all structs that implement core.Resource
+
+	// allTypesRef is basically a cache: for each TypeRef we see, is it a core.Resource?
+	// We do this because we look for packages using `/...`, so it's very possible that we check a package and then its
+	// sibling; this approach lets us avoid dupes. The concern isn't so much unnecessary compute, but that we don't want
+	// to return dupes to the caller.
+	allTypeRefs := make(map[TypeRef]bool)
+	for pkg := range packageNames {
+		resourcesTypes, err := getTypesInPackage(pkg + `/...`)
+		if !a.NoError(err) {
+			return nil
+		}
+		for ref, methods := range resourcesTypes {
+			if _, alreadyThere := allTypeRefs[ref]; alreadyThere {
+				continue
+			}
+			// Ignore all interfaces, and all structs/typedefs that don't implement core.Resource
+			allTypeRefs[ref] = (!methods.isInterface) && methods.containsAllMethodsIn(coreResourceType)
+		}
+	}
+
+	results := make([]TypeRef, 0, len(allTypeRefs))
+	for ref, isResource := range allTypeRefs {
+		if isResource {
+			results = append(results, ref)
+		}
+	}
+	return results
+}
+
+// getTypesInPackage finds all types within a package (which may be "..."-ed).
+func getTypesInPackage(packageName string) (map[TypeRef]Methods, error) {
+	config := &packages.Config{Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo}
+	pkgs, err := packages.Load(config, packageName)
+	if err != nil {
+		return nil, err
+	}
+	result := make(map[TypeRef]Methods)
+	for _, pkg := range pkgs {
+		for _, obj := range pkg.TypesInfo.Defs {
+			if obj == nil {
+				continue
+			}
+			if _, ok := obj.(*types.TypeName); !ok {
+				continue
+			}
+			typ, ok := obj.Type().(*types.Named)
+			if !ok {
+				continue
+			}
+			key := TypeRef{
+				Pkg:  pkg.PkgPath,
+				Name: obj.Name(),
+			}
+			result[key] = getMethods(typ)
+		}
+	}
+	if len(result) == 0 {
+		return nil, errors.Errorf(`couldn't find any packages in %s`, packageName)
+	}
+	return result, nil
+}
+
+func getMethods(t *types.Named) Methods {
+	type hasMethods interface {
+		NumMethods() int
+		Method(int) *types.Func
+	}
+	result := Methods{}
+	var tMethods hasMethods = t
+	if underlyingInterface, ok := t.Underlying().(*types.Interface); ok {
+		tMethods = underlyingInterface
+		result.isInterface = true
+	}
+	result.signatures = make(map[string]struct{}, tMethods.NumMethods())
+	for i := 0; i < tMethods.NumMethods(); i++ {
+		method := tMethods.Method(i)
+		result.signatures[fmt.Sprintf(`%s %s`, method.Name(), method.Type().String())] = struct{}{}
+	}
+	return result
+}
+
+func (m Methods) containsAllMethodsIn(other Methods) bool {
+	for sig := range other.signatures {
+		if _, exists := m.signatures[sig]; !exists {
+			return false
+		}
+	}
+	return true
+}
diff --git a/pkg/infra/iac2/inputs_outputs_test.go b/pkg/infra/iac2/inputs_outputs_test.go
index 2fd1118c..9ef40886 100644
--- a/pkg/infra/iac2/inputs_outputs_test.go
+++ b/pkg/infra/iac2/inputs_outputs_test.go
@@ -1,38 +1,15 @@
 package iac2
 
 import (
-	"fmt"
-	"go/types"
 	"io/fs"
 	"reflect"
 	"strings"
 	"testing"
 
 	"github.com/klothoplatform/klotho/pkg/core"
-	"github.com/klothoplatform/klotho/pkg/infra/kubernetes"
+	"github.com/klothoplatform/klotho/pkg/core/coretesting"
 	"github.com/klothoplatform/klotho/pkg/provider/aws/resources"
-	"github.com/pkg/errors"
 	assert "github.com/stretchr/testify/assert"
-	"golang.org/x/tools/go/packages"
-)
-
-type (
-	Methods struct {
-		// signatures is the set of all methods declared on a type. Each signature follows the general format:
-		//
-		//	<name> func(<args>) <return type>
-		//
-		// The args do not include the receiver type. For example:
-		//
-		//	KlothoConstructRef func() []github.com/klothoplatform/klotho/pkg/core.AnnotationKey
-		signatures  map[string]struct{}
-		isInterface bool
-	}
-
-	TypeRef struct {
-		pkg  string
-		name string
-	}
 )
 
 // TestKnownTemplates performs several tests to make sure that our Go structs match up with the factory.ts templates.
@@ -55,74 +32,22 @@ type (
 // we use the reflective [packages.Load] to find all the types within that package, and then filter down to those types
 // that conform to core.Resource. Then we simply check that each one of those is in the list of types we checked.
 func TestKnownTemplates(t *testing.T) {
-	var allResources = []core.Resource{
-		&resources.Region{},
-		&resources.Vpc{},
-		&resources.VpcEndpoint{},
-		&kubernetes.HelmChart{},
-		&resources.LambdaFunction{},
-		&resources.EcrImage{},
-		&resources.LogGroup{},
-		&resources.ElasticIp{},
-		&resources.EksCluster{},
-		&resources.EksNodeGroup{},
-		&resources.EksFargateProfile{},
-		&resources.EksAddon{},
-		&resources.NatGateway{},
-		&resources.Subnet{},
-		&resources.InternetGateway{},
-		&resources.IamRole{},
-		&resources.IamPolicy{},
-		&resources.EcrRepository{},
-		&resources.S3Bucket{},
-		&resources.S3Object{},
-		&resources.DynamodbTable{},
-		&resources.Secret{},
-		&resources.SecretVersion{},
-		&resources.VpcLink{},
-		&resources.RestApi{},
-		&resources.ApiDeployment{},
-		&resources.ApiIntegration{},
-		&resources.ApiMethod{},
-		&resources.ApiResource{},
-		&resources.ApiStage{},
-		&resources.LambdaPermission{},
-		&resources.ElasticacheCluster{},
-		&resources.ElasticacheSubnetgroup{},
-		&resources.SecurityGroup{},
-		&resources.AvailabilityZones{},
-		&resources.AccountId{},
-		&resources.CloudfrontDistribution{},
-		&resources.OriginAccessIdentity{},
-		&resources.S3BucketPolicy{},
-		&kubernetes.HelmChart{},
-		&kubernetes.Kubeconfig{},
-		&resources.LoadBalancer{},
-		&resources.TargetGroup{},
-		&resources.Listener{},
-		&resources.RdsInstance{},
-		&resources.RdsProxy{},
-		&resources.RdsSubnetGroup{},
-		&resources.RdsProxyTargetGroup{},
-		&kubernetes.Manifest{},
+	allResources := resources.ListAll()
+	allResources = append(allResources,
 		&KubernetesProvider{},
 		&RolePolicyAttachment{},
-		&resources.PrivateDnsNamespace{},
-		&kubernetes.KustomizeDirectory{},
-		&resources.OpenIdConnectProvider{},
 		&RouteTableAssociation{},
-		&resources.RouteTable{},
 		&SecurityGroupRule{},
-	}
+	)
 
 	tp := standardTemplatesProvider()
-	testedTypes := make(map[TypeRef]struct{})
+	testedTypes := make(map[coretesting.TypeRef]struct{})
 	for _, res := range allResources {
 		resType := reflect.TypeOf(res)
 		for resType.Kind() == reflect.Pointer {
 			resType = resType.Elem()
 		}
-		testedTypes[TypeRef{pkg: resType.PkgPath(), name: resType.Name()}] = struct{}{}
+		testedTypes[coretesting.TypeRef{Pkg: resType.PkgPath(), Name: resType.Name()}] = struct{}{}
 		t.Run(resType.String(), func(t *testing.T) {
 			var tmpl ResourceCreationTemplate
 
@@ -178,34 +103,8 @@ func TestKnownTemplates(t *testing.T) {
 		})
 	}
 	t.Run("all types tested", func(t *testing.T) {
-		a := assert.New(t)
-
-		// Find the methods for core.Resource
-		var t2 reflect.Type = reflect.TypeOf((*core.Resource)(nil)).Elem()
-		coreResourceRef := TypeRef{
-			pkg:  t2.PkgPath(),
-			name: t2.Name(),
-		}
-		coreTypes, err := getTypesInPackage(coreResourceRef.pkg)
-		if !a.NoError(err) {
-			return
-		}
-		coreResourceType := coreTypes[coreResourceRef]
-		if !a.NotEmptyf(coreResourceType, `couldn't find %v!`, coreResourceRef) {
-			return
-		}
-
-		// Find all structs that implement core.Resource
-		resourcesTypes, err := getTypesInPackage("github.com/klothoplatform/klotho/pkg/provider/aws/...")
-		if !a.NoError(err) {
-			return
-		}
-		for ref, methods := range resourcesTypes {
-			// Ignore all interfaces, and all structs/typedefs that don't implement core.Resource
-			if methods.isInterface || !methods.containsAllMethodsIn(coreResourceType) {
-				continue
-			}
-			t.Run(ref.name, func(t *testing.T) {
+		for _, ref := range coretesting.FindAllResources(assert.New(t), allResources) {
+			t.Run(ref.Name, func(t *testing.T) {
 				assert := assert.New(t)
 				assert.Contains(
 					testedTypes,
@@ -292,64 +191,3 @@ func buildExpectedTsType(out *strings.Builder, tp *templatesProvider, t reflect.
 	}
 	return nil
 }
-
-// getTypesInPackage finds all types within a package (which may be "..."-ed).
-func getTypesInPackage(packageName string) (map[TypeRef]Methods, error) {
-	config := &packages.Config{Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo}
-	pkgs, err := packages.Load(config, packageName)
-	if err != nil {
-		return nil, err
-	}
-	result := make(map[TypeRef]Methods)
-	for _, pkg := range pkgs {
-		for _, obj := range pkg.TypesInfo.Defs {
-			if obj == nil {
-				continue
-			}
-			if _, ok := obj.(*types.TypeName); !ok {
-				continue
-			}
-			typ, ok := obj.Type().(*types.Named)
-			if !ok {
-				continue
-			}
-			key := TypeRef{
-				pkg:  pkg.PkgPath,
-				name: obj.Name(),
-			}
-			result[key] = getMethods(typ)
-		}
-	}
-	if len(result) == 0 {
-		return nil, errors.Errorf(`couldn't find any packages in %s`, packageName)
-	}
-	return result, nil
-}
-
-func getMethods(t *types.Named) Methods {
-	type hasMethods interface {
-		NumMethods() int
-		Method(int) *types.Func
-	}
-	result := Methods{}
-	var tMethods hasMethods = t
-	if underlyingInterface, ok := t.Underlying().(*types.Interface); ok {
-		tMethods = underlyingInterface
-		result.isInterface = true
-	}
-	result.signatures = make(map[string]struct{}, tMethods.NumMethods())
-	for i := 0; i < tMethods.NumMethods(); i++ {
-		method := tMethods.Method(i)
-		result.signatures[fmt.Sprintf(`%s %s`, method.Name(), method.Type().String())] = struct{}{}
-	}
-	return result
-}
-
-func (m Methods) containsAllMethodsIn(other Methods) bool {
-	for sig := range other.signatures {
-		if _, exists := m.signatures[sig]; !exists {
-			return false
-		}
-	}
-	return true
-}