├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── feature.yml │ └── help.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── labels.yml ├── release.yml └── workflows │ ├── build-test.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── labeler.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go └── pkg └── drivers └── linode ├── linode.go └── linode_test.go /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: input 7 | id: docker-version 8 | attributes: 9 | label: Docker Version 10 | description: What version of Docker are you running? `docker version` 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: error 16 | attributes: 17 | label: Error Output 18 | description: If you received an error output that is too long, use Gists 19 | 20 | - type: textarea 21 | id: expected 22 | attributes: 23 | label: Expected Behavior 24 | description: What should have happened? 25 | 26 | - type: textarea 27 | id: actual 28 | attributes: 29 | label: Actual Behavior 30 | description: What actually happened? 31 | 32 | - type: textarea 33 | id: reproduce 34 | attributes: 35 | label: Steps to Reproduce 36 | description: List any custom configurations and the steps to reproduce this error 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement 2 | description: Request a feature 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: What would you like this feature to do in detail? 11 | validations: 12 | required: true 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.yml: -------------------------------------------------------------------------------- 1 | name: Help 2 | description: You're pretty sure it's not a bug but you can't figure out why it's not working 3 | title: "[Help]: " 4 | labels: ["help wanted"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: What are you attempting to do, what error messages are you getting? 11 | validations: 12 | required: true 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 Description 2 | 3 | **What does this PR do and why is this change necessary?** 4 | 5 | ## ✔️ How to Test 6 | 7 | **What are the steps to reproduce the issue or verify the changes?** 8 | 9 | **How do I run the relevant unit/integration tests?** 10 | 11 | ## 📷 Preview 12 | 13 | **If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | # PR Labels 2 | - name: new-feature 3 | description: for new features in the changelog. 4 | color: 225fee 5 | - name: project 6 | description: for new projects in the changelog. 7 | color: 46BAF0 8 | - name: improvement 9 | description: for improvements in existing functionality in the changelog. 10 | color: 22ee47 11 | - name: repo-ci-improvement 12 | description: for improvements in the repository or CI workflow in the changelog. 13 | color: c922ee 14 | - name: bugfix 15 | description: for any bug fixes in the changelog. 16 | color: ed8e21 17 | - name: documentation 18 | description: for updates to the documentation in the changelog. 19 | color: d3e1e6 20 | - name: dependencies 21 | description: dependency updates usually from dependabot 22 | color: 5c9dff 23 | - name: testing 24 | description: for updates to the testing suite in the changelog. 25 | color: 933ac9 26 | - name: breaking-change 27 | description: for breaking changes in the changelog. 28 | color: ff0000 29 | - name: ignore-for-release 30 | description: PRs you do not want to render in the changelog 31 | color: 7b8eac 32 | - name: do-not-merge 33 | description: PRs that should not be merged until the commented issue is resolved 34 | color: eb1515 35 | # Issue Labels 36 | - name: enhancement 37 | description: issues that request a enhancement 38 | color: 22ee47 39 | - name: bug 40 | description: issues that report a bug 41 | color: ed8e21 42 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: 📋 New Project 7 | labels: 8 | - project 9 | - title: ⚠️ Breaking Change 10 | labels: 11 | - breaking-change 12 | - title: 🐛 Bug Fixes 13 | labels: 14 | - bugfix 15 | - title: 🚀 New Features 16 | labels: 17 | - new-feature 18 | - title: 💡 Improvements 19 | labels: 20 | - improvement 21 | - title: 🧪 Testing Improvements 22 | labels: 23 | - testing 24 | - title: ⚙️ Repo/CI Improvements 25 | labels: 26 | - repo-ci-improvement 27 | - title: 📖 Documentation 28 | labels: 29 | - documentation 30 | - title: 📦 Dependency Updates 31 | labels: 32 | - dependencies 33 | - title: Other Changes 34 | labels: 35 | - "*" 36 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: build and test 2 | 3 | on: 4 | workflow_dispatch: null 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | go-version: ["stable", "oldstable"] 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | 25 | - name: Build binary 26 | run: make 27 | 28 | - name: Test 29 | run: make test 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Advanced" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '0 13 * * 5' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - language: go 23 | build-mode: autobuild 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | build-mode: ${{ matrix.build-mode }} 34 | queries: security-and-quality 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v3 38 | with: 39 | category: "/language:${{matrix.language}}" 40 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency review' 2 | on: 3 | pull_request: 4 | branches: [ "main" ] 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: write 9 | 10 | jobs: 11 | dependency-review: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 'Checkout repository' 15 | uses: actions/checkout@v4 16 | - name: 'Dependency Review' 17 | uses: actions/dependency-review-action@v4 18 | with: 19 | comment-summary-in-pr: on-failure 20 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: labeler 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '.github/labels.yml' 9 | - '.github/workflows/labeler.yml' 10 | pull_request: 11 | paths: 12 | - '.github/labels.yml' 13 | - '.github/workflows/labeler.yml' 14 | 15 | jobs: 16 | labeler: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@v4 22 | - 23 | name: Run Labeler 24 | uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | yaml-file: .github/labels.yml 28 | dry-run: ${{ github.event_name == 'pull_request' }} 29 | exclude: | 30 | help* 31 | *issue 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "stable" 20 | 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6 23 | with: 24 | distribution: goreleaser 25 | version: latest 26 | args: release --clean 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | vendor 27 | # Bin 28 | docker-machine-driver-linode 29 | dist 30 | out 31 | 32 | dist/ 33 | 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | # You may remove this if you don't use go modules. 5 | - go mod tidy 6 | # you may remove this if you don't need go generate 7 | - go generate ./... 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | archives: 16 | - id: linode-docker-machine-archives 17 | format: zip 18 | name_template: >- 19 | {{ .ProjectName }}_ 20 | {{- .Os }}- 21 | {{- if eq .Arch "386" }}i386 22 | {{- else }}{{ .Arch }}{{ end }} 23 | checksum: 24 | name_template: 'checksums.txt' 25 | snapshot: 26 | name_template: "{{ incpatch .Version }}-next" 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - '^docs:' 32 | - '^test:' 33 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @linode/dx 2 | 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | :+1::tada: First off, we appreciate you taking the time to contribute! THANK YOU! :tada::+1: 4 | 5 | We put together the handy guide below to help you get support for your work. Read on! 6 | 7 | ## I Just Want to Ask the Maintainers a Question 8 | 9 | The [Linode Community](https://www.linode.com/community/questions/) is a great place to get additional support. 10 | 11 | ## How Do I Submit A (Good) Bug Report or Feature Request 12 | 13 | Please open a [GitHub issue](https://github.com/linode/docker-machine-driver-linode/issues/new/choose) to report bugs or suggest features. 14 | 15 | Please accurately fill out the appropriate GitHub issue form. 16 | 17 | When filing an issue or feature request, help us avoid duplication and redundant effort -- check existing open or recently closed issues first. 18 | 19 | Detailed bug reports and requests are easier for us to work with. Please include the following in your issue: 20 | 21 | * A reproducible test case or series of steps 22 | * The version of our code being used 23 | * Any modifications you've made, relevant to the bug 24 | * Anything unusual about your environment or deployment 25 | * Screenshots and code samples where illustrative and helpful 26 | 27 | ## How to Open a Pull Request 28 | 29 | We follow the [fork and pull model](https://opensource.guide/how-to-contribute/#opening-a-pull-request) for open source contributions. 30 | 31 | Tips for a faster merge: 32 | * address one feature or bug per pull request. 33 | * large formatting changes make it hard for us to focus on your work. 34 | * follow language coding conventions. 35 | * make sure that tests pass. 36 | * make sure your commits are atomic, [addressing one change per commit](https://chris.beams.io/posts/git-commit/). 37 | * add tests! 38 | 39 | ## Code of Conduct 40 | 41 | This project follows the [Linode Community Code of Conduct](https://www.linode.com/community/questions/conduct). 42 | 43 | ## Vulnerability Reporting 44 | 45 | If you discover a potential security issue in this project we ask that you notify Linode Security via our [vulnerability reporting process](https://hackerone.com/linode). Please do **not** create a public github issue. 46 | 47 | ## Licensing 48 | 49 | See the [LICENSE file](/LICENSE) for our project's licensing. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 TH 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT_DIR := out 2 | PROG := docker-machine-driver-linode 3 | 4 | GOOS ?= $(shell go env GOOS) 5 | GOARCH ?= $(shell go env GOARCH) 6 | 7 | export GO111MODULE=on 8 | 9 | ifeq ($(GOOS),windows) 10 | BIN_SUFFIX := ".exe" 11 | endif 12 | 13 | .PHONY: build 14 | build: dep 15 | go build -ldflags "-X github.com/linode/docker-machine-driver-linode/pkg/drivers/linode.VERSION=`git describe --always`" -o $(OUT_DIR)/$(PROG)$(BIN_SUFFIX) ./ 16 | 17 | .PHONY: dep 18 | dep: 19 | @GO111MODULE=on 20 | go get -d ./ 21 | go mod verify 22 | 23 | .PHONY: test 24 | test: dep 25 | go test -race ./... 26 | 27 | .PHONY: check 28 | check: 29 | gofmt -l -s -d pkg/ 30 | go vet 31 | 32 | .PHONY: clean 33 | clean: 34 | $(RM) $(OUT_DIR)/$(PROG)$(BIN_SUFFIX) 35 | 36 | .PHONY: uninstall 37 | uninstall: 38 | $(RM) $(GOPATH)/bin/$(PROG)$(BIN_SUFFIX) 39 | 40 | .PHONY: install 41 | install: build 42 | go install 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-machine-driver-linode 2 | 3 | [![GoDoc](https://godoc.org/github.com/linode/docker-machine-driver-linode?status.svg)](https://godoc.org/github.com/linode/docker-machine-driver-linode) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/linode/docker-machine-driver-linode)](https://goreportcard.com/report/github.com/linode/docker-machine-driver-linode) 5 | [![GitHub release](https://img.shields.io/github/release/linode/docker-machine-driver-linode.svg)](https://github.com/linode/docker-machine-driver-linode/releases/) 6 | 7 | Linode Driver Plugin for docker-machine. 8 | 9 | ## Install 10 | 11 | `docker-machine` is required, [see the installation documentation](https://docs.docker.com/machine/install-machine/). 12 | 13 | Then, install the latest release for your environment from the [releases list](https://github.com/linode/docker-machine-driver-linode/releases). 14 | 15 | ### Installing from source 16 | 17 | If you would rather build from source, you will need to have a working `go` 1.11+ environment, 18 | 19 | ```bash 20 | eval $(go env) 21 | export PATH="$PATH:$GOPATH/bin" 22 | ``` 23 | 24 | You can then install `docker-machine` from source by running: 25 | 26 | ```bash 27 | go get github.com/docker/machine 28 | cd $GOPATH/src/github.com/docker/machine 29 | make build 30 | ``` 31 | 32 | And then compile the `docker-machine-driver-linode` driver: 33 | 34 | ```bash 35 | go get github.com/linode/docker-machine-driver-linode 36 | cd $GOPATH/src/github.com/linode/docker-machine-driver-linode 37 | make install 38 | ``` 39 | 40 | ## Run 41 | 42 | You will need a Linode APIv4 Personal Access Token. Get one here: 43 | 44 | ```bash 45 | docker-machine create -d linode --linode-token= linode 46 | ``` 47 | 48 | ### Options 49 | 50 | | Argument | Env | Default | Description 51 | | --- | --- | --- | --- 52 | | `linode-token` | `LINODE_TOKEN` | None | **required** Linode APIv4 Token (see [here](https://www.linode.com/docs/products/tools/api/guides/manage-api-tokens/)) 53 | | `linode-root-pass` | `LINODE_ROOT_PASSWORD` | *generated* | The Linode Instance `root_pass` (password assigned to the `root` account) 54 | | `linode-authorized-users` | `LINODE_AUTHORIZED_USERS` | None | Linode user accounts (separated by commas) whose Linode SSH keys will be permitted root access to the created node 55 | | `linode-label` | `LINODE_LABEL` | *generated* | The Linode Instance `label`, unless overridden this will match the docker-machine name. This `label` must be unique on the account. 56 | | `linode-region` | `LINODE_REGION` | `us-east` | The Linode Instance `region` (see [here](https://api.linode.com/v4/regions)) 57 | | `linode-instance-type` | `LINODE_INSTANCE_TYPE` | `g6-standard-4` | The Linode Instance `type` (see [here](https://api.linode.com/v4/linode/types)) 58 | | `linode-image` | `LINODE_IMAGE` | `linode/ubuntu18.04` | The Linode Instance `image` which provides the Linux distribution (see [here](https://api.linode.com/v4/images)). 59 | | `linode-ssh-port` | `LINODE_SSH_PORT` | `22` | The port that SSH is running on, needed for Docker Machine to provision the Linode. 60 | | `linode-ssh-user` | `LINODE_SSH_USER` | `root` | The user as which docker-machine should log in to the Linode instance to install Docker. This user must have passwordless sudo. 61 | | `linode-docker-port` | `LINODE_DOCKER_PORT` | `2376` | The TCP port of the Linode that Docker will be listening on 62 | | `linode-swap-size` | `LINODE_SWAP_SIZE` | `512` | The amount of swap space provisioned on the Linode Instance 63 | | `linode-stackscript` | `LINODE_STACKSCRIPT` | None | Specifies the Linode StackScript to use to create the instance, either by numeric ID, or using the form *username*/*label*. 64 | | `linode-stackscript-data` | `LINODE_STACKSCRIPT_DATA` | None | A JSON string specifying data that is passed (via UDF) to the selected StackScript. 65 | | `linode-create-private-ip` | `LINODE_CREATE_PRIVATE_IP` | None | A flag specifying to create private IP for the Linode instance. 66 | | `linode-tags` | `LINODE_TAGS` | None | A comma separated list of tags to apply to the Linode resource 67 | | `linode-ua-prefix` | `LINODE_UA_PREFIX` | None | Prefix the User-Agent in Linode API calls with some 'product/version' 68 | 69 | ## Notes 70 | 71 | * When using the `linode/containerlinux` `linode-image`, the `linode-ssh-user` will default to `core` 72 | * A `linode-root-pass` will be generated if not provided. This password will not be shown. Rely on `docker-machine ssh`, `linode-authorized-users`, or [Linode's Rescue features](https://www.linode.com/docs/quick-answers/linode-platform/reset-the-root-password-on-your-linode/) to access the node directly. 73 | 74 | ### Docker Volume Driver 75 | 76 | The [Docker Volume plugin for Linode Block Storage](https://github.com/linode/docker-volume-linode) can be installed while reusing the docker-machine properties: 77 | 78 | ```sh 79 | MACHINE=my-docker-machine 80 | 81 | docker-machine create -d linode $MACHINE 82 | 83 | eval $(docker-machine env $MACHINE) 84 | 85 | # Region and Label are not needed. They would be inferred. Included here for illustration purposes. 86 | docker plugin install --alias linode linode/docker-volume-linode:latest \ 87 | linode-token=$(docker-machine inspect $MACHINE -f "{{ .Driver.APIToken }}") \ 88 | linode-region=$(docker-machine inspect $MACHINE -f "{{ .Driver.Region }}") \ 89 | linode-label=$(docker-machine inspect $MACHINE -f "{{ .Driver.InstanceLabel }}") 90 | 91 | docker run -it --rm --mount volume-driver=linode,source=test-vol,destination=/test,volume-opt=size=25 alpine 92 | 93 | docker volume rm test-vol 94 | ``` 95 | 96 | ## Debugging 97 | 98 | Detailed run output will be emitted when using the LinodeGo `LINODE_DEBUG=1` option along with the `docker-machine` `--debug` option. 99 | 100 | ```bash 101 | LINODE_DEBUG=1 docker-machine --debug create -d linode --linode-token=$LINODE_TOKEN machinename 102 | ``` 103 | 104 | ## Examples 105 | 106 | ### Simple Example 107 | 108 | ```bash 109 | LINODE_TOKEN=e332cf8e1a78427f1368a5a0a67946ad1e7c8e28e332cf8e1a78427f1368a5a0 # Should be 65 lowercase hex chars 110 | 111 | docker-machine create -d linode --linode-token=$LINODE_TOKEN linode 112 | eval $(docker-machine env linode) 113 | docker run --rm -it debian bash 114 | ``` 115 | 116 | ```bash 117 | $ docker-machine ls 118 | NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS 119 | linode * linode Running tcp://45.79.139.196:2376 v18.05.0-ce 120 | 121 | $ docker-machine rm linode 122 | About to remove linode 123 | WARNING: This action will delete both local reference and remote instance. 124 | Are you sure? (y/n): y 125 | (default) Removing linode: 8753395 126 | Successfully removed linode 127 | ``` 128 | 129 | ### Provisioning Docker Swarm 130 | 131 | The following script serves as an example for creating a [Docker Swarm](https://docs.docker.com/engine/swarm/) with master and worker nodes using the Linode Docker machine driver and private networking. 132 | 133 | This script is provided for demonstrative use. A production swarm environment would require hardening. 134 | 135 | 1. Create an `install.sh` bash script using the source below. Run `bash install.sh` and provide a Linode APIv4 Token when prompted. 136 | 137 | ```sh 138 | #!/bin/bash 139 | set -e 140 | 141 | read -p "Linode Token: " LINODE_TOKEN 142 | # LINODE_TOKEN=... 143 | LINODE_ROOT_PASSWORD=$(openssl rand -base64 32); echo Password for root: $LINODE_ROOT_PASSWORD 144 | LINODE_REGION=eu-central 145 | 146 | create_node() { 147 | local name=$1 148 | docker-machine create \ 149 | -d linode \ 150 | --linode-label=$name \ 151 | --linode-instance-type=g6-nanode-1 \ 152 | --linode-image=linode/ubuntu18.04 \ 153 | --linode-region=$LINODE_REGION \ 154 | --linode-token=$LINODE_TOKEN \ 155 | --linode-root-pass=$LINODE_ROOT_PASSWORD \ 156 | --linode-create-private-ip \ 157 | $name 158 | } 159 | 160 | get_private_ip() { 161 | local name=$1 162 | docker-machine inspect -f '{{.Driver.PrivateIPAddress}}' $name 163 | } 164 | 165 | init_swarm_master() { 166 | local name=$1 167 | local ip=$(get_private_ip $name) 168 | docker-machine ssh $name "docker swarm init --advertise-addr ${ip}" 169 | } 170 | 171 | init_swarm_worker() { 172 | local master_name=$1 173 | local worker_name=$2 174 | local master_addr=$(get_private_ip $master_name):2377 175 | local join_token=$(docker-machine ssh $master_name "docker swarm join-token worker -q") 176 | docker-machine ssh $worker_name "docker swarm join --token=${join_token} ${master_addr}" 177 | } 178 | 179 | # create master and worker node 180 | create_node swarm-master-01 & create_node swarm-worker-01 181 | 182 | # init swarm master 183 | init_swarm_master swarm-master-01 184 | 185 | # init swarm worker 186 | init_swarm_worker swarm-master-01 swarm-worker-01 187 | 188 | # install the docker-volume-linode plugin on each node 189 | for NODE in swarm-master-01 swarm-worker-01; do 190 | eval $(docker-machine env $NODE) 191 | docker plugin install --alias linode linode/docker-volume-linode:latest linode-token=$LINODE_TOKEN 192 | done 193 | ``` 194 | 195 | 1. After provisioning succeeds, check the Docker Swarm status. The output should show active an swarm leader and worker. 196 | 197 | ```sh 198 | $ eval $(docker-machine env master01) 199 | $ docker node ls 200 | 201 | ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION 202 | f8x7zutegt2dn1imeiw56v9hc * master01 Ready Active Leader 18.09.0 203 | ja8b3ut6uaivz5hf98gah469y worker01 Ready Active 18.09.0 204 | ``` 205 | 206 | 1. [Create and scale Docker services](https://docs.docker.com/engine/reference/commandline/service_create/) (left as an excercise for the reader). 207 | 208 | ```bash 209 | $ docker service create --name my-service --replicas 3 nginx:alpine 210 | $ docker node ps master01 worker01 211 | ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS 212 | 7cggbrqfqopn \_ my-service.1 nginx:alpine master01 Running Running 4 minutes ago 213 | 7cggbrqfqopn \_ my-service.1 nginx:alpine master01 Running Running 4 minutes ago 214 | v7c1ni5q43uu my-service.2 nginx:alpine worker01 Running Running 4 minutes ago 215 | 2w6d8o3hdyh4 my-service.3 nginx:alpine worker01 Running Running 4 minutes ago 216 | ``` 217 | 218 | 1. Cleanup the resources 219 | 220 | ```sh 221 | docker-machine rm worker01 -y 222 | docker-machine rm master01 -y 223 | ``` 224 | 225 | ## Discussion / Help 226 | 227 | Join us at [#linodego](https://gophers.slack.com/messages/CAG93EB2S) on the [gophers slack](https://gophers.slack.com) 228 | 229 | ## License 230 | 231 | [MIT License](LICENSE) 232 | 233 | ## Contribution Guidelines 234 | 235 | Want to improve docker-machine-driver-linode? Please start [here](CONTRIBUTING.md). 236 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/linode/docker-machine-driver-linode 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | // This replacement is necessary to support Docker versions > v20.x.x 8 | // which provide critical security fixes. 9 | replace github.com/docker/machine => gitlab.com/gitlab-org/ci-cd/docker-machine v0.16.2-gitlab.27 10 | 11 | require ( 12 | github.com/docker/machine v0.16.2 13 | github.com/google/go-cmp v0.7.0 14 | github.com/linode/linodego v1.52.1 15 | github.com/stretchr/testify v1.10.0 16 | golang.org/x/oauth2 v0.30.0 17 | ) 18 | 19 | require ( 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/go-resty/resty/v2 v2.16.5 // indirect 22 | github.com/google/go-querystring v1.1.0 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | golang.org/x/crypto v0.38.0 // indirect 25 | golang.org/x/net v0.40.0 // indirect 26 | golang.org/x/sys v0.33.0 // indirect 27 | golang.org/x/term v0.32.0 // indirect 28 | golang.org/x/text v0.25.0 // indirect 29 | gopkg.in/ini.v1 v1.66.6 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= 4 | github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= 5 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 7 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 8 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 9 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 10 | github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= 11 | github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= 12 | github.com/linode/linodego v1.52.1 h1:HJ1cz1n9n3chRP9UrtqmP91+xTi0Q5l+H/4z4tpkwgQ= 13 | github.com/linode/linodego v1.52.1/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 | gitlab.com/gitlab-org/ci-cd/docker-machine v0.16.2-gitlab.27 h1:6XE5SIyDteS5BFR3EhlEt7UUBhkcf77loHYWaVS4BHM= 19 | gitlab.com/gitlab-org/ci-cd/docker-machine v0.16.2-gitlab.27/go.mod h1:WX9wJGY7+MC7527nUL2hvFOLNlowPeNjeiLtX5B6MnQ= 20 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 21 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 22 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 23 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 24 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 25 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 26 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 27 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 28 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 29 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 30 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 31 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 32 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 33 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 34 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 h1:+j1SppRob9bAgoYmsdW9NNBdKZfgYuWpqnYHv78Qt8w= 37 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= 39 | gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/docker/machine/libmachine/drivers/plugin" 5 | "github.com/linode/docker-machine-driver-linode/pkg/drivers/linode" 6 | ) 7 | 8 | func main() { 9 | plugin.RegisterDriver(linode.NewDriver("", "")) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/drivers/linode/linode.go: -------------------------------------------------------------------------------- 1 | package linode 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "os" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | 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/linode/linodego" 23 | "golang.org/x/oauth2" 24 | ) 25 | 26 | // Driver is the implementation of BaseDriver interface 27 | type Driver struct { 28 | *drivers.BaseDriver 29 | client *linodego.Client 30 | 31 | APIToken string 32 | UserAgentPrefix string 33 | IPAddress string 34 | PrivateIPAddress string 35 | CreatePrivateIP bool 36 | DockerPort int 37 | 38 | InstanceID int 39 | InstanceLabel string 40 | 41 | Region string 42 | InstanceType string 43 | RootPassword string 44 | AuthorizedUsers string 45 | SSHPort int 46 | InstanceImage string 47 | SwapSize int 48 | 49 | StackScriptID int 50 | StackScriptUser string 51 | StackScriptLabel string 52 | StackScriptData map[string]string 53 | 54 | Tags string 55 | } 56 | 57 | // VERSION represents the semver version of the package 58 | var VERSION = "devel" 59 | 60 | const ( 61 | defaultSSHPort = 22 62 | defaultSSHUser = "root" 63 | defaultInstanceImage = "linode/ubuntu18.04" 64 | defaultRegion = "us-east" 65 | defaultInstanceType = "g6-standard-4" 66 | defaultSwapSize = 512 67 | defaultDockerPort = 2376 68 | 69 | defaultContainerLinuxSSHUser = "core" 70 | ) 71 | 72 | // NewDriver creates and returns a new instance of the Linode driver 73 | func NewDriver(hostName, storePath string) *Driver { 74 | return &Driver{ 75 | InstanceImage: defaultInstanceImage, 76 | InstanceType: defaultInstanceType, 77 | Region: defaultRegion, 78 | SwapSize: defaultSwapSize, 79 | BaseDriver: &drivers.BaseDriver{ 80 | MachineName: hostName, 81 | StorePath: storePath, 82 | }, 83 | } 84 | } 85 | 86 | // getClient prepares the Linode APIv4 Client 87 | func (d *Driver) getClient() *linodego.Client { 88 | if d.client == nil { 89 | tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: d.APIToken}) 90 | 91 | oauth2Client := &http.Client{ 92 | Transport: &oauth2.Transport{ 93 | Source: tokenSource, 94 | }, 95 | } 96 | 97 | ua := fmt.Sprintf("docker-machine-driver-%s/%s", d.DriverName(), VERSION) 98 | 99 | client := linodego.NewClient(oauth2Client) 100 | if len(d.UserAgentPrefix) > 0 { 101 | ua = fmt.Sprintf("%s %s", d.UserAgentPrefix, ua) 102 | } 103 | 104 | client.SetUserAgent(ua) 105 | client.SetDebug(true) 106 | d.client = &client 107 | } 108 | return d.client 109 | } 110 | 111 | func createRandomRootPassword() (string, error) { 112 | rawRootPass := make([]byte, 50) 113 | _, err := rand.Read(rawRootPass) 114 | if err != nil { 115 | return "", fmt.Errorf("Failed to generate random password") 116 | } 117 | rootPass := base64.StdEncoding.EncodeToString(rawRootPass) 118 | return rootPass, nil 119 | } 120 | 121 | // DriverName returns the name of the driver 122 | func (d *Driver) DriverName() string { 123 | return "linode" 124 | } 125 | 126 | // GetSSHHostname returns hostname for use with ssh 127 | func (d *Driver) GetSSHHostname() (string, error) { 128 | return d.GetIP() 129 | } 130 | 131 | // GetIP returns an IP or hostname that this host is available at 132 | // e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net 133 | // Note that currently the IP Address is cached 134 | func (d *Driver) GetIP() (string, error) { 135 | if d.IPAddress == "" { 136 | return "", fmt.Errorf("IP address is not set") 137 | } 138 | return d.IPAddress, nil 139 | } 140 | 141 | // GetCreateFlags returns the mcnflag.Flag slice representing the flags 142 | // that can be set, their descriptions and defaults. 143 | func (d *Driver) GetCreateFlags() []mcnflag.Flag { 144 | return []mcnflag.Flag{ 145 | mcnflag.StringFlag{ 146 | EnvVar: "LINODE_TOKEN", 147 | Name: "linode-token", 148 | Usage: "Linode API Token", 149 | Value: "", 150 | }, 151 | mcnflag.StringFlag{ 152 | EnvVar: "LINODE_ROOT_PASSWORD", 153 | Name: "linode-root-pass", 154 | Usage: "Root Password", 155 | }, 156 | mcnflag.StringFlag{ 157 | EnvVar: "LINODE_AUTHORIZED_USERS", 158 | Name: "linode-authorized-users", 159 | Usage: "Linode user accounts (separated by commas) whose Linode SSH keys will be permitted root access to the created node", 160 | }, 161 | mcnflag.StringFlag{ 162 | EnvVar: "LINODE_LABEL", 163 | Name: "linode-label", 164 | Usage: "Linode Instance Label", 165 | }, 166 | mcnflag.StringFlag{ 167 | EnvVar: "LINODE_REGION", 168 | Name: "linode-region", 169 | Usage: "Specifies the region (location) of the Linode instance", 170 | Value: defaultRegion, // "us-central", "ap-south", "eu-central", ... 171 | }, 172 | mcnflag.StringFlag{ 173 | EnvVar: "LINODE_INSTANCE_TYPE", 174 | Name: "linode-instance-type", 175 | Usage: "Specifies the Linode Instance type which determines CPU, memory, disk size, etc.", 176 | Value: defaultInstanceType, // "g6-nanode-1", g6-highmem-2, ... 177 | }, 178 | mcnflag.IntFlag{ 179 | EnvVar: "LINODE_SSH_PORT", 180 | Name: "linode-ssh-port", 181 | Usage: "Linode Instance SSH Port", 182 | Value: defaultSSHPort, 183 | }, 184 | mcnflag.StringFlag{ 185 | EnvVar: "LINODE_SSH_USER", 186 | Name: "linode-ssh-user", 187 | Usage: "Specifies the user as which docker-machine should log in to the Linode instance to install Docker.", 188 | Value: "", 189 | }, 190 | mcnflag.StringFlag{ 191 | EnvVar: "LINODE_IMAGE", 192 | Name: "linode-image", 193 | Usage: "Specifies the Linode Instance image which determines the OS distribution and base files", 194 | Value: defaultInstanceImage, // "linode/ubuntu18.04", "linode/arch", ... 195 | }, 196 | mcnflag.IntFlag{ 197 | EnvVar: "LINODE_DOCKER_PORT", 198 | Name: "linode-docker-port", 199 | Usage: "Docker Port", 200 | Value: defaultDockerPort, 201 | }, 202 | mcnflag.IntFlag{ 203 | EnvVar: "LINODE_SWAP_SIZE", 204 | Name: "linode-swap-size", 205 | Usage: "Linode Instance Swap Size (MB)", 206 | Value: defaultSwapSize, 207 | }, 208 | mcnflag.StringFlag{ 209 | EnvVar: "LINODE_STACKSCRIPT", 210 | Name: "linode-stackscript", 211 | Usage: "Specifies the Linode StackScript to use to create the instance", 212 | Value: "", 213 | }, 214 | mcnflag.StringFlag{ 215 | EnvVar: "LINODE_STACKSCRIPT_DATA", 216 | Name: "linode-stackscript-data", 217 | Usage: "A JSON string specifying data for the selected StackScript", 218 | Value: "", 219 | }, 220 | mcnflag.BoolFlag{ 221 | EnvVar: "LINODE_CREATE_PRIVATE_IP", 222 | Name: "linode-create-private-ip", 223 | Usage: "Create private IP for the instance", 224 | }, 225 | mcnflag.StringFlag{ 226 | EnvVar: "LINODE_UA_PREFIX", 227 | Name: "linode-ua-prefix", 228 | Usage: fmt.Sprintf("Prefix the User-Agent in Linode API calls with some 'product/version'"), 229 | }, 230 | mcnflag.StringFlag{ 231 | EnvVar: "LINODE_TAGS", 232 | Name: "linode-tags", 233 | Usage: fmt.Sprintf("A comma separated list of tags to apply to the Linode resource"), 234 | }, 235 | } 236 | } 237 | 238 | // GetSSHPort returns port for use with ssh 239 | func (d *Driver) GetSSHPort() (int, error) { 240 | if d.SSHPort == 0 { 241 | d.SSHPort = defaultSSHPort 242 | } 243 | 244 | return d.SSHPort, nil 245 | } 246 | 247 | // GetSSHUsername returns username for use with ssh 248 | func (d *Driver) GetSSHUsername() string { 249 | if d.SSHUser == "" { 250 | if strings.Contains(d.InstanceImage, "linode/containerlinux") { 251 | d.SSHUser = defaultContainerLinuxSSHUser 252 | } else { 253 | d.SSHUser = defaultSSHUser 254 | } 255 | } 256 | 257 | return d.SSHUser 258 | } 259 | 260 | // SetConfigFromFlags configures the driver with the object that was returned 261 | // by RegisterCreateFlags 262 | func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { 263 | d.APIToken = flags.String("linode-token") 264 | d.Region = flags.String("linode-region") 265 | d.InstanceType = flags.String("linode-instance-type") 266 | d.AuthorizedUsers = flags.String("linode-authorized-users") 267 | d.RootPassword = flags.String("linode-root-pass") 268 | d.SSHPort = flags.Int("linode-ssh-port") 269 | d.SSHUser = flags.String("linode-ssh-user") 270 | d.InstanceImage = flags.String("linode-image") 271 | d.InstanceLabel = flags.String("linode-label") 272 | d.SwapSize = flags.Int("linode-swap-size") 273 | d.DockerPort = flags.Int("linode-docker-port") 274 | d.CreatePrivateIP = flags.Bool("linode-create-private-ip") 275 | d.UserAgentPrefix = flags.String("linode-ua-prefix") 276 | d.Tags = flags.String("linode-tags") 277 | 278 | d.SetSwarmConfigFromFlags(flags) 279 | 280 | if d.APIToken == "" { 281 | return fmt.Errorf("linode driver requires the --linode-token option") 282 | } 283 | 284 | stackScript := flags.String("linode-stackscript") 285 | if stackScript != "" { 286 | sid, err := strconv.Atoi(stackScript) 287 | if err == nil { 288 | d.StackScriptID = sid 289 | } else { 290 | ss := strings.SplitN(stackScript, "/", 2) 291 | if len(ss) != 2 { 292 | return fmt.Errorf("linode StackScripts must be specified using username/label syntax, or using their identifier") 293 | } 294 | 295 | d.StackScriptUser = ss[0] 296 | d.StackScriptLabel = ss[1] 297 | } 298 | 299 | stackScriptDataStr := flags.String("linode-stackscript-data") 300 | if stackScriptDataStr != "" { 301 | err := json.Unmarshal([]byte(stackScriptDataStr), &d.StackScriptData) 302 | if err != nil { 303 | return fmt.Errorf("linode StackScript data must be valid JSON: %s", err) 304 | } 305 | } 306 | } 307 | 308 | if len(d.InstanceLabel) == 0 { 309 | d.InstanceLabel = d.GetMachineName() 310 | } 311 | 312 | newLabel, err := normalizeInstanceLabel(d.InstanceLabel) 313 | if err != nil { 314 | return fmt.Errorf("failed to normalize instance label: %s", err) 315 | } 316 | 317 | d.InstanceLabel = newLabel 318 | 319 | return nil 320 | } 321 | 322 | // PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation 323 | func (d *Driver) PreCreateCheck() error { 324 | // TODO(displague) linode-stackscript-file should be read and uploaded (private), then used for boot. 325 | // RevNote could be sha256 of file so the file can be referenced instead of reuploaded. 326 | 327 | client := d.getClient() 328 | 329 | if d.RootPassword == "" { 330 | log.Info("Generating a secure disposable linode-root-pass...") 331 | var err error 332 | d.RootPassword, err = createRandomRootPassword() 333 | if err != nil { 334 | return err 335 | } 336 | } 337 | 338 | if d.StackScriptUser != "" { 339 | /* N.B. username isn't on the list of filterable fields, however 340 | adding it doesn't make anything fail, so if it becomes 341 | filterable in future this will become more efficient */ 342 | options := map[string]string{ 343 | "username": d.StackScriptUser, 344 | "label": d.StackScriptLabel, 345 | } 346 | b, err := json.Marshal(options) 347 | if err != nil { 348 | return err 349 | } 350 | opts := linodego.NewListOptions(0, string(b)) 351 | stackscripts, err := client.ListStackscripts(context.TODO(), opts) 352 | if err != nil { 353 | return err 354 | } 355 | var script *linodego.Stackscript 356 | for _, s := range stackscripts { 357 | if s.Username == d.StackScriptUser { 358 | script = &s 359 | break 360 | } 361 | } 362 | if script == nil { 363 | return fmt.Errorf("StackScript not found: %s/%s", d.StackScriptUser, d.StackScriptLabel) 364 | } 365 | 366 | d.StackScriptUser = script.Username 367 | d.StackScriptLabel = script.Label 368 | d.StackScriptID = script.ID 369 | } else if d.StackScriptID != 0 { 370 | script, err := client.GetStackscript(context.TODO(), d.StackScriptID) 371 | if err != nil { 372 | return fmt.Errorf("StackScript %d could not be used: %s", d.StackScriptID, err) 373 | } 374 | 375 | d.StackScriptUser = script.Username 376 | d.StackScriptLabel = script.Label 377 | } 378 | 379 | return nil 380 | } 381 | 382 | // Create a host using the driver's config 383 | func (d *Driver) Create() error { 384 | log.Info("Creating Linode machine instance...") 385 | 386 | if d.SSHPort != defaultSSHPort { 387 | log.Infof("Using SSH port %d", d.SSHPort) 388 | } 389 | 390 | publicKey, err := d.createSSHKey() 391 | if err != nil { 392 | return err 393 | } 394 | 395 | client := d.getClient() 396 | boolBooted := !d.CreatePrivateIP 397 | 398 | // Create a linode 399 | createOpts := linodego.InstanceCreateOptions{ 400 | Region: d.Region, 401 | Type: d.InstanceType, 402 | Label: d.InstanceLabel, 403 | RootPass: d.RootPassword, 404 | AuthorizedKeys: []string{strings.TrimSpace(publicKey)}, 405 | Image: d.InstanceImage, 406 | SwapSize: &d.SwapSize, 407 | PrivateIP: d.CreatePrivateIP, 408 | Booted: &boolBooted, 409 | } 410 | 411 | if len(d.AuthorizedUsers) > 0 { 412 | createOpts.AuthorizedUsers = strings.Split(d.AuthorizedUsers, ",") 413 | } 414 | 415 | if d.Tags != "" { 416 | createOpts.Tags = strings.Split(d.Tags, ",") 417 | } 418 | 419 | if d.StackScriptID != 0 { 420 | createOpts.StackScriptID = d.StackScriptID 421 | createOpts.StackScriptData = d.StackScriptData 422 | log.Infof("Using StackScript %d: %s/%s", d.StackScriptID, d.StackScriptUser, d.StackScriptLabel) 423 | } 424 | 425 | linode, err := client.CreateInstance(context.TODO(), createOpts) 426 | if err != nil { 427 | return err 428 | } 429 | 430 | d.InstanceID = linode.ID 431 | d.InstanceLabel = linode.Label 432 | 433 | // Don't persist alias region names 434 | d.Region = linode.Region 435 | 436 | for _, address := range linode.IPv4 { 437 | if private := privateIP(*address); !private { 438 | d.IPAddress = address.String() 439 | } else if d.CreatePrivateIP { 440 | d.PrivateIPAddress = address.String() 441 | } 442 | } 443 | 444 | if d.IPAddress == "" { 445 | return errors.New("Linode IP Address is not found") 446 | } 447 | 448 | if d.CreatePrivateIP && d.PrivateIPAddress == "" { 449 | return errors.New("Linode Private IP Address is not found") 450 | } 451 | 452 | log.Debugf("Created Linode Instance %s (%d), IP address %q, Private IP address %q", 453 | d.InstanceLabel, 454 | d.InstanceID, 455 | d.IPAddress, 456 | d.PrivateIPAddress, 457 | ) 458 | 459 | if err != nil { 460 | return err 461 | } 462 | 463 | if d.CreatePrivateIP { 464 | log.Debugf("Enabling Network Helper for Private IP configuration...") 465 | 466 | configs, err := client.ListInstanceConfigs(context.TODO(), linode.ID, nil) 467 | if err != nil { 468 | return err 469 | } 470 | if len(configs) == 0 { 471 | return fmt.Errorf("Linode Config was not found for Linode %d", linode.ID) 472 | } 473 | updateOpts := configs[0].GetUpdateOptions() 474 | updateOpts.Helpers.Network = true 475 | if _, err := client.UpdateInstanceConfig(context.TODO(), linode.ID, configs[0].ID, updateOpts); err != nil { 476 | return err 477 | } 478 | 479 | if err := client.BootInstance(context.TODO(), linode.ID, configs[0].ID); err != nil { 480 | return err 481 | } 482 | } 483 | 484 | log.Info("Waiting for Machine Running...") 485 | if _, err := client.WaitForInstanceStatus(context.TODO(), d.InstanceID, linodego.InstanceRunning, 180); err != nil { 486 | return fmt.Errorf("wait for machine running failed: %s", err) 487 | } 488 | 489 | return nil 490 | } 491 | 492 | // GetURL returns a Docker compatible host URL for connecting to this host 493 | // e.g. tcp://1.2.3.4:2376 494 | func (d *Driver) GetURL() (string, error) { 495 | ip, err := d.GetIP() 496 | if err != nil { 497 | return "", err 498 | } 499 | if ip == "" { 500 | return "", nil 501 | } 502 | 503 | return fmt.Sprintf("tcp://%s:%d", ip, d.DockerPort), nil 504 | } 505 | 506 | // GetState returns the state that the host is in (running, stopped, etc) 507 | func (d *Driver) GetState() (state.State, error) { 508 | linode, err := d.getClient().GetInstance(context.TODO(), d.InstanceID) 509 | if err != nil { 510 | return state.Error, err 511 | } 512 | 513 | switch linode.Status { 514 | case linodego.InstanceRunning: 515 | return state.Running, nil 516 | case linodego.InstanceOffline, 517 | linodego.InstanceRebuilding, 518 | linodego.InstanceMigrating: 519 | return state.Stopped, nil 520 | case linodego.InstanceShuttingDown, linodego.InstanceDeleting: 521 | return state.Stopping, nil 522 | case linodego.InstanceProvisioning, 523 | linodego.InstanceRebooting, 524 | linodego.InstanceBooting, 525 | linodego.InstanceCloning, 526 | linodego.InstanceRestoring: 527 | return state.Starting, nil 528 | 529 | } 530 | 531 | // deleting, migrating, rebuilding, cloning, restoring ... 532 | return state.None, nil 533 | } 534 | 535 | // Start a host 536 | func (d *Driver) Start() error { 537 | log.Debug("Start...") 538 | err := d.getClient().BootInstance(context.TODO(), d.InstanceID, 0) 539 | return err 540 | } 541 | 542 | // Stop a host gracefully 543 | func (d *Driver) Stop() error { 544 | log.Debug("Stop...") 545 | err := d.getClient().ShutdownInstance(context.TODO(), d.InstanceID) 546 | return err 547 | } 548 | 549 | // Remove a host 550 | func (d *Driver) Remove() error { 551 | client := d.getClient() 552 | log.Infof("Removing linode: %d", d.InstanceID) 553 | if err := client.DeleteInstance(context.TODO(), d.InstanceID); err != nil { 554 | if apiErr, ok := err.(*linodego.Error); ok && apiErr.Code == 404 { 555 | log.Debug("Linode was already removed") 556 | return nil 557 | } 558 | 559 | return err 560 | } 561 | return nil 562 | } 563 | 564 | // Restart a host. This may just call Stop(); Start() if the provider does not 565 | // have any special restart behaviour. 566 | func (d *Driver) Restart() error { 567 | log.Debug("Restarting...") 568 | err := d.getClient().RebootInstance(context.TODO(), d.InstanceID, 0) 569 | return err 570 | } 571 | 572 | // Kill stops a host forcefully 573 | func (d *Driver) Kill() error { 574 | log.Debug("Killing...") 575 | err := d.getClient().ShutdownInstance(context.TODO(), d.InstanceID) 576 | return err 577 | } 578 | 579 | func (d *Driver) createSSHKey() (string, error) { 580 | if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { 581 | return "", err 582 | } 583 | 584 | publicKey, err := os.ReadFile(d.publicSSHKeyPath()) 585 | if err != nil { 586 | return "", err 587 | } 588 | 589 | return string(publicKey), nil 590 | } 591 | 592 | // publicSSHKeyPath is always SSH Key Path appended with ".pub" 593 | func (d *Driver) publicSSHKeyPath() string { 594 | return d.GetSSHKeyPath() + ".pub" 595 | } 596 | 597 | // privateIP determines if an IP is for private use (RFC1918) 598 | // https://stackoverflow.com/a/41273687 599 | func privateIP(ip net.IP) bool { 600 | return ipInCIDR(ip, "10.0.0.0/8") || ipInCIDR(ip, "172.16.0.0/12") || ipInCIDR(ip, "192.168.0.0/16") 601 | } 602 | 603 | func ipInCIDR(ip net.IP, CIDR string) bool { 604 | _, ipNet, err := net.ParseCIDR(CIDR) 605 | if err != nil { 606 | log.Errorf("Error parsing CIDR %s: %s", CIDR, err) 607 | 608 | return false 609 | } 610 | return ipNet.Contains(ip) 611 | } 612 | 613 | const noLabelDuplicates = "._-" 614 | 615 | func normalizeInstanceLabel(label string) (string, error) { 616 | noLabelDuplicatesMap := make(map[rune]bool) 617 | for _, c := range noLabelDuplicates { 618 | noLabelDuplicatesMap[c] = true 619 | } 620 | 621 | result := label 622 | 623 | // Replace invalid characters 624 | r, err := regexp.Compile("[^a-zA-Z\\d_.-]") 625 | if err != nil { 626 | return "", fmt.Errorf("failed to compile label normalization regex: %s", err) 627 | } 628 | 629 | replaced := r.ReplaceAllString(result, "") 630 | if replaced != result { 631 | log.Warnf("The name for this machine contains invalid characters. Normalizing to \"%s\"", replaced) 632 | } 633 | 634 | result = replaced 635 | 636 | // Remove duplicates (no backrefs in regexp :( ) 637 | var lastChar rune 638 | var resultBuilder strings.Builder 639 | resultCharCount := len(result) 640 | 641 | for i, c := range result { 642 | currentLastChar := lastChar 643 | lastChar = c 644 | 645 | // If the rune is not a special char, keep it 646 | if _, ok := noLabelDuplicatesMap[c]; !ok { 647 | resultBuilder.WriteRune(c) 648 | continue 649 | } 650 | 651 | // If the rune is a special character and is the first or last rune, skip it 652 | if i == 0 || i == resultCharCount-1 { 653 | continue 654 | } 655 | 656 | // If the char is not a repeat (e.g. --, __, ..), keep it 657 | if currentLastChar != c { 658 | resultBuilder.WriteRune(c) 659 | } 660 | } 661 | 662 | newResult := resultBuilder.String() 663 | if newResult != result { 664 | log.Warnf("Removed duplicate special characters from Linode label: \"%s\" -> \"%s\"", result, newResult) 665 | } 666 | 667 | result = newResult 668 | 669 | // Truncate length 670 | if len(result) > 64 { 671 | result = result[:64] 672 | log.Warnf("The name for this machine exceeds the 64 character Linode label limit. Truncating to \"%s\"", result) 673 | } 674 | 675 | return result, nil 676 | } 677 | -------------------------------------------------------------------------------- /pkg/drivers/linode/linode_test.go: -------------------------------------------------------------------------------- 1 | package linode 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/docker/machine/libmachine/drivers" 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSetConfigFromFlags(t *testing.T) { 14 | driver := NewDriver("", "") 15 | 16 | checkFlags := &drivers.CheckDriverOptions{ 17 | FlagsValues: map[string]interface{}{ 18 | "linode-token": "PROJECT", 19 | "linode-root-pass": "ROOTPASS", 20 | }, 21 | CreateFlags: driver.GetCreateFlags(), 22 | } 23 | 24 | err := driver.SetConfigFromFlags(checkFlags) 25 | 26 | assert.NoError(t, err) 27 | assert.Empty(t, checkFlags.InvalidFlags) 28 | } 29 | 30 | func TestPrivateIP(t *testing.T) { 31 | ip := net.IP{} 32 | for _, addr := range [][]byte{ 33 | []byte("172.16.0.1"), 34 | []byte("192.168.0.1"), 35 | []byte("10.0.0.1"), 36 | } { 37 | if err := ip.UnmarshalText(addr); err != nil { 38 | t.Error(err) 39 | } 40 | assert.True(t, privateIP(ip)) 41 | } 42 | 43 | if err := ip.UnmarshalText([]byte("1.1.1.1")); err != nil { 44 | t.Error(err) 45 | } 46 | assert.False(t, privateIP(ip)) 47 | } 48 | 49 | func TestIPInCIDR(t *testing.T) { 50 | tenOne := net.IP{} 51 | 52 | if err := tenOne.UnmarshalText([]byte("10.0.0.1")); err != nil { 53 | t.Error(err) 54 | } 55 | assert.True(t, ipInCIDR(tenOne, "10.0.0.0/8"), "10.0.0.1 is in 10.0.0.0/8") 56 | assert.False(t, ipInCIDR(tenOne, "254.0.0.0/8"), "10.0.0.1 is not in 254.0.0.0/8") 57 | } 58 | 59 | func TestNormalizeInstanceLabel(t *testing.T) { 60 | inputLabel := "_mycoollabel25';./__----=][[this,label,is,really[good]and]long[wow+that'scrazy[]what[a\\good!labelname." 61 | expectedResult := "mycoollabel25._-thislabelisreallygoodandlongwowthatscrazywhatago" 62 | 63 | result, err := normalizeInstanceLabel(inputLabel) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | if !reflect.DeepEqual(result, expectedResult) { 69 | t.Fatal(cmp.Diff(result, expectedResult)) 70 | } 71 | } 72 | --------------------------------------------------------------------------------