Whilst it is acceptable in certain cases to map the schema of a new resource or feature when extending an existing resource, one-to-one from the Azure API, in the majority of cases more consideration needs to be given how to expose the Azure API in Terraform so that the provider presents a consistent and intuitive experience to the end user.
Below are a list of common patterns found in the Azure API and how these typically get mapped within Terraform.
It is commonplace for features to be toggled on and off by an Enabled
property within an object in the SDK used to interact with the Azure API. See the examples below.
Example A.
type ManagedClusterStorageProfileBlobCSIDriver struct {
Enabled *bool `json:"enabled,omitempty"`
}
Example B.
type ManagedClusterWorkloadAutoScalerProfileVerticalPodAutoscaler struct {
ControlledValues ControlledValues `json:"controlledValues"`
Enabled bool `json:"enabled"`
UpdateMode UpdateMode `json:"updateMode"`
}
Although there are still resources within the provider where this property is exposed in the Terraform schema, the provider is moving away from this and instead translates this behaviour in one of two ways.
In cases where Enabled
is the only field required to turn a feature on and off we opt to flatten the block into a single top level property (or higher level property if already nested inside a block). So in the case of Example A, this would become:
"storage_blob_driver_enabled": {
Type: pluginsdk.TypeBool,
Optional: true,
Default: false,
},
For features that can accept or require configuration, i.e. the object contains additional properties other than Enabled
like in Example B, the behaviour should be such that when the block is present the feature is enabled, and when it is absent it is disabled. The corresponding Terraform schema would be as follows:
"vertical_pod_autoscaler": {
Type: pluginsdk.TypeList,
Optional: true,
MaxItems: 1,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"update_mode": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string(managedclusters.UpdateModeAuto),
string(managedclusters.UpdateModeInitial),
string(managedclusters.UpdateModeRecreate),
}, false),
},
"controlled_values": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string(managedclusters.ControlledValuesRequestsAndLimits),
string(managedclusters.ControlledValuesRequestsOnly),
}, false),
},
},
},
},
There are instances where configuration properties for a feature is optional, as shown below.
Example C.
type ManagedClusterStorageProfileDiskCSIDriver struct {
Enabled *bool `json:"enabled,omitempty"`
Version *string `json:"version,omitempty"`
}
In cases like these one option is to flatten the block into two top level properties.
"storage_disk_driver_enabled": {
Type: pluginsdk.TypeBool,
Optional: true,
Default: false,
},
"storage_disk_driver_version": {
Type: pluginsdk.TypeString,
Optional: true,
Default: "V1",
ValidateFunc: validation.StringInSlice([]string{
"V1",
"V2",
}, false),
},
Depending on the behaviour of the Azure API and the default set by it, a worthwhile alternative is to set the property as required in the Terraform schema, to avoid having to set empty blocks to enable features.
"storage_disk_driver": {
Type: pluginsdk.TypeList,
Optional: true,
MaxItems: 1,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"version": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
"V1",
"V2",
}, false),
},
},
},
},
Many Azure APIs and services will accept the values like None
, Off
, or Default
as a default value and expose it as a constant in the API specification.
"shutdownOnIdleMode": {
"type": "string",
"enum": [
"None",
"UserAbsence",
"LowUsage"
],
Whilst it isn't uncommon to stumble across older resources in the provider that expose and accept these as a valid values, the provider is moving away from this pattern, since Terraform has its own null type i.e. by omitting the field. Existing None
, Off
or Default
values within the provider are planned for removal in version 4.0.
This ultimately means that the end user doesn't need to bloat their configuration with superfluous information that is implied through the omission of information.
The resulting schema in Terraform would look as follows and also requires a conversion between the Terraform null value and None
within the Create and Read functions.
// How the property is exposed in the schema
"shutdown_on_idle": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{
string(labplan.ShutdownOnIdleModeUserAbsence),
string(labplan.ShutdownOnIdleModeLowUsage),
// NOTE: Whilst the `None` value exists it's handled in the Create/Update and Read functions.
// string(labplan.ShutdownOnIdleModeNone),
}, false),
},
// Normalising in the create or expand function
func (r resource) Create() sdk.ResourceFunc {
...
var config resourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
// The resource property shutdown_on_idle maps to the attribute shutdownOnIdle in the defined model for a typed resource in this example
shutdownOnIdle := string(labplan.ShutdownOnIdleModeNone)
if v := model.ShutdownOnIdle; v != "" {
shutdownOnIdle = v
}
...
}
// Normalising in the read or flatten function
func (r resource) Read() sdk.ResourceFunc {
...
shutdownOnIdle := ""
if v := props.ShutdownOnIdle; v != nil && v != string(labplan.ShutdownOnIdleModeNone) {
shutdownOnIdle = string(*v)
}
state.ShutdownOnIdle = shutdownOnIdle
...
}