├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── pkg └── drivers │ └── metal │ ├── config.go │ ├── metal.go │ └── metal_test.go └── renovate.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v3 17 | with: 18 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 19 | version: v1.56 20 | - 21 | name: Check GoReleaser 22 | uses: goreleaser/goreleaser-action@v6 23 | with: 24 | version: latest 25 | args: check 26 | test: 27 | name: test 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | go: [ '1.22' ] 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-go@v4 35 | with: 36 | go-version: ${{ matrix.go }} 37 | - run: go test -v ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.22 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | version: latest 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/docker-machine-driver-* 2 | docker-machine-driver-* 3 | checksums 4 | .idea/ 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | version: 2 4 | before: 5 | hooks: 6 | - go mod download 7 | builds: 8 | - id: docker-machine-driver-metal 9 | env: 10 | - CGO_ENABLED=0 11 | - GO111MODULE=on 12 | binary: docker-machine-driver-metal 13 | ldflags: 14 | - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.version={{.Version}} 15 | goos: 16 | - windows 17 | - darwin 18 | - linux 19 | goarch: 20 | - amd64 21 | - arm 22 | - arm64 23 | goarm: 24 | - 6 25 | - 7 26 | ignore: 27 | - goos: windows 28 | goarch: arm 29 | - goos: windows 30 | goarch: arm64 31 | - goos: darwin 32 | goarch: arm64 33 | - goos: darwin 34 | goarch: arm 35 | - id: docker-machine-driver-packet 36 | env: 37 | - CGO_ENABLED=0 38 | - GO111MODULE=on 39 | binary: docker-machine-driver-packet 40 | ldflags: 41 | - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.version={{.Version}} 42 | - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.driverName=packet 43 | goos: 44 | - windows 45 | - darwin 46 | - linux 47 | goarch: 48 | - amd64 49 | - arm 50 | - arm64 51 | goarm: 52 | - 6 53 | - 7 54 | ignore: 55 | - goos: windows 56 | goarch: arm 57 | - goos: windows 58 | goarch: arm64 59 | - goos: darwin 60 | goarch: arm64 61 | - goos: darwin 62 | goarch: arm 63 | archives: 64 | - name_template: "{{ .Binary }}_{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 65 | format: zip 66 | # replacements: 67 | # darwin: Darwin 68 | # linux: Linux 69 | # windows: Windows 70 | # 386: i386 71 | # amd64: x86_64 72 | checksum: 73 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 74 | release: 75 | name_template: "{{.ProjectName}}-v{{.Version}}" 76 | snapshot: 77 | version_template: "{{ .Tag }}-next" 78 | changelog: 79 | sort: asc 80 | filters: 81 | exclude: 82 | - "^docs:" 83 | - "^test:" 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@equinixmetal.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Hello Contributors! 2 | Thx for your interest! We're so glad you're here. 3 | 4 | ### Important Resources 5 | - bugs: [https://github.com/equinix/docker-machine-driver-metal/issues](https://github.com/equinix/docker-machine-driver-metal/issues) 6 | 7 | ### Code of Conduct 8 | Available via [https://github.com/equinix/docker-machine-driver-metal/blob/main/.github/CODE_OF_CONDUCT.md](https://github.com/equinix/docker-machine-driver-metal/blob/main/.github/CODE_OF_CONDUCT.md) 9 | 10 | ### How to Submit Change Requests 11 | Please submit change requests and / or features via [Issues](https://github.com/equinix/docker-machine-driver-metal/issues). There's no guarantee it'll be changed, but you never know until you try. We'll try to add comments as soon as possible, though. 12 | 13 | ### How to Report a Bug 14 | Bugs are problems in code, in the functionality of an application or in its UI design; you can submit them through [Issues](https://github.com/equinix/docker-machine-driver-metal/issues) as well. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Equinix Metal, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) 4 | current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path)))) 5 | github_user := "equinix" 6 | project := "github.com/$(github_user)/$(current_dir)" 7 | bin_suffix := "" 8 | 9 | containerbuild: 10 | docker build -t $(current_dir) . 11 | docker run \ 12 | -v $(shell pwd):/go/src/$(project) \ 13 | -e GOOS \ 14 | -e GOARCH \ 15 | -e GO15VENDOREXPERIMENT=1 \ 16 | $(current_dir) \ 17 | make build 18 | 19 | clean: 20 | rm -r docker-machine-driver-metal bin/docker-machine-driver-metal 21 | 22 | compile: 23 | GO111MODULE=on GOGC=off CGOENABLED=0 go build -ldflags "-s" 24 | 25 | # deprecated in favor of goreleaser 26 | pack: cross 27 | find ./bin -mindepth 1 -type d -exec zip -r -j {}.zip {} \; 28 | 29 | # deprecated in favor of goreleaser 30 | checksums: pack 31 | for file in $(shell find bin -type f -name '*.zip'); do \ 32 | ( \ 33 | cd $$(dirname $$file); \ 34 | f=$$(basename $$file); \ 35 | b2sum --tag $$f && \ 36 | sha256sum --tag $$f && \ 37 | sha512sum --tag $$f ; \ 38 | ) \ 39 | done | sort >$@.tmp 40 | @mv $@.tmp $@ 41 | 42 | print-success: 43 | @echo 44 | @echo "Plugin built." 45 | @echo 46 | @echo "To use it, either run 'make install' or set your PATH environment variable correctly." 47 | 48 | build: compile print-success 49 | 50 | # deprecated in favor of goreleaser 51 | cross: 52 | for os in darwin windows linux; do \ 53 | for arch in amd64; do \ 54 | GOOS=$$os GOARCH=$$arch BIN_SUFFIX=_$$os-$$arch $(MAKE) compile & \ 55 | done; \ 56 | done; \ 57 | wait 58 | 59 | install: 60 | cp bin/$(current_dir)/$(current_dir) /usr/local/bin/$(current_dir) 61 | 62 | tag: 63 | if ! git tag | grep -q $(version); then \ 64 | git tag -m $(version) $(version); \ 65 | git push --tags; \ 66 | fi 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-machine-driver-metal 2 | 3 | [![GitHub release](https://img.shields.io/github/release/equinix/docker-machine-driver-metal/all.svg?style=flat-square)](https://github.com/equinix/docker-machine-driver-metal/releases) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/equinix/docker-machine-driver-metal)](https://goreportcard.com/report/github.com/equinix/docker-machine-driver-metal) 5 | [![Equinix Community](https://img.shields.io/badge/Equinix%20Community%20-%20%23E91C24?logo=equinixmetal)](https://community.equinix.com) 6 | 7 | The [Equinix Metal](https://metal.equinix.com) cloud bare-metal machine driver for Docker. 8 | 9 | ## Usage 10 | 11 | Provision bare-metal hosts by either building and installing this docker-machine driver or downloading the latest [prebuilt release asset](https://github.com/equinix/docker-machine-driver-metal/releases) for your platform. The binaries must be placed in your `$PATH`. 12 | 13 | Test that the installation worked by typing in: 14 | 15 | ```sh 16 | docker-machine create --driver metal 17 | ``` 18 | 19 | You can find the supported arguments by running `docker-machine create -d metal --help` (Equinix Metal specific arguments are shown below): 20 | 21 | | Argument | Default | Description | Environment | Config | 22 | | --------------------------- | -------------- | ---------------------------------------------------------------------------- | ------------------------ | ----------------------- | 23 | | `--metal-api-key` | | Deprecated API Key flag (use auth token) | `METAL_API_KEY` | 24 | | `--metal-auth-token` | | Equinix Metal Authentication Token | `METAL_AUTH_TOKEN` | `token` or `auth-token` | 25 | | `--metal-billing-cycle` | `hourly` | Equinix Metal billing cycle, hourly or monthly | `METAL_BILLING_CYCLE` | 26 | | `--metal-facility-code` | | Equinix Metal facility code | `METAL_FACILITY_CODE` | `facility` | 27 | | `--metal-hw-reservation-id` | | Equinix Metal Reserved hardware ID | `METAL_HW_ID` | 28 | | `--metal-metro-code` | | Equinix Metal metro code ("dc" is used if empty and facility is not set) | `METAL_METRO_CODE` | `metro` | 29 | | `--metal-os` | `ubuntu_20_04` | Equinix Metal OS | `METAL_OS` | `operating-system` | 30 | | `--metal-plan` | `c3.small.x86` | Equinix Metal Server Plan | `METAL_PLAN` | `plan` | 31 | | `--metal-project-id` | | Equinix Metal Project Id | `METAL_PROJECT_ID` | `project` | 32 | | `--metal-spot-instance` | | Request a Equinix Metal Spot Instance | `METAL_SPOT_INSTANCE` | 33 | | `--metal-spot-price-max` | | The maximum Equinix Metal Spot Price | `METAL_SPOT_PRICE_MAX` | 34 | | `--metal-termination-time` | | The Equinix Metal Instance Termination Time | `METAL_TERMINATION_TIME` | 35 | | `--metal-ua-prefix` | | Prefix the User-Agent in Equinix Metal API calls with some 'product/version' | `METAL_UA_PREFIX` | 36 | | `--metal-userdata` | | Path to file with cloud-init user-data | `METAL_USERDATA` | 37 | 38 | Where denoted, values may be loaded from the environment or from the `~/.config/equinix/metal.yaml` file which can be created with the [Equinix Metal CLI](https://github.com/equinix/metal-cli#metal-cli). 39 | 40 | In order to support existing installations, a Packet branded binary is also available with each [release](https://github.com/equinix/docker-machine-driver-metal/releases) (after v0.5.0). When the `packet` binary is used, all `METAL` environment variables and `metal` arguments should be substituted for `PACKET` and `packet`, respectively. 41 | 42 | ### Example usage 43 | 44 | This creates the following: 45 | 46 | - c3.small.x86 machine 47 | - in the NY metro 48 | - with Ubuntu 20.04 49 | - in project $PROJECT 50 | - Using $API_KEY - [get yours from the Portal](https://console.equinix.com/users/me/api-keys) 51 | 52 | ```sh 53 | $ docker-machine create sloth \ 54 | --driver metal --metal-api-key=$API_KEY --metal-os=ubuntu_20_04 --metal-project-id=$PROJECT --metal-metro-code "ny" --metal-plan "c3.small.x86" 55 | 56 | Creating CA: /home/alex/.docker/machine/certs/ca.pem 57 | Creating client certificate: /home/alex/.docker/machine/certs/cert.pem 58 | Running pre-create checks... 59 | Creating machine... 60 | (sloth) Creating SSH key... 61 | (sloth) Provisioning Equinix Metal server... 62 | (sloth) Created device ID $DEVICE, IP address 147.x.x.x 63 | (sloth) Waiting for Provisioning... 64 | Waiting for machine to be running, this may take a few minutes... 65 | Detecting operating system of created instance... 66 | Waiting for SSH to be available... 67 | Detecting the provisioner... 68 | Provisioning with ubuntu(systemd)... 69 | Installing Docker... 70 | Copying certs to the local machine directory... 71 | Copying certs to the remote machine... 72 | Setting Docker configuration on the remote daemon... 73 | Checking connection to Docker... 74 | Docker is up and running! 75 | To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env sloth 76 | ``` 77 | 78 | > Provision time can take several minutes 79 | 80 | At this point you can now `docker-machine env sloth` and then start using your Docker bare-metal host! 81 | 82 | ## Development 83 | 84 | ### Building 85 | 86 | Pre-reqs: `docker-machine` and `make` 87 | 88 | - Install the Golang SDK [https://golang.org/dl/](https://golang.org/dl/) (at least 1.11 required for [modules](https://github.com/golang/go/wiki/Modules) support 89 | 90 | - Download the source-code with `git clone http://github.com/equinix/docker-machine-driver-metal.git` 91 | 92 | - Build and install the driver: 93 | 94 | ```sh 95 | cd docker-machine-driver-metal 96 | make 97 | sudo make install 98 | ``` 99 | 100 | Now you will now be able to specify a `-driver` of `metal` to `docker-machine` commands. 101 | 102 | ### Debugging 103 | 104 | To monitor the Docker debugging details and the Equinix Metal API calls: 105 | 106 | ```sh 107 | go build 108 | PACKNGO_DEBUG=1 PATH=`pwd`:$PATH docker-machine \ 109 | --debug create -d metal \ 110 | --metal-auth-token=$METAL_AUTH_TOKEN \ 111 | --metal-project-id=$METAL_PROJECT \ 112 | foo 113 | ``` 114 | 115 | ### Release Process 116 | 117 | This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 118 | 119 | Releases are handled by [GitHub Workflows](.github/workflows/release.yml) and [goreleaser](.goreleaser.yml). 120 | 121 | To push a new release, checkout the commit that you want released and: `make tag version=v0.2.3`. Robots handle the rest. 122 | 123 | Maintainers should verify that the release notes convey to users all of the notable changes between releases, in a human readable way. 124 | The format for each release should be based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). 125 | 126 | ## Releases and Changes 127 | 128 | See for the latest releases, install archives, and the project changelog. 129 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/equinix/docker-machine-driver-metal 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/carmo-evan/strtotime v0.0.0-20200108203155-3136cf889e3b 7 | github.com/docker/machine v0.16.2 8 | github.com/equinix/equinix-sdk-go v0.41.0 9 | github.com/stretchr/testify v1.9.0 10 | sigs.k8s.io/yaml v1.4.0 11 | ) 12 | 13 | require ( 14 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/docker/docker v20.10.27+incompatible // indirect 17 | github.com/moby/term v0.5.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/crypto v0.17.0 // indirect 20 | golang.org/x/sys v0.15.0 // indirect 21 | golang.org/x/term v0.15.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 2 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/carmo-evan/strtotime v0.0.0-20200108203155-3136cf889e3b h1:U3vQoEOmB8zQ34LR8jQt6XMIdOiq49OZ1aPGjG1fJKE= 4 | github.com/carmo-evan/strtotime v0.0.0-20200108203155-3136cf889e3b/go.mod h1:IqY2NtbgybBNEdxOZLXCF/OAAisAOiB4+F7/ovbQN8M= 5 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 6 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/docker/docker v20.10.27+incompatible h1:Id/ZooynV4ZlD6xX20RCd3SR0Ikn7r4QZDa2ECK2TgA= 10 | github.com/docker/docker v20.10.27+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 11 | github.com/docker/machine v0.16.2 h1:jyF9k3Zg+oIGxxSdYKPScyj3HqFZ6FjgA/3sblcASiU= 12 | github.com/docker/machine v0.16.2/go.mod h1:I8mPNDeK1uH+JTcUU7X0ZW8KiYz0jyAgNaeSJ1rCfDI= 13 | github.com/equinix/equinix-sdk-go v0.41.0 h1:yNOTOPsN6nGDcWaPSsKe8OrPcbSUucHXxl3yghQM2Gg= 14 | github.com/equinix/equinix-sdk-go v0.41.0/go.mod h1:hEb3XLaedz7xhl/dpPIS6eOIiXNPeqNiVoyDrT6paIg= 15 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 18 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 22 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 24 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 25 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 27 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 29 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 35 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/docker/machine/libmachine/drivers/plugin" 7 | metal "github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal" 8 | ) 9 | 10 | func main() { 11 | plugin.RegisterDriver(new(metal.Driver)) 12 | } 13 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /pkg/drivers/metal/config.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package metal 4 | 5 | import ( 6 | "os" 7 | "path" 8 | "runtime" 9 | ) 10 | 11 | type metalSnakeConfig struct { 12 | Token string `json:"token,omitempty"` 13 | AuthToken string `json:"auth-token,omitempty"` 14 | Facility string `json:"facility,omitempty"` 15 | Metro string `json:"metro,omitempty"` 16 | OS string `json:"operating-system,omitempty"` 17 | Plan string `json:"plan,omitempty"` 18 | ProjectID string `json:"project-id,omitempty"` 19 | } 20 | 21 | func getConfigFile() string { 22 | configFile := os.Getenv("METAL_CONFIG") 23 | if configFile != "" { 24 | return configFile 25 | } 26 | 27 | return path.Join(userHomeDir(), "/.config/equinix/metal.yaml") 28 | } 29 | 30 | func userHomeDir() string { 31 | if runtime.GOOS == "windows" { 32 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 33 | if home == "" { 34 | home = os.Getenv("USERPROFILE") 35 | } 36 | return home 37 | } 38 | return os.Getenv("HOME") 39 | } 40 | -------------------------------------------------------------------------------- /pkg/drivers/metal/metal.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package metal 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "io/fs" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/carmo-evan/strtotime" 17 | "github.com/docker/machine/libmachine/drivers" 18 | "github.com/docker/machine/libmachine/log" 19 | "github.com/docker/machine/libmachine/mcnflag" 20 | "github.com/docker/machine/libmachine/ssh" 21 | "github.com/docker/machine/libmachine/state" 22 | "github.com/equinix/equinix-sdk-go/services/metalv1" 23 | metal "github.com/equinix/equinix-sdk-go/services/metalv1" 24 | "sigs.k8s.io/yaml" 25 | ) 26 | 27 | const ( 28 | dockerConfigDir = "/etc/docker" 29 | consumerToken = "24e70949af5ecd17fe8e867b335fc88e7de8bd4ad617c0403d8769a376ddea72" 30 | defaultOS = "ubuntu_20_04" 31 | defaultMetro = "dc" 32 | ) 33 | 34 | type envSuffix string 35 | type argSuffix string 36 | 37 | var ( 38 | // version is set by goreleaser at build time 39 | version = "devel" 40 | 41 | driverName = "metal" 42 | 43 | envAuthToken envSuffix = "_AUTH_TOKEN" 44 | envApiKey envSuffix = "_API_KEY" 45 | envProjectID envSuffix = "_PROJECT_ID" 46 | envOS envSuffix = "_OS" 47 | envFacilityCode envSuffix = "_FACILITY_CODE" 48 | envMetroCode envSuffix = "_METRO_CODE" 49 | envPlan envSuffix = "_PLAN" 50 | envHwId envSuffix = "_HW_ID" 51 | envBillingCycle envSuffix = "_BILLING_CYCLE" 52 | envUserdata envSuffix = "_USERDATA" 53 | envSpotInstance envSuffix = "_SPOT_INSTANCE" 54 | envSpotPriceMax envSuffix = "_SPOT_PRICE_MAX" 55 | envTerminationTime envSuffix = "_TERMINATION_TIME" 56 | envUAPrefix envSuffix = "_UA_PREFIX" 57 | 58 | argAuthToken argSuffix = "-auth-token" 59 | argApiKey argSuffix = "-api-key" 60 | argProjectID argSuffix = "-project-id" 61 | argOS argSuffix = "-os" 62 | argFacilityCode argSuffix = "-facility-code" 63 | argMetroCode argSuffix = "-metro-code" 64 | argPlan argSuffix = "-plan" 65 | argHwId argSuffix = "-hw-reservation-id" 66 | argBillingCycle argSuffix = "-billing-cycle" 67 | argUserdata argSuffix = "-userdata" 68 | argSpotInstance argSuffix = "-spot-instance" 69 | argSpotPriceMax argSuffix = "-spot-price-max" 70 | argTerminationTime argSuffix = "-termination-time" 71 | argUAPrefix argSuffix = "-ua-prefix" 72 | 73 | // build time check that the Driver type implements the Driver interface 74 | _ drivers.Driver = &Driver{} 75 | ) 76 | 77 | func argPrefix(f argSuffix) string { 78 | return driverName + string(f) 79 | } 80 | 81 | func envPrefix(f envSuffix) string { 82 | return strings.ToUpper(driverName) + string(f) 83 | } 84 | 85 | type Driver struct { 86 | *drivers.BaseDriver 87 | ApiKey string 88 | ProjectID string 89 | Plan string 90 | HardwareReserverationID string 91 | Facility string 92 | Metro string 93 | OperatingSystem string 94 | BillingCycle string 95 | DeviceID string 96 | UserData string 97 | Tags []string 98 | CaCertPath string 99 | SSHKeyID string 100 | UserDataFile string 101 | UserAgentPrefix string 102 | SpotInstance bool 103 | SpotPriceMax float32 104 | TerminationTime *time.Time 105 | } 106 | 107 | // NewDriver is a backward compatible Driver factory method. Using 108 | // new(metal.Driver) is preferred. 109 | func NewDriver(hostName, storePath string) *Driver { 110 | return &Driver{ 111 | BaseDriver: &drivers.BaseDriver{ 112 | MachineName: hostName, 113 | StorePath: storePath, 114 | }, 115 | } 116 | } 117 | 118 | func (d *Driver) GetCreateFlags() []mcnflag.Flag { 119 | return []mcnflag.Flag{ 120 | mcnflag.StringFlag{ 121 | Name: argPrefix(argAuthToken), 122 | Usage: "Equinix Metal Authentication Token", 123 | EnvVar: envPrefix(envAuthToken), 124 | }, 125 | mcnflag.StringFlag{ 126 | Name: argPrefix(argApiKey), 127 | Usage: "Authentication Key (deprecated name, use Auth Token)", 128 | EnvVar: envPrefix(envApiKey), 129 | }, 130 | mcnflag.StringFlag{ 131 | Name: argPrefix(argProjectID), 132 | Usage: "Equinix Metal Project Id", 133 | EnvVar: envPrefix(envProjectID), 134 | }, 135 | mcnflag.StringFlag{ 136 | Name: argPrefix(argOS), 137 | Usage: "Equinix Metal OS", 138 | Value: defaultOS, 139 | EnvVar: envPrefix(envOS), 140 | }, 141 | mcnflag.StringFlag{ 142 | Name: argPrefix(argFacilityCode), 143 | Usage: "Equinix Metal facility code", 144 | EnvVar: envPrefix(envFacilityCode), 145 | }, 146 | mcnflag.StringFlag{ 147 | Name: argPrefix(argMetroCode), 148 | Usage: fmt.Sprintf("Equinix Metal metro code (%q is used if empty and facility is not set)", defaultMetro), 149 | EnvVar: envPrefix(envMetroCode), 150 | // We don't set Value because Facility was previously required and 151 | // defaulted. Existing configurations with "Facility" should not 152 | // break. Setting a default metro value would break those 153 | // configurations. 154 | }, 155 | mcnflag.StringFlag{ 156 | Name: argPrefix(argPlan), 157 | Usage: "Equinix Metal Server Plan", 158 | Value: "c3.small.x86", 159 | EnvVar: envPrefix(envPlan), 160 | }, 161 | mcnflag.StringFlag{ 162 | Name: argPrefix(argHwId), 163 | Usage: "Equinix Metal Reserved hardware ID", 164 | EnvVar: envPrefix(envHwId), 165 | }, 166 | mcnflag.StringFlag{ 167 | Name: argPrefix(argBillingCycle), 168 | Usage: "Equinix Metal billing cycle, hourly or monthly", 169 | Value: "hourly", 170 | EnvVar: envPrefix(envBillingCycle), 171 | }, 172 | mcnflag.StringFlag{ 173 | Name: argPrefix(argUserdata), 174 | Usage: "Path to file with cloud-init user-data", 175 | EnvVar: envPrefix(envUserdata), 176 | }, 177 | mcnflag.BoolFlag{ 178 | Name: argPrefix(argSpotInstance), 179 | Usage: "Request a Equinix Metal Spot Instance", 180 | EnvVar: envPrefix(envSpotInstance), 181 | }, 182 | mcnflag.StringFlag{ 183 | Name: argPrefix(argSpotPriceMax), 184 | Usage: "The maximum Equinix Metal Spot Price", 185 | EnvVar: envPrefix(envSpotPriceMax), 186 | }, 187 | mcnflag.StringFlag{ 188 | Name: argPrefix(argTerminationTime), 189 | Usage: "The Equinix Metal Instance Termination Time", 190 | EnvVar: envPrefix(envTerminationTime), 191 | }, 192 | mcnflag.StringFlag{ 193 | Name: argPrefix(argUAPrefix), 194 | Usage: fmt.Sprintf("Prefix the User-Agent in Equinix Metal API calls with some 'product/version' %s %s", version, driverName), 195 | EnvVar: envPrefix(envUAPrefix), 196 | }, 197 | } 198 | } 199 | 200 | func (d *Driver) DriverName() string { 201 | return driverName 202 | } 203 | 204 | func (d *Driver) setConfigFromFile() error { 205 | configFile := getConfigFile() 206 | 207 | config := metalSnakeConfig{} 208 | 209 | if raw, err := os.ReadFile(configFile); err != nil { 210 | if errors.Is(err, fs.ErrNotExist) { 211 | return nil 212 | } 213 | return err 214 | } else if jsonErr := yaml.Unmarshal(raw, &config); jsonErr != nil { 215 | return jsonErr 216 | } 217 | d.Plan = config.Plan 218 | d.ApiKey = config.AuthToken 219 | if config.Token != "" { 220 | d.ApiKey = config.Token 221 | } 222 | d.Facility = config.Facility 223 | d.Metro = config.Metro 224 | d.OperatingSystem = config.OS 225 | d.ProjectID = config.ProjectID 226 | return nil 227 | } 228 | 229 | func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { 230 | if err := d.setConfigFromFile(); err != nil { 231 | return err 232 | } 233 | // override config file values with command-line values 234 | for k, p := range map[string]*string{ 235 | argPrefix(argOS): &d.OperatingSystem, 236 | argPrefix(argAuthToken): &d.ApiKey, 237 | argPrefix(argProjectID): &d.ProjectID, 238 | argPrefix(argMetroCode): &d.Metro, 239 | argPrefix(argFacilityCode): &d.Facility, 240 | argPrefix(argPlan): &d.Plan, 241 | } { 242 | if v := flags.String(k); v != "" { 243 | *p = v 244 | } 245 | } 246 | 247 | oldApiKey := flags.String(argPrefix(argApiKey)) 248 | 249 | if d.ApiKey == "" { 250 | d.ApiKey = oldApiKey 251 | 252 | if d.ApiKey == "" { 253 | return fmt.Errorf("%s driver requires the --%s option", driverName, argPrefix(argAuthToken)) 254 | } 255 | } else if oldApiKey != "" { 256 | log.Warnf("ignoring API Key setting (%s, %s)", argPrefix(argApiKey), envPrefix(envApiKey)) 257 | } 258 | 259 | if strings.Contains(d.OperatingSystem, "coreos") { 260 | d.SSHUser = "core" 261 | } 262 | if strings.Contains(d.OperatingSystem, "rancher") { 263 | d.SSHUser = "rancher" 264 | } 265 | 266 | d.BillingCycle = flags.String(argPrefix(argBillingCycle)) 267 | d.UserAgentPrefix = flags.String(argPrefix(argUAPrefix)) 268 | d.UserDataFile = flags.String(argPrefix(argUserdata)) 269 | d.HardwareReserverationID = flags.String(argPrefix(argHwId)) 270 | d.SpotInstance = flags.Bool(argPrefix(argSpotInstance)) 271 | 272 | if d.SpotInstance { 273 | SpotPriceMax := flags.String(argPrefix(argSpotPriceMax)) 274 | if SpotPriceMax == "" { 275 | d.SpotPriceMax = -1 276 | } else { 277 | SpotPriceMax, err := strconv.ParseFloat(SpotPriceMax, 32) 278 | if err != nil { 279 | return err 280 | } 281 | d.SpotPriceMax = float32(SpotPriceMax) 282 | } 283 | 284 | TerminationTime := flags.String(argPrefix(argTerminationTime)) 285 | if TerminationTime == "" { 286 | d.TerminationTime = nil 287 | } else { 288 | Timestamp, err := strtotime.Parse(TerminationTime, time.Now().Unix()) 289 | if err != nil { 290 | return err 291 | } 292 | if Timestamp <= time.Now().Unix() { 293 | return fmt.Errorf("--%s cannot be in the past", argPrefix(argTerminationTime)) 294 | } 295 | t := time.Unix(Timestamp, 0) 296 | d.TerminationTime = &t 297 | } 298 | } 299 | 300 | if d.ProjectID == "" { 301 | return fmt.Errorf("%s driver requires the --%s option", driverName, argPrefix(argProjectID)) 302 | } 303 | 304 | return nil 305 | } 306 | 307 | func (d *Driver) GetSSHHostname() (string, error) { 308 | return d.GetIP() 309 | } 310 | 311 | func (d *Driver) PreCreateCheck() error { 312 | if d.UserDataFile != "" { 313 | if _, err := os.Stat(d.UserDataFile); os.IsNotExist(err) { 314 | return fmt.Errorf("user-data file %s could not be found", d.UserDataFile) 315 | } 316 | } 317 | 318 | flavors, err := d.getOsFlavors() 319 | if err != nil { 320 | return err 321 | } 322 | if !stringInSlice(d.OperatingSystem, flavors) { 323 | return fmt.Errorf("specified --%s not one of %v", argPrefix(argOS), strings.Join(flavors, ", ")) 324 | } 325 | 326 | if d.Metro == "" && d.Facility == "" { 327 | d.Metro = defaultMetro 328 | } 329 | 330 | if d.Metro != "" && d.Facility != "" { 331 | return fmt.Errorf("facility and metro can not be used together") 332 | } 333 | 334 | client := d.getClient() 335 | 336 | if d.Metro != "" { 337 | return validateMetro(client, d.Metro) 338 | } 339 | 340 | return validateFacility(client, d.Facility) 341 | } 342 | 343 | type DeviceCreator interface { 344 | SetPlan(string) 345 | SetOperatingSystem(string) 346 | SetHostname(string) 347 | SetUserdata(string) 348 | SetTags([]string) 349 | SetHardwareReservationId(string) 350 | SetBillingCycle(metalv1.DeviceCreateInputBillingCycle) 351 | SetSpotInstance(bool) 352 | SetSpotPriceMax(float32) 353 | SetTerminationTime(time.Time) 354 | } 355 | 356 | type OneOfDeviceCreator interface { 357 | DeviceCreator 358 | GetActualInstance() interface{} 359 | } 360 | 361 | var _ DeviceCreator = (*metal.DeviceCreateInMetroInput)(nil) 362 | var _ DeviceCreator = (*metal.DeviceCreateInFacilityInput)(nil) 363 | 364 | func (d *Driver) Create() error { 365 | var userdata string 366 | if d.UserDataFile != "" { 367 | buf, err := os.ReadFile(d.UserDataFile) 368 | if err != nil { 369 | return err 370 | } 371 | userdata = string(buf) 372 | } 373 | 374 | log.Info("Creating SSH key...") 375 | 376 | key, err := d.createSSHKey() 377 | if err != nil { 378 | return err 379 | } 380 | 381 | d.SSHKeyID = key.GetId() 382 | 383 | hardwareReservationId := "" 384 | //check if hardware reservation requested 385 | if d.HardwareReserverationID != "" { 386 | hardwareReservationId = d.HardwareReserverationID 387 | } 388 | 389 | client := d.getClient() 390 | 391 | var dc DeviceCreator 392 | var createRequest metal.CreateDeviceRequest 393 | 394 | if d.Facility != "" { 395 | dc = &metal.DeviceCreateInFacilityInput{ 396 | Facility: []string{d.Facility}, 397 | } 398 | createRequest = metal.CreateDeviceRequest{DeviceCreateInFacilityInput: dc.(*metal.DeviceCreateInFacilityInput)} 399 | } else { 400 | dc = &metal.DeviceCreateInMetroInput{ 401 | Metro: d.Metro, 402 | } 403 | createRequest = metal.CreateDeviceRequest{DeviceCreateInMetroInput: dc.(*metal.DeviceCreateInMetroInput)} 404 | } 405 | 406 | dc.SetHostname(d.MachineName) 407 | dc.SetPlan(d.Plan) 408 | dc.SetHardwareReservationId(hardwareReservationId) 409 | dc.SetOperatingSystem(d.OperatingSystem) 410 | dc.SetBillingCycle(metalv1.DeviceCreateInputBillingCycle(d.BillingCycle)) 411 | dc.SetUserdata(userdata) 412 | dc.SetTags(d.Tags) 413 | dc.SetSpotInstance(d.SpotInstance) 414 | dc.SetSpotPriceMax(d.SpotPriceMax) 415 | if d.TerminationTime != nil { 416 | dc.SetTerminationTime(*d.TerminationTime) 417 | } 418 | 419 | log.Info("Provisioning Equinix Metal server...") 420 | newDevice, _, err := client.DevicesApi.CreateDevice(context.TODO(), d.ProjectID).CreateDeviceRequest(createRequest).Execute() 421 | if err != nil { 422 | log.Errorf("device could not be created: %s", err) 423 | 424 | //cleanup ssh keys if device failed 425 | if resp, err := client.SSHKeysApi.DeleteSSHKey(context.TODO(), d.SSHKeyID).Execute(); ignoreStatusCodes(resp, err, http.StatusForbidden, http.StatusNotFound) != nil { 426 | log.Errorf("ssh-key could not be deleted: %s", err) 427 | return err 428 | } 429 | return err 430 | } 431 | t0 := time.Now() 432 | 433 | d.DeviceID = newDevice.GetId() 434 | 435 | for { 436 | newDevice, _, err = client.DevicesApi.FindDeviceById(context.TODO(), d.DeviceID).Execute() 437 | if err != nil { 438 | return err 439 | } 440 | 441 | for _, ip := range newDevice.GetIpAddresses() { 442 | if ip.GetPublic() && ip.GetAddressFamily() == 4 { 443 | d.IPAddress = ip.GetAddress() 444 | } 445 | } 446 | 447 | if d.IPAddress != "" { 448 | break 449 | } 450 | 451 | time.Sleep(1 * time.Second) 452 | } 453 | 454 | log.Infof("Created device ID %s, IP address %s", 455 | newDevice.GetId(), 456 | d.IPAddress) 457 | 458 | log.Info("Waiting for Provisioning...") 459 | stage := float32(0) 460 | for { 461 | newDevice, _, err = client.DevicesApi.FindDeviceById(context.TODO(), d.DeviceID).Execute() 462 | if err != nil { 463 | return err 464 | } 465 | if newDevice.GetState() == metal.DEVICESTATE_PROVISIONING && stage != newDevice.GetProvisioningPercentage() { 466 | stage = newDevice.GetProvisioningPercentage() 467 | log.Debugf("Provisioning %v%% complete", newDevice.GetProvisioningPercentage()) 468 | } 469 | if newDevice.GetState() == metal.DEVICESTATE_ACTIVE { 470 | log.Debugf("Device State: %s", newDevice.GetState()) 471 | break 472 | } 473 | time.Sleep(10 * time.Second) 474 | } 475 | 476 | log.Debugf("Provision time: %v.", time.Since(t0)) 477 | 478 | log.Debug("Waiting for SSH...") 479 | if err := drivers.WaitForSSH(d); err != nil { 480 | return err 481 | } 482 | 483 | return nil 484 | } 485 | 486 | func (d *Driver) createSSHKey() (*metal.SSHKey, error) { 487 | sshKeyPath := d.GetSSHKeyPath() 488 | log.Debugf("Writing SSH Key to %s", sshKeyPath) 489 | 490 | if err := ssh.GenerateSSHKey(sshKeyPath); err != nil { 491 | return nil, err 492 | } 493 | 494 | publicKey, err := os.ReadFile(sshKeyPath + ".pub") 495 | if err != nil { 496 | return nil, err 497 | } 498 | 499 | createRequest := metal.SSHKeyCreateInput{} 500 | createRequest.SetLabel(fmt.Sprintf("docker machine: %s", d.MachineName)) 501 | createRequest.SetKey(string(publicKey)) 502 | r := metal.ApiCreateSSHKeyRequest{} 503 | r.SSHKeyCreateInput(createRequest) 504 | 505 | key, _, err := d.getClient().SSHKeysApi.CreateSSHKeyExecute(r) 506 | if err != nil { 507 | return key, err 508 | } 509 | 510 | return key, nil 511 | } 512 | 513 | func (d *Driver) GetURL() (string, error) { 514 | ip, err := d.GetIP() 515 | if err != nil { 516 | return "", err 517 | } 518 | return fmt.Sprintf("tcp://%s:2376", ip), nil 519 | } 520 | 521 | func (d *Driver) GetIP() (string, error) { 522 | if d.IPAddress == "" { 523 | return "", fmt.Errorf("IP address is not set") 524 | } 525 | return d.IPAddress, nil 526 | } 527 | 528 | func (d *Driver) GetState() (state.State, error) { 529 | device, _, err := d.getClient().DevicesApi.FindDeviceById(context.TODO(), d.DeviceID).Execute() 530 | if err != nil { 531 | return state.Error, err 532 | } 533 | 534 | switch device.GetState() { 535 | case metal.DEVICESTATE_QUEUED, metal.DEVICESTATE_PROVISIONING, metal.DEVICESTATE_POWERING_ON: 536 | return state.Starting, nil 537 | case metal.DEVICESTATE_ACTIVE: 538 | return state.Running, nil 539 | case metal.DEVICESTATE_POWERING_OFF: 540 | return state.Stopping, nil 541 | case metal.DEVICESTATE_INACTIVE: 542 | return state.Stopped, nil 543 | } 544 | return state.None, nil 545 | } 546 | 547 | func (d *Driver) Start() error { 548 | r := metal.DeviceActionInput{Type: metal.DEVICEACTIONINPUTTYPE_POWER_ON} 549 | _, err := d.getClient().DevicesApi.PerformAction(context.TODO(), d.DeviceID).DeviceActionInput(r).Execute() 550 | return err 551 | } 552 | 553 | func (d *Driver) Stop() error { 554 | r := metal.DeviceActionInput{Type: metal.DEVICEACTIONINPUTTYPE_POWER_OFF} 555 | _, err := d.getClient().DevicesApi.PerformAction(context.TODO(), d.DeviceID).DeviceActionInput(r).Execute() 556 | return err 557 | } 558 | 559 | func ignoreStatusCodes(resp *http.Response, err error, codes ...int) error { 560 | if err == nil && resp == nil { 561 | return nil 562 | } 563 | if err != nil { 564 | for _, c := range codes { 565 | if resp.StatusCode == c { 566 | return nil 567 | } 568 | } 569 | } 570 | 571 | return err 572 | } 573 | 574 | func (d *Driver) Remove() error { 575 | client := d.getClient() 576 | if resp, err := client.SSHKeysApi.DeleteSSHKey(context.TODO(), d.SSHKeyID).Execute(); ignoreStatusCodes(resp, err, http.StatusForbidden, http.StatusNotFound) != nil { 577 | return err 578 | } 579 | 580 | resp, err := client.DevicesApi.DeleteDevice(context.TODO(), d.DeviceID).Execute() 581 | return ignoreStatusCodes(resp, err, http.StatusForbidden, http.StatusNotFound) 582 | } 583 | 584 | func (d *Driver) Restart() error { 585 | r := metal.DeviceActionInput{Type: metal.DEVICEACTIONINPUTTYPE_REBOOT} 586 | _, err := d.getClient().DevicesApi.PerformAction(context.TODO(), d.DeviceID).DeviceActionInput(r).Execute() 587 | return err 588 | } 589 | 590 | func (d *Driver) Kill() error { 591 | return d.Stop() 592 | } 593 | 594 | func (d *Driver) GetDockerConfigDir() string { 595 | return dockerConfigDir 596 | } 597 | 598 | func (d *Driver) getClient() *metal.APIClient { 599 | config := metal.NewConfiguration() 600 | config.AddDefaultHeader("X-Consumer-Token", consumerToken) 601 | config.AddDefaultHeader("X-Auth-Token", d.ApiKey) 602 | userAgent := fmt.Sprintf("docker-machine-driver-%s/%s %s", d.DriverName(), version, config.UserAgent) 603 | if len(d.UserAgentPrefix) > 0 { 604 | userAgent = fmt.Sprintf("%s %s", d.UserAgentPrefix, userAgent) 605 | } 606 | config.UserAgent = userAgent 607 | client := metal.NewAPIClient(config) 608 | 609 | return client 610 | } 611 | 612 | func (d *Driver) getOsFlavors() ([]string, error) { 613 | operatingSystems, _, err := d.getClient().OperatingSystemsApi.FindOperatingSystems(context.TODO()).Execute() 614 | if err != nil { 615 | return nil, err 616 | } 617 | 618 | supportedDistros := []string{ 619 | "centos", 620 | "coreos", 621 | "debian", 622 | "opensuse", 623 | "rancher", 624 | "ubuntu", 625 | } 626 | flavors := make([]string, 0, len(operatingSystems.OperatingSystems)) 627 | for _, flavor := range operatingSystems.OperatingSystems { 628 | if stringInSlice(flavor.GetDistro(), supportedDistros) { 629 | flavors = append(flavors, flavor.GetSlug()) 630 | } 631 | } 632 | return flavors, nil 633 | } 634 | 635 | func validateFacility(client *metal.APIClient, facility string) error { 636 | if facility == "any" { 637 | return nil 638 | } 639 | 640 | facilities, _, err := client.FacilitiesApi.FindFacilities(context.TODO()).Execute() 641 | if err != nil { 642 | return err 643 | } 644 | for _, f := range facilities.Facilities { 645 | if f.GetCode() == facility { 646 | return nil 647 | } 648 | } 649 | 650 | return fmt.Errorf("%s requires a valid facility", driverName) 651 | } 652 | 653 | func validateMetro(client *metal.APIClient, metro string) error { 654 | metros, _, err := client.MetrosApi.FindMetros(context.TODO()).Execute() 655 | if err != nil { 656 | return err 657 | } 658 | for _, m := range metros.Metros { 659 | if m.GetCode() == metro { 660 | return nil 661 | } 662 | } 663 | 664 | return fmt.Errorf("%s requires a valid metro", driverName) 665 | } 666 | 667 | func stringInSlice(a string, list []string) bool { 668 | for _, b := range list { 669 | if b == a { 670 | return true 671 | } 672 | } 673 | return false 674 | } 675 | -------------------------------------------------------------------------------- /pkg/drivers/metal/metal_test.go: -------------------------------------------------------------------------------- 1 | package metal 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/docker/machine/libmachine/drivers" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetConfigFromFlags(t *testing.T) { 12 | driver := NewDriver("", "") 13 | configPath := os.Getenv("METAL_CONFIG") 14 | os.Setenv("METAL_CONFIG", "/does-not-exist") 15 | checkFlags := &drivers.CheckDriverOptions{ 16 | FlagsValues: map[string]interface{}{ 17 | "metal-api-key": "APIKEY", 18 | "metal-project-id": "PROJECT", 19 | }, 20 | CreateFlags: driver.GetCreateFlags(), 21 | } 22 | 23 | err := driver.SetConfigFromFlags(checkFlags) 24 | os.Setenv("METAL_CONFIG", configPath) 25 | assert.NoError(t, err) 26 | assert.Empty(t, checkFlags.InvalidFlags) 27 | } 28 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------