diff --git a/Makefile b/Makefile index ab61c7b..399c132 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,4 @@ lint: docs: go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs tfplugindocs generate + @echo "Use this site to preview markdown rendering: https://registry.terraform.io/tools/doc-preview" diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md index fc5fe16..3063686 100644 --- a/docs/data-sources/database.md +++ b/docs/data-sources/database.md @@ -25,4 +25,13 @@ Retrieves a database. Use this data source to retrieve information for a specifi - `cluster_id` (String) The ID of the cluster that you want to manage. - `max_columns_per_table` (Number) The maximum number of columns per table for the cluster database. - `max_tables` (Number) The maximum number of tables for the cluster database. +- `partition_template` (Attributes List) The template partitioning of the cluster database. (see [below for nested schema](#nestedatt--partition_template)) - `retention_period` (Number) The retention period of the cluster database in nanoseconds. + + +### Nested Schema for `partition_template` + +Read-Only: + +- `type` (String) The type of template part. +- `value` (String) The value of template part. diff --git a/docs/data-sources/databases.md b/docs/data-sources/databases.md index 73f2c5f..2ec6e4c 100644 --- a/docs/data-sources/databases.md +++ b/docs/data-sources/databases.md @@ -29,4 +29,13 @@ Read-Only: - `max_columns_per_table` (Number) The maximum number of columns per table for the cluster database. - `max_tables` (Number) The maximum number of tables for the cluster database. - `name` (String) The name of the cluster database. +- `partition_template` (Attributes List) The template partitioning of the cluster database. (see [below for nested schema](#nestedatt--databases--partition_template)) - `retention_period` (Number) The retention period of the cluster database in nanoseconds. + + +### Nested Schema for `databases.partition_template` + +Read-Only: + +- `type` (String) The type of template part. +- `value` (String) The value of template part. diff --git a/docs/resources/database.md b/docs/resources/database.md index ac1f410..e147e54 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -1,5 +1,3 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "influxdb3_database Resource - terraform-provider-influxdb3" subcategory: "" description: |- @@ -10,7 +8,36 @@ description: |- Creates and manages a database. +## Example Usage + +```terraform +resource "influxdb3_database" "signals" { + name = "signals" + retention_period = 604800 + partition_template = [ + { + type = "tag" + value = "line" + }, + { + type = "tag" + value = "station" + }, + { + type = "time" + value = "%Y-%m-%d" + }, + { + type = "bucket" + value = jsonencode({ + "tagName" : "temperature", + "numberOfBuckets" : 10 + }) + }, + ] +} +``` ## Schema @@ -23,9 +50,18 @@ Creates and manages a database. - `max_columns_per_table` (Number) The maximum number of columns per table for the cluster database. The default is `200` - `max_tables` (Number) The maximum number of tables for the cluster database. The default is `500` +- `partition_template` (Attributes List) A template for [partitioning](https://docs.influxdata.com/influxdb/cloud-dedicated/admin/custom-partitions/partition-templates/) a cluster database. **Note:** A partition template can include up to 7 total tag and tag bucket parts and only 1 time part. (see [below for nested schema](#nestedatt--partition_template)) - `retention_period` (Number) The retention period of the cluster database in nanoseconds. The default is `0`. If the retention period is not set or is set to `0`, the database will have infinite retention. ### Read-Only - `account_id` (String) The ID of the account that the cluster belongs to. - `cluster_id` (String) The ID of the cluster that you want to manage. + + +### Nested Schema for `partition_template` + +Required: + +- `type` (String) The type of template part. Valid values are `bucket`, `tag` or `time`. +- `value` (String) The value of template part. **Note:** For `bucket` partition template type use `jsonencode()` function to encode the value to a string. diff --git a/examples/resources/database/main.tf b/examples/resources/database/main.tf new file mode 100755 index 0000000..30dba7e --- /dev/null +++ b/examples/resources/database/main.tf @@ -0,0 +1,26 @@ +resource "influxdb3_database" "signals" { + name = "signals" + retention_period = 604800 + + partition_template = [ + { + type = "tag" + value = "line" + }, + { + type = "tag" + value = "station" + }, + { + type = "time" + value = "%Y-%m-%d" + }, + { + type = "bucket" + value = jsonencode({ + "tagName" : "temperature", + "numberOfBuckets" : 10 + }) + }, + ] +} diff --git a/examples/resources/database/outputs.tf b/examples/resources/database/outputs.tf new file mode 100644 index 0000000..17357da --- /dev/null +++ b/examples/resources/database/outputs.tf @@ -0,0 +1,3 @@ +output "signals_database" { + value = influxdb3_database.signals +} diff --git a/examples/resources/database/provider.tf b/examples/resources/database/provider.tf new file mode 100644 index 0000000..738d843 --- /dev/null +++ b/examples/resources/database/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + influxdb3 = { + source = "komminarlabs/influxdb3" + } + } +} + +provider "influxdb3" {} diff --git a/examples/resources/database/resource.tf b/examples/resources/database/resource.tf deleted file mode 100644 index ff749e1..0000000 --- a/examples/resources/database/resource.tf +++ /dev/null @@ -1,18 +0,0 @@ -terraform { - required_providers { - influxdb3 = { - source = "komminarlabs/influxdb3" - } - } -} - -provider "influxdb3" {} - -resource "influxdb3_database" "signals" { - name = "signals" - retention_period = 604800 -} - -output "signals_database" { - value = influxdb3_database.signals -} diff --git a/internal/provider/database_data_source.go b/internal/provider/database_data_source.go index 4462c8a..8a9612c 100644 --- a/internal/provider/database_data_source.go +++ b/internal/provider/database_data_source.go @@ -63,6 +63,22 @@ func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRe Computed: true, Description: "The retention period of the cluster database in nanoseconds.", }, + "partition_template": schema.ListNestedAttribute{ + Computed: true, + Description: "The template partitioning of the cluster database.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + Description: "The type of template part.", + }, + "value": schema.StringAttribute{ + Computed: true, + Description: "The value of template part.", + }, + }, + }, + }, }, } } @@ -124,7 +140,14 @@ func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadReques } // Check if the database exists - readDatabase := getDatabaseByName(*readDatabasesResponse, databaseName.ValueString()) + readDatabase, err := getDatabaseByName(*readDatabasesResponse, databaseName.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error getting database", + "Unexpected error: "+err.Error(), + ) + return + } if readDatabase == nil { resp.Diagnostics.AddError( "Database not found", diff --git a/internal/provider/database_model.go b/internal/provider/database_model.go index 87ccc08..8b183e9 100644 --- a/internal/provider/database_model.go +++ b/internal/provider/database_model.go @@ -1,33 +1,91 @@ package provider import ( + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/komminarlabs/influxdb3" ) // DatabaseModel maps InfluxDB database schema data. type DatabaseModel struct { - AccountId types.String `tfsdk:"account_id"` - ClusterId types.String `tfsdk:"cluster_id"` - Name types.String `tfsdk:"name"` - MaxTables types.Int64 `tfsdk:"max_tables"` - MaxColumnsPerTable types.Int64 `tfsdk:"max_columns_per_table"` - RetentionPeriod types.Int64 `tfsdk:"retention_period"` + AccountId types.String `tfsdk:"account_id"` + ClusterId types.String `tfsdk:"cluster_id"` + Name types.String `tfsdk:"name"` + MaxTables types.Int64 `tfsdk:"max_tables"` + MaxColumnsPerTable types.Int64 `tfsdk:"max_columns_per_table"` + RetentionPeriod types.Int64 `tfsdk:"retention_period"` + PartitionTemplate []DatabasePartitionTemplateModel `tfsdk:"partition_template"` +} + +// DatabasePartitionTemplateModel maps InfluxDB database partition template schema data. +type DatabasePartitionTemplateModel struct { + Type types.String `json:"type" tfsdk:"type"` + Value types.String `json:"value" tfsdk:"value"` +} + +// GetAttrType returns the attribute type for the DatabasePartitionTemplateModel. +func (d DatabasePartitionTemplateModel) GetAttrType() attr.Type { + return types.ObjectType{AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, + }} } -func getDatabaseByName(databases influxdb3.GetClusterDatabasesResponse, name string) *DatabaseModel { +func getDatabaseByName(databases influxdb3.GetClusterDatabasesResponse, name string) (*DatabaseModel, error) { for _, database := range *databases.JSON200 { if database.Name == name { + partitionTemplate, err := getPartitionTemplate(database.PartitionTemplate) + if err != nil { + return nil, err + } + db := DatabaseModel{ AccountId: types.StringValue(database.AccountId.String()), ClusterId: types.StringValue(database.ClusterId.String()), Name: types.StringValue(database.Name), MaxTables: types.Int64Value(int64(database.MaxTables)), MaxColumnsPerTable: types.Int64Value(int64(database.MaxColumnsPerTable)), + PartitionTemplate: partitionTemplate, RetentionPeriod: types.Int64Value(database.RetentionPeriod), } - return &db + return &db, nil + } + } + return nil, nil +} + +func getPartitionTemplate(partitionTemplates *influxdb3.ClusterDatabasePartitionTemplate) ([]DatabasePartitionTemplateModel, error) { + if partitionTemplates == nil { + return nil, nil + } + + partitionTemplateModels := make([]DatabasePartitionTemplateModel, 0) + for _, v := range *partitionTemplates { + partitionTemplate := make(map[string]any) + b, err := v.MarshalJSON() + if err != nil { + return nil, err + } + + json.Unmarshal(b, &partitionTemplate) + if partitionTemplate["type"] == "time" || partitionTemplate["type"] == "tag" { + partitionTemplateModels = append(partitionTemplateModels, DatabasePartitionTemplateModel{ + Type: types.StringValue(partitionTemplate["type"].(string)), + Value: types.StringValue(partitionTemplate["value"].(string)), + }) + } else if partitionTemplate["type"] == "bucket" { + jsonEncoded, err := json.Marshal(partitionTemplate["value"]) + if err != nil { + return nil, err + } + + partitionTemplateModels = append(partitionTemplateModels, DatabasePartitionTemplateModel{ + Type: types.StringValue(partitionTemplate["type"].(string)), + Value: types.StringValue(string(jsonEncoded)), + }) } } - return nil + return partitionTemplateModels, nil } diff --git a/internal/provider/database_resource.go b/internal/provider/database_resource.go index 17ee67c..c2bbca7 100644 --- a/internal/provider/database_resource.go +++ b/internal/provider/database_resource.go @@ -2,13 +2,17 @@ package provider import ( "context" + "encoding/json" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -83,6 +87,35 @@ func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaReques Default: int64default.StaticInt64(0), Description: "The retention period of the cluster database in nanoseconds. The default is `0`. If the retention period is not set or is set to `0`, the database will have infinite retention.", }, + "partition_template": schema.ListNestedAttribute{ + Computed: true, + Optional: true, + Default: listdefault.StaticValue(types.ListNull(DatabasePartitionTemplateModel{}.GetAttrType())), + Description: "A template for [partitioning](https://docs.influxdata.com/influxdb/cloud-dedicated/admin/custom-partitions/partition-templates/) a cluster database. **Note:** A partition template can include up to 7 total tag and tag bucket parts and only 1 time part.", + Validators: []validator.List{ + listvalidator.UniqueValues(), + listvalidator.SizeBetween(1, 8), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + Description: "The type of template part. Valid values are `bucket`, `tag` or `time`.", + Validators: []validator.String{ + stringvalidator.OneOf([]string{"bucket", "tag", "time"}...), + }, + }, + "value": schema.StringAttribute{ + Required: true, + Description: "The value of template part. **Note:** For `bucket` partition template type use `jsonencode()` function to encode the value to a string.", + }, + }, + }, + }, }, } } @@ -98,16 +131,54 @@ func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateReques } // Generate API request body from plan + partitionTemplates := []influxdb3.ClusterDatabasePartitionTemplatePart{} + for _, pt := range plan.PartitionTemplate { + t := influxdb3.ClusterDatabasePartitionTemplatePart{} + if pt.Type.ValueString() == "time" { + timeTemplate := influxdb3.ClusterDatabasePartitionTemplatePartTimeFormat{ + Type: (*influxdb3.ClusterDatabasePartitionTemplatePartTimeFormatType)(pt.Type.ValueStringPointer()), + Value: pt.Value.ValueStringPointer(), + } + t.MergeClusterDatabasePartitionTemplatePartTimeFormat(timeTemplate) + } else if pt.Type.ValueString() == "tag" { + tagTemplate := influxdb3.ClusterDatabasePartitionTemplatePartTagValue{ + Type: (*influxdb3.ClusterDatabasePartitionTemplatePartTagValueType)(pt.Type.ValueStringPointer()), + Value: pt.Value.ValueStringPointer(), + } + t.MergeClusterDatabasePartitionTemplatePartTagValue(tagTemplate) + } else if pt.Type.ValueString() == "bucket" { + var encodedJSONData struct { + NumberOfBuckets *int32 `json:"numberOfBuckets,omitempty"` + TagName *string `json:"tagName,omitempty"` + } + err := json.Unmarshal([]byte(pt.Value.ValueString()), &encodedJSONData) + if err != nil { + resp.Diagnostics.AddError( + "Error creating database partition template", + "Failed to unmarshal JSON data: "+err.Error(), + ) + return + } + bucketTemplate := influxdb3.ClusterDatabasePartitionTemplatePartBucket{ + Type: (*influxdb3.ClusterDatabasePartitionTemplatePartBucketType)(pt.Type.ValueStringPointer()), + Value: &encodedJSONData, + } + t.MergeClusterDatabasePartitionTemplatePartBucket(bucketTemplate) + } + partitionTemplates = append(partitionTemplates, t) + } + maxTables := int32(plan.MaxTables.ValueInt64()) maxColumnsPerTable := int32(plan.MaxColumnsPerTable.ValueInt64()) createDatabaseRequest := influxdb3.CreateClusterDatabaseJSONRequestBody{ - Name: plan.Name.ValueString(), MaxTables: &maxTables, MaxColumnsPerTable: &maxColumnsPerTable, + Name: plan.Name.ValueString(), + PartitionTemplate: &partitionTemplates, RetentionPeriod: plan.RetentionPeriod.ValueInt64Pointer(), } - createDatabasesResponse, err := r.client.CreateClusterDatabaseWithResponse(ctx, r.accountID, r.clusterID, createDatabaseRequest) + createDatabaseResponse, err := r.client.CreateClusterDatabaseWithResponse(ctx, r.accountID, r.clusterID, createDatabaseRequest) if err != nil { resp.Diagnostics.AddError( "Error creating database", @@ -116,22 +187,32 @@ func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateReques return } - if createDatabasesResponse.StatusCode() != 200 { + if createDatabaseResponse.StatusCode() != 200 { resp.Diagnostics.AddError( "Error creating database", - fmt.Sprintf("Status: %s", createDatabasesResponse.Status()), + fmt.Sprintf("Status: %s", createDatabaseResponse.Status()), ) return } - createDatabases := createDatabasesResponse.JSON200 + createDatabase := createDatabaseResponse.JSON200 // Map response body to schema and populate Computed attribute values - plan.AccountId = types.StringValue(createDatabases.AccountId.String()) - plan.ClusterId = types.StringValue(createDatabases.ClusterId.String()) - plan.Name = types.StringValue(createDatabases.Name) - plan.MaxTables = types.Int64Value(int64(createDatabases.MaxTables)) - plan.MaxColumnsPerTable = types.Int64Value(int64(createDatabases.MaxColumnsPerTable)) - plan.RetentionPeriod = types.Int64Value(createDatabases.RetentionPeriod) + plan.AccountId = types.StringValue(createDatabase.AccountId.String()) + plan.ClusterId = types.StringValue(createDatabase.ClusterId.String()) + plan.MaxTables = types.Int64Value(int64(createDatabase.MaxTables)) + plan.MaxColumnsPerTable = types.Int64Value(int64(createDatabase.MaxColumnsPerTable)) + plan.Name = types.StringValue(createDatabase.Name) + plan.RetentionPeriod = types.Int64Value(createDatabase.RetentionPeriod) + + partitionTemplate, err := getPartitionTemplate(createDatabase.PartitionTemplate) + if err != nil { + resp.Diagnostics.AddError( + "Error getting database partition template", + "Could not create database, unexpected error: "+err.Error(), + ) + return + } + plan.PartitionTemplate = partitionTemplate // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -170,7 +251,14 @@ func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, r } // Check if the database exists - readDatabase := getDatabaseByName(*readDatabasesResponse, state.Name.ValueString()) + readDatabase, err := getDatabaseByName(*readDatabasesResponse, state.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error getting database", + err.Error(), + ) + return + } if readDatabase == nil { resp.Diagnostics.AddError( "Database not found", @@ -230,9 +318,9 @@ func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateReques // Map response body to schema and populate Computed attribute values plan.AccountId = types.StringValue(updateDatabase.AccountId.String()) plan.ClusterId = types.StringValue(updateDatabase.ClusterId.String()) - plan.Name = types.StringValue(updateDatabase.Name) plan.MaxTables = types.Int64Value(int64(updateDatabase.MaxTables)) plan.MaxColumnsPerTable = types.Int64Value(int64(updateDatabase.MaxColumnsPerTable)) + plan.Name = types.StringValue(updateDatabase.Name) plan.RetentionPeriod = types.Int64Value(updateDatabase.RetentionPeriod) // Save updated data into Terraform state diff --git a/internal/provider/databases_data_source.go b/internal/provider/databases_data_source.go index 68bc575..faae0fd 100644 --- a/internal/provider/databases_data_source.go +++ b/internal/provider/databases_data_source.go @@ -73,6 +73,22 @@ func (d *DatabasesDataSource) Schema(ctx context.Context, req datasource.SchemaR Computed: true, Description: "The retention period of the cluster database in nanoseconds.", }, + "partition_template": schema.ListNestedAttribute{ + Computed: true, + Description: "The template partitioning of the cluster database.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + Description: "The type of template part.", + }, + "value": schema.StringAttribute{ + Computed: true, + Description: "The value of template part.", + }, + }, + }, + }, }, }, }, @@ -124,12 +140,22 @@ func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadReque // Map response body to model for _, database := range *readDatabasesResponse.JSON200 { + partitionTemplate, err := getPartitionTemplate(database.PartitionTemplate) + if err != nil { + resp.Diagnostics.AddError( + "Error getting Databases", + err.Error(), + ) + return + } + databaseState := DatabaseModel{ AccountId: types.StringValue(database.AccountId.String()), ClusterId: types.StringValue(database.ClusterId.String()), - Name: types.StringValue(database.Name), MaxTables: types.Int64Value(int64(database.MaxTables)), MaxColumnsPerTable: types.Int64Value(int64(database.MaxColumnsPerTable)), + Name: types.StringValue(database.Name), + PartitionTemplate: partitionTemplate, RetentionPeriod: types.Int64Value(database.RetentionPeriod), } state.Databases = append(state.Databases, databaseState) diff --git a/templates/resources/database.md.tmpl b/templates/resources/database.md.tmpl new file mode 100644 index 0000000..fb7062a --- /dev/null +++ b/templates/resources/database.md.tmpl @@ -0,0 +1,23 @@ +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +{{tffile "examples/resources/database/main.tf" }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{codefile "shell" .ImportFile }} +{{- end }}