From 8d831eb92219e3d4e3792cde01f7ba048641a865 Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Fri, 29 May 2026 10:10:50 +0200 Subject: [PATCH 1/8] feat: support new api Signed-off-by: Florian Fa Richter --- cmd/angryjet/main.go | 84 ++++++++++++++++++++++++++++++++ internal/fields/fields.go | 16 ++++-- internal/match/match.go | 37 ++++++++++++++ internal/method/resolver_test.go | 2 + 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/cmd/angryjet/main.go b/cmd/angryjet/main.go index 1f076bb..f3dd44f 100644 --- a/cmd/angryjet/main.go +++ b/cmd/angryjet/main.go @@ -53,6 +53,9 @@ const ( RuntimeAlias = "xpv1" RuntimeImport = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + RuntimeV2Alias = "v2" + RuntimeV2Import = "github.com/crossplane/crossplane/apis/v2/core/v2" + ResourceAlias = "resource" ResourceImport = "github.com/crossplane/crossplane-runtime/v2/pkg/resource" @@ -92,8 +95,10 @@ func main() { } kingpin.FatalIfError(GenerateManagedLegacy(*filenameManaged, header, p), "cannot write managed resource method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedModern(*filenameManaged, header, p), "cannot write managed resource method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateManagedCluster(*filenameManaged, header, p), "cannot write cluster managed resource method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedListLegacy(*filenameManagedList, header, p), "cannot write managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedListModern(*filenameManagedList, header, p), "cannot write managed resource list method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateManagedClusterList(*filenameManagedList, header, p), "cannot write cluster managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfig(*filenamePC, header, p), "cannot write provider config method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageLegacy(*filenamePCU, header, p), "cannot write provider config usage method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageModern(*filenamePCU, header, p), "cannot write provider config usage method set for package %s", p.PkgPath) @@ -101,6 +106,7 @@ func main() { kingpin.FatalIfError(GenerateProviderConfigUsageListModern(*filenamePCUList, header, p), "cannot write provider config usage list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateReferencesLegacy(*filenameResolvers, header, p), "cannot write reference resolvers for package %s", p.PkgPath) kingpin.FatalIfError(GenerateReferencesModern(*filenameResolvers, header, p), "cannot write reference resolvers for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateReferencesCluster(*filenameResolvers, header, p), "cannot write cluster reference resolvers for package %s", p.PkgPath) } } @@ -166,6 +172,38 @@ func GenerateManagedModern(filename, header string, p *packages.Package) error { return errors.Wrap(err, "cannot write V2 managed resource methods") } +// GenerateManagedCluster generates the resource.Managed method set for cluster-scoped MRs. +func GenerateManagedCluster(filename, header string, p *packages.Package) error { + receiver := "mg" + + methods := method.Set{ + "SetConditions": method.NewSetConditions(receiver, RuntimeV2Import), + "GetCondition": method.NewGetCondition(receiver, RuntimeV2Import), + "GetProviderConfigReference": method.NewGetProviderConfigReference(receiver, RuntimeV2Import), + "SetProviderConfigReference": method.NewSetProviderConfigReference(receiver, RuntimeV2Import), + "SetWriteConnectionSecretToReference": method.NewSetWriteConnectionSecretToReference(receiver, RuntimeV2Import), + "GetWriteConnectionSecretToReference": method.NewGetWriteConnectionSecretToReference(receiver, RuntimeV2Import), + "SetManagementPolicies": method.NewSetManagementPolicies(receiver, RuntimeV2Import), + "GetManagementPolicies": method.NewGetManagementPolicies(receiver, RuntimeV2Import), + "SetDeletionPolicy": method.NewSetDeletionPolicy(receiver, RuntimeV2Import), + "GetDeletionPolicy": method.NewGetDeletionPolicy(receiver, RuntimeV2Import), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + CoreImport: CoreAlias, + RuntimeV2Import: RuntimeV2Alias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedCluster(), + match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write cluster managed resource methods") +} + // GenerateManagedListLegacy generates the resource.ManagedList method set. func GenerateManagedListLegacy(filename, header string, p *packages.Package) error { receiver := "l" @@ -210,6 +248,28 @@ func GenerateManagedListModern(filename, header string, p *packages.Package) err return errors.Wrap(err, "cannot write V2 managed resource list methods") } +// GenerateManagedClusterList generates the resource.ManagedList method set for cluster-scoped MRs. +func GenerateManagedClusterList(filename, header string, p *packages.Package) error { + receiver := "l" + + methods := method.Set{ + "GetItems": method.NewManagedGetItems(receiver, ResourceImport), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + ResourceImport: ResourceAlias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedClusterList(), + match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write cluster managed resource list methods") +} + // GenerateProviderConfig generates the resource.ProviderConfig method set. func GenerateProviderConfig(filename, header string, p *packages.Package) error { receiver := "p" @@ -369,3 +429,27 @@ func GenerateReferencesModern(filename, header string, p *packages.Package) erro return errors.Wrap(err, "cannot write V2 reference resolver methods") } + +// GenerateReferencesCluster generates reference resolver calls for cluster-scoped MRs. +func GenerateReferencesCluster(filename, header string, p *packages.Package) error { + receiver := "mg" + comm := comments.In(p) + + methods := method.Set{ + "ResolveReferences": method.NewResolveReferences(types.NewTraverser(comm), receiver, ClientImport, ReferenceImport), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + ClientImport: ClientAlias, + ReferenceImport: ReferenceAlias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedCluster(), + match.DoesNotHaveMarker(comm, DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write cluster reference resolver methods") +} diff --git a/internal/fields/fields.go b/internal/fields/fields.go index 6638722..0ee9cfe 100644 --- a/internal/fields/fields.go +++ b/internal/fields/fields.go @@ -37,8 +37,9 @@ const ( NameProviderConfigUsage = "ProviderConfigUsage" NameItems = "Items" - NameTypedProviderConfigUsage = "TypedProviderConfigUsage" - NameResourceV2Spec = "ManagedResourceSpec" + NameTypedProviderConfigUsage = "TypedProviderConfigUsage" + NameResourceV2Spec = "ManagedResourceSpec" + NameClusterManagedResourceSpec = "ClusterManagedResourceSpec" ) // Field type suffixes. @@ -55,8 +56,9 @@ const ( TypeSuffixProviderConfigStatus = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigStatus" TypeSuffixProviderConfigUsage = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigUsage" - TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.TypedProviderConfigUsage" - TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" + TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.TypedProviderConfigUsage" + TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" + TypeSuffixClusterManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ClusterManagedResourceSpec" ) func matches(s *types.Struct, m Matcher) bool { @@ -198,6 +200,12 @@ func IsResourceSpec() Matcher { return IsTypeNamed(TypeSuffixResourceSpec, NameR // appears to be a Crossplane managed resource spec. func IsResourceV2Spec() Matcher { return IsTypeNamed(TypeSuffixResourceV2Spec, NameResourceV2Spec) } +// IsClusterManagedResourceSpec returns a Matcher that returns true if the supplied field +// appears to be a Crossplane cluster-scoped managed resource spec. +func IsClusterManagedResourceSpec() Matcher { + return IsTypeNamed(TypeSuffixClusterManagedResourceSpec, NameClusterManagedResourceSpec) +} + // IsResourceStatus returns a Matcher that returns true if the supplied field // appears to be a Crossplane managed resource status. func IsResourceStatus() Matcher { return IsTypeNamed(TypeSuffixResourceStatus, NameResourceStatus) } diff --git a/internal/match/match.go b/internal/match/match.go index 758767a..70e3d39 100644 --- a/internal/match/match.go +++ b/internal/match/match.go @@ -102,6 +102,43 @@ func ManagedListV2() Object { } } +// ManagedCluster returns an Object matcher that returns true if the supplied Object is +// a cluster-scoped Crossplane managed resource using ClusterManagedResourceSpec. +func ManagedCluster() Object { + return func(o types.Object) bool { + return fields.Has(o, + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsObjectMeta().And(fields.IsEmbedded()), + fields.IsSpec().And(fields.HasFieldThat( + fields.IsClusterManagedResourceSpec().And(fields.IsEmbedded()), + )), + fields.IsStatus().And(fields.HasFieldThat( + fields.IsResourceStatus().And(fields.IsEmbedded()), + )), + ) + } +} + +// ManagedClusterList returns an Object matcher that returns true if the supplied +// Object is a list of cluster-scoped Crossplane managed resources. +func ManagedClusterList() Object { + return func(o types.Object) bool { + return fields.Has(o, + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsItems().And(fields.IsSlice()).And(fields.HasFieldThat( + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsObjectMeta().And(fields.IsEmbedded()), + fields.IsSpec().And(fields.HasFieldThat( + fields.IsClusterManagedResourceSpec().And(fields.IsEmbedded()), + )), + fields.IsStatus().And(fields.HasFieldThat( + fields.IsResourceStatus().And(fields.IsEmbedded()), + )), + )), + ) + } +} + // ProviderConfig returns an Object matcher that returns true if the supplied // Object is a Crossplane ProviderConfig. func ProviderConfig() Object { diff --git a/internal/method/resolver_test.go b/internal/method/resolver_test.go index 42b9417..0eee38d 100644 --- a/internal/method/resolver_test.go +++ b/internal/method/resolver_test.go @@ -89,6 +89,8 @@ type OtherSpec struct { OtherID string } +type Configuration struct {} + type ModelSpec struct { ForProvider ModelParameters } From 549a9dce2cf50b0d1fd0b1d0b5dc92e79af13b39 Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Fri, 29 May 2026 10:34:03 +0200 Subject: [PATCH 2/8] fix: support new resource path Signed-off-by: Florian Fa Richter --- internal/fields/fields.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/fields/fields.go b/internal/fields/fields.go index 0ee9cfe..8dd2d56 100644 --- a/internal/fields/fields.go +++ b/internal/fields/fields.go @@ -56,9 +56,10 @@ const ( TypeSuffixProviderConfigStatus = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigStatus" TypeSuffixProviderConfigUsage = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigUsage" - TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.TypedProviderConfigUsage" - TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" - TypeSuffixClusterManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ClusterManagedResourceSpec" + TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.TypedProviderConfigUsage" + TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" + TypeSuffixNamespacedManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ManagedResourceSpec" + TypeSuffixClusterManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ClusterManagedResourceSpec" ) func matches(s *types.Struct, m Matcher) bool { @@ -197,8 +198,14 @@ func IsStatus() Matcher { return IsTypeNamed(NameStatus, TypeSuffixStatus) } func IsResourceSpec() Matcher { return IsTypeNamed(TypeSuffixResourceSpec, NameResourceSpec) } // IsResourceV2Spec returns a Matcher that returns true if the supplied field -// appears to be a Crossplane managed resource spec. -func IsResourceV2Spec() Matcher { return IsTypeNamed(TypeSuffixResourceV2Spec, NameResourceV2Spec) } +// appears to be a Crossplane managed resource spec. It matches both the +// crossplane-runtime and crossplane core ManagedResourceSpec types. +func IsResourceV2Spec() Matcher { + return func(f *types.Var) bool { + return IsTypeNamed(TypeSuffixResourceV2Spec, NameResourceV2Spec)(f) || + IsTypeNamed(TypeSuffixNamespacedManagedResourceSpec, NameResourceV2Spec)(f) + } +} // IsClusterManagedResourceSpec returns a Matcher that returns true if the supplied field // appears to be a Crossplane cluster-scoped managed resource spec. From 07006b760a348e70b3e44df6a49c46bece428d2f Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Fri, 29 May 2026 10:39:32 +0200 Subject: [PATCH 3/8] fix: use new ManagedResourceStatus Signed-off-by: Florian Fa Richter --- internal/fields/fields.go | 16 ++++++++++++++++ internal/match/match.go | 8 ++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/fields/fields.go b/internal/fields/fields.go index 8dd2d56..838f7b7 100644 --- a/internal/fields/fields.go +++ b/internal/fields/fields.go @@ -40,6 +40,7 @@ const ( NameTypedProviderConfigUsage = "TypedProviderConfigUsage" NameResourceV2Spec = "ManagedResourceSpec" NameClusterManagedResourceSpec = "ClusterManagedResourceSpec" + NameManagedResourceStatus = "ManagedResourceStatus" ) // Field type suffixes. @@ -60,6 +61,7 @@ const ( TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" TypeSuffixNamespacedManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ManagedResourceSpec" TypeSuffixClusterManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ClusterManagedResourceSpec" + TypeSuffixManagedResourceStatus = "github.com/crossplane/crossplane/apis/v2/core/v2.ManagedResourceStatus" ) func matches(s *types.Struct, m Matcher) bool { @@ -82,6 +84,14 @@ func (o Matcher) And(m Matcher) Matcher { } } +// Or returns a Matcher that returns true if either the original Matcher o or +// the new Matcher m matches. +func (o Matcher) Or(m Matcher) Matcher { + return func(f *types.Var) bool { + return o(f) || m(f) + } +} + // Has returns true if the supplied Object's underlying type is struct (or a // slice or map of struct), and it matches all of the supplied field Matchers. func Has(o types.Object, m ...Matcher) bool { @@ -217,6 +227,12 @@ func IsClusterManagedResourceSpec() Matcher { // appears to be a Crossplane managed resource status. func IsResourceStatus() Matcher { return IsTypeNamed(TypeSuffixResourceStatus, NameResourceStatus) } +// IsManagedResourceStatus returns a Matcher that returns true if the supplied +// field appears to be a Crossplane managed resource status from the core API. +func IsManagedResourceStatus() Matcher { + return IsTypeNamed(TypeSuffixManagedResourceStatus, NameManagedResourceStatus) +} + // IsProviderConfigSpec returns a Matcher that returns true if the supplied // field appears to be a Crossplane provider config spec. func IsProviderConfigSpec() Matcher { diff --git a/internal/match/match.go b/internal/match/match.go index 70e3d39..7ebf907 100644 --- a/internal/match/match.go +++ b/internal/match/match.go @@ -56,7 +56,7 @@ func ManagedV2() Object { fields.IsResourceV2Spec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().And(fields.IsEmbedded()), + fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), )), ) } @@ -95,7 +95,7 @@ func ManagedListV2() Object { fields.IsResourceV2Spec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().And(fields.IsEmbedded()), + fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), )), )), ) @@ -113,7 +113,7 @@ func ManagedCluster() Object { fields.IsClusterManagedResourceSpec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().And(fields.IsEmbedded()), + fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), )), ) } @@ -132,7 +132,7 @@ func ManagedClusterList() Object { fields.IsClusterManagedResourceSpec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().And(fields.IsEmbedded()), + fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), )), )), ) From 986329f6745f5002a8843789420c7c7d175611dd Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Fri, 29 May 2026 10:46:34 +0200 Subject: [PATCH 4/8] fix: separate output for new resource import Signed-off-by: Florian Fa Richter --- cmd/angryjet/main.go | 82 +++++++++++++++++++++++++++++++++++++++ internal/fields/fields.go | 14 +++---- internal/match/match.go | 45 +++++++++++++++++++-- 3 files changed, 130 insertions(+), 11 deletions(-) diff --git a/cmd/angryjet/main.go b/cmd/angryjet/main.go index f3dd44f..e6f84c3 100644 --- a/cmd/angryjet/main.go +++ b/cmd/angryjet/main.go @@ -95,9 +95,11 @@ func main() { } kingpin.FatalIfError(GenerateManagedLegacy(*filenameManaged, header, p), "cannot write managed resource method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedModern(*filenameManaged, header, p), "cannot write managed resource method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateManagedNamespaced(*filenameManaged, header, p), "cannot write namespaced managed resource method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedCluster(*filenameManaged, header, p), "cannot write cluster managed resource method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedListLegacy(*filenameManagedList, header, p), "cannot write managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedListModern(*filenameManagedList, header, p), "cannot write managed resource list method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateManagedNamespacedList(*filenameManagedList, header, p), "cannot write namespaced managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedClusterList(*filenameManagedList, header, p), "cannot write cluster managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfig(*filenamePC, header, p), "cannot write provider config method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageLegacy(*filenamePCU, header, p), "cannot write provider config usage method set for package %s", p.PkgPath) @@ -106,6 +108,7 @@ func main() { kingpin.FatalIfError(GenerateProviderConfigUsageListModern(*filenamePCUList, header, p), "cannot write provider config usage list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateReferencesLegacy(*filenameResolvers, header, p), "cannot write reference resolvers for package %s", p.PkgPath) kingpin.FatalIfError(GenerateReferencesModern(*filenameResolvers, header, p), "cannot write reference resolvers for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateReferencesNamespaced(*filenameResolvers, header, p), "cannot write namespaced reference resolvers for package %s", p.PkgPath) kingpin.FatalIfError(GenerateReferencesCluster(*filenameResolvers, header, p), "cannot write cluster reference resolvers for package %s", p.PkgPath) } } @@ -172,6 +175,37 @@ func GenerateManagedModern(filename, header string, p *packages.Package) error { return errors.Wrap(err, "cannot write V2 managed resource methods") } +// GenerateManagedNamespaced generates the resource.Managed method set for namespaced MRs +// using the crossplane core API types. +func GenerateManagedNamespaced(filename, header string, p *packages.Package) error { + receiver := "mg" + + methods := method.Set{ + "SetConditions": method.NewSetConditions(receiver, RuntimeV2Import), + "GetCondition": method.NewGetCondition(receiver, RuntimeV2Import), + "GetProviderConfigReference": method.NewGetTypedProviderConfigReference(receiver, RuntimeV2Import), + "SetProviderConfigReference": method.NewSetTypedProviderConfigReference(receiver, RuntimeV2Import), + "SetWriteConnectionSecretToReference": method.NewLocalSetWriteConnectionSecretToReference(receiver, RuntimeV2Import), + "GetWriteConnectionSecretToReference": method.NewLocalGetWriteConnectionSecretToReference(receiver, RuntimeV2Import), + "SetManagementPolicies": method.NewSetManagementPolicies(receiver, RuntimeV2Import), + "GetManagementPolicies": method.NewGetManagementPolicies(receiver, RuntimeV2Import), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + CoreImport: CoreAlias, + RuntimeV2Import: RuntimeV2Alias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedNamespaced(), + match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write namespaced managed resource methods") +} + // GenerateManagedCluster generates the resource.Managed method set for cluster-scoped MRs. func GenerateManagedCluster(filename, header string, p *packages.Package) error { receiver := "mg" @@ -248,6 +282,29 @@ func GenerateManagedListModern(filename, header string, p *packages.Package) err return errors.Wrap(err, "cannot write V2 managed resource list methods") } +// GenerateManagedNamespacedList generates the resource.ManagedList method set for namespaced MRs +// using the crossplane core API types. +func GenerateManagedNamespacedList(filename, header string, p *packages.Package) error { + receiver := "l" + + methods := method.Set{ + "GetItems": method.NewManagedGetItems(receiver, ResourceImport), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + ResourceImport: ResourceAlias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedNamespacedList(), + match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write namespaced managed resource list methods") +} + // GenerateManagedClusterList generates the resource.ManagedList method set for cluster-scoped MRs. func GenerateManagedClusterList(filename, header string, p *packages.Package) error { receiver := "l" @@ -430,6 +487,31 @@ func GenerateReferencesModern(filename, header string, p *packages.Package) erro return errors.Wrap(err, "cannot write V2 reference resolver methods") } +// GenerateReferencesNamespaced generates reference resolver calls for namespaced MRs +// using the crossplane core API types. +func GenerateReferencesNamespaced(filename, header string, p *packages.Package) error { + receiver := "mg" + comm := comments.In(p) + + methods := method.Set{ + "ResolveReferences": method.NewResolveReferencesV2(types.NewTraverser(comm), receiver, ClientImport, ReferenceImport), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{ + ClientImport: ClientAlias, + ReferenceImport: ReferenceAlias, + }), + generate.WithMatcher(match.AllOf( + match.ManagedNamespaced(), + match.DoesNotHaveMarker(comm, DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write namespaced reference resolver methods") +} + // GenerateReferencesCluster generates reference resolver calls for cluster-scoped MRs. func GenerateReferencesCluster(filename, header string, p *packages.Package) error { receiver := "mg" diff --git a/internal/fields/fields.go b/internal/fields/fields.go index 838f7b7..5645618 100644 --- a/internal/fields/fields.go +++ b/internal/fields/fields.go @@ -208,13 +208,13 @@ func IsStatus() Matcher { return IsTypeNamed(NameStatus, TypeSuffixStatus) } func IsResourceSpec() Matcher { return IsTypeNamed(TypeSuffixResourceSpec, NameResourceSpec) } // IsResourceV2Spec returns a Matcher that returns true if the supplied field -// appears to be a Crossplane managed resource spec. It matches both the -// crossplane-runtime and crossplane core ManagedResourceSpec types. -func IsResourceV2Spec() Matcher { - return func(f *types.Var) bool { - return IsTypeNamed(TypeSuffixResourceV2Spec, NameResourceV2Spec)(f) || - IsTypeNamed(TypeSuffixNamespacedManagedResourceSpec, NameResourceV2Spec)(f) - } +// appears to be a Crossplane managed resource spec from crossplane-runtime. +func IsResourceV2Spec() Matcher { return IsTypeNamed(TypeSuffixResourceV2Spec, NameResourceV2Spec) } + +// IsNamespacedManagedResourceSpec returns a Matcher that returns true if the supplied field +// appears to be a Crossplane namespaced managed resource spec from the core API. +func IsNamespacedManagedResourceSpec() Matcher { + return IsTypeNamed(TypeSuffixNamespacedManagedResourceSpec, NameResourceV2Spec) } // IsClusterManagedResourceSpec returns a Matcher that returns true if the supplied field diff --git a/internal/match/match.go b/internal/match/match.go index 7ebf907..767c5d3 100644 --- a/internal/match/match.go +++ b/internal/match/match.go @@ -46,7 +46,7 @@ func Managed() Object { } // ManagedV2 returns an Object matcher that returns true if the supplied Object is -// a v2-style Crossplane managed resource. +// a v2-style Crossplane managed resource using crossplane-runtime types. func ManagedV2() Object { return func(o types.Object) bool { return fields.Has(o, @@ -56,7 +56,24 @@ func ManagedV2() Object { fields.IsResourceV2Spec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), + fields.IsResourceStatus().And(fields.IsEmbedded()), + )), + ) + } +} + +// ManagedNamespaced returns an Object matcher that returns true if the supplied Object is +// a namespaced Crossplane managed resource using the core API types. +func ManagedNamespaced() Object { + return func(o types.Object) bool { + return fields.Has(o, + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsObjectMeta().And(fields.IsEmbedded()), + fields.IsSpec().And(fields.HasFieldThat( + fields.IsNamespacedManagedResourceSpec().And(fields.IsEmbedded()), + )), + fields.IsStatus().And(fields.HasFieldThat( + fields.IsManagedResourceStatus().And(fields.IsEmbedded()), )), ) } @@ -83,7 +100,7 @@ func ManagedList() Object { } // ManagedListV2 returns an Object matcher that returns true if the supplied -// Object is a list of Crossplane managed resource. +// Object is a list of Crossplane managed resource using crossplane-runtime types. func ManagedListV2() Object { return func(o types.Object) bool { return fields.Has(o, @@ -95,7 +112,27 @@ func ManagedListV2() Object { fields.IsResourceV2Spec().And(fields.IsEmbedded()), )), fields.IsStatus().And(fields.HasFieldThat( - fields.IsResourceStatus().Or(fields.IsManagedResourceStatus()).And(fields.IsEmbedded()), + fields.IsResourceStatus().And(fields.IsEmbedded()), + )), + )), + ) + } +} + +// ManagedNamespacedList returns an Object matcher that returns true if the supplied +// Object is a list of namespaced Crossplane managed resources using the core API types. +func ManagedNamespacedList() Object { + return func(o types.Object) bool { + return fields.Has(o, + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsItems().And(fields.IsSlice()).And(fields.HasFieldThat( + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsObjectMeta().And(fields.IsEmbedded()), + fields.IsSpec().And(fields.HasFieldThat( + fields.IsNamespacedManagedResourceSpec().And(fields.IsEmbedded()), + )), + fields.IsStatus().And(fields.HasFieldThat( + fields.IsManagedResourceStatus().And(fields.IsEmbedded()), )), )), ) From 242321a252630fdaba5ea9c3f4203f48b15ca7e6 Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Fri, 29 May 2026 11:01:12 +0200 Subject: [PATCH 5/8] fix: update README Signed-off-by: Florian Fa Richter --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2483248..701bb92 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,14 @@ considered capable of satisfying crossplane-runtime's interfaces based on the heuristics described in the [Provider Development Guide], for example a managed resource must: -* Embed a [`ResourceStatus`] struct in their `Status` struct. -* Embed a [`ResourceSpec`] struct in their `Spec` struct. +* Embed a [`ResourceStatus`] or [`ManagedResourceStatus`] struct in their `Status` struct. +* Embed a [`ResourceSpec`], [`ManagedResourceSpec`], or [`ClusterManagedResourceSpec`] struct in their `Spec` struct. * Embed a `Parameters` struct in their `Spec` struct. +The newer [`ManagedResourceSpec`], [`ClusterManagedResourceSpec`], and +[`ManagedResourceStatus`] types from `github.com/crossplane/crossplane/apis/v2/core/v2` +are supported for namespaced and cluster-scoped managed resources respectively. + Methods are not written if they are already defined outside of the file that would be generated. Use the `//+crossplane:generate:methods=false` comment marker to explicitly disable generation of any methods for a type. Use `go @@ -94,4 +98,7 @@ Args: [`resource.Managed`]: https://godoc.org/github.com/crossplane/crossplane-runtime/v2/pkg/resource#Managed [`ResourceSpec`]: https://godoc.org/github.com/crossplane/crossplane-runtime/v2/apis/common/v1#ResourceSpec [`ResourceStatus`]: https://godoc.org/github.com/crossplane/crossplane-runtime/v2/apis/common/v1#ResourceStatus +[`ManagedResourceSpec`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ManagedResourceSpec +[`ClusterManagedResourceSpec`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ClusterManagedResourceSpec +[`ManagedResourceStatus`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ManagedResourceStatus [Provider Development Guide]: https://github.com/crossplane/crossplane/blob/master/contributing/guide-provider-development.md#defining-resource-kinds \ No newline at end of file From 6685794cb877c6d54d85c1f5a45585f28caf811a Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Mon, 29 Jun 2026 07:53:43 +0200 Subject: [PATCH 6/8] fix: use xpv2 alias Signed-off-by: Florian Fa Richter --- cmd/angryjet/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/angryjet/main.go b/cmd/angryjet/main.go index e6f84c3..41eaea5 100644 --- a/cmd/angryjet/main.go +++ b/cmd/angryjet/main.go @@ -53,7 +53,7 @@ const ( RuntimeAlias = "xpv1" RuntimeImport = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" - RuntimeV2Alias = "v2" + RuntimeV2Alias = "xpv2" RuntimeV2Import = "github.com/crossplane/crossplane/apis/v2/core/v2" ResourceAlias = "resource" From 2dd3d84163f5725ad352aba8c97682b45608e4a8 Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Mon, 6 Jul 2026 08:00:43 +0200 Subject: [PATCH 7/8] chore: add tests Signed-off-by: Florian Fa Richter --- cmd/angryjet/main_test.go | 442 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 cmd/angryjet/main_test.go diff --git a/cmd/angryjet/main_test.go b/cmd/angryjet/main_test.go new file mode 100644 index 0000000..bb72a7c --- /dev/null +++ b/cmd/angryjet/main_test.go @@ -0,0 +1,442 @@ +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/packages/packagestest" +) + +const ( + angryjetFixtureSource = `package v1alpha1 + +import ( + xprv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xprv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" +) + +type ReferenceTargetSpec struct { + xpv2.ClusterManagedResourceSpec +} + +type ReferenceTargetStatus struct { + xpv2.ManagedResourceStatus +} + +type ReferenceTarget struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec ReferenceTargetSpec + Status ReferenceTargetStatus +} + +type ReferenceTargetList struct { + metav1.TypeMeta + Items []ReferenceTarget +} + +type NamespacedResourceParameters struct { + // +crossplane:generate:reference:type=ReferenceTarget + Target string +} + +type NamespacedResourceSpec struct { + xpv2.ManagedResourceSpec + ForProvider NamespacedResourceParameters +} + +type NamespacedResourceStatus struct { + xpv2.ManagedResourceStatus +} + +type NamespacedResource struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec NamespacedResourceSpec + Status NamespacedResourceStatus +} + +type NamespacedResourceList struct { + metav1.TypeMeta + Items []NamespacedResource +} + +type ClusterResourceParameters struct { + // +crossplane:generate:reference:type=ReferenceTarget + Target string +} + +type ClusterResourceSpec struct { + xpv2.ClusterManagedResourceSpec + ForProvider ClusterResourceParameters +} + +type ClusterResourceStatus struct { + xpv2.ManagedResourceStatus +} + +type ClusterResource struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec ClusterResourceSpec + Status ClusterResourceStatus +} + +type ClusterResourceList struct { + metav1.TypeMeta + Items []ClusterResource +} + +type ModernResourceParameters struct { + // +crossplane:generate:reference:type=ReferenceTarget + Target string +} + +type ModernResourceSpec struct { + xprv2.ManagedResourceSpec + ForProvider ModernResourceParameters +} + +type ModernResourceStatus struct { + xprv1.ResourceStatus +} + +type ModernResource struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec ModernResourceSpec + Status ModernResourceStatus +} + +type ModernResourceList struct { + metav1.TypeMeta + Items []ModernResource +} + +type ModernProviderConfigUsage struct { + metav1.TypeMeta + metav1.ObjectMeta + xprv2.TypedProviderConfigUsage +} + +type ModernProviderConfigUsageList struct { + metav1.TypeMeta + Items []ModernProviderConfigUsage +} +` + + metaV1FixtureSource = `package v1 + +type TypeMeta struct{} + +type ObjectMeta struct{} + +type ListMeta struct{} +` + + coreV2FixtureSource = `package v2 + +type ManagedResourceSpec struct{} + +type ClusterManagedResourceSpec struct{} + +type ManagedResourceStatus struct{} +` + + runtimeV1FixtureSource = `package v1 + +type ResourceStatus struct{} +` + + runtimeV2FixtureSource = `package v2 + +type ManagedResourceSpec struct{} + +type TypedProviderConfigUsage struct{} +` +) + +type generatorFunc func(string, string, *packages.Package) error + +func TestGenerateMethods(t *testing.T) { + type args struct { + generate generatorFunc + filename string + } + + type want struct { + contains []string + notContains []string + } + + cases := map[string]struct { + args + want + }{ + "Managed namespaced": { + args: args{ + generate: GenerateManagedNamespaced, + filename: "zz_generated.namespaced.go", + }, + want: want{ + contains: []string{ + `import xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2"`, + `func (mg *NamespacedResource) SetProviderConfigReference(r *xpv2.ProviderConfigReference) {`, + `func (mg *NamespacedResource) GetWriteConnectionSecretToReference() *xpv2.LocalSecretReference {`, + `func (mg *NamespacedResource) GetManagementPolicies() xpv2.ManagementPolicies {`, + }, + notContains: []string{ + `func (mg *ClusterResource)`, + `SetDeletionPolicy`, + }, + }, + }, + "Managed cluster": { + args: args{ + generate: GenerateManagedCluster, + filename: "zz_generated.cluster.go", + }, + want: want{ + contains: []string{ + `import xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2"`, + `func (mg *ClusterResource) SetProviderConfigReference(r *xpv2.Reference) {`, + `func (mg *ClusterResource) GetWriteConnectionSecretToReference() *xpv2.SecretReference {`, + `func (mg *ClusterResource) SetDeletionPolicy(r xpv2.DeletionPolicy) {`, + }, + notContains: []string{ + `func (mg *NamespacedResource)`, + }, + }, + }, + "Managed list namespaced": { + args: args{ + generate: GenerateManagedNamespacedList, + filename: "zz_generated.namespaced_list.go", + }, + want: want{ + contains: []string{ + `import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource"`, + `func (l *NamespacedResourceList) GetItems() []resource.Managed {`, + }, + notContains: []string{ + `func (l *ClusterResourceList)`, + }, + }, + }, + "Managed list cluster": { + args: args{ + generate: GenerateManagedClusterList, + filename: "zz_generated.cluster_list.go", + }, + want: want{ + contains: []string{ + `import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource"`, + `func (l *ClusterResourceList) GetItems() []resource.Managed {`, + }, + notContains: []string{ + `func (l *NamespacedResourceList)`, + }, + }, + }, + "References namespaced": { + args: args{ + generate: GenerateReferencesNamespaced, + filename: "zz_generated.namespaced_refs.go", + }, + want: want{ + contains: []string{ + `reference.NewAPINamespacedResolver(c, mg)`, + `var rsp reference.NamespacedResolutionResponse`, + `reference.NamespacedResolutionRequest{`, + `Namespace: mg.GetNamespace(),`, + `func (mg *NamespacedResource) ResolveReferences(ctx context.Context, c client.Reader) error {`, + }, + }, + }, + "References cluster": { + args: args{ + generate: GenerateReferencesCluster, + filename: "zz_generated.cluster_refs.go", + }, + want: want{ + contains: []string{ + `reference.NewAPIResolver(c, mg)`, + `var rsp reference.ResolutionResponse`, + `reference.ResolutionRequest{`, + `Namespace: mg.GetNamespace(),`, + `func (mg *ClusterResource) ResolveReferences(ctx context.Context, c client.Reader) error {`, + }, + }, + }, + "Managed modern": { + args: args{ + generate: GenerateManagedModern, + filename: "zz_generated.modern.go", + }, + want: want{ + contains: []string{ + `import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1"`, + `func (mg *ModernResource) SetProviderConfigReference(r *xpv1.ProviderConfigReference) {`, + `func (mg *ModernResource) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference {`, + }, + notContains: []string{ + `func (mg *NamespacedResource)`, + `SetDeletionPolicy`, + }, + }, + }, + "Managed list modern": { + args: args{ + generate: GenerateManagedListModern, + filename: "zz_generated.modern_list.go", + }, + want: want{ + contains: []string{ + `func (l *ModernResourceList) GetItems() []resource.Managed {`, + }, + notContains: []string{ + `func (l *NamespacedResourceList)`, + }, + }, + }, + "Provider config usage modern": { + args: args{ + generate: GenerateProviderConfigUsageModern, + filename: "zz_generated.modern_pcu.go", + }, + want: want{ + contains: []string{ + `func (p *ModernProviderConfigUsage) SetProviderConfigReference(r xpv1.ProviderConfigReference) {`, + `func (p *ModernProviderConfigUsage) GetResourceReference() xpv1.TypedReference {`, + }, + }, + }, + "Provider config usage list modern": { + args: args{ + generate: GenerateProviderConfigUsageListModern, + filename: "zz_generated.modern_pcu_list.go", + }, + want: want{ + contains: []string{ + `func (p *ModernProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage {`, + }, + }, + }, + "References modern": { + args: args{ + generate: GenerateReferencesModern, + filename: "zz_generated.modern_refs.go", + }, + want: want{ + contains: []string{ + `reference.NewAPINamespacedResolver(c, mg)`, + `var rsp reference.NamespacedResolutionResponse`, + `func (mg *ModernResource) ResolveReferences(ctx context.Context, c client.Reader) error {`, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + pkg := loadFixturePackage(t) + got := generateOutput(t, pkg, tc.args.filename, tc.args.generate) + + for _, want := range tc.want.contains { + if !strings.Contains(got, want) { + t.Fatalf("generated output missing snippet %q\n%s", want, got) + } + } + + for _, unwanted := range tc.want.notContains { + if strings.Contains(got, unwanted) { + t.Fatalf("generated output unexpectedly contained snippet %q\n%s", unwanted, got) + } + } + }) + } +} + +func loadFixturePackage(t *testing.T) *packages.Package { + t.Helper() + + exported := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ + { + Name: "golang.org/fake", + Files: map[string]any{ + "v1alpha1/model.go": angryjetFixtureSource, + }, + }, + { + Name: "k8s.io/apimachinery", + Files: map[string]any{ + "pkg/apis/meta/v1/meta.go": metaV1FixtureSource, + }, + }, + { + Name: "github.com/crossplane/crossplane", + Files: map[string]any{ + "apis/v2/core/v2/types.go": coreV2FixtureSource, + }, + }, + { + Name: "github.com/crossplane/crossplane-runtime/v2", + Files: map[string]any{ + "apis/common/v1/types.go": runtimeV1FixtureSource, + "apis/common/v2/types.go": runtimeV2FixtureSource, + }, + }, + }) + t.Cleanup(exported.Cleanup) + + exported.Config.Mode = LoadMode + pkgs, err := packages.Load(exported.Config, fmt.Sprintf("file=%s", exported.File("golang.org/fake", "v1alpha1/model.go"))) + if err != nil { + t.Fatal(err) + } + if len(pkgs) != 1 { + t.Fatalf("packages.Load() returned %d packages, want 1", len(pkgs)) + } + for _, err := range pkgs[0].Errors { + t.Fatal(err) + } + + return pkgs[0] +} + +func generateOutput(t *testing.T, pkg *packages.Package, filename string, generate generatorFunc) string { + t.Helper() + + if err := generate(filename, "", pkg); err != nil { + t.Fatal(err) + } + + b, err := os.ReadFile(filepath.Join(filepath.Dir(pkg.GoFiles[0]), filename)) + if err != nil { + t.Fatal(err) + } + + return string(b) +} From 9b37924a929dde2932c29b2f7c27aa346aaab4f2 Mon Sep 17 00:00:00 2001 From: Florian Fa Richter Date: Tue, 7 Jul 2026 08:11:44 +0200 Subject: [PATCH 8/8] fix: add more missing types Signed-off-by: Florian Fa Richter --- README.md | 61 +++++++++++--- cmd/angryjet/main.go | 44 +++++++--- cmd/angryjet/main_test.go | 108 ++++++++++++++++++++++++- internal/fields/fields.go | 9 ++- internal/match/match.go | 21 ++++- internal/method/reference_processor.go | 8 +- 6 files changed, 221 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 701bb92..6a3e4a4 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,32 @@ considered capable of satisfying crossplane-runtime's interfaces based on the heuristics described in the [Provider Development Guide], for example a managed resource must: -* Embed a [`ResourceStatus`] or [`ManagedResourceStatus`] struct in their `Status` struct. -* Embed a [`ResourceSpec`], [`ManagedResourceSpec`], or [`ClusterManagedResourceSpec`] struct in their `Spec` struct. -* Embed a `Parameters` struct in their `Spec` struct. +- Embed a [`ResourceStatus`] or [`ManagedResourceStatus`] struct in their `Status` struct. +- Embed a [`ResourceSpec`], [`ManagedResourceSpec`], or [`ClusterManagedResourceSpec`] struct in their `Spec` struct. +- Embed a `Parameters` struct in their `Spec` struct. The newer [`ManagedResourceSpec`], [`ClusterManagedResourceSpec`], and [`ManagedResourceStatus`] types from `github.com/crossplane/crossplane/apis/v2/core/v2` are supported for namespaced and cluster-scoped managed resources respectively. +`angryjet` also supports provider config and provider config usage migrations +across the legacy crossplane-runtime types and the newer core API v2 types: + +- Provider configs are detected when their `Status` embeds either the legacy + [`ProviderConfigStatus`] or the core API v2 [`ProviderConfigStatusV2`] type. +- For provider configs that embed the core API v2 status type, generated + condition methods use the core API v2 [`ConditionV2`] and + [`ConditionTypeV2`] types. +- Provider config usages are detected when they embed either the legacy + [`ProviderConfigUsage`] or the newer [`TypedProviderConfigUsage`] type. +- For provider config usages that embed [`TypedProviderConfigUsage`], generated + provider config reference methods use the core API v2 + [`ProviderConfigReferenceV2`] type and resource reference methods use the + core API v2 [`TypedReferenceV2`] type. +- Provider config specs that migrate nested credential helper fields such as + [`CredentialsSourceV2`] and [`CommonCredentialSelectorsV2`] continue to be + supported. + Methods are not written if they are already defined outside of the file that would be generated. Use the `//+crossplane:generate:methods=false` comment marker to explicitly disable generation of any methods for a type. Use `go @@ -35,6 +53,7 @@ the top level of your `api/` directory, for example: In addition to functions that satisfy `resource.Managed`, you can use `angryjet` to generate a `ResolveReferences` method as well. In order to generate a resolution call for given field, you need to add the following comment marker: + ``` // +crossplane:generate:reference:type= ``` @@ -45,9 +64,15 @@ as `github.com/crossplane/provider-aws/apis/ec2/v1beta1.VPC`. The generated resolver will use the external name annotation of the target resource to fetch the value and it assumes that reference field is named as -`FieldNameRef`/`FieldNameRefs if array` and selector field is named as -`FieldNameSelector`. You can override these defaults by adding the optional comment -markers, see the following example: +`FieldNameRef`/`FieldNameRefs if array` and selector field is named as +`FieldNameSelector`. + +For cluster-scoped resolvers these fields should use the core API v2 +[`ReferenceV2`] and [`SelectorV2`] types. For namespaced resolvers they should +use [`NamespacedReferenceV2`] and [`NamespacedSelectorV2`]. You can override +the default field names by adding the optional comment markers, see the +following example: + ```go type SomeParameters struct { // +crossplane:generate:reference:type=github.com/crossplane/provider-aws/apis/ec2/v1beta1.Subnet @@ -55,10 +80,10 @@ type SomeParameters struct { // +crossplane:generate:reference:refFieldName=SubnetIDRefs // +crossplane:generate:reference:selectorFieldName=SubnetIDSelector SubnetIDs []string `json:"subnetIds,omitempty"` - - SubnetIDRefs []xpv1.Reference `json:"subnetIdRefs,omitempty"` - - SubnetIDSelector *xpv1.Selector `json:"subnetIdSelector,omitempty"` + + SubnetIDRefs []xpv2.Reference `json:"subnetIdRefs,omitempty"` + + SubnetIDSelector *xpv2.Selector `json:"subnetIdSelector,omitempty"` } ``` @@ -101,4 +126,18 @@ Args: [`ManagedResourceSpec`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ManagedResourceSpec [`ClusterManagedResourceSpec`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ClusterManagedResourceSpec [`ManagedResourceStatus`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ManagedResourceStatus -[Provider Development Guide]: https://github.com/crossplane/crossplane/blob/master/contributing/guide-provider-development.md#defining-resource-kinds \ No newline at end of file +[`ProviderConfigStatus`]: https://pkg.go.dev/github.com/crossplane/crossplane-runtime/v2/apis/common/v1#ProviderConfigStatus +[`ProviderConfigStatusV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ProviderConfigStatus +[`ConditionV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#Condition +[`ConditionTypeV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ConditionType +[`ProviderConfigUsage`]: https://pkg.go.dev/github.com/crossplane/crossplane-runtime/v2/apis/common/v1#ProviderConfigUsage +[`TypedProviderConfigUsage`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#TypedProviderConfigUsage +[`ProviderConfigReferenceV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#ProviderConfigReference +[`ReferenceV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#Reference +[`SelectorV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#Selector +[`NamespacedReferenceV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#NamespacedReference +[`NamespacedSelectorV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#NamespacedSelector +[`TypedReferenceV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#TypedReference +[`CredentialsSourceV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#CredentialsSource +[`CommonCredentialSelectorsV2`]: https://pkg.go.dev/github.com/crossplane/crossplane/apis/v2/core/v2#CommonCredentialSelectors +[Provider Development Guide]: https://github.com/crossplane/crossplane/blob/master/contributing/guide-provider-development.md#defining-resource-kinds diff --git a/cmd/angryjet/main.go b/cmd/angryjet/main.go index 41eaea5..438dbe5 100644 --- a/cmd/angryjet/main.go +++ b/cmd/angryjet/main.go @@ -101,7 +101,8 @@ func main() { kingpin.FatalIfError(GenerateManagedListModern(*filenameManagedList, header, p), "cannot write managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedNamespacedList(*filenameManagedList, header, p), "cannot write namespaced managed resource list method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateManagedClusterList(*filenameManagedList, header, p), "cannot write cluster managed resource list method set for package %s", p.PkgPath) - kingpin.FatalIfError(GenerateProviderConfig(*filenamePC, header, p), "cannot write provider config method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateProviderConfigLegacy(*filenamePC, header, p), "cannot write provider config method set for package %s", p.PkgPath) + kingpin.FatalIfError(GenerateProviderConfigModern(*filenamePC, header, p), "cannot write provider config method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageLegacy(*filenamePCU, header, p), "cannot write provider config usage method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageModern(*filenamePCU, header, p), "cannot write provider config usage method set for package %s", p.PkgPath) kingpin.FatalIfError(GenerateProviderConfigUsageListLegacy(*filenamePCUList, header, p), "cannot write provider config usage list method set for package %s", p.PkgPath) @@ -327,8 +328,8 @@ func GenerateManagedClusterList(filename, header string, p *packages.Package) er return errors.Wrap(err, "cannot write cluster managed resource list methods") } -// GenerateProviderConfig generates the resource.ProviderConfig method set. -func GenerateProviderConfig(filename, header string, p *packages.Package) error { +// GenerateProviderConfigLegacy generates the legacy resource.ProviderConfig method set. +func GenerateProviderConfigLegacy(filename, header string, p *packages.Package) error { receiver := "p" methods := method.Set{ @@ -342,12 +343,35 @@ func GenerateProviderConfig(filename, header string, p *packages.Package) error generate.WithHeaders(header), generate.WithImportAliases(map[string]string{RuntimeImport: RuntimeAlias}), generate.WithMatcher(match.AllOf( - match.ProviderConfig(), + match.ProviderConfigLegacy(), match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), ), ) - return errors.Wrap(err, "cannot write provider config methods") + return errors.Wrap(err, "cannot write legacy provider config methods") +} + +// GenerateProviderConfigModern generates the core API v2 resource.ProviderConfig method set. +func GenerateProviderConfigModern(filename, header string, p *packages.Package) error { + receiver := "p" + + methods := method.Set{ + "SetUsers": method.NewSetUsers(receiver), + "GetUsers": method.NewGetUsers(receiver), + "SetConditions": method.NewSetConditions(receiver, RuntimeV2Import), + "GetCondition": method.NewGetCondition(receiver, RuntimeV2Import), + } + + err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), + generate.WithHeaders(header), + generate.WithImportAliases(map[string]string{RuntimeV2Import: RuntimeV2Alias}), + generate.WithMatcher(match.AllOf( + match.ProviderConfigV2(), + match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), + ), + ) + + return errors.Wrap(err, "cannot write v2 provider config methods") } // GenerateProviderConfigUsageLegacy generates the resource.ProviderConfigUsage method set. @@ -378,15 +402,15 @@ func GenerateProviderConfigUsageModern(filename, header string, p *packages.Pack receiver := "p" methods := method.Set{ - "SetProviderConfigReference": method.NewSetRootProviderConfigTypedReference(receiver, RuntimeImport), - "GetProviderConfigReference": method.NewGetRootProviderConfigTypedReference(receiver, RuntimeImport), - "SetResourceReference": method.NewSetRootResourceReference(receiver, RuntimeImport), - "GetResourceReference": method.NewGetRootResourceReference(receiver, RuntimeImport), + "SetProviderConfigReference": method.NewSetRootProviderConfigTypedReference(receiver, RuntimeV2Import), + "GetProviderConfigReference": method.NewGetRootProviderConfigTypedReference(receiver, RuntimeV2Import), + "SetResourceReference": method.NewSetRootResourceReference(receiver, RuntimeV2Import), + "GetResourceReference": method.NewGetRootResourceReference(receiver, RuntimeV2Import), } err := generate.WriteMethods(p, methods, filepath.Join(filepath.Dir(p.GoFiles[0]), filename), generate.WithHeaders(header), - generate.WithImportAliases(map[string]string{RuntimeImport: RuntimeAlias}), + generate.WithImportAliases(map[string]string{RuntimeV2Import: RuntimeV2Alias}), generate.WithMatcher(match.AllOf( match.ProviderConfigUsageV2(), match.DoesNotHaveMarker(comments.In(p), DisableMarker, "false")), diff --git a/cmd/angryjet/main_test.go b/cmd/angryjet/main_test.go index bb72a7c..e4c0119 100644 --- a/cmd/angryjet/main_test.go +++ b/cmd/angryjet/main_test.go @@ -138,13 +138,50 @@ type ModernResourceList struct { type ModernProviderConfigUsage struct { metav1.TypeMeta metav1.ObjectMeta - xprv2.TypedProviderConfigUsage + xpv2.TypedProviderConfigUsage } type ModernProviderConfigUsageList struct { metav1.TypeMeta Items []ModernProviderConfigUsage } + +type ModernProviderCredentials struct { + Source xpv2.CredentialsSource + xpv2.CommonCredentialSelectors +} + +type ModernProviderConfigSpec struct { + xprv1.ProviderConfigSpec + Credentials ModernProviderCredentials +} + +// A ModernProviderConfigStatus reflects the observed state of a ProviderConfig. +type ModernProviderConfigStatus struct { + xpv2.ProviderConfigStatus +} + +type ModernProviderConfig struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec ModernProviderConfigSpec + Status ModernProviderConfigStatus +} + +type LegacyProviderConfigSpec struct { + xprv1.ProviderConfigSpec +} + +type LegacyProviderConfigStatus struct { + xprv1.ProviderConfigStatus +} + +type LegacyProviderConfig struct { + metav1.TypeMeta + metav1.ObjectMeta + Spec LegacyProviderConfigSpec + Status LegacyProviderConfigStatus +} ` metaV1FixtureSource = `package v1 @@ -168,13 +205,44 @@ type ManagedResourceStatus struct{} runtimeV1FixtureSource = `package v1 type ResourceStatus struct{} + +type Condition struct{} + +type ConditionType string + +type User struct{} + +type ProviderConfigSpec struct{} + +type ProviderConfigStatus struct{} + +func (p *ProviderConfigStatus) SetConditions(c ...Condition) {} + +func (p *ProviderConfigStatus) GetCondition(ct ConditionType) Condition { return Condition{} } + +func (p *ProviderConfigStatus) SetUsers(u ...User) {} + +func (p *ProviderConfigStatus) GetUsers() []User { return nil } ` runtimeV2FixtureSource = `package v2 type ManagedResourceSpec struct{} +` + + coreV2ProviderConfigFixtureSource = `package v2 + +type ProviderConfigStatus struct{} + +type ProviderConfigReference struct{} + +type TypedReference struct{} type TypedProviderConfigUsage struct{} + +type CredentialsSource string + +type CommonCredentialSelectors struct{} ` ) @@ -328,6 +396,12 @@ func TestGenerateMethods(t *testing.T) { }, want: want{ contains: []string{ + `func (p *ModernProviderConfigUsage) SetProviderConfigReference(r xpv2.ProviderConfigReference) {`, + `func (p *ModernProviderConfigUsage) GetProviderConfigReference() xpv2.ProviderConfigReference {`, + `func (p *ModernProviderConfigUsage) SetResourceReference(r xpv2.TypedReference) {`, + `func (p *ModernProviderConfigUsage) GetResourceReference() xpv2.TypedReference {`, + }, + notContains: []string{ `func (p *ModernProviderConfigUsage) SetProviderConfigReference(r xpv1.ProviderConfigReference) {`, `func (p *ModernProviderConfigUsage) GetResourceReference() xpv1.TypedReference {`, }, @@ -344,6 +418,35 @@ func TestGenerateMethods(t *testing.T) { }, }, }, + "Provider config legacy": { + args: args{ + generate: GenerateProviderConfigLegacy, + filename: "zz_generated.legacy_pc.go", + }, + want: want{ + contains: []string{ + `func (p *LegacyProviderConfig) SetConditions(c ...xpv1.Condition) {`, + `func (p *LegacyProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition {`, + }, + }, + }, + "Provider config modern status and credential migration": { + args: args{ + generate: GenerateProviderConfigModern, + filename: "zz_generated.modern_pc.go", + }, + want: want{ + contains: []string{ + `func (p *ModernProviderConfig) SetUsers(i int64) {`, + `func (p *ModernProviderConfig) GetUsers() int64 {`, + `func (p *ModernProviderConfig) SetConditions(c ...xpv2.Condition) {`, + `func (p *ModernProviderConfig) GetCondition(ct xpv2.ConditionType) xpv2.Condition {`, + }, + notContains: []string{ + `func (p *ModernProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition {`, + }, + }, + }, "References modern": { args: args{ generate: GenerateReferencesModern, @@ -398,7 +501,8 @@ func loadFixturePackage(t *testing.T) *packages.Package { { Name: "github.com/crossplane/crossplane", Files: map[string]any{ - "apis/v2/core/v2/types.go": coreV2FixtureSource, + "apis/v2/core/v2/types.go": coreV2FixtureSource, + "apis/v2/core/v2/providerconfig.go": coreV2ProviderConfigFixtureSource, }, }, { diff --git a/internal/fields/fields.go b/internal/fields/fields.go index 5645618..eb75cc6 100644 --- a/internal/fields/fields.go +++ b/internal/fields/fields.go @@ -57,7 +57,8 @@ const ( TypeSuffixProviderConfigStatus = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigStatus" TypeSuffixProviderConfigUsage = "github.com/crossplane/crossplane-runtime/v2/apis/common/v1.ProviderConfigUsage" - TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.TypedProviderConfigUsage" + TypeSuffixProviderConfigStatusV2 = "github.com/crossplane/crossplane/apis/v2/core/v2.ProviderConfigStatus" + TypeSuffixProviderConfigUsageV2 = "github.com/crossplane/crossplane/apis/v2/core/v2.TypedProviderConfigUsage" TypeSuffixResourceV2Spec = "github.com/crossplane/crossplane-runtime/v2/apis/common/v2.ManagedResourceSpec" TypeSuffixNamespacedManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ManagedResourceSpec" TypeSuffixClusterManagedResourceSpec = "github.com/crossplane/crossplane/apis/v2/core/v2.ClusterManagedResourceSpec" @@ -245,6 +246,12 @@ func IsProviderConfigStatus() Matcher { return IsTypeNamed(TypeSuffixProviderConfigStatus, NameProviderConfigStatus) } +// IsProviderConfigStatusV2 returns a Matcher that returns true if the supplied +// field appears to be a Crossplane core API provider config status. +func IsProviderConfigStatusV2() Matcher { + return IsTypeNamed(TypeSuffixProviderConfigStatusV2, NameProviderConfigStatus) +} + // IsProviderConfigUsage returns a Matcher that returns true if the supplied // field appears to be a Crossplane provider config usage. func IsProviderConfigUsage() Matcher { diff --git a/internal/match/match.go b/internal/match/match.go index 767c5d3..946ecc7 100644 --- a/internal/match/match.go +++ b/internal/match/match.go @@ -176,9 +176,9 @@ func ManagedClusterList() Object { } } -// ProviderConfig returns an Object matcher that returns true if the supplied -// Object is a Crossplane ProviderConfig. -func ProviderConfig() Object { +// ProviderConfigLegacy returns an Object matcher that returns true if the supplied +// Object is a legacy Crossplane ProviderConfig. +func ProviderConfigLegacy() Object { return func(o types.Object) bool { return fields.Has(o, fields.IsTypeMeta().And(fields.IsEmbedded()), @@ -191,6 +191,21 @@ func ProviderConfig() Object { } } +// ProviderConfigV2 returns an Object matcher that returns true if the supplied +// Object is a Crossplane core API v2 ProviderConfig. +func ProviderConfigV2() Object { + return func(o types.Object) bool { + return fields.Has(o, + fields.IsTypeMeta().And(fields.IsEmbedded()), + fields.IsObjectMeta().And(fields.IsEmbedded()), + fields.IsSpec(), + fields.IsStatus().And(fields.HasFieldThat( + fields.IsProviderConfigStatusV2().And(fields.IsEmbedded()), + )), + ) + } +} + // ProviderConfigUsage returns an Object matcher that returns true if the supplied // Object is a Crossplane ProviderConfigUsage. func ProviderConfigUsage() Object { diff --git a/internal/method/reference_processor.go b/internal/method/reference_processor.go index 011d428..d6569d3 100644 --- a/internal/method/reference_processor.go +++ b/internal/method/reference_processor.go @@ -58,11 +58,13 @@ type Reference struct { // * for pointer fields or []* for array of pointer fields. GoValueFieldPath []string - // GoRefFieldName is the name of the field whose type is *xpv1.Reference or - // []xpv1.Reference. + // GoRefFieldName is the name of the field whose type is either + // *xpv2.Reference / []xpv2.Reference or + // *xpv2.NamespacedReference / []xpv2.NamespacedReference. GoRefFieldName string - // GoSelectorFieldName is the name of the field whose type is *xpv1.Selector + // GoSelectorFieldName is the name of the field whose type is either + // *xpv2.Selector or *xpv2.NamespacedSelector. GoSelectorFieldName string // IsSlice tells whether the current value type is a slice kind.