Skip to content

Commit a667e19

Browse files
authored
Merge pull request #553 from ulucinar/prep-release-1.11
[Backport release-1.11] PR#533 PR#534 PR#538 PR#549 PR#551
2 parents bc8ec41 + 7489cff commit a667e19

9 files changed

Lines changed: 294 additions & 39 deletions

File tree

OWNERS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ repository maintainers in their own `OWNERS.md` file.
1212

1313
## Maintainers
1414

15-
* Alper Ulucinar <alper@upbound.com> ([ulucinar](https://github.com/ulucinar))
16-
* Sergen Yalcin <sergen@upbound.com> ([sergenyalcin](https://github.com/sergenyalcin))
17-
* Jean du Plessis <jean@upbound.com> ([jeanduplessis](https://github.com/jeanduplessis))
18-
* Erhan Cagirici <erhan@upbound.com> ([erhancagirici](https://github.com/erhancagirici))
19-
* Cem Mergenci <cem@upbound.com> ([mergenci](https://github.com/mergenci))
15+
* Alper Ulucinar <alper@upbound.io> ([ulucinar](https://github.com/ulucinar))
16+
* Sergen Yalcin <sergen@upbound.io> ([sergenyalcin](https://github.com/sergenyalcin))
17+
* Jean du Plessis <jean@upbound.io> ([jeanduplessis](https://github.com/jeanduplessis))
18+
* Erhan Cagirici <erhan@upbound.io> ([erhancagirici](https://github.com/erhancagirici))
19+
* Cem Mergenci <cem@upbound.io> ([mergenci](https://github.com/mergenci))
2020

2121
See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment.

pkg/config/externalname.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package config
77
import (
88
"bytes"
99
"context"
10+
"fmt"
1011
"regexp"
1112
"strings"
1213
"text/template"
@@ -210,6 +211,35 @@ func GetExternalNameFromTemplated(tmpl, val string) (string, error) { //nolint:g
210211
return "", errors.Errorf("unhandled case with template %s and value %s", tmpl, val)
211212
}
212213

214+
// FrameworkResourceWithComputedIdentifier returns an ExternalName
215+
// configuration for a Terraform plugin framework resource with the specified
216+
// computed identifier attribute and the specified placeholder identifier for
217+
// the initial external read calls.
218+
func FrameworkResourceWithComputedIdentifier(identifier, placeholder string) ExternalName {
219+
en := NewExternalNameFrom(IdentifierFromProvider,
220+
WithSetIdentifierArgumentsFn(func(fn SetIdentifierArgumentsFn, base map[string]any, externalName string) {
221+
if id, ok := base[identifier]; !ok || id == placeholder {
222+
if externalName == "" {
223+
base[identifier] = placeholder
224+
} else {
225+
base[identifier] = externalName
226+
}
227+
}
228+
}),
229+
WithGetExternalNameFn(func(fn GetExternalNameFn, tfState map[string]any) (string, error) {
230+
if id, ok := tfState[identifier]; ok {
231+
idStr := fmt.Sprintf("%v", id)
232+
if len(idStr) > 0 {
233+
return idStr, nil
234+
}
235+
}
236+
return "", errors.Errorf("cannot find attribute %q in tfstate", identifier)
237+
}),
238+
)
239+
en.TFPluginFrameworkOptions.ComputedIdentifierAttributes = []string{identifier}
240+
return en
241+
}
242+
213243
// ExternalNameFrom is an ExternalName configuration which uses a parent
214244
// configuration as its base and modifies any of the GetIDFn,
215245
// GetExternalNameFn or SetIdentifierArgumentsFn. This enables us to reuse
@@ -276,7 +306,9 @@ func WithSetIdentifierArgumentsFn(fn func(fn SetIdentifierArgumentsFn, base map[
276306
// return fn(ctx, externalName, parameters, terraformProviderConfig)
277307
// }))
278308
func NewExternalNameFrom(parent ExternalName, opts ...ExternalNameFromOption) ExternalName {
279-
ec := &ExternalNameFrom{}
309+
ec := &ExternalNameFrom{
310+
ExternalName: parent,
311+
}
280312
for _, o := range opts {
281313
o(ec)
282314
}

pkg/config/provider.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ func (cc ResourceConfiguratorChain) Configure(r *Resource) {
5252
type BasePackages struct {
5353
APIVersion []string
5454
// Deprecated: Use ControllerMap instead.
55-
Controller []string
55+
Controller []string
56+
// ControllerMap is a map from:
57+
// <API group name>/<resource name> to <provider package name>.
58+
// An example is "azure/resourcegroup: config", where "config" represents
59+
// the config package (provider family package).
5660
ControllerMap map[string]string
5761
}
5862

pkg/config/resource.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ type ExternalName struct {
165165
// management policy is including the Observe Only, different from other
166166
// (required) fields.
167167
IdentifierFields []string
168+
169+
// TFPluginFrameworkOptions represents options related to Terraform plugin
170+
// framework resources.
171+
TFPluginFrameworkOptions TFPluginFrameworkOptions
172+
}
173+
174+
// TFPluginFrameworkOptions are external-name configuration options that
175+
// are specific to Terraform plugin framework resources.
176+
type TFPluginFrameworkOptions struct {
177+
// ComputedIdentifierAttributes is the list of computed Terraform identifier
178+
// attribute names for a framework resource. When set,
179+
// these computed identifier attributes will be ignored from the desired
180+
// state when calculating the drifts between the desired and actual states.
181+
ComputedIdentifierAttributes []string
168182
}
169183

170184
// References represents reference resolver configurations for the fields of a

pkg/controller/external_tfpluginfw.go

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"encoding/json"
1010
"fmt"
11+
"maps"
1112
"math"
1213
"math/big"
1314
"strings"
@@ -18,7 +19,6 @@ import (
1819
"github.com/crossplane/crossplane-runtime/pkg/meta"
1920
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
2021
xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
21-
fwdiag "github.com/hashicorp/terraform-plugin-framework/diag"
2222
fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
2323
"github.com/hashicorp/terraform-plugin-framework/providerserver"
2424
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
@@ -35,6 +35,7 @@ import (
3535
"github.com/crossplane/upjet/pkg/resource"
3636
upjson "github.com/crossplane/upjet/pkg/resource/json"
3737
"github.com/crossplane/upjet/pkg/terraform"
38+
tferrors "github.com/crossplane/upjet/pkg/terraform/errors"
3839
)
3940

4041
// TerraformPluginFrameworkConnector is an external client, with credentials and
@@ -189,8 +190,7 @@ func (c *TerraformPluginFrameworkConnector) getResourceSchema(ctx context.Contex
189190
schemaResp := &fwresource.SchemaResponse{}
190191
res.Schema(ctx, fwresource.SchemaRequest{}, schemaResp)
191192
if schemaResp.Diagnostics.HasError() {
192-
fwErrors := frameworkDiagnosticsToString(schemaResp.Diagnostics)
193-
return rschema.Schema{}, errors.Errorf("could not retrieve resource schema: %s", fwErrors)
193+
return rschema.Schema{}, tferrors.FrameworkDiagnosticsError("could not retrieve resource schema", schemaResp.Diagnostics)
194194
}
195195

196196
return schemaResp.Schema, nil
@@ -209,8 +209,7 @@ func (c *TerraformPluginFrameworkConnector) configureProvider(ctx context.Contex
209209
var schemaResp fwprovider.SchemaResponse
210210
ts.FrameworkProvider.Schema(ctx, fwprovider.SchemaRequest{}, &schemaResp)
211211
if schemaResp.Diagnostics.HasError() {
212-
fwDiags := frameworkDiagnosticsToString(schemaResp.Diagnostics)
213-
return nil, fmt.Errorf("cannot retrieve provider schema: %s", fwDiags)
212+
return nil, tferrors.FrameworkDiagnosticsError("cannot retrieve provider schema", schemaResp.Diagnostics)
214213
}
215214
providerServer := providerserver.NewProtocol5(ts.FrameworkProvider)()
216215

@@ -254,15 +253,20 @@ func (n *terraformPluginFrameworkExternalClient) filteredDiffExists(rawDiff []tf
254253
// If plan response contains non-empty RequiresReplace (i.e. the resource needs
255254
// to be recreated) an error is returned as Crossplane Resource Model (XRM)
256255
// prohibits resource re-creations and rejects this plan.
257-
func (n *terraformPluginFrameworkExternalClient) getDiffPlanResponse(ctx context.Context,
258-
tfStateValue tftypes.Value) (*tfprotov5.PlanResourceChangeResponse, bool, error) {
259-
tfConfigDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
256+
func (n *terraformPluginFrameworkExternalClient) getDiffPlanResponse(ctx context.Context, tfStateValue tftypes.Value) (*tfprotov5.PlanResourceChangeResponse, bool, error) {
257+
params := maps.Clone(n.params)
258+
// if some computed identifiers have been configured,
259+
// remove them from config.
260+
for _, id := range n.config.ExternalName.TFPluginFrameworkOptions.ComputedIdentifierAttributes {
261+
delete(params, id)
262+
}
263+
tfConfigDynamicVal, err := protov5DynamicValueFromMap(params, n.resourceValueTerraformType)
260264
if err != nil {
261265
return nil, false, errors.Wrap(err, "cannot construct dynamic value for TF Config")
262266
}
263267

264268
//
265-
tfPlannedStateDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
269+
tfPlannedStateDynamicVal, err := protov5DynamicValueFromMap(params, n.resourceValueTerraformType)
266270
if err != nil {
267271
return nil, false, errors.Wrap(err, "cannot construct dynamic value for TF Planned State")
268272
}
@@ -708,18 +712,6 @@ func getFatalDiagnostics(diags []*tfprotov5.Diagnostic) error {
708712
return errs
709713
}
710714

711-
// frameworkDiagnosticsToString constructs an error string from the provided
712-
// Plugin Framework diagnostics instance. Only Error severity diagnostics are
713-
// included.
714-
func frameworkDiagnosticsToString(fwdiags fwdiag.Diagnostics) string {
715-
frameworkErrorDiags := fwdiags.Errors()
716-
diagErrors := make([]string, 0, len(frameworkErrorDiags))
717-
for _, tfdiag := range frameworkErrorDiags {
718-
diagErrors = append(diagErrors, fmt.Sprintf("%s: %s", tfdiag.Summary(), tfdiag.Detail()))
719-
}
720-
return strings.Join(diagErrors, "\n")
721-
}
722-
723715
// protov5DynamicValueFromMap constructs a protov5 DynamicValue given the
724716
// map[string]any using the terraform type as reference.
725717
func protov5DynamicValueFromMap(data map[string]any, terraformType tftypes.Type) (*tfprotov5.DynamicValue, error) {

pkg/examples/conversion/example_conversions.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import (
2323
"github.com/crossplane/upjet/pkg/config/conversion"
2424
)
2525

26-
// ConvertSingletonListToEmbeddedObject generates the example manifests for
27-
// the APIs with converted singleton lists in their new API versions with the
28-
// embedded objects. All manifests under `startPath` are scanned and the
26+
// ApplyAPIConverters applies the registered converters to generated
27+
// example manifests under the given root directory.
28+
// All (generated) manifests under the `startPath` are scanned and the
2929
// header at the specified path `licenseHeaderPath` is used for the converted
3030
// example manifests.
31-
func ConvertSingletonListToEmbeddedObject(pc *config.Provider, startPath, licenseHeaderPath string) error {
31+
func ApplyAPIConverters(pc *config.Provider, startPath, licenseHeaderPath string) error {
3232
resourceRegistry := prepareResourceRegistry(pc)
3333

3434
var license string
@@ -64,15 +64,15 @@ func ConvertSingletonListToEmbeddedObject(pc *config.Provider, startPath, licens
6464
return nil
6565
}
6666

67-
newPath := strings.ReplaceAll(path, examples[0].GroupVersionKind().Version, rootResource.Version)
68-
if path == newPath {
69-
return nil
70-
}
7167
annotationValue := strings.ToLower(fmt.Sprintf("%s/%s/%s", rootResource.ShortGroup, rootResource.Version, rootResource.Kind))
7268
for _, e := range examples {
7369
if resource, ok := resourceRegistry[fmt.Sprintf("%s/%s", e.GroupVersionKind().Kind, e.GroupVersionKind().Group)]; ok {
7470
conversionPaths := resource.CRDListConversionPaths()
75-
if conversionPaths != nil && e.GroupVersionKind().Version != resource.Version {
71+
// if the latest version has conversions, run the conversions on the
72+
// example manifest.
73+
// Please note that only the version being generated (latest version)
74+
// is processed.
75+
if conversionPaths != nil && e.GroupVersionKind().Version == resource.Version {
7676
for i, cp := range conversionPaths {
7777
// Here, for the manifests to be converted, only `forProvider
7878
// is converted, assuming the `initProvider` field is empty in the
@@ -100,7 +100,7 @@ func ConvertSingletonListToEmbeddedObject(pc *config.Provider, startPath, licens
100100
}
101101
}
102102
convertedFileContent = license + "\n\n"
103-
if err := writeExampleContent(path, convertedFileContent, examples, newPath); err != nil {
103+
if err := writeExampleContent(path, convertedFileContent, examples, path); err != nil {
104104
return errors.Wrap(err, "failed to write example content")
105105
}
106106
}

pkg/pipeline/run.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func Run(pc *config.Provider, rootDir string) { //nolint:gocyclo
102102
panic(errors.Wrapf(err, "cannot insert type definitions from the previous versions into the package scope for group %q", group))
103103
}
104104

105+
modulePathControllers := filepath.Join(pc.ModulePath, "internal", "controller")
105106
for _, name := range sortedResources(resources) {
106107
paramTypeName, err := crdGen.Generate(resources[name])
107108
if err != nil {
@@ -125,7 +126,11 @@ func Run(pc *config.Provider, rootDir string) { //nolint:gocyclo
125126
panic(errors.Wrapf(err, "cannot generate controller for resource %s", name))
126127
}
127128
controllerPkgMap[shortGroup] = append(controllerPkgMap[shortGroup], ctrlPkgPath)
128-
controllerPkgMap[config.PackageNameMonolith] = append(controllerPkgMap[config.PackageNameMonolith], ctrlPkgPath)
129+
// if the controller is not already added as a base package controller
130+
// to the monolith provider.
131+
if len(pc.BasePackages.ControllerMap[strings.TrimPrefix(ctrlPkgPath, strings.TrimSuffix(modulePathControllers, "/")+"/")]) == 0 {
132+
controllerPkgMap[config.PackageNameMonolith] = append(controllerPkgMap[config.PackageNameMonolith], ctrlPkgPath)
133+
}
129134
if err := exampleGen.Generate(group, version, resources[name]); err != nil {
130135
panic(errors.Wrapf(err, "cannot generate example manifest for resource %s", name))
131136
}

pkg/terraform/errors/fw_diag.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package errors
6+
7+
import (
8+
"strings"
9+
10+
"github.com/crossplane/crossplane-runtime/pkg/errors"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
)
13+
14+
// FrameworkDiagnosticsError returns an error representing
15+
// the collected diagnostics at SeverityError level.
16+
func FrameworkDiagnosticsError(parentMessage string, diags diag.Diagnostics) error {
17+
eds := diags.Errors()
18+
if len(eds) == 0 {
19+
return nil
20+
}
21+
22+
errs := make([]error, 0, len(eds)+1)
23+
errs = append(errs, errors.New(parentMessage))
24+
for _, d := range eds {
25+
errs = append(errs, errors.New(frameworkDiagnosticString(d)))
26+
}
27+
return errors.Join(errs...)
28+
}
29+
30+
// frameworkDiagnosticString formats the given framework Diagnostic
31+
// as a string <summary>[: <detail>][: <path>].
32+
func frameworkDiagnosticString(d diag.Diagnostic) string {
33+
var msg strings.Builder
34+
msg.WriteString(strings.TrimSpace(d.Summary()))
35+
36+
detail := strings.TrimSpace(d.Detail())
37+
if detail != "" {
38+
msg.WriteString(": ")
39+
msg.WriteString(detail)
40+
}
41+
42+
if p, ok := d.(diag.DiagnosticWithPath); ok {
43+
wp := strings.TrimSpace(p.Path().String())
44+
if wp != "" {
45+
msg.WriteString(": ")
46+
msg.WriteString(wp)
47+
}
48+
}
49+
50+
return msg.String()
51+
}

0 commit comments

Comments
 (0)