├── .gitignore
├── tools
├── mongo.js
└── wait-for-it.sh
├── internal
├── pritunl
│ ├── organization.go
│ ├── host.go
│ ├── route.go
│ ├── transport.go
│ ├── user.go
│ ├── server.go
│ └── client.go
└── provider
│ ├── data_source_hosts_test.go
│ ├── resource_organization_test.go
│ ├── data_source_host_test.go
│ ├── data_source_hosts.go
│ ├── provider.go
│ ├── provider_test.go
│ ├── resource_user_test.go
│ ├── resource_organization.go
│ ├── data_source_host.go
│ ├── resource_user.go
│ ├── resource_server_test.go
│ └── resource_server.go
├── examples
└── provider
│ ├── test.auto.tfvars
│ ├── multiple-hosts
│ ├── variables.tf
│ └── main.tf
│ ├── variables.tf
│ └── provider.tf
├── .github
├── workflows
│ ├── tests.yaml
│ ├── release.yml
│ └── codeql-analysis.yml
└── dependabot.yml
├── docs
├── resources
│ ├── organization.md
│ ├── user.md
│ └── server.md
├── data-sources
│ ├── hosts.md
│ └── host.md
└── index.md
├── main.go
├── Makefile
├── .goreleaser.yml
├── go.mod
├── README.md
├── LICENSE
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .terraform/
3 | terraform.tfstate
4 | terraform.tfstate.backup
5 | terraform.tfvars
6 | .terraform.lock.hcl
--------------------------------------------------------------------------------
/tools/mongo.js:
--------------------------------------------------------------------------------
1 | db.administrators.updateOne({"username": "pritunl"}, {$set: {auth_api: true, token: "tfacctest_token", secret: "tfacctest_secret"}});
--------------------------------------------------------------------------------
/internal/pritunl/organization.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | type Organization struct {
4 | ID string `json:"id,omitempty"`
5 | Name string `json:"name"`
6 | }
7 |
--------------------------------------------------------------------------------
/examples/provider/test.auto.tfvars:
--------------------------------------------------------------------------------
1 | pritunl_url = "https://localhost"
2 | pritunl_api_token = "tfacctest"
3 | pritunl_api_secret = "tfacctest"
4 | pritunl_insecure = true
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: GitHub Actions CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | tests:
11 | name: Tests
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v6
15 | - uses: actions/setup-go@v6
16 | with:
17 | go-version: 1.24
18 | - run: make test
19 |
--------------------------------------------------------------------------------
/examples/provider/multiple-hosts/variables.tf:
--------------------------------------------------------------------------------
1 | variable "pritunl_url" {
2 | type = string
3 | default = "http://localhost"
4 | }
5 |
6 | variable "pritunl_api_token" {
7 | type = string
8 | default = "secret"
9 | }
10 |
11 | variable "pritunl_api_secret" {
12 | type = string
13 | default = "secret"
14 | }
15 |
16 | variable "pritunl_insecure" {
17 | type = bool
18 | default = false
19 | }
--------------------------------------------------------------------------------
/examples/provider/variables.tf:
--------------------------------------------------------------------------------
1 | variable "pritunl_url" {
2 | type = string
3 | default = "http://localhost"
4 | }
5 |
6 | variable "pritunl_api_token" {
7 | type = string
8 | default = "secret"
9 | }
10 |
11 | variable "pritunl_api_secret" {
12 | type = string
13 | default = "secret"
14 | }
15 |
16 | variable "common_routes" {
17 | type = list(map(any))
18 | default = []
19 | }
20 |
21 | variable "pritunl_insecure" {
22 | type = bool
23 | default = false
24 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "github-actions"
13 | directory: "/"
14 | schedule:
15 | interval: "weekly"
16 |
--------------------------------------------------------------------------------
/docs/resources/organization.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "pritunl_organization Resource - terraform-provider-pritunl"
4 | subcategory: ""
5 | description: |-
6 | The organization resource allows managing information about a particular Pritunl organization.
7 | ---
8 |
9 | # pritunl_organization (Resource)
10 |
11 | The organization resource allows managing information about a particular Pritunl organization.
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Required
19 |
20 | - `name` (String) The name of the resource, also acts as it's unique ID
21 |
22 | ### Read-Only
23 |
24 | - `id` (String) The ID of this resource.
25 |
--------------------------------------------------------------------------------
/internal/pritunl/host.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | type Host struct {
4 | ID string `json:"id,omitempty"`
5 | Name string `json:"name"`
6 | Hostname string `json:"hostname"`
7 | PublicAddr string `json:"public_addr"`
8 | PublicAddr6 string `json:"public_addr6"`
9 | RoutedSubnet6 string `json:"routed_subnet6"`
10 | RoutedSubnet6WG string `json:"routed_subnet6_wg"`
11 | LocalAddr string `json:"local_addr"`
12 | LocalAddr6 string `json:"local_addr6"`
13 | AvailabilityGroup string `json:"availability_group"`
14 | LinkAddr string `json:"link_addr"`
15 | SyncAddress string `json:"sync_address"`
16 | Status string `json:"status"`
17 | }
18 |
--------------------------------------------------------------------------------
/examples/provider/multiple-hosts/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | pritunl = {
4 | version = "0.1.0"
5 | source = "disc/pritunl"
6 | }
7 | }
8 | }
9 |
10 | provider "pritunl" {
11 | url = var.pritunl_url
12 | token = var.pritunl_api_token
13 | secret = var.pritunl_api_secret
14 |
15 | insecure = var.pritunl_insecure
16 | }
17 |
18 | data "pritunl_host" "main" {
19 | hostname = "nyc1.vpn.host"
20 | }
21 |
22 | data "pritunl_host" "reserve" {
23 | hostname = "nyc3.vpn.host"
24 | }
25 |
26 | resource "pritunl_server" "test" {
27 | name = "some-server"
28 | network = "192.168.250.0/24"
29 | port = 15500
30 |
31 | host_ids = [
32 | data.pritunl_host.main.id,
33 | data.pritunl_host.reserve.id,
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/internal/provider/data_source_hosts_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
6 | "testing"
7 | )
8 |
9 | func TestDataSourceHosts(t *testing.T) {
10 | resource.ParallelTest(t, resource.TestCase{
11 | PreCheck: func() {},
12 | ProviderFactories: providerFactories,
13 | Steps: []resource.TestStep{
14 | {
15 | Config: testPritunlHostsConfig(),
16 | Check: resource.ComposeTestCheckFunc(
17 | resource.TestCheckOutput("num_hosts", "1"),
18 | ),
19 | },
20 | },
21 | })
22 | }
23 |
24 | func testPritunlHostsConfig() string {
25 | return fmt.Sprintf(`
26 | data "pritunl_hosts" "my-server-hosts" {}
27 |
28 | output "num_hosts" {
29 | value = length(data.pritunl_hosts.my-server-hosts.hosts)
30 | }
31 | `)
32 | }
33 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "github.com/disc/terraform-provider-pritunl/internal/provider"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
9 | "log"
10 | )
11 |
12 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
13 |
14 | func main() {
15 | var debugMode bool
16 |
17 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve")
18 | flag.Parse()
19 |
20 | opts := &plugin.ServeOpts{
21 | ProviderFunc: func() *schema.Provider {
22 | return provider.Provider()
23 | },
24 | }
25 |
26 | if debugMode {
27 | err := plugin.Debug(context.Background(), "registry.terraform.io/disc/pritunl", opts)
28 | if err != nil {
29 | log.Fatal(err.Error())
30 | }
31 | return
32 | }
33 |
34 | plugin.Serve(opts)
35 | }
36 |
--------------------------------------------------------------------------------
/internal/provider/resource_organization_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
6 | "testing"
7 | )
8 |
9 | func TestAccPritunlOrganization(t *testing.T) {
10 |
11 | t.Run("creates organizations without error", func(t *testing.T) {
12 | orgName := "tfacc-org1"
13 |
14 | check := resource.ComposeTestCheckFunc(
15 | resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
16 | )
17 |
18 | resource.Test(t, resource.TestCase{
19 | PreCheck: func() { preCheck(t) },
20 | ProviderFactories: providerFactories,
21 | Steps: []resource.TestStep{
22 | {
23 | Config: testPritunlOrganizationConfig(orgName),
24 | Check: check,
25 | },
26 | // import test
27 | importStep("pritunl_organization.test"),
28 | },
29 | })
30 | })
31 | }
32 |
33 | func testPritunlOrganizationConfig(name string) string {
34 | return fmt.Sprintf(`
35 | resource "pritunl_organization" "test" {
36 | name = "%[1]s"
37 | }
38 | `, name)
39 | }
40 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 | build:
3 | go build -gcflags="all=-N -l" -o ~/.terraform.d/plugins/registry.terraform.io/disc/pritunl/0.0.1/darwin_amd64/terraform-provider-pritunl_v0.0.1 main.go
4 |
5 | .PHONY: test
6 | test:
7 | @docker rm tf_pritunl_acc_test -f || true
8 | @docker run --name tf_pritunl_acc_test --hostname pritunl.local --rm -d --privileged \
9 | -p 1194:1194/udp \
10 | -p 1194:1194/tcp \
11 | -p 80:80/tcp \
12 | -p 443:443/tcp \
13 | -p 27017:27017/tcp \
14 | ghcr.io/jippi/docker-pritunl:1.32.4399.86
15 |
16 | sleep 20
17 |
18 | @chmod +x ./tools/wait-for-it.sh
19 | ./tools/wait-for-it.sh localhost:27017 -- echo "mongodb is up"
20 |
21 | # enables an api access for the pritunl user, updates an api token and secret
22 | @docker exec -i tf_pritunl_acc_test mongo pritunl < ./tools/mongo.js
23 |
24 | TF_ACC=1 \
25 | PRITUNL_URL="https://localhost/" \
26 | PRITUNL_INSECURE="true" \
27 | PRITUNL_TOKEN=tfacctest_token \
28 | PRITUNL_SECRET=tfacctest_secret \
29 | go test -v -cover -count 1 ./internal/provider
30 |
31 | @docker rm tf_pritunl_acc_test -f
32 |
--------------------------------------------------------------------------------
/docs/data-sources/hosts.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "pritunl_hosts Data Source - terraform-provider-pritunl"
4 | subcategory: ""
5 | description: |-
6 | Use this data source to get a list of the Pritunl hosts.
7 | ---
8 |
9 | # pritunl_hosts (Data Source)
10 |
11 | Use this data source to get a list of the Pritunl hosts.
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Read-Only
19 |
20 | - `hosts` (List of Object) A list of the Pritunl hosts resources. (see [below for nested schema](#nestedatt--hosts))
21 | - `id` (String) The ID of this resource.
22 |
23 |
24 | ### Nested Schema for `hosts`
25 |
26 | Read-Only:
27 |
28 | - `availability_group` (String)
29 | - `hostname` (String)
30 | - `id` (String)
31 | - `link_addr` (String)
32 | - `local_addr` (String)
33 | - `local_addr6` (String)
34 | - `name` (String)
35 | - `public_addr` (String)
36 | - `public_addr6` (String)
37 | - `routed_subnet6` (String)
38 | - `routed_subnet6_wg` (String)
39 | - `status` (String)
40 | - `sync_address` (String)
41 |
--------------------------------------------------------------------------------
/internal/provider/data_source_host_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
6 | "regexp"
7 | "testing"
8 | )
9 |
10 | func TestDataSourceHost(t *testing.T) {
11 | // pritunl.local sets in Makefile's "test" target
12 | existsHostname := "pritunl.local"
13 | notExistHostname := "not-exist-hostname"
14 | resource.ParallelTest(t, resource.TestCase{
15 | PreCheck: func() {},
16 | ProviderFactories: providerFactories,
17 | Steps: []resource.TestStep{
18 | {
19 | Config: testPritunlHostSimpleConfig(existsHostname),
20 | Check: resource.ComposeTestCheckFunc(),
21 | },
22 | {
23 | Config: testPritunlHostSimpleConfig(notExistHostname),
24 | ExpectError: regexp.MustCompile(fmt.Sprintf("could not find host with a hostname %s. Previous error message: could not find a host with specified parameters", notExistHostname)),
25 | },
26 | },
27 | })
28 | }
29 |
30 | func testPritunlHostSimpleConfig(name string) string {
31 | return fmt.Sprintf(`
32 | data "pritunl_host" "test" {
33 | hostname = "%[1]s"
34 | }
35 | `, name)
36 | }
37 |
--------------------------------------------------------------------------------
/internal/pritunl/route.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | import (
4 | "encoding/hex"
5 | )
6 |
7 | type Route struct {
8 | Network string `json:"network"`
9 | Nat bool `json:"nat"`
10 | Comment string `json:"comment,omitempty"`
11 | VirtualNetwork bool `json:"virtual_network,omitempty"`
12 | WgNetwork string `json:"wg_network,omitempty"`
13 | NetworkLink bool `json:"network_link,omitempty"`
14 | ServerLink bool `json:"server_link,omitempty"`
15 | NetGateway bool `json:"net_gateway,omitempty"`
16 | VpcID string `json:"vpc_id,omitempty"`
17 | VpcRegion string `json:"vpc_region,omitempty"`
18 | Metric string `json:"metric,omitempty"`
19 | Advertise bool `json:"advertise,omitempty"`
20 | NatInterface string `json:"nat_interface,omitempty"`
21 | NatNetmap string `json:"nat_netmap,omitempty"`
22 | }
23 |
24 | func (r Route) GetID() string {
25 | if len(r.Network) > 0 {
26 | return hex.EncodeToString([]byte(r.Network))
27 | }
28 |
29 | return ""
30 | }
31 |
32 | func ConvertMapToRoute(data map[string]interface{}) Route {
33 | var route Route
34 |
35 | if v, ok := data["network"]; ok {
36 | route.Network = v.(string)
37 | }
38 | if v, ok := data["comment"]; ok {
39 | route.Comment = v.(string)
40 | }
41 | if v, ok := data["nat"]; ok {
42 | route.Nat = v.(bool)
43 | }
44 | if v, ok := data["net_gateway"]; ok {
45 | route.NetGateway = v.(bool)
46 | }
47 |
48 | return route
49 | }
50 |
--------------------------------------------------------------------------------
/docs/data-sources/host.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "pritunl_host Data Source - terraform-provider-pritunl"
4 | subcategory: ""
5 | description: |-
6 | Use this data source to get information about the Pritunl hosts.
7 | ---
8 |
9 | # pritunl_host (Data Source)
10 |
11 | Use this data source to get information about the Pritunl hosts.
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Required
19 |
20 | - `hostname` (String) Hostname
21 |
22 | ### Read-Only
23 |
24 | - `availability_group` (String) Availability group for host. Replicated servers will only be replicated to a group of hosts in the same availability group"
25 | - `id` (String) The ID of this resource.
26 | - `link_addr` (String) IP address or domain used when linked servers connect to a linked server on this host
27 | - `local_addr` (String) Local network address for server
28 | - `local_addr6` (String) Local IPv6 network address for server
29 | - `name` (String) Name of host
30 | - `public_addr` (String) Public IP address or domain name of the host
31 | - `public_addr6` (String) Public IPv6 address or domain name of the host
32 | - `routed_subnet6` (String) IPv6 subnet that is routed to the host
33 | - `routed_subnet6_wg` (String) IPv6 WG subnet that is routed to the host
34 | - `status` (String) Status of host
35 | - `sync_address` (String) IP address or domain used by users when syncing configuration. This is needed when using a load balancer.
36 |
--------------------------------------------------------------------------------
/internal/pritunl/transport.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/md5"
6 | "crypto/sha256"
7 | "encoding/base64"
8 | "fmt"
9 | "net/http"
10 | "net/url"
11 | "path"
12 | "strconv"
13 | "strings"
14 | "time"
15 | )
16 |
17 | type transport struct {
18 | underlyingTransport http.RoundTripper
19 | apiToken string
20 | apiSecret string
21 | baseUrl string
22 | }
23 |
24 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
25 | if req.URL.Host == "" {
26 | u, err := url.Parse(t.baseUrl)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | u.Path = path.Join(u.Path, req.URL.Path)
32 | req.URL = u
33 | }
34 |
35 | timestamp := strconv.FormatInt(time.Now().Unix(), 10)
36 | timestampNano := strconv.FormatInt(time.Now().UnixNano(), 10)
37 |
38 | nonceMac := hmac.New(md5.New, []byte(t.apiSecret))
39 | nonceMac.Write([]byte(strings.Join([]string{timestampNano, req.URL.Path, t.apiToken}, "")))
40 | nonce := fmt.Sprintf("%x", nonceMac.Sum(nil))
41 | authString := strings.Join([]string{t.apiToken, timestamp, nonce, strings.ToUpper(req.Method), req.URL.Path}, "&")
42 |
43 | mac := hmac.New(sha256.New, []byte(t.apiSecret))
44 | mac.Write([]byte(authString))
45 | signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
46 |
47 | req.Header.Add("Auth-Token", t.apiToken)
48 | req.Header.Add("Auth-Timestamp", timestamp)
49 | req.Header.Add("Auth-Nonce", nonce)
50 | req.Header.Add("Auth-Signature", signature)
51 |
52 | req.Header.Add("Content-Type", "application/json")
53 |
54 | return t.underlyingTransport.RoundTrip(req)
55 | }
56 |
--------------------------------------------------------------------------------
/examples/provider/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | pritunl = {
4 | version = "~> 0.0.1"
5 | source = "disc/pritunl"
6 | }
7 | }
8 | }
9 |
10 | provider "pritunl" {
11 | url = "https://localhost"
12 | token = "api-token"
13 | secret = "api-secret"
14 |
15 | insecure = false
16 | connection_check = true
17 | }
18 |
19 | resource "pritunl_organization" "developers" {
20 | name = "Developers"
21 | }
22 |
23 | resource "pritunl_organization" "admins" {
24 | name = "Admins"
25 | }
26 |
27 | resource "pritunl_user" "test" {
28 | name = "test-user"
29 | organization_id = pritunl_organization.developers.id
30 | email = "test@test.com"
31 | groups = [
32 | "admins",
33 | ]
34 | }
35 |
36 | resource "pritunl_user" "test_pin" {
37 | name = "test-user-pin"
38 | organization_id = pritunl_organization.developers.id
39 | email = "test@test.com"
40 | pin = "123456"
41 | groups = [
42 | "admins",
43 | ]
44 | }
45 |
46 | resource "pritunl_server" "test" {
47 | name = "test"
48 |
49 | organization_ids = [
50 | pritunl_organization.developers.id,
51 | pritunl_organization.admins.id,
52 | ]
53 |
54 | route {
55 | network = "10.0.0.0/24"
56 | comment = "Private network #1"
57 | nat = true
58 | }
59 |
60 | route {
61 | network = "10.2.0.0/24"
62 | comment = "Private network #2"
63 | nat = false
64 | }
65 |
66 | route {
67 | network = "10.3.0.0/32"
68 | comment = "Private network #3"
69 | nat = false
70 | net_gateway = true
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This GitHub action can publish assets for release when a tag is created.
2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0).
3 | #
4 | # This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your
5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE`
6 | # secret. If you would rather own your own GPG handling, please fork this action
7 | # or use an alternative one for key handling.
8 | #
9 | # You will need to pass the `--batch` flag to `gpg` in your signing step
10 | # in `goreleaser` to indicate this is being used in a non-interactive mode.
11 | #
12 | name: release
13 | on:
14 | push:
15 | tags:
16 | - 'v*'
17 | jobs:
18 | goreleaser:
19 | runs-on: ubuntu-latest
20 | steps:
21 | -
22 | name: Checkout
23 | uses: actions/checkout@v6
24 | -
25 | name: Unshallow
26 | run: git fetch --prune --unshallow
27 | -
28 | name: Set up Go
29 | uses: actions/setup-go@v6
30 | with:
31 | go-version: 1.24
32 | -
33 | name: Import GPG key
34 | id: import_gpg
35 | uses: crazy-max/ghaction-import-gpg@v6
36 | with:
37 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
38 | passphrase: ${{ secrets.PASSPHRASE }}
39 | -
40 | name: Run GoReleaser
41 | uses: goreleaser/goreleaser-action@v6.4.0
42 | with:
43 | version: latest
44 | args: release --clean
45 | env:
46 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
47 | # GitHub sets this automatically
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # Visit https://goreleaser.com for documentation on how to customize this
2 | # behavior.
3 | version: 2
4 | before:
5 | hooks:
6 | # this is just an example and not a requirement for provider building/publishing
7 | - go mod tidy
8 | builds:
9 | - env:
10 | # goreleaser does not work with CGO, it could also complicate
11 | # usage by users in CI/CD systems like Terraform Cloud where
12 | # they are unable to install libraries.
13 | - CGO_ENABLED=0
14 | mod_timestamp: "{{ .CommitTimestamp }}"
15 | flags:
16 | - -trimpath
17 | ldflags:
18 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}"
19 | goos:
20 | - freebsd
21 | - windows
22 | - linux
23 | - darwin
24 | goarch:
25 | - amd64
26 | - "386"
27 | - arm
28 | - arm64
29 | ignore:
30 | - goos: darwin
31 | goarch: "386"
32 | - goos: windows
33 | goarch: arm64
34 | binary: "{{ .ProjectName }}_v{{ .Version }}"
35 | archives:
36 | - formats: [ 'zip' ]
37 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
38 | checksum:
39 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS"
40 | algorithm: sha256
41 | signs:
42 | - artifacts: checksum
43 | args:
44 | # if you are using this is a GitHub action or some other automated pipeline, you
45 | # need to pass the batch flag to indicate its not interactive.
46 | - "--batch"
47 | - "--local-user"
48 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
49 | - "--output"
50 | - "${signature}"
51 | - "--detach-sign"
52 | - "${artifact}"
53 | release:
54 | # If you want to manually examine the release before its live, uncomment this line:
55 | # draft: true
56 | changelog:
57 | disable: true
58 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "Provider: Pritunl"
4 | subcategory: ""
5 | description: |-
6 | Terraform provider for interacting with Pritunl API.
7 | ---
8 |
9 | # Pritunl Provider
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | terraform {
17 | required_providers {
18 | pritunl = {
19 | version = "~> 0.0.1"
20 | source = "disc/pritunl"
21 | }
22 | }
23 | }
24 |
25 | provider "pritunl" {
26 | url = "https://vpn.server.com"
27 | token = "api-token"
28 | secret = "api-secret-key"
29 | insecure = false
30 | }
31 |
32 | resource "pritunl_organization" "developers" {
33 | name = "Developers"
34 | }
35 |
36 | resource "pritunl_organization" "admins" {
37 | name = "Admins"
38 | }
39 |
40 | resource "pritunl_user" "test" {
41 | name = "test-user"
42 | organization_id = pritunl_organization.developers.id
43 | email = "test@test.com"
44 | groups = [
45 | "admins",
46 | ]
47 | }
48 |
49 | resource "pritunl_server" "test" {
50 | name = "test"
51 |
52 | organization_ids = [
53 | pritunl_organization.developers.id,
54 | pritunl_organization.admins.id,
55 | ]
56 |
57 | route {
58 | network = "10.0.0.0/24"
59 | comment = "Private network #1"
60 | nat = true
61 | }
62 |
63 | route {
64 | network = "10.2.0.0/24"
65 | comment = "Private network #2"
66 | nat = false
67 | }
68 |
69 | route {
70 | network = "10.3.0.0/32"
71 | comment = "Private network #3"
72 | nat = false
73 | net_gateway = true
74 | }
75 |
76 | }
77 | ```
78 |
79 |
80 | ## Schema
81 |
82 | ### Optional
83 |
84 | - `connection_check` (Boolean)
85 | - `insecure` (Boolean)
86 | - `secret` (String)
87 | - `token` (String)
88 | - `url` (String)
89 |
--------------------------------------------------------------------------------
/internal/provider/data_source_hosts.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | )
9 |
10 | func dataSourceHosts() *schema.Resource {
11 | return &schema.Resource{
12 | Description: "Use this data source to get a list of the Pritunl hosts.",
13 | ReadContext: dataSourceHostsRead,
14 | Schema: map[string]*schema.Schema{
15 | "hosts": {
16 | Description: "A list of the Pritunl hosts resources.",
17 | Type: schema.TypeList,
18 | Computed: true,
19 | Elem: &schema.Resource{
20 | Schema: dataSourceHost().Schema,
21 | },
22 | },
23 | },
24 | }
25 | }
26 |
27 | func dataSourceHostsRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
28 | apiClient := meta.(pritunl.Client)
29 |
30 | hosts, err := apiClient.GetHosts()
31 | if err != nil {
32 | return diag.Errorf("could not find any host. Previous error message: %v", err)
33 | }
34 |
35 | var resultHosts []interface{}
36 |
37 | for _, host := range hosts {
38 | resultHosts = append(resultHosts, flattenHost(&host))
39 | }
40 |
41 | if err = d.Set("hosts", resultHosts); err != nil {
42 | return diag.FromErr(err)
43 | }
44 |
45 | d.SetId("hosts")
46 |
47 | return nil
48 | }
49 |
50 | func flattenHost(host *pritunl.Host) interface{} {
51 | result := map[string]interface{}{}
52 |
53 | result["id"] = host.ID
54 | result["name"] = host.Name
55 | result["hostname"] = host.Hostname
56 | result["public_addr"] = host.PublicAddr
57 | result["public_addr6"] = host.PublicAddr6
58 | result["routed_subnet6"] = host.RoutedSubnet6
59 | result["routed_subnet6_wg"] = host.RoutedSubnet6WG
60 | result["local_addr"] = host.LocalAddr
61 | result["local_addr6"] = host.LocalAddr6
62 | result["link_addr"] = host.LinkAddr
63 | result["sync_address"] = host.SyncAddress
64 | result["availability_group"] = host.AvailabilityGroup
65 | result["status"] = host.Status
66 |
67 | return result
68 | }
69 |
--------------------------------------------------------------------------------
/internal/provider/provider.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func Provider() *schema.Provider {
12 | return &schema.Provider{
13 | Schema: map[string]*schema.Schema{
14 | "url": {
15 | Type: schema.TypeString,
16 | Required: true,
17 | DefaultFunc: schema.EnvDefaultFunc("PRITUNL_URL", ""),
18 | },
19 | "token": {
20 | Type: schema.TypeString,
21 | Required: true,
22 | DefaultFunc: schema.EnvDefaultFunc("PRITUNL_TOKEN", ""),
23 | },
24 | "secret": {
25 | Type: schema.TypeString,
26 | Required: true,
27 | DefaultFunc: schema.EnvDefaultFunc("PRITUNL_SECRET", ""),
28 | },
29 | "insecure": {
30 | Type: schema.TypeBool,
31 | Required: true,
32 | DefaultFunc: schema.EnvDefaultFunc("PRITUNL_INSECURE", false),
33 | },
34 | "connection_check": {
35 | Type: schema.TypeBool,
36 | Optional: true,
37 | DefaultFunc: schema.EnvDefaultFunc("PRITUNL_CONNECTION_CHECK", true),
38 | },
39 | },
40 | ResourcesMap: map[string]*schema.Resource{
41 | "pritunl_organization": resourceOrganization(),
42 | "pritunl_server": resourceServer(),
43 | "pritunl_user": resourceUser(),
44 | },
45 | DataSourcesMap: map[string]*schema.Resource{
46 | "pritunl_host": dataSourceHost(),
47 | "pritunl_hosts": dataSourceHosts(),
48 | },
49 | ConfigureContextFunc: providerConfigure,
50 | }
51 | }
52 |
53 | func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
54 | url := d.Get("url").(string)
55 | token := d.Get("token").(string)
56 | secret := d.Get("secret").(string)
57 | insecure := d.Get("insecure").(bool)
58 | connectionCheck := d.Get("connection_check").(bool)
59 |
60 | apiClient := pritunl.NewClient(url, token, secret, insecure)
61 |
62 | if connectionCheck {
63 | // execute test api call to ensure that provided credentials are valid and pritunl api works
64 | err := apiClient.TestApiCall()
65 | if err != nil {
66 | return nil, diag.FromErr(err)
67 | }
68 | }
69 |
70 | return apiClient, nil
71 | }
72 |
--------------------------------------------------------------------------------
/internal/provider/provider_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
9 | "os"
10 | "strconv"
11 | "testing"
12 | )
13 |
14 | var providerFactories = map[string]func() (*schema.Provider, error){
15 | "pritunl": func() (*schema.Provider, error) {
16 | return Provider(), nil
17 | },
18 | }
19 |
20 | var testClient pritunl.Client
21 |
22 | func TestMain(m *testing.M) {
23 | if os.Getenv("TF_ACC") == "" {
24 | // short circuit non-acceptance test runs
25 | os.Exit(m.Run())
26 | }
27 |
28 | url := os.Getenv("PRITUNL_URL")
29 | token := os.Getenv("PRITUNL_TOKEN")
30 | secret := os.Getenv("PRITUNL_SECRET")
31 | insecure, _ := strconv.ParseBool(os.Getenv("PRITUNL_INSECURE"))
32 |
33 | testClient = pritunl.NewClient(url, token, secret, insecure)
34 | err := testClient.TestApiCall()
35 | if err != nil {
36 | panic(err)
37 | }
38 |
39 | resource.TestMain(m)
40 | }
41 |
42 | func preCheck(t *testing.T) {
43 | variables := []string{
44 | "PRITUNL_URL",
45 | "PRITUNL_TOKEN",
46 | "PRITUNL_SECRET",
47 | }
48 |
49 | for _, variable := range variables {
50 | value := os.Getenv(variable)
51 | if value == "" {
52 | t.Fatalf("`%s` must be set for acceptance tests!", variable)
53 | }
54 | }
55 | }
56 |
57 | func importStep(name string, ignore ...string) resource.TestStep {
58 | step := resource.TestStep{
59 | ResourceName: name,
60 | ImportState: true,
61 | ImportStateVerify: true,
62 | }
63 |
64 | if len(ignore) > 0 {
65 | step.ImportStateVerifyIgnore = ignore
66 | }
67 |
68 | return step
69 | }
70 |
71 | // pritunl_user import requires organization and user IDs
72 | func pritunlUserImportStep(name string) resource.TestStep {
73 | step := resource.TestStep{
74 | ResourceName: name,
75 | ImportState: true,
76 | ImportStateVerify: true,
77 | ImportStateVerifyIgnore: []string{"pin"},
78 | ImportStateIdFunc: func(state *terraform.State) (string, error) {
79 | userId := state.RootModule().Resources["pritunl_user.test"].Primary.Attributes["id"]
80 | orgId := state.RootModule().Resources["pritunl_organization.test"].Primary.Attributes["id"]
81 |
82 | return fmt.Sprintf("%s-%s", orgId, userId), nil
83 | },
84 | }
85 |
86 | return step
87 | }
88 |
--------------------------------------------------------------------------------
/internal/provider/resource_user_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
6 | "testing"
7 | )
8 |
9 | func TestAccPritunlUser(t *testing.T) {
10 |
11 | t.Run("creates users without error", func(t *testing.T) {
12 | username := "tfacc-user1"
13 | orgName := "tfacc-org1"
14 |
15 | check := resource.ComposeTestCheckFunc(
16 | resource.TestCheckResourceAttr("pritunl_user.test", "name", username),
17 | resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
18 | resource.TestCheckNoResourceAttr("pritunl_user.test", "pin"),
19 | )
20 |
21 | resource.Test(t, resource.TestCase{
22 | PreCheck: func() { preCheck(t) },
23 | ProviderFactories: providerFactories,
24 | Steps: []resource.TestStep{
25 | {
26 | Config: testPritunlUserConfig(username, orgName),
27 | Check: check,
28 | },
29 | // import test
30 | pritunlUserImportStep("pritunl_user.test"),
31 | },
32 | })
33 | })
34 | t.Run("creates users with PIN without error", func(t *testing.T) {
35 | username := "tfacc-user2"
36 | orgName := "tfacc-org2"
37 | pin := "123456"
38 |
39 | check := resource.ComposeTestCheckFunc(
40 | resource.TestCheckResourceAttr("pritunl_user.test", "name", username),
41 | resource.TestCheckResourceAttr("pritunl_user.test", "pin", pin),
42 | resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
43 | )
44 |
45 | resource.Test(t, resource.TestCase{
46 | PreCheck: func() { preCheck(t) },
47 | ProviderFactories: providerFactories,
48 | Steps: []resource.TestStep{
49 | {
50 | Config: testPritunlUserConfigWithPin(username, orgName, pin),
51 | Check: check,
52 | },
53 | // import test
54 | pritunlUserImportStep("pritunl_user.test"),
55 | },
56 | })
57 | })
58 | }
59 |
60 | func testPritunlUserConfig(username, orgName string) string {
61 | return testPritunlUserConfigWithPin(username, orgName, "")
62 | }
63 |
64 | func testPritunlUserConfigWithPin(username, orgName, pin string) string {
65 | resources := fmt.Sprintf(`
66 | resource "pritunl_organization" "test" {
67 | name = "%[2]s"
68 | }
69 |
70 | resource "pritunl_user" "test" {
71 | name = "%[1]s"
72 | organization_id = pritunl_organization.test.id
73 | `, username, orgName)
74 |
75 | if pin != "" {
76 | resources += fmt.Sprintf("pin = \"%[1]s\"\n", pin)
77 | }
78 |
79 | resources += "}\n"
80 |
81 | return resources
82 | }
83 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ master ]
9 | schedule:
10 | - cron: '26 1 * * 6'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [ 'go' ]
25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
26 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
27 |
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v6
31 |
32 | # Initializes the CodeQL tools for scanning.
33 | - name: Initialize CodeQL
34 | uses: github/codeql-action/init@v4
35 | with:
36 | languages: ${{ matrix.language }}
37 | # If you wish to specify custom queries, you can do so here or in a config file.
38 | # By default, queries listed here will override any specified in a config file.
39 | # Prefix the list here with "+" to use these queries and those in the config file.
40 |
41 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
42 | # queries: security-extended,security-and-quality
43 |
44 |
45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
46 | # If this step fails, then you should remove it and run the build manually (see below)
47 | - name: Autobuild
48 | uses: github/codeql-action/autobuild@v4
49 |
50 | # ℹ️ Command-line programs to run using the OS shell.
51 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
52 |
53 | # If the Autobuild fails above, remove it and uncomment the following three lines.
54 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
55 |
56 | # - run: |
57 | # echo "Run, Build Application using script"
58 | # ./location_of_script_within_repo/buildscript.sh
59 |
60 | - name: Perform CodeQL Analysis
61 | uses: github/codeql-action/analyze@v4
62 |
--------------------------------------------------------------------------------
/docs/resources/user.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "pritunl_user Resource - terraform-provider-pritunl"
4 | subcategory: ""
5 | description: |-
6 | The organization resource allows managing information about a particular Pritunl user.
7 | ---
8 |
9 | # pritunl_user (Resource)
10 |
11 | The organization resource allows managing information about a particular Pritunl user.
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Required
19 |
20 | - `name` (String) The name of the user.
21 | - `organization_id` (String) The organizations that user belongs to.
22 |
23 | ### Optional
24 |
25 | - `auth_type` (String) User authentication type. This will determine how the user authenticates. This should be set automatically when the user authenticates with single sign-on.
26 | - `bypass_secondary` (Boolean) Bypass secondary authentication such as the PIN and two-factor authentication. Use for server users that can't provide a two-factor code.
27 | - `client_to_client` (Boolean) Only allow this client to communicate with other clients. Access to routed networks will be blocked.
28 | - `disabled` (Boolean) Shows if user is disabled
29 | - `dns_servers` (List of String) Dns server with port to forward sub-domain dns requests coming from this users domain. Multiple dns servers may be separated by a comma.
30 | - `dns_suffix` (String) The suffix to use when forwarding dns requests. The full dns request will be the combination of the sub-domain of the users dns name suffixed by the dns suffix.
31 | - `email` (String) User email address.
32 | - `groups` (List of String) Enter list of groups to allow connections from. Names are case sensitive. If empty all groups will able to connect.
33 | - `mac_addresses` (List of String) Comma separated list of MAC addresses client is allowed to connect from. The validity of the MAC address provided by the VPN client cannot be verified.
34 | - `network_links` (List of String) Network address with cidr subnet. This will provision access to a clients local network to the attached vpn servers and other clients. Multiple networks may be separated by a comma. Router must have a static route to VPN virtual network through client.
35 | - `pin` (String) The PIN code for the user.
36 | - `port_forwarding` (List of Map of String) Comma seperated list of ports to forward using format source_port:dest_port/protocol or start_port-end_port/protocol. Such as 80, 80/tcp, 80:8000/tcp, 1000-2000/udp.
37 |
38 | ### Read-Only
39 |
40 | - `id` (String) The ID of this resource.
41 |
--------------------------------------------------------------------------------
/internal/provider/resource_organization.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | )
9 |
10 | func resourceOrganization() *schema.Resource {
11 | return &schema.Resource{
12 | Description: "The organization resource allows managing information about a particular Pritunl organization.",
13 | Schema: map[string]*schema.Schema{
14 | "name": {
15 | Type: schema.TypeString,
16 | Required: true,
17 | Description: "The name of the resource, also acts as it's unique ID",
18 | },
19 | },
20 | CreateContext: resourceCreateOrganization,
21 | ReadContext: resourceReadOrganization,
22 | UpdateContext: resourceUpdateOrganization,
23 | DeleteContext: resourceDeleteOrganization,
24 | Importer: &schema.ResourceImporter{
25 | StateContext: schema.ImportStatePassthroughContext,
26 | },
27 | }
28 | }
29 |
30 | // Uses for importing
31 | func resourceReadOrganization(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
32 | apiClient := meta.(pritunl.Client)
33 |
34 | organization, err := apiClient.GetOrganization(d.Id())
35 | if err != nil {
36 | return diag.FromErr(err)
37 | }
38 |
39 | d.Set("name", organization.Name)
40 |
41 | return nil
42 | }
43 |
44 | func resourceDeleteOrganization(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
45 | apiClient := meta.(pritunl.Client)
46 |
47 | err := apiClient.DeleteOrganization(d.Id())
48 | if err != nil {
49 | return diag.FromErr(err)
50 | }
51 |
52 | d.SetId("")
53 |
54 | return nil
55 | }
56 |
57 | func resourceUpdateOrganization(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
58 | apiClient := meta.(pritunl.Client)
59 |
60 | organization, err := apiClient.GetOrganization(d.Id())
61 | if err != nil {
62 | return diag.FromErr(err)
63 | }
64 |
65 | if d.HasChange("name") {
66 | organization.Name = d.Get("name").(string)
67 |
68 | err = apiClient.UpdateOrganization(d.Id(), organization)
69 | if err != nil {
70 | return diag.FromErr(err)
71 | }
72 | }
73 |
74 | return nil
75 | }
76 |
77 | func resourceCreateOrganization(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
78 | apiClient := meta.(pritunl.Client)
79 |
80 | organization, err := apiClient.CreateOrganization(d.Get("name").(string))
81 | if err != nil {
82 | return diag.FromErr(err)
83 | }
84 |
85 | d.SetId(organization.ID)
86 |
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/disc/terraform-provider-pritunl
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/hashicorp/go-cty v1.5.0
9 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
10 | )
11 |
12 | require (
13 | github.com/ProtonMail/go-crypto v1.1.6 // indirect
14 | github.com/agext/levenshtein v1.2.3 // indirect
15 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
16 | github.com/cloudflare/circl v1.6.1 // indirect
17 | github.com/fatih/color v1.16.0 // indirect
18 | github.com/go-test/deep v1.0.7 // indirect
19 | github.com/golang/protobuf v1.5.4 // indirect
20 | github.com/google/go-cmp v0.7.0 // indirect
21 | github.com/hashicorp/errwrap v1.1.0 // indirect
22 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect
23 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
24 | github.com/hashicorp/go-hclog v1.6.3 // indirect
25 | github.com/hashicorp/go-multierror v1.1.1 // indirect
26 | github.com/hashicorp/go-plugin v1.7.0 // indirect
27 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
28 | github.com/hashicorp/go-uuid v1.0.3 // indirect
29 | github.com/hashicorp/go-version v1.7.0 // indirect
30 | github.com/hashicorp/hc-install v0.9.2 // indirect
31 | github.com/hashicorp/hcl/v2 v2.24.0 // indirect
32 | github.com/hashicorp/logutils v1.0.0 // indirect
33 | github.com/hashicorp/terraform-exec v0.23.1 // indirect
34 | github.com/hashicorp/terraform-json v0.27.1 // indirect
35 | github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect
36 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
37 | github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
38 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect
39 | github.com/hashicorp/yamux v0.1.2 // indirect
40 | github.com/mattn/go-colorable v0.1.13 // indirect
41 | github.com/mattn/go-isatty v0.0.20 // indirect
42 | github.com/mitchellh/copystructure v1.2.0 // indirect
43 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
44 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect
45 | github.com/mitchellh/mapstructure v1.5.0 // indirect
46 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
47 | github.com/oklog/run v1.1.0 // indirect
48 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
49 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
50 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
51 | github.com/zclconf/go-cty v1.17.0 // indirect
52 | golang.org/x/crypto v0.45.0 // indirect
53 | golang.org/x/mod v0.29.0 // indirect
54 | golang.org/x/net v0.47.0 // indirect
55 | golang.org/x/sync v0.18.0 // indirect
56 | golang.org/x/sys v0.38.0 // indirect
57 | golang.org/x/text v0.31.0 // indirect
58 | golang.org/x/tools v0.38.0 // indirect
59 | google.golang.org/appengine v1.6.8 // indirect
60 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
61 | google.golang.org/grpc v1.75.1 // indirect
62 | google.golang.org/protobuf v1.36.9 // indirect
63 | )
64 |
--------------------------------------------------------------------------------
/internal/pritunl/user.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | type User struct {
8 | ID string `json:"id,omitempty"`
9 | Name string `json:"name"`
10 | Type string `json:"type,omitempty"`
11 | AuthType string `json:"auth_type,omitempty"`
12 | DnsServers []string `json:"dns_servers,omitempty"`
13 | DnsSuffix string `json:"dns_suffix,omitempty"`
14 | DnsMapping string `json:"dns_mapping,omitempty"`
15 | Disabled bool `json:"disabled,omitempty"`
16 | NetworkLinks []string `json:"network_links,omitempty"`
17 | PortForwarding []map[string]interface{} `json:"port_forwarding,omitempty"`
18 | Email string `json:"email,omitempty"`
19 | Status bool `json:"status,omitempty"`
20 | OtpSecret string `json:"otp_secret,omitempty"`
21 | ClientToClient bool `json:"client_to_client,omitempty"`
22 | MacAddresses []string `json:"mac_addresses,omitempty"`
23 | YubicoID string `json:"yubico_id,omitempty"`
24 | SSO interface{} `json:"sso,omitempty"`
25 | BypassSecondary bool `json:"bypass_secondary,omitempty"`
26 | Groups []string `json:"groups,omitempty"`
27 | Audit bool `json:"audit,omitempty"`
28 | Gravatar bool `json:"gravatar,omitempty"`
29 | OtpAuth bool `json:"otp_auth,omitempty"`
30 | DeviceAuth bool `json:"device_auth,omitempty"`
31 | Organization string `json:"organization,omitempty"`
32 | Pin *Pin `json:"pin,omitempty"`
33 | }
34 |
35 | type PortForwarding struct {
36 | Dport string `json:"dport"`
37 | Protocol string `json:"protocol"`
38 | Port string `json:"port"`
39 | }
40 |
41 | type Pin struct {
42 | IsSet bool
43 | Secret string
44 | }
45 |
46 | // MarshalJSON customizes the JSON encoding of the Pin struct.
47 | //
48 | // When marshaling a User JSON, the "pin" field will contain the PIN secret
49 | // if it is set, otherwise the field is excluded. This is used when making
50 | // a user create or update request to the Pritunl API.
51 | func (p *Pin) MarshalJSON() ([]byte, error) {
52 | if p.Secret != "" {
53 | return json.Marshal(p.Secret)
54 | }
55 | return json.Marshal(nil)
56 | }
57 |
58 | // UnmarshalJSON customizes the JSON decoding of the Pin struct.
59 | //
60 | // When unmarshaling a User JSON, the "pin" field will contain a boolean
61 | // indicating whether the user has a PIN set or not. This is used when
62 | // reading a user response from the Pritunl API.
63 | func (p *Pin) UnmarshalJSON(data []byte) error {
64 | var b bool
65 | err := json.Unmarshal(data, &b)
66 | if err == nil {
67 | p.IsSet = b
68 | p.Secret = ""
69 | }
70 | return err
71 | }
72 |
--------------------------------------------------------------------------------
/internal/pritunl/server.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | )
7 |
8 | const (
9 | ServerStatusOnline = "online"
10 | ServerStatusOffline = "offline"
11 |
12 | ServerNetworkModeTunnel = "tunnel"
13 | ServerNetworkModeBridge = "bridge"
14 | )
15 |
16 | type Server struct {
17 | ID string `json:"id,omitempty"`
18 | Name string `json:"name"`
19 | Protocol string `json:"protocol,omitempty"`
20 | Cipher string `json:"cipher,omitempty"`
21 | Hash string `json:"hash,omitempty"`
22 | Port int `json:"port,omitempty"`
23 | Network string `json:"network,omitempty"`
24 | WG bool `json:"wg,omitempty"`
25 | PortWG int `json:"port_wg,omitempty"`
26 | NetworkWG string `json:"network_wg,omitempty"`
27 | NetworkMode string `json:"network_mode,omitempty"`
28 | NetworkStart string `json:"network_start,omitempty"`
29 | NetworkEnd string `json:"network_end,omitempty"`
30 | RestrictRoutes bool `json:"restrict_routes,omitempty"`
31 | IPv6 bool `json:"ipv6,omitempty"`
32 | IPv6Firewall bool `json:"ipv6_firewall,omitempty"`
33 | BindAddress string `json:"bind_address,omitempty"`
34 | DhParamBits int `json:"dh_param_bits,omitempty"`
35 | Groups []string `json:"groups,omitempty"`
36 | MultiDevice bool `json:"multi_device,omitempty"`
37 | DnsServers []string `json:"dns_servers,omitempty"`
38 | SearchDomain string `json:"search_domain,omitempty"`
39 | InterClient bool `json:"inter_client,omitempty"`
40 | PingInterval int `json:"ping_interval,omitempty"`
41 | PingTimeout int `json:"ping_timeout,omitempty"`
42 | LinkPingInterval int `json:"link_ping_interval,omitempty"`
43 | LinkPingTimeout int `json:"link_ping_timeout,omitempty"`
44 | InactiveTimeout int `json:"inactive_timeout,omitempty"`
45 | SessionTimeout int `json:"session_timeout,omitempty"`
46 | AllowedDevices string `json:"allowed_devices,omitempty"`
47 | MaxClients int `json:"max_clients,omitempty"`
48 | MaxDevices int `json:"max_devices,omitempty"`
49 | ReplicaCount int `json:"replica_count,omitempty"`
50 | VxLan bool `json:"vxlan,omitempty"`
51 | DnsMapping bool `json:"dns_mapping,omitempty"`
52 | PreConnectMsg string `json:"pre_connect_msg,omitempty"`
53 | SsoAuth bool `json:"sso_auth,omitempty"`
54 | OtpAuth bool `json:"otp_auth,omitempty"`
55 | DeviceAuth bool `json:"device_auth,omitempty"`
56 | DynamicFirewall bool `json:"dynamic_firewall,omitempty"`
57 | MssFix int `json:"mss_fix,omitempty"`
58 | LzoCompression bool `json:"lzo_compression,omitempty"`
59 | BlockOutsideDns bool `json:"block_outside_dns,omitempty"`
60 | JumboFrames bool `json:"jumbo_frames,omitempty"`
61 | Debug bool `json:"debug,omitempty"`
62 | Status string `json:"status,omitempty"`
63 | }
64 |
65 | func (s *Server) MarshalJSON() ([]byte, error) {
66 | type Alias Server
67 | return json.Marshal(&struct {
68 | // Pritunl API expects input mss_fix value as a string, but returns as an int
69 | MssFix string `json:"mss_fix"`
70 | *Alias
71 | }{
72 | MssFix: strconv.Itoa(s.MssFix),
73 | Alias: (*Alias)(s),
74 | })
75 | }
76 |
--------------------------------------------------------------------------------
/internal/provider/data_source_host.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func dataSourceHost() *schema.Resource {
12 | return &schema.Resource{
13 | Description: "Use this data source to get information about the Pritunl hosts.",
14 | ReadContext: dataSourceHostRead,
15 | Schema: map[string]*schema.Schema{
16 | "id": {
17 | Type: schema.TypeString,
18 | Computed: true,
19 | },
20 | "hostname": {
21 | Description: "Hostname",
22 | Type: schema.TypeString,
23 | Required: true,
24 | },
25 | "name": {
26 | Description: "Name of host",
27 | Type: schema.TypeString,
28 | Computed: true,
29 | },
30 | "public_addr": {
31 | Description: "Public IP address or domain name of the host",
32 | Type: schema.TypeString,
33 | Computed: true,
34 | },
35 | "public_addr6": {
36 | Description: "Public IPv6 address or domain name of the host",
37 | Type: schema.TypeString,
38 | Computed: true,
39 | },
40 | "routed_subnet6": {
41 | Description: "IPv6 subnet that is routed to the host",
42 | Type: schema.TypeString,
43 | Computed: true,
44 | },
45 | "routed_subnet6_wg": {
46 | Description: "IPv6 WG subnet that is routed to the host",
47 | Type: schema.TypeString,
48 | Computed: true,
49 | },
50 | "local_addr": {
51 | Description: "Local network address for server",
52 | Type: schema.TypeString,
53 | Computed: true,
54 | },
55 | "local_addr6": {
56 | Description: "Local IPv6 network address for server",
57 | Type: schema.TypeString,
58 | Computed: true,
59 | },
60 | "availability_group": {
61 | Description: "Availability group for host. Replicated servers will only be replicated to a group of hosts in the same availability group\"",
62 | Type: schema.TypeString,
63 | Computed: true,
64 | },
65 | "link_addr": {
66 | Description: "IP address or domain used when linked servers connect to a linked server on this host",
67 | Type: schema.TypeString,
68 | Computed: true,
69 | },
70 | "sync_address": {
71 | Description: "IP address or domain used by users when syncing configuration. This is needed when using a load balancer.",
72 | Type: schema.TypeString,
73 | Computed: true,
74 | },
75 | "status": {
76 | Description: "Status of host",
77 | Type: schema.TypeString,
78 | Computed: true,
79 | },
80 | },
81 | }
82 | }
83 |
84 | func dataSourceHostRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
85 | hostname := d.Get("hostname")
86 | filterFunction := func(host pritunl.Host) bool {
87 | return host.Hostname == hostname
88 | }
89 |
90 | host, err := filterHosts(meta, filterFunction)
91 | if err != nil {
92 | return diag.Errorf("could not find host with a hostname %s. Previous error message: %v", hostname, err)
93 | }
94 |
95 | d.SetId(host.ID)
96 | d.Set("name", host.Name)
97 | d.Set("hostname", host.Hostname)
98 | d.Set("public_addr", host.PublicAddr)
99 | d.Set("public_addr6", host.PublicAddr6)
100 | d.Set("routed_subnet6", host.RoutedSubnet6)
101 | d.Set("routed_subnet6_wg", host.RoutedSubnet6WG)
102 | d.Set("local_addr", host.LocalAddr)
103 | d.Set("local_addr6", host.LocalAddr6)
104 | d.Set("link_addr", host.LinkAddr)
105 | d.Set("sync_address", host.SyncAddress)
106 | d.Set("availability_group", host.AvailabilityGroup)
107 | d.Set("status", host.Status)
108 |
109 | return nil
110 | }
111 |
112 | func filterHosts(meta interface{}, test func(host pritunl.Host) bool) (pritunl.Host, error) {
113 | apiClient := meta.(pritunl.Client)
114 |
115 | hosts, err := apiClient.GetHosts()
116 |
117 | if err != nil {
118 | return pritunl.Host{}, err
119 | }
120 |
121 | for _, dir := range hosts {
122 | if test(dir) {
123 | return dir, nil
124 | }
125 | }
126 |
127 | return pritunl.Host{}, errors.New("could not find a host with specified parameters")
128 | }
129 |
--------------------------------------------------------------------------------
/docs/resources/server.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "pritunl_server Resource - terraform-provider-pritunl"
4 | subcategory: ""
5 | description: |-
6 | The organization resource allows managing information about a particular Pritunl server.
7 | ---
8 |
9 | # pritunl_server (Resource)
10 |
11 | The organization resource allows managing information about a particular Pritunl server.
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Required
19 |
20 | - `name` (String) The name of the server
21 |
22 | ### Optional
23 |
24 | - `allowed_devices` (String) Device types permitted to connect to server.
25 | - `bind_address` (String) Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks
26 | - `block_outside_dns` (Boolean) Block outside DNS on Windows clients.
27 | - `cipher` (String) The cipher for the server
28 | - `debug` (Boolean) Show server debugging information in output.
29 | - `device_auth` (Boolean) Require administrator to approve every client device using TPM or Apple Secure Enclave
30 | - `dh_param_bits` (Number) Size of DH parameters
31 | - `dns_mapping` (Boolean) Map the vpn clients ip address to the .vpn domain such as example_user.example_org.vpn This will conflict with the DNS port if systemd-resolve is running.
32 | - `dns_servers` (List of String) Enter list of DNS servers applied on the client
33 | - `dynamic_firewall` (Boolean) Block VPN server ports by default and open port for client IP address after authenticating with HTTPS request
34 | - `groups` (List of String) Enter list of groups to allow connections from. Names are case sensitive. If empty all groups will able to connect
35 | - `hash` (String) The hash for the server
36 | - `host_ids` (List of String) The list of attached hosts to the server
37 | - `inactive_timeout` (Number) Disconnects users after the specified number of seconds of inactivity.
38 | - `inter_client` (Boolean) Enable inter-client routing across hosts.
39 | - `ipv6` (Boolean) Enables IPv6 on server, requires IPv6 network interface
40 | - `link_ping_interval` (Number) Time in between pings used when multiple users have the same network link to failover to another user when one network link fails.
41 | - `link_ping_timeout` (Number) Optional, ping timeout used when multiple users have the same network link to failover to another user when one network link fails..
42 | - `max_clients` (Number) Maximum number of clients connected to a server or to each server replica.
43 | - `max_devices` (Number) Maximum number of devices per client connected to a server.
44 | - `mss_fix` (Number) MSS fix value
45 | - `multi_device` (Boolean) Allow users to connect with multiple devices concurrently.
46 | - `network` (String) Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks
47 | - `network_end` (String) Ending network address for the bridged VPN client IP addresses. Must be in the subnet of the server network.
48 | - `network_mode` (String) Sets network mode. Bridged mode is not recommended using it will impact performance and client support will be limited.
49 | - `network_start` (String) Starting network address for the bridged VPN client IP addresses. Must be in the subnet of the server network.
50 | - `network_wg` (String) Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks
51 | - `organization_ids` (List of String) The list of attached organizations to the server.
52 | - `otp_auth` (Boolean) Enables two-step authentication using Google Authenticator. Verification code is entered as the user password when connecting
53 | - `ping_interval` (Number) Interval to ping client
54 | - `ping_timeout` (Number) Timeout for client ping. Must be greater then ping interval
55 | - `port` (Number) The port for the server
56 | - `port_wg` (Number) Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks
57 | - `pre_connect_msg` (String) Messages that will be shown after connect to the server
58 | - `protocol` (String) The protocol for the server
59 | - `replica_count` (Number) Replicate server across multiple hosts.
60 | - `restrict_routes` (Boolean) Prevent traffic from networks not specified in the servers routes from being tunneled over the vpn.
61 | - `route` (Block List) The list of attached routes to the server (see [below for nested schema](#nestedblock--route))
62 | - `search_domain` (String) DNS search domain for clients. Separate multiple search domains by a comma.
63 | - `session_timeout` (Number) Disconnect users after the specified number of seconds.
64 | - `sso_auth` (Boolean) Require client to authenticate with single sign-on provider on each connection using web browser. Requires client to have access to Pritunl web server port and running updated Pritunl Client. Single sign-on provider must already be configured for this feature to work properly
65 | - `status` (String) The status of the server
66 | - `vxlan` (Boolean) Use VXLan for routing client-to-client traffic with replicated servers.
67 |
68 | ### Read-Only
69 |
70 | - `id` (String) The ID of this resource.
71 |
72 |
73 | ### Nested Schema for `route`
74 |
75 | Required:
76 |
77 | - `network` (String) Network address with subnet to route
78 |
79 | Optional:
80 |
81 | - `comment` (String) Comment for route
82 | - `nat` (Boolean) NAT vpn traffic destined to this network
83 | - `net_gateway` (Boolean) Net Gateway vpn traffic destined to this network
84 |
--------------------------------------------------------------------------------
/tools/wait-for-it.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to test if a given TCP host/port are available
3 |
4 | WAITFORIT_cmdname=${0##*/}
5 |
6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
7 |
8 | usage()
9 | {
10 | cat << USAGE >&2
11 | Usage:
12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
13 | -h HOST | --host=HOST Host or IP under test
14 | -p PORT | --port=PORT TCP port under test
15 | Alternatively, you specify the host and port as host:port
16 | -s | --strict Only execute subcommand if the test succeeds
17 | -q | --quiet Don't output any status messages
18 | -t TIMEOUT | --timeout=TIMEOUT
19 | Timeout in seconds, zero for no timeout
20 | -- COMMAND ARGS Execute command with args after the test finishes
21 | USAGE
22 | exit 1
23 | }
24 |
25 | wait_for()
26 | {
27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
29 | else
30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
31 | fi
32 | WAITFORIT_start_ts=$(date +%s)
33 | while :
34 | do
35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT
37 | WAITFORIT_result=$?
38 | else
39 | (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
40 | WAITFORIT_result=$?
41 | fi
42 | if [[ $WAITFORIT_result -eq 0 ]]; then
43 | WAITFORIT_end_ts=$(date +%s)
44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
45 | break
46 | fi
47 | sleep 1
48 | done
49 | return $WAITFORIT_result
50 | }
51 |
52 | wait_for_wrapper()
53 | {
54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then
56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
57 | else
58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
59 | fi
60 | WAITFORIT_PID=$!
61 | trap "kill -INT -$WAITFORIT_PID" INT
62 | wait $WAITFORIT_PID
63 | WAITFORIT_RESULT=$?
64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then
65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
66 | fi
67 | return $WAITFORIT_RESULT
68 | }
69 |
70 | # process arguments
71 | while [[ $# -gt 0 ]]
72 | do
73 | case "$1" in
74 | *:* )
75 | WAITFORIT_hostport=(${1//:/ })
76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]}
77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]}
78 | shift 1
79 | ;;
80 | --child)
81 | WAITFORIT_CHILD=1
82 | shift 1
83 | ;;
84 | -q | --quiet)
85 | WAITFORIT_QUIET=1
86 | shift 1
87 | ;;
88 | -s | --strict)
89 | WAITFORIT_STRICT=1
90 | shift 1
91 | ;;
92 | -h)
93 | WAITFORIT_HOST="$2"
94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi
95 | shift 2
96 | ;;
97 | --host=*)
98 | WAITFORIT_HOST="${1#*=}"
99 | shift 1
100 | ;;
101 | -p)
102 | WAITFORIT_PORT="$2"
103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi
104 | shift 2
105 | ;;
106 | --port=*)
107 | WAITFORIT_PORT="${1#*=}"
108 | shift 1
109 | ;;
110 | -t)
111 | WAITFORIT_TIMEOUT="$2"
112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
113 | shift 2
114 | ;;
115 | --timeout=*)
116 | WAITFORIT_TIMEOUT="${1#*=}"
117 | shift 1
118 | ;;
119 | --)
120 | shift
121 | WAITFORIT_CLI=("$@")
122 | break
123 | ;;
124 | --help)
125 | usage
126 | ;;
127 | *)
128 | echoerr "Unknown argument: $1"
129 | usage
130 | ;;
131 | esac
132 | done
133 |
134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
135 | echoerr "Error: you need to provide a host and port to test."
136 | usage
137 | fi
138 |
139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
143 |
144 | # Check to see if timeout is from busybox?
145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
147 |
148 | WAITFORIT_BUSYTIMEFLAG=""
149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
150 | WAITFORIT_ISBUSY=1
151 | # Check if busybox timeout uses -t flag
152 | # (recent Alpine versions don't support -t anymore)
153 | if timeout &>/dev/stdout | grep -q -e '-t '; then
154 | WAITFORIT_BUSYTIMEFLAG="-t"
155 | fi
156 | else
157 | WAITFORIT_ISBUSY=0
158 | fi
159 |
160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then
161 | wait_for
162 | WAITFORIT_RESULT=$?
163 | exit $WAITFORIT_RESULT
164 | else
165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
166 | wait_for_wrapper
167 | WAITFORIT_RESULT=$?
168 | else
169 | wait_for
170 | WAITFORIT_RESULT=$?
171 | fi
172 | fi
173 |
174 | if [[ $WAITFORIT_CLI != "" ]]; then
175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
177 | exit $WAITFORIT_RESULT
178 | fi
179 | exec "${WAITFORIT_CLI[@]}"
180 | else
181 | exit $WAITFORIT_RESULT
182 | fi
183 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Terraform Provider for Pritunl VPN Server
9 |
10 | [](https://github.com/disc/terraform-provider-pritunl/releases)
11 | [](https://registry.terraform.io/providers/disc/pritunl/latest/docs)
12 | [](https://github.com/disc/terraform-provider-pritunl/blob/master/LICENSE)
13 | [](https://goreportcard.com/report/github.com/disc/terraform-provider-pritunl)
14 |
15 | - Website: https://www.terraform.io
16 | - Pritunl VPN Server: https://pritunl.com/
17 | - Provider: [disc/pritunl](https://registry.terraform.io/providers/disc/pritunl/latest)
18 |
19 | ## Maintainers
20 |
21 | [Alexandr Hacicheant](mailto:a.hacicheant@gmail.com)
22 |
23 | ## Requirements
24 | - [Terraform](https://www.terraform.io/downloads.html) >=0.13.x
25 | - [Go](https://golang.org/doc/install) 1.24.x (to build the provider plugin)
26 |
27 | ## Building The Provider
28 |
29 | ```sh
30 | $ git clone git@github.com:disc/terraform-provider-pritunl
31 | $ make build
32 | ```
33 |
34 | ## Example usage
35 |
36 | Take a look at the examples in the [documentation](https://registry.terraform.io/providers/disc/pritunl/latest/docs) of the registry
37 | or use the following example:
38 |
39 |
40 | ```hcl
41 | # Set the required provider and versions
42 | terraform {
43 | required_providers {
44 | pritunl = {
45 | source = "disc/pritunl"
46 | version = "0.3.1"
47 | }
48 | }
49 | }
50 |
51 | # Configure the pritunl provider
52 | provider "pritunl" {
53 | url = "https://vpn.server.com"
54 | token = "api-token"
55 | secret = "api-secret"
56 | insecure = false
57 | }
58 |
59 | # Create a pritunl organization resource
60 | resource "pritunl_organization" "developers" {
61 | name = "Developers"
62 | }
63 |
64 | # Create a pritunl user resource
65 | resource "pritunl_user" "steve" {
66 | name = "steve"
67 | organization_id = pritunl_organization.developers.id
68 | email = "steve@developers.com"
69 | groups = [
70 | "developers",
71 | ]
72 | }
73 |
74 | # Create a pritunl server resource
75 | resource "pritunl_server" "example" {
76 | name = "example"
77 | port = 15500
78 | protocol = "udp"
79 | network = "192.168.1.0/24"
80 | groups = [
81 | "admins",
82 | "developers",
83 | ]
84 |
85 | # Attach the organization to the server
86 | organization_ids = [
87 | pritunl_organization.developers.id,
88 | ]
89 |
90 | # Describe all the routes manually
91 | # Default route 0.0.0.0/0 will be deleted on the server creation
92 | route {
93 | network = "10.0.0.0/24"
94 | comment = "Private network #1"
95 | nat = true
96 | }
97 |
98 | route {
99 | network = "10.2.0.0/24"
100 | comment = "Private network #2"
101 | nat = false
102 | }
103 |
104 | # Or create dynamic routes from variables
105 | dynamic "route" {
106 | for_each = var.common_routes
107 | content {
108 | network = route.value["network"]
109 | comment = route.value["comment"]
110 | nat = route.value["nat"]
111 | }
112 | }
113 | }
114 | ```
115 | ### Multiple hosts per server (Replicated servers feature)
116 | It also supports multiple host server's configuration with host datasource which can be matched by a hostname.
117 | ```hcl
118 | data "pritunl_host" "main" {
119 | hostname = "nyc1.vpn.host"
120 | }
121 |
122 | data "pritunl_host" "reserve" {
123 | hostname = "nyc3.vpn.host"
124 | }
125 |
126 | resource "pritunl_server" "test" {
127 | name = "some-server"
128 | network = "192.168.250.0/24"
129 | port = 15500
130 |
131 | host_ids = [
132 | data.pritunl_host.main.id,
133 | data.pritunl_host.reserve.id,
134 | ]
135 | }
136 | ```
137 |
138 | ## Importing exist resources
139 |
140 | Describe exist resource in the terraform file first and then import them:
141 |
142 | Import an organization:
143 | ```hcl
144 | # Describe a pritunl organization resource
145 | resource "pritunl_organization" "developers" {
146 | name = "Developers"
147 | }
148 | ```
149 |
150 | Execute the shell command:
151 | ```sh
152 | terraform import pritunl_organization.developers ${ORGANIZATION_ID}
153 | terraform import pritunl_organization.developers 610e42d2a0ed366f41dfe6e8
154 | ```
155 | The organization ID (as well as other resource IDs) can be found in the Pritunl API responses or in the HTML document response.
156 |
157 | Import a user:
158 | ```hcl
159 | # Describe a pritunl user resource
160 | resource "pritunl_user" "steve" {
161 | name = "steve"
162 | organization_id = pritunl_organization.developers.id
163 | email = "steve@developers.com"
164 | }
165 | ```
166 |
167 | Execute the shell command:
168 | ```sh
169 | terraform import pritunl_user.steve ${ORGANIZATION_ID}-${USER_ID}
170 | terraform import pritunl_user.steve 610e42d2a0ed366f41dfe6e8-610e42d6a0ed366f41dfe72b
171 | ```
172 |
173 | Import a server:
174 |
175 | ```hcl
176 | # Describe a pritunl server resource
177 | resource "pritunl_server" "example" {
178 | name = "example"
179 | port = 15500
180 | protocol = "udp"
181 | network = "192.168.1.0/24"
182 | groups = [
183 | "developers",
184 | ]
185 |
186 | # Attach the organization to the server
187 | organization_ids = [
188 | pritunl_organization.developers.id,
189 | ]
190 |
191 | # Describe all the routes manually
192 | # Default route 0.0.0.0/0 will be deleted on the server creation
193 | route {
194 | network = "10.0.0.0/24"
195 | comment = "Private network #1"
196 | nat = true
197 | }
198 | }
199 | ```
200 |
201 | Execute the shell command:
202 | ```sh
203 | terraform import pritunl_server.example ${SERVER_ID}
204 | terraform import pritunl_server.example 60cd0bfa7723cf3c911468a8
205 | ```
206 |
207 | ## License
208 |
209 | The Terraform Pritunl Provider is available to everyone under the terms of the Mozilla Public License Version 2.0. [Take a look the LICENSE file](LICENSE).
210 |
--------------------------------------------------------------------------------
/internal/provider/resource_user.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
12 | )
13 |
14 | func resourceUser() *schema.Resource {
15 | return &schema.Resource{
16 | Description: "The organization resource allows managing information about a particular Pritunl user.",
17 | Schema: map[string]*schema.Schema{
18 | "name": {
19 | Type: schema.TypeString,
20 | Required: true,
21 | Description: "The name of the user.",
22 | },
23 | "organization_id": {
24 | Type: schema.TypeString,
25 | Required: true,
26 | ForceNew: true,
27 | Description: "The organizations that user belongs to.",
28 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
29 | return validation.StringIsNotEmpty(i, s)
30 | },
31 | },
32 | "groups": {
33 | Type: schema.TypeList,
34 | Elem: &schema.Schema{
35 | Type: schema.TypeString,
36 | },
37 | Optional: true,
38 | Description: "Enter list of groups to allow connections from. Names are case sensitive. If empty all groups will able to connect.",
39 | },
40 | "email": {
41 | Type: schema.TypeString,
42 | Optional: true,
43 | Description: "User email address.",
44 | },
45 | "disabled": {
46 | Type: schema.TypeBool,
47 | Optional: true,
48 | Description: "Shows if user is disabled",
49 | },
50 | "port_forwarding": {
51 | Type: schema.TypeList,
52 | Elem: &schema.Schema{
53 | Type: schema.TypeMap,
54 | },
55 | Optional: true,
56 | Description: "Comma seperated list of ports to forward using format source_port:dest_port/protocol or start_port-end_port/protocol. Such as 80, 80/tcp, 80:8000/tcp, 1000-2000/udp.",
57 | },
58 | "network_links": {
59 | Type: schema.TypeList,
60 | Elem: &schema.Schema{
61 | Type: schema.TypeString,
62 | },
63 | Optional: true,
64 | Description: "Network address with cidr subnet. This will provision access to a clients local network to the attached vpn servers and other clients. Multiple networks may be separated by a comma. Router must have a static route to VPN virtual network through client.",
65 | },
66 | "client_to_client": {
67 | Type: schema.TypeBool,
68 | Optional: true,
69 | Description: "Only allow this client to communicate with other clients. Access to routed networks will be blocked.",
70 | },
71 | "auth_type": {
72 | Type: schema.TypeString,
73 | Optional: true,
74 | Computed: true,
75 | Description: "User authentication type. This will determine how the user authenticates. This should be set automatically when the user authenticates with single sign-on.",
76 | ValidateFunc: validation.StringInSlice([]string{"local", "duo", "yubico", "azure", "azure_duo", "azure_yubico", "google", "google_duo", "google_yubico", "slack", "slack_duo", "slack_yubico", "saml", "saml_duo", "saml_yubico", "saml_okta", "saml_okta_duo", "saml_okta_yubico", "saml_onelogin", "saml_onelogin_duo", "saml_onelogin_yubico", "radius", "radius_duo", "plugin"}, false),
77 | },
78 | "mac_addresses": {
79 | Type: schema.TypeList,
80 | Elem: &schema.Schema{
81 | Type: schema.TypeString,
82 | },
83 | Optional: true,
84 | Description: "Comma separated list of MAC addresses client is allowed to connect from. The validity of the MAC address provided by the VPN client cannot be verified.",
85 | },
86 | "dns_servers": {
87 | Type: schema.TypeList,
88 | Elem: &schema.Schema{
89 | Type: schema.TypeString,
90 | },
91 | Optional: true,
92 | Description: "Dns server with port to forward sub-domain dns requests coming from this users domain. Multiple dns servers may be separated by a comma.",
93 | },
94 | "dns_suffix": {
95 | Type: schema.TypeString,
96 | Optional: true,
97 | Description: "The suffix to use when forwarding dns requests. The full dns request will be the combination of the sub-domain of the users dns name suffixed by the dns suffix.",
98 | },
99 | "bypass_secondary": {
100 | Type: schema.TypeBool,
101 | Optional: true,
102 | Description: "Bypass secondary authentication such as the PIN and two-factor authentication. Use for server users that can't provide a two-factor code.",
103 | },
104 | "pin": {
105 | Type: schema.TypeString,
106 | Optional: true,
107 | Sensitive: true,
108 | Description: "The PIN for user authentication.",
109 | },
110 | },
111 | CreateContext: resourceUserCreate,
112 | ReadContext: resourceUserRead,
113 | UpdateContext: resourceUserUpdate,
114 | DeleteContext: resourceUserDelete,
115 | Importer: &schema.ResourceImporter{
116 | StateContext: resourceUserImport,
117 | },
118 | }
119 | }
120 |
121 | func resourceUserRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
122 | apiClient := meta.(pritunl.Client)
123 |
124 | user, err := apiClient.GetUser(d.Id(), d.Get("organization_id").(string))
125 | if err != nil {
126 | return diag.FromErr(err)
127 | }
128 |
129 | d.Set("name", user.Name)
130 | d.Set("auth_type", user.AuthType)
131 | d.Set("dns_servers", user.DnsServers)
132 | d.Set("dns_suffix", user.DnsSuffix)
133 | d.Set("disabled", user.Disabled)
134 | d.Set("network_links", user.NetworkLinks)
135 | d.Set("port_forwarding", user.PortForwarding)
136 | d.Set("email", user.Email)
137 | d.Set("client_to_client", user.ClientToClient)
138 | d.Set("mac_addresses", user.MacAddresses)
139 | d.Set("bypass_secondary", user.BypassSecondary)
140 | d.Set("organization_id", user.Organization)
141 |
142 | if len(user.Groups) > 0 {
143 | groupsList := make([]string, 0)
144 |
145 | for _, group := range user.Groups {
146 | groupsList = append(groupsList, group)
147 | }
148 |
149 | declaredGroups, ok := d.Get("groups").([]interface{})
150 | if !ok {
151 | return diag.Errorf("failed to parse groups for the user: %s", user.Name)
152 | }
153 | d.Set("groups", matchStringEntitiesWithSchema(groupsList, declaredGroups))
154 | }
155 |
156 | return nil
157 | }
158 |
159 | func resourceUserDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
160 | apiClient := meta.(pritunl.Client)
161 |
162 | err := apiClient.DeleteUser(d.Id(), d.Get("organization_id").(string))
163 | if err != nil {
164 | return diag.FromErr(err)
165 | }
166 |
167 | d.SetId("")
168 |
169 | return nil
170 | }
171 |
172 | func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
173 | apiClient := meta.(pritunl.Client)
174 |
175 | user, err := apiClient.GetUser(d.Id(), d.Get("organization_id").(string))
176 | if err != nil {
177 | return diag.FromErr(err)
178 | }
179 |
180 | if d.HasChange("pin") {
181 | if v, ok := d.GetOk("pin"); ok {
182 | user.Pin = &pritunl.Pin{Secret: v.(string)}
183 | }
184 | }
185 |
186 | if v, ok := d.GetOk("name"); ok {
187 | user.Name = v.(string)
188 | }
189 |
190 | if v, ok := d.GetOk("organization_id"); ok {
191 | user.Organization = v.(string)
192 | }
193 |
194 | if d.HasChange("groups") {
195 | groups := make([]string, 0)
196 | for _, v := range d.Get("groups").([]interface{}) {
197 | groups = append(groups, v.(string))
198 | }
199 | user.Groups = groups
200 | }
201 |
202 | if v, ok := d.GetOk("email"); ok {
203 | user.Email = v.(string)
204 | }
205 |
206 | // TODO: Fixme
207 | if v, ok := d.GetOk("disabled"); ok {
208 | user.Disabled = v.(bool)
209 | }
210 |
211 | if d.HasChange("port_forwarding") {
212 | portForwarding := make([]map[string]interface{}, 0)
213 | for _, v := range d.Get("port_forwarding").([]interface{}) {
214 | portForwarding = append(portForwarding, v.(map[string]interface{}))
215 | }
216 | user.PortForwarding = portForwarding
217 | }
218 |
219 | if d.HasChange("network_links") {
220 | networkLinks := make([]string, 0)
221 | for _, v := range d.Get("network_links").([]interface{}) {
222 | networkLinks = append(networkLinks, v.(string))
223 | }
224 | user.NetworkLinks = networkLinks
225 | }
226 |
227 | if v, ok := d.GetOk("client_to_client"); ok {
228 | user.ClientToClient = v.(bool)
229 | }
230 |
231 | if v, ok := d.GetOk("auth_type"); ok {
232 | user.AuthType = v.(string)
233 | }
234 |
235 | if d.HasChange("mac_addresses") {
236 | macAddresses := make([]string, 0)
237 | for _, v := range d.Get("mac_addresses").([]interface{}) {
238 | macAddresses = append(macAddresses, v.(string))
239 | }
240 | user.MacAddresses = macAddresses
241 | }
242 |
243 | if d.HasChange("dns_servers") {
244 | dnsServers := make([]string, 0)
245 | for _, v := range d.Get("dns_servers").([]interface{}) {
246 | dnsServers = append(dnsServers, v.(string))
247 | }
248 | user.DnsServers = dnsServers
249 | }
250 |
251 | if v, ok := d.GetOk("dns_suffix"); ok {
252 | user.DnsSuffix = v.(string)
253 | }
254 |
255 | if v, ok := d.GetOk("bypass_secondary"); ok {
256 | user.BypassSecondary = v.(bool)
257 | }
258 |
259 | err = apiClient.UpdateUser(d.Id(), user)
260 | if err != nil {
261 | return diag.FromErr(err)
262 | }
263 |
264 | return resourceUserRead(ctx, d, meta)
265 | }
266 |
267 | func resourceUserCreate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
268 | apiClient := meta.(pritunl.Client)
269 |
270 | dnsServers := make([]string, 0)
271 | for _, v := range d.Get("dns_servers").([]interface{}) {
272 | dnsServers = append(dnsServers, v.(string))
273 | }
274 |
275 | macAddresses := make([]string, 0)
276 | for _, v := range d.Get("mac_addresses").([]interface{}) {
277 | macAddresses = append(macAddresses, v.(string))
278 | }
279 |
280 | networkLinks := make([]string, 0)
281 | for _, v := range d.Get("network_links").([]interface{}) {
282 | networkLinks = append(networkLinks, v.(string))
283 | }
284 |
285 | portForwarding := make([]map[string]interface{}, 0)
286 | for _, v := range d.Get("port_forwarding").([]interface{}) {
287 | portForwarding = append(portForwarding, v.(map[string]interface{}))
288 | }
289 |
290 | groups := make([]string, 0)
291 | for _, v := range d.Get("groups").([]interface{}) {
292 | groups = append(groups, v.(string))
293 | }
294 |
295 | userData := pritunl.User{
296 | Name: d.Get("name").(string),
297 | Organization: d.Get("organization_id").(string),
298 | AuthType: d.Get("auth_type").(string),
299 | DnsServers: dnsServers,
300 | DnsSuffix: d.Get("dns_suffix").(string),
301 | Disabled: d.Get("disabled").(bool),
302 | NetworkLinks: networkLinks,
303 | PortForwarding: portForwarding,
304 | Email: d.Get("email").(string),
305 | ClientToClient: d.Get("client_to_client").(bool),
306 | MacAddresses: macAddresses,
307 | BypassSecondary: d.Get("bypass_secondary").(bool),
308 | Groups: groups,
309 | }
310 |
311 | if pin, ok := d.GetOk("pin"); ok {
312 | userData.Pin = &pritunl.Pin{
313 | Secret: pin.(string),
314 | }
315 | }
316 |
317 | user, err := apiClient.CreateUser(userData)
318 | if err != nil {
319 | return diag.FromErr(err)
320 | }
321 |
322 | d.SetId(user.ID)
323 |
324 | return nil
325 | }
326 |
327 | func resourceUserImport(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
328 | apiClient := meta.(pritunl.Client)
329 |
330 | attributes := strings.Split(d.Id(), "-")
331 | if len(attributes) < 2 {
332 | return nil, fmt.Errorf("invalid format: expected ${organizationId}-${userId}, e.g. 60cd0be07723cf3c9114686c-60cd0be17723cf3c91146873, actual id is %s", d.Id())
333 | }
334 |
335 | orgId := attributes[0]
336 | userId := attributes[1]
337 |
338 | d.SetId(userId)
339 | d.Set("organization_id", orgId)
340 |
341 | _, err := apiClient.GetUser(userId, orgId)
342 | if err != nil {
343 | return nil, fmt.Errorf("error on getting user during import: %s", err)
344 | }
345 |
346 | return []*schema.ResourceData{d}, nil
347 | }
348 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
5 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
6 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
7 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
8 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
9 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
10 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
11 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
12 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
13 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
14 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
15 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
16 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
17 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
23 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
24 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
25 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
26 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
27 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
28 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
29 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
30 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
31 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
32 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
33 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
34 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
35 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
36 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
37 | github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
38 | github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
39 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
40 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
41 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
42 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
43 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
44 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
45 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
47 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
48 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
49 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
50 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
51 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
52 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
53 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
54 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
55 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
56 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
57 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
58 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
59 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
60 | github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0=
61 | github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM=
62 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
63 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
64 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
65 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
66 | github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
67 | github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
68 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
69 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
70 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
71 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
72 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
73 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
74 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
75 | github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
76 | github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
77 | github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
78 | github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
79 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
80 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
81 | github.com/hashicorp/terraform-exec v0.23.1 h1:diK5NSSDXDKqHEOIQefBMu9ny+FhzwlwV0xgUTB7VTo=
82 | github.com/hashicorp/terraform-exec v0.23.1/go.mod h1:e4ZEg9BJDRaSalGm2z8vvrPONt0XWG0/tXpmzYTf+dM=
83 | github.com/hashicorp/terraform-json v0.27.1 h1:zWhEracxJW6lcjt/JvximOYyc12pS/gaKSy/wzzE7nY=
84 | github.com/hashicorp/terraform-json v0.27.1/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
85 | github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
86 | github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
87 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
88 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
89 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4=
90 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU=
91 | github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
92 | github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
93 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
94 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
95 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
96 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
97 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
98 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
99 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
100 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
101 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
102 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
103 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
104 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
105 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
106 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
107 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
108 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
109 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
110 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
111 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
112 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
113 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
114 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
115 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
116 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
117 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
118 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
119 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
120 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
121 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
122 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
123 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
124 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
125 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
126 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
127 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
128 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
129 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
130 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
131 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
132 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
133 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
134 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
135 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
136 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
137 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
138 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
139 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
140 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
141 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
142 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
143 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
144 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
145 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
146 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
147 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
148 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
149 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
150 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
151 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
152 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
153 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
154 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
155 | github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
156 | github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
157 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
158 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
159 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
160 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
161 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
162 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
163 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
164 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
165 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
166 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
167 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
168 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
169 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
170 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
171 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
172 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
173 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
174 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
175 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
176 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
177 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
178 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
179 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
180 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
181 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
182 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
183 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
184 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
185 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
186 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
187 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
188 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
189 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
190 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
191 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
192 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
193 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
194 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
195 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
196 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
197 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
198 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
199 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
200 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
201 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
202 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
203 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
204 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
205 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
206 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
207 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
208 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
209 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
210 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
211 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
212 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
213 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
214 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
215 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
216 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
217 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
218 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
219 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
220 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
221 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
222 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
223 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
224 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
225 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
226 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
227 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
228 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
229 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
230 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
231 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
232 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
233 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
234 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
235 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
236 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
237 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
238 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
239 |
--------------------------------------------------------------------------------
/internal/provider/resource_server_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 | "testing"
8 |
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11 | )
12 |
13 | func TestAccPritunlServer(t *testing.T) {
14 |
15 | t.Run("creates a server with default configuration", func(t *testing.T) {
16 | serverName := "tfacc-server1"
17 |
18 | resource.Test(t, resource.TestCase{
19 | PreCheck: func() { preCheck(t) },
20 | ProviderFactories: providerFactories,
21 | CheckDestroy: testPritunlServerDestroy,
22 | Steps: []resource.TestStep{
23 | {
24 | Config: testPritunlServerSimpleConfig(serverName),
25 | Check: resource.ComposeTestCheckFunc(
26 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
27 | ),
28 | },
29 | // import test
30 | importStep("pritunl_server.test"),
31 | },
32 | })
33 | })
34 |
35 | t.Run("creates a server with sso_auth attribute", func(t *testing.T) {
36 | serverName := "tfacc-server1"
37 |
38 | testCase := func(t *testing.T, ssoAuth bool) {
39 | resource.Test(t, resource.TestCase{
40 | PreCheck: func() { preCheck(t) },
41 | ProviderFactories: providerFactories,
42 | CheckDestroy: testPritunlServerDestroy,
43 | Steps: []resource.TestStep{
44 | {
45 | Config: testPritunlServerConfigWithSsoAuth(serverName, ssoAuth),
46 | Check: resource.ComposeTestCheckFunc(
47 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
48 | resource.TestCheckResourceAttr("pritunl_server.test", "sso_auth", strconv.FormatBool(ssoAuth)),
49 | ),
50 | },
51 | // import test
52 | importStep("pritunl_server.test"),
53 | },
54 | })
55 | }
56 |
57 | t.Run("with enabled option", func(t *testing.T) {
58 | testCase(t, true)
59 | })
60 |
61 | t.Run("with disabled option", func(t *testing.T) {
62 | testCase(t, false)
63 | })
64 |
65 | t.Run("without an option", func(t *testing.T) {
66 | resource.Test(t, resource.TestCase{
67 | PreCheck: func() { preCheck(t) },
68 | ProviderFactories: providerFactories,
69 | CheckDestroy: testPritunlServerDestroy,
70 | Steps: []resource.TestStep{
71 | {
72 | Config: testPritunlServerSimpleConfig(serverName),
73 | Check: resource.ComposeTestCheckFunc(
74 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
75 | resource.TestCheckResourceAttr("pritunl_server.test", "sso_auth", "false"),
76 | ),
77 | },
78 | // import test
79 | importStep("pritunl_server.test"),
80 | },
81 | })
82 | })
83 | })
84 |
85 | t.Run("creates a server with device_auth attribute", func(t *testing.T) {
86 | serverName := "tfacc-server1"
87 |
88 | testCase := func(t *testing.T, deviceAuth bool) {
89 | resource.Test(t, resource.TestCase{
90 | PreCheck: func() { preCheck(t) },
91 | ProviderFactories: providerFactories,
92 | CheckDestroy: testPritunlServerDestroy,
93 | Steps: []resource.TestStep{
94 | {
95 | Config: testPritunlServerConfigWithDeviceAuth(serverName, deviceAuth),
96 | Check: resource.ComposeTestCheckFunc(
97 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
98 | resource.TestCheckResourceAttr("pritunl_server.test", "device_auth", strconv.FormatBool(deviceAuth)),
99 | ),
100 | },
101 | // import test
102 | importStep("pritunl_server.test"),
103 | },
104 | })
105 | }
106 |
107 | t.Run("with enabled option", func(t *testing.T) {
108 | testCase(t, true)
109 | })
110 |
111 | t.Run("with disabled option", func(t *testing.T) {
112 | testCase(t, false)
113 | })
114 |
115 | t.Run("without an option", func(t *testing.T) {
116 | resource.Test(t, resource.TestCase{
117 | PreCheck: func() { preCheck(t) },
118 | ProviderFactories: providerFactories,
119 | CheckDestroy: testPritunlServerDestroy,
120 | Steps: []resource.TestStep{
121 | {
122 | Config: testPritunlServerSimpleConfig(serverName),
123 | Check: resource.ComposeTestCheckFunc(
124 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
125 | resource.TestCheckResourceAttr("pritunl_server.test", "device_auth", "false"),
126 | ),
127 | },
128 | // import test
129 | importStep("pritunl_server.test"),
130 | },
131 | })
132 | })
133 | })
134 |
135 | t.Run("creates a server with dynamic_firewall attribute", func(t *testing.T) {
136 | serverName := "tfacc-server1"
137 |
138 | testCase := func(t *testing.T, dynamicFirewall bool) {
139 | resource.Test(t, resource.TestCase{
140 | PreCheck: func() { preCheck(t) },
141 | ProviderFactories: providerFactories,
142 | CheckDestroy: testPritunlServerDestroy,
143 | Steps: []resource.TestStep{
144 | {
145 | Config: testPritunlServerConfigWithDynamicFirewall(serverName, dynamicFirewall),
146 | Check: resource.ComposeTestCheckFunc(
147 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
148 | resource.TestCheckResourceAttr("pritunl_server.test", "dynamic_firewall", strconv.FormatBool(dynamicFirewall)),
149 | ),
150 | },
151 | // import test
152 | importStep("pritunl_server.test"),
153 | },
154 | })
155 | }
156 |
157 | t.Run("with enabled option", func(t *testing.T) {
158 | testCase(t, true)
159 | })
160 |
161 | t.Run("with disabled option", func(t *testing.T) {
162 | testCase(t, false)
163 | })
164 |
165 | t.Run("without an option", func(t *testing.T) {
166 | resource.Test(t, resource.TestCase{
167 | PreCheck: func() { preCheck(t) },
168 | ProviderFactories: providerFactories,
169 | CheckDestroy: testPritunlServerDestroy,
170 | Steps: []resource.TestStep{
171 | {
172 | Config: testPritunlServerSimpleConfig(serverName),
173 | Check: resource.ComposeTestCheckFunc(
174 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
175 | resource.TestCheckResourceAttr("pritunl_server.test", "dynamic_firewall", "false"),
176 | ),
177 | },
178 | // import test
179 | importStep("pritunl_server.test"),
180 | },
181 | })
182 | })
183 | })
184 |
185 | t.Run("creates a server with an attached organization", func(t *testing.T) {
186 | serverName := "tfacc-server1"
187 | orgName := "tfacc-org1"
188 |
189 | resource.Test(t, resource.TestCase{
190 | PreCheck: func() { preCheck(t) },
191 | ProviderFactories: providerFactories,
192 | CheckDestroy: testPritunlServerDestroy,
193 | Steps: []resource.TestStep{
194 | {
195 | Config: testPritunlServerConfigWithAttachedOrganization(serverName, orgName),
196 | Check: resource.ComposeTestCheckFunc(
197 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
198 | resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
199 |
200 | func(s *terraform.State) error {
201 | attachedOrganizationId := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["organization_ids.0"]
202 | organizationId := s.RootModule().Resources["pritunl_organization.test"].Primary.Attributes["id"]
203 | if attachedOrganizationId != organizationId {
204 | return fmt.Errorf("organization_id is invalid or empty")
205 | }
206 | return nil
207 | },
208 | ),
209 | },
210 | // import test
211 | importStep("pritunl_server.test"),
212 | },
213 | })
214 | })
215 |
216 | t.Run("creates a server with a few attached organizations", func(t *testing.T) {
217 | serverName := "tfacc-server1"
218 | org1Name := "tfacc-org1"
219 | org2Name := "tfacc-org2"
220 |
221 | expectedOrganizationIds := make(map[string]struct{})
222 |
223 | resource.Test(t, resource.TestCase{
224 | PreCheck: func() { preCheck(t) },
225 | ProviderFactories: providerFactories,
226 | CheckDestroy: testPritunlServerDestroy,
227 | Steps: []resource.TestStep{
228 | {
229 | Config: testPritunlServerConfigWithAFewAttachedOrganizations(serverName, org1Name, org2Name),
230 | Check: resource.ComposeTestCheckFunc(
231 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
232 | resource.TestCheckResourceAttr("pritunl_organization.test", "name", org1Name),
233 | resource.TestCheckResourceAttr("pritunl_organization.test2", "name", org2Name),
234 |
235 | func(s *terraform.State) error {
236 | attachedOrganization1Id := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["organization_ids.0"]
237 | attachedOrganization2Id := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["organization_ids.1"]
238 | organization1Id := s.RootModule().Resources["pritunl_organization.test"].Primary.Attributes["id"]
239 | organization2Id := s.RootModule().Resources["pritunl_organization.test2"].Primary.Attributes["id"]
240 | expectedOrganizationIds = map[string]struct{}{
241 | organization1Id: {},
242 | organization2Id: {},
243 | }
244 |
245 | if attachedOrganization1Id == attachedOrganization2Id {
246 | return fmt.Errorf("first and seconds attached organization_id is the same")
247 | }
248 |
249 | if _, ok := expectedOrganizationIds[attachedOrganization1Id]; !ok {
250 | return fmt.Errorf("attached organization_id %s doesn't contain in expected organizations list", attachedOrganization1Id)
251 | }
252 |
253 | if _, ok := expectedOrganizationIds[attachedOrganization2Id]; !ok {
254 | return fmt.Errorf("attached organization_id %s doesn't contain in expected organizations list", attachedOrganization2Id)
255 | }
256 |
257 | return nil
258 | },
259 | ),
260 | },
261 | // import test (custom import that ignores order of organization IDs)
262 | {
263 | ResourceName: "pritunl_server.test",
264 | ImportStateCheck: func(states []*terraform.InstanceState) error {
265 | importedOrganization1Id := states[0].Attributes["organization_ids.0"]
266 | importedOrganization2Id := states[0].Attributes["organization_ids.1"]
267 |
268 | if _, ok := expectedOrganizationIds[importedOrganization1Id]; !ok {
269 | return fmt.Errorf("imported organization_id %s doesn't contain in expected organizations list", importedOrganization1Id)
270 | }
271 |
272 | if _, ok := expectedOrganizationIds[importedOrganization2Id]; !ok {
273 | return fmt.Errorf("imported organization_id %s doesn't contain in expected organizations list", importedOrganization2Id)
274 | }
275 |
276 | return nil
277 | },
278 | ImportState: true,
279 | ImportStateVerify: false,
280 | },
281 | },
282 | })
283 | })
284 |
285 | t.Run("creates a server with routes", func(t *testing.T) {
286 | t.Run("with one attached route", func(t *testing.T) {
287 | serverName := "tfacc-server1"
288 | routeNetwork := "10.5.0.0/24"
289 | routeComment := "tfacc-route"
290 |
291 | resource.Test(t, resource.TestCase{
292 | PreCheck: func() { preCheck(t) },
293 | ProviderFactories: providerFactories,
294 | CheckDestroy: testPritunlServerDestroy,
295 | Steps: []resource.TestStep{
296 | {
297 | Config: testPritunlServerConfigWithAttachedRoute(serverName, routeNetwork),
298 | Check: resource.ComposeTestCheckFunc(
299 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
300 |
301 | func(s *terraform.State) error {
302 | actualRouteNetwork := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.0.network"]
303 | actualRouteComment := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.0.comment"]
304 | if actualRouteNetwork != routeNetwork {
305 | return fmt.Errorf("route network is invalid: expected is %s, but actual is %s", routeNetwork, actualRouteNetwork)
306 | }
307 | if actualRouteComment != routeComment {
308 | return fmt.Errorf("route comment is invalid: expected is %s, but actual is %s", routeComment, actualRouteComment)
309 | }
310 | return nil
311 | },
312 | ),
313 | },
314 | // import test
315 | importStep("pritunl_server.test"),
316 | },
317 | })
318 | })
319 |
320 | t.Run("with a few attached routes", func(t *testing.T) {
321 | serverName := "tfacc-server1"
322 | route1Network := "10.2.0.0/24"
323 | route2Network := "10.3.0.0/24"
324 | route3Network := "10.4.0.0/32"
325 | routeComment := "tfacc-route"
326 |
327 | resource.Test(t, resource.TestCase{
328 | PreCheck: func() { preCheck(t) },
329 | ProviderFactories: providerFactories,
330 | CheckDestroy: testPritunlServerDestroy,
331 | Steps: []resource.TestStep{
332 | {
333 | Config: testPritunlServerConfigWithAFewAttachedRoutes(serverName, route1Network, route2Network, route3Network),
334 | Check: resource.ComposeTestCheckFunc(
335 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
336 |
337 | func(s *terraform.State) error {
338 | actualRoute1Network := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.0.network"]
339 | actualRoute2Network := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.1.network"]
340 | actualRoute3Network := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.2.network"]
341 | actualRoute1Comment := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.0.comment"]
342 | actualRoute2Comment := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.1.comment"]
343 | actualRoute3Comment := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["route.2.comment"]
344 | if actualRoute1Network != route1Network {
345 | return fmt.Errorf("first route network is invalid: expected is %s, but actual is %s", route1Network, actualRoute1Network)
346 | }
347 | if actualRoute2Network != route2Network {
348 | return fmt.Errorf("second route network is invalid: expected is %s, but actual is %s", route2Network, actualRoute2Network)
349 | }
350 | if actualRoute3Network != route3Network {
351 | return fmt.Errorf("second route network is invalid: expected is %s, but actual is %s", route3Network, actualRoute3Network)
352 | }
353 | if actualRoute1Comment != routeComment {
354 | return fmt.Errorf("first route comment is invalid: expected is %s, but actual is %s", routeComment, actualRoute1Comment)
355 | }
356 | if actualRoute2Comment != routeComment {
357 | return fmt.Errorf("second route comment is invalid: expected is %s, but actual is %s", routeComment, actualRoute2Comment)
358 | }
359 | if actualRoute3Comment != routeComment {
360 | return fmt.Errorf(" route comment is invalid: expected is %s, but actual is %s", routeComment, actualRoute3Comment)
361 | }
362 | return nil
363 | },
364 | ),
365 | },
366 | // import test
367 | importStep("pritunl_server.test"),
368 | },
369 | })
370 | })
371 | })
372 |
373 | t.Run("creates a server with error", func(t *testing.T) {
374 | t.Run("due to an invalid network", func(t *testing.T) {
375 | serverName := "tfacc-server1"
376 | port := 11111
377 | missedSubnetNetwork := "10.100.0.2"
378 | invalidNetwork := "10.100.0"
379 |
380 | resource.Test(t, resource.TestCase{
381 | PreCheck: func() { preCheck(t) },
382 | ProviderFactories: providerFactories,
383 | CheckDestroy: testPritunlServerDestroy,
384 | Steps: []resource.TestStep{
385 | {
386 | Config: testGetServerConfigWithNetworkAndPort(serverName, missedSubnetNetwork, port),
387 | ExpectError: regexp.MustCompile(fmt.Sprintf("invalid CIDR address: %s", missedSubnetNetwork)),
388 | },
389 | {
390 | Config: testGetServerConfigWithNetworkAndPort(serverName, invalidNetwork, port),
391 | ExpectError: regexp.MustCompile(fmt.Sprintf("invalid CIDR address: %s", invalidNetwork)),
392 | },
393 | },
394 | })
395 | })
396 |
397 | t.Run("due to an unsupported network", func(t *testing.T) {
398 | serverName := "tfacc-server1"
399 | port := 11111
400 | unsupportedNetwork := "172.14.68.0/24"
401 | supportedNetwork := "172.16.68.0/24"
402 |
403 | resource.Test(t, resource.TestCase{
404 | PreCheck: func() { preCheck(t) },
405 | ProviderFactories: providerFactories,
406 | CheckDestroy: testPritunlServerDestroy,
407 | Steps: []resource.TestStep{
408 | {
409 | Config: testGetServerConfigWithNetworkAndPort(serverName, unsupportedNetwork, port),
410 | ExpectError: regexp.MustCompile(fmt.Sprintf("provided subnet %s does not belong to expected subnets 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16", unsupportedNetwork)),
411 | },
412 | {
413 | Config: testGetServerConfigWithNetworkAndPort(serverName, supportedNetwork, port),
414 | Check: resource.ComposeTestCheckFunc(
415 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
416 | resource.TestCheckResourceAttr("pritunl_server.test", "network", supportedNetwork),
417 | ),
418 | },
419 | },
420 | })
421 | })
422 |
423 | t.Run("due to an invalid route", func(t *testing.T) {
424 | serverName := "tfacc-server1"
425 | invalidRouteNetwork := "10.100.0.2"
426 |
427 | resource.Test(t, resource.TestCase{
428 | PreCheck: func() { preCheck(t) },
429 | ProviderFactories: providerFactories,
430 | CheckDestroy: testPritunlServerDestroy,
431 | Steps: []resource.TestStep{
432 | {
433 | Config: testPritunlServerConfigWithAttachedRoute(serverName, invalidRouteNetwork),
434 | ExpectError: regexp.MustCompile(fmt.Sprintf("invalid CIDR address: %s", invalidRouteNetwork)),
435 | },
436 | },
437 | })
438 | })
439 |
440 | t.Run("due to an invalid bind_address", func(t *testing.T) {
441 | serverName := "tfacc-server1"
442 | network := "172.16.68.0/24"
443 | port := 11111
444 | invalidBindAddress := "10.100.0.1/24"
445 | correctBindAddress := "10.100.0.1"
446 |
447 | resource.Test(t, resource.TestCase{
448 | PreCheck: func() { preCheck(t) },
449 | ProviderFactories: providerFactories,
450 | CheckDestroy: testPritunlServerDestroy,
451 | Steps: []resource.TestStep{
452 | {
453 | Config: testGetServerConfigWithBindAddress(serverName, network, invalidBindAddress, port),
454 | ExpectError: regexp.MustCompile(fmt.Sprintf("expected bind_address to contain a valid IP, got: %s", invalidBindAddress)),
455 | },
456 | {
457 | Config: testGetServerConfigWithBindAddress(serverName, network, correctBindAddress, port),
458 | Check: resource.ComposeTestCheckFunc(
459 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
460 | resource.TestCheckResourceAttr("pritunl_server.test", "bind_address", correctBindAddress),
461 | ),
462 | },
463 | },
464 | })
465 | })
466 | })
467 |
468 | t.Run("creates a server with groups attribute", func(t *testing.T) {
469 | serverName := "tfacc-server1"
470 |
471 | t.Run("with correct group name", func(t *testing.T) {
472 | correctGroupName := "Group-Has-No-Spaces"
473 | resource.Test(t, resource.TestCase{
474 | PreCheck: func() { preCheck(t) },
475 | ProviderFactories: providerFactories,
476 | CheckDestroy: testPritunlServerDestroy,
477 | Steps: []resource.TestStep{
478 | {
479 | Config: testPritunlServerConfigWithGroups(serverName, correctGroupName),
480 | Check: resource.ComposeTestCheckFunc(
481 | resource.TestCheckResourceAttr("pritunl_server.test", "name", serverName),
482 |
483 | func(s *terraform.State) error {
484 | groupName := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["groups.0"]
485 | if groupName != correctGroupName {
486 | return fmt.Errorf("group name mismatch")
487 | }
488 |
489 | return nil
490 | },
491 | ),
492 | },
493 | // import test
494 | importStep("pritunl_server.test"),
495 | },
496 | })
497 | })
498 |
499 | t.Run("with invalid group name", func(t *testing.T) {
500 | invalidGroupName := "Group Name With Spaces"
501 | resource.Test(t, resource.TestCase{
502 | PreCheck: func() { preCheck(t) },
503 | ProviderFactories: providerFactories,
504 | CheckDestroy: testPritunlServerDestroy,
505 | Steps: []resource.TestStep{
506 | {
507 | Config: testPritunlServerConfigWithGroups(serverName, invalidGroupName),
508 | ExpectError: regexp.MustCompile(fmt.Sprintf("%s contains spaces", invalidGroupName)),
509 | },
510 | },
511 | })
512 | })
513 | })
514 | }
515 |
516 | func testPritunlServerSimpleConfig(name string) string {
517 | return fmt.Sprintf(`
518 | resource "pritunl_server" "test" {
519 | name = "%[1]s"
520 | }
521 | `, name)
522 | }
523 |
524 | func testPritunlServerConfigWithSsoAuth(name string, ssoAuth bool) string {
525 | return fmt.Sprintf(`
526 | resource "pritunl_server" "test" {
527 | name = "%[1]s"
528 | sso_auth = %[2]v
529 | }
530 | `, name, ssoAuth)
531 | }
532 |
533 | func testPritunlServerConfigWithDeviceAuth(name string, deviceAuth bool) string {
534 | return fmt.Sprintf(`
535 | resource "pritunl_server" "test" {
536 | name = "%[1]s"
537 | device_auth = %[2]v
538 | }
539 | `, name, deviceAuth)
540 | }
541 |
542 | func testPritunlServerConfigWithDynamicFirewall(name string, dynamicFirewall bool) string {
543 | return fmt.Sprintf(`
544 | resource "pritunl_server" "test" {
545 | name = "%[1]s"
546 | dynamic_firewall = %[2]v
547 | }
548 | `, name, dynamicFirewall)
549 | }
550 |
551 | func testPritunlServerConfigWithAttachedOrganization(name, organizationName string) string {
552 | return fmt.Sprintf(`
553 | resource "pritunl_organization" "test" {
554 | name = "%[2]s"
555 | }
556 |
557 | resource "pritunl_server" "test" {
558 | name = "%[1]s"
559 | organization_ids = [
560 | pritunl_organization.test.id
561 | ]
562 | }
563 | `, name, organizationName)
564 | }
565 |
566 | func testPritunlServerConfigWithAFewAttachedOrganizations(name, organization1Name, organization2Name string) string {
567 | return fmt.Sprintf(`
568 | resource "pritunl_organization" "test" {
569 | name = "%[2]s"
570 | }
571 |
572 | resource "pritunl_organization" "test2" {
573 | name = "%[3]s"
574 | }
575 |
576 | resource "pritunl_server" "test" {
577 | name = "%[1]s"
578 | organization_ids = [
579 | pritunl_organization.test.id,
580 | pritunl_organization.test2.id
581 | ]
582 | }
583 | `, name, organization1Name, organization2Name)
584 | }
585 |
586 | func testPritunlServerConfigWithAttachedRoute(name, route string) string {
587 | return fmt.Sprintf(`
588 | resource "pritunl_server" "test" {
589 | name = "%[1]s"
590 |
591 | route {
592 | network = "%[2]s"
593 | comment = "tfacc-route"
594 | }
595 | }
596 | `, name, route)
597 | }
598 |
599 | func testPritunlServerConfigWithAFewAttachedRoutes(name, route1, route2, route3 string) string {
600 | return fmt.Sprintf(`
601 | resource "pritunl_server" "test" {
602 | name = "%[1]s"
603 |
604 | route {
605 | network = "%[2]s"
606 | comment = "tfacc-route"
607 | }
608 |
609 | route {
610 | network = "%[3]s"
611 | comment = "tfacc-route"
612 | }
613 |
614 | route {
615 | network = "%[4]s"
616 | comment = "tfacc-route"
617 | net_gateway = true
618 | }
619 | }
620 | `, name, route1, route2, route3)
621 | }
622 |
623 | func testGetServerConfigWithNetworkAndPort(name, network string, port int) string {
624 | return fmt.Sprintf(`
625 | resource "pritunl_server" "test" {
626 | name = "%[1]s"
627 | network = "%[2]s"
628 | port = %[3]d
629 | protocol = "tcp"
630 | }
631 | `, name, network, port)
632 | }
633 |
634 | func testGetServerConfigWithBindAddress(name, network, bindAddress string, port int) string {
635 | return fmt.Sprintf(`
636 | resource "pritunl_server" "test" {
637 | name = "%[1]s"
638 | network = "%[2]s"
639 | bind_address = "%[3]s"
640 | port = %[4]d
641 | protocol = "tcp"
642 | }
643 | `, name, network, bindAddress, port)
644 | }
645 |
646 | func testPritunlServerConfigWithGroups(name string, groupName string) string {
647 | return fmt.Sprintf(`
648 | resource "pritunl_server" "test" {
649 | name = "%[1]s"
650 | groups = ["%[2]s"]
651 | }
652 | `, name, groupName)
653 | }
654 |
655 | func testPritunlServerDestroy(s *terraform.State) error {
656 | serverId := s.RootModule().Resources["pritunl_server.test"].Primary.Attributes["id"]
657 |
658 | servers, err := testClient.GetServers()
659 | if err != nil {
660 | return err
661 | }
662 | for _, server := range servers {
663 | if server.ID == serverId {
664 | return fmt.Errorf("a server is not destroyed")
665 | }
666 | }
667 | return nil
668 | }
669 |
--------------------------------------------------------------------------------
/internal/pritunl/client.go:
--------------------------------------------------------------------------------
1 | package pritunl
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | )
11 |
12 | type Client interface {
13 | TestApiCall() error
14 |
15 | GetOrganizations() ([]Organization, error)
16 | GetOrganization(id string) (*Organization, error)
17 | CreateOrganization(name string) (*Organization, error)
18 | UpdateOrganization(id string, organization *Organization) error
19 | DeleteOrganization(name string) error
20 |
21 | GetUser(id string, orgId string) (*User, error)
22 | CreateUser(newUser User) (*User, error)
23 | UpdateUser(id string, user *User) error
24 | DeleteUser(id string, orgId string) error
25 |
26 | GetServers() ([]Server, error)
27 | GetServer(id string) (*Server, error)
28 | CreateServer(serverData map[string]interface{}) (*Server, error)
29 | UpdateServer(id string, server *Server) error
30 | DeleteServer(id string) error
31 |
32 | GetOrganizationsByServer(serverId string) ([]Organization, error)
33 | AttachOrganizationToServer(organizationId, serverId string) error
34 | DetachOrganizationFromServer(organizationId, serverId string) error
35 |
36 | GetRoutesByServer(serverId string) ([]Route, error)
37 | AddRouteToServer(serverId string, route Route) error
38 | AddRoutesToServer(serverId string, route []Route) error
39 | DeleteRouteFromServer(serverId string, route Route) error
40 | UpdateRouteOnServer(serverId string, route Route) error
41 |
42 | GetHosts() ([]Host, error)
43 | GetHostsByServer(serverId string) ([]Host, error)
44 | AttachHostToServer(hostId, serverId string) error
45 | DetachHostFromServer(hostId, serverId string) error
46 |
47 | StartServer(serverId string) error
48 | StopServer(serverId string) error
49 | }
50 |
51 | type client struct {
52 | httpClient *http.Client
53 | baseUrl string
54 | }
55 |
56 | func (c client) TestApiCall() error {
57 | url := fmt.Sprintf("/state")
58 | req, err := http.NewRequest("GET", url, nil)
59 |
60 | resp, err := c.httpClient.Do(req)
61 | if err != nil {
62 | return fmt.Errorf("TestApiCall: Error on HTTP request: %s", err)
63 | }
64 | defer resp.Body.Close()
65 | body, _ := io.ReadAll(resp.Body)
66 |
67 | if resp.StatusCode != 200 {
68 | return fmt.Errorf("Non-200 response on the tests api call\ncode=%d\nbody=%s\n", resp.StatusCode, body)
69 | }
70 |
71 | // 401 - invalid credentials
72 | if resp.StatusCode == 401 {
73 | return fmt.Errorf("unauthorized: Invalid token or secret")
74 | }
75 |
76 | return nil
77 | }
78 |
79 | func (c client) GetOrganization(id string) (*Organization, error) {
80 | url := fmt.Sprintf("/organization/%s", id)
81 | req, err := http.NewRequest("GET", url, nil)
82 |
83 | resp, err := c.httpClient.Do(req)
84 | if err != nil {
85 | return nil, fmt.Errorf("GetOrganization: Error on HTTP request: %s", err)
86 | }
87 | defer resp.Body.Close()
88 |
89 | body, _ := io.ReadAll(resp.Body)
90 | if resp.StatusCode != 200 {
91 | return nil, fmt.Errorf("Non-200 response on getting the organization\nbody=%s", body)
92 | }
93 |
94 | var organization Organization
95 |
96 | err = json.Unmarshal(body, &organization)
97 | if err != nil {
98 | return nil, fmt.Errorf("GetOrganization: %s: %+v, id=%s, body=%s", err, organization, id, body)
99 | }
100 |
101 | return &organization, nil
102 | }
103 |
104 | func (c client) GetOrganizations() ([]Organization, error) {
105 | url := fmt.Sprintf("/organization")
106 | req, err := http.NewRequest("GET", url, nil)
107 |
108 | resp, err := c.httpClient.Do(req)
109 | if err != nil {
110 | return nil, fmt.Errorf("GetOrganization: Error on HTTP request: %s", err)
111 | }
112 | defer resp.Body.Close()
113 |
114 | body, _ := io.ReadAll(resp.Body)
115 | if resp.StatusCode != 200 {
116 | return nil, fmt.Errorf("Non-200 response on getting the organization\nbody=%s", body)
117 | }
118 |
119 | var organizations []Organization
120 |
121 | err = json.Unmarshal(body, &organizations)
122 | if err != nil {
123 | return nil, fmt.Errorf("GetOrganization: %s: %+v, body=%s", err, organizations, body)
124 | }
125 |
126 | return organizations, nil
127 | }
128 |
129 | func (c client) CreateOrganization(name string) (*Organization, error) {
130 | var jsonStr = []byte(`{"name": "` + name + `"}`)
131 |
132 | url := "/organization"
133 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
134 |
135 | resp, err := c.httpClient.Do(req)
136 | if err != nil {
137 | return nil, fmt.Errorf("CreateOrganization: Error on HTTP request: %s", err)
138 | }
139 | defer resp.Body.Close()
140 |
141 | body, _ := io.ReadAll(resp.Body)
142 | if resp.StatusCode != 200 {
143 | return nil, fmt.Errorf("Non-200 response on creating the organization\nbody=%s", body)
144 | }
145 |
146 | var organization Organization
147 | err = json.Unmarshal(body, &organization)
148 | if err != nil {
149 | return nil, fmt.Errorf("CreateOrganization: %s: %+v, name=%s, body=%s", err, organization, name, body)
150 | }
151 |
152 | return &organization, nil
153 | }
154 |
155 | func (c client) UpdateOrganization(id string, organization *Organization) error {
156 | jsonData, err := json.Marshal(organization)
157 | if err != nil {
158 | return fmt.Errorf("UpdateOrganization: Error on marshalling data: %s", err)
159 | }
160 |
161 | url := fmt.Sprintf("/organization/%s", id)
162 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
163 |
164 | resp, err := c.httpClient.Do(req)
165 | if err != nil {
166 | return fmt.Errorf("UpdateOrganization: Error on HTTP request: %s", err)
167 | }
168 | defer resp.Body.Close()
169 |
170 | body, _ := io.ReadAll(resp.Body)
171 | if resp.StatusCode != 200 {
172 | return fmt.Errorf("Non-200 response on updating the organization\nbody=%s", body)
173 | }
174 |
175 | return nil
176 | }
177 |
178 | func (c client) DeleteOrganization(id string) error {
179 | url := fmt.Sprintf("/organization/%s", id)
180 | req, err := http.NewRequest("DELETE", url, nil)
181 |
182 | resp, err := c.httpClient.Do(req)
183 | if err != nil {
184 | return fmt.Errorf("DeleteOrganization: Error on HTTP request: %s", err)
185 | }
186 | defer resp.Body.Close()
187 |
188 | body, _ := io.ReadAll(resp.Body)
189 | if resp.StatusCode != 200 {
190 | return fmt.Errorf("Non-200 response on deleting the organization\nbody=%s", body)
191 | }
192 |
193 | return nil
194 | }
195 |
196 | func (c client) GetServer(id string) (*Server, error) {
197 | url := fmt.Sprintf("/server/%s", id)
198 | req, err := http.NewRequest("GET", url, nil)
199 |
200 | resp, err := c.httpClient.Do(req)
201 | if err != nil {
202 | return nil, fmt.Errorf("GetServer: Error on HTTP request: %s", err)
203 | }
204 | defer resp.Body.Close()
205 |
206 | body, _ := io.ReadAll(resp.Body)
207 | if resp.StatusCode != 200 {
208 | return nil, fmt.Errorf("Non-200 response on getting the server\nbody=%s", body)
209 | }
210 |
211 | var server Server
212 | err = json.Unmarshal(body, &server)
213 |
214 | if err != nil {
215 | return nil, fmt.Errorf("GetServer: %s: %+v, id=%s, body=%s", err, server, id, body)
216 | }
217 |
218 | return &server, nil
219 | }
220 |
221 | func (c client) GetServers() ([]Server, error) {
222 | url := fmt.Sprintf("/server")
223 | req, err := http.NewRequest("GET", url, nil)
224 |
225 | resp, err := c.httpClient.Do(req)
226 | if err != nil {
227 | return nil, fmt.Errorf("GetServers: Error on HTTP request: %s", err)
228 | }
229 | defer resp.Body.Close()
230 |
231 | body, _ := io.ReadAll(resp.Body)
232 | if resp.StatusCode != 200 {
233 | return nil, fmt.Errorf("Non-200 response on getting servers\nbody=%s", body)
234 | }
235 |
236 | var servers []Server
237 | err = json.Unmarshal(body, &servers)
238 |
239 | if err != nil {
240 | return nil, fmt.Errorf("GetServers: %s: %+v, body=%s", err, servers, body)
241 | }
242 |
243 | return servers, nil
244 | }
245 |
246 | func (c client) CreateServer(serverData map[string]interface{}) (*Server, error) {
247 | serverStruct := Server{}
248 |
249 | if v, ok := serverData["name"]; ok {
250 | serverStruct.Name = v.(string)
251 | }
252 | if v, ok := serverData["protocol"]; ok {
253 | serverStruct.Protocol = v.(string)
254 | }
255 | if v, ok := serverData["cipher"]; ok {
256 | serverStruct.Cipher = v.(string)
257 | }
258 | if v, ok := serverData["network"]; ok {
259 | serverStruct.Network = v.(string)
260 | }
261 | if v, ok := serverData["hash"]; ok {
262 | serverStruct.Hash = v.(string)
263 | }
264 | if v, ok := serverData["port"]; ok {
265 | serverStruct.Port = v.(int)
266 | }
267 | if v, ok := serverData["bind_address"]; ok {
268 | serverStruct.BindAddress = v.(string)
269 | }
270 | if v, ok := serverData["groups"]; ok {
271 | groups := make([]string, 0)
272 | for _, group := range v.([]interface{}) {
273 | groups = append(groups, group.(string))
274 | }
275 | serverStruct.Groups = groups
276 | }
277 | if v, ok := serverData["dns_servers"]; ok {
278 | dnsServers := make([]string, 0)
279 | for _, dns := range v.([]interface{}) {
280 | dnsServers = append(dnsServers, dns.(string))
281 | }
282 | serverStruct.DnsServers = dnsServers
283 | }
284 | if v, ok := serverData["network_wg"]; ok {
285 | serverStruct.NetworkWG = v.(string)
286 | }
287 | if v, ok := serverData["port_wg"]; ok {
288 | serverStruct.PortWG = v.(int)
289 | }
290 |
291 | isWgEnabled := serverStruct.NetworkWG != "" && serverStruct.PortWG > 0
292 | serverStruct.WG = isWgEnabled
293 |
294 | if v, ok := serverData["sso_auth"]; ok {
295 | serverStruct.SsoAuth = v.(bool)
296 | }
297 |
298 | if v, ok := serverData["otp_auth"]; ok {
299 | serverStruct.OtpAuth = v.(bool)
300 | }
301 |
302 | if v, ok := serverData["device_auth"]; ok {
303 | serverStruct.DeviceAuth = v.(bool)
304 | }
305 |
306 | if v, ok := serverData["dynamic_firewall"]; ok {
307 | serverStruct.DynamicFirewall = v.(bool)
308 | }
309 |
310 | if v, ok := serverData["ipv6"]; ok {
311 | serverStruct.IPv6 = v.(bool)
312 | }
313 |
314 | if v, ok := serverData["dh_param_bits"]; ok {
315 | serverStruct.DhParamBits = v.(int)
316 | }
317 |
318 | if v, ok := serverData["ping_interval"]; ok {
319 | serverStruct.PingInterval = v.(int)
320 | }
321 |
322 | if v, ok := serverData["ping_timeout"]; ok {
323 | serverStruct.PingTimeout = v.(int)
324 | }
325 |
326 | if v, ok := serverData["link_ping_interval"]; ok {
327 | serverStruct.LinkPingInterval = v.(int)
328 | }
329 |
330 | if v, ok := serverData["link_ping_timeout"]; ok {
331 | serverStruct.LinkPingTimeout = v.(int)
332 | }
333 |
334 | if v, ok := serverData["session_timeout"]; ok {
335 | serverStruct.SessionTimeout = v.(int)
336 | }
337 |
338 | if v, ok := serverData["inactive_timeout"]; ok {
339 | serverStruct.InactiveTimeout = v.(int)
340 | }
341 |
342 | if v, ok := serverData["max_clients"]; ok {
343 | serverStruct.MaxClients = v.(int)
344 | }
345 |
346 | if v, ok := serverData["network_mode"]; ok {
347 | serverStruct.NetworkMode = v.(string)
348 | }
349 |
350 | if v, ok := serverData["network_start"]; ok {
351 | serverStruct.NetworkStart = v.(string)
352 | }
353 |
354 | if v, ok := serverData["network_end"]; ok {
355 | serverStruct.NetworkEnd = v.(string)
356 | }
357 |
358 | if serverStruct.NetworkMode == ServerNetworkModeBridge && (serverStruct.NetworkStart == "" || serverStruct.NetworkEnd == "") {
359 | return nil, fmt.Errorf("the attribute network_mode = %s requires network_start and network_end attributes", ServerNetworkModeBridge)
360 | }
361 |
362 | if v, ok := serverData["mss_fix"]; ok {
363 | serverStruct.MssFix = v.(int)
364 | }
365 |
366 | if v, ok := serverData["max_devices"]; ok {
367 | serverStruct.MaxDevices = v.(int)
368 | }
369 |
370 | if v, ok := serverData["pre_connect_msg"]; ok {
371 | serverStruct.PreConnectMsg = v.(string)
372 | }
373 |
374 | if v, ok := serverData["allowed_devices"]; ok {
375 | serverStruct.AllowedDevices = v.(string)
376 | }
377 |
378 | if v, ok := serverData["search_domain"]; ok {
379 | serverStruct.SearchDomain = v.(string)
380 | }
381 |
382 | if v, ok := serverData["replica_count"]; ok {
383 | serverStruct.ReplicaCount = v.(int)
384 | }
385 |
386 | if v, ok := serverData["multi_device"]; ok {
387 | serverStruct.MultiDevice = v.(bool)
388 | }
389 |
390 | if v, ok := serverData["debug"]; ok {
391 | serverStruct.Debug = v.(bool)
392 | }
393 |
394 | if v, ok := serverData["restrict_routes"]; ok {
395 | serverStruct.RestrictRoutes = v.(bool)
396 | }
397 |
398 | if v, ok := serverData["block_outside_dns"]; ok {
399 | serverStruct.BlockOutsideDns = v.(bool)
400 | }
401 |
402 | if v, ok := serverData["dns_mapping"]; ok {
403 | serverStruct.DnsMapping = v.(bool)
404 | }
405 |
406 | if v, ok := serverData["inter_client"]; ok {
407 | serverStruct.InterClient = v.(bool)
408 | }
409 |
410 | if v, ok := serverData["vxlan"]; ok {
411 | serverStruct.VxLan = v.(bool)
412 | }
413 |
414 | jsonData, err := serverStruct.MarshalJSON()
415 |
416 | url := "/server"
417 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
418 |
419 | resp, err := c.httpClient.Do(req)
420 | if err != nil {
421 | return nil, fmt.Errorf("CreateServer: Error on HTTP request: %s", err)
422 | }
423 | defer resp.Body.Close()
424 |
425 | body, _ := io.ReadAll(resp.Body)
426 | if resp.StatusCode != 200 {
427 | return nil, fmt.Errorf("Non-200 response on creating the server\ncode=%d\nbody=%s", resp.StatusCode, body)
428 | }
429 |
430 | var server Server
431 | err = json.Unmarshal(body, &server)
432 | if err != nil {
433 | return nil, fmt.Errorf("CreateServer: Error on unmarshalling http response: %s", err)
434 | }
435 |
436 | return &server, nil
437 | }
438 |
439 | func (c client) UpdateServer(id string, server *Server) error {
440 | jsonData, err := server.MarshalJSON()
441 | if err != nil {
442 | return fmt.Errorf("UpdateServer: Error on marshalling data: %s", err)
443 | }
444 |
445 | url := fmt.Sprintf("/server/%s", id)
446 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
447 |
448 | resp, err := c.httpClient.Do(req)
449 | if err != nil {
450 | return fmt.Errorf("UpdateServer: Error on HTTP request: %s", err)
451 | }
452 | defer resp.Body.Close()
453 |
454 | body, _ := io.ReadAll(resp.Body)
455 | if resp.StatusCode != 200 {
456 | return fmt.Errorf("Non-200 response on updating the server\nbody=%s", body)
457 | }
458 |
459 | return nil
460 | }
461 |
462 | func (c client) DeleteServer(id string) error {
463 | url := fmt.Sprintf("/server/%s", id)
464 | req, err := http.NewRequest("DELETE", url, nil)
465 |
466 | resp, err := c.httpClient.Do(req)
467 | if err != nil {
468 | return fmt.Errorf("DeleteServer: Error on HTTP request: %s", err)
469 | }
470 | defer resp.Body.Close()
471 |
472 | body, _ := io.ReadAll(resp.Body)
473 | if resp.StatusCode != 200 {
474 | return fmt.Errorf("Non-200 response on deleting the server\nbody=%s", body)
475 | }
476 |
477 | return nil
478 | }
479 |
480 | func (c client) GetOrganizationsByServer(serverId string) ([]Organization, error) {
481 | url := fmt.Sprintf("/server/%s/organization", serverId)
482 | req, err := http.NewRequest("GET", url, nil)
483 |
484 | resp, err := c.httpClient.Do(req)
485 | if err != nil {
486 | return nil, fmt.Errorf("GeteOrganizationsByServer: Error on HTTP request: %s", err)
487 | }
488 | defer resp.Body.Close()
489 |
490 | body, _ := io.ReadAll(resp.Body)
491 | if resp.StatusCode != 200 {
492 | return nil, fmt.Errorf("Non-200 response on getting organizations on the server\nbody=%s", body)
493 | }
494 |
495 | var organizations []Organization
496 | json.Unmarshal(body, &organizations)
497 |
498 | return organizations, nil
499 | }
500 |
501 | func (c client) AttachOrganizationToServer(organizationId, serverId string) error {
502 | url := fmt.Sprintf("/server/%s/organization/%s", serverId, organizationId)
503 | req, err := http.NewRequest("PUT", url, nil)
504 |
505 | resp, err := c.httpClient.Do(req)
506 | if err != nil {
507 | return fmt.Errorf("AttachOrganizationToServer: Error on HTTP request: %s", err)
508 | }
509 | defer resp.Body.Close()
510 |
511 | body, _ := io.ReadAll(resp.Body)
512 | if resp.StatusCode != 200 {
513 | return fmt.Errorf("Non-200 response on attaching an organization the server\nbody=%s", body)
514 | }
515 |
516 | return nil
517 | }
518 |
519 | func (c client) DetachOrganizationFromServer(organizationId, serverId string) error {
520 | url := fmt.Sprintf("/server/%s/organization/%s", serverId, organizationId)
521 | req, err := http.NewRequest("DELETE", url, nil)
522 |
523 | resp, err := c.httpClient.Do(req)
524 | if err != nil {
525 | return fmt.Errorf("DetachOrganizationFromServer: Error on HTTP request: %s", err)
526 | }
527 | defer resp.Body.Close()
528 |
529 | body, _ := io.ReadAll(resp.Body)
530 | if resp.StatusCode != 200 {
531 | return fmt.Errorf("Non-200 response on detaching the organization from the server\nbody=%s", body)
532 | }
533 |
534 | return nil
535 | }
536 |
537 | func (c client) StartServer(serverId string) error {
538 | url := fmt.Sprintf("/server/%s/operation/start", serverId)
539 | req, err := http.NewRequest("PUT", url, nil)
540 |
541 | resp, err := c.httpClient.Do(req)
542 | if err != nil {
543 | return fmt.Errorf("StartServer: Error on HTTP request: %s", err)
544 | }
545 | defer resp.Body.Close()
546 |
547 | body, _ := io.ReadAll(resp.Body)
548 | if resp.StatusCode != 200 {
549 | return fmt.Errorf("Non-200 response on starting the server\nbody=%s", body)
550 | }
551 |
552 | return nil
553 | }
554 |
555 | func (c client) StopServer(serverId string) error {
556 | url := fmt.Sprintf("/server/%s/operation/stop", serverId)
557 | req, err := http.NewRequest("PUT", url, nil)
558 |
559 | resp, err := c.httpClient.Do(req)
560 | if err != nil {
561 | return fmt.Errorf("StopServer: Error on HTTP request: %s", err)
562 | }
563 | defer resp.Body.Close()
564 |
565 | body, _ := io.ReadAll(resp.Body)
566 | if resp.StatusCode != 200 {
567 | return fmt.Errorf("Non-200 response on stopping the server\nbody=%s", body)
568 | }
569 |
570 | return nil
571 | }
572 |
573 | func (c client) GetRoutesByServer(serverId string) ([]Route, error) {
574 | url := fmt.Sprintf("/server/%s/route", serverId)
575 | req, err := http.NewRequest("GET", url, nil)
576 |
577 | resp, err := c.httpClient.Do(req)
578 | if err != nil {
579 | return nil, fmt.Errorf("GetRoutesByServer: Error on HTTP request: %s", err)
580 | }
581 | defer resp.Body.Close()
582 |
583 | body, _ := io.ReadAll(resp.Body)
584 | if resp.StatusCode != 200 {
585 | return nil, fmt.Errorf("Non-200 response on getting routes on the server\nbody=%s", body)
586 | }
587 |
588 | var routes []Route
589 | json.Unmarshal(body, &routes)
590 |
591 | return routes, nil
592 | }
593 |
594 | func (c client) AddRouteToServer(serverId string, route Route) error {
595 | jsonData, err := json.Marshal(route)
596 |
597 | url := fmt.Sprintf("/server/%s/route", serverId)
598 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
599 |
600 | resp, err := c.httpClient.Do(req)
601 | if err != nil {
602 | return fmt.Errorf("AddRouteToServer: Error on HTTP request: %s", err)
603 | }
604 | defer resp.Body.Close()
605 |
606 | body, _ := io.ReadAll(resp.Body)
607 | if resp.StatusCode != 200 {
608 | return fmt.Errorf("Non-200 response on adding a route to the server\nbody=%s", body)
609 | }
610 |
611 | return nil
612 | }
613 |
614 | func (c client) AddRoutesToServer(serverId string, routes []Route) error {
615 | jsonData, err := json.Marshal(routes)
616 |
617 | url := fmt.Sprintf("/server/%s/routes", serverId)
618 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
619 |
620 | resp, err := c.httpClient.Do(req)
621 | if err != nil {
622 | return fmt.Errorf("AddRoutesToServer: Error on HTTP request: %s", err)
623 | }
624 | defer resp.Body.Close()
625 |
626 | body, _ := io.ReadAll(resp.Body)
627 | if resp.StatusCode != 200 {
628 | return fmt.Errorf("Non-200 response on adding routes to the server\nbody=%s", body)
629 | }
630 |
631 | return nil
632 | }
633 |
634 | func (c client) UpdateRouteOnServer(serverId string, route Route) error {
635 | jsonData, err := json.Marshal(route)
636 |
637 | url := fmt.Sprintf("/server/%s/route/%s", serverId, route.GetID())
638 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
639 |
640 | resp, err := c.httpClient.Do(req)
641 | if err != nil {
642 | return fmt.Errorf("UpdateRouteOnServer: Error on HTTP request: %s", err)
643 | }
644 | defer resp.Body.Close()
645 |
646 | body, _ := io.ReadAll(resp.Body)
647 | if resp.StatusCode != 200 {
648 | return fmt.Errorf("Non-200 response on updating a route on the server\nbody=%s", body)
649 | }
650 |
651 | return nil
652 | }
653 |
654 | func (c client) DeleteRouteFromServer(serverId string, route Route) error {
655 | url := fmt.Sprintf("/server/%s/route/%s", serverId, route.GetID())
656 | req, err := http.NewRequest("DELETE", url, nil)
657 |
658 | resp, err := c.httpClient.Do(req)
659 | if err != nil {
660 | return fmt.Errorf("DeleteRouteFromServer: Error on HTTP request: %s", err)
661 | }
662 | defer resp.Body.Close()
663 |
664 | body, _ := io.ReadAll(resp.Body)
665 | if resp.StatusCode != 200 {
666 | return fmt.Errorf("Non-200 response on deleting a route on the server\nbody=%s", body)
667 | }
668 |
669 | return nil
670 | }
671 |
672 | func (c client) GetUser(id string, orgId string) (*User, error) {
673 | url := fmt.Sprintf("/user/%s/%s", orgId, id)
674 | req, err := http.NewRequest("GET", url, nil)
675 |
676 | resp, err := c.httpClient.Do(req)
677 | if err != nil {
678 | return nil, fmt.Errorf("GetUser: Error on HTTP request: %s", err)
679 | }
680 | defer resp.Body.Close()
681 |
682 | body, _ := io.ReadAll(resp.Body)
683 | if resp.StatusCode != 200 {
684 | return nil, fmt.Errorf("Non-200 response on getting the user\nbody=%s", body)
685 | }
686 |
687 | var user User
688 | err = json.Unmarshal(body, &user)
689 | if err != nil {
690 | return nil, fmt.Errorf("GetUser: %s: %+v, id=%s, body=%s", err, user, id, body)
691 | }
692 |
693 | return &user, nil
694 | }
695 |
696 | func (c client) CreateUser(newUser User) (*User, error) {
697 | jsonData, err := json.Marshal(newUser)
698 | if err != nil {
699 | return nil, fmt.Errorf("CreateUser: Error on marshalling data: %s", err)
700 | }
701 |
702 | url := fmt.Sprintf("/user/%s", newUser.Organization)
703 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
704 |
705 | resp, err := c.httpClient.Do(req)
706 | if err != nil {
707 | return nil, fmt.Errorf("CreateUser: Error on HTTP request: %s", err)
708 | }
709 | defer resp.Body.Close()
710 |
711 | body, _ := io.ReadAll(resp.Body)
712 | if resp.StatusCode != 200 {
713 | return nil, fmt.Errorf("Non-200 response on creating the user\ncode=%d\nbody=%s", resp.StatusCode, body)
714 | }
715 |
716 | var users []User
717 | err = json.Unmarshal(body, &users)
718 | if err != nil {
719 | return nil, fmt.Errorf("CreateUser: Error on unmarshalling API response %s (body=%+v)", err, string(body))
720 | }
721 |
722 | if len(users) > 0 {
723 | return &users[0], nil
724 | }
725 |
726 | return nil, fmt.Errorf("empty users response")
727 | }
728 |
729 | func (c client) UpdateUser(id string, user *User) error {
730 | jsonData, err := json.Marshal(user)
731 | if err != nil {
732 | return fmt.Errorf("UpdateUser: Error on marshalling data: %s", err)
733 | }
734 |
735 | url := fmt.Sprintf("/user/%s/%s", user.Organization, id)
736 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
737 |
738 | resp, err := c.httpClient.Do(req)
739 | if err != nil {
740 | return fmt.Errorf("UpdateUser: Error on HTTP request: %s", err)
741 | }
742 | defer resp.Body.Close()
743 |
744 | body, _ := io.ReadAll(resp.Body)
745 | if resp.StatusCode != 200 {
746 | return fmt.Errorf("Non-200 response on updating the user\nbody=%s", body)
747 | }
748 |
749 | return nil
750 | }
751 |
752 | func (c client) DeleteUser(id string, orgId string) error {
753 | url := fmt.Sprintf("/user/%s/%s", orgId, id)
754 | req, err := http.NewRequest("DELETE", url, nil)
755 |
756 | resp, err := c.httpClient.Do(req)
757 | if err != nil {
758 | return fmt.Errorf("DeleteUser: Error on HTTP request: %s", err)
759 | }
760 | defer resp.Body.Close()
761 |
762 | body, _ := io.ReadAll(resp.Body)
763 | if resp.StatusCode != 200 {
764 | return fmt.Errorf("Non-200 response on deleting the user\nbody=%s", body)
765 | }
766 |
767 | return nil
768 | }
769 |
770 | func (c client) GetHosts() ([]Host, error) {
771 | url := fmt.Sprintf("/host")
772 | req, err := http.NewRequest("GET", url, nil)
773 |
774 | resp, err := c.httpClient.Do(req)
775 | if err != nil {
776 | return nil, fmt.Errorf("GetHosts: Error on HTTP request: %s", err)
777 | }
778 | defer resp.Body.Close()
779 |
780 | body, _ := io.ReadAll(resp.Body)
781 | if resp.StatusCode != 200 {
782 | return nil, fmt.Errorf("Non-200 response on getting the hosts\nbody=%s", body)
783 | }
784 |
785 | var hosts []Host
786 |
787 | err = json.Unmarshal(body, &hosts)
788 | if err != nil {
789 | return nil, fmt.Errorf("GetHosts: %s: %+v, body=%s", err, hosts, body)
790 | }
791 |
792 | return hosts, nil
793 | }
794 |
795 | func (c client) GetHostsByServer(serverId string) ([]Host, error) {
796 | url := fmt.Sprintf("/server/%s/host", serverId)
797 | req, err := http.NewRequest("GET", url, nil)
798 |
799 | resp, err := c.httpClient.Do(req)
800 | if err != nil {
801 | return nil, fmt.Errorf("GetHostsByServer: Error on HTTP request: %s", err)
802 | }
803 | defer resp.Body.Close()
804 |
805 | body, _ := io.ReadAll(resp.Body)
806 | if resp.StatusCode != 200 {
807 | return nil, fmt.Errorf("Non-200 response on getting hosts by the server\nbody=%s", body)
808 | }
809 |
810 | var hosts []Host
811 |
812 | err = json.Unmarshal(body, &hosts)
813 | if err != nil {
814 | return nil, fmt.Errorf("GetHostsByServer: %s: %+v, body=%s", err, hosts, body)
815 | }
816 |
817 | return hosts, nil
818 | }
819 |
820 | func (c client) AttachHostToServer(hostId, serverId string) error {
821 | url := fmt.Sprintf("/server/%s/host/%s", serverId, hostId)
822 | req, err := http.NewRequest("PUT", url, nil)
823 |
824 | resp, err := c.httpClient.Do(req)
825 | if err != nil {
826 | return fmt.Errorf("AttachHostToServer: Error on HTTP request: %s", err)
827 | }
828 | defer resp.Body.Close()
829 |
830 | body, _ := io.ReadAll(resp.Body)
831 | if resp.StatusCode != 200 {
832 | return fmt.Errorf("Non-200 response on attachhing the host the server\nbody=%s", body)
833 | }
834 |
835 | return nil
836 | }
837 |
838 | func (c client) DetachHostFromServer(hostId, serverId string) error {
839 | url := fmt.Sprintf("/server/%s/host/%s", serverId, hostId)
840 | req, err := http.NewRequest("DELETE", url, nil)
841 |
842 | resp, err := c.httpClient.Do(req)
843 | if err != nil {
844 | return fmt.Errorf("DetachHostFromServer: Error on HTTP request: %s", err)
845 | }
846 | defer resp.Body.Close()
847 |
848 | body, _ := io.ReadAll(resp.Body)
849 | if resp.StatusCode != 200 {
850 | return fmt.Errorf("Non-200 response on detaching the host from the server\nbody=%s", body)
851 | }
852 |
853 | return nil
854 | }
855 |
856 | func NewClient(baseUrl, apiToken, apiSecret string, insecure bool) Client {
857 | underlyingTransport := &http.Transport{
858 | Proxy: http.ProxyFromEnvironment,
859 | TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
860 | }
861 | httpClient := &http.Client{
862 | Transport: &transport{
863 | baseUrl: baseUrl,
864 | apiToken: apiToken,
865 | apiSecret: apiSecret,
866 | underlyingTransport: underlyingTransport,
867 | },
868 | }
869 |
870 | return &client{httpClient: httpClient}
871 | }
872 |
--------------------------------------------------------------------------------
/internal/provider/resource_server.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strings"
8 |
9 | "github.com/disc/terraform-provider-pritunl/internal/pritunl"
10 | "github.com/hashicorp/go-cty/cty"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14 | )
15 |
16 | func resourceServer() *schema.Resource {
17 | return &schema.Resource{
18 | Description: "The organization resource allows managing information about a particular Pritunl server.",
19 | Schema: map[string]*schema.Schema{
20 | "name": {
21 | Type: schema.TypeString,
22 | Required: true,
23 | Description: "The name of the server",
24 | },
25 | "protocol": {
26 | Type: schema.TypeString,
27 | Optional: true,
28 | Description: "The protocol for the server",
29 | Default: "udp",
30 | ValidateFunc: validation.StringInSlice([]string{"udp", "tcp"}, false),
31 | },
32 | "cipher": {
33 | Type: schema.TypeString,
34 | Optional: true,
35 | Description: "The cipher for the server",
36 | Default: "aes128",
37 | ValidateFunc: validation.StringInSlice([]string{"none", "bf128", "bf256", "aes128", "aes192", "aes256"}, false),
38 | },
39 | "hash": {
40 | Type: schema.TypeString,
41 | Optional: true,
42 | Description: "The hash for the server",
43 | Default: "sha1",
44 | ValidateFunc: validation.StringInSlice([]string{"none", "md5", "sha1", "sha256", "sha512"}, false),
45 | },
46 | "port": {
47 | Type: schema.TypeInt,
48 | Required: false,
49 | Optional: true,
50 | Computed: true,
51 | Description: "The port for the server",
52 | ValidateFunc: validation.IntBetween(1, 65535),
53 | },
54 | "network": {
55 | Type: schema.TypeString,
56 | Required: false,
57 | Optional: true,
58 | Computed: true,
59 | Description: "Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks",
60 |
61 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
62 | // [10,172,192].[0-255,16-31,168].[0-255].0/[8-24]
63 | // 10.0.0.0/8
64 | // 172.16.0.0/12
65 | // 192.168.0.0/16
66 | warnings := make([]string, 0)
67 | errors := make([]error, 0)
68 |
69 | _, actualIpNet, err := net.ParseCIDR(i.(string))
70 | if err != nil {
71 | errors = append(errors, err)
72 |
73 | return warnings, errors
74 | }
75 |
76 | expectedIpNets := []string{
77 | "10.0.0.0/8",
78 | "172.16.0.0/12",
79 | "192.168.0.0/16",
80 | }
81 |
82 | found := false
83 | for _, v := range expectedIpNets {
84 | _, expectedIpNet, _ := net.ParseCIDR(v)
85 | if actualIpNet.Contains(expectedIpNet.IP) || expectedIpNet.Contains(actualIpNet.IP) {
86 | found = true
87 | break
88 | }
89 | }
90 |
91 | if !found {
92 | errors = append(errors, fmt.Errorf("provided subnet %s does not belong to expected subnets %s", actualIpNet.String(), strings.Join(expectedIpNets, ", ")))
93 | }
94 |
95 | return warnings, errors
96 | },
97 | },
98 | "bind_address": {
99 | Type: schema.TypeString,
100 | Required: false,
101 | Optional: true,
102 | Description: "Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks",
103 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
104 | return validation.IsIPAddress(i, s)
105 | },
106 | },
107 | "network_wg": {
108 | Type: schema.TypeString,
109 | Required: false,
110 | Optional: true,
111 | Description: "Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks",
112 | RequiredWith: []string{"port_wg"},
113 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
114 | // [10,172,192].[0-255,16-31,168].[0-255].0/[8-24]
115 | // 10.0.0.0/8
116 | // 172.16.0.0/12
117 | // 192.168.0.0/16
118 | warnings := make([]string, 0)
119 | errors := make([]error, 0)
120 |
121 | _, actualIpNet, err := net.ParseCIDR(i.(string))
122 | if err != nil {
123 | errors = append(errors, err)
124 |
125 | return warnings, errors
126 | }
127 |
128 | expectedIpNets := []string{
129 | "10.0.0.0/8",
130 | "172.16.0.0/12",
131 | "192.168.0.0/16",
132 | }
133 |
134 | found := false
135 | for _, v := range expectedIpNets {
136 | _, expectedIpNet, _ := net.ParseCIDR(v)
137 | if actualIpNet.Contains(expectedIpNet.IP) || expectedIpNet.Contains(actualIpNet.IP) {
138 | found = true
139 | break
140 | }
141 | }
142 |
143 | if !found {
144 | errors = append(errors, fmt.Errorf("provided subnet %s does not belong to expected subnets %s", actualIpNet.String(), strings.Join(expectedIpNets, ", ")))
145 | }
146 |
147 | return warnings, errors
148 | },
149 | },
150 | "port_wg": {
151 | Type: schema.TypeInt,
152 | Required: false,
153 | Optional: true,
154 | Description: "Network address for the private network that will be created for clients. This network cannot conflict with any existing local networks",
155 | RequiredWith: []string{"network_wg"},
156 | ValidateFunc: validation.IntBetween(1, 65535),
157 | // TODO: Add validation
158 | },
159 | "groups": {
160 | Type: schema.TypeList,
161 | Elem: &schema.Schema{
162 | Type: schema.TypeString,
163 | ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics {
164 | groupName := v.(string)
165 | if strings.Contains(groupName, " ") {
166 | return diag.Diagnostics{
167 | {
168 | Severity: diag.Error,
169 | Summary: "Group name must not contain spaces",
170 | Detail: groupName + " contains spaces",
171 | AttributePath: cty.Path{cty.GetAttrStep{Name: "groups"}},
172 | },
173 | }
174 | }
175 |
176 | return nil
177 | },
178 | },
179 | Required: false,
180 | Optional: true,
181 | Description: "Enter list of groups to allow connections from. Names are case sensitive. If empty all groups will able to connect",
182 | },
183 | "dns_servers": {
184 | Type: schema.TypeList,
185 | Elem: &schema.Schema{
186 | Type: schema.TypeString,
187 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
188 | return validation.IsIPAddress(i, s)
189 | },
190 | },
191 | Required: false,
192 | Optional: true,
193 | Description: "Enter list of DNS servers applied on the client",
194 | },
195 | "sso_auth": {
196 | Type: schema.TypeBool,
197 | Required: false,
198 | Optional: true,
199 | Description: "Require client to authenticate with single sign-on provider on each connection using web browser. Requires client to have access to Pritunl web server port and running updated Pritunl Client. Single sign-on provider must already be configured for this feature to work properly",
200 | },
201 | "otp_auth": {
202 | Type: schema.TypeBool,
203 | Required: false,
204 | Optional: true,
205 | Description: "Enables two-step authentication using Google Authenticator. Verification code is entered as the user password when connecting",
206 | },
207 | "device_auth": {
208 | Type: schema.TypeBool,
209 | Required: false,
210 | Optional: true,
211 | Description: "Require administrator to approve every client device using TPM or Apple Secure Enclave",
212 | },
213 | "dynamic_firewall": {
214 | Type: schema.TypeBool,
215 | Required: false,
216 | Optional: true,
217 | Description: "Block VPN server ports by default and open port for client IP address after authenticating with HTTPS request",
218 | },
219 | "ipv6": {
220 | Type: schema.TypeBool,
221 | Required: false,
222 | Optional: true,
223 | Description: "Enables IPv6 on server, requires IPv6 network interface",
224 | },
225 | "dh_param_bits": {
226 | Type: schema.TypeInt,
227 | Required: false,
228 | Optional: true,
229 | Computed: true,
230 | Description: "Size of DH parameters",
231 | ValidateFunc: validation.IntInSlice([]int{1024, 1536, 2048, 2048, 3072, 4096}),
232 | // TODO: Cover the case " Generating DH parameters, please wait..." before start the server
233 | },
234 | "ping_interval": {
235 | Type: schema.TypeInt,
236 | Required: false,
237 | Optional: true,
238 | Computed: true,
239 | Description: "Interval to ping client",
240 | ValidateFunc: validation.IntAtLeast(1),
241 | },
242 | "ping_timeout": {
243 | Type: schema.TypeInt,
244 | Required: false,
245 | Optional: true,
246 | Computed: true,
247 | Description: "Timeout for client ping. Must be greater then ping interval",
248 | ValidateFunc: validation.All(
249 | validation.IntAtLeast(1),
250 | //func(i interface{}, s string) ([]string, []error) {
251 | // TODO: Implement "Must be greater then ping interval" rule
252 | //},
253 | ),
254 | },
255 | "link_ping_interval": {
256 | Type: schema.TypeInt,
257 | Required: false,
258 | Optional: true,
259 | Computed: true,
260 | Description: "Time in between pings used when multiple users have the same network link to failover to another user when one network link fails.",
261 | ValidateFunc: validation.IntAtLeast(1),
262 | },
263 | "link_ping_timeout": {
264 | Type: schema.TypeInt,
265 | Required: false,
266 | Optional: true,
267 | Computed: true,
268 | Description: "Optional, ping timeout used when multiple users have the same network link to failover to another user when one network link fails..",
269 | ValidateFunc: validation.IntAtLeast(0),
270 | },
271 | "session_timeout": {
272 | Type: schema.TypeInt,
273 | Required: false,
274 | Optional: true,
275 | Description: "Disconnect users after the specified number of seconds.",
276 | ValidateFunc: validation.IntAtLeast(1),
277 | },
278 | "inactive_timeout": {
279 | Type: schema.TypeInt,
280 | Required: false,
281 | Optional: true,
282 | Description: "Disconnects users after the specified number of seconds of inactivity.",
283 | ValidateFunc: validation.IntAtLeast(1),
284 | },
285 | "max_clients": {
286 | Type: schema.TypeInt,
287 | Required: false,
288 | Optional: true,
289 | Computed: true,
290 | Description: "Maximum number of clients connected to a server or to each server replica.",
291 | ValidateFunc: validation.IntAtLeast(1),
292 | },
293 | "network_mode": {
294 | Type: schema.TypeString,
295 | Required: false,
296 | Optional: true,
297 | Description: "Sets network mode. Bridged mode is not recommended using it will impact performance and client support will be limited.",
298 | ValidateFunc: validation.StringInSlice([]string{"tunnel", "bridge"}, false),
299 | },
300 | "network_start": {
301 | Type: schema.TypeString,
302 | Required: false,
303 | Optional: true,
304 | Description: "Starting network address for the bridged VPN client IP addresses. Must be in the subnet of the server network.",
305 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
306 | return validation.IsIPAddress(i, s)
307 | },
308 | RequiredWith: []string{"network_mode", "network_end"},
309 | },
310 | "network_end": {
311 | Type: schema.TypeString,
312 | Required: false,
313 | Optional: true,
314 | Description: "Ending network address for the bridged VPN client IP addresses. Must be in the subnet of the server network.",
315 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
316 | return validation.IsIPAddress(i, s)
317 | },
318 | RequiredWith: []string{"network_mode", "network_start"},
319 | },
320 | "mss_fix": {
321 | Type: schema.TypeInt,
322 | Required: false,
323 | Optional: true,
324 | Description: "MSS fix value",
325 | },
326 | "max_devices": {
327 | Type: schema.TypeInt,
328 | Required: false,
329 | Optional: true,
330 | Description: "Maximum number of devices per client connected to a server.",
331 | ValidateFunc: validation.IntAtLeast(0),
332 | },
333 | "pre_connect_msg": {
334 | Type: schema.TypeString,
335 | Required: false,
336 | Optional: true,
337 | Description: "Messages that will be shown after connect to the server",
338 | },
339 | "allowed_devices": {
340 | Type: schema.TypeString,
341 | Required: false,
342 | Optional: true,
343 | Description: "Device types permitted to connect to server.",
344 | ValidateFunc: validation.StringInSlice([]string{"mobile", "desktop"}, false),
345 | },
346 | "search_domain": {
347 | Type: schema.TypeString,
348 | Required: false,
349 | Optional: true,
350 | Description: "DNS search domain for clients. Separate multiple search domains by a comma.",
351 | // TODO: Add validation
352 | },
353 | "replica_count": {
354 | Type: schema.TypeInt,
355 | Required: false,
356 | Optional: true,
357 | Computed: true,
358 | Description: "Replicate server across multiple hosts.",
359 | ValidateFunc: validation.IntAtLeast(1),
360 | },
361 | "multi_device": {
362 | Type: schema.TypeBool,
363 | Required: false,
364 | Optional: true,
365 | Description: "Allow users to connect with multiple devices concurrently.",
366 | },
367 | "debug": {
368 | Type: schema.TypeBool,
369 | Required: false,
370 | Optional: true,
371 | Description: "Show server debugging information in output.",
372 | },
373 | "restrict_routes": {
374 | Type: schema.TypeBool,
375 | Required: false,
376 | Optional: true,
377 | Description: "Prevent traffic from networks not specified in the servers routes from being tunneled over the vpn.",
378 | },
379 | "block_outside_dns": {
380 | Type: schema.TypeBool,
381 | Required: false,
382 | Optional: true,
383 | Description: "Block outside DNS on Windows clients.",
384 | },
385 | "dns_mapping": {
386 | Type: schema.TypeBool,
387 | Required: false,
388 | Optional: true,
389 | Description: "Map the vpn clients ip address to the .vpn domain such as example_user.example_org.vpn This will conflict with the DNS port if systemd-resolve is running.",
390 | },
391 | "inter_client": {
392 | Type: schema.TypeBool,
393 | Required: false,
394 | Optional: true,
395 | Description: "Enable inter-client routing across hosts.",
396 | },
397 | "vxlan": {
398 | Type: schema.TypeBool,
399 | Required: false,
400 | Optional: true,
401 | Description: "Use VXLan for routing client-to-client traffic with replicated servers.",
402 | },
403 | "organization_ids": {
404 | Type: schema.TypeList,
405 | Elem: &schema.Schema{
406 | Type: schema.TypeString,
407 | },
408 | Required: false,
409 | Optional: true,
410 | Description: "The list of attached organizations to the server.",
411 | },
412 | "host_ids": {
413 | Type: schema.TypeList,
414 | Elem: &schema.Schema{
415 | Type: schema.TypeString,
416 | },
417 | Required: false,
418 | Optional: true,
419 | Computed: true,
420 | Description: "The list of attached hosts to the server",
421 | },
422 | "route": {
423 | Type: schema.TypeList,
424 | Elem: &schema.Resource{
425 | Schema: map[string]*schema.Schema{
426 | "network": {
427 | Type: schema.TypeString,
428 | Required: true,
429 | Description: "Network address with subnet to route",
430 | ValidateFunc: func(i interface{}, s string) ([]string, []error) {
431 | return validation.IsCIDR(i, s)
432 | },
433 | },
434 | "comment": {
435 | Type: schema.TypeString,
436 | Required: false,
437 | Optional: true,
438 | Description: "Comment for route",
439 | },
440 | "nat": {
441 | Type: schema.TypeBool,
442 | Required: false,
443 | Optional: true,
444 | Description: "NAT vpn traffic destined to this network",
445 | Computed: true,
446 | },
447 | "net_gateway": {
448 | Type: schema.TypeBool,
449 | Required: false,
450 | Optional: true,
451 | Description: "Net Gateway vpn traffic destined to this network",
452 | Computed: true,
453 | },
454 | },
455 | },
456 | Required: false,
457 | Optional: true,
458 | Description: "The list of attached routes to the server",
459 | },
460 | "status": {
461 | Type: schema.TypeString,
462 | Required: false,
463 | Optional: true,
464 | Computed: true,
465 | Description: "The status of the server",
466 | RequiredWith: []string{"organization_ids"},
467 | ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics {
468 | allowedStatusesMap := map[string]struct{}{
469 | pritunl.ServerStatusOffline: {},
470 | pritunl.ServerStatusOnline: {},
471 | }
472 |
473 | allowedStatusesList := make([]string, 0)
474 | for status := range allowedStatusesMap {
475 | allowedStatusesList = append(allowedStatusesList, status)
476 | }
477 |
478 | if _, ok := allowedStatusesMap[strings.ToLower(v.(string))]; !ok {
479 | return diag.Diagnostics{
480 | {
481 | Severity: diag.Error,
482 | Summary: "Unsupported value for the `status` attribute",
483 | Detail: fmt.Sprintf("Supported values are: %s", strings.Join(allowedStatusesList, ", ")),
484 | AttributePath: cty.Path{cty.GetAttrStep{Name: "status"}},
485 | },
486 | }
487 | }
488 |
489 | return nil
490 | },
491 | },
492 | },
493 | CreateContext: resourceCreateServer,
494 | ReadContext: resourceReadServer,
495 | UpdateContext: resourceUpdateServer,
496 | DeleteContext: resourceDeleteServer,
497 | Importer: &schema.ResourceImporter{
498 | StateContext: schema.ImportStatePassthroughContext,
499 | },
500 | }
501 | }
502 |
503 | // Uses for importing
504 | func resourceReadServer(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
505 | apiClient := meta.(pritunl.Client)
506 |
507 | server, err := apiClient.GetServer(d.Id())
508 | if err != nil {
509 | return diag.FromErr(err)
510 | }
511 |
512 | // get organizations
513 | organizations, err := apiClient.GetOrganizationsByServer(d.Id())
514 | if err != nil {
515 | return diag.FromErr(err)
516 | }
517 |
518 | // get routes
519 | routes, err := apiClient.GetRoutesByServer(d.Id())
520 | if err != nil {
521 | return diag.FromErr(err)
522 | }
523 |
524 | // get hosts
525 | hosts, err := apiClient.GetHostsByServer(d.Id())
526 | if err != nil {
527 | return diag.FromErr(err)
528 | }
529 |
530 | d.Set("name", server.Name)
531 | d.Set("protocol", server.Protocol)
532 | d.Set("port", server.Port)
533 | d.Set("cipher", server.Cipher)
534 | d.Set("hash", server.Hash)
535 | d.Set("network", server.Network)
536 | d.Set("bind_address", server.BindAddress)
537 | d.Set("dns_servers", server.DnsServers)
538 | d.Set("network_wg", server.NetworkWG)
539 | d.Set("port_wg", server.PortWG)
540 | d.Set("sso_auth", server.SsoAuth)
541 | d.Set("otp_auth", server.OtpAuth)
542 | d.Set("device_auth", server.DeviceAuth)
543 | d.Set("dynamic_firewall", server.DynamicFirewall)
544 | d.Set("ipv6", server.IPv6)
545 | d.Set("dh_param_bits", server.DhParamBits)
546 | d.Set("ping_interval", server.PingInterval)
547 | d.Set("ping_timeout", server.PingTimeout)
548 | d.Set("link_ping_interval", server.LinkPingInterval)
549 | d.Set("link_ping_timeout", server.LinkPingTimeout)
550 | d.Set("session_timeout", server.SessionTimeout)
551 | d.Set("inactive_timeout", server.InactiveTimeout)
552 | d.Set("max_clients", server.MaxClients)
553 | d.Set("network_mode", server.NetworkMode)
554 | d.Set("network_start", server.NetworkStart)
555 | d.Set("network_end", server.NetworkEnd)
556 | d.Set("mss_fix", server.MssFix)
557 | d.Set("max_devices", server.MaxDevices)
558 | d.Set("pre_connect_msg", server.PreConnectMsg)
559 | d.Set("allowed_devices", server.AllowedDevices)
560 | d.Set("search_domain", server.SearchDomain)
561 | d.Set("replica_count", server.ReplicaCount)
562 | d.Set("multi_device", server.MultiDevice)
563 | d.Set("debug", server.Debug)
564 | d.Set("restrict_routes", server.RestrictRoutes)
565 | d.Set("block_outside_dns", server.BlockOutsideDns)
566 | d.Set("dns_mapping", server.DnsMapping)
567 | d.Set("inter_client", server.InterClient)
568 | d.Set("vxlan", server.VxLan)
569 | d.Set("status", server.Status)
570 |
571 | if len(organizations) > 0 {
572 | organizationsList := make([]string, 0)
573 |
574 | if organizations != nil {
575 | for _, organization := range organizations {
576 | organizationsList = append(organizationsList, organization.ID)
577 | }
578 | }
579 |
580 | declaredOrganizations, ok := d.Get("organization_ids").([]interface{})
581 | if !ok {
582 | return diag.Errorf("failed to parse organization_ids for the server: %s", server.Name)
583 | }
584 |
585 | if len(declaredOrganizations) > 0 {
586 | organizationsList = matchStringEntitiesWithSchema(organizationsList, declaredOrganizations)
587 | }
588 |
589 | d.Set("organization_ids", organizationsList)
590 | }
591 |
592 | if len(server.Groups) > 0 {
593 | groupsList := make([]string, 0)
594 |
595 | for _, group := range server.Groups {
596 | groupsList = append(groupsList, group)
597 | }
598 |
599 | declaredGroups, ok := d.Get("groups").([]interface{})
600 | if !ok {
601 | return diag.Errorf("failed to parse groups for the server: %s", server.Name)
602 | }
603 |
604 | if len(declaredGroups) > 0 {
605 | groupsList = matchStringEntitiesWithSchema(groupsList, declaredGroups)
606 | }
607 |
608 | d.Set("groups", groupsList)
609 | }
610 |
611 | if len(routes) > 0 {
612 | declaredRoutes, ok := d.Get("route").([]interface{})
613 | if !ok {
614 | return diag.Errorf("failed to parse routes for the server: %s", server.Name)
615 | }
616 |
617 | if len(declaredRoutes) > 0 {
618 | routes = matchRoutesWithSchema(routes, declaredRoutes)
619 | }
620 |
621 | d.Set("route", flattenRoutesData(routes))
622 | }
623 |
624 | if len(hosts) > 0 {
625 | hostsList := make([]string, 0)
626 |
627 | if hosts != nil {
628 | for _, host := range hosts {
629 | hostsList = append(hostsList, host.ID)
630 | }
631 | }
632 |
633 | declaredHosts, ok := d.Get("host_ids").([]interface{})
634 | if !ok {
635 | return diag.Errorf("failed to parse host_ids for the server: %s", server.Name)
636 | }
637 |
638 | if len(declaredHosts) > 0 {
639 | hostsList = matchStringEntitiesWithSchema(hostsList, declaredHosts)
640 | }
641 |
642 | d.Set("host_ids", hostsList)
643 | }
644 |
645 | return nil
646 | }
647 |
648 | func resourceCreateServer(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
649 | apiClient := meta.(pritunl.Client)
650 |
651 | serverData := map[string]interface{}{
652 | "name": d.Get("name"),
653 | "protocol": d.Get("protocol"),
654 | "port": d.Get("port"),
655 | "network": d.Get("network"),
656 | "cipher": d.Get("cipher"),
657 | "hash": d.Get("hash"),
658 | "bind_address": d.Get("bind_address"),
659 | "groups": d.Get("groups"),
660 | "dns_servers": d.Get("dns_servers"),
661 | "network_wg": d.Get("network_wg"),
662 | "port_wg": d.Get("port_wg"),
663 | "sso_auth": d.Get("sso_auth"),
664 | "otp_auth": d.Get("otp_auth"),
665 | "device_auth": d.Get("device_auth"),
666 | "dynamic_firewall": d.Get("dynamic_firewall"),
667 | "ipv6": d.Get("ipv6"),
668 | "dh_param_bits": d.Get("dh_param_bits"),
669 | "ping_interval": d.Get("ping_interval"),
670 | "ping_timeout": d.Get("ping_timeout"),
671 | "link_ping_interval": d.Get("link_ping_interval"),
672 | "link_ping_timeout": d.Get("link_ping_timeout"),
673 | "session_timeout": d.Get("session_timeout"),
674 | "inactive_timeout": d.Get("inactive_timeout"),
675 | "max_clients": d.Get("max_clients"),
676 | "network_mode": d.Get("network_mode"),
677 | "network_start": d.Get("network_start"),
678 | "network_end": d.Get("network_end"),
679 | "mss_fix": d.Get("mss_fix"),
680 | "max_devices": d.Get("max_devices"),
681 | "pre_connect_msg": d.Get("pre_connect_msg"),
682 | "allowed_devices": d.Get("allowed_devices"),
683 | "search_domain": d.Get("search_domain"),
684 | "replica_count": d.Get("replica_count"),
685 | "multi_device": d.Get("multi_device"),
686 | "debug": d.Get("debug"),
687 | "restrict_routes": d.Get("restrict_routes"),
688 | "block_outside_dns": d.Get("block_outside_dns"),
689 | "dns_mapping": d.Get("dns_mapping"),
690 | "inter_client": d.Get("inter_client"),
691 | "vxlan": d.Get("vxlan"),
692 | }
693 |
694 | server, err := apiClient.CreateServer(serverData)
695 | if err != nil {
696 | return diag.FromErr(err)
697 | }
698 |
699 | d.SetId(server.ID)
700 |
701 | if d.HasChange("organization_ids") {
702 | _, newOrgs := d.GetChange("organization_ids")
703 | for _, v := range newOrgs.([]interface{}) {
704 | err = apiClient.AttachOrganizationToServer(v.(string), d.Id())
705 | if err != nil {
706 | return diag.Errorf("Error on attaching server to the organization: %s", err)
707 | }
708 | }
709 | }
710 |
711 | // Delete default route
712 | defaultRoute := pritunl.Route{
713 | Network: "0.0.0.0/0",
714 | Nat: true,
715 | }
716 | err = apiClient.DeleteRouteFromServer(d.Id(), defaultRoute)
717 | if err != nil {
718 | return diag.Errorf("Error on attaching server to the organization: %s", err)
719 | }
720 |
721 | if d.HasChange("route") {
722 | _, newRoutes := d.GetChange("route")
723 | routes := make([]pritunl.Route, 0)
724 |
725 | for _, v := range newRoutes.([]interface{}) {
726 | routes = append(routes, pritunl.ConvertMapToRoute(v.(map[string]interface{})))
727 | }
728 |
729 | err = apiClient.AddRoutesToServer(d.Id(), routes)
730 | if err != nil {
731 | return diag.Errorf("Error on attaching route from the server: %s", err)
732 | }
733 | }
734 |
735 | if d.HasChange("host_ids") {
736 | // delete default host(s) only when host_ids aren't empty
737 |
738 | hosts, err := apiClient.GetHostsByServer(d.Id())
739 | if err != nil {
740 | return diag.FromErr(err)
741 | }
742 | for _, host := range hosts {
743 | err = apiClient.DetachHostFromServer(host.ID, d.Id())
744 | if err != nil {
745 | return diag.Errorf("Error on detaching a host from the server: %s", err)
746 | }
747 | }
748 |
749 | _, newHosts := d.GetChange("host_ids")
750 | for _, v := range newHosts.([]interface{}) {
751 | err = apiClient.AttachHostToServer(v.(string), d.Id())
752 | if err != nil {
753 | return diag.Errorf("Error on attaching a host to the server: %s", err)
754 | }
755 | }
756 | }
757 |
758 | if d.Get("status").(string) == pritunl.ServerStatusOnline {
759 | err = apiClient.StartServer(d.Id())
760 | if err != nil {
761 | return diag.Errorf("Error on starting server: %s", err)
762 | }
763 | }
764 |
765 | return resourceReadServer(ctx, d, meta)
766 | }
767 |
768 | func resourceUpdateServer(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
769 | apiClient := meta.(pritunl.Client)
770 |
771 | server, err := apiClient.GetServer(d.Id())
772 | if err != nil {
773 | return diag.FromErr(err)
774 | }
775 |
776 | prevServerStatus := server.Status
777 |
778 | if v, ok := d.GetOk("name"); ok {
779 | server.Name = v.(string)
780 | }
781 |
782 | if v, ok := d.GetOk("protocol"); ok {
783 | server.Protocol = v.(string)
784 | }
785 |
786 | if v, ok := d.GetOk("cipher"); ok {
787 | server.Cipher = v.(string)
788 | }
789 |
790 | if v, ok := d.GetOk("hash"); ok {
791 | server.Hash = v.(string)
792 | }
793 |
794 | if v, ok := d.GetOk("port"); ok {
795 | server.Port = v.(int)
796 | }
797 |
798 | if v, ok := d.GetOk("network"); ok {
799 | server.Network = v.(string)
800 | }
801 |
802 | if d.HasChange("bind_address") {
803 | server.BindAddress = d.Get("bind_address").(string)
804 | }
805 |
806 | if d.HasChange("network_wg") {
807 | server.NetworkWG = d.Get("network_wg").(string)
808 | }
809 |
810 | if d.HasChange("port_wg") {
811 | server.PortWG = d.Get("port_wg").(int)
812 | }
813 |
814 | isWgEnabled := server.NetworkWG != "" && server.PortWG > 0
815 | server.WG = isWgEnabled
816 |
817 | if d.HasChange("sso_auth") {
818 | server.SsoAuth = d.Get("sso_auth").(bool)
819 | }
820 |
821 | if d.HasChange("otp_auth") {
822 | server.OtpAuth = d.Get("otp_auth").(bool)
823 | }
824 |
825 | if d.HasChange("device_auth") {
826 | server.DeviceAuth = d.Get("device_auth").(bool)
827 | }
828 |
829 | if d.HasChange("dynamic_firewall") {
830 | server.DynamicFirewall = d.Get("dynamic_firewall").(bool)
831 | }
832 |
833 | if d.HasChange("ipv6") {
834 | server.IPv6 = d.Get("ipv6").(bool)
835 | }
836 |
837 | if d.HasChange("dh_param_bits") {
838 | server.DhParamBits = d.Get("dh_param_bits").(int)
839 | }
840 |
841 | if d.HasChange("ping_interval") {
842 | server.PingInterval = d.Get("ping_interval").(int)
843 | }
844 |
845 | if d.HasChange("ping_timeout") {
846 | server.PingTimeout = d.Get("ping_timeout").(int)
847 | }
848 |
849 | if d.HasChange("link_ping_interval") {
850 | server.LinkPingInterval = d.Get("link_ping_interval").(int)
851 | }
852 |
853 | if d.HasChange("link_ping_timeout") {
854 | server.LinkPingTimeout = d.Get("link_ping_timeout").(int)
855 | }
856 |
857 | if d.HasChange("session_timeout") {
858 | server.SessionTimeout = d.Get("session_timeout").(int)
859 | }
860 |
861 | if d.HasChange("inactive_timeout") {
862 | server.InactiveTimeout = d.Get("inactive_timeout").(int)
863 | }
864 |
865 | if d.HasChange("max_clients") {
866 | server.MaxClients = d.Get("max_clients").(int)
867 | }
868 |
869 | if d.HasChange("network_mode") {
870 | server.NetworkMode = d.Get("network_mode").(string)
871 | }
872 |
873 | if d.HasChange("network_start") {
874 | server.NetworkStart = d.Get("network_start").(string)
875 | }
876 |
877 | if d.HasChange("network_end") {
878 | server.NetworkEnd = d.Get("network_end").(string)
879 | }
880 |
881 | if server.NetworkMode == pritunl.ServerNetworkModeBridge && (server.NetworkStart == "" || server.NetworkEnd == "") {
882 | return diag.Errorf("the attribute network_mode = %s requires network_start and network_end attributes", pritunl.ServerNetworkModeBridge)
883 | }
884 |
885 | if d.HasChange("mss_fix") {
886 | server.MssFix = d.Get("mss_fix").(int)
887 | }
888 |
889 | if d.HasChange("max_devices") {
890 | server.MaxDevices = d.Get("max_devices").(int)
891 | }
892 |
893 | if d.HasChange("pre_connect_msg") {
894 | server.PreConnectMsg = d.Get("pre_connect_msg").(string)
895 | }
896 |
897 | if d.HasChange("allowed_devices") {
898 | server.AllowedDevices = d.Get("allowed_devices").(string)
899 | }
900 |
901 | if d.HasChange("search_domain") {
902 | server.SearchDomain = d.Get("search_domain").(string)
903 | }
904 |
905 | if d.HasChange("replica_count") {
906 | server.ReplicaCount = d.Get("replica_count").(int)
907 | }
908 |
909 | if d.HasChange("multi_device") {
910 | server.MultiDevice = d.Get("multi_device").(bool)
911 | }
912 |
913 | if d.HasChange("debug") {
914 | server.Debug = d.Get("debug").(bool)
915 | }
916 |
917 | if d.HasChange("restrict_routes") {
918 | server.RestrictRoutes = d.Get("restrict_routes").(bool)
919 | }
920 |
921 | if d.HasChange("block_outside_dns") {
922 | server.BlockOutsideDns = d.Get("block_outside_dns").(bool)
923 | }
924 |
925 | if d.HasChange("dns_mapping") {
926 | server.DnsMapping = d.Get("dns_mapping").(bool)
927 | }
928 |
929 | if d.HasChange("vxlan") {
930 | server.VxLan = d.Get("vxlan").(bool)
931 | }
932 |
933 | if d.HasChange("groups") {
934 | groups := make([]string, 0)
935 | for _, v := range d.Get("groups").([]interface{}) {
936 | groups = append(groups, v.(string))
937 | }
938 | server.Groups = groups
939 | }
940 |
941 | if d.HasChange("dns_servers") {
942 | dnsServers := make([]string, 0)
943 | for _, v := range d.Get("dns_servers").([]interface{}) {
944 | dnsServers = append(dnsServers, v.(string))
945 | }
946 | server.DnsServers = dnsServers
947 | }
948 |
949 | // Stop server before applying any change
950 | err = apiClient.StopServer(d.Id())
951 | if err != nil {
952 | return diag.Errorf("Error on stopping server: %s", err)
953 | }
954 |
955 | if d.HasChange("organization_ids") {
956 | oldOrgs, newOrgs := d.GetChange("organization_ids")
957 |
958 | oldOrgsOnly := diffStringLists(oldOrgs.([]interface{}), newOrgs.([]interface{}))
959 | for _, v := range oldOrgsOnly {
960 | err = apiClient.DetachOrganizationFromServer(v, d.Id())
961 | if err != nil {
962 | return diag.Errorf("Error on detaching server to the organization: %s", err)
963 | }
964 | }
965 |
966 | newOrgsOnly := diffStringLists(newOrgs.([]interface{}), oldOrgs.([]interface{}))
967 | for _, v := range newOrgsOnly {
968 | err = apiClient.AttachOrganizationToServer(v, d.Id())
969 | if err != nil {
970 | return diag.Errorf("Error on attaching server to the organization: %s", err)
971 | }
972 | }
973 | }
974 |
975 | if d.HasChange("route") {
976 | oldRoutes, newRoutes := d.GetChange("route")
977 |
978 | newRoutesMap := make(map[string]pritunl.Route)
979 | for _, v := range newRoutes.([]interface{}) {
980 | route := pritunl.ConvertMapToRoute(v.(map[string]interface{}))
981 | newRoutesMap[route.Network] = route
982 | }
983 | oldRoutesMap := make(map[string]pritunl.Route)
984 | for _, v := range oldRoutes.([]interface{}) {
985 | route := pritunl.ConvertMapToRoute(v.(map[string]interface{}))
986 | oldRoutesMap[route.Network] = route
987 | }
988 |
989 | for network, newRoute := range newRoutesMap {
990 | if oldRoute, found := oldRoutesMap[network]; found {
991 | // update if something changed or skip
992 | if oldRoute.Nat != newRoute.Nat || oldRoute.NetGateway != newRoute.NetGateway || oldRoute.Comment != newRoute.Comment {
993 | err = apiClient.UpdateRouteOnServer(d.Id(), newRoute)
994 | if err != nil {
995 | return diag.Errorf("Error on updating route on the server: %s", err)
996 | }
997 | }
998 | } else {
999 | // add route
1000 | err = apiClient.AddRouteToServer(d.Id(), newRoute)
1001 | if err != nil {
1002 | return diag.Errorf("Error on adding route to the server: %s", err)
1003 | }
1004 | }
1005 | }
1006 |
1007 | for network, oldRoute := range oldRoutesMap {
1008 | if _, found := newRoutesMap[network]; !found {
1009 | // delete route
1010 | err = apiClient.DeleteRouteFromServer(d.Id(), oldRoute)
1011 | if err != nil {
1012 | return diag.Errorf("Error on deleting route from the server: %s", err)
1013 | }
1014 | }
1015 | }
1016 | }
1017 |
1018 | if d.HasChange("host_ids") {
1019 | oldHosts, newHosts := d.GetChange("host_ids")
1020 | for _, v := range oldHosts.([]interface{}) {
1021 | err = apiClient.DetachHostFromServer(v.(string), d.Id())
1022 | if err != nil {
1023 | return diag.Errorf("Error on detaching server to the organization: %s", err)
1024 | }
1025 | }
1026 | for _, v := range newHosts.([]interface{}) {
1027 | err = apiClient.AttachHostToServer(v.(string), d.Id())
1028 | if err != nil {
1029 | return diag.Errorf("Error on attaching server to the organization: %s", err)
1030 | }
1031 | }
1032 | }
1033 |
1034 | // Start server if it was ONLINE before and status wasn't changed OR status was changed to ONLINE
1035 | shouldServerBeStarted := (prevServerStatus == pritunl.ServerStatusOnline && !d.HasChange("status")) || (d.HasChange("status") && d.Get("status").(string) != pritunl.ServerStatusOffline)
1036 |
1037 | err = apiClient.UpdateServer(d.Id(), server)
1038 | if err != nil {
1039 | // start server in case of error?
1040 | return diag.FromErr(err)
1041 | }
1042 |
1043 | if shouldServerBeStarted {
1044 | err = apiClient.StartServer(d.Id())
1045 | if err != nil {
1046 | return diag.Errorf("Error on starting server: %s", err)
1047 | }
1048 | }
1049 |
1050 | return resourceReadServer(ctx, d, meta)
1051 | }
1052 |
1053 | func resourceDeleteServer(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
1054 | apiClient := meta.(pritunl.Client)
1055 |
1056 | err := apiClient.DeleteServer(d.Id())
1057 | if err != nil {
1058 | return diag.FromErr(err)
1059 | }
1060 |
1061 | d.SetId("")
1062 |
1063 | return nil
1064 | }
1065 |
1066 | func diffStringLists(mainList []interface{}, otherList []interface{}) []string {
1067 | result := make([]string, 0)
1068 | var found bool
1069 |
1070 | for _, i := range mainList {
1071 | found = false
1072 | for _, j := range otherList {
1073 | if i.(string) == j.(string) {
1074 | found = true
1075 | break
1076 | }
1077 | }
1078 | if !found {
1079 | result = append(result, i.(string))
1080 | }
1081 | }
1082 |
1083 | return result
1084 | }
1085 |
1086 | func flattenRoutesData(routesList []pritunl.Route) []interface{} {
1087 | routes := make([]interface{}, 0)
1088 |
1089 | if routesList != nil {
1090 | for _, route := range routesList {
1091 | if route.VirtualNetwork {
1092 | // skip virtual network route
1093 | continue
1094 | }
1095 |
1096 | routeMap := make(map[string]interface{})
1097 |
1098 | routeMap["network"] = route.Network
1099 | routeMap["nat"] = route.Nat
1100 | routeMap["net_gateway"] = route.NetGateway
1101 | if route.Comment != "" {
1102 | routeMap["comment"] = route.Comment
1103 | }
1104 |
1105 | routes = append(routes, routeMap)
1106 | }
1107 | }
1108 |
1109 | return routes
1110 | }
1111 |
1112 | // This cannot currently be handled efficiently by a DiffSuppressFunc
1113 | // See: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
1114 | func matchRoutesWithSchema(routes []pritunl.Route, declaredRoutes []interface{}) []pritunl.Route {
1115 | result := make([]pritunl.Route, len(declaredRoutes))
1116 |
1117 | routesMap := make(map[string]pritunl.Route)
1118 | for _, route := range routes {
1119 | routesMap[route.Network] = route
1120 | }
1121 |
1122 | for i, declaredRoute := range declaredRoutes {
1123 | declaredRouteMap := declaredRoute.(map[string]interface{})
1124 | network, ok := declaredRouteMap["network"].(string)
1125 | if !ok {
1126 | continue
1127 | }
1128 |
1129 | if apiRoute, exists := routesMap[network]; exists {
1130 | result[i] = apiRoute
1131 | delete(routesMap, network)
1132 | }
1133 | }
1134 |
1135 | for _, route := range routesMap {
1136 | result = append(result, route)
1137 | }
1138 |
1139 | return result
1140 | }
1141 |
1142 | // This cannot currently be handled efficiently by a DiffSuppressFunc
1143 | // See: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
1144 | func matchStringEntitiesWithSchema(entities []string, declaredEntities []interface{}) []string {
1145 | if len(declaredEntities) == 0 {
1146 | return entities
1147 | }
1148 |
1149 | result := make([]string, len(declaredEntities))
1150 |
1151 | for i, declaredEntity := range declaredEntities {
1152 | for _, entity := range entities {
1153 | if entity != declaredEntity.(string) {
1154 | continue
1155 | }
1156 |
1157 | result[i] = entity
1158 | break
1159 | }
1160 | }
1161 |
1162 | return result
1163 | }
1164 |
--------------------------------------------------------------------------------