├── .DEREK.yml
├── .gitignore
├── LICENSE
├── README.md
├── cloud-config.txt
├── cmd
├── README.md
├── go.mod
├── go.sum
├── k3sup.txt
├── main.go
└── nginx.txt
├── go.mod
├── go.sum
└── provision
├── azure.go
├── azure_test.go
├── digitalocean.go
├── ec2.go
├── ec2_test.go
├── gce.go
├── gce_test.go
├── hetzner.go
├── linode.go
├── linode_test.go
├── mock_linode.go
├── ovh.go
├── provision.go
├── scaleway.go
├── userdata.go
├── userdata_test.go
└── vultr.go
/.DEREK.yml:
--------------------------------------------------------------------------------
1 | redirect: https://raw.githubusercontent.com/inlets/inlets-pro/master/.DEREK.yml
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /cmd/cmd
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 inlets
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## cloud-provision
2 |
3 | > Get cloud instances with your favourite software pre-loaded
4 |
5 | This Golang package can be used to provision cloud hosts using a simple CRUD-style API along with a cloud-init user-data script. It could be used to automate anything from k3s clusters, to blogs, or CI runners. We use it to create the cheapest possible hosts in the cloud with a public IP address.
6 |
7 | [provision.go](https://github.com/inlets/inletsctl/blob/master/pkg/provision/provision.go)
8 |
9 | ```go
10 | type Provisioner interface {
11 | Provision(BasicHost) (*ProvisionedHost, error)
12 | Status(id string) (*ProvisionedHost, error)
13 | Delete(HostDeleteRequest) error
14 | }
15 | ```
16 |
17 | ## Where is this package used?
18 |
19 | > Feel free to send a PR to add your project
20 |
21 | This package is used by:
22 |
23 | * [inletsctl](https://github.com/inlets/inletsctl) - Go CLI to create/delete exit-servers and inlets/-pro tunnels
24 | * [inlets-operator](https://github.com/inlets/inlets-operator) - Kubernetes operator to automate exit-servers and inlets/-pro tunnels via CRDs and Service definitions
25 |
26 | ### Try an example program
27 |
28 | The tester app takes in a cloud-config file and provisions a host with Nginx - polling until it is ready for access.
29 |
30 | ```yaml
31 | #cloud-config
32 | packages:
33 | - nginx
34 | runcmd:
35 | - systemctl enable nginx
36 | - systemctl start nginx
37 | ```
38 |
39 | See the example here: [Tester app](https://github.com/inlets/cloud-provision/tree/master/cmd)
40 |
41 | ## Rules for adding a new provisioner
42 |
43 | The first rule about the `provision` package is that we don't do SSH. Key management and statefulness are out of scope. Cheap servers should be treated like cattle, not pets. `ssh` may well be enabled by default, but is out of scope for management. For instance, with DigitalOcean, you can get a root password if you need to log in. Configure as much as you can via cloud-init / user-data.
44 |
45 | * Use the Ubuntu 16.04 LTS image
46 | * Select the cheapest plan and update the [README](https://github.com/inlets/inletsctl/blob/master/README.md) with the estimated monthly cost
47 | * You need to open all ports on any firewall rules since the inlets client advertises its ports at runtime
48 | * This API is event-driven and is expected to use polling from the Kubernetes Operator or inletsctl CLI, not callbacks or waits
49 | * Do not use any wait or blocking calls, all API calls should return ideally within < 1s
50 | * Document how you chose any image or configuration, so that the code can be maintained, so that means links and `// comments`
51 | * All provisioning code should detect the correct "status" for the provider and set the standard known value
52 | * Always show your testing in PRs.
53 |
54 | Finally please [add an example to the documentation](https://docs.inlets.dev/#/tools/inletsctl?id=inletsctl-reference-documentation) for your provider in the [inlets/docs](https://github.com/inlets/docs) repo.
55 |
56 | If you would like to add a provider please propose it with an Issue, to make sure that the community are happy to accept the change, and to maintain the code on an ongoing basis.
57 |
58 | ## Maintainers for each provider
59 |
60 | * DigitalOcean, Equinix Metal, Civo - [alexellis](https://github.com/alexellis/)
61 | * Scaleway - [alexandrevilain](https://github.com/alexandrevilain/)
62 | * AWS EC2 - [adamjohnson01](https://github.com/adamjohnson01/)
63 | * GCE - [utsavanand2](https://github.com/utsavanand2/)
64 | * Azure, Linode - [zechenbit](https://github.com/zechenbit/)
65 | * Hetzner [Johannestegner](https://github.com/johannestegner)
66 | * Vultr [jsiebens](https://github.com/jsiebens)
67 |
68 |
--------------------------------------------------------------------------------
/cloud-config.txt:
--------------------------------------------------------------------------------
1 | #cloud-config
2 | packages:
3 | - nginx
4 | runcmd:
5 | - systemctl enable nginx
6 | - systemctl start nginx
7 |
--------------------------------------------------------------------------------
/cmd/README.md:
--------------------------------------------------------------------------------
1 | ## Provision and automate cloud hosts in Go
2 |
3 | This is an example CLI that shows how to use the [provision](https://github.com/inlets/inletsctl/tree/master/pkg/provision) package by inlets to create and automate a cloud host, optionally installing some packages using [user-data/cloud-init](https://cloudinit.readthedocs.io/en/latest/topics/examples.html).
4 |
5 | What's this for? This is an example of how to use the provision package as used in [inletsctl](https://github.com/inlets/inletsctl) and the [inlets-operator](https://github.com/inlets/inlets-operator).
6 |
7 | * Have fun
8 | * Learn about cloud + platform engineering
9 | * Practice or build your Go skills
10 |
11 | Want to know more about inlets? [Inlets is a Cloud Native Tunnel](https://docs.inlets.dev/)
12 | Want to learn Go? Start with Alex's [golang basics series](https://blog.alexellis.io/tag/golang-basics/)
13 |
14 | ## Tutorial
15 |
16 | Checkout the [provision](https://github.com/inlets/inletsctl/tree/master/pkg/provision) README file to find out more, before starting.
17 |
18 | Clone/build:
19 |
20 | ```sh
21 | export GOPATH=$HOME/go
22 | mkdir -p $GOPATH/inlets/
23 | cd $GOPATH/inlets
24 |
25 | git clone https://github.com/inlets/provision-example
26 | cd provision-example
27 | go build
28 | ```
29 |
30 | Now read the code, in main.go, and make sure you understand what's happening. You will need a cloud API token saved into a file, you can create this via your dashboard. The example uses DigitalOcean, but you can customise it to use any provisioner.
31 |
32 | Create a file `./cloud-config.txt`
33 |
34 | ```sh
35 | #cloud-config
36 | packages:
37 | - nginx
38 | runcmd:
39 | - systemctl enable nginx
40 | - systemctl start nginx
41 | ```
42 |
43 | For more examples of cloud-init, including how to add a custom user and SSH key, see [cloud-init examples](https://cloudinit.readthedocs.io/en/latest/topics/examples.html).
44 |
45 | Run the example:
46 |
47 | ```sh
48 | ./provision-example \
49 | --access-token $(cat ~/Downloads/do-access-token) \
50 | --userdata-file cloud-config.txt
51 |
52 | 2020/02/22 11:01:58 Provisioning host with DigitalOcean
53 | Host ID: 181660892
54 | Polling status: 1/250
55 | Polling status: 2/250
56 | Polling status: 3/250
57 | Polling status: 4/250
58 | Polling status: 5/250
59 | Polling status: 6/250
60 | Polling status: 7/250
61 | Polling status: 8/250
62 | Polling status: 9/250
63 | Polling status: 10/250
64 | Polling status: 11/250
65 | Your IP address is: 64.227.34.235
66 | ```
67 |
68 | Then try the host:
69 |
70 | ```
71 | curl -s 64.227.34.235 | head -n 4
72 |
73 |
74 |
75 | Welcome to nginx!
76 | ```
77 |
78 | Delete any hosts you create via your dashboard, or using the cloud-provider's CLI.
79 |
80 | ## What about SSH?
81 |
82 | Here's an example with my SSH key pre-installed on the host:
83 |
84 | ```yaml
85 | #cloud-config
86 | ssh_authorized_keys:
87 | ## Note: Replace with your own public key
88 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD5P+iIgayz0nM6ra5y25KyKEPZyyRiXssOQ92GS7+JHKcerHndzbgWHUBH4Sc0HE/IeIFiZ20iYVBMLJZteXfrrEV91LNPApZ010RRNehzGIfPj0C/5bNVA3NtwsXWtz6O17goEPlblhcbJ1XoS5xdy4U2GMfaB8C9bZ0RvYFuXP+FJfgvJ9mp4MesLKMH/rKUc7uLIWCQWgrTLoTx1r+merWkQCiWU8onvfh+B7vXggY9ffOxADJ+JqXjEbG49CiXG3zJq2xnbiyf0zVvvG2Utr45lPm1cxbqch5BdJrZpIb8qSvjuV/oq6AUvUqpBei2YLZKz1sPKONkBB1t5e3Xa9+PYB19PUmn8/WUQYWGJ4LB5mTe87nfs1Q1p/cQe4pq+Y8s3rUitnqwv16g5CdUAXG8KPWlAXB+VK04cj1E3CYkEOUIeeeyUDlMPezrEFEKjDcqhaReDMjHMma95SeMjlt3ZrO2FsXjmgLfjv5kvsWFZdDjl3zQovVfs+pVzGk= alex@am1.local
89 | runcmd:
90 | - curl -SLs https://get.k3sup.dev | sh
91 | - k3sup install --local --k3s-channel stable
92 | ```
93 |
94 | Then:
95 |
96 | ```bash
97 | Polling status: 15/250
98 | Your IP address is: 64.227.37.14
99 | ```
100 |
101 | Connect with:
102 |
103 | ```bash
104 |
105 | # Log in to change root password, check your email for the value
106 | ssh root@64.227.37.14
107 |
108 | # Then run the below:
109 | ssh root@64.227.37.14 "sudo kubectl get nodes -o wide"
110 |
111 | NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
112 | provision-example Ready control-plane,master 21s v1.21.5+k3s2 64.227.42.201 Ubuntu 18.04.6 LTS 4.15.0-159-generic containerd://1.4.11-k3s1
113 | ```
114 |
115 | Merge it to your local kubeconfig for administration by running k3sup on your local machine:
116 |
117 | ```bash
118 | k3sup install --skip-install --ip 64.227.37.14 \
119 | --user root \
120 | --local-path $HOME/.kube/config \
121 | --context auto-k3s
122 |
123 | Saving file to: /Users/alex/.kube/config
124 |
125 | # Test your cluster with:
126 | export KUBECONFIG=/Users/alex/.kube/config
127 | kubectl config set-context auto-k3s
128 | kubectl get node -o wide
129 |
130 | NAME STATUS ROLES AGE VERSION
131 | provision-example Ready control-plane,master 68s v1.21.5+k3s2
132 | ```
133 |
134 | Need to troubleshoot?
135 |
136 | Log in to the instance via SSH and explore the logs:
137 |
138 | ```bash
139 | cat /var/log/cloud-init.log
140 | cat /var/log/cloud-init-output.log
141 | ```
142 |
143 | ## What next?
144 |
145 | Have fun, start learning and customise the example to create something cool.
146 |
147 | Remember to delete the instances you create using this tool.
148 |
149 | ## Contributing
150 |
151 | Please follow the [inlets contributing guide](https://github.com/inlets/inlets/blob/master/CONTRIBUTING.md)
152 |
153 | Need support? Join the [OpenFaaS Slack and the #inlets channel](https://slack.openfaas.io/), or open an issue if there is a genuine issue with the code.
154 |
--------------------------------------------------------------------------------
/cmd/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/inlets/cloud-provision/cmd
2 |
3 | go 1.23
4 |
5 | replace github.com/inlets/cloud-provision/provision => ../
6 |
7 | require github.com/inlets/cloud-provision v0.7.0
8 |
9 | require (
10 | cloud.google.com/go/auth v0.14.0 // indirect
11 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
12 | cloud.google.com/go/compute/metadata v0.6.0 // indirect
13 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
14 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect
15 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
16 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 // indirect
17 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 // indirect
18 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect
19 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
20 | github.com/aws/aws-sdk-go v1.55.6 // indirect
21 | github.com/beorn7/perks v1.0.1 // indirect
22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
24 | github.com/digitalocean/godo v1.134.0 // indirect
25 | github.com/dimchansky/utfbom v1.1.1 // indirect
26 | github.com/dirien/ovh-go-sdk v0.2.0 // indirect
27 | github.com/felixge/httpsnoop v1.0.4 // indirect
28 | github.com/go-logr/logr v1.4.2 // indirect
29 | github.com/go-logr/stdr v1.2.2 // indirect
30 | github.com/go-resty/resty/v2 v2.16.3 // indirect
31 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
32 | github.com/golang/mock v1.6.0 // indirect
33 | github.com/google/go-querystring v1.1.0 // indirect
34 | github.com/google/s2a-go v0.1.9 // indirect
35 | github.com/google/uuid v1.6.0 // indirect
36 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
37 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect
38 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
39 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
40 | github.com/hetznercloud/hcloud-go v1.59.2 // indirect
41 | github.com/jmespath/go-jmespath v0.4.0 // indirect
42 | github.com/klauspost/compress v1.17.11 // indirect
43 | github.com/kylelemons/godebug v1.1.0 // indirect
44 | github.com/linode/linodego v1.46.0 // indirect
45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
46 | github.com/ovh/go-ovh v1.6.0 // indirect
47 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
48 | github.com/pkg/errors v0.9.1 // indirect
49 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
50 | github.com/prometheus/client_golang v1.20.5 // indirect
51 | github.com/prometheus/client_model v0.6.1 // indirect
52 | github.com/prometheus/common v0.61.0 // indirect
53 | github.com/prometheus/procfs v0.15.1 // indirect
54 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
55 | github.com/sethvargo/go-password v0.3.1 // indirect
56 | github.com/vultr/govultr/v2 v2.17.2 // indirect
57 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect
58 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
59 | go.opentelemetry.io/otel v1.33.0 // indirect
60 | go.opentelemetry.io/otel/metric v1.33.0 // indirect
61 | go.opentelemetry.io/otel/trace v1.33.0 // indirect
62 | golang.org/x/crypto v0.32.0 // indirect
63 | golang.org/x/net v0.34.0 // indirect
64 | golang.org/x/oauth2 v0.25.0 // indirect
65 | golang.org/x/sys v0.29.0 // indirect
66 | golang.org/x/text v0.21.0 // indirect
67 | golang.org/x/time v0.9.0 // indirect
68 | google.golang.org/api v0.217.0 // indirect
69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
70 | google.golang.org/grpc v1.69.4 // indirect
71 | google.golang.org/protobuf v1.36.3 // indirect
72 | gopkg.in/ini.v1 v1.67.0 // indirect
73 | gopkg.in/yaml.v2 v2.4.0 // indirect
74 | )
75 |
--------------------------------------------------------------------------------
/cmd/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
2 | cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
3 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
4 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
5 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
6 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
7 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
8 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
9 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
10 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
11 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
12 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
13 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
14 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
15 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 h1:/Di3vB4sNeQ+7A8efjUVENvyB945Wruvstucqp7ZArg=
16 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0/go.mod h1:gM3K25LQlsET3QR+4V74zxCsFAy0r6xMNN9n80SZn+4=
17 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ=
18 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0/go.mod h1:ceIuwmxDWptoW3eCqSXlnPsZFKh4X+R38dWPv7GS9Vs=
19 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
20 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
21 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
22 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
23 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA=
24 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
25 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
26 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
27 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
28 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
29 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
30 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
31 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
32 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
33 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
34 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
35 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
36 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
39 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
40 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
41 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
42 | github.com/digitalocean/godo v1.134.0 h1:dT7aQR9jxNOQEZwzP+tAYcxlj5szFZScC33+PAYGQVM=
43 | github.com/digitalocean/godo v1.134.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
44 | github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
45 | github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
46 | github.com/dirien/ovh-go-sdk v0.2.0 h1:hIL39yxXnUNEUw1gn3g2cA9QKj2cHdbbowAyq8tHug4=
47 | github.com/dirien/ovh-go-sdk v0.2.0/go.mod h1:kz6dmFoAym8NbdVTdGRzQuTGfRNoMrSuevxvxxBPVjA=
48 | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
49 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
50 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
51 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
52 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
53 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
54 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
55 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
56 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
57 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
58 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
59 | github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
60 | github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
61 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
62 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
63 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
64 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
65 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
66 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
67 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
68 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
69 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
70 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
71 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
72 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
73 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
74 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
75 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
76 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
77 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
78 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
79 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
80 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
81 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
82 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
83 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
84 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
85 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
86 | github.com/hetznercloud/hcloud-go v1.59.2 h1:NkCPwYiPv85FnOV3IW9/gxfW61TPIUSwyPHRSLwCkHA=
87 | github.com/hetznercloud/hcloud-go v1.59.2/go.mod h1:oTebZCjd+osj75jlI76Z+zjN1sTxmMiQ1MWoO8aRl1c=
88 | github.com/inlets/cloud-provision v0.7.0 h1:ci0OjyN+u9VE/SIuyt68yp4nn2+VOJQPnz0DA2MsEmM=
89 | github.com/inlets/cloud-provision v0.7.0/go.mod h1:UE/q1bx3Bl0Kxxsrgn4TmpC+PiHanolR7Y8PJihVHfI=
90 | github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
91 | github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
92 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
93 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
94 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
95 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
96 | github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
97 | github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
98 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
99 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
100 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
101 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
102 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
103 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
104 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
105 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
106 | github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU=
107 | github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk=
108 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
109 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
110 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
111 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
112 | github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
113 | github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
114 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
115 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
116 | github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
117 | github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
118 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
119 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
120 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
121 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
122 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
123 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
124 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
125 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
126 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
127 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
128 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
129 | github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
130 | github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
131 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
132 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
133 | github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
134 | github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
135 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
136 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
137 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
138 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
139 | github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
140 | github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
141 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
142 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
143 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
144 | github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
145 | github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
146 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
147 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
148 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
149 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
150 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
151 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
152 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
153 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
154 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
155 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
156 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
157 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
158 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
159 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
160 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
161 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
162 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
163 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
164 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
165 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
166 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
167 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
168 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
169 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
170 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
171 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
172 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
173 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
174 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
175 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
176 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
177 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
178 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
179 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
180 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
181 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
182 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
183 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
184 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
185 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
186 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
187 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
188 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
189 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
190 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
191 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
192 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
193 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
194 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
195 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
196 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
197 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
198 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
199 | google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU=
200 | google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI=
201 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
202 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
203 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
204 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
205 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
206 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
207 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
208 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
212 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
213 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
214 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
215 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
216 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
217 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
218 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
219 |
--------------------------------------------------------------------------------
/cmd/k3sup.txt:
--------------------------------------------------------------------------------
1 | #cloud-config
2 | ssh_authorized_keys:
3 | ## Note: Replace with your own public key
4 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD5P+iIgayz0nM6ra5y25KyKEPZyyRiXssOQ92GS7+JHKcerHndzbgWHUBH4Sc0HE/IeIFiZ20iYVBMLJZteXfrrEV91LNPApZ010RRNehzGIfPj0C/5bNVA3NtwsXWtz6O17goEPlblhcbJ1XoS5xdy4U2GMfaB8C9bZ0RvYFuXP+FJfgvJ9mp4MesLKMH/rKUc7uLIWCQWgrTLoTx1r+merWkQCiWU8onvfh+B7vXggY9ffOxADJ+JqXjEbG49CiXG3zJq2xnbiyf0zVvvG2Utr45lPm1cxbqch5BdJrZpIb8qSvjuV/oq6AUvUqpBei2YLZKz1sPKONkBB1t5e3Xa9+PYB19PUmn8/WUQYWGJ4LB5mTe87nfs1Q1p/cQe4pq+Y8s3rUitnqwv16g5CdUAXG8KPWlAXB+VK04cj1E3CYkEOUIeeeyUDlMPezrEFEKjDcqhaReDMjHMma95SeMjlt3ZrO2FsXjmgLfjv5kvsWFZdDjl3zQovVfs+pVzGk= alex@am1.local
5 | runcmd:
6 | - curl -SLs https://get.k3sup.dev | sh
7 | - k3sup install --local --k3s-channel stable
8 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "time"
9 |
10 | "github.com/inlets/cloud-provision/provision"
11 | )
12 |
13 | func main() {
14 | var (
15 | accessToken string
16 | userDataFile string
17 | userdata string
18 | hostname string
19 | region string
20 | )
21 |
22 | flag.StringVar(&accessToken, "access-token", "", "Access token for provisioning a host")
23 | flag.StringVar(&userDataFile, "userdata-file", "", "Apply user-data from a file to configure the host")
24 | flag.StringVar(&hostname, "hostname", "provision-example", "Name for the host")
25 | flag.StringVar(®ion, "region", "lon1", "Region for the host")
26 |
27 | flag.Parse()
28 |
29 | if len(accessToken) == 0 {
30 | fmt.Fprintf(os.Stderr, "--access-token required\n")
31 | os.Exit(1)
32 | }
33 |
34 | provisioner, err := provision.NewDigitalOceanProvisioner(accessToken)
35 | if err != nil {
36 | fmt.Fprintf(os.Stderr, "%s\n", err.Error())
37 | os.Exit(1)
38 | }
39 |
40 | if len(userDataFile) > 0 {
41 | res, err := ioutil.ReadFile(userDataFile)
42 | if err != nil {
43 | fmt.Fprintf(os.Stderr, "%s\n", err.Error())
44 | os.Exit(1)
45 | }
46 |
47 | userdata = string(res)
48 | }
49 |
50 | // Find examples here for other clouds -> https://github.com/inlets/inletsctl/blob/356886f41e7c48a9644a24532027d1defa1d69e8/cmd/create.go
51 | res, err := provisioner.Provision(provision.BasicHost{
52 | Name: hostname,
53 | OS: "ubuntu-18-04-x64",
54 | Plan: "s-1vcpu-1gb",
55 | Region: region,
56 | UserData: userdata,
57 | Additional: map[string]string{},
58 | })
59 |
60 | if err != nil {
61 | fmt.Fprintf(os.Stderr, "%s\n", err.Error())
62 | os.Exit(1)
63 | }
64 |
65 | fmt.Printf("Host ID: %s\n", res.ID)
66 |
67 | pollStatusAttempts := 250
68 | waitInterval := time.Second * 2
69 | for i := 0; i <= pollStatusAttempts; i++ {
70 | fmt.Printf("Polling status: %d/%d\n", i+1, pollStatusAttempts)
71 | res, err := provisioner.Status(res.ID)
72 |
73 | if err != nil {
74 | fmt.Fprintf(os.Stderr, "%s\n", err.Error())
75 | os.Exit(1)
76 | }
77 | if res.Status == provision.ActiveStatus {
78 | fmt.Printf("Your IP address is: %s\n", res.IP)
79 | break
80 | }
81 | time.Sleep(waitInterval)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/nginx.txt:
--------------------------------------------------------------------------------
1 | #cloud-config
2 | packages:
3 | - nginx
4 | runcmd:
5 | - systemctl enable nginx
6 | - systemctl start nginx
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/inlets/cloud-provision
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
8 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0
10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
11 | github.com/aws/aws-sdk-go v1.55.6
12 | github.com/digitalocean/godo v1.134.0
13 | github.com/dimchansky/utfbom v1.1.1
14 | github.com/dirien/ovh-go-sdk v0.2.0
15 | github.com/golang/mock v1.6.0
16 | github.com/google/uuid v1.6.0
17 | github.com/hetznercloud/hcloud-go v1.59.2
18 | github.com/linode/linodego v1.46.0
19 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30
20 | github.com/sethvargo/go-password v0.3.1
21 | github.com/vultr/govultr/v2 v2.17.2
22 | golang.org/x/oauth2 v0.25.0
23 | google.golang.org/api v0.217.0
24 | )
25 |
26 | require (
27 | cloud.google.com/go/auth v0.14.0 // indirect
28 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
29 | cloud.google.com/go/compute v1.31.1 // indirect
30 | cloud.google.com/go/compute/metadata v0.6.0 // indirect
31 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
32 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
33 | github.com/beorn7/perks v1.0.1 // indirect
34 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
35 | github.com/felixge/httpsnoop v1.0.4 // indirect
36 | github.com/go-logr/logr v1.4.2 // indirect
37 | github.com/go-logr/stdr v1.2.2 // indirect
38 | github.com/go-resty/resty/v2 v2.16.3 // indirect
39 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
40 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
41 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
42 | github.com/golang/protobuf v1.5.4 // indirect
43 | github.com/google/go-querystring v1.1.0 // indirect
44 | github.com/google/s2a-go v0.1.9 // indirect
45 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
46 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect
47 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
48 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
49 | github.com/jmespath/go-jmespath v0.4.0 // indirect
50 | github.com/klauspost/compress v1.17.11 // indirect
51 | github.com/kr/text v0.2.0 // indirect
52 | github.com/kylelemons/godebug v1.1.0 // indirect
53 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
54 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
55 | github.com/ovh/go-ovh v1.6.0 // indirect
56 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
57 | github.com/pkg/errors v0.9.1 // indirect
58 | github.com/prometheus/client_golang v1.20.5 // indirect
59 | github.com/prometheus/client_model v0.6.1 // indirect
60 | github.com/prometheus/common v0.61.0 // indirect
61 | github.com/prometheus/procfs v0.15.1 // indirect
62 | go.opencensus.io v0.24.0 // indirect
63 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect
64 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
65 | go.opentelemetry.io/otel v1.33.0 // indirect
66 | go.opentelemetry.io/otel/metric v1.33.0 // indirect
67 | go.opentelemetry.io/otel/trace v1.33.0 // indirect
68 | golang.org/x/crypto v0.32.0 // indirect
69 | golang.org/x/net v0.34.0 // indirect
70 | golang.org/x/sys v0.29.0 // indirect
71 | golang.org/x/text v0.21.0 // indirect
72 | golang.org/x/time v0.9.0 // indirect
73 | google.golang.org/appengine v1.6.8 // indirect
74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
75 | google.golang.org/grpc v1.69.4 // indirect
76 | google.golang.org/protobuf v1.36.3 // indirect
77 | gopkg.in/ini.v1 v1.67.0 // indirect
78 | gopkg.in/yaml.v2 v2.4.0 // indirect
79 | )
80 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
3 | cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
4 | cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
5 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
6 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
7 | cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
8 | cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
9 | cloud.google.com/go/compute v1.31.1 h1:SObuy8Fs6woazArpXp1fsHCw+ZH4iJ/8dGGTxUhHZQA=
10 | cloud.google.com/go/compute v1.31.1/go.mod h1:hyOponWhXviDptJCJSoEh89XO1cfv616wbwbkde1/+8=
11 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
12 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
13 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
14 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
15 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3 h1:8LoU8N2lIUzkmstvwXvVfniMZlFbesfT2AmA1aqvRr8=
16 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
17 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
18 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
19 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
20 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
21 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
22 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
23 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY=
24 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
25 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
26 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
27 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 h1:/Di3vB4sNeQ+7A8efjUVENvyB945Wruvstucqp7ZArg=
28 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0/go.mod h1:gM3K25LQlsET3QR+4V74zxCsFAy0r6xMNN9n80SZn+4=
29 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ=
30 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0/go.mod h1:ceIuwmxDWptoW3eCqSXlnPsZFKh4X+R38dWPv7GS9Vs=
31 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 h1:nBy98uKOIfun5z6wx6jwWLrULcM0+cjBalBFZlEZ7CA=
32 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
33 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA=
34 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
35 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
36 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
37 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
38 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
39 | github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE=
40 | github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
41 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
42 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
43 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
44 | github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
45 | github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
46 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
47 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
48 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
49 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
50 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
51 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
52 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
53 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
54 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
55 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
56 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
57 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
58 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
59 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
60 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
61 | github.com/digitalocean/godo v1.108.0 h1:fWyMENvtxpCpva1UbKzOFnyAS04N1FNuBWWfPeTGquQ=
62 | github.com/digitalocean/godo v1.108.0/go.mod h1:R6EmmWI8CT1+fCtjWY9UCB+L5uufuZH13wk3YhxycCs=
63 | github.com/digitalocean/godo v1.134.0 h1:dT7aQR9jxNOQEZwzP+tAYcxlj5szFZScC33+PAYGQVM=
64 | github.com/digitalocean/godo v1.134.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
65 | github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
66 | github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
67 | github.com/dirien/ovh-go-sdk v0.2.0 h1:hIL39yxXnUNEUw1gn3g2cA9QKj2cHdbbowAyq8tHug4=
68 | github.com/dirien/ovh-go-sdk v0.2.0/go.mod h1:kz6dmFoAym8NbdVTdGRzQuTGfRNoMrSuevxvxxBPVjA=
69 | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
70 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
71 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
72 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
73 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
74 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
75 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
76 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
77 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
78 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
79 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
80 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
81 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
82 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
83 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
84 | github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY=
85 | github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
86 | github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
87 | github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
88 | github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
89 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
90 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
91 | github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
92 | github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
93 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
94 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
95 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
96 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
97 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
98 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
99 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
100 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
101 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
102 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
103 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
104 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
105 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
106 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
107 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
108 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
109 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
110 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
111 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
112 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
113 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
114 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
115 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
116 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
117 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
118 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
119 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
120 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
121 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
122 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
123 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
124 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
125 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
126 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
127 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
128 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
129 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
130 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
131 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
132 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
133 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
134 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
135 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
136 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
137 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
138 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
139 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
140 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
141 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
142 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
143 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
144 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
145 | github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
146 | github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
147 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
148 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
149 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
150 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
151 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
152 | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
153 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
154 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
155 | github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
156 | github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
157 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
158 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
159 | github.com/hetznercloud/hcloud-go v1.39.0 h1:RUlzI458nGnPR6dlcZlrsGXYC1hQlFbKdm8tVtEQQB0=
160 | github.com/hetznercloud/hcloud-go v1.39.0/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA=
161 | github.com/hetznercloud/hcloud-go v1.59.2 h1:NkCPwYiPv85FnOV3IW9/gxfW61TPIUSwyPHRSLwCkHA=
162 | github.com/hetznercloud/hcloud-go v1.59.2/go.mod h1:oTebZCjd+osj75jlI76Z+zjN1sTxmMiQ1MWoO8aRl1c=
163 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
164 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
165 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
166 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
167 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
168 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
169 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
170 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
171 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
172 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
173 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
174 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
175 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
176 | github.com/linode/linodego v1.12.0 h1:33mOIrZ+gVva14gyJMKPZ85mQGovAvZCEP1ftgmFBjA=
177 | github.com/linode/linodego v1.12.0/go.mod h1:NJlzvlNtdMRRkXb0oN6UWzUkj6t+IBsyveHgZ5Ppjyk=
178 | github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU=
179 | github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk=
180 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
181 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
182 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
183 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
184 | github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
185 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
186 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
187 | github.com/ovh/go-ovh v1.3.0 h1:mvZaddk4E4kLcXhzb+cxBsMPYp2pHqiQpWYkInsuZPQ=
188 | github.com/ovh/go-ovh v1.3.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
189 | github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
190 | github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
191 | github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
192 | github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
193 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
194 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
195 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
196 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
197 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
199 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
200 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
201 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
202 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
203 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
204 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
205 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
206 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
207 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
208 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
209 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
210 | github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
211 | github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
212 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
213 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
214 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
215 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
216 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
217 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
218 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
219 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12 h1:Aaz4T7dZp7cB2cv7D/tGtRdSMh48sRaDYr7Jh0HV4qQ=
220 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
221 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
222 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
223 | github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
224 | github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
225 | github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
226 | github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
227 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
228 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
229 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
230 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
231 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
232 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
233 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
234 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
235 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
236 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
237 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
238 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
239 | github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
240 | github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
241 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
242 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
243 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
244 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
245 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
246 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
247 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
248 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
249 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
250 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
251 | go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
252 | go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
253 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
254 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
255 | go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
256 | go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
257 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
258 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
259 | go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
260 | go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
261 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
262 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
263 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
264 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
265 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
266 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
267 | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
268 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
269 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
270 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
271 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
272 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
273 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
274 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
275 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
276 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
277 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
278 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
279 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
280 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
281 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
282 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
283 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
284 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
285 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
286 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
287 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
288 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
289 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
290 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
291 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
292 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
293 | golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
294 | golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
295 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
296 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
297 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
298 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
299 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
300 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
301 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
302 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
303 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
304 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
305 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
306 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
307 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
308 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
309 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
310 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
311 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
312 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
313 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
314 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
315 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
316 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
317 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
318 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
319 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
320 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
321 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
322 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
323 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
324 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
325 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
326 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
327 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
328 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
329 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
330 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
331 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
332 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
333 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
334 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
335 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
336 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
337 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
338 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
339 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
340 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
341 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
342 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
343 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
344 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
345 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
346 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
347 | google.golang.org/api v0.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20=
348 | google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g=
349 | google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU=
350 | google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI=
351 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
352 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
353 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
354 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
355 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
356 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
357 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
358 | google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg=
359 | google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0=
360 | google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
361 | google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
362 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
363 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
364 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
365 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
366 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
367 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
368 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
369 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
370 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
371 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
372 | google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
373 | google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
374 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
375 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
376 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
377 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
378 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
379 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
380 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
381 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
382 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
383 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
384 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
385 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
386 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
387 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
388 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
389 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
390 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
391 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
392 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
393 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
394 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
395 | gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
396 | gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
397 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
398 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
399 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
400 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
401 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
402 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
403 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
404 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
405 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
406 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
407 |
--------------------------------------------------------------------------------
/provision/azure.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/binary"
7 | "encoding/json"
8 | "fmt"
9 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
10 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
11 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
12 | "io/ioutil"
13 | "log"
14 | "os"
15 | "strings"
16 | "unicode/utf16"
17 |
18 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
19 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
20 | "github.com/dimchansky/utfbom"
21 | "github.com/google/uuid"
22 | "github.com/sethvargo/go-password/password"
23 | )
24 |
25 | const AzureStatusSucceeded = "Succeeded"
26 |
27 | type AzureProvisioner struct {
28 | subscriptionId string
29 | resourceGroupName string
30 | deploymentName string
31 | azidentityCredential *azidentity.EnvironmentCredential
32 | ctx context.Context
33 | }
34 |
35 | var fileToEnvMap = map[string]string{
36 | "subscriptionId": "AZURE_SUBSCRIPTION_ID",
37 | "tenantId": "AZURE_TENANT_ID",
38 | "auxiliaryTenantIds": "AZURE_AUXILIARY_TENANT_IDS",
39 | "clientId": "AZURE_CLIENT_ID",
40 | "clientSecret": "AZURE_CLIENT_SECRET",
41 | "certificatePath": "AZURE_CERTIFICATE_PATH",
42 | "certificatePassword": "AZURE_CERTIFICATE_PASSWORD",
43 | "username": "AZURE_USERNAME",
44 | "password": "AZURE_PASSWORD",
45 | "environmentName": "AZURE_ENVIRONMENT",
46 | "resource": "AZURE_AD_RESOURCE",
47 | "activeDirectoryEndpointUrl": "ActiveDirectoryEndpoint",
48 | "resourceManagerEndpointUrl": "ResourceManagerEndpoint",
49 | "graphResourceId": "GraphResourceID",
50 | "sqlManagementEndpointUrl": "SQLManagementEndpoint",
51 | "galleryEndpointUrl": "GalleryEndpoint",
52 | "managementEndpointUrl": "ManagementEndpoint",
53 | }
54 |
55 | // buildAzureHostID creates an ID for Azure based upon the group name,
56 | // and deployment name
57 | func buildAzureHostID(groupName, deploymentName string) (id string) {
58 | return fmt.Sprintf("%s|%s", groupName, deploymentName)
59 | }
60 |
61 | // get some required fields from the custom Azure host ID
62 | func getAzureFieldsFromID(id string) (groupName, deploymentName string, err error) {
63 | fields := strings.Split(id, "|")
64 | err = nil
65 | if len(fields) == 2 {
66 | groupName = fields[0]
67 | deploymentName = fields[1]
68 | } else {
69 | err = fmt.Errorf("could not get fields from custom ID: fields: %v", fields)
70 | return "", "", err
71 | }
72 | return groupName, deploymentName, nil
73 | }
74 |
75 | // In case azure auth file is encoded as UTF-16 instead of UTF-8
76 | func decodeAzureAuthContents(b []byte) ([]byte, error) {
77 | reader, enc := utfbom.Skip(bytes.NewReader(b))
78 |
79 | switch enc {
80 | case utfbom.UTF16LittleEndian:
81 | u16 := make([]uint16, (len(b)/2)-1)
82 | err := binary.Read(reader, binary.LittleEndian, &u16)
83 | if err != nil {
84 | return nil, err
85 | }
86 | return []byte(string(utf16.Decode(u16))), nil
87 | case utfbom.UTF16BigEndian:
88 | u16 := make([]uint16, (len(b)/2)-1)
89 | err := binary.Read(reader, binary.BigEndian, &u16)
90 | if err != nil {
91 | return nil, err
92 | }
93 | return []byte(string(utf16.Decode(u16))), nil
94 | }
95 | return ioutil.ReadAll(reader)
96 | }
97 |
98 | func NewAzureProvisioner(subscriptionId, authFileContents string) (*AzureProvisioner, error) {
99 | decodedAuthContents, err := decodeAzureAuthContents([]byte(authFileContents))
100 | if err != nil {
101 | log.Printf("Failed to decode auth contents: '%s', error: '%s'", authFileContents, err.Error())
102 | return nil, err
103 | }
104 | authMap := map[string]string{}
105 | err = json.Unmarshal(decodedAuthContents, &authMap)
106 | if err != nil {
107 | log.Printf("Failed to parse auth contents: '%s', error: '%s'", authFileContents, err.Error())
108 | return nil, err
109 | }
110 | for fileKey, envKey := range fileToEnvMap {
111 | err := os.Setenv(envKey, authMap[fileKey])
112 | if err != nil {
113 | log.Printf("Failed to set env: '%s', error: '%s'", fileKey, err.Error())
114 | }
115 | }
116 | credential, err := azidentity.NewEnvironmentCredential(nil)
117 | ctx := context.Background()
118 | return &AzureProvisioner{
119 | subscriptionId: subscriptionId,
120 | azidentityCredential: credential,
121 | ctx: ctx,
122 | }, err
123 | }
124 |
125 | // Provision provisions a new Azure instance as an exit node
126 | func (p *AzureProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
127 |
128 | log.Printf("Provisioning host with Azure\n")
129 |
130 | p.resourceGroupName = "inlets-" + host.Name
131 | p.deploymentName = "deployment-" + uuid.New().String()
132 |
133 | log.Printf("Creating resource group %s", p.resourceGroupName)
134 | group, err := createGroup(p, host)
135 | if err != nil {
136 | return nil, err
137 | }
138 | log.Printf("Resource group created %s", *group.Name)
139 |
140 | log.Printf("Creating deployment %s", p.deploymentName)
141 | err = createDeployment(p, host)
142 | if err != nil {
143 | return nil, err
144 | }
145 | return &ProvisionedHost{
146 | IP: "",
147 | ID: buildAzureHostID(p.resourceGroupName, p.deploymentName),
148 | Status: ActiveStatus,
149 | }, nil
150 | }
151 |
152 | // Status checks the status of the provisioning Azure exit node
153 | func (p *AzureProvisioner) Status(id string) (*ProvisionedHost, error) {
154 | deploymentsClient, err := armresources.NewDeploymentsClient(p.subscriptionId, p.azidentityCredential, nil)
155 | if err != nil {
156 | return nil, err
157 | }
158 |
159 | resourceGroupName, deploymentName, err := getAzureFieldsFromID(id)
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | deployment, err := deploymentsClient.Get(p.ctx, resourceGroupName, deploymentName, nil)
165 | if err != nil {
166 | return nil, err
167 | }
168 | var deploymentStatus string
169 | if *deployment.Properties.ProvisioningState == AzureStatusSucceeded {
170 | deploymentStatus = ActiveStatus
171 | } else {
172 | deploymentStatus = string(*deployment.Properties.ProvisioningState)
173 | }
174 | IP := ""
175 | if deploymentStatus == ActiveStatus {
176 | IP = deployment.Properties.Outputs.(map[string]interface{})["publicIP"].(map[string]interface{})["value"].(string)
177 | }
178 | return &ProvisionedHost{
179 | IP: IP,
180 | ID: id,
181 | Status: deploymentStatus,
182 | }, nil
183 | }
184 |
185 | // Delete deletes the Azure exit node
186 | func (p *AzureProvisioner) Delete(request HostDeleteRequest) error {
187 | groupsClient, err := armresources.NewResourceGroupsClient(p.subscriptionId, p.azidentityCredential, nil)
188 | if err != nil {
189 | return err
190 | }
191 | resourceGroupName, _, err := getAzureFieldsFromID(request.ID)
192 | if err != nil {
193 | return err
194 | }
195 | _, err = groupsClient.BeginDelete(p.ctx, resourceGroupName, nil)
196 | return err
197 | }
198 |
199 | func createGroup(p *AzureProvisioner, host BasicHost) (*armresources.ResourceGroup, error) {
200 | groupsClient, err := armresources.NewResourceGroupsClient(p.subscriptionId, p.azidentityCredential, nil)
201 | if err != nil {
202 | return nil, err
203 | }
204 | resourceGroupResp, err := groupsClient.CreateOrUpdate(
205 | p.ctx,
206 | p.resourceGroupName,
207 | armresources.ResourceGroup{Location: to.Ptr(host.Region)}, nil)
208 |
209 | if err != nil {
210 | return nil, err
211 | }
212 | return &resourceGroupResp.ResourceGroup, nil
213 | }
214 |
215 | func getSecurityRuleList(host BasicHost) []interface{} {
216 | var rules []interface{}
217 | if host.Additional["pro"] == "true" {
218 | rules = []interface{}{
219 | getSecurityRule("AllPorts", 280, "TCP", "*"),
220 | }
221 | } else {
222 | rules = []interface{}{
223 | getSecurityRule("HTTPS", 320, "TCP", "443"),
224 | getSecurityRule("HTTP", 340, "TCP", "80"),
225 | getSecurityRule("HTTP8080", 360, "TCP", "8080"),
226 | }
227 | }
228 | return rules
229 | }
230 |
231 | func getSecurityRule(name string, priority int, protocol, destPortRange string) map[string]interface{} {
232 | return map[string]interface{}{
233 | "name": name,
234 | "properties": map[string]interface{}{
235 | "priority": priority,
236 | "protocol": protocol,
237 | "access": "Allow",
238 | "direction": "Inbound",
239 | "sourceAddressPrefix": "*",
240 | "sourcePortRange": "*",
241 | "destinationAddressPrefix": "*",
242 | "destinationPortRange": destPortRange,
243 | },
244 | }
245 | }
246 |
247 | func azureParameterType(typeName string) map[string]interface{} {
248 | return map[string]interface{}{
249 | "type": typeName,
250 | }
251 | }
252 |
253 | func azureParameterValue(typeValue string) map[string]interface{} {
254 | return map[string]interface{}{
255 | "value": typeValue,
256 | }
257 | }
258 |
259 | func getTemplateParameterDefinition() map[string]interface{} {
260 | return map[string]interface{}{
261 | "location": azureParameterType("string"),
262 | "networkInterfaceName": azureParameterType("string"),
263 | "networkSecurityGroupName": azureParameterType("string"),
264 | "networkSecurityGroupRules": azureParameterType("array"),
265 | "subnetName": azureParameterType("string"),
266 | "virtualNetworkName": azureParameterType("string"),
267 | "addressPrefixes": azureParameterType("array"),
268 | "subnets": azureParameterType("array"),
269 | "publicIpAddressName": azureParameterType("string"),
270 | "virtualMachineName": azureParameterType("string"),
271 | "virtualMachineRG": azureParameterType("string"),
272 | "osDiskType": azureParameterType("string"),
273 | "virtualMachineSize": azureParameterType("string"),
274 | "adminUsername": azureParameterType("string"),
275 | "adminPassword": azureParameterType("secureString"),
276 | "customData": azureParameterType("string"),
277 | }
278 | }
279 |
280 | func getTemplateResourceVirtualMachine(host BasicHost) map[string]interface{} {
281 | return map[string]interface{}{
282 | "name": "[parameters('virtualMachineName')]",
283 | "type": "Microsoft.Compute/virtualMachines",
284 | "apiVersion": "2019-07-01",
285 | "location": "[parameters('location')]",
286 | "dependsOn": []interface{}{
287 | "[concat('Microsoft.Network/networkInterfaces/', parameters('networkInterfaceName'))]",
288 | },
289 | "properties": map[string]interface{}{
290 | "hardwareProfile": map[string]interface{}{
291 | "vmSize": "[parameters('virtualMachineSize')]",
292 | },
293 | "storageProfile": map[string]interface{}{
294 | "osDisk": map[string]interface{}{
295 | "createOption": "fromImage",
296 | "managedDisk": map[string]interface{}{
297 | "storageAccountType": "[parameters('osDiskType')]",
298 | },
299 | },
300 | "imageReference": map[string]interface{}{
301 | "publisher": host.Additional["imagePublisher"],
302 | "offer": host.Additional["imageOffer"],
303 | "sku": host.Additional["imageSku"],
304 | "version": host.Additional["imageVersion"],
305 | },
306 | },
307 | "networkProfile": map[string]interface{}{
308 | "networkInterfaces": []interface{}{
309 | map[string]interface{}{
310 | "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaceName'))]",
311 | },
312 | },
313 | },
314 | "osProfile": map[string]interface{}{
315 | "computerName": "[parameters('virtualMachineName')]",
316 | "adminUsername": "[parameters('adminUsername')]",
317 | "adminPassword": "[parameters('adminPassword')]",
318 | "customData": "[base64(parameters('customData'))]",
319 | },
320 | },
321 | }
322 | }
323 |
324 | func getTemplateResourceNetworkInterface() map[string]interface{} {
325 | return map[string]interface{}{
326 | "name": "[parameters('networkInterfaceName')]",
327 | "type": "Microsoft.Network/networkInterfaces",
328 | "apiVersion": "2019-07-01",
329 | "location": "[parameters('location')]",
330 | "dependsOn": []interface{}{
331 | "[concat('Microsoft.Network/networkSecurityGroups/', parameters('networkSecurityGroupName'))]",
332 | "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]",
333 | "[concat('Microsoft.Network/publicIpAddresses/', parameters('publicIpAddressName'))]",
334 | },
335 | "properties": map[string]interface{}{
336 | "ipConfigurations": []interface{}{
337 | map[string]interface{}{
338 | "name": "ipconfig1",
339 | "properties": map[string]interface{}{
340 | "subnet": map[string]interface{}{
341 | "id": "[variables('subnetRef')]",
342 | },
343 | "privateIPAllocationMethod": "Dynamic",
344 | "publicIpAddress": map[string]interface{}{
345 | "id": "[resourceId(resourceGroup().name, 'Microsoft.Network/publicIpAddresses', parameters('publicIpAddressName'))]",
346 | },
347 | },
348 | },
349 | },
350 | "networkSecurityGroup": map[string]interface{}{
351 | "id": "[variables('nsgId')]",
352 | },
353 | },
354 | }
355 | }
356 |
357 | func getTemplate(host BasicHost) map[string]interface{} {
358 | return map[string]interface{}{
359 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
360 | "contentVersion": "1.0.0.0",
361 | "parameters": getTemplateParameterDefinition(),
362 | "variables": map[string]interface{}{
363 | "nsgId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
364 | "vnetId": "[resourceId(resourceGroup().name,'Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]",
365 | "subnetRef": "[concat(variables('vnetId'), '/subnets/', parameters('subnetName'))]",
366 | },
367 | "resources": []interface{}{
368 | getTemplateResourceNetworkInterface(),
369 | map[string]interface{}{
370 | "name": "[parameters('networkSecurityGroupName')]",
371 | "type": "Microsoft.Network/networkSecurityGroups",
372 | "apiVersion": "2019-02-01",
373 | "location": host.Region,
374 | "properties": map[string]interface{}{
375 | "securityRules": "[parameters('networkSecurityGroupRules')]",
376 | },
377 | },
378 | map[string]interface{}{
379 | "name": "[parameters('virtualNetworkName')]",
380 | "type": "Microsoft.Network/virtualNetworks",
381 | "apiVersion": "2019-04-01",
382 | "location": host.Region,
383 | "properties": map[string]interface{}{
384 | "addressSpace": map[string]interface{}{
385 | "addressPrefixes": "[parameters('addressPrefixes')]",
386 | },
387 | "subnets": "[parameters('subnets')]",
388 | },
389 | },
390 | map[string]interface{}{
391 | "name": "[parameters('publicIpAddressName')]",
392 | "type": "Microsoft.Network/publicIpAddresses",
393 | "apiVersion": "2019-02-01",
394 | "location": host.Region,
395 | "properties": map[string]interface{}{
396 | "publicIpAllocationMethod": armnetwork.IPAllocationMethodStatic,
397 | },
398 | "sku": map[string]interface{}{
399 | "name": armnetwork.PublicIPAddressSKUNameBasic,
400 | },
401 | },
402 | getTemplateResourceVirtualMachine(host),
403 | },
404 | "outputs": map[string]interface{}{
405 | "adminUsername": map[string]interface{}{
406 | "type": "string",
407 | "value": "[parameters('adminUsername')]",
408 | },
409 | "publicIP": map[string]interface{}{
410 | "type": "string",
411 | "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressName')), '2019-02-01', 'Full').properties.ipAddress]",
412 | // See also https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions-resource#reference
413 | // and https://docs.microsoft.com/en-us/azure/templates/microsoft.network/2019-02-01/publicipaddresses
414 | },
415 | },
416 | }
417 | }
418 |
419 | func getParameters(p *AzureProvisioner, host BasicHost) (parameters map[string]interface{}) {
420 | return map[string]interface{}{
421 | "location": azureParameterValue(host.Region),
422 | "networkInterfaceName": azureParameterValue("inlets-vm-nic"),
423 | "networkSecurityGroupName": azureParameterValue("inlets-vm-nsg"),
424 | "networkSecurityGroupRules": map[string]interface{}{
425 | "value": getSecurityRuleList(host),
426 | },
427 | "subnetName": azureParameterValue("default"),
428 | "virtualNetworkName": azureParameterValue("inlets-vnet"),
429 | "addressPrefixes": map[string]interface{}{
430 | "value": []interface{}{
431 | "10.0.0.0/24",
432 | },
433 | },
434 | "subnets": map[string]interface{}{
435 | "value": []interface{}{
436 | map[string]interface{}{
437 | "name": "default",
438 | "properties": map[string]interface{}{
439 | "addressPrefix": "10.0.0.0/24",
440 | },
441 | },
442 | },
443 | },
444 | "publicIpAddressName": azureParameterValue("inlets-ip"),
445 | "virtualMachineName": azureParameterValue(host.Name),
446 | "virtualMachineRG": azureParameterValue(p.resourceGroupName),
447 | "osDiskType": map[string]interface{}{
448 | "value": armcompute.StorageAccountTypesStandardLRS,
449 | },
450 | "virtualMachineSize": azureParameterValue(host.Plan),
451 | "adminUsername": azureParameterValue("inletsuser"),
452 | "adminPassword": azureParameterValue(host.Additional["adminPassword"]),
453 | "customData": azureParameterValue(host.UserData),
454 | }
455 | }
456 |
457 | func createDeployment(p *AzureProvisioner, host BasicHost) (err error) {
458 | adminPassword, err := password.Generate(16, 4, 0, false, true)
459 | if err != nil {
460 | return
461 | }
462 | host.Additional["adminPassword"] = adminPassword
463 | template := getTemplate(host)
464 | params := getParameters(p, host)
465 | deploymentsClient, err := armresources.NewDeploymentsClient(p.subscriptionId, p.azidentityCredential, nil)
466 | if err != nil {
467 | return
468 | }
469 |
470 | _, err = deploymentsClient.BeginCreateOrUpdate(
471 | p.ctx,
472 | p.resourceGroupName,
473 | p.deploymentName,
474 | armresources.Deployment{
475 | Properties: &armresources.DeploymentProperties{
476 | Template: template,
477 | Parameters: params,
478 | Mode: to.Ptr(armresources.DeploymentModeComplete),
479 | },
480 | },
481 | nil,
482 | )
483 | return
484 | }
485 |
--------------------------------------------------------------------------------
/provision/azure_test.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "testing"
8 | )
9 |
10 | func Test_Azure_Auth_Contents_Invalid(t *testing.T) {
11 | _, err := NewAzureProvisioner("SubscriptionID", "invalid contents")
12 |
13 | if err == nil {
14 | t.Fatalf("want: error, but got: nil")
15 | }
16 | }
17 |
18 | func Test_Azure_Build_Group_Deployment_Name(t *testing.T) {
19 | hostID := buildAzureHostID("inlets-peaceful-chaum1", "deployments-e589c808-3936-4bd9-b558-36640eb98cb0")
20 | if hostID != "inlets-peaceful-chaum1|deployments-e589c808-3936-4bd9-b558-36640eb98cb0" {
21 | t.Errorf("want: inlets-peaceful-chaum1|deployments-e589c808-3936-4bd9-b558-36640eb98cb, but got: %s", hostID)
22 | }
23 | }
24 |
25 | func Test_Azure_Parse_Group_Deployment_Name_Success(t *testing.T) {
26 | resourceGroupName, deploymentName, err := getAzureFieldsFromID("inlets-peaceful-chaum1|deployments-e589c808-3936-4bd9-b558-36640eb98cb0")
27 | if err != nil {
28 | t.Errorf("want: nil, but got: %s", err.Error())
29 | }
30 | if resourceGroupName != "inlets-peaceful-chaum1" {
31 | t.Errorf("want: inlets-peaceful-chaum1, but got: %s", resourceGroupName)
32 | }
33 | if deploymentName != "deployments-e589c808-3936-4bd9-b558-36640eb98cb0" {
34 | t.Errorf("want: deployments-e589c808-3936-4bd9-b558-36640eb98cb0, but got: %s", deploymentName)
35 | }
36 | }
37 |
38 | func Test_Azure_Parse_Group_Deployment_Name_Fail(t *testing.T) {
39 | _, _, err := getAzureFieldsFromID("INVALID_ID")
40 | if err == nil {
41 | t.Errorf("want: error, but got nil")
42 | }
43 | }
44 |
45 | func Test_Azure_Template(t *testing.T) {
46 | want := map[string]interface{}{
47 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
48 | "contentVersion": "1.0.0.0",
49 | "outputs": map[string]interface{}{
50 | "adminUsername": map[string]interface{}{
51 | "type": "string",
52 | "value": "[parameters('adminUsername')]",
53 | },
54 | "publicIP": map[string]interface{}{
55 | "type": "string",
56 | "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressName')), '2019-02-01', 'Full').properties.ipAddress]",
57 | },
58 | },
59 | "parameters": map[string]interface{}{
60 | "addressPrefixes": map[string]interface{}{
61 | "type": "array",
62 | },
63 | "adminPassword": map[string]interface{}{
64 | "type": "secureString",
65 | },
66 | "adminUsername": map[string]interface{}{
67 | "type": "string",
68 | },
69 | "customData": map[string]interface{}{
70 | "type": "string",
71 | },
72 | "location": map[string]interface{}{
73 | "type": "string",
74 | },
75 | "networkInterfaceName": map[string]interface{}{
76 | "type": "string",
77 | },
78 | "networkSecurityGroupName": map[string]interface{}{
79 | "type": "string",
80 | },
81 | "networkSecurityGroupRules": map[string]interface{}{
82 | "type": "array",
83 | },
84 | "osDiskType": map[string]interface{}{
85 | "type": "string",
86 | },
87 | "publicIpAddressName": map[string]interface{}{
88 | "type": "string",
89 | },
90 | "subnetName": map[string]interface{}{
91 | "type": "string",
92 | },
93 | "subnets": map[string]interface{}{
94 | "type": "array",
95 | },
96 | "virtualMachineName": map[string]interface{}{
97 | "type": "string",
98 | },
99 | "virtualMachineRG": map[string]interface{}{
100 | "type": "string",
101 | },
102 | "virtualMachineSize": map[string]interface{}{
103 | "type": "string",
104 | },
105 | "virtualNetworkName": map[string]interface{}{
106 | "type": "string",
107 | },
108 | },
109 | "resources": []interface{}{
110 | map[string]interface{}{
111 | "apiVersion": "2019-07-01",
112 | "dependsOn": []interface{}{
113 | "[concat('Microsoft.Network/networkSecurityGroups/', parameters('networkSecurityGroupName'))]",
114 | "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]",
115 | "[concat('Microsoft.Network/publicIpAddresses/', parameters('publicIpAddressName'))]",
116 | },
117 | "location": "[parameters('location')]",
118 | "name": "[parameters('networkInterfaceName')]",
119 | "properties": map[string]interface{}{
120 | "ipConfigurations": []interface{}{
121 | map[string]interface{}{
122 | "name": "ipconfig1",
123 | "properties": map[string]interface{}{
124 | "privateIPAllocationMethod": "Dynamic",
125 | "publicIpAddress": map[string]interface{}{
126 | "id": "[resourceId(resourceGroup().name, 'Microsoft.Network/publicIpAddresses', parameters('publicIpAddressName'))]",
127 | },
128 | "subnet": map[string]interface{}{
129 | "id": "[variables('subnetRef')]",
130 | },
131 | },
132 | },
133 | },
134 | "networkSecurityGroup": map[string]interface{}{
135 | "id": "[variables('nsgId')]",
136 | },
137 | },
138 | "type": "Microsoft.Network/networkInterfaces",
139 | },
140 | map[string]interface{}{
141 | "apiVersion": "2019-02-01",
142 | "location": "eastus",
143 | "name": "[parameters('networkSecurityGroupName')]",
144 | "properties": map[string]interface{}{
145 | "securityRules": "[parameters('networkSecurityGroupRules')]",
146 | },
147 | "type": "Microsoft.Network/networkSecurityGroups",
148 | },
149 | map[string]interface{}{
150 | "apiVersion": "2019-04-01",
151 | "location": "eastus",
152 | "name": "[parameters('virtualNetworkName')]",
153 | "properties": map[string]interface{}{
154 | "addressSpace": map[string]interface{}{
155 | "addressPrefixes": "[parameters('addressPrefixes')]",
156 | },
157 | "subnets": "[parameters('subnets')]",
158 | },
159 | "type": "Microsoft.Network/virtualNetworks",
160 | },
161 | map[string]interface{}{
162 | "apiVersion": "2019-02-01",
163 | "location": "eastus",
164 | "name": "[parameters('publicIpAddressName')]",
165 | "properties": map[string]interface{}{
166 | "publicIpAllocationMethod": "Static",
167 | },
168 | "sku": map[string]interface{}{
169 | "name": "Basic",
170 | },
171 | "type": "Microsoft.Network/publicIpAddresses",
172 | },
173 | map[string]interface{}{
174 | "apiVersion": "2019-07-01",
175 | "dependsOn": []interface{}{
176 | "[concat('Microsoft.Network/networkInterfaces/', parameters('networkInterfaceName'))]",
177 | },
178 | "location": "[parameters('location')]",
179 | "name": "[parameters('virtualMachineName')]",
180 | "properties": map[string]interface{}{
181 | "hardwareProfile": map[string]interface{}{
182 | "vmSize": "[parameters('virtualMachineSize')]",
183 | },
184 | "networkProfile": map[string]interface{}{
185 | "networkInterfaces": []interface{}{
186 | map[string]interface{}{
187 | "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaceName'))]",
188 | },
189 | },
190 | },
191 | "osProfile": map[string]interface{}{
192 | "adminPassword": "[parameters('adminPassword')]",
193 | "adminUsername": "[parameters('adminUsername')]",
194 | "computerName": "[parameters('virtualMachineName')]",
195 | "customData": "[base64(parameters('customData'))]",
196 | },
197 | "storageProfile": map[string]interface{}{
198 | "imageReference": map[string]interface{}{
199 | "offer": "UbuntuServer",
200 | "publisher": "Canonical",
201 | "sku": "16.04-LTS",
202 | "version": "latest",
203 | },
204 | "osDisk": map[string]interface{}{
205 | "createOption": "fromImage",
206 | "managedDisk": map[string]interface{}{
207 | "storageAccountType": "[parameters('osDiskType')]",
208 | },
209 | },
210 | },
211 | },
212 | "type": "Microsoft.Compute/virtualMachines",
213 | },
214 | },
215 | "variables": map[string]interface{}{
216 | "nsgId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
217 | "subnetRef": "[concat(variables('vnetId'), '/subnets/', parameters('subnetName'))]",
218 | "vnetId": "[resourceId(resourceGroup().name,'Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]",
219 | },
220 | }
221 | host := BasicHost{
222 | Name: "test",
223 | OS: "Additional.imageOffer",
224 | Plan: "Standard_B1ls",
225 | Region: "eastus",
226 | UserData: "",
227 | Additional: map[string]string{
228 | "inlets-port": "8080",
229 | "pro": "8123",
230 | "imagePublisher": "Canonical",
231 | "imageOffer": "UbuntuServer",
232 | "imageSku": "16.04-LTS",
233 | "imageVersion": "latest",
234 | },
235 | }
236 | template := getTemplate(host)
237 | templateBytes, _ := json.Marshal(template)
238 | wantBytes, _ := json.Marshal(want)
239 | if !bytes.Equal(wantBytes, templateBytes) {
240 | t.Errorf("want: %v, but got: %v", want, template)
241 | }
242 | }
243 |
244 | func Test_Azure_Parameters(t *testing.T) {
245 | want := map[string]interface{}{
246 | "addressPrefixes": map[string]interface{}{
247 | "value": []interface{}{
248 | "10.0.0.0/24",
249 | },
250 | },
251 | "adminPassword": map[string]interface{}{
252 | "value": "k9eY3m0RY7PYnFAs",
253 | },
254 | "adminUsername": map[string]interface{}{
255 | "value": "inletsuser",
256 | },
257 | "customData": map[string]interface{}{
258 | "value": "foo-bar-baz",
259 | },
260 | "location": map[string]interface{}{
261 | "value": "eastus",
262 | },
263 | "networkInterfaceName": map[string]interface{}{
264 | "value": "inlets-vm-nic",
265 | },
266 | "networkSecurityGroupName": map[string]interface{}{
267 | "value": "inlets-vm-nsg",
268 | },
269 | "networkSecurityGroupRules": map[string]interface{}{
270 | "value": []interface{}{
271 | map[string]interface{}{
272 | "name": "HTTPS",
273 | "properties": map[string]interface{}{
274 | "access": "Allow",
275 | "destinationAddressPrefix": "*",
276 | "destinationPortRange": "443",
277 | "direction": "Inbound",
278 | "priority": 320,
279 | "protocol": "TCP",
280 | "sourceAddressPrefix": "*",
281 | "sourcePortRange": "*",
282 | },
283 | },
284 | map[string]interface{}{
285 | "name": "HTTP",
286 | "properties": map[string]interface{}{
287 | "access": "Allow",
288 | "destinationAddressPrefix": "*",
289 | "destinationPortRange": "80",
290 | "direction": "Inbound",
291 | "priority": 340,
292 | "protocol": "TCP",
293 | "sourceAddressPrefix": "*",
294 | "sourcePortRange": "*",
295 | },
296 | },
297 | map[string]interface{}{
298 | "name": "HTTP8080",
299 | "properties": map[string]interface{}{
300 | "access": "Allow",
301 | "destinationAddressPrefix": "*",
302 | "destinationPortRange": "8080",
303 | "direction": "Inbound",
304 | "priority": 360,
305 | "protocol": "TCP",
306 | "sourceAddressPrefix": "*",
307 | "sourcePortRange": "*",
308 | },
309 | },
310 | },
311 | },
312 | "osDiskType": map[string]interface{}{
313 | "value": "Standard_LRS",
314 | },
315 | "publicIpAddressName": map[string]interface{}{
316 | "value": "inlets-ip",
317 | },
318 | "subnetName": map[string]interface{}{
319 | "value": "default",
320 | },
321 | "subnets": map[string]interface{}{
322 | "value": []interface{}{
323 | map[string]interface{}{
324 | "name": "default",
325 | "properties": map[string]interface{}{
326 | "addressPrefix": "10.0.0.0/24",
327 | },
328 | },
329 | },
330 | },
331 | "virtualMachineName": map[string]interface{}{
332 | "value": "peaceful-chaum1",
333 | },
334 | "virtualMachineRG": map[string]interface{}{
335 | "value": "inlets-peaceful-chaum1",
336 | },
337 | "virtualMachineSize": map[string]interface{}{
338 | "value": "Standard_B1ls",
339 | },
340 | "virtualNetworkName": map[string]interface{}{
341 | "value": "inlets-vnet",
342 | },
343 | }
344 | ctx := context.Background()
345 | provisioner := AzureProvisioner{
346 | subscriptionId: "",
347 | resourceGroupName: "inlets-peaceful-chaum1",
348 | azidentityCredential: nil,
349 | ctx: ctx,
350 | }
351 | host := BasicHost{
352 | Name: "peaceful-chaum1",
353 | OS: "Additional.imageOffer",
354 | Plan: "Standard_B1ls",
355 | Region: "eastus",
356 | UserData: "foo-bar-baz",
357 | Additional: map[string]string{
358 | "inlets-port": "8080",
359 | "pro": "8123",
360 | "imagePublisher": "Canonical",
361 | "imageOffer": "UbuntuServer",
362 | "imageSku": "16.04-LTS",
363 | "imageVersion": "latest",
364 | "adminPassword": "k9eY3m0RY7PYnFAs",
365 | },
366 | }
367 | parameters := getParameters(&provisioner, host)
368 | parametersBytes, _ := json.Marshal(parameters)
369 | wantBytes, _ := json.Marshal(want)
370 | if !bytes.Equal(parametersBytes, wantBytes) {
371 | t.Errorf("want: %v, but got: %v", want, parameters)
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/provision/digitalocean.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strconv"
8 |
9 | "github.com/digitalocean/godo"
10 | "golang.org/x/oauth2"
11 | )
12 |
13 | // DigitalOceanProvisioner provision a VM on digitalocean.com
14 | type DigitalOceanProvisioner struct {
15 | client *godo.Client
16 | }
17 |
18 | // NewDigitalOceanProvisioner with an accessKey
19 | func NewDigitalOceanProvisioner(accessKey string) (*DigitalOceanProvisioner, error) {
20 |
21 | tokenSource := &TokenSource{
22 | AccessToken: accessKey,
23 | }
24 | oauthClient := oauth2.NewClient(context.Background(), tokenSource)
25 | client := godo.NewClient(oauthClient)
26 |
27 | return &DigitalOceanProvisioner{
28 | client: client,
29 | }, nil
30 | }
31 |
32 | // Status returns the status of an exit node
33 | func (p *DigitalOceanProvisioner) Status(id string) (*ProvisionedHost, error) {
34 | sid, _ := strconv.Atoi(id)
35 |
36 | droplet, _, err := p.client.Droplets.Get(context.Background(), sid)
37 |
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | state := droplet.Status
43 |
44 | ip := ""
45 | for _, network := range droplet.Networks.V4 {
46 | if network.Type == "public" {
47 | ip = network.IPAddress
48 | }
49 | }
50 |
51 | return &ProvisionedHost{
52 | ID: id,
53 | Status: state,
54 | IP: ip,
55 | }, nil
56 | }
57 |
58 | // Delete terminates an exit node
59 | func (p *DigitalOceanProvisioner) Delete(request HostDeleteRequest) error {
60 | var id string
61 | var err error
62 | if len(request.ID) > 0 {
63 | id = request.ID
64 | } else {
65 | id, err = p.lookupID(request)
66 | if err != nil {
67 | return err
68 | }
69 | }
70 | sid, err := strconv.Atoi(id)
71 | if err != nil {
72 | return err
73 | }
74 | _, err = p.client.Droplets.Delete(context.Background(), sid)
75 | return err
76 | }
77 |
78 | // List returns a list of exit nodes
79 | func (p *DigitalOceanProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
80 | var inlets []*ProvisionedHost
81 | opt := &godo.ListOptions{}
82 | for {
83 | droplets, resp, err := p.client.Droplets.ListByTag(context.Background(), filter.Filter, opt)
84 | if err != nil {
85 | return inlets, err
86 | }
87 | for _, droplet := range droplets {
88 | publicIP, err := droplet.PublicIPv4()
89 | if err != nil {
90 | return inlets, err
91 | }
92 | host := &ProvisionedHost{
93 | IP: publicIP,
94 | ID: fmt.Sprintf("%d", droplet.ID),
95 | }
96 | inlets = append(inlets, host)
97 | }
98 | if resp.Links == nil || resp.Links.IsLastPage() {
99 | break
100 | }
101 | page, err := resp.Links.CurrentPage()
102 | if err != nil {
103 | return inlets, err
104 | }
105 | opt.Page = page + 1
106 | }
107 | return inlets, nil
108 | }
109 |
110 | func (p *DigitalOceanProvisioner) lookupID(request HostDeleteRequest) (string, error) {
111 | inlets, err := p.List(ListFilter{Filter: "inlets"})
112 | if err != nil {
113 | return "", err
114 | }
115 | for _, inlet := range inlets {
116 | if inlet.IP == request.IP {
117 | return inlet.ID, nil
118 | }
119 | }
120 | return "", fmt.Errorf("no host with ip: %s", request.IP)
121 | }
122 |
123 | // Provision creates an exit node
124 | func (p *DigitalOceanProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
125 |
126 | log.Printf("Provisioning host with DigitalOcean\n")
127 |
128 | if host.Region == "" {
129 | host.Region = "lon1"
130 | }
131 |
132 | createReq := &godo.DropletCreateRequest{
133 | Name: host.Name,
134 | Region: host.Region,
135 | Size: host.Plan,
136 | Image: godo.DropletCreateImage{
137 | Slug: host.OS,
138 | },
139 | Tags: []string{"inlets"},
140 | UserData: host.UserData,
141 | }
142 |
143 | droplet, _, err := p.client.Droplets.Create(context.Background(), createReq)
144 |
145 | if err != nil {
146 | return nil, err
147 | }
148 |
149 | return &ProvisionedHost{
150 | ID: fmt.Sprintf("%d", droplet.ID),
151 | }, nil
152 | }
153 |
154 | // TokenSource contains an access token
155 | type TokenSource struct {
156 | AccessToken string
157 | }
158 |
159 | // Token returns an oauth2 token
160 | func (t *TokenSource) Token() (*oauth2.Token, error) {
161 | token := &oauth2.Token{
162 | AccessToken: t.AccessToken,
163 | }
164 | return token, nil
165 | }
166 |
--------------------------------------------------------------------------------
/provision/ec2.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/aws/aws-sdk-go/aws/credentials"
9 |
10 | "github.com/aws/aws-sdk-go/aws"
11 | "github.com/aws/aws-sdk-go/aws/session"
12 | "github.com/aws/aws-sdk-go/service/ec2"
13 | "github.com/google/uuid"
14 | )
15 |
16 | // EC2Provisioner contains the EC2 client
17 | type EC2Provisioner struct {
18 | ec2Provisioner *ec2.EC2
19 | }
20 |
21 | // NewEC2Provisioner creates an EC2Provisioner and initialises an EC2 client
22 | func NewEC2Provisioner(region, accessKey, secretKey, sessionToken string) (*EC2Provisioner, error) {
23 | sess, err := session.NewSession(&aws.Config{
24 | Region: aws.String(region),
25 | Credentials: credentials.NewStaticCredentials(accessKey, secretKey, sessionToken),
26 | })
27 | svc := ec2.New(sess)
28 | return &EC2Provisioner{ec2Provisioner: svc}, err
29 | }
30 |
31 | // Provision deploys an exit node into AWS EC2
32 | func (p *EC2Provisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
33 | image, err := p.lookupAMI(host.OS)
34 | if err != nil {
35 | return nil, err
36 | }
37 | controlPort := 8123
38 |
39 | openHighPortsV := host.Additional["pro"]
40 |
41 | openHighPorts, _ := strconv.ParseBool(openHighPortsV)
42 |
43 | ports := host.Additional["ports"]
44 |
45 | keyName := host.Additional["key-name"]
46 |
47 | extraPorts, err := parsePorts(ports)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | var vpcID = host.Additional["vpc-id"]
53 | var subnetID = host.Additional["subnet-id"]
54 |
55 | groupID, name, err := p.createEC2SecurityGroup(vpcID, controlPort, openHighPorts, extraPorts)
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | var networkSpec = ec2.InstanceNetworkInterfaceSpecification{
61 | DeviceIndex: aws.Int64(int64(0)),
62 | AssociatePublicIpAddress: aws.Bool(true),
63 | DeleteOnTermination: aws.Bool(true),
64 | Groups: []*string{groupID},
65 | }
66 |
67 | if len(subnetID) > 0 {
68 | networkSpec.SubnetId = aws.String(subnetID)
69 | }
70 | runInput := &ec2.RunInstancesInput{
71 | ImageId: image,
72 | InstanceType: aws.String(host.Plan),
73 | MinCount: aws.Int64(1),
74 | MaxCount: aws.Int64(1),
75 | UserData: &host.UserData,
76 | NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
77 | &networkSpec,
78 | },
79 | }
80 |
81 | if len(keyName) > 0 {
82 | runInput.KeyName = aws.String(keyName)
83 | }
84 |
85 | runResult, err := p.ec2Provisioner.RunInstances(runInput)
86 | if err != nil {
87 | // clean up SG if there was an issue provisioning the EC2 instance
88 | input := ec2.DeleteSecurityGroupInput{
89 | GroupId: groupID,
90 | }
91 | _, sgErr := p.ec2Provisioner.DeleteSecurityGroup(&input)
92 | if sgErr != nil {
93 | return nil, fmt.Errorf("error provisioning ec2 instance: %v; error deleting SG: %v", err, sgErr)
94 | }
95 | return nil, err
96 | }
97 |
98 | if len(runResult.Instances) == 0 {
99 | return nil, fmt.Errorf("could not create host: %s", runResult.String())
100 | }
101 |
102 | // AE: not sure why this error isn't handled?
103 | _, err = p.ec2Provisioner.CreateTags(&ec2.CreateTagsInput{
104 | Resources: []*string{runResult.Instances[0].InstanceId},
105 | Tags: []*ec2.Tag{
106 | {
107 | Key: aws.String("Name"),
108 | Value: aws.String(*name),
109 | },
110 | {
111 | Key: aws.String("inlets"),
112 | Value: aws.String("exit-node"),
113 | },
114 | },
115 | })
116 |
117 | return &ProvisionedHost{
118 | ID: *runResult.Instances[0].InstanceId,
119 | Status: "creating",
120 | }, nil
121 | }
122 |
123 | // Status returns the ID, Status and IP of the exit node
124 | func (p *EC2Provisioner) Status(id string) (*ProvisionedHost, error) {
125 | var status string
126 | s, err := p.ec2Provisioner.DescribeInstanceStatus(&ec2.DescribeInstanceStatusInput{
127 | InstanceIds: []*string{aws.String(id)},
128 | })
129 | if err != nil {
130 | return nil, err
131 | }
132 | if len(s.InstanceStatuses) > 0 {
133 | if *s.InstanceStatuses[0].InstanceStatus.Status == "ok" {
134 | status = ActiveStatus
135 | } else {
136 | status = "initialising"
137 | }
138 | } else {
139 | status = "creating"
140 | }
141 |
142 | d, err := p.ec2Provisioner.DescribeInstances(&ec2.DescribeInstancesInput{
143 | InstanceIds: []*string{aws.String(id)},
144 | })
145 | if err != nil {
146 | return nil, err
147 | }
148 | if len(d.Reservations) == 0 {
149 | return nil, fmt.Errorf("cannot describe host: %s", id)
150 | }
151 |
152 | return &ProvisionedHost{
153 | ID: id,
154 | Status: status,
155 | IP: aws.StringValue(d.Reservations[0].Instances[0].PublicIpAddress),
156 | }, nil
157 | }
158 |
159 | // Delete removes the exit node
160 | func (p *EC2Provisioner) Delete(request HostDeleteRequest) error {
161 | var id string
162 | var err error
163 | if len(request.ID) > 0 {
164 | id = request.ID
165 | } else {
166 | id, err = p.lookupID(request)
167 | if err != nil {
168 | return err
169 | }
170 | }
171 |
172 | i, err := p.ec2Provisioner.DescribeInstances(&ec2.DescribeInstancesInput{
173 | InstanceIds: []*string{aws.String(id)},
174 | })
175 | if err != nil {
176 | return err
177 | }
178 | groups := i.Reservations[0].Instances[0].SecurityGroups
179 |
180 | _, err = p.ec2Provisioner.TerminateInstances(&ec2.TerminateInstancesInput{
181 | InstanceIds: []*string{aws.String(id)},
182 | })
183 | if err != nil {
184 | return err
185 | }
186 |
187 | // Instance has to be terminated before we can remove the security group
188 | err = p.ec2Provisioner.WaitUntilInstanceTerminated(&ec2.DescribeInstancesInput{
189 | InstanceIds: []*string{aws.String(id)},
190 | })
191 | if err != nil {
192 | return err
193 | }
194 |
195 | for _, group := range groups {
196 | _, err := p.ec2Provisioner.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
197 | GroupId: group.GroupId,
198 | })
199 | if err != nil {
200 | return err
201 | }
202 | }
203 |
204 | return nil
205 | }
206 |
207 | // List returns a list of exit nodes
208 | func (p *EC2Provisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
209 | var inlets []*ProvisionedHost
210 | var nextToken *string
211 | filterValues := strings.Split(filter.Filter, ",")
212 | for {
213 | instances, err := p.ec2Provisioner.DescribeInstances(&ec2.DescribeInstancesInput{
214 | Filters: []*ec2.Filter{
215 | {
216 | Name: aws.String(filterValues[0]),
217 | Values: []*string{aws.String(filterValues[1])},
218 | },
219 | },
220 | NextToken: nextToken,
221 | })
222 | if err != nil {
223 | return nil, err
224 | }
225 | for _, r := range instances.Reservations {
226 | for _, i := range r.Instances {
227 | if *i.State.Name != ec2.InstanceStateNameTerminated {
228 | host := &ProvisionedHost{
229 | ID: *i.InstanceId,
230 | }
231 | if i.PublicIpAddress != nil {
232 | host.IP = *i.PublicIpAddress
233 | }
234 | inlets = append(inlets, host)
235 | }
236 | }
237 | }
238 | nextToken = instances.NextToken
239 | if nextToken == nil {
240 | break
241 | }
242 | }
243 | return inlets, nil
244 | }
245 |
246 | func (p *EC2Provisioner) lookupID(request HostDeleteRequest) (string, error) {
247 | inlets, err := p.List(ListFilter{
248 | Filter: "tag:inlets,exit-node",
249 | ProjectID: request.ProjectID,
250 | Zone: request.Zone,
251 | })
252 | if err != nil {
253 | return "", err
254 | }
255 |
256 | for _, inlet := range inlets {
257 | if inlet.IP == request.IP {
258 | return inlet.ID, nil
259 | }
260 | }
261 | return "", fmt.Errorf("no host with ip: %s", request.IP)
262 | }
263 |
264 | // createEC2SecurityGroup creates a security group for the exit-node
265 | func (p *EC2Provisioner) createEC2SecurityGroup(vpcID string, controlPort int, openHighPorts bool, extraPorts []int) (*string, *string, error) {
266 | ports := []int{controlPort}
267 |
268 | highPortRange := []int{1024, 65535}
269 |
270 | if len(extraPorts) > 0 {
271 | // disable high port range if extra ports are specified
272 | highPortRange = []int{}
273 |
274 | ports = append(ports, extraPorts...)
275 | }
276 |
277 | groupName := "inlets-" + uuid.New().String()
278 | var input = &ec2.CreateSecurityGroupInput{
279 | Description: aws.String("inlets security group"),
280 | GroupName: aws.String(groupName),
281 | }
282 |
283 | if len(vpcID) > 0 {
284 | input.VpcId = aws.String(vpcID)
285 | }
286 |
287 | group, err := p.ec2Provisioner.CreateSecurityGroup(input)
288 | if err != nil {
289 | return nil, nil, err
290 | }
291 |
292 | for _, port := range ports {
293 | if err = p.createEC2SecurityGroupRule(*group.GroupId, port, port); err != nil {
294 | return group.GroupId, &groupName,
295 | fmt.Errorf("failed to create security group on %s with port %d: %w", *group.GroupId, port, err)
296 | }
297 | }
298 |
299 | if openHighPorts && len(highPortRange) == 2 {
300 | err = p.createEC2SecurityGroupRule(*group.GroupId, highPortRange[0], highPortRange[1])
301 | if err != nil {
302 | return group.GroupId, &groupName, err
303 | }
304 | }
305 |
306 | return group.GroupId, &groupName, nil
307 | }
308 |
309 | func parsePorts(extraPorts string) ([]int, error) {
310 | var ports []int
311 |
312 | parts := strings.Split(extraPorts, ",")
313 | for _, part := range parts {
314 | if trimmed := strings.TrimSpace(part); len(trimmed) > 0 {
315 | port, err := strconv.Atoi(trimmed)
316 | if err != nil {
317 | return nil, err
318 | }
319 | ports = append(ports, port)
320 | }
321 | }
322 |
323 | return ports, nil
324 | }
325 |
326 | func (p *EC2Provisioner) createEC2SecurityGroupRule(groupID string, fromPort, toPort int) error {
327 | _, err := p.ec2Provisioner.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
328 | CidrIp: aws.String("0.0.0.0/0"),
329 | FromPort: aws.Int64(int64(fromPort)),
330 | IpProtocol: aws.String("tcp"),
331 | ToPort: aws.Int64(int64(toPort)),
332 | GroupId: aws.String(groupID),
333 | })
334 | if err != nil {
335 | return err
336 | }
337 | return nil
338 | }
339 |
340 | // lookupAMI gets the AMI ID that the exit node will use
341 | func (p *EC2Provisioner) lookupAMI(name string) (*string, error) {
342 | images, err := p.ec2Provisioner.DescribeImages(&ec2.DescribeImagesInput{
343 | Filters: []*ec2.Filter{
344 | {
345 | Name: aws.String("name"),
346 | Values: []*string{
347 | aws.String(name),
348 | },
349 | },
350 | },
351 | })
352 | if err != nil {
353 | return nil, err
354 | }
355 |
356 | if len(images.Images) == 0 {
357 | return nil, fmt.Errorf("image not found")
358 | }
359 | return images.Images[0].ImageId, nil
360 | }
361 |
--------------------------------------------------------------------------------
/provision/ec2_test.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import "testing"
4 |
5 | func Test_parsePorts_empty(t *testing.T) {
6 |
7 | ports, err := parsePorts("")
8 | if err != nil {
9 | t.Fatal(err)
10 | }
11 |
12 | if len(ports) != 0 {
13 | t.Fatalf("Expected empty slice, got %d", len(ports))
14 | }
15 | }
16 |
17 | func Test_parsePorts_single(t *testing.T) {
18 |
19 | wantPort := 80
20 | str := "80"
21 | ports, err := parsePorts(str)
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 |
26 | if len(ports) != 1 {
27 | t.Fatalf("Want single port, got %d", len(ports))
28 | }
29 |
30 | if ports[0] != wantPort {
31 | t.Fatalf("Want port %d, got %d", wantPort, ports[0])
32 | }
33 | }
34 |
35 | func Test_parsePorts_multiple(t *testing.T) {
36 |
37 | wantPorts := []int{27017, 22}
38 |
39 | str := "27017,22"
40 |
41 | ports, err := parsePorts(str)
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 |
46 | if len(ports) != len(wantPorts) {
47 | t.Fatalf("Want %d ports, got %d", len(wantPorts), len(ports))
48 | }
49 |
50 | found := 0
51 |
52 | for _, port := range ports {
53 | for _, wantPort := range wantPorts {
54 | if port == wantPort {
55 | found++
56 | }
57 | }
58 | }
59 |
60 | if found != len(wantPorts) {
61 | t.Fatalf("Want %v ports, got %v", wantPorts, ports)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/provision/gce.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "strings"
9 | "time"
10 |
11 | "google.golang.org/api/compute/v1"
12 | "google.golang.org/api/option"
13 | )
14 |
15 | const gceHostRunning = "RUNNING"
16 |
17 | // GCEProvisioner holds reference to the compute service to provision compute resources
18 | type GCEProvisioner struct {
19 | gceProvisioner *compute.Service
20 | }
21 |
22 | // NewGCEProvisioner returns a new GCEProvisioner
23 | func NewGCEProvisioner(accessKey string) (*GCEProvisioner, error) {
24 | gceService, err := compute.NewService(context.Background(), option.WithCredentialsJSON([]byte(accessKey)))
25 | return &GCEProvisioner{
26 | gceProvisioner: gceService,
27 | }, err
28 | }
29 |
30 | // Provision provisions a new GCE instance as an exit node
31 | func (p *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
32 |
33 | if host.Region == "" {
34 | return nil, fmt.Errorf("region is required")
35 | }
36 |
37 | projectID := host.Additional["projectid"]
38 |
39 | if err := p.createInletsFirewallRule(host.Additional["projectid"], host.Additional["firewall-name"], host.Additional["firewall-port"], host.Additional["pro"]); err != nil {
40 | return nil, fmt.Errorf("unable to create firewall rule in project: %s error: %w",
41 | host.Additional["projectid"], err)
42 | }
43 |
44 | addr := compute.Address{
45 | AddressType: "EXTERNAL",
46 | Description: "Static IP for inlets tunnel server",
47 | NetworkTier: "PREMIUM",
48 | Name: host.Name,
49 | }
50 |
51 | if _, err := p.gceProvisioner.Addresses.Insert(projectID, host.Region, &addr).Do(); err != nil {
52 | return nil, fmt.Errorf("unable to insert new IP external address %w", err)
53 | }
54 |
55 | var ipAddress string
56 | for i := 0; i < 20; i++ {
57 | log.Printf("GCE checking if IP is ready %d/10", i+1)
58 | ip, err := p.gceProvisioner.Addresses.Get(projectID, host.Region, host.Name).Do()
59 | if err != nil {
60 | return nil, fmt.Errorf("unable to get named IP address %s, error: %w", host.Name, err)
61 | }
62 |
63 | if ip.Address != "" {
64 | ipAddress = ip.Address
65 | log.Printf("GCE reserved static IP address: %s", ipAddress)
66 | break
67 | }
68 | time.Sleep(100 * time.Millisecond)
69 | }
70 |
71 | // instance auto restart on failure
72 | autoRestart := true
73 |
74 | var serviceAccounts []*compute.ServiceAccount
75 |
76 | if len(host.Additional["service_account"]) != 0 {
77 | serviceAccounts = append(serviceAccounts, &compute.ServiceAccount{
78 | Email: host.Additional["service_account"],
79 | Scopes: []string{
80 | compute.ComputeScope,
81 | },
82 | })
83 | }
84 |
85 | instance := &compute.Instance{
86 | Name: host.Name,
87 | Description: "Tunnel server for inlets",
88 | MachineType: fmt.Sprintf("zones/%s/machineTypes/%s", host.Additional["zone"], host.Plan),
89 | CanIpForward: true,
90 |
91 | Zone: fmt.Sprintf("projects/%s/zones/%s", host.Additional["projectid"], host.Additional["zone"]),
92 | Disks: []*compute.AttachedDisk{
93 | {
94 | AutoDelete: true,
95 | Boot: true,
96 | DeviceName: host.Name,
97 | Mode: "READ_WRITE",
98 | Type: "PERSISTENT",
99 | InitializeParams: &compute.AttachedDiskInitializeParams{
100 | Description: "Boot Disk for the exit-node created by inlets-operator",
101 | DiskName: host.Name,
102 | DiskSizeGb: 10,
103 | SourceImage: host.OS,
104 | },
105 | },
106 | },
107 | Metadata: &compute.Metadata{
108 | Items: []*compute.MetadataItems{
109 | {
110 | Key: "startup-script",
111 | Value: &host.UserData,
112 | },
113 | },
114 | },
115 | Labels: map[string]string{
116 | "inlets": "exit-node",
117 | },
118 | Tags: &compute.Tags{
119 | Items: []string{
120 | "http-server",
121 | "https-server",
122 | "inlets"},
123 | },
124 | Scheduling: &compute.Scheduling{
125 | AutomaticRestart: &autoRestart,
126 | OnHostMaintenance: "MIGRATE",
127 | Preemptible: false,
128 | },
129 | NetworkInterfaces: []*compute.NetworkInterface{
130 | {
131 | AccessConfigs: []*compute.AccessConfig{
132 | {
133 | Type: "ONE_TO_ONE_NAT",
134 | Name: "External NAT",
135 | NatIP: ipAddress,
136 | },
137 | },
138 | Network: "global/networks/default",
139 | },
140 | },
141 | ServiceAccounts: serviceAccounts,
142 | }
143 |
144 | op, err := p.gceProvisioner.Instances.Insert(
145 | host.Additional["projectid"],
146 | host.Additional["zone"],
147 | instance).Do()
148 |
149 | if err != nil {
150 | return nil, fmt.Errorf("could not provision GCE instance: %s", err)
151 | }
152 |
153 | if op.HTTPStatusCode == http.StatusConflict {
154 | log.Println("Host already exists, status: conflict.")
155 | }
156 |
157 | return &ProvisionedHost{
158 | ID: toGCEID(host.Name,
159 | host.Additional["zone"],
160 | host.Additional["projectid"],
161 | host.Region),
162 | Status: "provisioning",
163 | }, nil
164 | }
165 |
166 | // gceFirewallExists checks if the inlets firewall rule exists or not
167 | func (p *GCEProvisioner) gceFirewallExists(projectID string, firewallRuleName string) (bool, error) {
168 | op, err := p.gceProvisioner.Firewalls.Get(projectID, firewallRuleName).Do()
169 | if err != nil {
170 | return false, fmt.Errorf("could not get inlets firewall rule: %v", err)
171 | }
172 | if op.Name == firewallRuleName {
173 | return true, nil
174 | }
175 | return false, nil
176 | }
177 |
178 | // createInletsFirewallRule creates a firewall rule opening up the control port for inlets
179 | func (p *GCEProvisioner) createInletsFirewallRule(projectID string, firewallRuleName string, controlPort string, pro string) error {
180 | var firewallRule *compute.Firewall
181 | if pro == "true" {
182 | firewallRule = &compute.Firewall{
183 | Name: firewallRuleName,
184 | Description: "Firewall rule created by inlets-operator",
185 | Network: fmt.Sprintf("projects/%s/global/networks/default", projectID),
186 | Allowed: []*compute.FirewallAllowed{
187 | {
188 | IPProtocol: "tcp",
189 | },
190 | },
191 | SourceRanges: []string{"0.0.0.0/0"},
192 | Direction: "INGRESS",
193 | TargetTags: []string{"inlets"},
194 | }
195 | } else {
196 | firewallRule = &compute.Firewall{
197 | Name: firewallRuleName,
198 | Description: "Firewall rule created by inlets-operator",
199 | Network: fmt.Sprintf("projects/%s/global/networks/default", projectID),
200 | Allowed: []*compute.FirewallAllowed{
201 | {
202 | IPProtocol: "tcp",
203 | Ports: []string{controlPort, "80", "443"},
204 | },
205 | },
206 | SourceRanges: []string{"0.0.0.0/0"},
207 | Direction: "INGRESS",
208 | TargetTags: []string{"inlets"},
209 | }
210 | }
211 |
212 | exists, _ := p.gceFirewallExists(projectID, firewallRuleName)
213 | if exists {
214 | log.Printf("Creating firewall exists, updating: %s\n", firewallRuleName)
215 |
216 | _, err := p.gceProvisioner.Firewalls.Update(projectID, firewallRuleName, firewallRule).Do()
217 | if err != nil {
218 | return fmt.Errorf("could not update inlets firewall rule %s, error: %s", firewallRuleName, err)
219 | }
220 | return nil
221 | }
222 |
223 | _, err := p.gceProvisioner.Firewalls.Insert(projectID, firewallRule).Do()
224 | log.Printf("Creating firewall rule: %s\n", firewallRuleName)
225 | if err != nil {
226 | return fmt.Errorf("could not create inlets firewall rule: %v", err)
227 | }
228 | return nil
229 | }
230 |
231 | // Delete deletes the GCE exit node
232 | func (p *GCEProvisioner) Delete(request HostDeleteRequest) error {
233 | var instanceName, region, projectID, zone string
234 | var err error
235 | if len(request.ID) > 0 {
236 | instanceName, zone, projectID, region, err = getGCEFieldsFromID(request.ID)
237 | if err != nil {
238 | return err
239 | }
240 | } else {
241 | inletsID, err := p.lookupID(request)
242 | if err != nil {
243 | return err
244 | }
245 | instanceName, zone, projectID, region, err = getGCEFieldsFromID(inletsID)
246 | if err != nil {
247 | return err
248 | }
249 | }
250 |
251 | if len(request.ProjectID) > 0 {
252 | projectID = request.ProjectID
253 | }
254 |
255 | if len(request.Zone) > 0 {
256 | zone = request.Zone
257 | }
258 |
259 | if ip, err := p.gceProvisioner.Addresses.Get(projectID, region, instanceName).Do(); err == nil && ip.Address != "" {
260 | log.Printf("GCE Deleting reserved IP address for: %s project: %s in: %s\n", instanceName, projectID, region)
261 | if _, err = p.gceProvisioner.Addresses.Delete(projectID, region, instanceName).Do(); err != nil {
262 | log.Printf("Unable to delete reserved IP address: %v", err)
263 | }
264 | }
265 |
266 | log.Printf("GCE Deleting host: %s in project: %s, zone: %s\n", instanceName, projectID, zone)
267 |
268 | _, err = p.gceProvisioner.Instances.Delete(projectID, zone, instanceName).Do()
269 | if err != nil {
270 | return fmt.Errorf("could not delete the GCE instance: %v", err)
271 | }
272 | return err
273 | }
274 |
275 | // Status checks the status of the provisioning GCE exit node
276 | func (p *GCEProvisioner) Status(id string) (*ProvisionedHost, error) {
277 | instanceName, zone, projectID, region, err := getGCEFieldsFromID(id)
278 | if err != nil {
279 | return nil, fmt.Errorf("could not get custom GCE fields: %v", err)
280 | }
281 |
282 | op, err := p.gceProvisioner.Instances.Get(projectID, zone, instanceName).Do()
283 | if err != nil {
284 | return nil, fmt.Errorf("could not get instance: %v", err)
285 | }
286 |
287 | var ip string
288 | if len(op.NetworkInterfaces) > 0 {
289 | ip = op.NetworkInterfaces[0].AccessConfigs[0].NatIP
290 | }
291 |
292 | status := gceToInletsStatus(op.Status)
293 |
294 | return &ProvisionedHost{
295 | IP: ip,
296 | ID: toGCEID(instanceName, zone, projectID, region),
297 | Status: status,
298 | }, nil
299 | }
300 |
301 | func gceToInletsStatus(gce string) string {
302 | status := gce
303 | if status == gceHostRunning {
304 | status = ActiveStatus
305 | }
306 | return status
307 | }
308 |
309 | // toGCEID creates an ID for GCE based upon the instance ID,
310 | // zone, and projectID fields
311 | func toGCEID(instanceName, zone, projectID, region string) (id string) {
312 | return fmt.Sprintf("%s|%s|%s|%s", instanceName, zone, projectID, region)
313 | }
314 |
315 | // get some required fields from the custom GCE instance ID
316 | func getGCEFieldsFromID(id string) (instanceName, zone, projectID, region string, err error) {
317 | fields := strings.Split(id, "|")
318 | err = nil
319 | if len(fields) == 4 {
320 | instanceName = fields[0]
321 | zone = fields[1]
322 | projectID = fields[2]
323 | region = fields[3]
324 | } else {
325 | err = fmt.Errorf("could not get fields from custom ID: fields: %v", fields)
326 | return "", "", "", "", err
327 | }
328 | return instanceName, zone, projectID, region, nil
329 | }
330 |
331 | // List returns a list of exit nodes
332 | func (p *GCEProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
333 | var inlets []*ProvisionedHost
334 | var pageToken string
335 | for {
336 | call := p.gceProvisioner.Instances.List(filter.ProjectID, filter.Zone).Filter(filter.Filter)
337 | if len(pageToken) > 0 {
338 | call = call.PageToken(pageToken)
339 | }
340 |
341 | instances, err := call.Do()
342 | if err != nil {
343 | return inlets, fmt.Errorf("could not list instances: %v", err)
344 | }
345 | for _, instance := range instances.Items {
346 | var status string
347 | if instance.Status == gceHostRunning {
348 | status = ActiveStatus
349 | }
350 | host := &ProvisionedHost{
351 | IP: instance.NetworkInterfaces[0].AccessConfigs[0].NatIP,
352 | ID: toGCEID(instance.Name, filter.Zone, filter.ProjectID, filter.Region),
353 | Status: status,
354 | }
355 | inlets = append(inlets, host)
356 | }
357 | if len(instances.NextPageToken) == 0 {
358 | break
359 | }
360 | }
361 | return inlets, nil
362 | }
363 |
364 | func (p *GCEProvisioner) lookupID(request HostDeleteRequest) (string, error) {
365 | inletHosts, err := p.List(ListFilter{
366 | Filter: "labels.inlets=exit-node",
367 | ProjectID: request.ProjectID,
368 | Zone: request.Zone,
369 | Region: request.Region,
370 | })
371 | if err != nil {
372 | return "", err
373 | }
374 |
375 | for _, host := range inletHosts {
376 | if host.IP == request.IP {
377 | return host.ID, nil
378 | }
379 | }
380 |
381 | return "", fmt.Errorf("no host found with IP: %s", request.IP)
382 | }
383 |
--------------------------------------------------------------------------------
/provision/gce_test.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import "testing"
4 |
5 | func TestCustomGCEIDConstAndDest(t *testing.T) {
6 | inputInstanceName := "inlets"
7 | inputZone := "us-central1-a"
8 | inputProjectID := "playground"
9 | inputRegion := "us-central"
10 |
11 | customID := toGCEID(inputInstanceName, inputZone, inputProjectID, inputRegion)
12 |
13 | outputInstanceName, outputZone, outputProjectID, outputRegion, err := getGCEFieldsFromID(customID)
14 | if err != nil {
15 | t.Error(err)
16 | }
17 | if inputInstanceName != outputInstanceName ||
18 | inputZone != outputZone ||
19 | inputProjectID != outputProjectID ||
20 | inputRegion != outputRegion {
21 | t.Errorf("Input fields: %s, %s, %s are not equal to the ouput fields: %s, %s, %s",
22 | inputInstanceName, inputZone, inputProjectID, outputInstanceName, outputZone, outputProjectID)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/provision/hetzner.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/hetznercloud/hcloud-go/hcloud"
7 | "strconv"
8 | )
9 |
10 | var Status = "running"
11 |
12 | type HetznerProvisioner struct {
13 | client *hcloud.Client
14 | }
15 |
16 | // Creates a new Hetzner provisioner using an auth token.
17 | func NewHetznerProvisioner(authToken string) (*HetznerProvisioner, error) {
18 | client := hcloud.NewClient(hcloud.WithToken(authToken))
19 | return &HetznerProvisioner{
20 | client: client,
21 | }, nil
22 | }
23 |
24 | // Get status of a specific server by id.
25 | func (p *HetznerProvisioner) Status(id string) (*ProvisionedHost, error) {
26 | intId, err := strconv.Atoi(id)
27 | if err != nil {
28 | return nil, err
29 | }
30 | server, _, err := p.client.Server.GetByID(context.Background(), intId)
31 | if err != nil {
32 | return nil, err
33 | }
34 | if server == nil {
35 | return nil, fmt.Errorf("failed to find server with id %s", id)
36 | }
37 |
38 | status := ""
39 | ip := ""
40 |
41 | if server.Status == hcloud.ServerStatusRunning {
42 | status = ActiveStatus
43 | ip = server.PublicNet.IPv4.IP.String()
44 | } else {
45 | status = string(server.Status)
46 | }
47 |
48 | return &ProvisionedHost{
49 | IP: ip,
50 | ID: id,
51 | Status: status,
52 | }, nil
53 | }
54 |
55 | // Provision a new server on Hetzner cloud to use as an inlet node.
56 | func (p *HetznerProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
57 | img, _, err := p.client.Image.GetByName(context.Background(), host.OS)
58 | loc, _, err := p.client.Location.GetByName(context.Background(), host.Region)
59 | pln, _, err := p.client.ServerType.GetByName(context.Background(), host.Plan)
60 |
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | server, _, err := p.client.Server.Create(context.Background(), hcloud.ServerCreateOpts{
66 | Name: host.Name,
67 | ServerType: pln,
68 | Image: img,
69 | Location: loc,
70 | UserData: host.UserData,
71 | StartAfterCreate: hcloud.Bool(true),
72 | Labels: map[string]string{
73 | "managed-by": "inlets",
74 | },
75 | })
76 |
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | return &ProvisionedHost{
82 | IP: server.Server.PublicNet.IPv4.IP.String(),
83 | ID: strconv.Itoa(server.Server.ID),
84 | Status: "creating",
85 | }, nil
86 | }
87 |
88 | // List all nodes that are managed by inlets.
89 | func (p *HetznerProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
90 | var hosts []*ProvisionedHost
91 | hostList, err := p.client.Server.AllWithOpts(context.Background(), hcloud.ServerListOpts{
92 | // Adding a label to the VPS so that it is easier to select inlets managed servers and also
93 | // to tell the user that the server in question is managed by inlets.
94 | ListOpts: hcloud.ListOpts{
95 | LabelSelector: "managed-by=inlets",
96 | },
97 | })
98 |
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | for _, instance := range hostList {
104 | hosts = append(hosts, &ProvisionedHost{
105 | IP: instance.PublicNet.IPv4.IP.String(),
106 | ID: strconv.Itoa(instance.ID),
107 | Status: string(instance.Status),
108 | })
109 | }
110 |
111 | return hosts, nil
112 | }
113 |
114 | // Delete a specific server from the Hetzner cloud.
115 | func (p *HetznerProvisioner) Delete(request HostDeleteRequest) error {
116 | id := request.ID
117 | if len(id) <= 0 {
118 | hosts, err := p.List(ListFilter{})
119 | if err != nil {
120 | return err
121 | }
122 | for _, instance := range hosts {
123 | if instance.IP == request.IP {
124 | id = instance.ID
125 | }
126 | }
127 | if len(id) <= 0 {
128 | return fmt.Errorf("failed to find server with id with IP %s", request.IP)
129 | }
130 | }
131 |
132 | idAsInt, err := strconv.Atoi(id)
133 | if err != nil {
134 | return err
135 | }
136 |
137 | server, _, err := p.client.Server.GetByID(context.Background(), idAsInt)
138 | if err != nil {
139 | return err
140 | }
141 |
142 | if server == nil {
143 | return fmt.Errorf("failed to find server with id %s", id)
144 | }
145 |
146 | _, err = p.client.Server.Delete(context.Background(), server)
147 | if err != nil {
148 | return err
149 | }
150 |
151 | return nil
152 | }
153 |
--------------------------------------------------------------------------------
/provision/linode.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "strconv"
8 |
9 | "github.com/linode/linodego"
10 | "github.com/sethvargo/go-password/password"
11 | "golang.org/x/oauth2"
12 | )
13 |
14 | type LinodeInterface interface {
15 | CreateStackscript(createOpts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error)
16 | CreateInstance(instance linodego.InstanceCreateOptions) (*linodego.Instance, error)
17 | GetInstance(linodeID int) (*linodego.Instance, error)
18 | DeleteInstance(linodeID int) error
19 | DeleteStackscript(id int) error
20 | }
21 |
22 | type LinodeClient struct {
23 | client linodego.Client
24 | }
25 |
26 | func (p *LinodeClient) CreateStackscript(createOpts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) {
27 | return p.client.CreateStackscript(context.Background(), createOpts)
28 | }
29 |
30 | func (p *LinodeClient) CreateInstance(instance linodego.InstanceCreateOptions) (*linodego.Instance, error) {
31 | return p.client.CreateInstance(context.Background(), instance)
32 | }
33 |
34 | func (p *LinodeClient) GetInstance(linodeID int) (*linodego.Instance, error) {
35 | return p.client.GetInstance(context.Background(), linodeID)
36 | }
37 |
38 | func (p *LinodeClient) DeleteInstance(linodeID int) error {
39 | return p.client.DeleteInstance(context.Background(), linodeID)
40 | }
41 |
42 | func (p *LinodeClient) DeleteStackscript(id int) error {
43 | return p.client.DeleteStackscript(context.Background(), id)
44 | }
45 |
46 | type LinodeProvisioner struct {
47 | client LinodeInterface
48 | stackscriptID int
49 | }
50 |
51 | func NewLinodeProvisioner(apiKey string) (*LinodeProvisioner, error) {
52 | tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: apiKey})
53 |
54 | oauth2Client := &http.Client{
55 | Transport: &oauth2.Transport{
56 | Source: tokenSource,
57 | },
58 | }
59 |
60 | linodeClient := linodego.NewClient(oauth2Client)
61 | return &LinodeProvisioner{
62 | client: &LinodeClient{linodeClient},
63 | }, nil
64 | }
65 |
66 | // Provision provisions a new Linode instance as an exit node
67 | func (p *LinodeProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
68 |
69 | if len(host.Name) > 32 {
70 | return nil, fmt.Errorf("name cannot be longer than 32 characters for Linode due to label limitations")
71 | }
72 |
73 | // Stack script is how linode does the cloud-init when provisioning a VM.
74 | // Stack script is the inlets user data containing inlets auth token.
75 | // Making stack script public will allow everyone to read the stack script
76 | // and hence allow them to access the inlets auth token.
77 | // Because of above, here the stack script is created as IsPublic: false, so no one else can read it.
78 | // This script will be deleted once VM instance is running.
79 | stackscriptOption := linodego.StackscriptCreateOptions{
80 | IsPublic: false, Images: []string{host.OS}, Script: host.UserData, Label: host.Name,
81 | }
82 |
83 | stackscript, err := p.client.CreateStackscript(stackscriptOption)
84 | if err != nil {
85 | return nil, err
86 | }
87 | p.stackscriptID = stackscript.ID
88 |
89 | // Create instance
90 | rootPassword, err := password.Generate(16, 4, 0, false, true)
91 | if err != nil {
92 | return nil, err
93 | }
94 | instanceOptions := linodego.InstanceCreateOptions{
95 | Label: host.Name,
96 | StackScriptID: stackscript.ID,
97 | Image: host.OS,
98 | Region: host.Region,
99 | Type: host.Plan,
100 | RootPass: rootPassword,
101 | Tags: []string{"inlets"},
102 | }
103 | instance, err := p.client.CreateInstance(instanceOptions)
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | return &ProvisionedHost{
109 | IP: "",
110 | ID: fmt.Sprintf("%d", instance.ID),
111 | Status: string(instance.Status),
112 | }, nil
113 | }
114 |
115 | // Status checks the status of the provisioning Linode exit node
116 | func (p *LinodeProvisioner) Status(id string) (*ProvisionedHost, error) {
117 | instanceId, err := strconv.Atoi(id)
118 | if err != nil {
119 | return nil, err
120 | }
121 | instance, err := p.client.GetInstance(instanceId)
122 | if err != nil {
123 | return nil, err
124 | }
125 | var status string
126 | if instance.Status == linodego.InstanceRunning {
127 | status = ActiveStatus
128 | } else {
129 | status = string(instance.Status)
130 | }
131 | IP := ""
132 | if status == ActiveStatus {
133 | if len(instance.IPv4) > 0 {
134 | IP = instance.IPv4[0].String()
135 | }
136 | // If it fails to delete stack script, we will ignore and let user to delete manually.
137 | // It won't create security issue as this script was created as a private script.
138 | _ = p.client.DeleteStackscript(p.stackscriptID)
139 | }
140 | return &ProvisionedHost{
141 | IP: IP,
142 | ID: id,
143 | Status: status,
144 | }, nil
145 | }
146 |
147 | // Delete deletes the Linode exit node
148 | func (p *LinodeProvisioner) Delete(request HostDeleteRequest) error {
149 | instanceId, err := strconv.Atoi(request.ID)
150 | if err != nil {
151 | return err
152 | }
153 | err = p.client.DeleteInstance(instanceId)
154 | return err
155 | }
156 |
--------------------------------------------------------------------------------
/provision/linode_test.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "testing"
7 |
8 | "github.com/golang/mock/gomock"
9 |
10 | "github.com/linode/linodego"
11 | )
12 |
13 | func Test_Linode_Provision(t *testing.T) {
14 | mockCtrl := gomock.NewController(t)
15 | defer mockCtrl.Finish()
16 | mockClient := newMockLinodeInterface(mockCtrl)
17 | provisioner := &LinodeProvisioner{
18 | client: mockClient,
19 | }
20 | host := BasicHost{
21 | Region: "us-east",
22 | Plan: "g6-nanode-1",
23 | OS: "linode/ubuntu16.04lts",
24 | Name: "testvm",
25 | UserData: "user-data",
26 | Additional: nil,
27 | }
28 | stackscriptOption := linodego.StackscriptCreateOptions{
29 | IsPublic: false, Images: []string{host.OS}, Script: host.UserData, Label: host.Name,
30 | }
31 | returnedStackscript := &linodego.Stackscript{ID: 10}
32 |
33 | expectedInstanceOptions := linodego.InstanceCreateOptions{
34 | Label: host.Name,
35 | StackScriptID: returnedStackscript.ID,
36 | Image: host.OS,
37 | Region: host.Region,
38 | Type: host.Plan,
39 | RootPass: "testpass",
40 | Tags: []string{"inlets"},
41 | }
42 |
43 | returnedInstance := &linodego.Instance{
44 | ID: 42,
45 | Status: linodego.InstanceBooting,
46 | }
47 |
48 | mockClient.EXPECT().CreateStackscript(gomock.Eq(stackscriptOption)).Return(returnedStackscript, nil).Times(1)
49 | mockClient.EXPECT().CreateInstance(gomock.Any()).Return(returnedInstance, nil).Times(1).
50 | Do(func(instanceOptions linodego.InstanceCreateOptions) {
51 | if instanceOptions.Label != expectedInstanceOptions.Label {
52 | t.Fatalf("Label didn't match")
53 | }
54 | if instanceOptions.StackScriptID != expectedInstanceOptions.StackScriptID {
55 | t.Fatalf("StackScriptID didn't match")
56 | }
57 | if instanceOptions.Image != expectedInstanceOptions.Image {
58 | t.Fatalf("Image didn't match")
59 | }
60 | if instanceOptions.Region != expectedInstanceOptions.Region {
61 | t.Fatalf("Region didn't match")
62 | }
63 | if instanceOptions.Type != expectedInstanceOptions.Type {
64 | t.Fatalf("Type didn't match")
65 | }
66 | if len(instanceOptions.Tags) != len(expectedInstanceOptions.Tags) {
67 | t.Fatalf("tags don't match")
68 | }
69 | })
70 | provisionedHost, _ := provisioner.Provision(host)
71 | if provisionedHost.ID != fmt.Sprintf("%d", returnedInstance.ID) {
72 | t.Errorf("provisionedHost.ID want: %v, but got: %v", returnedInstance.ID, provisionedHost.ID)
73 | }
74 | if provisionedHost.Status != string(returnedInstance.Status) {
75 | t.Errorf("provisionedHost.Status want: %v, but got: %v", returnedInstance.Status, provisionedHost.Status)
76 | }
77 | }
78 |
79 | func Test_Linode_StatusBooting(t *testing.T) {
80 | mockCtrl := gomock.NewController(t)
81 | defer mockCtrl.Finish()
82 | mockClient := newMockLinodeInterface(mockCtrl)
83 | provisioner := &LinodeProvisioner{
84 | client: mockClient,
85 | }
86 |
87 | instanceId := 42
88 | returnedInstance := &linodego.Instance{
89 | ID: 42,
90 | Status: linodego.InstanceBooting,
91 | }
92 | expectedReturn := &ProvisionedHost{
93 | IP: "",
94 | ID: fmt.Sprintf("%d", instanceId),
95 | Status: string(returnedInstance.Status),
96 | }
97 |
98 | mockClient.EXPECT().GetInstance(gomock.Eq(instanceId)).Return(returnedInstance, nil).Times(1)
99 | provisionedHost, _ := provisioner.Status(fmt.Sprintf("%d", instanceId))
100 | if *expectedReturn != *provisionedHost {
101 | t.Errorf("provisionedHost want: %v, but got: %v", expectedReturn, provisionedHost)
102 | }
103 | }
104 |
105 | func Test_Linode_StatusActive(t *testing.T) {
106 | mockCtrl := gomock.NewController(t)
107 | defer mockCtrl.Finish()
108 | mockClient := newMockLinodeInterface(mockCtrl)
109 | provisioner := &LinodeProvisioner{
110 | client: mockClient,
111 | stackscriptID: 10,
112 | }
113 |
114 | instanceId := 42
115 | ip := net.IPv4(127, 0, 0, 1)
116 | returnedInstance := &linodego.Instance{
117 | ID: 42,
118 | Status: linodego.InstanceRunning,
119 | IPv4: []*net.IP{&ip},
120 | }
121 | expectedReturn := &ProvisionedHost{
122 | IP: ip.String(),
123 | ID: fmt.Sprintf("%d", instanceId),
124 | Status: ActiveStatus,
125 | }
126 |
127 | mockClient.EXPECT().GetInstance(gomock.Eq(instanceId)).Return(returnedInstance, nil).Times(1)
128 | mockClient.EXPECT().DeleteStackscript(gomock.Eq(provisioner.stackscriptID)).Return(nil).Times(1)
129 | provisionedHost, _ := provisioner.Status(fmt.Sprintf("%d", instanceId))
130 | if *expectedReturn != *provisionedHost {
131 | t.Errorf("provisionedHost want: %v, but got: %v", expectedReturn, provisionedHost)
132 | }
133 | }
134 |
135 | func Test_Linode_Delete(t *testing.T) {
136 | mockCtrl := gomock.NewController(t)
137 | defer mockCtrl.Finish()
138 | mockClient := newMockLinodeInterface(mockCtrl)
139 | provisioner := &LinodeProvisioner{
140 | client: mockClient,
141 | }
142 | instanceId := 42
143 | request := HostDeleteRequest{
144 | ID: "42",
145 | }
146 |
147 | mockClient.EXPECT().DeleteInstance(gomock.Eq(instanceId)).Return(nil).Times(1)
148 | _ = provisioner.Delete(request)
149 | }
150 |
--------------------------------------------------------------------------------
/provision/mock_linode.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: pkg/provision/linode.go
3 |
4 | // Package mock is a generated GoMock package.
5 | package provision
6 |
7 | import (
8 | gomock "github.com/golang/mock/gomock"
9 | linodego "github.com/linode/linodego"
10 | reflect "reflect"
11 | )
12 |
13 | // mockLinodeInterface is a mock of LinodeInterface interface
14 | type mockLinodeInterface struct {
15 | ctrl *gomock.Controller
16 | recorder *mockLinodeInterfaceMockRecorder
17 | }
18 |
19 | // mockLinodeInterfaceMockRecorder is the mock recorder for mockLinodeInterface
20 | type mockLinodeInterfaceMockRecorder struct {
21 | mock *mockLinodeInterface
22 | }
23 |
24 | // newMockLinodeInterface creates a new mock instance
25 | func newMockLinodeInterface(ctrl *gomock.Controller) *mockLinodeInterface {
26 | mock := &mockLinodeInterface{ctrl: ctrl}
27 | mock.recorder = &mockLinodeInterfaceMockRecorder{mock}
28 | return mock
29 | }
30 |
31 | // EXPECT returns an object that allows the caller to indicate expected use
32 | func (m *mockLinodeInterface) EXPECT() *mockLinodeInterfaceMockRecorder {
33 | return m.recorder
34 | }
35 |
36 | // CreateStackscript mocks base method
37 | func (m *mockLinodeInterface) CreateStackscript(createOpts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) {
38 | m.ctrl.T.Helper()
39 | ret := m.ctrl.Call(m, "CreateStackscript", createOpts)
40 | ret0, _ := ret[0].(*linodego.Stackscript)
41 | ret1, _ := ret[1].(error)
42 | return ret0, ret1
43 | }
44 |
45 | // CreateStackscript indicates an expected call of CreateStackscript
46 | func (mr *mockLinodeInterfaceMockRecorder) CreateStackscript(createOpts interface{}) *gomock.Call {
47 | mr.mock.ctrl.T.Helper()
48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStackscript", reflect.TypeOf((*mockLinodeInterface)(nil).CreateStackscript), createOpts)
49 | }
50 |
51 | // CreateInstance mocks base method
52 | func (m *mockLinodeInterface) CreateInstance(instance linodego.InstanceCreateOptions) (*linodego.Instance, error) {
53 | m.ctrl.T.Helper()
54 | ret := m.ctrl.Call(m, "CreateInstance", instance)
55 | ret0, _ := ret[0].(*linodego.Instance)
56 | ret1, _ := ret[1].(error)
57 | return ret0, ret1
58 | }
59 |
60 | // CreateInstance indicates an expected call of CreateInstance
61 | func (mr *mockLinodeInterfaceMockRecorder) CreateInstance(instance interface{}) *gomock.Call {
62 | mr.mock.ctrl.T.Helper()
63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInstance", reflect.TypeOf((*mockLinodeInterface)(nil).CreateInstance), instance)
64 | }
65 |
66 | // GetInstance mocks base method
67 | func (m *mockLinodeInterface) GetInstance(linodeID int) (*linodego.Instance, error) {
68 | m.ctrl.T.Helper()
69 | ret := m.ctrl.Call(m, "GetInstance", linodeID)
70 | ret0, _ := ret[0].(*linodego.Instance)
71 | ret1, _ := ret[1].(error)
72 | return ret0, ret1
73 | }
74 |
75 | // GetInstance indicates an expected call of GetInstance
76 | func (mr *mockLinodeInterfaceMockRecorder) GetInstance(linodeID interface{}) *gomock.Call {
77 | mr.mock.ctrl.T.Helper()
78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstance", reflect.TypeOf((*mockLinodeInterface)(nil).GetInstance), linodeID)
79 | }
80 |
81 | // DeleteInstance mocks base method
82 | func (m *mockLinodeInterface) DeleteInstance(linodeID int) error {
83 | m.ctrl.T.Helper()
84 | ret := m.ctrl.Call(m, "DeleteInstance", linodeID)
85 | ret0, _ := ret[0].(error)
86 | return ret0
87 | }
88 |
89 | // DeleteInstance indicates an expected call of DeleteInstance
90 | func (mr *mockLinodeInterfaceMockRecorder) DeleteInstance(linodeID interface{}) *gomock.Call {
91 | mr.mock.ctrl.T.Helper()
92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInstance", reflect.TypeOf((*mockLinodeInterface)(nil).DeleteInstance), linodeID)
93 | }
94 |
95 | // DeleteStackscript mocks base method
96 | func (m *mockLinodeInterface) DeleteStackscript(id int) error {
97 | m.ctrl.T.Helper()
98 | ret := m.ctrl.Call(m, "DeleteStackscript", id)
99 | ret0, _ := ret[0].(error)
100 | return ret0
101 | }
102 |
103 | // DeleteStackscript indicates an expected call of DeleteStackscript
104 | func (mr *mockLinodeInterfaceMockRecorder) DeleteStackscript(id interface{}) *gomock.Call {
105 | mr.mock.ctrl.T.Helper()
106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStackscript", reflect.TypeOf((*mockLinodeInterface)(nil).DeleteStackscript), id)
107 | }
108 |
--------------------------------------------------------------------------------
/provision/ovh.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | ovhsdk "github.com/dirien/ovh-go-sdk/pkg/sdk"
7 | )
8 |
9 | type OVHProvisioner struct {
10 | client *ovhsdk.OVHcloud
11 | }
12 |
13 | func NewOVHProvisioner(endpoint, appKey, appSecret, consumerKey, region, serviceName string) (*OVHProvisioner, error) {
14 | client, err := ovhsdk.NewOVHClient(endpoint, appKey, appSecret, consumerKey, region, serviceName)
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return &OVHProvisioner{
20 | client: client,
21 | }, nil
22 | }
23 |
24 | func (o *OVHProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
25 | image, err := o.client.GetImage(context.Background(), host.OS, host.Region)
26 | if err != nil {
27 | return nil, err
28 | }
29 | flavor, err := o.client.GetFlavor(context.Background(), host.Plan, host.Region)
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | instance, err := o.client.CreateInstance(context.Background(), ovhsdk.InstanceCreateOptions{
35 | Name: host.Name,
36 | ImageID: image.ID,
37 | Region: host.Region,
38 | FlavorID: flavor.ID,
39 | UserData: host.UserData,
40 | })
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | //ignore missing ip4 during build
46 | ip4, _ := ovhsdk.IPv4(instance)
47 |
48 | return &ProvisionedHost{
49 | ID: instance.ID,
50 | IP: ip4,
51 | Status: string(ovhsdk.InstanceBuilding),
52 | }, nil
53 | }
54 |
55 | func ovhToInletsStatus(ovhStatus ovhsdk.InstanceStatus) string {
56 | status := string(ovhStatus)
57 | if status == string(ovhsdk.InstanceActive) {
58 | status = ActiveStatus
59 | }
60 | return status
61 | }
62 |
63 | func (o *OVHProvisioner) Status(id string) (*ProvisionedHost, error) {
64 | instance, err := o.client.GetInstance(context.Background(), id)
65 | if err != nil {
66 | return nil, err
67 | }
68 | //ignore missing ip4 during build
69 | ip4, _ := ovhsdk.IPv4(instance)
70 |
71 | status := ovhToInletsStatus(instance.Status)
72 | return &ProvisionedHost{
73 | ID: instance.ID,
74 | IP: ip4,
75 | Status: status,
76 | }, nil
77 | }
78 |
79 | func (o *OVHProvisioner) lookupID(request HostDeleteRequest) (string, error) {
80 | instances, err := o.client.ListInstance(context.Background())
81 | if err != nil {
82 | return "", err
83 | }
84 | for _, instance := range instances {
85 | ip4, _ := ovhsdk.IPv4(&instance)
86 | if ip4 == request.IP {
87 | return instance.ID, nil
88 | }
89 | }
90 | return "", fmt.Errorf("no host with ip: %s", request.IP)
91 | }
92 |
93 | func (o *OVHProvisioner) Delete(request HostDeleteRequest) error {
94 | var id string
95 | var err error
96 | if len(request.ID) > 0 {
97 | id = request.ID
98 | } else {
99 | id, err = o.lookupID(request)
100 | if err != nil {
101 | return err
102 | }
103 | }
104 | err = o.client.DeleteInstance(context.Background(), id)
105 | if err != nil {
106 | return err
107 | }
108 | return nil
109 | }
110 |
--------------------------------------------------------------------------------
/provision/provision.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | // Provisioner is an interface used for deploying exit nodes into cloud providers
4 | type Provisioner interface {
5 | Provision(BasicHost) (*ProvisionedHost, error)
6 | Status(id string) (*ProvisionedHost, error)
7 | Delete(HostDeleteRequest) error
8 | }
9 |
10 | // ActiveStatus is the status returned by an active exit node
11 | const ActiveStatus = "active"
12 |
13 | // ProvisionedHost contains the IP, ID and Status of an exit node
14 | type ProvisionedHost struct {
15 | IP string
16 | ID string
17 | Status string
18 | }
19 |
20 | // BasicHost contains the data required to deploy a exit node
21 | type BasicHost struct {
22 | Region string
23 | Plan string
24 | OS string
25 | Name string
26 | UserData string
27 | Additional map[string]string
28 | }
29 |
30 | // HostDeleteRequest contains the data required to delete an exit node by either IP or ID
31 | type HostDeleteRequest struct {
32 | ID string
33 | IP string
34 | ProjectID string
35 | Zone string
36 | Region string
37 | }
38 |
39 | // ListFilter is used to filter results to return only exit nodes
40 | type ListFilter struct {
41 | Filter string
42 | ProjectID string
43 | Zone string
44 | Region string
45 | }
46 |
--------------------------------------------------------------------------------
/provision/scaleway.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 | "time"
8 |
9 | instance "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
10 | "github.com/scaleway/scaleway-sdk-go/scw"
11 | )
12 |
13 | // ScalewayProvisioner provision a VM on scaleway.com
14 | type ScalewayProvisioner struct {
15 | instanceAPI *instance.API
16 | }
17 |
18 | // NewScalewayProvisioner with an accessKey and secretKey
19 | func NewScalewayProvisioner(accessKey, secretKey, organizationID, region string) (*ScalewayProvisioner, error) {
20 | if region == "" {
21 | region = "fr-par-1"
22 | }
23 |
24 | zone, err := scw.ParseZone(region)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | client, err := scw.NewClient(
30 | scw.WithAuth(accessKey, secretKey),
31 | scw.WithDefaultOrganizationID(organizationID),
32 | scw.WithDefaultZone(zone),
33 | )
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return &ScalewayProvisioner{
39 | instanceAPI: instance.NewAPI(client),
40 | }, nil
41 | }
42 |
43 | // Provision creates a new scaleway instance from the BasicHost type
44 | // To provision the instance we first create the server, then set its user-data and power it on
45 | func (p *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
46 | log.Printf("Provisioning host with Scaleway\n")
47 |
48 | if host.OS == "" {
49 | host.OS = "ubuntu-bionic"
50 | }
51 |
52 | if host.Plan == "" {
53 | host.Plan = "DEV1-S"
54 | }
55 |
56 | res, err := p.instanceAPI.CreateServer(&instance.CreateServerRequest{
57 | Name: host.Name,
58 | CommercialType: host.Plan,
59 | Image: host.OS,
60 | Tags: []string{"inlets"},
61 | // DynamicIPRequired is mandatory to get a public IP
62 | // otherwise scaleway doesn't attach a public IP to the instance
63 | DynamicIPRequired: scw.BoolPtr(true),
64 | })
65 |
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | server := res.Server
71 |
72 | if err := p.instanceAPI.SetServerUserData(&instance.SetServerUserDataRequest{
73 | ServerID: server.ID,
74 | Key: "cloud-init",
75 | Content: strings.NewReader(host.UserData),
76 | }); err != nil {
77 | return nil, err
78 | }
79 |
80 | if _, err = p.instanceAPI.ServerAction(&instance.ServerActionRequest{
81 | ServerID: server.ID,
82 | Action: instance.ServerActionPoweron,
83 | }); err != nil {
84 | return nil, err
85 | }
86 |
87 | return serverToProvisionedHost(server), nil
88 |
89 | }
90 |
91 | // Status returns the status of the scaleway instance
92 | func (p *ScalewayProvisioner) Status(id string) (*ProvisionedHost, error) {
93 | res, err := p.instanceAPI.GetServer(&instance.GetServerRequest{
94 | ServerID: id,
95 | })
96 |
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | return serverToProvisionedHost(res.Server), nil
102 | }
103 |
104 | // Delete deletes the provisionned instance by ID
105 | // We should first poweroff the instance,
106 | // otherwise we'll have: http error 400 Bad Request: instance should be powered off.
107 | // Then we have to delete the server and attached volumes
108 | func (p *ScalewayProvisioner) Delete(request HostDeleteRequest) error {
109 | var id string
110 | var err error
111 |
112 | if len(request.ID) > 0 {
113 | id = request.ID
114 | } else {
115 | id, err = p.lookupID(request)
116 | if err != nil {
117 | return err
118 | }
119 | }
120 |
121 | server, err := p.instanceAPI.GetServer(&instance.GetServerRequest{
122 | ServerID: id,
123 | })
124 |
125 | if err != nil {
126 | return err
127 | }
128 |
129 | timeout := time.Minute * 5
130 | if err = p.instanceAPI.ServerActionAndWait(&instance.ServerActionAndWaitRequest{
131 | ServerID: id,
132 | Action: instance.ServerActionPoweroff,
133 | Timeout: &timeout,
134 | }); err != nil {
135 | return err
136 | }
137 |
138 | if err = p.instanceAPI.DeleteServer(&instance.DeleteServerRequest{
139 | ServerID: id,
140 | }); err != nil {
141 | return err
142 | }
143 |
144 | for _, volume := range server.Server.Volumes {
145 | if err := p.instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
146 | VolumeID: volume.ID,
147 | }); err != nil {
148 | return err
149 | }
150 | }
151 |
152 | return nil
153 | }
154 |
155 | // List returns a list of exit nodes
156 | func (p *ScalewayProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
157 | var inlets []*ProvisionedHost
158 | page := int32(1)
159 | perPage := uint32(20)
160 | for {
161 | servers, err := p.instanceAPI.ListServers(&instance.ListServersRequest{Page: &page, PerPage: &perPage})
162 | if err != nil {
163 | return inlets, err
164 | }
165 | for _, server := range servers.Servers {
166 | for _, t := range server.Tags {
167 | if t == filter.Filter {
168 | host := &ProvisionedHost{
169 | IP: server.PublicIP.Address.String(),
170 | ID: server.ID,
171 | Status: server.State.String(),
172 | }
173 | inlets = append(inlets, host)
174 | }
175 | }
176 | }
177 | if len(servers.Servers) < int(perPage) {
178 | break
179 | }
180 | page = page + 1
181 | }
182 | return inlets, nil
183 | }
184 |
185 | func (p *ScalewayProvisioner) lookupID(request HostDeleteRequest) (string, error) {
186 | inlets, err := p.List(ListFilter{Filter: "inlets"})
187 | if err != nil {
188 | return "", err
189 | }
190 | for _, inlet := range inlets {
191 | if inlet.IP == request.IP {
192 | return inlet.ID, nil
193 | }
194 | }
195 |
196 | return "", fmt.Errorf("no host with ip: %s", request.IP)
197 | }
198 |
199 | func serverToProvisionedHost(server *instance.Server) *ProvisionedHost {
200 | var ip = ""
201 | if server.PublicIP != nil {
202 | ip = server.PublicIP.Address.String()
203 | }
204 |
205 | state := server.State.String()
206 | if server.State.String() == "running" {
207 | state = ActiveStatus
208 | }
209 |
210 | return &ProvisionedHost{
211 | ID: server.ID,
212 | IP: ip,
213 | Status: state,
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/provision/userdata.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | // MakeExitServerUserdata makes a user-data script in bash to setup inlets
4 | // PRO with a systemd service and the given version.
5 | func MakeExitServerUserdata(authToken, version string) string {
6 |
7 | return `#!/bin/bash
8 | export AUTHTOKEN="` + authToken + `"
9 | export IP=$(curl -sfSL https://checkip.amazonaws.com)
10 |
11 | curl -SLsf https://github.com/inlets/inlets-pro/releases/download/` + version + `/inlets-pro -o /tmp/inlets-pro && \
12 | chmod +x /tmp/inlets-pro && \
13 | mv /tmp/inlets-pro /usr/local/bin/inlets-pro
14 |
15 | curl -SLsf https://github.com/inlets/inlets-pro/releases/download/` + version + `/inlets-pro.service -o inlets-pro.service && \
16 | mv inlets-pro.service /etc/systemd/system/inlets-pro.service && \
17 | echo "AUTHTOKEN=$AUTHTOKEN" >> /etc/default/inlets-pro && \
18 | echo "IP=$IP" >> /etc/default/inlets-pro && \
19 | systemctl daemon-reload && \
20 | systemctl start inlets-pro && \
21 | systemctl enable inlets-pro
22 | `
23 | }
24 |
--------------------------------------------------------------------------------
/provision/userdata_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) Inlets Author(s) 2020. All rights reserved.
2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 |
4 | package provision
5 |
6 | import (
7 | "testing"
8 | )
9 |
10 | func Test_makeUserdata_InletsPro(t *testing.T) {
11 | userData := MakeExitServerUserdata("auth", "0.7.0")
12 |
13 | wantUserdata := `#!/bin/bash
14 | export AUTHTOKEN="auth"
15 | export IP=$(curl -sfSL https://checkip.amazonaws.com)
16 |
17 | curl -SLsf https://github.com/inlets/inlets-pro/releases/download/0.7.0/inlets-pro -o /tmp/inlets-pro && \
18 | chmod +x /tmp/inlets-pro && \
19 | mv /tmp/inlets-pro /usr/local/bin/inlets-pro
20 |
21 | curl -SLsf https://github.com/inlets/inlets-pro/releases/download/0.7.0/inlets-pro.service -o inlets-pro.service && \
22 | mv inlets-pro.service /etc/systemd/system/inlets-pro.service && \
23 | echo "AUTHTOKEN=$AUTHTOKEN" >> /etc/default/inlets-pro && \
24 | echo "IP=$IP" >> /etc/default/inlets-pro && \
25 | systemctl daemon-reload && \
26 | systemctl start inlets-pro && \
27 | systemctl enable inlets-pro
28 | `
29 |
30 | // ioutil.WriteFile("/tmp/pro", []byte(userData), 0600)
31 | if userData != wantUserdata {
32 | t.Errorf("want: %s, but got: %s", wantUserdata, userData)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/provision/vultr.go:
--------------------------------------------------------------------------------
1 | package provision
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "fmt"
7 | "github.com/vultr/govultr/v2"
8 | "golang.org/x/oauth2"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | const vultrHostRunning = "ok"
14 | const exiteNodeTag = "inlets-exit-node"
15 |
16 | type VultrProvisioner struct {
17 | client *govultr.Client
18 | }
19 |
20 | func NewVultrProvisioner(accessKey string) (*VultrProvisioner, error) {
21 | config := &oauth2.Config{}
22 | ts := config.TokenSource(context.Background(), &oauth2.Token{AccessToken: accessKey})
23 | vultrClient := govultr.NewClient(oauth2.NewClient(context.Background(), ts))
24 | return &VultrProvisioner{
25 | client: vultrClient,
26 | }, nil
27 | }
28 |
29 | func (v *VultrProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) {
30 | script, err := v.client.StartupScript.Create(context.Background(), &govultr.StartupScriptReq{
31 | Script: base64.StdEncoding.EncodeToString([]byte(host.UserData)),
32 | Name: host.Name,
33 | Type: "boot",
34 | })
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | os, err := strconv.Atoi(host.OS)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | opts := &govultr.InstanceCreateReq{
45 | ScriptID: script.ID,
46 | Region: host.Region,
47 | Plan: host.Plan,
48 | OsID: os,
49 | Hostname: host.Name,
50 | Label: host.Name,
51 | Tag: exiteNodeTag,
52 | }
53 |
54 | result, err := v.client.Instance.Create(context.Background(), opts)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | return &ProvisionedHost{
60 | IP: result.MainIP,
61 | ID: result.ID,
62 | Status: result.ServerStatus,
63 | }, nil
64 | }
65 |
66 | func (v *VultrProvisioner) Status(id string) (*ProvisionedHost, error) {
67 | server, err := v.client.Instance.Get(context.Background(), id)
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | status := server.ServerStatus
73 | if status == vultrHostRunning {
74 | status = ActiveStatus
75 | }
76 |
77 | return &ProvisionedHost{
78 | IP: server.MainIP,
79 | ID: server.ID,
80 | Status: status,
81 | }, nil
82 | }
83 |
84 | func (v *VultrProvisioner) Delete(request HostDeleteRequest) error {
85 | var id string
86 | var err error
87 | if len(request.ID) > 0 {
88 | id = request.ID
89 | } else {
90 | id, err = v.lookupID(request)
91 | if err != nil {
92 | return err
93 | }
94 | }
95 |
96 | server, err := v.client.Instance.Get(context.Background(), id)
97 | if err != nil {
98 | return err
99 | }
100 |
101 | err = v.client.Instance.Delete(context.Background(), id)
102 | if err != nil {
103 | return err
104 | }
105 |
106 | scripts, _, err := v.client.StartupScript.List(context.Background(), nil)
107 | for _, s := range scripts {
108 | if s.Name == server.Label {
109 | _ = v.client.StartupScript.Delete(context.Background(), s.ID)
110 | break
111 | }
112 | }
113 |
114 | return nil
115 | }
116 |
117 | // List returns a list of exit nodes
118 | func (v *VultrProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) {
119 | servers, _, err := v.client.Instance.List(context.Background(), nil)
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | var inlets []*ProvisionedHost
125 | for _, server := range servers {
126 | if server.Tag == filter.Filter {
127 | host := &ProvisionedHost{
128 | IP: server.MainIP,
129 | ID: server.ID,
130 | Status: vultrToInletsStatus(server.Status),
131 | }
132 | inlets = append(inlets, host)
133 | }
134 |
135 | }
136 |
137 | return inlets, nil
138 | }
139 |
140 | func (v *VultrProvisioner) lookupID(request HostDeleteRequest) (string, error) {
141 |
142 | inlets, err := v.List(ListFilter{Filter: exiteNodeTag})
143 | if err != nil {
144 | return "", err
145 | }
146 | for _, inlet := range inlets {
147 | if inlet.IP == request.IP {
148 | return inlet.ID, nil
149 | }
150 | }
151 | return "", fmt.Errorf("no host with ip: %s", request.IP)
152 | }
153 |
154 | func (v *VultrProvisioner) lookupRegion(id string) (*int, error) {
155 | result, err := strconv.Atoi(id)
156 | if err == nil {
157 | return &result, nil
158 | }
159 |
160 | regions, _, err := v.client.Region.List(context.Background(), nil)
161 | if err != nil {
162 | return nil, err
163 | }
164 |
165 | for _, region := range regions {
166 | if strings.EqualFold(id, region.ID) {
167 | regionId, _ := strconv.Atoi(region.ID)
168 | return ®ionId, nil
169 | }
170 | }
171 |
172 | return nil, fmt.Errorf("region '%s' not available", id)
173 | }
174 |
175 | func vultrToInletsStatus(vultr string) string {
176 | status := vultr
177 | if status == vultrHostRunning {
178 | status = ActiveStatus
179 | }
180 | return status
181 | }
182 |
--------------------------------------------------------------------------------