diff --git a/README.md b/README.md index 0b14846..db1e918 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ terraform { required_providers { pomeriumzero = { source = "rasschaert/pomeriumzero" - version = "1.0.0" } } } @@ -31,6 +30,8 @@ This can be used to reference the cluster ID for managing the cluster configurat It can also be used to reference the namespace ID for creating `pomeriumzero_route` and `pomeriumzero_policy` resources on this cluster. +This may be useful if you're managing `pomeriumzero_route` or `pomeriumzero_policy` resources in a terraform state that does not contain a `pomeriumzero_cluster` resource. + ```hcl data "pomeriumzero_cluster" "default" { name = "gifted-nightingale-1337" @@ -40,6 +41,22 @@ data "pomeriumzero_cluster" "default" { ## Resources +### pomeriumzero_cluster + +You can use this resource to change the name of your Pomerium Zero cluster. + +This resource can be used to reference the cluster ID for managing the cluster configuration in a `pomeriumzero_cluster_settings` resource. + +It can also be used to reference the namespace ID for creating `pomeriumzero_route` and `pomeriumzero_policy` resources on this cluster. + +```hcl +resource "pomeriumzero_cluster" "default" { + name = "gifted-nightingale-1337" + domain = "gifted-nightingale-1337" +} + +``` + ### pomeriumzero_cluster_settings Manages the settings for a Pomerium Zero cluster. diff --git a/docs/resources/cluster.md b/docs/resources/cluster.md new file mode 100644 index 0000000..09d2025 --- /dev/null +++ b/docs/resources/cluster.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pomeriumzero_cluster Resource - terraform-provider-pomeriumzero" +subcategory: "" +description: |- + Manages a Pomerium Zero Cluster. This resource allows you to create, update, and delete clusters in your Pomerium Zero organization. +--- + +# pomeriumzero_cluster (Resource) + +Manages a Pomerium Zero Cluster. This resource allows you to create, update, and delete clusters in your Pomerium Zero organization. + + + + +## Schema + +### Required + +- `domain` (String) The domain associated with this cluster. This is used to generate the FQDN for the cluster. +- `name` (String) The name of the cluster. This must be unique within your organization. + +### Read-Only + +- `auto_detect_ip_address` (String) The auto-detected IP address of the cluster. This is determined by Pomerium Zero. +- `created_at` (String) The timestamp when the cluster was created. +- `fqdn` (String) The fully qualified domain name (FQDN) of the cluster. This is automatically generated based on the cluster's domain. +- `id` (String) The unique identifier of the cluster. This is automatically generated by Pomerium Zero. +- `namespace_id` (String) The namespace ID of the cluster. This is automatically generated by Pomerium Zero and is used for creating routes and policies. +- `updated_at` (String) The timestamp when the cluster was last updated. diff --git a/docs/resources/cluster_settings.md b/docs/resources/cluster_settings.md index 6dd6095..0aece53 100644 --- a/docs/resources/cluster_settings.md +++ b/docs/resources/cluster_settings.md @@ -3,12 +3,12 @@ page_title: "pomeriumzero_cluster_settings Resource - terraform-provider-pomeriumzero" subcategory: "" description: |- - Manages Pomerium Zero Cluster Settings. + Manages settings for a Pomerium Zero Cluster. This resource allows you to configure various aspects of your cluster, including authentication, timeouts, and logging. --- # pomeriumzero_cluster_settings (Resource) -Manages Pomerium Zero Cluster Settings. +Manages settings for a Pomerium Zero Cluster. This resource allows you to configure various aspects of your cluster, including authentication, timeouts, and logging. @@ -17,7 +17,7 @@ Manages Pomerium Zero Cluster Settings. ### Optional -- `address` (String) The address of the Pomerium Zero cluster. +- `address` (String) The address of the Pomerium Zero cluster. Typically set to ':443' for HTTPS traffic. - `authenticate_service_url` (String) The URL of the authentication service (required if using custom IDP). - `auto_apply_changesets` (Boolean) Whether to automatically apply changesets. - `cookie_expire` (String) The expiration time for cookies. @@ -40,4 +40,4 @@ Manages Pomerium Zero Cluster Settings. ### Read-Only -- `id` (String) The unique identifier of the cluster settings. +- `id` (String) The unique identifier of the cluster settings. This corresponds to the cluster ID. diff --git a/go.mod b/go.mod index 9896bf3..b19e294 100644 --- a/go.mod +++ b/go.mod @@ -10,24 +10,24 @@ require ( ) require ( - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/oklog/run v1.0.0 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/grpc v1.66.2 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index 6e9bf7f..33997c0 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -28,6 +30,8 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -43,6 +47,8 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -59,6 +65,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -68,14 +76,24 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/provider/cluster_resource.go b/internal/provider/cluster_resource.go new file mode 100644 index 0000000..34d6af9 --- /dev/null +++ b/internal/provider/cluster_resource.go @@ -0,0 +1,418 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ClusterResource{} +var _ resource.ResourceWithImportState = &ClusterResource{} + +// NewClusterResource creates a new ClusterResource. +func NewClusterResource() resource.Resource { + return &ClusterResource{} +} + +// ClusterResource defines the resource implementation. +type ClusterResource struct { + client *http.Client + token string + organizationID string +} + +// ClusterResourceModel describes the resource data model. +type ClusterResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NamespaceID types.String `tfsdk:"namespace_id"` + Domain types.String `tfsdk:"domain"` + FQDN types.String `tfsdk:"fqdn"` + AutoDetectIPAddress types.String `tfsdk:"auto_detect_ip_address"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +// Metadata sets the resource type name for the ClusterResource. +func (r *ClusterResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_cluster" +} + +// Schema defines the structure and attributes of the ClusterResource. +func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages a Pomerium Zero Cluster. This resource allows you to create, update, and delete clusters in your Pomerium Zero organization.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the cluster. This is automatically generated by Pomerium Zero.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the cluster. This must be unique within your organization.", + Required: true, + }, + "namespace_id": schema.StringAttribute{ + MarkdownDescription: "The namespace ID of the cluster. This is automatically generated by Pomerium Zero and is used for creating routes and policies.", + Computed: true, + }, + "domain": schema.StringAttribute{ + MarkdownDescription: "The domain associated with this cluster. This is used to generate the FQDN for the cluster.", + Required: true, + }, + "fqdn": schema.StringAttribute{ + MarkdownDescription: "The fully qualified domain name (FQDN) of the cluster. This is automatically generated based on the cluster's domain.", + Computed: true, + }, + "auto_detect_ip_address": schema.StringAttribute{ + MarkdownDescription: "The auto-detected IP address of the cluster. This is determined by Pomerium Zero.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp when the cluster was created.", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp when the cluster was last updated.", + Computed: true, + }, + }, + } +} + +// Configure prepares a Pomerium Zero API client for the ClusterResource. +func (r *ClusterResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + provider, ok := req.ProviderData.(*pomeriumZeroProvider) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *pomeriumZeroProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = provider.client + r.token = provider.token + r.organizationID = provider.organizationID +} + +// Create creates a new cluster in Pomerium Zero. +func (r *ClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClusterResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create the cluster + cluster, err := r.createCluster(ctx, plan) + if err != nil { + resp.Diagnostics.AddError("Error creating cluster", err.Error()) + return + } + + updateClusterResourceModel(&plan, cluster) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +// Read retrieves information about a Pomerium Zero cluster. +func (r *ClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClusterResourceModel + // Read Terraform configuration data into the model + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Fetch the cluster from Pomerium Zero + cluster, err := r.getCluster(ctx, state.ID.ValueString()) + if err != nil { + if strings.Contains(err.Error(), "cluster not found") { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error reading cluster", err.Error()) + return + } + + // Map the fetched cluster data to our ClusterResourceModel + updateClusterResourceModel(&state, cluster) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +// Update updates an existing cluster in Pomerium Zero. +func (r *ClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClusterResourceModel + // Read the updated Terraform configuration data into the model + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Update the cluster + cluster, err := r.updateCluster(ctx, plan) + if err != nil { + resp.Diagnostics.AddError("Error updating cluster", err.Error()) + return + } + + updateClusterResourceModel(&plan, cluster) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +// Delete deletes a cluster from Pomerium Zero. +func (r *ClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClusterResourceModel + // Read Terraform configuration data into the model + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Delete the cluster + err := r.deleteCluster(ctx, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error deleting cluster", err.Error()) + return + } +} + +// ImportState imports an existing cluster into Terraform. +func (r *ClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // The import ID is now expected to be the cluster name + name := req.ID + + // Find the cluster by name + cluster, err := r.findClusterByName(ctx, name) + if err != nil { + resp.Diagnostics.AddError( + "Error importing cluster", + fmt.Sprintf("Could not find cluster with name %s: %s", name, err), + ) + return + } + + // Set all the attributes + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), cluster.ID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), cluster.Name)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("namespace_id"), cluster.NamespaceID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), cluster.Domain)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("fqdn"), cluster.FQDN)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("auto_detect_ip_address"), cluster.AutoDetectIPAddress)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("created_at"), cluster.CreatedAt)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("updated_at"), cluster.UpdatedAt)...) +} + +// ExportState exports the state of a cluster. +func (r *ClusterResource) findClusterByName(ctx context.Context, name string) (*Cluster, error) { + // Fetch all clusters + url := fmt.Sprintf("%s/organizations/%s/clusters", apiBaseURL, r.organizationID) + // Create a new request + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + // Set the Authorization header + req.Header.Set("Authorization", "Bearer "+r.token) + // Make the request + resp, err := r.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var clusters []Cluster + if err := json.NewDecoder(resp.Body).Decode(&clusters); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + for _, cluster := range clusters { + if cluster.Name == name { + return &cluster, nil + } + } + + return nil, fmt.Errorf("cluster with name %s not found", name) +} + +// createCluster creates a new cluster in Pomerium Zero. +func (r *ClusterResource) createCluster(ctx context.Context, plan ClusterResourceModel) (*Cluster, error) { + // Assemble the API URL + url := fmt.Sprintf("%s/organizations/%s/clusters", apiBaseURL, r.organizationID) + + body := map[string]interface{}{ + "name": plan.Name.ValueString(), + "domain": plan.Domain.ValueString(), + } + + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("error marshaling cluster: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonBody))) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+r.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := r.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var cluster Cluster + if err := json.NewDecoder(resp.Body).Decode(&cluster); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + return &cluster, nil +} + +// getCluster fetches a cluster from Pomerium Zero. +func (r *ClusterResource) getCluster(ctx context.Context, id string) (*Cluster, error) { + // Assemble the API URL + url := fmt.Sprintf("%s/organizations/%s/clusters/%s", apiBaseURL, r.organizationID, id) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+r.token) + + resp, err := r.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("cluster not found") + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var cluster Cluster + if err := json.NewDecoder(resp.Body).Decode(&cluster); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + return &cluster, nil +} + +// updateCluster updates a cluster in Pomerium Zero. +func (r *ClusterResource) updateCluster(ctx context.Context, plan ClusterResourceModel) (*Cluster, error) { + // Assemble the API URL + url := fmt.Sprintf("%s/organizations/%s/clusters/%s", apiBaseURL, r.organizationID, plan.ID.ValueString()) + + body := map[string]interface{}{ + "name": plan.Name.ValueString(), + "domain": plan.Domain.ValueString(), + } + + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("error marshaling cluster: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "PUT", url, strings.NewReader(string(jsonBody))) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+r.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := r.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var cluster Cluster + if err := json.NewDecoder(resp.Body).Decode(&cluster); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + return &cluster, nil +} + +// deleteCluster deletes a cluster from Pomerium Zero. +func (r *ClusterResource) deleteCluster(ctx context.Context, id string) error { + url := fmt.Sprintf("%s/organizations/%s/clusters/%s", apiBaseURL, r.organizationID, id) + + req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+r.token) + + resp, err := r.client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil +} + +// updateClusterResourceModel updates the ClusterResourceModel with the data from the Cluster. +func updateClusterResourceModel(model *ClusterResourceModel, cluster *Cluster) { + model.ID = types.StringValue(cluster.ID) + model.Name = types.StringValue(cluster.Name) + model.NamespaceID = types.StringValue(cluster.NamespaceID) + model.Domain = types.StringValue(cluster.Domain) + model.FQDN = types.StringValue(cluster.FQDN) + model.AutoDetectIPAddress = types.StringValue(cluster.AutoDetectIPAddress) + model.CreatedAt = types.StringValue(cluster.CreatedAt) + model.UpdatedAt = types.StringValue(cluster.UpdatedAt) +} diff --git a/internal/provider/cluster_settings_resource.go b/internal/provider/cluster_settings_resource.go index 9399501..757b02b 100644 --- a/internal/provider/cluster_settings_resource.go +++ b/internal/provider/cluster_settings_resource.go @@ -70,20 +70,20 @@ func (r *ClusterSettingsResource) Metadata(_ context.Context, req resource.Metad // to interact with the Pomerium Zero Cluster Settings resource. func (r *ClusterSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = resource_schema.Schema{ - MarkdownDescription: "Manages Pomerium Zero Cluster Settings.", + MarkdownDescription: "Manages settings for a Pomerium Zero Cluster. This resource allows you to configure various aspects of your cluster, including authentication, timeouts, and logging.", Attributes: map[string]resource_schema.Attribute{ // ID is a computed attribute that uniquely identifies the cluster settings "id": resource_schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The unique identifier of the cluster settings. This corresponds to the cluster ID.", + Computed: true, PlanModifiers: []resource_schema_planmodifier.String{ resource_schema_stringplanmodifier.UseStateForUnknown(), }, - MarkdownDescription: "The unique identifier of the cluster settings.", }, // Address specifies the location of the Pomerium Zero cluster "address": resource_schema.StringAttribute{ Optional: true, - MarkdownDescription: "The address of the Pomerium Zero cluster.", + MarkdownDescription: "The address of the Pomerium Zero cluster. Typically set to ':443' for HTTPS traffic.", }, // AutoApplyChangesets determines if changes should be applied automatically "auto_apply_changesets": resource_schema.BoolAttribute{ @@ -195,6 +195,11 @@ func (r *ClusterSettingsResource) ValidateConfig(ctx context.Context, req resour return } + // Normalize ProxyLogLevel + if !data.ProxyLogLevel.IsNull() && data.ProxyLogLevel.ValueString() == "" { + data.ProxyLogLevel = types.StringNull() + } + // Check if any of the identity provider fields are set idpFieldsSet := !data.IdentityProvider.IsNull() || !data.IdentityProviderClientId.IsNull() || @@ -373,10 +378,10 @@ func (r *ClusterSettingsResource) Read(ctx context.Context, req resource.ReadReq // Special handling for ProxyLogLevel // The API may return null for this field, but doesn't accept null as a value when updating // If it's an empty string from the API, we set it to null in the Terraform state - if apiSettings.ProxyLogLevel != "" { - state.ProxyLogLevel = types.StringValue(apiSettings.ProxyLogLevel) - } else { + if apiSettings.ProxyLogLevel == "" { state.ProxyLogLevel = types.StringNull() + } else { + state.ProxyLogLevel = types.StringValue(apiSettings.ProxyLogLevel) } // Ensure the ID in the state matches the one from the API @@ -400,6 +405,11 @@ func (r *ClusterSettingsResource) Update(ctx context.Context, req resource.Updat return } + // Normalize ProxyLogLevel in the plan + if !plan.ProxyLogLevel.IsNull() && plan.ProxyLogLevel.ValueString() == "" { + plan.ProxyLogLevel = types.StringNull() + } + // Validate the configuration validateResp := &resource.ValidateConfigResponse{ Diagnostics: resp.Diagnostics, @@ -687,7 +697,13 @@ func updateClusterSettingsResourceModel(model *ClusterSettingsResourceModel, set model.IdentityProviderUrl = types.StringValue(settings.IdentityProviderUrl) model.LogLevel = types.StringValue(settings.LogLevel) model.PassIdentityHeaders = types.BoolValue(settings.PassIdentityHeaders) - model.ProxyLogLevel = types.StringValue(settings.ProxyLogLevel) + // Special handling for ProxyLogLevel + if settings.ProxyLogLevel == "" { + model.ProxyLogLevel = types.StringNull() + } else { + model.ProxyLogLevel = types.StringValue(settings.ProxyLogLevel) + } + // Note: If ProxyLogLevel is null or an empty string, it will be omitted from the request model.SkipXffAppend = types.BoolValue(settings.SkipXffAppend) model.TimeoutIdle = types.StringValue(settings.TimeoutIdle) model.TimeoutRead = types.StringValue(settings.TimeoutRead) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 791af1c..f07746b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -229,8 +229,9 @@ func (p *pomeriumZeroProvider) DataSources(_ context.Context) []func() datasourc // Resources defines the resources implemented in the provider. func (p *pomeriumZeroProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ + NewClusterResource, + NewClusterSettingsResource, NewPolicyResource, NewRouteResource, - NewClusterSettingsResource, } }