├── .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 | --------------------------------------------------------------------------------