Skip to content

Commit

Permalink
Add allowed ips feature
Browse files Browse the repository at this point in the history
  • Loading branch information
kislerdm authored Dec 21, 2023
2 parents 1008e03 + 34358a4 commit 845631a
Show file tree
Hide file tree
Showing 14 changed files with 424 additions and 92 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions docs/resources/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
220 changes: 213 additions & 7 deletions internal/provider/acc_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
//go:build acceptance
// +build acceptance

package provider

import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"testing"
"time"

Expand All @@ -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
Expand Down Expand Up @@ -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")
}

Expand All @@ -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")
}

Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions internal/provider/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ 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
}
o := make(neon.PgSettingsData, len(v))
for k, v := range v {
o[k] = v
}
return o
return &o
}

func intValidationNotNegative(v interface{}, s string) (warn []string, errs []error) {
Expand Down
11 changes: 0 additions & 11 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
14 changes: 9 additions & 5 deletions internal/provider/resource_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 845631a

Please sign in to comment.