Skip to content
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
28 changes: 28 additions & 0 deletions list/config_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list

import "context"

// ConfigValidator describes reusable ListResource configuration validation
// functionality.
type ConfigValidator interface {
// Description describes the validation in plain text formatting.
//
// This information may be automatically added to list resource plain text
// descriptions by external tooling.
Description(context.Context) string

// MarkdownDescription describes the validation in Markdown formatting.
//
// This information may be automatically added to list resource Markdown
// descriptions by external tooling.
MarkdownDescription(context.Context) string

// ValidateResource performs the validation.
//
// This method name is separate from ConfigValidators in resource and other packages in
// order to allow generic validators.
ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse)
}
163 changes: 163 additions & 0 deletions list/list_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list

import (
"context"
"iter"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)

// ListResource represents an implementation of listing instances of a managed resource
// This is the core interface for all list resource implementations.
//
// ListResource implementations can optionally implement these additional concepts:
//
// - Configure: Include provider-level data or clients.
// - Validation: Schema-based or entire configuration via
// ListResourceWithConfigValidators or ListResourceWithValidateConfig.
type ListResource interface {
// Metadata should return the full name of the list resource such as
// examplecloud_thing. This name should match the full name of the managed
// resource to be listed; otherwise, the GetMetadata RPC will return an
// error diagnostic.
//
// The method signature is intended to be compatible with the Metadata
// method signature in the Resource interface. One implementation of
// Metadata can satisfy both interfaces.
Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A trade-off of compatibility: a bit of overloading of resource.MetadataRequest and resource.MetadataResponse. I think it's ok. I'm also open to improving on this.

Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense, i briefly mentioned that you could share the interfaces internally if you wanted in our 1/1, but that would mean you couldn't write a descriptive package comment for the method, so not really a big deal to duplicate imo.


// ListResourceConfigSchema should return the schema for list blocks.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In ConfigValidators, we have the precedent of a comment about method naming, such as:

	// This method name is separate from ConfigValidators in resource and other packages in
	// order to allow generic validators.

This method is not named Schema for a similar reason. To comment or not to comment?

ListResourceConfigSchema(context.Context, ListResourceSchemaRequest, *ListResourceSchemaResponse)

// List is called when the provider must list instances of a managed
// resource type that satisfy a user-provided request.
List(context.Context, ListRequest, *ListResultsStream)
}

// ListResourceWithConfigure is an interface type that extends ListResource to include a method
// which the framework will automatically call so provider developers have the
// opportunity to setup any necessary provider-level data or clients.
type ListResourceWithConfigure interface {
ListResource

// Configure enables provider-level data or clients to be set. The method
// signature is intended to be compatible with the Configure method
// signature in the Resource interface. One implementation of Configure can
// satisfy both interfaces.
Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A trade-off of compatibility: a bit of overloading of resource.ConfigureRequest and resource.ConfigureResponse. I think it's ok. I'm also open to improving on this.

}

// ListResourceWithConfigValidators is an interface type that extends
// ListResource to include declarative validations.
//
// Declaring validation using this methodology simplifies implementation of
// reusable functionality. These also include descriptions, which can be used
// for automating documentation.
//
// Validation will include ListResourceConfigValidators and
// ValidateListResourceConfig, if both are implemented, in addition to any
// Attribute or Type validation.
type ListResourceWithConfigValidators interface {
ListResource

// ListResourceConfigValidators returns a list of functions which will all be performed during validation.
ListResourceConfigValidators(context.Context) []ConfigValidator
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In ConfigValidators, we have the precedent of a comment about method naming, such as:

	// This method name is separate from ConfigValidators in resource and other packages in
	// order to allow generic validators.

This method is not named ConfigValidators for a similar reason. To comment or not to comment?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah it might be worth being a little more specific in what these validators are used for at some point (i.e. list config validation when used with terraform query), then point to the other method name for resource config validation.

Same for the other methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 – will revisit the comments in a follow-up PR.

}

// ListResourceWithValidateConfig is an interface type that extends ListResource to include
// imperative validation.
//
// Declaring validation using this methodology simplifies one-off
// functionality that typically applies to a single resource. Any documentation
// of this functionality must be manually added into schema descriptions.
//
// Validation will include ListResourceConfigValidators and ValidateListResourceConfig, if both
// are implemented, in addition to any Attribute or Type validation.
type ListResourceWithValidateConfig interface {
ListResource

// ValidateListResourceConfig performs the validation.
ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In ConfigValidators, we have the precedent of a comment about method naming, such as:

	// This method name is separate from ConfigValidators in resource and other packages in
	// order to allow generic validators.

This method is not named ValidateConfig for a similar reason. To comment or not to comment?

}

// ListRequest represents a request for the provider to list instances
// of a managed resource type that satisfy a user-defined request. An instance
// of this reqeuest struct is passed as an argument to the provider's
// ListResource function implementation.
type ListRequest struct {
// Config is the configuration the user supplied for listing resource
// instances.
Config tfsdk.Config

// IncludeResource indicates whether the provider should populate the
// Resource field in the ListResult struct.
IncludeResource bool
}

// ListResultsStream represents a streaming response to a ListRequest.
// An instance of this struct is supplied as an argument to the provider's
// ListResource function implementation function. The provider should set a Results
// iterator function that yields zero or more results of type ListResult.
//
// For convenience, a provider implementation may choose to convert a slice of
// results into an iterator using [slices.Values].
//
// [slices.Values]: https://pkg.go.dev/slices#Values
type ListResultsStream struct {
// Results is a function that emits ListResult values via its yield
// function argument.
Results iter.Seq[ListResult]
}

// ListResult represents a listed managed resource instance.
type ListResult struct {
// Identity is the identity of the managed resource instance.
//
// A nil value will raise will raise a diagnostic.
Identity *tfsdk.ResourceIdentity

// Resource is the provider's representation of the attributes of the
// listed managed resource instance.
//
// If ListRequest.IncludeResource is true, a nil value will raise
// a warning diagnostic.
Resource *tfsdk.Resource

// DisplayName is a provider-defined human-readable description of the
// listed managed resource instance, intended for CLI and browser UIs.
DisplayName string

// Diagnostics report errors or warnings related to the listed managed
// resource instance. An empty slice indicates a successful operation with
// no warnings or errors generated.
Diagnostics diag.Diagnostics
}

// ValidateConfigRequest represents a request to validate the configuration of
// a list resource. An instance of this request struct is supplied as an
// argument to the ValidateListResourceConfig receiver method or automatically
// passed through to each ListResourceConfigValidator.
type ValidateConfigRequest struct {
// Config is the configuration the user supplied for the resource.
//
// This configuration may contain unknown values if a user uses
// interpolation or other functionality that would prevent Terraform
// from knowing the value at request time.
Config tfsdk.Config
}

// ValidateConfigResponse represents a response to a ValidateConfigRequest. An
// instance of this response struct is supplied as an argument to the
// list.ValidateListResourceConfig receiver method or automatically passed
// through to each ConfigValidator.
type ValidateConfigResponse struct {
// Diagnostics report errors or warnings related to validating the list
// configuration. An empty slice indicates success, with no warnings
// or errors generated.
Diagnostics diag.Diagnostics
}
51 changes: 51 additions & 0 deletions list/list_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list_test

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/list"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

type ComputeInstanceResource struct {
NoOpListResource
NoOpResource
}

type ComputeInstanceWithValidateListResourceConfig struct {
ComputeInstanceResource
}

type ComputeInstanceWithListResourceConfigValidators struct {
ComputeInstanceResource
}

func (c *ComputeInstanceResource) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) {
}

func (c *ComputeInstanceResource) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) {
}

func (c *ComputeInstanceWithValidateListResourceConfig) ValidateListResourceConfig(_ context.Context, _ list.ValidateConfigRequest, _ *list.ValidateConfigResponse) {
}

func (c *ComputeInstanceWithListResourceConfigValidators) ListResourceConfigValidators(_ context.Context) []list.ConfigValidator {
return nil
}

// ExampleResource_listable demonstrates a resource.Resource that implements
// list.ListResource interfaces.
func ExampleResource_listable() {
var _ list.ListResource = &ComputeInstanceResource{}
var _ list.ListResourceWithConfigure = &ComputeInstanceResource{}
var _ list.ListResourceWithValidateConfig = &ComputeInstanceWithValidateListResourceConfig{}
var _ list.ListResourceWithConfigValidators = &ComputeInstanceWithListResourceConfigValidators{}

var _ resource.Resource = &ComputeInstanceResource{}
var _ resource.ResourceWithConfigure = &ComputeInstanceResource{}

// Output:
}
18 changes: 18 additions & 0 deletions list/no_op_list_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list_test

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/list"
)

type NoOpListResource struct{}

func (*NoOpListResource) ListResourceConfigSchema(_ context.Context, _ list.ListResourceSchemaRequest, _ *list.ListResourceSchemaResponse) {
}

func (*NoOpListResource) List(_ context.Context, _ list.ListRequest, _ *list.ListResultsStream) {
}
27 changes: 27 additions & 0 deletions list/no_op_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list_test

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource"
)

type NoOpResource struct{}

func (*NoOpResource) Schema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) {
}

func (*NoOpResource) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) {
}

func (*NoOpResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) {
}

func (*NoOpResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
}

func (*NoOpResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
}
27 changes: 27 additions & 0 deletions list/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package list
Copy link
Member

Choose a reason for hiding this comment

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

A thought about the package location. Since list is coupled to managed resource (i.e. can't list without some managed resource existing in the provider), should we move all of these packages under resource?

resource/list
resource/list/schema

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 started from resource and ended up in a top-level list package.

I took a cue from: an instance of a "list resource" corresponds to a "list block" in Terraform.

So from a config perspective, list feels okay at the level of resource and ephemeral. And the import paths feel simpler.

And we leave the door open for our understanding of list to evolve.


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

// ListResourceSchemaRequest represents a request for the ListResource to
// return its schema. An instance of this request struct is supplied as an
// argument to the ListResource type ListResourceSchema method.
type ListResourceSchemaRequest struct{}

// ListResourceSchemaResponse represents a response to a
// ListResourceSchemaRequest. An instance of this response struct is supplied
// as an argument to the ListResource type ListResourceResourceSchema method.
type ListResourceSchemaResponse struct {
// Schema is the schema of the list resource.
Schema schema.Schema

// Diagnostics report errors or warnings related to retrieving the list
// resource schema. An empty slice indicates success, with no warnings
// or errors generated.
Diagnostics diag.Diagnostics
}
7 changes: 7 additions & 0 deletions list/schema/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

type Schema struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

💭 Clearly, this is a (very) partial implementation of a schema type 😃 In the interest of small pull requests, I'd like to save the details for the next PR.

}
53 changes: 53 additions & 0 deletions tfsdk/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfsdk

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// Resource represents a Terraform resource.
type Resource struct {
Raw tftypes.Value
Schema fwschema.Schema
}

// Get populates the struct passed as `target` with the resource.
func (c Resource) Get(ctx context.Context, target interface{}) diag.Diagnostics {
return c.data().Get(ctx, target)
}

// GetAttribute retrieves the attribute or block found at `path` and populates
// the `target` with the value. This method is intended for top level schema
// attributes or blocks. Use `types` package methods or custom types to step
// into collections.
//
// Attributes or elements under null or unknown collections return null
// values, however this behavior is not protected by compatibility promises.
func (c Resource) GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics {
return c.data().GetAtPath(ctx, path, target)
}

// PathMatches returns all matching path.Paths from the given path.Expression.
//
// If a parent path is null or unknown, which would prevent a full expression
// from matching, the parent path is returned rather than no match to prevent
// false positives.
func (c Resource) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) {
return c.data().PathMatches(ctx, pathExpr)
}

func (c Resource) data() fwschemadata.Data {
return fwschemadata.Data{
Description: fwschemadata.DataDescriptionConfiguration,
Schema: c.Schema,
TerraformValue: c.Raw,
}
}
Loading