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

tfsdk: Initial ImportResourceState support #149

Merged
merged 6 commits into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/149.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:breaking-change
tfsdk: `Resource` implementations must now include the `ImportState(context.Context, ImportResourceStateRequest, *ImportResourceStateResponse)` method. If import is not supported, call the `ResourceImportStateNotImplemented()` function or return an error.
```

```release-note:feature
tfsdk: Support resource import
```
14 changes: 14 additions & 0 deletions tfsdk/request_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tfsdk

// ImportResourceStateRequest represents a request for the provider to import a
// resource. An instance of this request struct is supplied as an argument to
// the Resource's ImportState method.
type ImportResourceStateRequest struct {
// ID represents the import identifier supplied by the practitioner when
// calling the import command. In many cases, this may align with the
// unique identifier for the resource, which can optionally be stored
// as an Attribute. However, this identifier can also be treated as
// its own type of value and parsed during import. This value
// is not stored in the state unless the provider explicitly stores it.
ID string
}
9 changes: 9 additions & 0 deletions tfsdk/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ type Resource interface {
// Delete is called when the provider must delete the resource. Config
// values may be read from the DeleteResourceRequest.
Delete(context.Context, DeleteResourceRequest, *DeleteResourceResponse)

// ImportState is called when the provider must import the resource.
//
// If import is not supported, it is recommended to use the
// ResourceImportStateNotImplemented() call in this method.
//
// If setting an attribute with the import identifier, it is recommended
// to use the ResourceImportStatePassthroughID() call in this method.
ImportState(context.Context, ImportResourceStateRequest, *ImportResourceStateResponse)
}

// ResourceWithModifyPlan represents a resource instance with a ModifyPlan
Expand Down
37 changes: 37 additions & 0 deletions tfsdk/resource_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tfsdk

import (
"context"

"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// ResourceImportStateNotImplemented is a helper function to return an error
// diagnostic about the resource not supporting import. The details defaults
// to a generic message to contact the provider developer, but can be
// customized to provide specific information or recommendations.
func ResourceImportStateNotImplemented(ctx context.Context, details string, resp *ImportResourceStateResponse) {
if details == "" {
details = "This resource does not support import. Please contact the provider developer for additional information."
}

resp.Diagnostics.AddError(
"Resource Import Not Implemented",
details,
)
}

// ResourceImportStatePassthroughID is a helper function to set the import
// identifier to a given state attribute path. The attribute must accept a
// string value.
func ResourceImportStatePassthroughID(ctx context.Context, path *tftypes.AttributePath, req ImportResourceStateRequest, resp *ImportResourceStateResponse) {
if path == nil || tftypes.NewAttributePath().Equal(path) {
resp.Diagnostics.AddError(
"Resource Import Passthrough Missing Attribute Path",
"This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"Resource ImportState method call to ResourceImportStatePassthroughID path must be set to a valid attribute path that can accept a string value.",
)
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path, req.ID)...)
}
21 changes: 21 additions & 0 deletions tfsdk/response_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tfsdk

import (
"github.com/hashicorp/terraform-plugin-framework/diag"
)

// ImportResourceStateResponse represents a response to a ImportResourceStateRequest.
// An instance of this response struct is supplied as an argument to the
// Resource's ImportState method, in which the provider should set values on
// the ImportResourceStateResponse as appropriate.
type ImportResourceStateResponse struct {
// Diagnostics report errors or warnings related to importing the
// resource. An empty slice indicates a successful operation with no
// warnings or errors generated.
Diagnostics diag.Diagnostics

// State is the state of the resource following the import operation.
// It must contain enough information so Terraform can successfully
// refresh the resource, e.g. call the Resource Read method.
State State
}
8 changes: 0 additions & 8 deletions tfsdk/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -1122,14 +1122,6 @@ func (s *server) applyResourceChange(ctx context.Context, req *tfprotov6.ApplyRe
}
}

func (s *server) ImportResourceState(ctx context.Context, _ *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) {
// uncomment when we implement this function
//ctx = s.registerContext(ctx)

// TODO: support resource importing
return &tfprotov6.ImportResourceStateResponse{}, nil
}

// validateDataResourceConfigResponse is a thin abstraction to allow native Diagnostics usage
type validateDataResourceConfigResponse struct {
Diagnostics diag.Diagnostics
Expand Down
1 change: 1 addition & 0 deletions tfsdk/serve_data_source_config_validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type testServeDataSourceConfigValidators struct {
}

func (r testServeDataSourceConfigValidators) Read(ctx context.Context, req ReadDataSourceRequest, resp *ReadDataSourceResponse) {
// Intentionally blank. Not expected to be called during testing.
}

func (r testServeDataSourceConfigValidators) ConfigValidators(ctx context.Context) []DataSourceConfigValidator {
Expand Down
1 change: 1 addition & 0 deletions tfsdk/serve_data_source_validate_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type testServeDataSourceValidateConfig struct {
}

func (r testServeDataSourceValidateConfig) Read(ctx context.Context, req ReadDataSourceRequest, resp *ReadDataSourceResponse) {
// Intentionally blank. Not expected to be called during testing.
}

func (r testServeDataSourceValidateConfig) ValidateConfig(ctx context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) {
Expand Down
128 changes: 128 additions & 0 deletions tfsdk/serve_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package tfsdk

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// importedResource represents a resource that was imported.
//
// This type is not exported as the framework import implementation is
// currently designed for the most common use case of single resource import.
type importedResource struct {
Private []byte
State State
TypeName string
}

func (r importedResource) toTfprotov6(ctx context.Context) (*tfprotov6.ImportedResource, diag.Diagnostics) {
var diags diag.Diagnostics
irProto6 := &tfprotov6.ImportedResource{
Private: r.Private,
TypeName: r.TypeName,
}

stateProto6, err := tfprotov6.NewDynamicValue(r.State.Schema.TerraformType(ctx), r.State.Raw)

if err != nil {
diags.AddError(
"Error converting imported resource response",
"An unexpected error was encountered when converting the imported resource response to a usable type. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+err.Error(),
)
return nil, diags
}

irProto6.State = &stateProto6

return irProto6, diags
}

// importResourceStateResponse is a thin abstraction to allow native Diagnostics usage
type importResourceStateResponse struct {
Diagnostics diag.Diagnostics
ImportedResources []importedResource
}

func (r importResourceStateResponse) toTfprotov6(ctx context.Context) *tfprotov6.ImportResourceStateResponse {
resp := &tfprotov6.ImportResourceStateResponse{
Diagnostics: r.Diagnostics.ToTfprotov6Diagnostics(),
}

for _, ir := range r.ImportedResources {
irProto6, diags := ir.toTfprotov6(ctx)
resp.Diagnostics = append(resp.Diagnostics, diags.ToTfprotov6Diagnostics()...)
resp.ImportedResources = append(resp.ImportedResources, irProto6)
}

return resp
}

func (s *server) importResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest, resp *importResourceStateResponse) {
resourceType, diags := s.getResourceType(ctx, req.TypeName)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

resourceSchema, diags := resourceType.GetSchema(ctx)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

resource, diags := resourceType.NewResource(ctx, s.p)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

emptyState := tftypes.NewValue(resourceSchema.TerraformType(ctx), nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

A thing worth thinking about here:

We need a resolution to #148 to make this work the way we want.

Another option is to have it do something like:

vals := map[string]tftypes.Value{}
typ := resourceSchema.TerraformType(ctx)
for name, t := range typ.(tftypes.Object).AttributeTypes {
  vals[name] = tftypes.NewValue(t, nil)
}
emptyState := tftypes.NewValue(typ, vals)

This matches the more-expected (imho) default state, which is an object in state with nothing filled in, instead of nothing in state.

However, this is weird during failure cases. I'm not clear on whether Terraform will persist the object to state or write nothing to state in the face of an error diagnostic. If the former, we should default to an empty state and just rely on #148. If the latter, we should confirm that will continue to be the case, and maybe defaulting to the empty object (instead of the null object) is more expected? I dunno. I don't know that it makes a huge difference, just wanted to call it out here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current logic in Terraform CLI is to return early on ImportResourceState error diagnostics, before saving state information:

https://github.com/hashicorp/terraform/blob/2afa0a5e75d76de7e47157114c579b3b1bff994f/internal/terraform/transform_import_state.go#L141-L154

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 how do we feel about just leaving it and waiting for feedback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're going to merge this in as-is for now and iterate if necessary.

If we do wind up switching the implementation here in the future -- maybe creating the above code snippet as a method such as (Schema).newState() would be good to try and prevent "empty" object problems here and in any other potential places that might need it. That might be something we could export if/when multiple resource import support makes sense.

importReq := ImportResourceStateRequest{
ID: req.ID,
}
importResp := ImportResourceStateResponse{
State: State{
Raw: emptyState,
Schema: resourceSchema,
},
}

resource.ImportState(ctx, importReq, &importResp)
resp.Diagnostics.Append(importResp.Diagnostics...)

if resp.Diagnostics.HasError() {
return
}

if importResp.State.Raw.Equal(emptyState) {
resp.Diagnostics.AddError(
"Missing Resource Import State",
"An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+
"Resource ImportState method returned no State in response. If import is intentionally not supported, call the ResourceImportStateNotImplemented() function or return an error.",
)
return
}

resp.ImportedResources = []importedResource{
{
State: importResp.State,
TypeName: req.TypeName,
},
}
}

// ImportResourceState satisfies the tfprotov6.ProviderServer interface.
func (s *server) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) {
ctx = s.registerContext(ctx)
resp := &importResourceStateResponse{}

s.importResourceState(ctx, req, resp)

return resp.toTfprotov6(ctx), nil
}
Loading