# README

SDK for Strongly-Typed Resources

This package is a prototype for creating strongly-typed Data Sources and Resources - and in future will likely form the foundation for Terraform Data Sources and Resources in this Provider going forward.

Should I use this package to build resources?

Not at this time - please use Terraform's Plugin SDK instead - reference examples can be found in ./internal/services/notificationhub.

More documentation for this package will ship in the future when this is ready for general use.


What's the long-term intention for this package?

Each Service Package contains the following:

  • Client - giving reference to the SDK Client which should be used to interact with Azure
  • ID Parsers, Formatters and a Validator - giving a canonical ID for each Resource
  • Validation functions specific to this service package, for example for the Name

This package can be used to tie these together in a more strongly typed fashion, for example:

package example

import (
	"context"
	"fmt"
	"time"

	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-06-01/resources"
	"github.com/hashicorp/go-azure-helpers/lang/response"
	"github.com/hashicorp/terraform-provider-azurestack/internal/location"
	"github.com/hashicorp/terraform-provider-azurestack/internal/sdk"
	"github.com/hashicorp/terraform-provider-azurestack/internal/services/resource/parse"
	"github.com/hashicorp/terraform-provider-azurestack/internal/services/resource/validate"

	"github.com/hashicorp/terraform-provider-azurestack/internal/tf/pluginsdk"
	
)

type ResourceGroup struct {
	Name     string            `tfschema:"name"`
	Location string            `tfschema:"location"`
	Tags     map[string]string `tfschema:"tags"`
}

type ResourceGroupResource struct {
}

func (r ResourceGroupResource) Arguments() map[string]*pluginsdk.Schema {
	return map[string]*pluginsdk.Schema{
		"name": {
			Type:     pluginsdk.TypeString,
			Required: true,
		},

		"location": location.Schema(),

		"tags": tags.Schema(),
	}
}

func (r ResourceGroupResource) Attributes() map[string]*pluginsdk.Schema {
	return map[string]*pluginsdk.Schema{}
}

func (r ResourceGroupResource) ResourceType() string {
	return "azurestack_example"
}

func (r ResourceGroupResource) Create() sdk.ResourceFunc {
	return sdk.ResourceFunc{
		Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
			metadata.Logger.Info("Decoding state..")
			var state ResourceGroup
			if err := metadata.Decode(&state); err != nil {
				return err
			}

			metadata.Logger.Infof("creating Resource Group %q..", state.Name)
			client := metadata.Client.Resource.GroupsClient
			subscriptionId := metadata.Client.Account.SubscriptionId

			id := parse.NewResourceGroupID(subscriptionId, state.Name)
			existing, err := client.Get(ctx, state.Name)
			if err != nil && !utils.ResponseWasNotFound(existing.Response) {
				return fmt.Errorf("checking for the presence of an existing Resource Group %q: %+v", state.Name, err)
			}
			if !utils.ResponseWasNotFound(existing.Response) {
				return metadata.ResourceRequiresImport(r.ResourceType(), id)
			}

			input := resources.Group{
				Location: pointer.FromString(state.Location),
				Tags:     tags.FromTypedObject(state.Tags),
			}
			if _, err := client.CreateOrUpdate(ctx, state.Name, input); err != nil {
				return fmt.Errorf("creating Resource Group %q: %+v", state.Name, err)
			}

			metadata.SetID(id)
			return nil
		},
		Timeout: 30 * time.Minute,
	}
}

func (r ResourceGroupResource) Read() sdk.ResourceFunc {
	return sdk.ResourceFunc{
		Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
			client := metadata.Client.Resource.GroupsClient
			id, err := parse.ResourceGroupID(metadata.ResourceData.Id())
			if err != nil {
				return err
			}

			metadata.Logger.Infof("retrieving Resource Group %q..", id.ResourceGroup)
			group, err := client.Get(ctx, id.ResourceGroup)
			if err != nil {
				if utils.ResponseWasNotFound(group.Response) {
					metadata.Logger.Infof("%s was not found - removing from state!", *id)
					return metadata.MarkAsGone(id)
				}

				return fmt.Errorf("retrieving %s: %+v", *id, err)
			}

			return metadata.Encode(&ResourceGroup{
				Name:     id.ResourceGroup,
				Location: location.NormalizeNilable(group.Location),
				Tags:     tags.ToTypedObject(group.Tags),
			})
		},
		Timeout: 5 * time.Minute,
	}
}

func (r ResourceGroupResource) Update() sdk.ResourceFunc {
	return sdk.ResourceFunc{
		Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
			id, err := parse.ResourceGroupID(metadata.ResourceData.Id())
			if err != nil {
				return err
			}

			metadata.Logger.Info("Decoding state..")
			var state ResourceGroup
			if err := metadata.Decode(&state); err != nil {
				return err
			}

			metadata.Logger.Infof("updating %s..", *id)
			client := metadata.Client.Resource.GroupsClient

			input := resources.GroupPatchable{
				Tags: tags.FromTypedObject(state.Tags),
			}

			if _, err := client.Update(ctx, id.ResourceGroup, input); err != nil {
				return fmt.Errorf("updating %s: %+v", *id, err)
			}

			return nil
		},
		Timeout: 30 * time.Minute,
	}
}

func (r ResourceGroupResource) Delete() sdk.ResourceFunc {
	return sdk.ResourceFunc{
		Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
			client := metadata.Client.Resource.GroupsClient
			id, err := parse.ResourceGroupID(metadata.ResourceData.Id())
			if err != nil {
				return err
			}

			metadata.Logger.Infof("deleting %s..", *id)
			future, err := client.Delete(ctx, id.ResourceGroup, "")
			if err != nil {
				return fmt.Errorf("deleting %s: %+v", *id, err)
			}

			metadata.Logger.Infof("waiting for the deletion of %s..", *id)
			if err := future.WaitForCompletionRef(ctx, client.Client); err != nil {
				return fmt.Errorf("waiting for deletion of %s: %+v", *id, err)
			}

			return nil
		},
		Timeout: 30 * time.Minute,
	}
}

func (r ResourceGroupResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
	return validate.ResourceGroupID
}

func (r ResourceGroupResource) ModelObject() interface{} {
	return ResourceGroup{}
}

The end result being the removal of a lot of common bugs by moving to a convention - for example:

  • The Context object passed into each method always has a deadline/timeout attached to it
  • The Read function is automatically called at the end of a Create and Update function - meaning users don't have to do this
  • Each Resource has to have an ID Formatter and Validation Function
  • The Model Object is validated via unit tests to ensure it contains the relevant struct tags (TODO: also confirming these exist in the state and are of the correct type, so no Set errors occur)

Ultimately this allows bugs to be caught by the Compiler (for example if a Read function is unimplemented) - or Unit Tests (for example should the tfschema struct tags be missing) - rather than during Provider Initialization, which reduces the feedback loop.

# Functions

NewDataSourceWrapper returns a DataSourceWrapper for this Data Source implementation.
NewResourceWrapper returns a ResourceWrapper for this Resource implementation.
ValidateModelObject validates that the object contains the specified `tfschema` tags required to be used with the Encode and Decode functions.

# Structs

ConsoleLogger provides a Logger implementation which writes the log messages to StdOut - in Terraform's perspective that's proxied via the Plugin SDK.
DataSourceWrapper is a wrapper for converting a DataSource implementation into the object used by the Terraform Plugin SDK.
No description provided by the author
NullLogger disregards the log output - and is intended to be used when the contents of the debug logger aren't interesting to reduce console output.
No description provided by the author
No description provided by the author
ResourceWrapper is a wrapper for converting a Resource implementation into the object used by the Terraform Plugin SDK.
No description provided by the author

# Interfaces

A Data Source is an object which looks up information about an existing resource and returns this information for use elsewhere Notably not all Terraform Resources/Azure API's make sense as a Data Source - this information has to be available consistently since these are queried on-demand.
Logger is an interface for switching out the Logger implementation.
A Resource is an object which can be provisioned and managed by Terraform that is, Created, Retrieved, Deleted, Imported (and optionally, Updated, by implementing the 'ResourceWithUpdate' interface) It's worth calling out that not all Azure API's make sense as Terraform Resources - as a general rule if it supports CR(U)D it could, however.
No description provided by the author
ResourceWithCustomizeDiff is an optional interface.
ResourceWithDeprecationAndNoReplacement is an optional interface Resources implementing this interface will be marked as Deprecated and output the DeprecationMessage during Terraform operations.
ResourceWithDeprecationReplacedBy is an optional interface Resources implementing this interface will be marked as Deprecated and output the DeprecationMessage during Terraform operations.
No description provided by the author
ResourceWithUpdate is an optional interface Notably the Arguments for Resources implementing this interface cannot be entirely ForceNew - else this interface implementation is superfluous.
TypedServiceRegistration is a Service Registration using Types meaning that we can abstract on top of the Plugin SDK and use Native Types where possible.
UntypedServiceRegistration is the interface used for untyped/raw Plugin SDK resources in the future this'll be superseded by the TypedServiceRegistration which allows for stronger Typed resources to be used.

# Type aliases

ResourceRunFunc is the function which can be run ctx provides a Context instance with the user-provided timeout metadata is a reference to an object containing the Client, ResourceData and a Logger.