├── .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 | Pritunl logo 3 | 4 | 5 | Terraform logo 6 | 7 | 8 | # Terraform Provider for Pritunl VPN Server 9 | 10 | [![Release](https://img.shields.io/github/v/release/disc/terraform-provider-pritunl)](https://github.com/disc/terraform-provider-pritunl/releases) 11 | [![Registry](https://img.shields.io/badge/registry-doc%40latest-lightgrey?logo=terraform)](https://registry.terraform.io/providers/disc/pritunl/latest/docs) 12 | [![License](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://github.com/disc/terraform-provider-pritunl/blob/master/LICENSE) 13 | [![Go Report Card](https://goreportcard.com/badge/github.com/disc/terraform-provider-pritunl)](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 | --------------------------------------------------------------------------------