zeroshade commented on code in PR #8:
URL: https://github.com/apache/iceberg-terraform/pull/8#discussion_r2765704930
##########
internal/provider/provider.go:
##########
@@ -45,12 +64,93 @@ func (p *icebergProvider) Metadata(_ context.Context, _
provider.MetadataRequest
func (p *icebergProvider) Schema(_ context.Context, _ provider.SchemaRequest,
resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Use OpenTofu to interact with Iceberg REST
Catalog instances.",
+ Attributes: map[string]schema.Attribute{
+ "catalog_uri": schema.StringAttribute{
+ Description: "The URI of the Iceberg REST
catalog.",
+ Required: true,
+ },
+ "token": schema.StringAttribute{
+ Description: "The token to use for
authentication.",
+ Optional: true,
+ Sensitive: true,
+ },
+ "warehouse": schema.StringAttribute{
+ Description: "The warehouse to use for the
Iceberg REST catalog. This will be passed as `warehouse` property in the
catalog properties.",
+ Optional: true,
+ },
+ "headers": schema.MapAttribute{
+ Description: "The headers to use for
authentication.",
+ Optional: true,
+ Sensitive: true,
+ ElementType: types.StringType,
+ },
+ },
}
}
// Configure prepares a Iceberg API client for data sources and resources.
func (p *icebergProvider) Configure(ctx context.Context, req
provider.ConfigureRequest, resp *provider.ConfigureResponse) {
- // Provider schema is empty, so no configuration to retrieve.
+ var data icebergProviderModel
+
+ diags := req.Config.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if data.CatalogURI.IsUnknown() {
+ return
+ }
+
+ p.catalogURI = data.CatalogURI.ValueString()
+
+ if !data.Token.IsNull() && !data.Token.IsUnknown() {
+ p.token = data.Token.ValueString()
+ }
+
+ if !data.Warehouse.IsNull() && !data.Warehouse.IsUnknown() {
+ p.warehouse = data.Warehouse.ValueString()
+ }
+
+ if !data.Headers.IsNull() && !data.Headers.IsUnknown() {
+ headers := make(map[string]string)
+ resp.Diagnostics.Append(data.Headers.ElementsAs(ctx, &headers,
false)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ p.headers = headers
+ }
+
+ resp.DataSourceData = p
+ resp.ResourceData = p
Review Comment:
I don't know terraform well enough, what's the difference between these?
##########
internal/provider/resource_namespace.go:
##########
@@ -0,0 +1,410 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "strings"
+
+ "github.com/apache/iceberg-go"
+ "github.com/apache/iceberg-go/catalog"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+
"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/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = &icebergNamespaceResource{}
+)
+
+func NewNamespaceResource() resource.Resource {
+ return &icebergNamespaceResource{}
+}
+
+type icebergNamespaceResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ Name types.List `tfsdk:"name"`
+ Properties types.Map `tfsdk:"properties"`
+ FullProperties types.Map `tfsdk:"full_properties"`
Review Comment:
There's gotta be better names to use here, maybe `input_properties`,
`user_properties` or `initial_properties` for the user-input, maybe something
like `server_properties` or otherwise for `full_properties`?
##########
internal/provider/provider.go:
##########
@@ -45,12 +64,93 @@ func (p *icebergProvider) Metadata(_ context.Context, _
provider.MetadataRequest
func (p *icebergProvider) Schema(_ context.Context, _ provider.SchemaRequest,
resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Use OpenTofu to interact with Iceberg REST
Catalog instances.",
Review Comment:
do we want to be hardcoding "OpenTofu" as opposed to "terraform" or some
other text here?
##########
internal/provider/provider.go:
##########
@@ -17,24 +17,43 @@ package provider
import (
"context"
+ "net/http"
+ "github.com/apache/iceberg-go/catalog"
+ "github.com/apache/iceberg-go/catalog/rest"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ provider.Provider = &icebergProvider{}
)
// New is a helper function to simplify provider server and testing
implementation.
-func New() provider.Provider {
- return &icebergProvider{}
+func New() func() provider.Provider {
+ return func() provider.Provider {
+ return &icebergProvider{}
+ }
Review Comment:
is this a terraform thing that we actually need? Does this accept the
provider model?
##########
internal/provider/resource_namespace.go:
##########
@@ -0,0 +1,410 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "strings"
+
+ "github.com/apache/iceberg-go"
+ "github.com/apache/iceberg-go/catalog"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+
"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/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = &icebergNamespaceResource{}
+)
+
+func NewNamespaceResource() resource.Resource {
+ return &icebergNamespaceResource{}
+}
+
+type icebergNamespaceResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ Name types.List `tfsdk:"name"`
+ Properties types.Map `tfsdk:"properties"`
+ FullProperties types.Map `tfsdk:"full_properties"`
+}
+
+type icebergNamespaceResource struct {
+ catalog catalog.Catalog
+ provider *icebergProvider
+}
+
+func (r *icebergNamespaceResource) Metadata(_ context.Context, req
resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_namespace"
+}
+
+func (r *icebergNamespaceResource) Schema(_ context.Context, _
resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "A resource for managing Iceberg namespaces.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.ListAttribute{
+ Description: "The name of the namespace.",
+ Required: true,
+ ElementType: types.StringType,
+ PlanModifiers: []planmodifier.List{
+ listplanmodifier.RequiresReplace(),
+ },
+ },
+ "properties": schema.MapAttribute{
+ Description: "User-defined properties for the
namespace. Only properties listed in Terraform will be changed. All others on
the server will stay the same",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "full_properties": schema.MapAttribute{
+ Description: "Full properties returned by the
server for the namespace. This includes properties set by the user and
properties set by the server.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ },
+ }
+}
+
+func (r *icebergNamespaceResource) Configure(ctx context.Context, req
resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ provider, ok := req.ProviderData.(*icebergProvider)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ "Expected *icebergProvider, got: %T. Please report this
issue to the provider developers.",
+ )
+ return
+ }
+
+ r.provider = provider
+}
+
+func (r *icebergNamespaceResource) ConfigureCatalog(ctx context.Context, diags
*diag.Diagnostics) {
+ if r.catalog != nil {
+ return
+ }
+
+ if r.provider == nil {
+ diags.AddError(
+ "Provider not configured",
+ "The provider hasn't been configured before this
operation",
+ )
+ return
+ }
+
+ catalog, err := r.provider.NewCatalog(ctx)
+ if err != nil {
+ diags.AddError(
+ "Failed to create catalog",
+ "Failed to create catalog: "+err.Error(),
+ )
+ return
+ }
+ r.catalog = catalog
+}
+
+func (r *icebergNamespaceResource) Create(ctx context.Context, req
resource.CreateRequest, resp *resource.CreateResponse) {
+ r.ConfigureCatalog(ctx, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var data icebergNamespaceResourceModel
+
+ diags := req.Plan.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var namespaceName []string
+ diags = data.Name.ElementsAs(ctx, &namespaceName, false)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ namespaceIdent := catalog.ToIdentifier(namespaceName...)
+
+ properties := make(map[string]string)
+ if !data.Properties.IsNull() {
+ diags = data.Properties.ElementsAs(ctx, &properties, false)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ err := r.catalog.CreateNamespace(ctx, namespaceIdent, properties)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create namespace",
err.Error())
+ return
+ }
+
+ data.ID = types.StringValue(strings.Join(namespaceIdent, "."))
+
+ nsProps, err := r.catalog.LoadNamespaceProperties(ctx, namespaceIdent)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to read namespace
properties", err.Error())
+ return
+ }
+
+ // Update FullProperties with everything from the server
+ loadedFullProperties, diags := types.MapValueFrom(ctx,
types.StringType, nsProps)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ data.FullProperties = loadedFullProperties
+
+ // Update Properties to match what we sent/expected, but values
confirmed from server
+ // We only keep keys that were in the original plan (User managed)
+ managedProps := make(map[string]string)
+ for k := range properties {
+ if v, ok := nsProps[k]; ok {
+ managedProps[k] = v
+ }
+ }
+ // If the user didn't set any properties, Properties should be null or
empty based on input.
+ // However, if we sent it, we expect it back.
+ if !data.Properties.IsNull() {
+ data.Properties, diags = types.MapValueFrom(ctx,
types.StringType, managedProps)
+ resp.Diagnostics.Append(diags...)
+ }
+
+ diags = resp.State.Set(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *icebergNamespaceResource) Read(ctx context.Context, req
resource.ReadRequest, resp *resource.ReadResponse) {
+ r.ConfigureCatalog(ctx, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var data icebergNamespaceResourceModel
+
+ tflog.Info(ctx, "Reading namespace resource")
+ diags := req.State.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var namespaceName []string
+ diags = data.Name.ElementsAs(ctx, &namespaceName, false)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ namespaceIdent := catalog.ToIdentifier(namespaceName...)
+
+ nsProps, err := r.catalog.LoadNamespaceProperties(ctx, namespaceIdent)
+ if err != nil {
+ if errors.Is(err, catalog.ErrNoSuchNamespace) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("failed to load namespace",
err.Error())
+ return
+ }
+
+ // FullProperties gets everything
+ fullProperties, diags := types.MapValueFrom(ctx, types.StringType,
nsProps)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ data.FullProperties = fullProperties
+
+ // Properties only updates keys that are already tracked in the state
+ if !data.Properties.IsNull() {
+ stateProperties := make(map[string]string)
+ diags = data.Properties.ElementsAs(ctx, &stateProperties, false)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ managedProps := make(map[string]string)
+ for k := range stateProperties {
+ if v, ok := nsProps[k]; ok {
+ managedProps[k] = v
+ }
+ // If key is missing in nsProps, it was removed from
server, so we drop it from managedProps
+ // which effectively sets it to null/removed in the new
state, matching reality.
+ }
+ data.Properties, diags = types.MapValueFrom(ctx,
types.StringType, managedProps)
+ resp.Diagnostics.Append(diags...)
+ }
+
+ diags = resp.State.Set(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *icebergNamespaceResource) Update(ctx context.Context, req
resource.UpdateRequest, resp *resource.UpdateResponse) {
+ r.ConfigureCatalog(ctx, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var plan, state icebergNamespaceResourceModel
+
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+
+ diags = req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
Review Comment:
how do we tell it when it needs to delete and create a new thing versus
update (like if the "name" changes?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]