├── .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 | [](https://godoc.org/github.com/linode/docker-machine-driver-linode)
4 | [](https://goreportcard.com/report/github.com/linode/docker-machine-driver-linode)
5 | [](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 |
--------------------------------------------------------------------------------