diff --git a/CHANGELOG.md b/CHANGELOG.md index 482775d..9043177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.3.0] - 2023-12-21 + +### Added + +- `resource_project` includes two additional attributes to configure IP addresses allowed to connect to the project's +endpoints: + - `allowed_ips` + - `allowed_ips_primary_branch_only` + +### Fixed + +- Schema is set on per resource basis now. + +### Changed + +- Updated dependencies: Neon Go SDK [v0.4.0](https://github.com/kislerdm/neon-sdk-go/releases/tag/v0.4.0) + ## [v0.2.5] - 2023-11-02 ### Fixed diff --git a/GNUmakefile b/GNUmakefile index 3324859..3b16e03 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -26,7 +26,7 @@ test: ## Runs unit tests. @ echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 testacc: ## Runs acceptance tests. - @ TF_ACC=1 go test ./... -v -tags acceptance -timeout 120m + @ TF_ACC=1 go test -v -timeout 120m ./... -run TestAcc docu: ## Generates docu. @ go generate diff --git a/docs/resources/project.md b/docs/resources/project.md index 21ea1e7..92c8feb 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -61,6 +61,10 @@ resource "neon_project" "example" { ### Optional +- `allowed_ips` (List of String) A list of IP addresses that are allowed to connect to the endpoints. +Note that the feature is available to the Neon Pro Plan only. Details: https://neon.tech/docs/manage/projects#configure-ip-allow +- `allowed_ips_primary_branch_only` (Boolean) Apply the allow-list to the primary branch only. +Note that the feature is available to the Neon Pro Plan only. - `branch` (Block List, Max: 1) (see [below for nested schema](#nestedblock--branch)) - `compute_provisioner` (String) Provisioner The Neon compute provisioner. Specify the k8s-neonvm provisioner to create a compute endpoint that supports Autoscaling. diff --git a/go.mod b/go.mod index 413fef0..3484314 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/hashicorp/terraform-plugin-docs v0.13.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 - github.com/kislerdm/neon-sdk-go v0.3.1 + github.com/kislerdm/neon-sdk-go v0.4.0 ) require ( diff --git a/go.sum b/go.sum index 9efbee2..2e30ac2 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kislerdm/neon-sdk-go v0.3.1 h1:qgPGNjQ18x02UTsR09G9/ei1DTvNIYyNhsDEovoUUt8= -github.com/kislerdm/neon-sdk-go v0.3.1/go.mod h1:WSwEZ7oeR5KfQoCuDh/04LZxnSKDcvfsZyfG/QicDb8= +github.com/kislerdm/neon-sdk-go v0.4.0 h1:9HhG++QO1NCTOdz9fVIjK0a8i/JkrElJzXCf9lEj0yo= +github.com/kislerdm/neon-sdk-go v0.4.0/go.mod h1:WSwEZ7oeR5KfQoCuDh/04LZxnSKDcvfsZyfG/QicDb8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/internal/provider/acc_test.go b/internal/provider/acc_test.go index 8562709..fb4442a 100644 --- a/internal/provider/acc_test.go +++ b/internal/provider/acc_test.go @@ -1,6 +1,3 @@ -//go:build acceptance -// +build acceptance - package provider import ( @@ -8,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "testing" "time" @@ -17,12 +15,22 @@ import ( neon "github.com/kislerdm/neon-sdk-go" ) -func TestAccEndToEnd(t *testing.T) { +func TestAcc(t *testing.T) { + if os.Getenv("TF_ACC") != "1" { + t.Skip("TF_ACC must be set to 1") + } + client, err := neon.NewClient(neon.Config{Key: os.Getenv("NEON_API_KEY")}) if err != nil { t.Fatal(err) } + end2end(t, client) + + projectAllowedIPs(t, client) +} + +func end2end(t *testing.T, client *neon.Client) { var ( projectID string defaultBranchID string @@ -242,11 +250,11 @@ resource "neon_database" "this" { return errors.New("project " + projectName + " shall be created") } - if float64(ref.DefaultEndpointSettings.AutoscalingLimitMinCu) != mustParseFloat64(autoscalingCUMin) { + if float64(*ref.DefaultEndpointSettings.AutoscalingLimitMinCu) != mustParseFloat64(autoscalingCUMin) { return errors.New("AutoscalingLimitMinCu was not set") } - if float64(ref.DefaultEndpointSettings.AutoscalingLimitMaxCu) != mustParseFloat64(autoscalingCUMax) { + if float64(*ref.DefaultEndpointSettings.AutoscalingLimitMaxCu) != mustParseFloat64(autoscalingCUMax) { return errors.New("AutoscalingLimitMaxCu was not set") } @@ -255,7 +263,7 @@ resource "neon_database" "this" { t.Fatal(err) } - if int(ref.DefaultEndpointSettings.SuspendTimeoutSeconds) != v { + if int(*ref.DefaultEndpointSettings.SuspendTimeoutSeconds) != v { return errors.New("SuspendTimeoutSeconds was not set") } @@ -520,6 +528,204 @@ resource "neon_database" "this" { ) } +func projectAllowedIPs(t *testing.T, client *neon.Client) { + wantAllowedIPs := []string{"192.168.1.0", "192.168.2.0/24"} + ips := `["` + strings.Join(wantAllowedIPs, `", "`) + `"]` + + t.Run("shall provision a project with a custom list of allowed IPs", func(t *testing.T) { + projectName := strconv.FormatInt(time.Now().UnixMilli(), 10) + + resourceDefinition := fmt.Sprintf(`resource "neon_project" "this" { + name = "%s" + region_id = "aws-us-west-2" + pg_version = 14 + allowed_ips = %s + }`, projectName, ips) + + const resourceNameProject = "neon_project.this" + resource.UnitTest( + t, resource.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "neon": func() (*schema.Provider, error) { + return New("0.3.0"), nil + }, + }, + Steps: []resource.TestStep{ + { + ResourceName: "project", + Config: resourceDefinition, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + resourceNameProject, + "name", projectName, + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.#", fmt.Sprintf("%d", len(wantAllowedIPs)), + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.0", wantAllowedIPs[0], + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.1", wantAllowedIPs[1], + ), + + // check the project and its settings + func(state *terraform.State) error { + // WHEN + // list projects + resp, err := client.ListProjects(nil, nil) + if err != nil { + return errors.New("listing error: " + err.Error()) + } + + // THEN + var ref neon.ProjectListItem + for _, project := range resp.ProjectsResponse.Projects { + if project.Name == projectName { + ref = project + } + break + } + + if ref.ID == "" { + return errors.New("project " + projectName + " shall be created") + } + + var exceedingAllowedIPs []string + + missingIPs := map[string]struct{}{} + for _, ip := range wantAllowedIPs { + missingIPs[ip] = struct{}{} + } + + for _, ip := range ref.Settings.AllowedIps.Ips { + if _, ok := missingIPs[ip]; ok { + delete(missingIPs, ip) + continue + } + + exceedingAllowedIPs = append(exceedingAllowedIPs, ip) + } + + if len(exceedingAllowedIPs) > 0 || len(missingIPs) > 0 { + return fmt.Errorf("unexpected allowed ips. want=%v, got=%v", + wantAllowedIPs, ref.Settings.AllowedIps.Ips) + } + + if ref.Settings.AllowedIps.PrimaryBranchOnly { + return errors.New("primary_branch_only is expected to be set to 'false'") + } + + return nil + }, + ), + }, + }, + }, + ) + }) + + t.Run("shall provision a project with a custom list of allowed IPs set for default branch only", func(t *testing.T) { + projectName := strconv.FormatInt(time.Now().UnixMilli(), 10) + + resourceDefinition := fmt.Sprintf(`resource "neon_project" "this" { + name = "%s" + region_id = "aws-us-west-2" + pg_version = 14 + allowed_ips = %s + allowed_ips_primary_branch_only = true + }`, projectName, ips) + + const resourceNameProject = "neon_project.this" + resource.UnitTest( + t, resource.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "neon": func() (*schema.Provider, error) { + return New("0.3.0"), nil + }, + }, + Steps: []resource.TestStep{ + { + ResourceName: "project", + Config: resourceDefinition, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + resourceNameProject, + "name", projectName, + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.#", fmt.Sprintf("%d", len(wantAllowedIPs)), + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.0", wantAllowedIPs[0], + ), + resource.TestCheckResourceAttr( + resourceNameProject, + "allowed_ips.1", wantAllowedIPs[1], + ), + + // check the project and its settings + func(state *terraform.State) error { + // WHEN + // list projects + resp, err := client.ListProjects(nil, nil) + if err != nil { + return errors.New("listing error: " + err.Error()) + } + + // THEN + var ref neon.ProjectListItem + for _, project := range resp.ProjectsResponse.Projects { + if project.Name == projectName { + ref = project + } + break + } + + if ref.ID == "" { + return errors.New("project " + projectName + " shall be created") + } + + var exceedingAllowedIPs []string + + missingIPs := map[string]struct{}{} + for _, ip := range wantAllowedIPs { + missingIPs[ip] = struct{}{} + } + + for _, ip := range ref.Settings.AllowedIps.Ips { + if _, ok := missingIPs[ip]; ok { + delete(missingIPs, ip) + continue + } + + exceedingAllowedIPs = append(exceedingAllowedIPs, ip) + } + + if len(exceedingAllowedIPs) > 0 || len(missingIPs) > 0 { + return fmt.Errorf("unexpected allowed ips. want=%v, got=%v", + wantAllowedIPs, ref.Settings.AllowedIps.Ips) + } + + if !ref.Settings.AllowedIps.PrimaryBranchOnly { + return errors.New("primary_branch_only is expected to be set to 'true'") + } + + return nil + }, + ), + }, + }, + }, + ) + }) +} + func mustParseFloat64(s string) float64 { v, err := strconv.ParseFloat(s, 64) if err != nil { diff --git a/internal/provider/helper.go b/internal/provider/helper.go index 3165545..39c2a26 100644 --- a/internal/provider/helper.go +++ b/internal/provider/helper.go @@ -16,7 +16,7 @@ func pgSettingsToMap(v neon.PgSettingsData) map[string]interface{} { return o } -func mapToPgSettings(v map[string]interface{}) neon.PgSettingsData { +func mapToPgSettings(v map[string]interface{}) *neon.PgSettingsData { if len(v) == 0 { return nil } @@ -24,7 +24,7 @@ func mapToPgSettings(v map[string]interface{}) neon.PgSettingsData { for k, v := range v { o[k] = v } - return o + return &o } func intValidationNotNegative(v interface{}, s string) (warn []string, errs []error) { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e681914..7c788a6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -63,17 +63,6 @@ func init() { schema.DescriptionKind = schema.StringMarkdown } -// version is mapped to the sdk: https://github.com/kislerdm/neon-sdk-go -// 0.1.0: 0 -// 0.1.1: 1 -// 0.1.3: 2 -// 0.1.4: 3 -// 0.2.0: 4 -// 0.2.1: 5 -// 0.2.2: 6 -// 0.2.3: 7 -const versionSchema = 7 - func New(version string) *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ diff --git a/internal/provider/resource_branch.go b/internal/provider/resource_branch.go index f8b46e2..71bde17 100644 --- a/internal/provider/resource_branch.go +++ b/internal/provider/resource_branch.go @@ -15,7 +15,7 @@ import ( func resourceBranch() *schema.Resource { return &schema.Resource{ Description: "Project Branch. See details: https://neon.tech/docs/introduction/branching/", - SchemaVersion: versionSchema, + SchemaVersion: 7, Importer: &schema.ResourceImporter{ StateContext: resourceBranchImport, }, @@ -84,11 +84,15 @@ func updateStateBranch(d *schema.ResourceData, v neon.Branch) error { if err := d.Set("parent_lsn", v.ParentLsn); err != nil { return err } - if err := d.Set("parent_timestamp", int(v.ParentTimestamp.Unix())); err != nil { - return err + if v.ParentTimestamp != nil { + if err := d.Set("parent_timestamp", int(v.ParentTimestamp.Unix())); err != nil { + return err + } } - if err := d.Set("logical_size", int(v.LogicalSize)); err != nil { - return err + if v.LogicalSize != nil { + if err := d.Set("logical_size", int(*v.LogicalSize)); err != nil { + return err + } } return nil } diff --git a/internal/provider/resource_database.go b/internal/provider/resource_database.go index 4ca2856..f2fe339 100644 --- a/internal/provider/resource_database.go +++ b/internal/provider/resource_database.go @@ -14,7 +14,7 @@ import ( func resourceDatabase() *schema.Resource { return &schema.Resource{ Description: `Project Database. See details: https://neon.tech/docs/manage/databases/`, - SchemaVersion: versionSchema, + SchemaVersion: 7, Importer: &schema.ResourceImporter{ StateContext: resourceDatabaseImport, }, diff --git a/internal/provider/resource_endpoint.go b/internal/provider/resource_endpoint.go index a750f92..88da85a 100644 --- a/internal/provider/resource_endpoint.go +++ b/internal/provider/resource_endpoint.go @@ -18,7 +18,7 @@ func resourceEndpoint() *schema.Resource { `It means that no additional endpoints can be provisioned for branches with existing endpoints. ` + `It also means that no endpoints can be created for branches provisioned with this terraform provider ` + `because every branch has the default endpoint attached.`, - SchemaVersion: versionSchema, + SchemaVersion: 8, Importer: &schema.ResourceImporter{ StateContext: resourceEndpointImport, }, @@ -81,7 +81,6 @@ func resourceEndpoint() *schema.Resource { "pg_settings": { Type: schema.TypeMap, Optional: true, - Computed: true, }, "pooler_enabled": { Type: schema.TypeBool, @@ -158,8 +157,10 @@ func updateStateEndpoint(d *schema.ResourceData, v neon.Endpoint) error { if err := d.Set("autoscaling_limit_max_cu", float64(v.AutoscalingLimitMaxCu)); err != nil { return err } - if err := d.Set("pg_settings", pgSettingsToMap(v.Settings.PgSettings)); err != nil { - return err + if v.Settings.PgSettings != nil { + if err := d.Set("pg_settings", pgSettingsToMap(*v.Settings.PgSettings)); err != nil { + return err + } } if err := d.Set("pooler_enabled", v.PoolerEnabled); err != nil { return err @@ -223,6 +224,7 @@ func resourceEndpointCreate(ctx context.Context, d *schema.ResourceData, meta in } d.SetId(resp.Endpoint.ID) + return updateStateEndpoint(d, resp.EndpointResponse.Endpoint) } diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go index 5ef5dc3..fe1d773 100644 --- a/internal/provider/resource_project.go +++ b/internal/provider/resource_project.go @@ -3,6 +3,7 @@ package provider import ( "context" "errors" + "fmt" "strconv" "time" @@ -20,7 +21,7 @@ func resourceProject() *schema.Resource { See details: https://neon.tech/docs/get-started-with-neon/setting-up-a-project/ API: https://api-docs.neon.tech/reference/createproject`, - SchemaVersion: versionSchema, + SchemaVersion: 8, Importer: &schema.ResourceImporter{ StateContext: resourceProjectImport, }, @@ -98,6 +99,21 @@ Specify the k8s-neonvm provisioner to create a compute endpoint that supports Au "quota": schemaQuota, "default_endpoint_settings": schemaDefaultEndpointSettings, "branch": schemaDefaultBranch, + "allowed_ips": { + Type: schema.TypeList, + MinItems: 1, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `A list of IP addresses that are allowed to connect to the endpoints. +Note that the feature is available to the Neon Pro Plan only. Details: https://neon.tech/docs/manage/projects#configure-ip-allow`, + }, + "allowed_ips_primary_branch_only": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: `Apply the allow-list to the primary branch only. +Note that the feature is available to the Neon Pro Plan only.`, + }, // computed fields "default_branch_id": { Type: schema.TypeString, @@ -193,30 +209,30 @@ The zero value per attributed means 'unlimited'.`, }, } -func mapToQuotaSettings(v map[string]interface{}) neon.ProjectQuota { +func mapToQuotaSettings(v map[string]interface{}) *neon.ProjectQuota { o := neon.ProjectQuota{} if v, ok := v["active_time_seconds"].(int); ok && v > 0 { - o.ActiveTimeSeconds = int64(v) + o.ActiveTimeSeconds = pointer(int64(v)) } if v, ok := v["compute_time_seconds"].(int); ok && v > 0 { - o.ComputeTimeSeconds = int64(v) + o.ComputeTimeSeconds = pointer(int64(v)) } if v, ok := v["written_data_bytes"].(int); ok && v > 0 { - o.WrittenDataBytes = int64(v) + o.WrittenDataBytes = pointer(int64(v)) } if v, ok := v["data_transfer_bytes"].(int); ok && v > 0 { - o.DataTransferBytes = int64(v) + o.DataTransferBytes = pointer(int64(v)) } if v, ok := v["logical_size_bytes"].(int); ok && v > 0 { - o.LogicalSizeBytes = int64(v) + o.LogicalSizeBytes = pointer(int64(v)) } - return o + return &o } var schemaDefaultEndpointSettings = &schema.Schema{ @@ -255,15 +271,15 @@ The maximum value is 604800 seconds (1 week)`, func mapToDefaultEndpointsSettings(v map[string]interface{}) *neon.DefaultEndpointSettings { o := neon.DefaultEndpointSettings{} if v, ok := v["autoscaling_limit_min_cu"].(float64); ok && v > 0 { - o.AutoscalingLimitMinCu = neon.ComputeUnit(v) + o.AutoscalingLimitMinCu = pointer(neon.ComputeUnit(v)) } if v, ok := v["autoscaling_limit_max_cu"].(float64); ok && v > 0 { - o.AutoscalingLimitMaxCu = neon.ComputeUnit(v) + o.AutoscalingLimitMaxCu = pointer(neon.ComputeUnit(v)) } if v, ok := v["suspend_timeout_seconds"].(int); ok && v > 0 { - o.SuspendTimeoutSeconds = neon.SuspendTimeoutSeconds(v) + o.SuspendTimeoutSeconds = pointer(neon.SuspendTimeoutSeconds(v)) } return &o } @@ -396,30 +412,20 @@ func updateStateProject( return err } - if err := d.Set( - "default_endpoint_settings", []interface{}{ - map[string]interface{}{ - "autoscaling_limit_min_cu": float64(r.DefaultEndpointSettings.AutoscalingLimitMinCu), - "autoscaling_limit_max_cu": float64(r.DefaultEndpointSettings.AutoscalingLimitMaxCu), - "suspend_timeout_seconds": int(r.DefaultEndpointSettings.SuspendTimeoutSeconds), - }, - }, - ); err != nil { - return err - } - - if err := d.Set( - "quota", []interface{}{ - map[string]interface{}{ - "active_time_seconds": int(r.Settings.Quota.ActiveTimeSeconds), - "compute_time_seconds": int(r.Settings.Quota.ComputeTimeSeconds), - "written_data_bytes": int(r.Settings.Quota.WrittenDataBytes), - "data_transfer_bytes": int(r.Settings.Quota.DataTransferBytes), - "logical_size_bytes": int(r.Settings.Quota.LogicalSizeBytes), - }, - }, - ); err != nil { - return err + if r.DefaultEndpointSettings != nil { + defaultEndpointSettings := map[string]interface{}{} + if r.DefaultEndpointSettings.AutoscalingLimitMinCu != nil { + defaultEndpointSettings["autoscaling_limit_min_cu"] = float64(*r.DefaultEndpointSettings.AutoscalingLimitMinCu) + } + if r.DefaultEndpointSettings.AutoscalingLimitMaxCu != nil { + defaultEndpointSettings["autoscaling_limit_max_cu"] = float64(*r.DefaultEndpointSettings.AutoscalingLimitMaxCu) + } + if r.DefaultEndpointSettings.SuspendTimeoutSeconds != nil { + defaultEndpointSettings["suspend_timeout_seconds"] = float64(*r.DefaultEndpointSettings.SuspendTimeoutSeconds) + } + if err := d.Set("default_endpoint_settings", []interface{}{defaultEndpointSettings}); err != nil { + return err + } } if err := d.Set( @@ -435,6 +441,33 @@ func updateStateProject( return err } + if r.Settings != nil { + if r.Settings.Quota != nil { + if err := d.Set( + "quota", []interface{}{ + map[string]interface{}{ + "active_time_seconds": int(*r.Settings.Quota.ActiveTimeSeconds), + "compute_time_seconds": int(*r.Settings.Quota.ComputeTimeSeconds), + "written_data_bytes": int(*r.Settings.Quota.WrittenDataBytes), + "data_transfer_bytes": int(*r.Settings.Quota.DataTransferBytes), + "logical_size_bytes": int(*r.Settings.Quota.LogicalSizeBytes), + }, + }, + ); err != nil { + return err + } + } + + if r.Settings.AllowedIps != nil { + if err := d.Set("allowed_ips", r.Settings.AllowedIps.Ips); err != nil { + return err + } + if err := d.Set("allowed_ips_primary_branch_only", r.Settings.AllowedIps.PrimaryBranchOnly); err != nil { + return err + } + } + } + if err := d.Set("default_branch_id", defaultBranchID); err != nil { return err } @@ -502,6 +535,25 @@ func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, meta int } } + if v, ok := d.GetOk("allowed_ips"); ok && len(v.([]interface{})) > 0 { + var ips = make([]string, len(v.([]interface{}))) + for i, vv := range v.([]interface{}) { + ips[i] = fmt.Sprintf("%v", vv) + } + + var q *neon.ProjectQuota + if projectDef.Settings != nil && projectDef.Settings.Quota != nil { + q = projectDef.Settings.Quota + } + projectDef.Settings = &neon.ProjectSettingsData{ + AllowedIps: &neon.AllowedIps{ + Ips: ips, + PrimaryBranchOnly: d.Get("allowed_ips_primary_branch_only").(bool), + }, + Quota: q, + } + } + if v, ok := d.GetOk("branch"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { if v, ok := v.([]interface{})[0].(map[string]interface{}); ok && len(v) > 0 { projectDef.Branch = mapToBranchSettings(v) @@ -563,6 +615,20 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, meta int } } + if v, ok := d.GetOk("allowed_ips"); ok && len(v.([]interface{})) > 0 { + var ips = make([]string, len(v.([]interface{}))) + for i, vv := range v.([]interface{}) { + ips[i] = fmt.Sprintf("%v", vv) + } + req.Settings = &neon.ProjectSettingsData{ + Quota: req.Settings.Quota, + AllowedIps: &neon.AllowedIps{ + Ips: ips, + PrimaryBranchOnly: d.Get("allowed_ips_primary_branch_only").(bool), + }, + } + } + _, err := meta.(sdkProject).UpdateProject(d.Id(), neon.ProjectUpdateRequest{Project: req}) return err diff --git a/internal/provider/resource_project_test.go b/internal/provider/resource_project_test.go index 940eac8..40d0ae5 100644 --- a/internal/provider/resource_project_test.go +++ b/internal/provider/resource_project_test.go @@ -38,8 +38,19 @@ func Test_resourceProjectCreate(t *testing.T) { branchName = "foo" branchRoleName = "bar" dbName = "baz" + + ipsPrimaryBranchOnly = true + ) + + var ( + ips = []string{"192.168.1.15", "192.168.2.0/20"} + ipsMap = map[string]struct{}{} ) + for _, ip := range ips { + ipsMap[ip] = struct{}{} + } + err = definition.Set( "default_endpoint_settings", []interface{}{ map[string]interface{}{ @@ -67,6 +78,14 @@ func Test_resourceProjectCreate(t *testing.T) { t.Fatal(err) } + if err := definition.Set("allowed_ips", ips); err != nil { + t.Fatal(err) + } + + if err := definition.Set("allowed_ips_primary_branch_only", ipsPrimaryBranchOnly); err != nil { + t.Fatal(err) + } + if err := definition.Set( "branch", []interface{}{ map[string]interface{}{ @@ -105,24 +124,24 @@ func Test_resourceProjectCreate(t *testing.T) { return } - if settings.AutoscalingLimitMinCu != autoScalingMin { + if *settings.AutoscalingLimitMinCu != autoScalingMin { t.Errorf( "unexpected AutoscalingLimitMinCu, want: %f, got: %f", autoScalingMin, - settings.AutoscalingLimitMinCu, + *settings.AutoscalingLimitMinCu, ) } - if settings.AutoscalingLimitMaxCu != autoScalingMax { + if *settings.AutoscalingLimitMaxCu != autoScalingMax { t.Errorf( "unexpected AutoscalingLimitMaxCu, want: %f, got: %f", autoScalingMax, - settings.AutoscalingLimitMaxCu, + *settings.AutoscalingLimitMaxCu, ) } - if settings.SuspendTimeoutSeconds != suspendTimeoutSeconds { + if *settings.SuspendTimeoutSeconds != suspendTimeoutSeconds { t.Errorf( "unexpected SuspendTimeoutSeconds, want: %d, got: %d", suspendTimeoutSeconds, - settings.SuspendTimeoutSeconds, + *settings.SuspendTimeoutSeconds, ) } }, @@ -136,38 +155,38 @@ func Test_resourceProjectCreate(t *testing.T) { } quota := v.Project.Settings.Quota - if quota.ActiveTimeSeconds != quotaActiveTimeSeconds { + if *quota.ActiveTimeSeconds != quotaActiveTimeSeconds { t.Errorf( "unexpected quota ActiveTimeSeconds, want: %d, got: %d", quotaActiveTimeSeconds, quota.ActiveTimeSeconds, ) } - if quota.DataTransferBytes != quotaDataTransferBytes { + if *quota.DataTransferBytes != quotaDataTransferBytes { t.Errorf( "unexpected quota DataTransferBytes, want: %d, got: %d", quotaDataTransferBytes, - quota.DataTransferBytes, + *quota.DataTransferBytes, ) } - if quota.LogicalSizeBytes != quotaLogicalSizeBytes { + if *quota.LogicalSizeBytes != quotaLogicalSizeBytes { t.Errorf( "unexpected quota LogicalSizeBytes, want: %d, got: %d", quotaLogicalSizeBytes, - quota.LogicalSizeBytes, + *quota.LogicalSizeBytes, ) } - if quota.WrittenDataBytes != quotaWrittenDataBytes { + if *quota.WrittenDataBytes != quotaWrittenDataBytes { t.Errorf( "unexpected quota WrittenDataBytes, want: %d, got: %d", quotaWrittenDataBytes, - quota.WrittenDataBytes, + *quota.WrittenDataBytes, ) } - if quota.ComputeTimeSeconds != quotaComputeTimeSeconds { + if *quota.ComputeTimeSeconds != quotaComputeTimeSeconds { t.Errorf( "unexpected quota ComputeTimeSeconds, want: %d, got: %d", quotaComputeTimeSeconds, - quota.ComputeTimeSeconds, + *quota.ComputeTimeSeconds, ) } }, @@ -201,6 +220,29 @@ func Test_resourceProjectCreate(t *testing.T) { }, ) + + t.Run( + "shall set allowed ips", func(t *testing.T) { + if v.Project.Settings == nil { + t.Fatal("unexpected Settings, shall be not nil") + } + + got := v.Project.Settings.AllowedIps + + var ipsExcess []string + for _, ip := range got.Ips { + if _, ok := ipsMap[ip]; ok { + delete(ipsMap, ip) + } else { + ipsExcess = append(ipsExcess, ip) + } + } + + if len(ipsMap) > 0 || len(ipsExcess) > 0 { + t.Fatalf("unexpected allowed IPs. want = %v, got = %v\n", ips, got) + } + }, + ) }, ) diff --git a/internal/provider/resource_role.go b/internal/provider/resource_role.go index f8115c4..621c0c3 100644 --- a/internal/provider/resource_role.go +++ b/internal/provider/resource_role.go @@ -14,7 +14,7 @@ func resourceRole() *schema.Resource { Description: `Project Role. **Note** that User and Role are synonymous terms in Neon. See details: https://neon.tech/docs/manage/users/ `, - SchemaVersion: versionSchema, + SchemaVersion: 7, Importer: &schema.ResourceImporter{ StateContext: resourceRoleImport, }, @@ -58,8 +58,10 @@ func updateStateRole(d *schema.ResourceData, v neon.Role) error { if err := d.Set("name", v.Name); err != nil { return err } - if err := d.Set("password", v.Password); err != nil { - return err + if v.Password != nil { + if err := d.Set("password", *v.Password); err != nil { + return err + } } if err := d.Set("protected", v.Protected); err != nil { return err @@ -93,12 +95,12 @@ func resourceRoleCreate(ctx context.Context, d *schema.ResourceData, meta interf d.SetId(r.toString()) role := resp.Role - if role.Password == "" { + if role.Password == nil { r, err := meta.(*neon.Client).GetProjectBranchRolePassword(r.ProjectID, r.ProjectID, role.Name) if err != nil { return err } - role.Password = r.Password + role.Password = pointer(r.Password) } return updateStateRole(d, role) @@ -119,13 +121,13 @@ func resourceRoleRead(ctx context.Context, d *schema.ResourceData, meta interfac } role := resp.Role - if role.Password == "" { + if role.Password == nil { r, err := meta.(*neon.Client).GetProjectBranchRolePassword(d.Get("project_id").(string), d.Get("branch_id").(string), d.Get("name").(string)) if err != nil { return err } - role.Password = r.Password + role.Password = pointer(r.Password) } return updateStateRole(d, role) @@ -172,7 +174,7 @@ func resourceRoleImport(ctx context.Context, d *schema.ResourceData, meta interf role := neon.Role{ BranchID: r.BranchID, Name: r.Name, - Password: resp.Password, + Password: pointer(resp.Password), } if err := updateStateRole(d, role); err != nil {