Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from SDKv2 => Framework #249

Merged
merged 104 commits into from Jul 1, 2022
Merged

Migrate from SDKv2 => Framework #249

merged 104 commits into from Jul 1, 2022

Conversation

bendbennett
Copy link
Contributor

@bendbennett bendbennett commented May 3, 2022

Objectives

Items Requiring Resolving

Breaking Changes

As described in the CHANGELOG:

NOTES:

* Provider has been re-written using the new [`terraform-plugin-framework`](https://www.terraform.io/plugin/framework) ([#177](https://github.com/hashicorp/terraform-provider-random/pull/177)).

BREAKING CHANGES:

* [Terraform `>=1.0`](https://www.terraform.io/language/upgrade-guides/1-0) is now required to use this provider.

* resource/random_password: Deprecated attribute `number` has been removed ([266](https://github.com/hashicorp/terraform-provider-random/issues/266)).
* resource/random_string: Deprecated attribute `number` has been removed ([266](https://github.com/hashicorp/terraform-provider-random/issues/266)). 

Closes #177

@bendbennett bendbennett force-pushed the bendbennett/issues-177 branch 3 times, most recently from 550bbcb to 97ed28f Compare May 6, 2022 08:48
@bendbennett bendbennett marked this pull request as ready for review May 9, 2022 08:04
@bendbennett bendbennett requested a review from a team as a code owner May 9, 2022 08:04
@bendbennett bendbennett requested review from detro and bflad May 9, 2022 08:04
Copy link
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments and nitpicks. In general this is looking really good.

Aside from those minor things, I do have a question (I might have asked this before already, so I apologise if I did): what's the plan in terms of Protocol 5 -> 6 support?

As part of the "Utility Providers RFC" we established we would be moving to 6 also as a way to "nudge" the ecosystem.

Is the intention here to do one last Protocol 5 release and then upgrade to 6, by removing mux? Or do you have a different plan?

.github/workflows/test.yml Outdated Show resolved Hide resolved
internal/provider/provider.go Outdated Show resolved Hide resolved
internal/provider/resource_id.go Outdated Show resolved Hide resolved
internal/provider/resource_id.go Outdated Show resolved Hide resolved
Comment on lines 157 to 182
func validatorMinInt(min int64) tfsdk.AttributeValidator {
return minIntValidator{min}
}

type minIntValidator struct {
val int64
}

func (m minIntValidator) Description(context.Context) string {
return "MinInt validator ensures that attribute is at least val"
}

func (m minIntValidator) MarkdownDescription(context.Context) string {
return "MinInt validator ensures that attribute is at least `val`"
}

func (m minIntValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
t := req.AttributeConfig.(types.Int64)

if t.Value < m.val {
resp.Diagnostics.AddError(
fmt.Sprintf("expected attribute to be at least %d, got %d", m.val, t.Value),
fmt.Sprintf("expected attribute to be at least %d, got %d", m.val, t.Value),
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Would it make sense to have the Validators and PlanModifiers into their own files?
Like a validators.go and a plan_modifiers.go?

I might be wrong but they feel a bit "out of their context" in here, given that some are also used in other contexts (not just random_string and random_password).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I agree with the above. It'll also help us as we consider making a corpus of "common" validators in the near future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me. Have moved validators and plan modifiers to separate files.

main.go Outdated Show resolved Hide resolved
Copy link
Member

@bflad bflad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also like to discuss the merits of trying to do this in a minor release versus major release, since the migration testing is currently manual and there could be unexpected value behaviors when upgrading.

Comment on lines 113 to 119
resp.Diagnostics.AddError(
"generated insufficient random bytes: %s",
fmt.Sprintf("generated insufficient random bytes: %s", err),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Diagnostic summaries should generally be a "title" of sorts, e.g. what's the higher level issue?, leaving the details to explain the actual problem, offer troubleshooting advice, and/or next steps. Similar comment applies to any other place that's copying contents between summary and details.

For something like this, it'd be great to say something like:

Suggested change
resp.Diagnostics.AddError(
"generated insufficient random bytes: %s",
fmt.Sprintf("generated insufficient random bytes: %s", err),
)
resp.Diagnostics.AddError(
"Randomness Generation Error",
"While attempting to generate a random value for this resource, an insufficient number of random bytes were generated. Most commonly, this is a hardware or operating system issue where their random number generator does not provide a sufficient randomness source. Otherwise, it may represent an issue in the randomness handling of the provider.\n\n"+
"Retry the Terraform operation. If the error still occurs or happens regularly, please contact the provider developer with hardware and operating system information.\n\n"+
fmt.Sprintf("Original Error: %s", err)
)

If desired, this diagnostic can then be wrapped in a quick function and reused for other resources.

Reference: https://www.terraform.io/plugin/framework/diagnostics

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used your wording and expanded the summary and detail messages for other diagnostics errors.

@@ -4,17 +4,19 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
hex2 "encoding/hex"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general with Go, it is better to avoid renaming imports, especially those from standard library. Instead I'd recommend renaming the variable that's trying to use hex instead. 👍

Reference: https://github.com/golang/go/wiki/CodeReviewComments#imports

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops. My bad. Fixed.

internal/provider/resource_integer.go Show resolved Hide resolved
Comment on lines 157 to 182
func validatorMinInt(min int64) tfsdk.AttributeValidator {
return minIntValidator{min}
}

type minIntValidator struct {
val int64
}

func (m minIntValidator) Description(context.Context) string {
return "MinInt validator ensures that attribute is at least val"
}

func (m minIntValidator) MarkdownDescription(context.Context) string {
return "MinInt validator ensures that attribute is at least `val`"
}

func (m minIntValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
t := req.AttributeConfig.(types.Int64)

if t.Value < m.val {
resp.Diagnostics.AddError(
fmt.Sprintf("expected attribute to be at least %d, got %d", m.val, t.Value),
fmt.Sprintf("expected attribute to be at least %d, got %d", m.val, t.Value),
)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I agree with the above. It'll also help us as we consider making a corpus of "common" validators in the near future.

}

func (m minIntValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
t := req.AttributeConfig.(types.Int64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd strongly encourage avoiding a direct type assertion in validators. This can cause a panic if it receives an unexpected type. My recommendation would be to at least use the two-value form of type assertion to return an error on an unexpected type and return early, e.g.

t, ok := req.AttributeConfig.(types.Int64)

if !ok {
  resp.Diagnostics.AddError()
  return
}

Or use something similar to the terraform.io documentation here with tfsdk.ValueAs():

var t types.Int64
diags := tfsdk.ValueAs(ctx, req.AttributeConfig, &t)
resp.Diagnostics.Append(diags...)

if diags.HasError() {
  return
}

Reference: https://go.dev/doc/effective_go#interface_conversions
Reference: https://www.terraform.io/plugin/framework/validation#creating-attribute-validators

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have amended using the latter of your suggestions.

@bendbennett
Copy link
Contributor Author

Left a few comments and nitpicks. In general this is looking really good.

Aside from those minor things, I do have a question (I might have asked this before already, so I apologise if I did): what's the plan in terms of Protocol 5 -> 6 support?

As part of the "Utility Providers RFC" we established we would be moving to 6 also as a way to "nudge" the ecosystem.

Is the intention here to do one last Protocol 5 release and then upgrade to 6, by removing mux? Or do you have a different plan?

TBH the mux is just present as I was using this whilst migrating one resource at a time from SDKv2 to Framework. As @bflad indicates, probably best to remove the muxing and do a breaking change release.

@bendbennett
Copy link
Contributor Author

I'd also like to discuss the merits of trying to do this in a minor release versus major release, since the migration testing is currently manual and there could be unexpected value behaviors when upgrading.

Agree, that it's probably best to remove the muxing and have a breaking change major release.

@bendbennett
Copy link
Contributor Author

I have separated out the resources into their own packages as it seems like there are details that are specific to the resources that shouldn’t be exposed, for instance, models and unexported funcs (e.g., upgradePasswordStateV1toV2(), generateHash() in password), but I'm happy to refactor if people would rather we kept the resources with the provider.

@bendbennett bendbennett requested review from detro and bflad June 27, 2022 12:56
Copy link
Contributor

@detro detro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A suggestion and a nitpick/question about the code restructuring.

@@ -17,5 +20,10 @@ import (
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs

func main() {
plugin.Serve(&plugin.ServeOpts{ProviderFunc: provider.New})
err := providerserver.Serve(context.Background(), provider.New, providerserver.ServeOpts{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding the -debug flag here, similarly to https://github.com/hashicorp/terraform-provider-tls/blob/main/main.go#L16 ?

This is not strictly in topic with this PR, but it seems small enough that I thought I'd mention it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout, have added.

@@ -0,0 +1,98 @@
package provider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: General about the new code structure: what motivated moving resources into their own separate sub-packages, and away from their tests?

Asking just because it looks a bit unconventional, but there is nothing bad per se.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe one thing that this causes, is that it removes the possibility to just see at the diff within the same files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have separated out the resources into their own packages as it seems like there are details that are specific to the resources that shouldn’t be exposed, for instance, models and unexported funcs (e.g., upgradePasswordStateV1toV2(), generateHash() in password), but I'm happy to refactor if people would rather we kept the resources with the provider.

In terms of the tests, I must admit I don't really see them as "resource" tests, we're exercising the provider and a resource it exposes (e.g., password, string etc) through the interaction with TF core , so I see them as provider tests. The tests that are specific to resources themselves (e.g., TestUpgradePasswordStateV0toV2) still live with the resource code, but there's an argument for removing these sorts of tests as they're essentially testing "private" (unexported) functions which is generally frowned upon.

Another, indirectly related aspect of the refactoring has to do with import cycles. Perhaps one of the reasons a provider and its resources normally reside within the same package (e.g., package provider) is to avoid the issue of import cycles when a resource needs to use the API client that is typically configured in the Configure() func, for instance:

func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
    p.client = client.NewClient()
}

Access to the client is then usually achieved in the resource by using something like the following in the NewResource() func:

func (r resource) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
    return resource{
        p: *(p.(*provider)),
    }, nil
}

This seems a slightly peculiar pattern to me, as we have a provider that has resources but each resource also contains the provider and accesses "private" (unexported) fields on the provider in order to get the client for instance.

Although a client is not being used by the random provider, I'd be keen to encourage a pattern whereby the client can be obtained from the provider by checking to see if it implements an interface that allows access to the client without exposing any of the underlying "private" (unexported) fields of the provider. Perhaps something along the following lines:

package client

type Client interface {
     Client() API
}

type API interface {
     GetString() string
}

func NewClient() *client {
    return &client{
        msg: "hello",
    }
}

type client struct {
    msg string
}

func (c *client) GetString() string {
     return c.msg
}
package provider

func NewProvider() tfsdk.Provider {
    return &provider{}
}

var _ tfsdk.Provider = (*provider)(nil)

type provider struct {
    client client.API
}

func (p *provider) Client() client.API {
    return p.client
}

func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
    p.client = client.NewClient()
}
package resource

func (r *resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
    c, ok := p.(client.Client)
    if !ok {
        panic("not ok")
    }

    return &resource{
        client: c.Client(),
    }, nil
}

var (
_ tfsdk.Resource                = (*resource)(nil)
_ tfsdk.ResourceWithImportState = (*resource)(nil)
)

type resource struct {
    client client.API
}

func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    fmt.Println(r.client.GetString())

So this is a very long-winded way of saying that the refactoring also encourages decoupling the provider and resources.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I can't argue against incapsulation: I frankly dislike how Go doesn't support controlling visibility more granularly within the same package, so I guess this is the most idiomatic way to go.

In the future I might copy this approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey folks 👋 I'd like to chat about this more, but it is probably easier to do over a higher bandwidth medium.

In Go terms, the only "publicly facing" things in a provider codebase would be anything outside the internal/ directory. As long as things abide by that, you can export all types, methods, etc. without suggesting any sort of exported API that needs to worry about compatibility. Historically, most small providers are implemented in a "flat" package structure, e.g. everything terraform-plugin-sdk/framework related in internal/provider, to make things easier for contributors and avoid overcomplicating things with extra interfaces, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bflad I've gone ahead and refactored to have all of the resources and provider in package provider as I think the case for making things easier for contributors is a fair one. If you can take another look and let me know what you think. Thanks.

Copy link
Member

@bflad bflad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 🚀 Excellent work

Comment on lines +31 to +38
func (d *defaultValueAttributePlanModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) {
// Do not set default if the attribute configuration has been set.
if !req.AttributeConfig.IsNull() {
return
}

resp.AttributePlan = d.val
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is okay for an attribute with RequiresReplace (as its used in this provider), however we should be careful to check whether the plan already has a known value if this code is moved to other implementations. Refer also to https://github.com/hashicorp/terraform-provider-tls/blob/27e3a082e712f81eb7f5bc126b3debdbedab550e/internal/provider/attribute_plan_modification/attribute_plan_modifiers.go#L37-L41

Comment on lines 12 to 14
"random": func() (tfprotov6.ProviderServer, error) {
return providerserver.NewProtocol6WithError(New())()
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

providerserver.NewProtocol6WithError() should already return the right thing to prevent the wrapping function 👍

Suggested change
"random": func() (tfprotov6.ProviderServer, error) {
return providerserver.NewProtocol6WithError(New())()
},
"random": providerserver.NewProtocol6WithError(New()),

Comment on lines 125 to 130
resp.Diagnostics.AddError(
"Randomness Generation Error",
"While attempting to generate a random value for this resource, an insufficient number of random bytes were generated. Most commonly, this is a hardware or operating system issue where their random number generator does not provide a sufficient randomness source. Otherwise, it may represent an issue in the randomness handling of the provider.\n\n"+
"Retry the Terraform operation. If the error still occurs or happens regularly, please contact the provider developer with hardware and operating system information.\n\n"+
fmt.Sprintf("Original Error: %s", err),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should this be using (or be added to) internal/diagnostics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 moved.

Check: resource.ComposeTestCheckFunc(
testAccResourceIntegerBasic("random_integer.integer_1"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on removing these 👍

@bendbennett bendbennett merged commit 6af97c9 into main Jul 1, 2022
@bendbennett bendbennett deleted the bendbennett/issues-177 branch July 1, 2022 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Migrate to terraform-plugin-framework
3 participants