├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── config.go
├── config_create.go
├── copy.go
├── create.go
├── defaults.go
├── delete.go
├── root.go
├── show.go
├── ssh.go
├── start.go
├── stop.go
└── version.go
├── demo
├── README.md
├── demo.cast
├── demo.sh
├── docker-in-vind.yaml
├── k8s-in-vind
│ ├── Dockerfile
│ ├── entrypoint
│ └── k8s-in-vind.yaml
├── ubuntu-1.yaml
└── ubuntu-2.yaml
├── go.mod
├── go.sum
├── images
├── README.md
├── amazonlinux
│ └── Dockerfile.2
├── build.sh
├── centos
│ ├── Dockerfile.7
│ └── Dockerfile.8
├── debian
│ ├── Dockerfile.bookworm
│ ├── Dockerfile.bullseye
│ └── Dockerfile.buster
├── fedora
│ ├── Dockerfile.40
│ ├── Dockerfile.41
│ └── Dockerfile.42
└── ubuntu
│ ├── Dockerfile.18.04.non-root
│ ├── Dockerfile.18.04.root
│ ├── Dockerfile.20.04.non-root
│ ├── Dockerfile.20.04.root
│ ├── Dockerfile.22.04.non-root
│ ├── Dockerfile.22.04.root
│ ├── Dockerfile.24.04.non-root
│ ├── Dockerfile.24.04.root
│ ├── Dockerfile.24.10.non-root
│ ├── Dockerfile.24.10.root
│ ├── Dockerfile.25.04.non-root
│ └── Dockerfile.25.04.root
├── main.go
└── pkg
├── cluster
├── cluster.go
├── cluster_test.go
├── key_store.go
├── machine.go
├── run.go
├── runtime_network.go
├── runtime_network_test.go
└── status.go
├── config
├── cluster.go
├── get.go
├── get_test.go
├── key.go
└── machine.go
├── docker
├── cp.go
├── create.go
├── doc.go
├── exec.go
├── inspect.go
├── kill.go
├── network_connect.go
├── pull.go
├── run.go
├── save.go
├── start.go
├── stop.go
└── userns_remap.go
├── exec
├── exec.go
└── local.go
├── utils
└── logging.go
└── version
└── release.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Workflow
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*' # Run only when explicitly tagging with v*, i.e. v0.1.0, v1.5.0
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | - name: Run GoReleaser
22 | uses: goreleaser/goreleaser-action@v6
23 | with:
24 | distribution: goreleaser
25 | version: '~> v2'
26 | args: -f .goreleaser.yaml release --clean
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | bin
3 | key
4 | vendor
5 | key.pub
6 | cluster-key
7 | cluster-key.pub
8 | vind
9 | vind.yaml
10 | .vscode
11 | **/.DS_Store
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | builds:
2 | - #
3 | # ID of the build.
4 | #
5 | # Default: Project directory name.
6 | id: "goreleaser-build"
7 |
8 | # Binary name.
9 | # Can be a path (e.g. `bin/app`) to wrap the binary in a directory.
10 | #
11 | # Default: Project directory name.
12 | binary: vind
13 |
14 | # Custom environment variables to be set during the builds.
15 | # Invalid environment variables will be ignored.
16 | # For more info refer to: https://pkg.go.dev/cmd/go#hdr-Environment_variables
17 | #
18 | # Default: os.Environ() ++ env config section.
19 | # Templates: allowed.
20 | env:
21 | - CGO_ENABLED=0
22 |
23 | # Custom ldflags.
24 | # For more info refer to: https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies
25 | # and https://pkg.go.dev/cmd/link
26 | #
27 | # Default: '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser'.
28 | # Templates: allowed.
29 | ldflags:
30 | - -s -w
31 | - -X github.com/brightzheng100/vind/cmd.version=v{{.Version}}
32 | - -X github.com/brightzheng100/vind/cmd.commit={{.Commit}}
33 | - -X github.com/brightzheng100/vind/cmd.date={{.Date}}
34 |
35 | # GOOS list to build for.
36 | # For more info refer to: https://pkg.go.dev/cmd/go#hdr-Environment_variables
37 | #
38 | # Default: [ 'darwin', 'linux', 'windows' ].
39 | goos:
40 | - darwin
41 | - linux
42 | - windows
43 |
44 | # GOARCH to build for.
45 | # For more info refer to: https://pkg.go.dev/cmd/go#hdr-Environment_variables
46 | #
47 | # Default: [ '386', 'amd64', 'arm64' ].
48 | goarch:
49 | - amd64
50 | - arm64
51 |
52 | # List of combinations of GOOS + GOARCH + GOARM to ignore.
53 | ignore:
54 | - goos: darwin
55 | goarch: 386
56 | - goos: linux
57 | goarch: arm
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | # Variables
3 | BINARY_PATH := ./bin
4 | BINARY_NAME := vind
5 | VERSION_TAG := $(shell git describe --abbrev=0 --tags)
6 | VERSION_COMMIT := $(shell git rev-parse --short HEAD)
7 | VERSION_DATE := $(shell date +%Y-%m-%dT%H:%M:%SZ)
8 | GOFLAGS := -ldflags="-X 'github.com/brightzheng100/vind/cmd.version=${VERSION_TAG}' -X 'github.com/brightzheng100/vind/cmd.commit=${VERSION_COMMIT}' -X 'github.com/brightzheng100/vind/cmd.date=${VERSION_DATE}'"
9 |
10 | .PHONY: build
11 | build:
12 | # MacOS
13 | GOARCH=amd64 GOOS=darwin go build ${GOFLAGS} -o ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_darwin_amd64 main.go
14 | GOARCH=arm64 GOOS=darwin go build ${GOFLAGS} -o ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_darwin_arm64 main.go
15 | # Linux
16 | GOARCH=amd64 GOOS=linux go build ${GOFLAGS} -o ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_linux_amd64 main.go
17 | GOARCH=arm64 GOOS=linux go build ${GOFLAGS} -o ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_linux_arm64 main.go
18 | # Windows
19 | #GOARCH=amd64 GOOS=windows go build ${GOFLAGS} -o ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_windows_amd64 main.go
20 |
21 | .PHONY: clean
22 | clean:
23 | go clean
24 | rm ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_darwin_*
25 | rm ${BINARY_PATH}/{BINARY_NAME}_${VERSION_TAG}_linux_*
26 | rm ${BINARY_PATH}/${BINARY_NAME}_${VERSION_TAG}_windows_*
27 |
28 | .PHONY: test
29 | test:
30 | go test ./...
31 |
32 | .PHONY: test_coverage
33 | test_coverage:
34 | go test ./... -coverprofile=coverage.out
35 |
36 | .PHONY: dep
37 | dep:
38 | go mod download
39 |
40 | .PHONY: vet
41 | vet:
42 | go vet
43 |
44 | .PHONY: lint
45 | lint:
46 | # Install it by:
47 | # curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
48 | # sh -s -- -b $(go env GOPATH)/bin v1.61.0
49 | golangci-lint run --enable-all
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Virtual Machines IN Docker (`vind`)
2 |
3 | `vind`, the name comes from **V**M **IN** **D**ocker, is a tool to create and manage a cluster of containers that look and work like virtual machines, on Docker (and Podman).
4 |
5 | The container, or called **Machine** in `vind`, runs `systemd` as PID 1 and a SSH daemon that can be used to log into.
6 | Such VM-like container behaves very much like a "normal" VM, it's even possible to run `dockerd` or `kubernetes` in it.
7 |
8 | [](https://asciinema.org/a/697321)
9 |
10 | > Note: `vind` is a rebuild on top of weaveworks' [footloose](https://github.com/weaveworks/footloose), which was archived in year 2023. Kudos to the original developers!
11 |
12 |
13 | ## Install
14 |
15 | `vind` binaries can be downloaded from this repo's [release page](https://github.com/brightzheng100/vind/releases).
16 |
17 | ### MacOS
18 |
19 | On ARM Mx chip:
20 |
21 | ```sh
22 | LATEST_VERSION=`curl -s "https://api.github.com/repos/brightzheng100/vind/releases/latest" | grep '"tag_name":' | cut -d '"' -f 4 | cut -c 2-`
23 | curl -Lo vind.tar.gz https://github.com/brightzheng100/vind/releases/download/v${LATEST_VERSION}/vind_${LATEST_VERSION}_darwin_arm64.tar.gz
24 | tar -xvf vind.tar.gz && chmod +x vind
25 | sudo mv vind /usr/local/bin/
26 | ```
27 |
28 | On Intel chip:
29 |
30 | ```sh
31 | LATEST_VERSION=`curl -s "https://api.github.com/repos/brightzheng100/vind/releases/latest" | grep '"tag_name":' | cut -d '"' -f 4 | cut -c 2-`
32 | curl -Lo vind.tar.gz https://github.com/brightzheng100/vind/releases/download/v${LATEST_VERSION}/vind_${LATEST_VERSION}_darwin_amd64.tar.gz
33 | tar -xvf vind.tar.gz && chmod +x vind
34 | sudo mv vind /usr/local/bin/
35 | ```
36 |
37 | ### Linux
38 |
39 | On AMD64 / x86_64 CPU:
40 |
41 | ```sh
42 | LATEST_VERSION=`curl -s "https://api.github.com/repos/brightzheng100/vind/releases/latest" | grep '"tag_name":' | cut -d '"' -f 4 | cut -c 2-`
43 | curl -Lo vind.tar.gz https://github.com/brightzheng100/vind/releases/download/v${LATEST_VERSION}/vind_${LATEST_VERSION}_linux_amd64.tar.gz
44 | tar -xvf vind.tar.gz && chmod +x vind
45 | sudo mv vind /usr/local/bin/
46 | ```
47 |
48 | On ARM64 CPU:
49 |
50 | ```sh
51 | LATEST_VERSION=`curl -s "https://api.github.com/repos/brightzheng100/vind/releases/latest" | grep '"tag_name":' | cut -d '"' -f 4 | cut -c 2-`
52 | curl -Lo vind.tar.gz https://github.com/brightzheng100/vind/releases/download/v${LATEST_VERSION}/vind_${LATEST_VERSION}_linux_arm64.tar.gz
53 | tar -xvf vind.tar.gz && chmod +x vind
54 | sudo mv vind /usr/local/bin/
55 | ```
56 |
57 | ### Windows
58 |
59 | It should just work as the binaries are cross compiled.
60 | But I personally haven't tried it yet. So please raise GitHub issues if there is any.
61 |
62 | ## Concepts
63 |
64 | There are some simple concepts in `vind`:
65 |
66 | - **`Machine`**: A Machine is a VM-like container that is created by the configured **`MachineSet`**'s specification.
67 | - **`MachineSet`**: A MachineSet is a set of Machines that share the same configuration specification. One MachineSet can have 1 or more replicas, each of which represents a Machine. Each Machine in the MachineSet has its own index, starting from 0.
68 | - **`Cluster`**: Cluster is the top level of objects in `vind`. A Cluster is a group of MachineSet(s), which has a unique name and authentication SSH key pair for the underlying Machines. The SSH key pair can be generated automatically if not exists, or you can generate it and assign to the cluster through the configuration YAML file.
69 |
70 |
71 | ## Config File & Lookup Strategy
72 |
73 | There is a need to refer to the config file for `vind` actions, which is in YAML format.
74 |
75 | There is a lookup sequence while looking for such a configuration:
76 | 1. Explicitly specified by `--config` or `-c` parameter while running the command.
77 | 2. Explicitly exported system variable namely `VIND_CONFIG`. For example, `export VIND_CONFIG=/path/to/file.yaml` parameter while running the command.
78 | 3. Current folder's `vind.yaml`, if any.
79 |
80 |
81 | ## Usage
82 |
83 | ```sh
84 | $ vind -h
85 | A tool to create containers that look and work like virtual machines, on Docker.
86 |
87 | Usage:
88 | vind [command]
89 |
90 | Available Commands:
91 | completion Generate the autocompletion script for the specified shell
92 | config Manage cluster configuration
93 | cp Copy files or folders between a machine and the host file system
94 | create Create a cluster
95 | delete Delete a cluster
96 | help Help about any command
97 | show Show all running machines or some specific machine(s) by the given machine name(s).
98 | ssh SSH into a machine
99 | start Start all cluster machines or specific machine(s) by given name(s)
100 | stop stop all cluster machines or specific machine(s) by given name(s)
101 | version Print vind version
102 |
103 | Flags:
104 | -c, --config string Cluster configuration file
105 | -h, --help help for vind
106 |
107 | Use "vind [command] --help" for more information about a command
108 | ```
109 |
110 | ### config
111 |
112 | `vind` reads a description of the **`Cluster`** to create and manage its **`Machines`** from a YAML file, `vind.yaml` by default.
113 |
114 | An alternate name can be specified on the command line with the `--config` or `-c` option, or through the `VIND_CONFIG` environment variable.
115 |
116 | The `config` command helps with creating the initial config file:
117 |
118 | ```sh
119 | $ vind config create --replicas 3
120 | INFO[0000] Creating config file vind.yaml
121 |
122 | $ cat vind.yaml
123 | cluster:
124 | name: cluster
125 | privateKey: cluster-key
126 | machineSets:
127 | - name: test
128 | replicas: 3
129 | spec:
130 | backend: docker
131 | image: brightzheng100/vind-ubuntu:22.04
132 | name: node%d
133 | portMappings:
134 | - containerPort: 22
135 | ```
136 |
137 | You may try `vind config create -h` to see what can be configured through the command, or simply update the YAML file manually if you want to further customize it.
138 |
139 | ### create
140 |
141 | Create the `vind` cluster:
142 |
143 | ```sh
144 | $ vind create
145 | INFO[0000] Pulling image: brightzheng100/vind-ubuntu:22.04 ...
146 | INFO[0005] Creating machine: cluster-test-node0 ...
147 | INFO[0005] Starting machine test-node0...
148 | INFO[0006] Creating machine: cluster-test-node1 ...
149 | INFO[0006] Starting machine test-node1...
150 | INFO[0006] Creating machine: cluster-test-node2 ...
151 | INFO[0006] Starting machine test-node2...
152 | ```
153 |
154 | At first time, it may take 1 minute or so to pull the Docker image and then create the machines.
155 | The creation of the machines typically takes just a few seconds.
156 |
157 | > Note: since we've created the `vind.yaml` by `vind config create --replicas 3` in above step, we need not to specify it in this step's command. The same applies to the rest of commands.
158 |
159 | ### show
160 |
161 | You may use `show` command to display the cluster details.
162 |
163 | ```sh
164 | $ vind show
165 | ```
166 |
167 | Output:
168 |
169 | ```
170 | CONTAINER NAME MACHINE NAME PORTS IP IMAGE CMD STATE BACKEND
171 | cluster-test-node0 test-node0 35827->22 10.88.0.21 brightzheng100/vind-ubuntu:22.04 /sbin/init Running docker
172 | cluster-test-node1 test-node1 34929->22 10.88.0.22 brightzheng100/vind-ubuntu:22.04 /sbin/init Running docker
173 | cluster-test-node2 test-node2 34237->22 10.88.0.23 brightzheng100/vind-ubuntu:22.04 /sbin/init Running docker
174 | ```
175 |
176 | Actually there are just some Docker containers.
177 |
178 | Here, let's understand a bit on the naming, by given `cluster-test-node0` in our case: **{CLUSTER_NAME}**-**{MACHINE_SET}**-**{MACHINE_NAME_WITH_INDEX}**.
179 | - `cluster` is really the Cluster name, which can be any sensible name specified in YAML file's `cluster.name`.
180 | - `test` is the MachineSet's name.
181 | - `node{n}` is the Machine's name with index. Typically, we need to specify the machine with a desired index pattern, like `node%d`, or `node-%d`.
182 |
183 | Please note that there are some useful output formats, which can be specified by `--output` or `-o` parameter:
184 |
185 | - `table`: the default tab-based table-like format, as shown above.
186 | - `json`: the JSON format.
187 | - `ansible`: the Ansible inventory format. Once exported as say `inventory.yaml`, you can play with `vind` Machines like `ansible -i inventory.yaml -m ping all`.
188 | - `ssh`: the SSH config format. Once exported as say `ssh.config`, you can play with regular SSH command like `ssh -F ssh.config vind-node0`.
189 |
190 |
191 | ### ssh
192 |
193 | SSH into a machine with `ssh [[USER@]]`, where the `` is the combination of MachineSet's name and Machine's name.
194 |
195 | ```sh
196 | $ vind ssh test-node0
197 | root@test-node0:~# ps fx
198 | PID TTY STAT TIME COMMAND
199 | 1 ? Ss 0:00 /sbin/init
200 | 15 ? Ss 0:00 /lib/systemd/systemd-journald
201 | 30 ? Ss 0:00 /lib/systemd/systemd-logind
202 | 32 ? Ss 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
203 | 63 ? Ss 0:00 \_ sshd: root@pts/1
204 | 81 pts/1 Ss 0:00 \_ -bash
205 | 87 pts/1 R+ 0:00 \_ ps fx
206 | 66 ? Ss 0:00 /lib/systemd/systemd --user
207 | 67 ? S 0:00 \_ (sd-pam)
208 | ```
209 |
210 | > Note:
211 | > 1. The machine user name can be other user, instead of `root`, if that's prepared in the Docker image and is specified in the YAML file.
212 | > 2. The `[[USER@]]` is optional: when no machine is specified, it will automatically pick the first machine.
213 |
214 | ### stop
215 |
216 | You can stop one, or some specific machines, or all if nothing is specified.
217 |
218 | To stop `test-node1`:
219 |
220 | ```sh
221 | $ vind stop test-node1
222 | INFO[0000] Stopping machine: cluster-test-node1 ...
223 | ```
224 |
225 | Or stop all machines in the cluster -- it will detect whether the machine is in `stopped` state:
226 |
227 | ```sh
228 | $ vind stop
229 | INFO[0000] Stopping machine: cluster-test-node0 ...
230 | INFO[0000] Machine cluster-test-node1 is already stopped...
231 | INFO[0000] Stopping machine: cluster-test-node2 ...
232 | ```
233 |
234 | ### start
235 |
236 | You can start one, or some specific machines, or all if nothing is specified.
237 |
238 | To start `test-node1`:
239 |
240 | ```sh
241 | $ vind start test-node1
242 | INFO[0000] Starting machine: test-node1 ...
243 | ```
244 |
245 | Or start all machines in the cluster -- it will detect whether the machine is in `started` state:
246 |
247 | ```sh
248 | $ vind start
249 | INFO[0000] Starting machine: test-node0 ...
250 | INFO[0000] Machine test-node1 is already started...
251 | INFO[0000] Starting machine: test-node2 ...
252 | ```
253 |
254 | ### cp
255 |
256 | Copying files / folders between the host and machine can be useful.
257 |
258 | - Copy a file from the machine to host:
259 |
260 | ```sh
261 | $ vind cp test-node1:/etc/resolv.conf .
262 | $ ls -l resolv.conf
263 | -rw-r--r-- 1 brightzheng staff 43 Jan 7 17:54 resolv.conf
264 | ```
265 |
266 | - Copy a file from the host to the machine:
267 |
268 | ```sh
269 | $ vind cp README.md test-node1:/root/
270 |
271 | $ vind ssh test-node1
272 | root@test-node1:~# ls -l
273 | total 4
274 | -rw-r--r--. 1 501 dialout 107 Jan 5 05:07 README.md
275 | ```
276 |
277 | ### delete
278 |
279 | Once the VM job is done, the machines can be easily deleted too.
280 |
281 | ```sh
282 | $ vind delete
283 | INFO[0000] Machine test-node0 is started, stopping and deleting machine...
284 | INFO[0000] Machine test-node1 is started, stopping and deleting machine...
285 | INFO[0001] Machine test-node2 is started, stopping and deleting machine...
286 |
287 | $ docker ps
288 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
289 | ```
290 |
291 | ## Images
292 |
293 | I've created a series of Docker images, covering Ubuntu, CentOS, Debian, Fedora, Amazon Linux, by inheriting from original `footloose`'s legacy with necessary enhancements (e.g. multi-arch build). Each of which will act like the VM by following some industrial practices.
294 |
295 | You may refer to [images/README.md](./images/README.md) for what have been prepared and how to customize.
296 |
297 | ## Use Cases
298 |
299 | In the [demo/README.md](./demo/README.md), I've shared some interesting experiments as use cases that you may explore, on top of the `vind` fundamental capabilities.
300 |
301 | For example:
302 | - [General demo](./demo/README.md#general-demo): The automated demo to showcase the basic usage of `vind`.
303 | - [Demo: Docker in `vind`](./demo/README.md#demo-docker-in-vind): About how to run Docker in `vind`'s Machine.
304 | - [Demo: Kubernetes in `vind`](./demo/README.md#demo-kubernetes-in-vind): About how to build multi-node Kubernetes cluster from scratch with `vind`'s Machines.
305 | - [Demo: Ansible](./demo/README.md#demo-ansible): About how to play with Ansible with `vind`'s Machines.
306 | - And more to come -- don't forget to let me know if you've got some more interesting use cases, and PRs are always welcome.
307 |
308 | ## How About `podman`?
309 |
310 | Under the hood, `vind` orchestrates the `docker` commands while having some logic on top.
311 |
312 | Since `podman` is `docker` compatible, at least in the commands that `vind` uses, `podman` is also supported.
313 |
314 | To make it work, what we need to do is to create a softlink from `docker` to `podman`.
315 |
316 | For example, in my Mac:
317 |
318 | ```sh
319 | $ which podman
320 | /opt/homebrew/bin/podman
321 |
322 | $ ln -s `which podman` /usr/local/bin/docker
323 | $ ls -al /usr/local/bin/docker
324 | lrwxr-xr-x ... /usr/local/bin/docker -> /opt/homebrew/bin/podman
325 |
326 | $ which docker
327 | /usr/local/bin/docker
328 | ```
329 |
330 | That's it, and `vind` will be working friendly with `podman` as it will treat `podman` as Docker.
331 |
332 |
333 | ## Helpful Tips
334 |
335 | ### Run Docker into `vind` Machine?
336 |
337 | Docker in Docker container is tricky but as promised, it's totally possible in `vind`.
338 |
339 | What we need to do is to enable `privileged: true` like [./demo/docker-in-vind.yaml](./demo/docker-in-vind.yaml).
340 |
341 | Then you're good to go to install Docker like you do in normal Linux, by following official doc [here](https://docs.docker.com/engine/install/ubuntu/).
342 |
343 |
344 | ### Auto Bind Mount Host
345 |
346 | Even `vind` offers `cp` command to streamline the folders / files sync up between host and machines, it would be great if the host file system is automatically bind mounted into the `vind` machines.
347 |
348 | This is achievable by defining a special bind mount like this, which simply says that the root file system, which is `/`, is bind mounted to `vind` machine's `/host`:
349 |
350 | ```yaml
351 | volumes:
352 | - type: bind
353 | source: /
354 | destination: /host
355 | ```
356 |
357 | You may refer to [./demo/ubunt-2.yaml](./demo/ubuntu-2.yaml) for the usage.
358 |
359 | Once you've done so, after `vind ssh`, the command will try to automatically redirect to where you're in the current host folder. For example:
360 |
361 | ```sh
362 | $ pwd
363 | /Users/brightzheng/development/go/projects/vind/demo
364 |
365 | $ ls
366 | README.md cluster-key cluster-key.pub demo.cast demo.sh ubuntu-1.yaml ubuntu-2.yaml
367 |
368 | $ vind create -c ubuntu-2.yaml
369 |
370 | $ vind ssh normal-node0 -c ubuntu-2.yaml
371 | INFO[0000] SSH into machine [normal-node0] with user [root]
372 | INFO[0000] Trying to cd into: /host/Users/brightzheng/development/go/projects/vind/demo
373 | root@normal-node0:/host/Users/brightzheng/development/go/projects/vind/demo# ls
374 | README.md cluster-key cluster-key.pub demo.cast demo.sh ubuntu-1.yaml ubuntu-2.yaml
375 | ```
376 |
377 | ## Contributions
378 |
379 | Your issues, PRs, feedback, and whatever makes sense to making `vind` better is always welcome!
380 |
--------------------------------------------------------------------------------
/cmd/config.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "os"
21 |
22 | "github.com/brightzheng100/vind/pkg/utils"
23 | "github.com/spf13/cobra"
24 | )
25 |
26 | // Footloose is the default name of the footloose file.
27 | const DEFAULT_CONFIG_FILE = "vind.yaml"
28 |
29 | // configCmd represents the config command
30 | var configCmd = &cobra.Command{
31 | Use: "config",
32 | Short: "Manage cluster configuration",
33 | }
34 |
35 | func init() {
36 | rootCmd.AddCommand(configCmd)
37 | }
38 |
39 | func configFile(file string) string {
40 | if file != "" {
41 | utils.Logger.Infof("Config file used: %s", file)
42 | return file
43 | } else {
44 | file = os.Getenv("VIND_CONFIG")
45 | utils.Logger.Debugf("No config file specified, try getting from $VIND_CONFIG: %s", file)
46 | if file != "" {
47 | utils.Logger.Infof("Config file used by $VIND_CONFIG: %s", file)
48 | } else {
49 | utils.Logger.Infof("Fall back to default config file: %s", DEFAULT_CONFIG_FILE)
50 | file = DEFAULT_CONFIG_FILE
51 | }
52 | return file
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/config_create.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "fmt"
21 | "os"
22 |
23 | "github.com/brightzheng100/vind/pkg/cluster"
24 | "github.com/brightzheng100/vind/pkg/utils"
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var configCreateCmd = &cobra.Command{
29 | Use: "create",
30 | Short: "Create a cluster configuration",
31 | Long: `Create a cluster configuration
32 |
33 | A sample of the cluster configuration file may include a list of configurable elements.
34 |
35 | For example, below command will create a new vind.yaml as follows:
36 |
37 | vind config create -n my-cluster -k key -s ubuntu --networks my-network -r 3 -i brightzheng100/vind-ubuntu22:arm64
38 |
39 | cluster:
40 | name: my-cluster
41 | privateKey: key
42 | machines:
43 | - count: 3
44 | name: ubuntu
45 | spec:
46 | backend: docker
47 | image: brightzheng100/vind-ubuntu22:arm64
48 | name: node%d
49 | networks:
50 | - my-network
51 | portMappings:
52 | - containerPort: 22
53 | `,
54 | RunE: configCreate,
55 | }
56 |
57 | var configCreateOptions struct {
58 | override bool
59 | file string
60 | }
61 |
62 | func init() {
63 | configCreateCmd.Flags().StringVarP(&configCreateOptions.file, "config", "c", DEFAULT_CONFIG_FILE, "Generated cluster configuration file")
64 | configCreateCmd.Flags().BoolVarP(&configCreateOptions.override, "override", "o", false, "Override configuration file if it exists")
65 |
66 | name := &defaultConfig.Cluster.Name
67 | configCreateCmd.PersistentFlags().StringVarP(name, "name", "n", *name, "Name of the cluster")
68 |
69 | private := &defaultConfig.Cluster.PrivateKey
70 | configCreateCmd.PersistentFlags().StringVarP(private, "key", "k", *private, "Name of the private and public key files")
71 |
72 | machineSetName := &defaultConfig.MachineSets[0].Name
73 | configCreateCmd.PersistentFlags().StringVarP(machineSetName, "machineset", "s", *machineSetName, "Name of the MachineSet")
74 |
75 | networks := &defaultConfig.MachineSets[0].Spec.Networks
76 | configCreateCmd.PersistentFlags().StringSliceVar(networks, "networks", *networks, "Networks names the machines are assigned to")
77 |
78 | replicas := &defaultConfig.MachineSets[0].Replicas
79 | configCreateCmd.PersistentFlags().IntVarP(replicas, "replicas", "r", *replicas, "Number of MachineSet's machine replicas")
80 |
81 | image := &defaultConfig.MachineSets[0].Spec.Image
82 | configCreateCmd.PersistentFlags().StringVarP(image, "image", "i", *image, "Docker image to use in the containers")
83 |
84 | privileged := &defaultConfig.MachineSets[0].Spec.Privileged
85 | configCreateCmd.PersistentFlags().BoolVar(privileged, "privileged", *privileged, "Create privileged containers")
86 |
87 | cmd := &defaultConfig.MachineSets[0].Spec.Cmd
88 | configCreateCmd.PersistentFlags().StringVarP(cmd, "cmd", "d", *cmd, "The command to execute on the container")
89 |
90 | configCmd.AddCommand(configCreateCmd)
91 | }
92 |
93 | func configCreate(cmd *cobra.Command, args []string) error {
94 | opts := &configCreateOptions
95 |
96 | utils.Logger.Infof("Creating config file %s", opts.file)
97 |
98 | cluster, err := cluster.New(defaultConfig)
99 | if err != nil {
100 | return err
101 | }
102 | if configExists(configFile(opts.file)) && !opts.override {
103 | utils.Logger.Warnf("Failed due to configuration file at %s already exists", opts.file)
104 | return fmt.Errorf("configuration file at %s already exists. Override it by specifying --override or -o", opts.file)
105 | }
106 | return cluster.Save(configFile(opts.file))
107 | }
108 |
109 | // configExists checks whether a configuration file exists.
110 | // Returns false if not true if it already exists.
111 | func configExists(path string) bool {
112 | info, err := os.Stat(path)
113 | if os.IsNotExist(err) || os.IsPermission(err) {
114 | return false
115 | }
116 | return !info.IsDir()
117 | }
118 |
--------------------------------------------------------------------------------
/cmd/copy.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024-2025 Bright Zheng
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "errors"
20 | "fmt"
21 | "strings"
22 |
23 | c "github.com/brightzheng100/vind/pkg/cluster"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | // cpCmd represents the cp command
28 | var cpCmd = &cobra.Command{
29 | Use: "cp",
30 | Aliases: []string{"copy"},
31 | Short: "Copy files or folders between a machine and the host file system",
32 | Long: `
33 | cp
34 | cp
35 |
36 | Copy files or folders between a machine and the host file system
37 | `,
38 | Args: validateCpArgs,
39 | RunE: copy,
40 | }
41 |
42 | func init() {
43 | rootCmd.AddCommand(cpCmd)
44 | }
45 |
46 | func copy(cmd *cobra.Command, args []string) error {
47 | cluster, err := c.NewFromFile(configFile(cfgFile.config))
48 | if err != nil {
49 | return err
50 | }
51 | // from: machine to host
52 | // to: host to machine
53 | copyFrom := strings.Contains(args[0], ":")
54 | copyTo := strings.Contains(args[1], ":")
55 |
56 | if copyFrom && copyTo || !copyFrom && !copyTo {
57 | return errors.New("either copy from or to machine is supported")
58 | }
59 |
60 | var machineName, srcPath, destPath string
61 |
62 | if copyFrom { // copy from machine, like: cp machine:/root/ .
63 | src := strings.Split(args[0], ":")
64 | machineName = src[0]
65 | srcPath = src[1]
66 |
67 | destPath = args[1]
68 |
69 | machine, err := cluster.GetMachineByMachineName(machineName)
70 | if err != nil {
71 | return fmt.Errorf("machine name not found: %s", machineName)
72 | }
73 |
74 | return cluster.CopyFrom(machine, srcPath, destPath)
75 | } else { // copy to machine, like: cp ./file machine:/root/
76 | srcPath := args[0]
77 |
78 | dest := strings.Split(args[1], ":")
79 | machineName = dest[0]
80 | destPath = dest[1]
81 |
82 | machine, err := cluster.GetMachineByMachineName(machineName)
83 | if err != nil {
84 | return fmt.Errorf("machine name not found: %s", machineName)
85 | }
86 |
87 | return cluster.CopyTo(srcPath, machine, destPath)
88 | }
89 | }
90 |
91 | func validateCpArgs(cmd *cobra.Command, args []string) error {
92 | if len(args) != 2 {
93 | return errors.New("both src and dest must be provided")
94 | }
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/cmd/create.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "github.com/brightzheng100/vind/pkg/cluster"
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | // createCmd represents the create command
25 | var createCmd = &cobra.Command{
26 | Use: "create",
27 | Short: "Create a cluster",
28 | RunE: create,
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(createCmd)
33 | }
34 |
35 | func create(cmd *cobra.Command, args []string) error {
36 | cluster, err := cluster.NewFromFile(configFile(cfgFile.config))
37 | if err != nil {
38 | return err
39 | }
40 | return cluster.Create()
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/defaults.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import "github.com/brightzheng100/vind/pkg/config"
20 |
21 | var defaultConfig = config.Config{
22 | Cluster: config.Cluster{
23 | Name: "cluster",
24 | PrivateKey: "cluster-key",
25 | },
26 | MachineSets: []config.MachineSet{{
27 | Replicas: 1,
28 | Name: "test",
29 | Spec: config.Machine{
30 | Name: "node%d",
31 | Image: "brightzheng100/vind-ubuntu:22.04",
32 | PortMappings: []config.PortMapping{{
33 | ContainerPort: 22,
34 | }},
35 | Backend: "docker",
36 | },
37 | }},
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/delete.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "github.com/brightzheng100/vind/pkg/cluster"
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | // deleteCmd represents the delete command
25 | var deleteCmd = &cobra.Command{
26 | Use: "delete",
27 | Short: "Delete a cluster",
28 | RunE: delete,
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(deleteCmd)
33 | }
34 |
35 | func delete(cmd *cobra.Command, args []string) error {
36 | cluster, err := cluster.NewFromFile(configFile(cfgFile.config))
37 | if err != nil {
38 | return err
39 | }
40 | return cluster.Delete()
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "os"
21 |
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // rootCmd represents the base command when called without any subcommands
26 | var rootCmd = &cobra.Command{
27 | Use: "vind",
28 | Short: "A tool to create containers that look and work like virtual machines, on Docker.",
29 | }
30 |
31 | // Execute adds all child commands to the root command and sets flags appropriately.
32 | // This is called by main.main(). It only needs to happen once to the rootCmd.
33 | func Execute() {
34 | err := rootCmd.Execute()
35 | if err != nil {
36 | os.Exit(1)
37 | }
38 | }
39 |
40 | var cfgFile struct {
41 | config string
42 | }
43 |
44 | func init() {
45 | rootCmd.PersistentFlags().StringVarP(&cfgFile.config, "config", "c", "", "Cluster configuration file")
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/show.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "fmt"
21 | "os"
22 |
23 | "github.com/brightzheng100/vind/pkg/cluster"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | // showCmd represents the show command
28 | var showCmd = &cobra.Command{
29 | Use: "show [MACHINE_NAME1 [MACHINE_NAME2] [...]]",
30 | Aliases: []string{"status"},
31 | Short: "Show all running machines or some specific machine(s) by the given machine name(s).",
32 | Long: `Shows all cluster machines or some specific machine(s) if the given machine name(s) are specified, in JSON or Table format.
33 | `,
34 | RunE: show,
35 | }
36 |
37 | var showOptions struct {
38 | output string
39 | }
40 |
41 | func init() {
42 | showCmd.Flags().StringVarP(&showOptions.output, "output", "o", "table", "Output formatting options: {table,json,ansible,ssh}.")
43 | rootCmd.AddCommand(showCmd)
44 | }
45 |
46 | // show will show all machines in a given cluster.
47 | func show(cmd *cobra.Command, args []string) error {
48 | c, err := cluster.NewFromFile(configFile(cfgFile.config))
49 | if err != nil {
50 | return err
51 | }
52 | var formatter cluster.Formatter
53 | switch showOptions.output {
54 | case "table":
55 | formatter = new(cluster.TableFormatter)
56 | case "json":
57 | formatter = new(cluster.JSONFormatter)
58 | case "ansible":
59 | formatter = new(cluster.AnsibleFormatter)
60 | case "ssh":
61 | formatter = new(cluster.SSHConfigFormatter)
62 | default:
63 | return fmt.Errorf("unknown formatter '%s'", showOptions.output)
64 | }
65 | machines, err := c.Show(args)
66 | if err != nil {
67 | return err
68 | }
69 | return formatter.Format(os.Stdout, c, machines)
70 | }
71 |
--------------------------------------------------------------------------------
/cmd/ssh.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "fmt"
21 | "strings"
22 |
23 | "github.com/pkg/errors"
24 |
25 | c "github.com/brightzheng100/vind/pkg/cluster"
26 | "github.com/spf13/cobra"
27 | )
28 |
29 | // sshCmd represents the ssh command
30 | var sshCmd = &cobra.Command{
31 | Use: "ssh [[USER@]]",
32 | Short: "SSH into a specific machine, or first machine if not specified",
33 | Args: validateSSHArgs,
34 | RunE: ssh,
35 | }
36 |
37 | var configOptions struct {
38 | extraSshArgs string
39 | }
40 |
41 | func init() {
42 | sshCmd.Flags().StringVarP(&configOptions.extraSshArgs, "extra-ssh-args", "e", "", "Extra args for SSH command")
43 | rootCmd.AddCommand(sshCmd)
44 | }
45 |
46 | func ssh(cmd *cobra.Command, args []string) error {
47 | cluster, err := c.NewFromFile(configFile(cfgFile.config))
48 | if err != nil {
49 | return err
50 | }
51 |
52 | var machine *c.Machine
53 | var machineName string
54 | var userName string
55 |
56 | if len(args) == 0 {
57 | machine, err = cluster.GetFirstMachine()
58 | if err != nil {
59 | return errors.Wrap(err, "SSH into the first machine failed")
60 | }
61 | } else {
62 | if strings.Contains(args[0], "@") {
63 | items := strings.Split(args[0], "@")
64 | if len(items) != 2 {
65 | return fmt.Errorf("bad syntax for user@machineName: %v", items)
66 | }
67 | userName = items[0]
68 | machineName = items[1]
69 | } else {
70 | machineName = args[0]
71 | }
72 |
73 | machine, err = cluster.GetMachineByMachineName(machineName)
74 | if err != nil {
75 | return fmt.Errorf("machine name not found: %s", machineName)
76 | }
77 | }
78 |
79 | if userName == "" {
80 | userName = machine.User()
81 | }
82 |
83 | return cluster.SSH(machine, userName, configOptions.extraSshArgs)
84 | }
85 |
86 | func validateSSHArgs(cmd *cobra.Command, args []string) error {
87 | if len(args) > 1 {
88 | return errors.New("too many args")
89 | }
90 | return nil
91 | }
92 |
--------------------------------------------------------------------------------
/cmd/start.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "github.com/brightzheng100/vind/pkg/cluster"
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | // startCmd represents the start command
25 | var startCmd = &cobra.Command{
26 | Use: "start [MACHINE_NAME1 [MACHINE_NAME2] [...]]",
27 | Short: "Start all cluster machines or specific machine(s) by given name(s)",
28 | RunE: start,
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(startCmd)
33 | }
34 |
35 | func start(cmd *cobra.Command, args []string) error {
36 | cluster, err := cluster.NewFromFile(configFile(cfgFile.config))
37 | if err != nil {
38 | return err
39 | }
40 | return cluster.Start(args)
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/stop.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "github.com/brightzheng100/vind/pkg/cluster"
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | // stopCmd represents the stop command
25 | var stopCmd = &cobra.Command{
26 | Use: "stop [MACHINE_NAME1 [MACHINE_NAME2] [...]]",
27 | Short: "stop all cluster machines or specific machine(s) by given name(s)",
28 | RunE: stop,
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(stopCmd)
33 | }
34 |
35 | func stop(cmd *cobra.Command, args []string) error {
36 | cluster, err := cluster.NewFromFile(configFile(cfgFile.config))
37 | if err != nil {
38 | return err
39 | }
40 | return cluster.Stop(args)
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cmd
18 |
19 | import (
20 | "fmt"
21 | "strings"
22 |
23 | release "github.com/brightzheng100/vind/pkg/version"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | // versionCmd represents the version command
28 | var versionCmd = &cobra.Command{
29 | Use: "version",
30 | Short: "Print vind version",
31 | Run: showVersion,
32 | }
33 |
34 | func init() {
35 | rootCmd.AddCommand(versionCmd)
36 | }
37 |
38 | var version = "git"
39 | var commit = ""
40 | var date = ""
41 |
42 | func showVersion(cmd *cobra.Command, args []string) {
43 | fmt.Println("version:", version, "commit:", commit, "date:", date)
44 | if version == "git" {
45 | return
46 | }
47 | release, err := release.FindLastRelease()
48 | if err != nil {
49 | fmt.Println("version: failed to check for new versions. You may want to check it out at https://github.com/brightzheng100/vind/releases.")
50 | return
51 | }
52 | if strings.Compare(version, *release.TagName) != 0 {
53 | fmt.Printf("New version %v is available. More information at: %v\n", *release.TagName, *release.HTMLURL)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Demo
2 |
3 | ## General demo
4 |
5 | [](https://asciinema.org/a/697321)
6 |
7 | I used [`asciinema`](https://docs.asciinema.org/) to record the demo, where the demo script is powered by []`demo magic`](https://github.com/paxtonhare/demo-magic) to make the demo process automated and reproducible.
8 |
9 | Check out the demo script [here](./demo.sh)
10 |
11 |
12 | ## Demo: Docker in `vind`
13 |
14 | To run Docker in `vind`, the only thing we need to do is to make sure the MachineSet specifies `privileged: true`, like [docker-in-vind.yaml](./docker-in-vind.yaml):
15 |
16 | ```sh
17 | cluster:
18 | name: cluster
19 | privateKey: cluster-key
20 | machineSets:
21 | - name: ubuntu
22 | replicas: 1
23 | spec:
24 | image: brightzheng100/vind-ubuntu:22.04
25 | name: node%d
26 | networks:
27 | - my-network
28 | portMappings:
29 | - containerPort: 22
30 | privileged: true # <-- this does the trick
31 | ```
32 |
33 | Once you've created the `vind` Machine, and you can `vind ssh` into it and do whatever you typically do in a VM for Docker.
34 |
35 |
36 | ## Demo: Kubernetes in `vind`
37 |
38 | Kubernetes in `vind` is much more complicated than one may typically expect.
39 |
40 | There are a few tweaks we have to do and the best place to learn is `kind` project's [entrypoint](https://github.com/kubernetes-sigs/kind/blob/main/images/base/files/usr/local/bin/entrypoint) and [provision.go](https://github.com/kubernetes-sigs/kind/blob/main/pkg/cluster/internal/providers/docker/provision.go#L135-L205).
41 |
42 | In this demo, I also reuse the work `kind` has done for the tweaks.
43 |
44 | ### 1. Rebuild the image with `kind`'s `entrypoint` shell script as the entrypoint.
45 |
46 | > Note: you may skip this step as I've built and host the image here: `brightzheng100/vind-ubuntu:k8s`
47 |
48 | Refer to [Dockerfile](./k8s-in-vind/Dockerfile):
49 |
50 | ```Dockerfile
51 | ...
52 | # Create the /kind folder to facilitate kind's hacking/patching process
53 | RUN mkdir /kind
54 |
55 | # The entrypoint is copied from kind project
56 | # Here: https://github.com/kubernetes-sigs/kind/blob/main/images/base/files/usr/local/bin/entrypoint
57 | COPY --chmod=0755 entrypoint /usr/local/bin/entrypoint
58 |
59 | # Comment out the default ENTRYPOINT and configure it in vind's YAML
60 | # ENTRYPOINT [ "/usr/local/bin/entrypoint", "/bin/bash" ]
61 | ```
62 |
63 | Then build it -- make sure we're in `./demo/k8s-in-vind` folder:
64 |
65 | By Docker:
66 |
67 | ```sh
68 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile --push -t brightzheng100/vind-ubuntu:k8s .
69 | ```
70 |
71 | Or, by Podman:
72 |
73 | ```sh
74 | # 0. If you already have the manifest (for example, by doing more than 1 time)
75 | podman manifest rm brightzheng100/vind-ubuntu:k8s-manifest
76 |
77 | # 1. Build them with a manifest specified
78 | podman build --platform linux/amd64,linux/arm64 --file Dockerfile --manifest brightzheng100/vind-ubuntu:k8s-manifest .
79 |
80 | # 2. Push manifest with the targeted image tag
81 | podman manifest push brightzheng100/vind-ubuntu:k8s-manifest brightzheng100/vind-ubuntu:k8s
82 | ```
83 |
84 | ### 2. Create `vind` cluster
85 |
86 | As usual, create the `vind` config file with the image built above -- refer to [k8s-in-vind.yaml](./k8s-in-vind/k8s-in-vind.yaml):
87 |
88 | ```yaml
89 | cluster:
90 | name: cluster
91 | privateKey: cluster-key
92 | machineSets:
93 | - name: k8s
94 | replicas: 3
95 | spec:
96 | image: brightzheng100/vind-ubuntu:k8s
97 | name: node%d
98 | user: ubuntu
99 | networks:
100 | - my-network
101 | portMappings:
102 | - containerPort: 22
103 | privileged: true
104 | cmd: /usr/local/bin/entrypoint /sbin/init
105 | ```
106 |
107 | Then, create the cluster -- before that, make sure the Docker network namely `my-network` is created by `docker network create my-network`:
108 |
109 | ```sh
110 | # Expose VIND_CONFIG and point to the right vind YAML file
111 | $ export VIND_CONFIG=`PWD`/k8s-in-vind/k8s-in-vind.yaml
112 |
113 | # Create the vind cluster
114 | $ vind create
115 |
116 | # Now we have 3 machines
117 | $ vind show
118 | CONTAINER NAME MACHINE NAME PORTS IP IMAGE CMD STATE
119 | cluster-k8s-node0 k8s-node0 44107->22 10.89.0.29 brightzheng100/vind-ubuntu:k8s /usr/local/bin/entrypoint,/sbin/init Running
120 | cluster-k8s-node1 k8s-node1 46607->22 10.89.0.30 brightzheng100/vind-ubuntu:k8s /usr/local/bin/entrypoint,/sbin/init Running
121 | cluster-k8s-node2 k8s-node2 33375->22 10.89.0.31 brightzheng100/vind-ubuntu:k8s /usr/local/bin/entrypoint,/sbin/init Running
122 | ```
123 |
124 | ### 3. Bootstrap it
125 |
126 | SSH into `node0`:
127 |
128 | ```sh
129 | vind ssh k8s-node0
130 | ```
131 |
132 | Reuse the boostrapping scripts I built with `kubeadm`:
133 |
134 | ```sh
135 | ubuntu@k8s-node0:~$ sudo apt-get update
136 | ubuntu@k8s-node0:~$ sudo apt-get install git -y
137 | ubuntu@k8s-node0:~$ git clone https://github.com/brightzheng100/instana-handson-labs.git
138 | ubuntu@k8s-node0:~$ cd instana-handson-labs/scripts
139 | ubuntu@k8s-node0:~/instana-handson-labs/scripts$ ./bootstrap-k8s.sh
140 | ```
141 |
142 | Then the `kubeadm` bootstrapped Kubernetes should be ready in just a few minutes:
143 |
144 | ```sh
145 | ubuntu@k8s-node0:~/instana-handson-labs/scripts$ kubectl get pod -A -w
146 | NAMESPACE NAME READY STATUS RESTARTS AGE
147 | kube-system calico-kube-controllers-5b9b456c66-tsgbc 1/1 Running 0 67s
148 | kube-system calico-node-crjmb 1/1 Running 0 67s
149 | kube-system coredns-55cb58b774-b9twc 1/1 Running 0 5m36s
150 | kube-system coredns-55cb58b774-jhnr4 1/1 Running 0 5m35s
151 | kube-system etcd-k8s-node0 1/1 Running 0 5m52s
152 | kube-system kube-apiserver-k8s-node0 1/1 Running 0 5m52s
153 | kube-system kube-controller-manager-k8s-node0 1/1 Running 0 5m52s
154 | kube-system kube-proxy-47zm2 1/1 Running 0 5m36s
155 | kube-system kube-scheduler-k8s-node0 1/1 Running 0 5m52s
156 | local-path-storage local-path-provisioner-8ffbb88cb-7dwwv 1/1 Running 0 57s
157 | ```
158 |
159 | ### 4. Join other nodes into the cluster
160 |
161 | At the end of the bootstrap command `./bootstrap-k8s.sh`, there should have printed out the join command like:
162 |
163 | ```sh
164 | kubeadm join 10.89.0.29:6443 --token ypmsa4.1u4ldos4vusk657o \
165 | --discovery-token-ca-cert-hash sha256:7467a28f6aa2e3ebbc0cbe055a83f5908620c0019fcbee733c1bbed784b8d617
166 | ```
167 |
168 | Copy and keep it first.
169 |
170 | Then, `vind ssh` into other nodes to join the cluster.
171 |
172 | #### 4.1 Join `node1` into the cluster
173 |
174 | SSH into machine `node1`:
175 |
176 | ```sh
177 | $ vind ssh k8s-node1
178 | ```
179 |
180 | Prepare the node:
181 |
182 | ```sh
183 | ubuntu@k8s-node1:~$ sudo apt-get update
184 | ubuntu@k8s-node1:~$ sudo apt-get install git -y
185 | ubuntu@k8s-node1:~$ git clone https://github.com/brightzheng100/instana-handson-labs.git
186 | ubuntu@k8s-node1:~$ cd instana-handson-labs/scripts
187 | ubuntu@k8s-node1:~/instana-handson-labs/scripts$ ./prepare-join-k8s.sh
188 | ```
189 |
190 | Now let's slightly update the generated `kubeadm join` command with `sudo` (as it needs root permission to run) and `--ignore-preflight-errors=all` flag:
191 |
192 | ```sh
193 | $ sudo kubeadm join 10.89.0.29:6443 --token ypmsa4.1u4ldos4vusk657o \
194 | --discovery-token-ca-cert-hash sha256:7467a28f6aa2e3ebbc0cbe055a83f5908620c0019fcbee733c1bbed784b8d617 \
195 | --ignore-preflight-errors=all
196 | ```
197 |
198 | Just wait for 1 minute or so, we can check in `node0` (NOT current `node1` as we haven't prepared the kubeconfig yet).
199 |
200 | ```sh
201 | ubuntu@k8s-node0:~$ kubectl get node
202 | NAME STATUS ROLES AGE VERSION
203 | k8s-node0 Ready control-plane 45m v1.30.9
204 | k8s-node1 Ready 4m26s v1.30.9
205 | ```
206 |
207 | Yes, our cluster has two nodes!
208 |
209 | > Note: it may take 1 minute or so to turn the status into `Ready`. That's fine and just be patient.
210 |
211 | #### 4.2 Join `node2` into the cluster
212 |
213 | Similarly, SSH into machine `node2`:
214 |
215 | ```sh
216 | $ vind ssh k8s-node2
217 | ```
218 |
219 | Prepare the node:
220 |
221 | ```sh
222 | ubuntu@k8s-node2:~$ sudo apt-get update
223 | ubuntu@k8s-node2:~$ sudo apt-get install git -y
224 | ubuntu@k8s-node2:~$ git clone https://github.com/brightzheng100/instana-handson-labs.git
225 | ubuntu@k8s-node2:~$ cd instana-handson-labs/scripts
226 | ubuntu@k8s-node2:~/instana-handson-labs/scripts$ ./prepare-join-k8s.sh
227 | ```
228 |
229 | Then, copy and run above updated `kubeadm join` command:
230 |
231 | ```sh
232 | $ sudo kubeadm join 10.89.0.29:6443 --token ypmsa4.1u4ldos4vusk657o \
233 | --discovery-token-ca-cert-hash sha256:7467a28f6aa2e3ebbc0cbe055a83f5908620c0019fcbee733c1bbed784b8d617 \
234 | --ignore-preflight-errors=all
235 | ```
236 |
237 | Just wait for 1 minute or so, we can check in `node0` (NOT current `node2` as we haven't prepared the kubeconfig yet).
238 |
239 | ```sh
240 | ubuntu@k8s-node0:~$ kubectl get node
241 | NAME STATUS ROLES AGE VERSION
242 | k8s-node0 Ready control-plane 51m v1.30.9
243 | k8s-node1 Ready 11m v1.30.9
244 | k8s-node2 Ready 91s v1.30.9
245 | ```
246 |
247 | Yes, our cluster has three nodes!
248 |
249 | > Note: it may take 1 minute or so to turn the status into `Ready`. That's fine and just be patient.
250 |
251 | And very soon, the DaemonSets will automatically roll out the pods to all three nodes:
252 |
253 | ```sh
254 | ubuntu@k8s-node0:~$ kubectl get pod -A
255 | NAMESPACE NAME READY STATUS RESTARTS AGE
256 | kube-system calico-kube-controllers-5b9b456c66-tsgbc 1/1 Running 0 47m
257 | kube-system calico-node-9gq4l 1/1 Running 0 2m19s
258 | kube-system calico-node-crjmb 1/1 Running 0 47m
259 | kube-system calico-node-w2x5k 1/1 Running 0 12m
260 | kube-system coredns-55cb58b774-b9twc 1/1 Running 0 52m
261 | kube-system coredns-55cb58b774-jhnr4 1/1 Running 0 52m
262 | kube-system etcd-k8s-node0 1/1 Running 0 52m
263 | kube-system kube-apiserver-k8s-node0 1/1 Running 0 52m
264 | kube-system kube-controller-manager-k8s-node0 1/1 Running 0 52m
265 | kube-system kube-proxy-47zm2 1/1 Running 0 52m
266 | kube-system kube-proxy-f8g4m 1/1 Running 0 12m
267 | kube-system kube-proxy-txqnw 1/1 Running 0 2m19s
268 | kube-system kube-scheduler-k8s-node0 1/1 Running 0 52m
269 | local-path-storage local-path-provisioner-8ffbb88cb-7dwwv 1/1 Running 0 47m
270 | ```
271 |
272 | Done! You've successfully bootstrapped the Kubernetes cluster in a 3-node environment, just like you have 3 "normal" VMs, but for free on Docker.
273 |
274 | ## Demo: Ansible
275 |
276 | When learning and practicing Ansible, we may need some VMs.
277 |
278 | `vind` can be a very handy tool to give you such VMs in seconds.
279 |
280 | Thanks to [@afbjorklund](https://github.com/afbjorklund) in PR #3, we now have a new output format for Ansible in `show` command.
281 |
282 | ### 1. Create vind cluster as usual
283 |
284 | Omitted.
285 |
286 | ### 2. Generate `inventory.yaml` with `show` command
287 |
288 | ```sh
289 | $ vind show
290 | CONTAINER NAME MACHINE NAME PORTS IP IMAGE CMD STATE
291 | cluster-ubuntu-node0 ubuntu-node0 43539->22 10.89.0.6 brightzheng100/vind-ubuntu:22.04 /sbin/init Running
292 | cluster-ubuntu-node1 ubuntu-node1 39615->22 10.89.0.8 brightzheng100/vind-ubuntu:22.04 /sbin/init Running
293 |
294 | $ vind show -o ansible > inventory.yaml
295 |
296 | $ cat inventory.yaml
297 | cluster:
298 | hosts:
299 | ubuntu-node0:
300 | ansible_connection: ssh
301 | ansible_host: localhost
302 | ansible_port: 43539
303 | ansible_ssh_common_args: -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
304 | ansible_ssh_private_key_file: /Users/brightzheng/demos/sandbox/vind/cluster-key
305 | ansible_user: ubuntu
306 | ubuntu-node1:
307 | ansible_connection: ssh
308 | ansible_host: localhost
309 | ansible_port: 39615
310 | ansible_ssh_common_args: -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
311 | ansible_ssh_private_key_file: /Users/brightzheng/demos/sandbox/vind/cluster-key
312 | ansible_user: ubuntu
313 | ```
314 | ### 3. Let's Play with Ansible
315 |
316 | ```sh
317 | $ ansible -i inventory.yaml -m ping all
318 | ubuntu-node1 | SUCCESS => {
319 | "ansible_facts": {
320 | "discovered_interpreter_python": "/usr/bin/python3.10"
321 | },
322 | "changed": false,
323 | "ping": "pong"
324 | }
325 | ubuntu-node0 | SUCCESS => {
326 | "ansible_facts": {
327 | "discovered_interpreter_python": "/usr/bin/python3.10"
328 | },
329 | "changed": false,
330 | "ping": "pong"
331 | }
332 | ```
333 |
--------------------------------------------------------------------------------
/demo/demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ########################
4 | # include the magic
5 | # Check it out from here: https://github.com/paxtonhare/demo-magic
6 | ########################
7 | # In my env, this is how it's initiated
8 | . ~/workspaces/bash/demo-magic/demo-magic.sh
9 |
10 | # Ready
11 | clear
12 |
13 | # Prerequisites
14 | p "# What are the prerequisites? Well, just Docker, or Podman (which is Docker compatible)"
15 | pe "docker --version"
16 |
17 | p "# Typically, you may want to run things in a dedicated network. Let's create a network named \"my-network\""
18 | pe "docker network create my-network"
19 | pe "docker network ls"
20 |
21 | p "# That's all we need as the prerequisites"
22 |
23 | # Get started
24 | p "# Now, let's get started."
25 | p "# Firstly, let's see what commands are supported"
26 | pe "vind -h"
27 |
28 | # CMD: config create
29 | p "# To start, we need to prepare a config yaml file, which can be generated by command."
30 | p "vind config create -n cluster -k cluster-key -s ubuntu --networks my-network -r 2 -i brightzheng100/vind-ubuntu:22.04 -c ubuntu-1.yaml"
31 | p "# Let's have a look at the generated \"ubuntu-1.yaml\" file"
32 | pe "cat ubuntu-1.yaml"
33 |
34 | # CMD: create (single machine)
35 | p "# Now, let's create the cluster"
36 | pe "vind create -c ubuntu-1.yaml"
37 |
38 | # CMD: show
39 | p "# Great, let's see what we've created"
40 | pe "vind show -c ubuntu-1.yaml"
41 | p "# Well, they're actually Docker containers"
42 | pe "docker ps"
43 |
44 | # CMD: ssh (manually exit demo magic is needed)
45 | p "# How does the VM experience look like?"
46 | p "# Firstly, ssh into it"
47 | pe "vind ssh ubuntu-node0 -c ubuntu-1.yaml"
48 | # in the VM, do something like
49 | # whoami
50 | # cat /etc/os-release
51 | # sudo apt-get update
52 | # sudo apt-get install apache2
53 | # sudo systemctl start apache2
54 | # sudo systemctl status apache2
55 | # curl -I localhost
56 | # ps aux
57 |
58 | # CMD: stop & start
59 | p "# We can stop and start the specific machine(s)"
60 | pe "vind stop ubuntu-node0 -c ubuntu-1.yaml"
61 | pe "vind start ubuntu-node0 -c ubuntu-1.yaml"
62 | p "# Or all machines"
63 | pe "vind stop -c ubuntu-1.yaml"
64 | pe "vind start -c ubuntu-1.yaml"
65 |
66 | # CMD: delete
67 | p "# Once the job is done, the machines can be disposed easily"
68 | pe "vind delete -c ubuntu-1.yaml"
69 |
70 | # CMD: create (multiple machine sets)
71 | p "# How if I want to create different machine sets in one shot?"
72 | pe "cat ubuntu-2.yaml"
73 | pe "vind create -c ubuntu-2.yaml"
74 | pe "docker ps"
75 | pe "vind ssh normal-node0 -c ubuntu-2.yaml"
76 | pe "# This is cool as my host directory is bind mounted into the machine too!"
77 |
--------------------------------------------------------------------------------
/demo/docker-in-vind.yaml:
--------------------------------------------------------------------------------
1 | cluster:
2 | name: cluster
3 | privateKey: cluster-key
4 | machineSets:
5 | - name: ubuntu
6 | replicas: 1
7 | spec:
8 | image: brightzheng100/vind-ubuntu:22.04
9 | name: node%d
10 | networks:
11 | - my-network
12 | portMappings:
13 | - containerPort: 22
14 | privileged: true
15 |
--------------------------------------------------------------------------------
/demo/k8s-in-vind/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo \
17 | conntrack iptables nftables && \
18 | apt-get clean && \
19 | rm -rf /var/lib/apt/lists/*
20 |
21 | RUN >/etc/machine-id
22 | RUN >/var/lib/dbus/machine-id
23 |
24 | # Make sure user "ubuntu" has sudoer permission
25 | RUN useradd -ms /bin/bash ubuntu && \
26 | echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
27 |
28 | EXPOSE 22
29 |
30 | RUN systemctl set-default multi-user.target
31 | RUN systemctl mask \
32 | dev-hugepages.mount \
33 | sys-fs-fuse-connections.mount \
34 | systemd-update-utmp.service \
35 | systemd-tmpfiles-setup.service \
36 | console-getty.service
37 | RUN systemctl disable \
38 | networkd-dispatcher.service
39 |
40 | # This container image doesn't have locales installed. Disable forwarding the
41 | # user locale env variables or we get warnings such as:
42 | # bash: warning: setlocale: LC_ALL: cannot change locale
43 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
44 |
45 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
46 | STOPSIGNAL SIGRTMIN+3
47 |
48 | # Create the /kind folder to facilitate kind's hacking/patching process
49 | RUN mkdir /kind
50 |
51 | # The entrypoint is copied from kind project
52 | # Here: https://github.com/kubernetes-sigs/kind/blob/main/images/base/files/usr/local/bin/entrypoint
53 | COPY --chmod=0755 entrypoint /usr/local/bin/entrypoint
54 |
55 | # ENTRYPOINT [ "/usr/local/bin/entrypoint", "/bin/bash" ]
56 |
--------------------------------------------------------------------------------
/demo/k8s-in-vind/k8s-in-vind.yaml:
--------------------------------------------------------------------------------
1 | cluster:
2 | name: cluster
3 | privateKey: cluster-key
4 | machineSets:
5 | - name: k8s
6 | replicas: 3
7 | spec:
8 | image: brightzheng100/vind-ubuntu:k8s
9 | name: node%d
10 | user: ubuntu
11 | networks:
12 | - my-network
13 | portMappings:
14 | - containerPort: 22
15 | privileged: true
16 | cmd: /usr/local/bin/entrypoint /sbin/init
17 |
--------------------------------------------------------------------------------
/demo/ubuntu-1.yaml:
--------------------------------------------------------------------------------
1 | cluster:
2 | name: cluster
3 | privateKey: cluster-key
4 | machineSets:
5 | - name: ubuntu
6 | replicas: 2
7 | spec:
8 | image: brightzheng100/vind-ubuntu:22.04
9 | name: node%d
10 | user: ubuntu
11 | portMappings:
12 | - containerPort: 22
13 | networks:
14 | - my-network
15 |
--------------------------------------------------------------------------------
/demo/ubuntu-2.yaml:
--------------------------------------------------------------------------------
1 | cluster:
2 | name: cluster
3 | privateKey: cluster-key
4 | machineSets:
5 | - name: normal
6 | replicas: 1
7 | spec:
8 | image: brightzheng100/vind-ubuntu-root:22.04
9 | name: node%d
10 | networks:
11 | - my-network
12 | portMappings:
13 | - containerPort: 22
14 | volumes:
15 | - type: bind
16 | source: /
17 | destination: /host
18 | - name: root
19 | replicas: 1
20 | spec:
21 | image: brightzheng100/vind-ubuntu:22.04
22 | name: node%d
23 | networks:
24 | - my-network
25 | portMappings:
26 | - containerPort: 22
27 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/brightzheng100/vind
2 |
3 | require (
4 | github.com/docker/docker v1.13.1
5 | github.com/ghodss/yaml v1.0.0
6 | github.com/google/go-github/v24 v24.0.1
7 | github.com/mitchellh/go-homedir v1.1.0
8 | github.com/pkg/errors v0.8.1
9 | github.com/sirupsen/logrus v1.3.0
10 | github.com/spf13/cobra v1.8.1
11 | github.com/stretchr/testify v1.2.2
12 | gopkg.in/yaml.v2 v2.2.2
13 | )
14 |
15 | require (
16 | github.com/davecgh/go-spew v1.1.1 // indirect
17 | github.com/docker/go-connections v0.4.0 // indirect
18 | github.com/docker/go-units v0.3.3 // indirect
19 | github.com/google/go-querystring v1.0.0 // indirect
20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
21 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
22 | github.com/pmezard/go-difflib v1.0.0 // indirect
23 | github.com/spf13/pflag v1.0.5 // indirect
24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
25 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
26 | )
27 |
28 | go 1.23
29 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
5 | github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
6 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
7 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
8 | github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
9 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
10 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
11 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
13 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
14 | github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju57UR4Q4=
15 | github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M=
16 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
17 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
18 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
19 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
20 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
21 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
22 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
23 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
24 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
25 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
28 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
29 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
30 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
31 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
32 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
33 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
34 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
35 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
36 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
37 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
38 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
39 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
42 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
43 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
44 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
45 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
47 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
48 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
52 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
53 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
55 |
--------------------------------------------------------------------------------
/images/README.md:
--------------------------------------------------------------------------------
1 | ## Images
2 |
3 | Here are some example images that I've built, which are out-of-the-box and available in my Docker Hub namespace.
4 |
5 | ### Prepare
6 |
7 | Assume you have installed Docker, like `brew install docker` in Mac OS.
8 |
9 | Now let's create and use a builder:
10 |
11 | ```sh
12 | docker buildx create --use
13 | ```
14 |
15 | ### Build
16 |
17 | This is how I built the images:
18 |
19 | ```sh
20 | ./build.sh
21 | ```
22 |
23 | If you want to host the images in your namespace, do this:
24 |
25 | ```sh
26 | export REPO_NAMESPACE=
27 | ./build.sh
28 | ```
29 |
30 | Or, you can build you own always by using `docker buildx build`.
31 | Refer to the [`build.sh](./build.sh) for how.
32 |
33 |
34 | Please note that when using `podman` to build, the commands are actually quite different.
35 |
36 | Let's take building `brightzheng100/vind-ubuntu:22.04` for example:
37 |
38 | ```sh
39 | # 1. Build them with a manifest specified
40 | podman build --platform linux/amd64,linux/arm64 --file Dockerfile.22.04.non-root --manifest brightzheng100/vind-ubuntu:ubuntu-manifest .
41 | # 2. Push manifest with the targeted image tag
42 | podman manifest push brightzheng100/vind-ubuntu:ubuntu-manifest brightzheng100/vind-ubuntu:22.04
43 | ```
44 |
45 | ### List
46 |
47 | #### Ubuntu
48 |
49 | - brightzheng100/vind-ubuntu:`version` -- with a "normal" `ubuntu` user builtin -- where the `version` can be:
50 | - 25.04
51 | - 24.10
52 | - 24.04
53 | - 22.04
54 | - 20.04
55 | - 18.04
56 | - brightzheng100/vind-ubuntu-root:`version`, where the `version` can be:
57 | - 25.04
58 | - 24.10
59 | - 24.04
60 | - 22.04
61 | - 20.04
62 | - 18.04
63 |
64 | #### Fedora
65 |
66 | - brightzheng100/vind-fedora:`version`, where the `version` can be:
67 | - 42
68 | - 41
69 | - 40
70 |
71 | #### Debian
72 |
73 | - brightzheng100/vind-debian:`version`, where the `version` can be:
74 | - bookworm
75 | - bullseye
76 | - buster
77 |
78 | #### CentOS
79 |
80 | - brightzheng100/vind-centos:`version`, where the `version` can be:
81 | - 8
82 | - 7
83 |
84 | #### Amazon Linux
85 |
86 | - brightzheng100/vind-amazonlinux:`version`, where the `version` can be:
87 | - 2
88 |
--------------------------------------------------------------------------------
/images/amazonlinux/Dockerfile.2:
--------------------------------------------------------------------------------
1 | FROM amazonlinux:2
2 |
3 | ENV container=docker
4 |
5 | RUN yum -y install sudo systemd hostname procps-ng net-tools iproute iputils wget && yum clean all
6 |
7 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
8 | systemd-tmpfiles-setup.service ] || rm -f $i; done); \
9 | rm -f /lib/systemd/system/multi-user.target.wants/*;\
10 | rm -f /etc/systemd/system/*.wants/*;\
11 | rm -f /lib/systemd/system/local-fs.target.wants/*; \
12 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
13 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
14 | rm -f /lib/systemd/system/basic.target.wants/*;\
15 | rm -f /lib/systemd/system/anaconda.target.wants/*;\
16 | rm -f /lib/systemd/system/*.wants/*update-utmp*;
17 |
18 | RUN yum -y install openssh-server && yum clean all
19 |
20 | EXPOSE 22
21 |
22 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
23 | STOPSIGNAL SIGRTMIN+3
24 |
25 | CMD ["/bin/bash"]
26 |
--------------------------------------------------------------------------------
/images/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | repo_namespace=${REPO_NAMESPACE:-brightzheng100}
4 | image_action=${IMAGE_ACTION:-push} # push or load
5 |
6 |
7 | # == Ubuntu ==
8 |
9 | for distro in "25.04" "24.10" "24.04" "22.04" "20.04" "18.04"; do
10 |
11 | pushd ubuntu
12 |
13 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.${distro}.non-root --${image_action} -t ${repo_namespace}/vind-ubuntu:${distro} .
14 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.${distro}.root --${image_action} -t ${repo_namespace}/vind-ubuntu-root:${distro} .
15 |
16 | popd
17 |
18 | done
19 |
20 |
21 | # == Amazon Linux ==
22 |
23 | pushd amazonlinux
24 |
25 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.2 --${image_action} -t ${repo_namespace}/vind-amazonlinux:2 .
26 |
27 | popd
28 |
29 |
30 | # == CentOS ==
31 |
32 | pushd centos
33 |
34 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.7 --${image_action} -t ${repo_namespace}/vind-centos:7 .
35 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.8 --${image_action} -t ${repo_namespace}/vind-centos:8 .
36 |
37 | popd
38 |
39 |
40 | # == Debian ==
41 |
42 | pushd debian
43 |
44 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.buster --${image_action} -t ${repo_namespace}/vind-debian:buster .
45 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.bullseye --${image_action} -t ${repo_namespace}/vind-debian:bullseye .
46 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.bookworm --${image_action} -t ${repo_namespace}/vind-debian:bookworm .
47 |
48 | popd
49 |
50 |
51 | # == Fedora ==
52 |
53 | pushd fedora
54 |
55 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.40 --${image_action} -t ${repo_namespace}/vind-fedora:40 .
56 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.41 --${image_action} -t ${repo_namespace}/vind-fedora:41 .
57 | docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile.42 --${image_action} -t ${repo_namespace}/vind-fedora:42 .
58 |
59 | popd
60 |
--------------------------------------------------------------------------------
/images/centos/Dockerfile.7:
--------------------------------------------------------------------------------
1 | FROM centos:7
2 |
3 | ENV container=docker
4 |
5 | # Fix: Could not resolve host: mirrorlist.centos.org
6 | RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/CentOS-*.repo && \
7 | sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/CentOS-*.repo && \
8 | sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/CentOS-*.repo
9 |
10 | RUN yum -y install sudo procps-ng net-tools iproute iputils wget && yum clean all
11 |
12 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
13 | systemd-tmpfiles-setup.service ] || rm -f $i; done); \
14 | rm -f /lib/systemd/system/multi-user.target.wants/*;\
15 | rm -f /etc/systemd/system/*.wants/*;\
16 | rm -f /lib/systemd/system/local-fs.target.wants/*; \
17 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
18 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
19 | rm -f /lib/systemd/system/basic.target.wants/*;\
20 | rm -f /lib/systemd/system/anaconda.target.wants/*;\
21 | rm -f /lib/systemd/system/*.wants/*update-utmp*;
22 |
23 | RUN yum -y install openssh-server && yum clean all
24 |
25 | EXPOSE 22
26 |
27 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
28 | STOPSIGNAL SIGRTMIN+3
29 |
30 | CMD ["/bin/bash"]
31 |
--------------------------------------------------------------------------------
/images/centos/Dockerfile.8:
--------------------------------------------------------------------------------
1 | FROM centos:centos8
2 |
3 | ENV container=docker
4 |
5 | # Fix: Could not resolve host: mirrorlist.centos.org
6 | RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/CentOS-*.repo && \
7 | sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/CentOS-*.repo && \
8 | sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/CentOS-*.repo
9 |
10 | RUN yum -y install sudo procps-ng net-tools iproute iputils wget && yum clean all
11 |
12 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
13 | systemd-tmpfiles-setup.service ] || rm -f $i; done); \
14 | rm -f /lib/systemd/system/multi-user.target.wants/*;\
15 | rm -f /etc/systemd/system/*.wants/*;\
16 | rm -f /lib/systemd/system/local-fs.target.wants/*; \
17 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
18 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
19 | rm -f /lib/systemd/system/basic.target.wants/*;\
20 | rm -f /lib/systemd/system/anaconda.target.wants/*;\
21 | rm -f /lib/systemd/system/*.wants/*update-utmp*;
22 |
23 | RUN yum -y install openssh-server && yum clean all
24 |
25 | EXPOSE 22
26 |
27 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
28 | STOPSIGNAL SIGRTMIN+3
29 |
30 | CMD ["/bin/bash"]
31 |
--------------------------------------------------------------------------------
/images/debian/Dockerfile.bookworm:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | EXPOSE 22
21 |
22 | RUN systemctl set-default multi-user.target
23 | RUN systemctl mask \
24 | dev-hugepages.mount \
25 | sys-fs-fuse-connections.mount \
26 | systemd-update-utmp.service \
27 | systemd-tmpfiles-setup.service \
28 | console-getty.service
29 |
30 | # This container image doesn't have locales installed. Disable forwarding the
31 | # user locale env variables or we get warnings such as:
32 | # bash: warning: setlocale: LC_ALL: cannot change locale
33 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
34 |
35 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
36 | STOPSIGNAL SIGRTMIN+3
37 |
38 | CMD ["/bin/bash"]
39 |
--------------------------------------------------------------------------------
/images/debian/Dockerfile.bullseye:
--------------------------------------------------------------------------------
1 | FROM debian:bullseye
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | EXPOSE 22
21 |
22 | RUN systemctl set-default multi-user.target
23 | RUN systemctl mask \
24 | dev-hugepages.mount \
25 | sys-fs-fuse-connections.mount \
26 | systemd-update-utmp.service \
27 | systemd-tmpfiles-setup.service \
28 | console-getty.service
29 |
30 | # This container image doesn't have locales installed. Disable forwarding the
31 | # user locale env variables or we get warnings such as:
32 | # bash: warning: setlocale: LC_ALL: cannot change locale
33 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
34 |
35 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
36 | STOPSIGNAL SIGRTMIN+3
37 |
38 | CMD ["/bin/bash"]
39 |
--------------------------------------------------------------------------------
/images/debian/Dockerfile.buster:
--------------------------------------------------------------------------------
1 | FROM debian:buster
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | EXPOSE 22
21 |
22 | RUN systemctl set-default multi-user.target
23 | RUN systemctl mask \
24 | dev-hugepages.mount \
25 | sys-fs-fuse-connections.mount \
26 | systemd-update-utmp.service \
27 | systemd-tmpfiles-setup.service \
28 | console-getty.service
29 |
30 | # This container image doesn't have locales installed. Disable forwarding the
31 | # user locale env variables or we get warnings such as:
32 | # bash: warning: setlocale: LC_ALL: cannot change locale
33 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
34 |
35 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
36 | STOPSIGNAL SIGRTMIN+3
37 |
38 | CMD ["/bin/bash"]
39 |
--------------------------------------------------------------------------------
/images/fedora/Dockerfile.40:
--------------------------------------------------------------------------------
1 | FROM fedora:41
2 |
3 | ENV container=docker
4 |
5 | RUN dnf -y install sudo openssh-server procps-ng hostname net-tools iproute iputils wget && dnf clean all
6 |
7 | EXPOSE 22
8 |
9 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
10 | STOPSIGNAL SIGRTMIN+3
11 |
12 | CMD ["/bin/bash"]
13 |
--------------------------------------------------------------------------------
/images/fedora/Dockerfile.41:
--------------------------------------------------------------------------------
1 | FROM fedora:41
2 |
3 | ENV container=docker
4 |
5 | RUN dnf -y install sudo openssh-server procps-ng hostname net-tools iproute iputils wget && dnf clean all
6 |
7 | EXPOSE 22
8 |
9 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
10 | STOPSIGNAL SIGRTMIN+3
11 |
12 | CMD ["/bin/bash"]
13 |
--------------------------------------------------------------------------------
/images/fedora/Dockerfile.42:
--------------------------------------------------------------------------------
1 | FROM fedora:42
2 |
3 | ENV container=docker
4 |
5 | RUN dnf -y install sudo openssh-server procps-ng hostname net-tools iproute iputils wget && dnf clean all
6 |
7 | EXPOSE 22
8 |
9 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
10 | STOPSIGNAL SIGRTMIN+3
11 |
12 | CMD ["/bin/bash"]
13 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.18.04.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm \{} \;
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Add a dedicated non-root user named "ubuntu" with sudoer permission
24 | RUN useradd -ms /bin/bash ubuntu && \
25 | echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
26 |
27 | EXPOSE 22
28 |
29 | RUN systemctl set-default multi-user.target
30 | RUN systemctl mask \
31 | dev-hugepages.mount \
32 | sys-fs-fuse-connections.mount \
33 | systemd-update-utmp.service \
34 | systemd-tmpfiles-setup.service \
35 | console-getty.service
36 | RUN systemctl disable \
37 | networkd-dispatcher.service
38 |
39 | # This container image doesn't have locales installed. Disable forwarding the
40 | # user locale env variables or we get warnings such as:
41 | # bash: warning: setlocale: LC_ALL: cannot change locale
42 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
43 |
44 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
45 | STOPSIGNAL SIGRTMIN+3
46 |
47 | CMD ["/bin/bash"]
48 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.18.04.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm \{} \;
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.20.04.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:20.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm \{} \;
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Add a dedicated non-root user named "ubuntu" with sudoer permission
24 | RUN useradd -ms /bin/bash ubuntu && \
25 | echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
26 |
27 | EXPOSE 22
28 |
29 | RUN systemctl set-default multi-user.target
30 | RUN systemctl mask \
31 | dev-hugepages.mount \
32 | sys-fs-fuse-connections.mount \
33 | systemd-update-utmp.service \
34 | systemd-tmpfiles-setup.service \
35 | console-getty.service
36 | RUN systemctl disable \
37 | networkd-dispatcher.service
38 |
39 | # This container image doesn't have locales installed. Disable forwarding the
40 | # user locale env variables or we get warnings such as:
41 | # bash: warning: setlocale: LC_ALL: cannot change locale
42 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
43 |
44 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
45 | STOPSIGNAL SIGRTMIN+3
46 |
47 | CMD ["/bin/bash"]
48 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.20.04.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:20.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm \{} \;
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.22.04.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Add a dedicated non-root user named "ubuntu" with sudoer permission
24 | RUN useradd -ms /bin/bash ubuntu && \
25 | echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
26 |
27 | EXPOSE 22
28 |
29 | RUN systemctl set-default multi-user.target
30 | RUN systemctl mask \
31 | dev-hugepages.mount \
32 | sys-fs-fuse-connections.mount \
33 | systemd-update-utmp.service \
34 | systemd-tmpfiles-setup.service \
35 | console-getty.service
36 | RUN systemctl disable \
37 | networkd-dispatcher.service
38 |
39 | # This container image doesn't have locales installed. Disable forwarding the
40 | # user locale env variables or we get warnings such as:
41 | # bash: warning: setlocale: LC_ALL: cannot change locale
42 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
43 |
44 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
45 | STOPSIGNAL SIGRTMIN+3
46 |
47 | CMD ["/bin/bash"]
48 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.22.04.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.24.04.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Make sure user "ubuntu" has sudoer permission
24 | RUN echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
25 |
26 | EXPOSE 22
27 |
28 | RUN systemctl set-default multi-user.target
29 | RUN systemctl mask \
30 | dev-hugepages.mount \
31 | sys-fs-fuse-connections.mount \
32 | systemd-update-utmp.service \
33 | systemd-tmpfiles-setup.service \
34 | console-getty.service
35 | RUN systemctl disable \
36 | networkd-dispatcher.service
37 |
38 | # This container image doesn't have locales installed. Disable forwarding the
39 | # user locale env variables or we get warnings such as:
40 | # bash: warning: setlocale: LC_ALL: cannot change locale
41 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
42 |
43 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
44 | STOPSIGNAL SIGRTMIN+3
45 |
46 | CMD ["/bin/bash"]
47 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.24.04.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.24.10.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.10
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Make sure user "ubuntu" has sudoer permission
24 | RUN echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
25 |
26 | EXPOSE 22
27 |
28 | RUN systemctl set-default multi-user.target
29 | RUN systemctl mask \
30 | dev-hugepages.mount \
31 | sys-fs-fuse-connections.mount \
32 | systemd-update-utmp.service \
33 | systemd-tmpfiles-setup.service \
34 | console-getty.service
35 | RUN systemctl disable \
36 | networkd-dispatcher.service
37 |
38 | # This container image doesn't have locales installed. Disable forwarding the
39 | # user locale env variables or we get warnings such as:
40 | # bash: warning: setlocale: LC_ALL: cannot change locale
41 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
42 |
43 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
44 | STOPSIGNAL SIGRTMIN+3
45 |
46 | CMD ["/bin/bash"]
47 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.24.10.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.10
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.25.04.non-root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:25.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | # Make sure user "ubuntu" has sudoer permission
24 | RUN echo "ubuntu ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/ubuntu
25 |
26 | EXPOSE 22
27 |
28 | RUN systemctl set-default multi-user.target
29 | RUN systemctl mask \
30 | dev-hugepages.mount \
31 | sys-fs-fuse-connections.mount \
32 | systemd-update-utmp.service \
33 | systemd-tmpfiles-setup.service \
34 | console-getty.service
35 | RUN systemctl disable \
36 | networkd-dispatcher.service
37 |
38 | # This container image doesn't have locales installed. Disable forwarding the
39 | # user locale env variables or we get warnings such as:
40 | # bash: warning: setlocale: LC_ALL: cannot change locale
41 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
42 |
43 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
44 | STOPSIGNAL SIGRTMIN+3
45 |
46 | CMD ["/bin/bash"]
47 |
--------------------------------------------------------------------------------
/images/ubuntu/Dockerfile.25.04.root:
--------------------------------------------------------------------------------
1 | FROM ubuntu:25.04
2 |
3 | ENV container=docker
4 |
5 | # Don't start any optional services except for the few we need.
6 | RUN find /etc/systemd/system \
7 | /lib/systemd/system \
8 | -path '*.wants/*' \
9 | -not -name '*journald*' \
10 | -not -name '*systemd-tmpfiles*' \
11 | -not -name '*systemd-user-sessions*' \
12 | -exec rm -f {} +
13 |
14 | RUN apt-get update && \
15 | apt-get install -y \
16 | dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \
17 | apt-get clean && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | RUN >/etc/machine-id
21 | RUN >/var/lib/dbus/machine-id
22 |
23 | EXPOSE 22
24 |
25 | RUN systemctl set-default multi-user.target
26 | RUN systemctl mask \
27 | dev-hugepages.mount \
28 | sys-fs-fuse-connections.mount \
29 | systemd-update-utmp.service \
30 | systemd-tmpfiles-setup.service \
31 | console-getty.service
32 | RUN systemctl disable \
33 | networkd-dispatcher.service
34 |
35 | # This container image doesn't have locales installed. Disable forwarding the
36 | # user locale env variables or we get warnings such as:
37 | # bash: warning: setlocale: LC_ALL: cannot change locale
38 | RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
39 |
40 | # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
41 | STOPSIGNAL SIGRTMIN+3
42 |
43 | CMD ["/bin/bash"]
44 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 Bright Zheng
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package main
17 |
18 | import (
19 | "github.com/brightzheng100/vind/cmd"
20 | )
21 |
22 | func main() {
23 | cmd.Execute()
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/cluster/cluster.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "fmt"
21 | "io"
22 | "os"
23 | "regexp"
24 | "slices"
25 | "strconv"
26 | "strings"
27 | "time"
28 |
29 | "github.com/brightzheng100/vind/pkg/config"
30 | "github.com/brightzheng100/vind/pkg/docker"
31 | "github.com/brightzheng100/vind/pkg/exec"
32 | "github.com/brightzheng100/vind/pkg/utils"
33 | "github.com/docker/docker/api/types"
34 | "github.com/ghodss/yaml"
35 | "github.com/mitchellh/go-homedir"
36 | "github.com/pkg/errors"
37 | )
38 |
39 | // cluster is a running cluster.
40 | type cluster struct {
41 | config config.Config
42 | keyStore *KeyStore
43 | }
44 |
45 | // Container represents a running machine.
46 | type Container struct {
47 | ID string
48 | }
49 |
50 | // New creates a new cluster. It takes as input the description of the cluster
51 | // and its machines.
52 | func New(conf config.Config) (*cluster, error) {
53 | if err := conf.Validate(); err != nil {
54 | return nil, err
55 | }
56 | return &cluster{
57 | config: conf,
58 | }, nil
59 | }
60 |
61 | // NewFromYAML creates a new Cluster from a YAML serialization of its
62 | // configuration available in the provided string.
63 | func NewFromYAML(data []byte) (*cluster, error) {
64 | config := config.Config{}
65 | if err := yaml.Unmarshal(data, &config); err != nil {
66 | return nil, err
67 | }
68 | return New(config)
69 | }
70 |
71 | // NewFromFile creates a new Cluster from a YAML serialization of its
72 | // configuration available in the provided file.
73 | func NewFromFile(path string) (*cluster, error) {
74 | data, err := os.ReadFile(path)
75 | if err != nil {
76 | return nil, err
77 | }
78 | return NewFromYAML(data)
79 | }
80 |
81 | // forEachMachine loops through every Machine for doing something
82 | func (c *cluster) forEachMachine(do func(*Machine) error) error {
83 | for _, machineSet := range c.config.MachineSets {
84 | for i := 0; i < machineSet.Replicas; i++ {
85 | machine := newMachine(&c.config.Cluster, &machineSet, &machineSet.Spec, i)
86 | if err := do(machine); err != nil {
87 | return err
88 | }
89 | }
90 | }
91 | return nil
92 | }
93 |
94 | // forEachMachine loops through all Machine and locates only specific ones for doing something
95 | func (c *cluster) forSpecificMachines(do func(*Machine) error, machineNames []string) error {
96 | // machineToStart map is used to track machines to make actions and non existing machines
97 | machineToHandle := make(map[string]bool)
98 | for _, machine := range machineNames {
99 | machineToHandle[machine] = false
100 | }
101 | for _, machineSet := range c.config.MachineSets {
102 | for i := 0; i < machineSet.Replicas; i++ {
103 | machine := newMachine(&c.config.Cluster, &machineSet, &machineSet.Spec, i)
104 | if _, ok := machineToHandle[machine.machineName]; ok {
105 | if err := do(machine); err != nil {
106 | return err
107 | }
108 | machineToHandle[machine.machineName] = true
109 | }
110 | }
111 | }
112 | // log warning for non existing machines
113 | for key, value := range machineToHandle {
114 | if !value {
115 | utils.Logger.Warnf("machine %v does not exist", key)
116 | }
117 | }
118 | return nil
119 | }
120 |
121 | // Create creates the cluster.
122 | func (c *cluster) Create() error {
123 | // make sure the SSH key pair exists
124 | if err := c.ensureSSHKey(); err != nil {
125 | return err
126 | }
127 |
128 | // make sure Docker is running
129 | if err := docker.IsRunning(); err != nil {
130 | return err
131 | }
132 |
133 | // pull the images if not exist
134 | for _, template := range c.config.MachineSets {
135 | if _, err := docker.PullIfNotPresent(template.Spec.Image, 2); err != nil {
136 | return err
137 | }
138 | }
139 |
140 | // create all machines
141 | return c.forEachMachine(func(m *Machine) error {
142 | pk, err := c.publicKey(m.spec)
143 | if err != nil {
144 | return errors.Wrap(err, "can't retrieve public key")
145 | }
146 | return m.Create(&c.config.Cluster, pk)
147 | })
148 | }
149 |
150 | // ensureSSHKey generates SSK key pair when needed
151 | func (c *cluster) ensureSSHKey() error {
152 | if c.config.Cluster.PrivateKey == "" {
153 | return nil
154 | }
155 | path, _ := homedir.Expand(c.config.Cluster.PrivateKey)
156 | if _, err := os.Stat(path); err == nil {
157 | return nil
158 | }
159 |
160 | utils.Logger.Infof("Creating SSH key: %s ...", path)
161 | return run(
162 | "ssh-keygen", "-q",
163 | "-t", "rsa",
164 | "-b", "4096",
165 | "-C", f("%s@vind.mail", c.Name()),
166 | "-f", path,
167 | "-N", "",
168 | )
169 | }
170 |
171 | // publicKey retrieves the public key content from machine or clus
172 | func (c *cluster) publicKey(machine *config.Machine) ([]byte, error) {
173 | // Prefer the machine public key over the cluster-wide key.
174 | if machine.PublicKey != "" && c.keyStore != nil {
175 | data, err := c.keyStore.Get(machine.PublicKey)
176 | if err != nil {
177 | return nil, err
178 | }
179 | data = append(data, byte('\n'))
180 | return data, err
181 | }
182 |
183 | // Cluster global key
184 | if c.config.Cluster.PrivateKey == "" {
185 | return nil, errors.New("no SSH key provided")
186 | }
187 |
188 | path, err := homedir.Expand(c.config.Cluster.PrivateKey)
189 | if err != nil {
190 | return nil, errors.Wrap(err, "public key expand")
191 | }
192 | return os.ReadFile(path + ".pub")
193 | }
194 |
195 | func f(format string, args ...interface{}) string {
196 | return fmt.Sprintf(format, args...)
197 | }
198 |
199 | // SetKeyStore provides a store where to persist public keys for this Cluster.
200 | func (c *cluster) SetKeyStore(keyStore *KeyStore) *cluster {
201 | c.keyStore = keyStore
202 | return c
203 | }
204 |
205 | // Name returns the cluster name.
206 | func (c *cluster) Name() string {
207 | return c.config.Cluster.Name
208 | }
209 |
210 | // Save writes the Cluster configure to a file.
211 | func (c *cluster) Save(path string) error {
212 | data, err := yaml.Marshal(c.config)
213 | if err != nil {
214 | return err
215 | }
216 | return os.WriteFile(path, data, 0666)
217 | }
218 |
219 | // Delete deletes the cluster.
220 | func (c *cluster) Delete() error {
221 | if err := docker.IsRunning(); err != nil {
222 | return err
223 | }
224 |
225 | return c.forEachMachine(func(m *Machine) error {
226 | return m.Delete()
227 | })
228 | }
229 |
230 | // Show will generate information about cluster's running or stopped machines.
231 | func (c *cluster) Show(machineNames []string) (machines []*Machine, err error) {
232 | if err = docker.IsRunning(); err != nil {
233 | return nil, err
234 | }
235 |
236 | // walk through the machineSets
237 | for _, machineSet := range c.config.MachineSets {
238 | // walk through the specific machine set
239 | for i := 0; i < machineSet.Replicas; i++ {
240 | m := newMachine(&c.config.Cluster, &machineSet, &machineSet.Spec, i)
241 |
242 | // Proceed only if no machine names specified or the machine name is included
243 | if len(machineNames) == 0 || slices.Contains(machineNames, m.machineName) {
244 | if !m.IsCreated() {
245 | utils.Logger.Warnf("machine not created: %s", m.machineName)
246 | continue
247 | }
248 |
249 | var inspect types.ContainerJSON
250 | if err := docker.InspectObject(m.containerName, ".", &inspect); err != nil {
251 | return machines, err
252 | }
253 |
254 | // Handle Ports
255 | ports := make([]config.PortMapping, 0)
256 | for k, v := range inspect.NetworkSettings.Ports {
257 | if len(v) < 1 {
258 | continue
259 | }
260 | p := config.PortMapping{}
261 | hostPort, _ := strconv.Atoi(v[0].HostPort)
262 | p.HostPort = uint16(hostPort)
263 | p.ContainerPort = uint16(k.Int())
264 | p.Address = v[0].HostIP
265 | ports = append(ports, p)
266 | }
267 | m.spec.PortMappings = ports
268 |
269 | // Handle Volumes
270 | var volumes []config.Volume
271 | for _, mount := range inspect.Mounts {
272 | v := config.Volume{
273 | Type: string(mount.Type),
274 | Source: mount.Source,
275 | Destination: mount.Destination,
276 | ReadOnly: mount.RW,
277 | }
278 | volumes = append(volumes, v)
279 | }
280 | m.spec.Volumes = volumes
281 |
282 | // Handle network
283 | m.runtimeNetworks = NewRuntimeNetworks(inspect.NetworkSettings.Networks)
284 |
285 | m.spec.Cmd = strings.Join(inspect.Config.Cmd, ",")
286 |
287 | machines = append(machines, m)
288 | }
289 | }
290 | }
291 | return
292 | }
293 |
294 | // Start starts all or specific machines in cluster.
295 | func (c *cluster) Start(machineNames []string) error {
296 | if err := docker.IsRunning(); err != nil {
297 | return err
298 | }
299 |
300 | startMachineFun := func(m *Machine) error {
301 | return m.Start()
302 | }
303 |
304 | // start all if no specific machines are specified
305 | if len(machineNames) < 1 {
306 | return c.forEachMachine(startMachineFun)
307 | }
308 |
309 | // Otherwise, start the specific machines only
310 | return c.forSpecificMachines(startMachineFun, machineNames)
311 | }
312 |
313 | // Stop stops all or specific machines in cluster.
314 | func (c *cluster) Stop(machineNames []string) error {
315 | if err := docker.IsRunning(); err != nil {
316 | return err
317 | }
318 |
319 | stopMachineFun := func(m *Machine) error {
320 | return m.Stop()
321 | }
322 |
323 | // stop all if no specific machines are specified
324 | if len(machineNames) < 1 {
325 | return c.forEachMachine(stopMachineFun)
326 | }
327 |
328 | // Otherwise, stop the specific machines only
329 | return c.forSpecificMachines(stopMachineFun, machineNames)
330 | }
331 |
332 | // io.Writer filter that writes that it receives to writer. Keeps track if it
333 | // has seen a write matching regexp.
334 | type matchFilter struct {
335 | writer io.Writer
336 | writeMatched bool // whether the filter should write the matched value or not.
337 |
338 | regexp *regexp.Regexp
339 | matched bool
340 | }
341 |
342 | func (f *matchFilter) Write(p []byte) (n int, err error) {
343 | // Assume the relevant log line is flushed in one write.
344 | if match := f.regexp.Match(p); match {
345 | f.matched = true
346 | if !f.writeMatched {
347 | return len(p), nil
348 | }
349 | }
350 | return f.writer.Write(p)
351 | }
352 |
353 | // Matches:
354 | //
355 | // ssh_exchange_identification: read: Connection reset by peer
356 | var connectRefused = regexp.MustCompile("^ssh_exchange_identification: ")
357 |
358 | // Matches:
359 | //
360 | // Warning:Permanently added '172.17.0.2' (ECDSA) to the list of known hosts
361 | var knownHosts = regexp.MustCompile("^Warning: Permanently added .* to the list of known hosts.")
362 |
363 | // ssh returns true if the command should be tried again.
364 | func ssh(args []string) (bool, error) {
365 | utils.Logger.Debug("ssh", args)
366 | cmd := exec.Command("ssh", args...)
367 |
368 | refusedFilter := &matchFilter{
369 | writer: os.Stderr,
370 | writeMatched: false,
371 | regexp: connectRefused,
372 | }
373 |
374 | errFilter := &matchFilter{
375 | writer: refusedFilter,
376 | writeMatched: false,
377 | regexp: knownHosts,
378 | }
379 |
380 | cmd.SetStdin(os.Stdin)
381 | cmd.SetStdout(os.Stdout)
382 | cmd.SetStderr(errFilter)
383 |
384 | err := cmd.Run()
385 | if err != nil && refusedFilter.matched {
386 | return true, err
387 | }
388 | return false, err
389 | }
390 |
391 | func (c *cluster) GetMachineByMachineName(machineName string) (*Machine, error) {
392 | for _, machineSet := range c.config.MachineSets {
393 | for i := 0; i < machineSet.Replicas; i++ {
394 | if machineName == f("%s-"+machineSet.Spec.Name, machineSet.Name, i) {
395 | return newMachine(&c.config.Cluster, &machineSet, &machineSet.Spec, i), nil
396 | }
397 | }
398 | }
399 | return nil, fmt.Errorf("Machine name not found: %s", machineName)
400 | }
401 |
402 | func (c *cluster) GetFirstMachine() (*Machine, error) {
403 | if len(c.config.MachineSets) == 0 {
404 | return nil, errors.New("no machineSet is configured")
405 | } else {
406 | machineSet := c.config.MachineSets[0]
407 | return newMachine(&c.config.Cluster, &machineSet, &machineSet.Spec, 0), nil
408 | }
409 | }
410 |
411 | func mappingFromPort(spec *config.Machine, containerPort int) (*config.PortMapping, error) {
412 | for i := range spec.PortMappings {
413 | if int(spec.PortMappings[i].ContainerPort) == containerPort {
414 | return &spec.PortMappings[i], nil
415 | }
416 | }
417 | return nil, fmt.Errorf("unknown containerPort %d", containerPort)
418 | }
419 |
420 | // SSH logs into the named machine with SSH.
421 | func (c *cluster) SSH(machine *Machine, username string, extraSshArgs string) error {
422 | utils.Logger.Infof("SSH into machine [%s] with user [%s]", machine.machineName, username)
423 |
424 | hostPort, err := machine.HostPort(22)
425 | if err != nil {
426 | return err
427 | }
428 | mapping, err := mappingFromPort(machine.spec, 22)
429 | if err != nil {
430 | return err
431 | }
432 | remote := "localhost"
433 | if mapping.Address != "" {
434 | remote = mapping.Address
435 | }
436 | path, _ := homedir.Expand(c.config.Cluster.PrivateKey)
437 | args := []string{
438 | "-o", "UserKnownHostsFile=/dev/null",
439 | "-o", "StrictHostKeyChecking=no",
440 | "-o", "IdentitiesOnly=yes",
441 | "-i", path,
442 | "-p", f("%d", hostPort),
443 | "-l", username,
444 | "-t", remote, // https://stackoverflow.com/questions/626533/how-can-i-ssh-directly-to-a-particular-directory
445 | }
446 |
447 | if len(extraSshArgs) > 0 {
448 | // if there are any extra SSH args, let's respect them
449 | utils.Logger.Infof("With extra SSH args: %s", extraSshArgs)
450 | args = append(args, extraSshArgs)
451 | } else {
452 | // try to auto cd into currently mapped folder
453 | // if bind mount to "/host" exists
454 | cd := machine.AutoCdTo()
455 | if cd != "" {
456 | utils.Logger.Infof("Trying to cd into: %s", cd)
457 | args = append(args, fmt.Sprintf("cd %s; exec $SHELL -l", cd))
458 | }
459 | }
460 |
461 | // If we ssh in a bit too quickly after the container creation, ssh errors out
462 | // with:
463 | // ssh_exchange_identification: read: Connection reset by peer
464 | // Let's loop a few times if we receive this message.
465 | retries := 25
466 | var retry bool
467 | for retries > 0 {
468 | retry, err = ssh(args)
469 | if !retry {
470 | break
471 | }
472 | retries--
473 | time.Sleep(200 * time.Millisecond)
474 | }
475 |
476 | return err
477 | }
478 |
479 | // CopyFrom copies files/folders from the machine to the host filesystem
480 | func (c *cluster) CopyFrom(from *Machine, srcPath, destPath string) error {
481 | // CopyTo(hostPath, containerNameOrID, destPath string) error
482 | return docker.CopyFrom(from.containerName, srcPath, destPath)
483 | }
484 |
485 | // CopyTo copies files/folders from the host filesystem to the machine
486 | func (c *cluster) CopyTo(srcPath string, to *Machine, destPath string) error {
487 | return docker.CopyTo(srcPath, to.containerName, destPath)
488 | }
489 |
--------------------------------------------------------------------------------
/pkg/cluster/cluster_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "io/ioutil"
21 | "testing"
22 |
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | func TestMatchFilter(t *testing.T) {
27 | const refused = "ssh: connect to host 172.17.0.2 port 22: Connection refused"
28 |
29 | filter := matchFilter{
30 | writer: ioutil.Discard,
31 | regexp: connectRefused,
32 | }
33 |
34 | _, err := filter.Write([]byte("foo\n"))
35 | assert.NoError(t, err)
36 | assert.Equal(t, false, filter.matched)
37 |
38 | _, err = filter.Write([]byte(refused))
39 | assert.NoError(t, err)
40 | assert.Equal(t, false, filter.matched)
41 | }
42 |
43 | func TestNewClusterWithHostPort(t *testing.T) {
44 | cluster, err := NewFromYAML([]byte(`
45 | cluster:
46 | name: cluster
47 | privateKey: cluster-key
48 | machineSets:
49 | - name: centos
50 | replicas: 2
51 | spec:
52 | image: quay.io/brightzheng100/centos7
53 | name: node%d
54 | portMappings:
55 | - containerPort: 22
56 | hostPort: 2222
57 | `))
58 | assert.NoError(t, err)
59 | assert.NotNil(t, cluster)
60 | assert.Equal(t, 1, len(cluster.config.MachineSets))
61 | template := cluster.config.MachineSets[0]
62 | assert.Equal(t, "centos", template.Name)
63 | assert.Equal(t, 2, template.Replicas)
64 | assert.Equal(t, 1, len(template.Spec.PortMappings))
65 | portMapping := template.Spec.PortMappings[0]
66 | assert.Equal(t, uint16(22), portMapping.ContainerPort)
67 | assert.Equal(t, uint16(2222), portMapping.HostPort)
68 |
69 | machine0 := newMachine(&cluster.config.Cluster, &cluster.config.MachineSets[0], &template.Spec, 0)
70 | args0 := machine0.generateContainerRunArgs(cluster.Name())
71 | i := indexOf("-p", args0)
72 | assert.NotEqual(t, -1, i)
73 | assert.Equal(t, "2222:22", args0[i+1])
74 |
75 | machine1 := newMachine(&cluster.config.Cluster, &cluster.config.MachineSets[0], &template.Spec, 1)
76 | args1 := machine1.generateContainerRunArgs(cluster.Name())
77 | i = indexOf("-p", args1)
78 | assert.NotEqual(t, -1, i)
79 | assert.Equal(t, "2223:22", args1[i+1])
80 | }
81 |
82 | func indexOf(element string, array []string) int {
83 | for k, v := range array {
84 | if element == v {
85 | return k
86 | }
87 | }
88 | return -1 // element not found.
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/cluster/key_store.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "os"
21 | "path/filepath"
22 |
23 | "github.com/pkg/errors"
24 | )
25 |
26 | // KeyStore is a store for public keys.
27 | type KeyStore struct {
28 | basePath string
29 | }
30 |
31 | // NewKeyStore creates a new KeyStore
32 | func NewKeyStore(basePath string) *KeyStore {
33 | return &KeyStore{
34 | basePath: basePath,
35 | }
36 | }
37 |
38 | // Init initializes the key store, creating the store directory if needed.
39 | func (s *KeyStore) Init() error {
40 | return os.MkdirAll(s.basePath, 0760)
41 | }
42 |
43 | func fileExists(path string) bool {
44 | // XXX: There's a subtle bug: if stat fails for another reason that the file
45 | // not existing, we return the file exists.
46 | _, err := os.Stat(path)
47 | return !os.IsNotExist(err)
48 | }
49 |
50 | func (s *KeyStore) keyPath(name string) string {
51 | return filepath.Join(s.basePath, name)
52 | }
53 |
54 | func (s *KeyStore) keyExists(name string) bool {
55 | return fileExists(s.keyPath(name))
56 | }
57 |
58 | // Store adds the key to the store.
59 | func (s *KeyStore) Store(name, key string) error {
60 | if s.keyExists(name) {
61 | return errors.Errorf("key store: store: key '%s' already exists", name)
62 | }
63 |
64 | if err := os.WriteFile(s.keyPath(name), []byte(key), 0644); err != nil {
65 | return errors.Wrap(err, "key store: write")
66 | }
67 |
68 | return nil
69 | }
70 |
71 | // Get retrieves a key from the store.
72 | func (s *KeyStore) Get(name string) ([]byte, error) {
73 | if !s.keyExists(name) {
74 | return nil, errors.Errorf("key store: get: unknown key '%s'", name)
75 | }
76 | return os.ReadFile(s.keyPath(name))
77 | }
78 |
79 | // Remove removes a key from the store.
80 | func (s *KeyStore) Remove(name string) error {
81 | if !s.keyExists(name) {
82 | return errors.Errorf("key store: remove: unknown key '%s'", name)
83 | }
84 | if err := os.Remove(s.keyPath(name)); err != nil {
85 | return errors.Wrap(err, "key store: remove")
86 | }
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/cluster/machine.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "fmt"
21 | "os"
22 | "strconv"
23 | "strings"
24 |
25 | "github.com/brightzheng100/vind/pkg/config"
26 | "github.com/brightzheng100/vind/pkg/docker"
27 | "github.com/brightzheng100/vind/pkg/exec"
28 | "github.com/brightzheng100/vind/pkg/utils"
29 | "github.com/docker/docker/api/types/network"
30 | "github.com/pkg/errors"
31 | )
32 |
33 | const KEY_PATH_ROOT = "/root/.ssh/authorized_keys"
34 | const KEY_PATH_NORMAL = "/home/%s/.ssh/authorized_keys"
35 | const INIT_SCRIPT = `
36 | set -e
37 | rm -f /run/nologin
38 | u=%s
39 | if [[ "$u" == "root" ]]; then
40 | sshdir=/root/.ssh
41 | mkdir -p $sshdir; chmod 700 $sshdir
42 | touch $sshdir/authorized_keys; chmod 600 $sshdir/authorized_keys
43 | else
44 | sshdir=/home/$u/.ssh
45 | mkdir -p $sshdir; chmod 700 $sshdir
46 | touch $sshdir/authorized_keys; chmod 600 $sshdir/authorized_keys
47 | chown -R $u:$u /home/$u/
48 | fi
49 | `
50 |
51 | // defaultUser is the default container user.
52 | const defaultUser = "root"
53 |
54 | // Machine is a running machine instance.
55 | type Machine struct {
56 | spec *config.Machine
57 |
58 | // index in the machine set
59 | index int
60 |
61 | // containerName is the container name in underlying platform.
62 | // Naming pattern: {cluster name}-{machineSet name}-{machineName with index}
63 | containerName string
64 | // machineName is the machine's name which is also the host's name.
65 | // Naming pattern: {machineSet name}-{node name with index}
66 | machineName string
67 |
68 | // runtimeNetwork are networks in Docker runtime
69 | runtimeNetworks []*RuntimeNetwork
70 |
71 | ports map[int]int
72 | // maps containerPort -> hostPort.
73 | }
74 |
75 | // newMachine inits a new indexed Machine in the cluster.
76 | func newMachine(cluster *config.Cluster, machineSet *config.MachineSet, machine *config.Machine, i int) *Machine {
77 | return &Machine{
78 | index: i,
79 | spec: machine,
80 | containerName: f("%s-%s-"+machine.Name, cluster.Name, machineSet.Name, i),
81 | machineName: f("%s-"+machine.Name, machineSet.Name, i),
82 | }
83 | }
84 |
85 | // CreateMachine creates and starts a new machine in the cluster.
86 | func (m *Machine) Create(c *config.Cluster, publicKey []byte) error {
87 | // Start the container.
88 | utils.Logger.Infof("Creating machine: %s ...", m.containerName)
89 |
90 | if m.IsCreated() {
91 | utils.Logger.Infof("Machine %s is already created...", m.containerName)
92 | return nil
93 | }
94 |
95 | cmd := []string{"/sbin/init"}
96 | if strings.TrimSpace(m.spec.Cmd) != "" {
97 | cmd = strings.Split(strings.TrimSpace(m.spec.Cmd), " ")
98 | }
99 |
100 | // create the actual Docker container
101 | runArgs := m.generateContainerRunArgs(c.Name)
102 | _, err := docker.Create(m.spec.Image,
103 | runArgs,
104 | cmd,
105 | )
106 | if err != nil {
107 | return err
108 | }
109 |
110 | if len(m.spec.Networks) > 1 {
111 | for _, network := range m.spec.Networks[1:] {
112 | utils.Logger.Infof("Connecting %s to the %s network...", m.machineName, network)
113 |
114 | // if default "bridge" network is specified, connect to it
115 | if network == "bridge" {
116 | if err := docker.ConnectNetwork(m.containerName, network); err != nil {
117 | return err
118 | }
119 | } else {
120 | if err := docker.ConnectNetworkWithAlias(m.containerName, network, m.machineName); err != nil {
121 | return err
122 | }
123 | }
124 | }
125 | }
126 |
127 | // start up the container
128 | utils.Logger.Infof("Starting machine %s...", m.machineName)
129 | if err := docker.Start(m.containerName); err != nil {
130 | return err
131 | }
132 |
133 | // Initial provisioning.
134 | var keyPath = KEY_PATH_ROOT
135 | if m.User() != "root" {
136 | keyPath = f(KEY_PATH_NORMAL, m.User())
137 | }
138 | if err := containerRunShell(m.containerName, f(INIT_SCRIPT, m.User())); err != nil {
139 | return err
140 | }
141 | if err := copy(m.containerName, publicKey, keyPath); err != nil {
142 | return err
143 | }
144 |
145 | return nil
146 | }
147 |
148 | // generateContainerRunArgs generates the container creation args
149 | func (m *Machine) generateContainerRunArgs(cluster string) []string {
150 | runArgs := []string{
151 | "-it",
152 | "--label", "creator=vind",
153 | "--label", f("cluster=%s", cluster),
154 | "--label", f("index=%d", m.index),
155 | "--name", m.containerName,
156 | "--hostname", m.machineName,
157 | "--tmpfs", "/run",
158 | "--tmpfs", "/run/lock",
159 | "--tmpfs", "/tmp:exec,mode=777",
160 | //"-v", "/sys/fs/cgroup:/sys/fs/cgroup:ro",
161 | }
162 |
163 | for _, volume := range m.spec.Volumes {
164 | mount := f("type=%s", volume.Type)
165 | if volume.Source != "" {
166 | mount += f(",src=%s", volume.Source)
167 | }
168 | mount += f(",dst=%s", volume.Destination)
169 | if volume.ReadOnly {
170 | mount += ",readonly"
171 | }
172 | runArgs = append(runArgs, "--mount", mount)
173 | }
174 |
175 | for _, mapping := range m.spec.PortMappings {
176 | publish := ""
177 | if mapping.Address != "" {
178 | publish += f("%s:", mapping.Address)
179 | }
180 | // add up the index to avoid host port conflicts
181 | if mapping.HostPort != 0 {
182 | publish += f("%d:", int(mapping.HostPort)+m.index)
183 | }
184 | publish += f("%d", mapping.ContainerPort)
185 | if mapping.Protocol != "" {
186 | publish += f("/%s", mapping.Protocol)
187 | }
188 | runArgs = append(runArgs, "-p", publish)
189 | }
190 |
191 | if m.spec.Privileged {
192 | runArgs = append(runArgs, "--privileged")
193 | }
194 |
195 | if len(m.spec.Networks) > 0 {
196 | network := m.spec.Networks[0]
197 | utils.Logger.Infof("Connecting %s to the %s network...", m.machineName, network)
198 | runArgs = append(runArgs, "--network", m.spec.Networks[0])
199 | if network != "bridge" {
200 | runArgs = append(runArgs, "--network-alias", m.machineName)
201 | }
202 | }
203 |
204 | return runArgs
205 | }
206 |
207 | // Delete deletes a Machine from the cluster.
208 | func (m *Machine) Delete() error {
209 | if !m.IsCreated() {
210 | utils.Logger.Infof("Machine %s hasn't been created", m.machineName)
211 | return nil
212 | }
213 |
214 | if m.IsStarted() {
215 | utils.Logger.Infof("Machine %s is started, stopping and deleting machine...", m.machineName)
216 | err := docker.Kill("KILL", m.containerName)
217 | if err != nil {
218 | return err
219 | }
220 | cmd := exec.Command(
221 | "docker", "rm", "--volumes",
222 | m.containerName,
223 | )
224 | return cmd.Run()
225 | }
226 | utils.Logger.Infof("Deleting machine: %s ...", m.machineName)
227 | cmd := exec.Command(
228 | "docker", "rm", "--volumes",
229 | m.containerName,
230 | )
231 | return cmd.Run()
232 | }
233 |
234 | // Start starts a Machine
235 | func (m *Machine) Start() error {
236 | if !m.IsCreated() {
237 | utils.Logger.Infof("Machine %s hasn't been created...", m.machineName)
238 | return nil
239 | }
240 | if m.IsStarted() {
241 | utils.Logger.Infof("Machine %s is already started...", m.machineName)
242 | return nil
243 | }
244 | utils.Logger.Infof("Starting machine: %s ...", m.machineName)
245 |
246 | // Run command while sigs.k8s.io/kind/pkg/container/docker doesn't
247 | // have a start command
248 | cmd := exec.Command(
249 | "docker", "start",
250 | m.containerName,
251 | )
252 | return cmd.Run()
253 | }
254 |
255 | // Stop stops a Machine
256 | func (m *Machine) Stop() error {
257 | if !m.IsCreated() {
258 | utils.Logger.Infof("Machine %s hasn't been created...", m.containerName)
259 | return nil
260 | }
261 | if !m.IsStarted() {
262 | utils.Logger.Infof("Machine %s is already stopped...", m.containerName)
263 | return nil
264 | }
265 | utils.Logger.Infof("Stopping machine: %s ...", m.containerName)
266 |
267 | // Run command while sigs.k8s.io/kind/pkg/container/docker doesn't
268 | // have a start command
269 | cmd := exec.Command(
270 | "docker", "stop",
271 | m.containerName,
272 | )
273 | return cmd.Run()
274 | }
275 |
276 | // User gets the machine's OS user, defaults to root if not specified.
277 | func (m *Machine) User() string {
278 | if m.spec.User == "" {
279 | return defaultUser
280 | }
281 | return m.spec.User
282 | }
283 |
284 | // IsCreated returns if a machine is has been created. A created machine could
285 | // either be running or stopped.
286 | func (m *Machine) IsCreated() bool {
287 | res, err := docker.Inspect(m.containerName, "{{.Name}}")
288 | if err != nil {
289 | return false
290 | }
291 | if len(res) > 0 && len(res[0]) > 0 {
292 | return true
293 | }
294 | return false
295 | }
296 |
297 | // IsStarted returns if a machine is currently started or not.
298 | func (m *Machine) IsStarted() bool {
299 | res, _ := docker.Inspect(m.containerName, "{{.State.Running}}")
300 | parsed, _ := strconv.ParseBool(strings.Trim(res[0], `'`))
301 | return parsed
302 | }
303 |
304 | // HostPort returns the host port corresponding to the given container port.
305 | func (m *Machine) HostPort(containerPort int) (int, error) {
306 | // Use the cached version first
307 | if hostPort, ok := m.ports[containerPort]; ok {
308 | return hostPort, nil
309 | }
310 |
311 | var hostPort int
312 |
313 | // retrieve the specific port mapping using docker inspect
314 | lines, err := docker.Inspect(m.containerName, fmt.Sprintf("{{(index (index .NetworkSettings.Ports \"%d/tcp\") 0).HostPort}}", containerPort))
315 | if err != nil {
316 | return -1, errors.Wrapf(err, "hostport: failed to inspect container: %v", lines)
317 | }
318 | if len(lines) != 1 {
319 | return -1, errors.Errorf("hostport: should only be one line, got %d lines", len(lines))
320 | }
321 |
322 | port := strings.Replace(lines[0], "'", "", -1)
323 | if hostPort, err = strconv.Atoi(port); err != nil {
324 | return -1, errors.Wrap(err, "hostport: failed to parse string to int")
325 | }
326 |
327 | if m.ports == nil {
328 | m.ports = make(map[int]int)
329 | }
330 |
331 | // Cache the result
332 | m.ports[containerPort] = hostPort
333 | return hostPort, nil
334 | }
335 |
336 | func (m *Machine) networks() ([]*RuntimeNetwork, error) {
337 | if len(m.runtimeNetworks) != 0 {
338 | return m.runtimeNetworks, nil
339 | }
340 |
341 | var networks map[string]*network.EndpointSettings
342 | if err := docker.InspectObject(m.containerName, ".NetworkSettings.Networks", &networks); err != nil {
343 | return nil, err
344 | }
345 | m.runtimeNetworks = NewRuntimeNetworks(networks)
346 | return m.runtimeNetworks, nil
347 | }
348 |
349 | func (m *Machine) dockerStatus(s *MachineStatus) error {
350 | var ports []port
351 | if m.IsCreated() {
352 | for _, v := range m.spec.PortMappings {
353 | hPort, err := m.HostPort(int(v.ContainerPort))
354 | if err != nil {
355 | hPort = 0
356 | }
357 | p := port{
358 | Host: hPort,
359 | Guest: int(v.ContainerPort),
360 | }
361 | ports = append(ports, p)
362 | }
363 | }
364 | if len(ports) < 1 {
365 | for _, p := range m.spec.PortMappings {
366 | ports = append(ports, port{Host: 0, Guest: int(p.ContainerPort)})
367 | }
368 | }
369 | s.Ports = ports
370 |
371 | s.RuntimeNetworks, _ = m.networks()
372 |
373 | return nil
374 | }
375 |
376 | // Status returns the machine status.
377 | func (m *Machine) Status() *MachineStatus {
378 | s := MachineStatus{}
379 | s.Container = m.containerName
380 | s.Image = m.spec.Image
381 | s.Command = m.spec.Cmd
382 | s.Spec = m.spec
383 | s.MachineName = m.machineName
384 | s.IP = strings.Join(m.IP(), ",")
385 | state := NotCreated
386 |
387 | if m.IsCreated() {
388 | state = Stopped
389 | if m.IsStarted() {
390 | state = Running
391 | }
392 | }
393 | s.State = state
394 |
395 | _ = m.dockerStatus(&s)
396 |
397 | return &s
398 | }
399 |
400 | func (m *Machine) IP() []string {
401 | ips := []string{}
402 | for _, network := range m.runtimeNetworks {
403 | ips = append(ips, network.IP)
404 | }
405 | return ips
406 | }
407 |
408 | // AutoCdTo is to cd into the current working directory if below bind mount exists.
409 | // For example:
410 | // - type: bind
411 | // source: /
412 | // destination: /host
413 | func (m *Machine) AutoCdTo() string {
414 | for _, volume := range m.spec.Volumes {
415 | if volume.Type == "bind" && volume.Destination == "/host" {
416 | pwd, err := os.Getwd()
417 | if err != nil {
418 | utils.Logger.Warn("can't get current working directory: %w", err)
419 | }
420 | return fmt.Sprintf("%s%s", "/host", pwd)
421 | }
422 | }
423 | return ""
424 | }
425 |
--------------------------------------------------------------------------------
/pkg/cluster/run.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "bytes"
21 | "fmt"
22 |
23 | "github.com/brightzheng100/vind/pkg/docker"
24 | "github.com/brightzheng100/vind/pkg/exec"
25 | "github.com/brightzheng100/vind/pkg/utils"
26 | )
27 |
28 | // run runs a command in host. It will output the combined stdout/error on failure.
29 | func run(name string, args ...string) error {
30 | cmd := exec.Command(name, args...)
31 | output, err := exec.CombinedOutputLines(cmd)
32 | if err != nil {
33 | // log error output if there was any
34 | for _, line := range output {
35 | utils.Logger.Error(line)
36 | }
37 | }
38 | return err
39 | }
40 |
41 | // Run a command in a container. It will output the combined stdout/error on failure.
42 | func containerRun(nameOrID string, name string, args ...string) error {
43 | exe := docker.ContainerCmder(nameOrID)
44 | cmd := exe.Command(name, args...)
45 | output, err := exec.CombinedOutputLines(cmd)
46 | if err != nil {
47 | // log error output if there was any
48 | for _, line := range output {
49 | utils.Logger.WithField("machine", nameOrID).Error(line)
50 | }
51 | }
52 | return err
53 | }
54 |
55 | func containerRunShell(nameOrID string, script string) error {
56 | return containerRun(nameOrID, "/bin/bash", "-c", script)
57 | }
58 |
59 | func copy(nameOrID string, content []byte, path string) error {
60 | buf := bytes.Buffer{}
61 | buf.WriteString(fmt.Sprintf("cat <<__EOF | tee -a %s\n", path))
62 | buf.Write(content)
63 | buf.WriteString("__EOF")
64 | return containerRunShell(nameOrID, buf.String())
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/cluster/runtime_network.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "net"
21 |
22 | "github.com/docker/docker/api/types/network"
23 | )
24 |
25 | const (
26 | ipv4Length = 32
27 | )
28 |
29 | // RuntimeNetwork contains information about the network
30 | type RuntimeNetwork struct {
31 | // Name of the network
32 | Name string `json:"name,omitempty"`
33 | // IP of the container
34 | IP string `json:"ip,omitempty"`
35 | // Mask of the network
36 | Mask string `json:"mask,omitempty"`
37 | // Gateway of the network
38 | Gateway string `json:"gateway,omitempty"`
39 | }
40 |
41 | // NewRuntimeNetworks returns a slice of networks
42 | func NewRuntimeNetworks(networks map[string]*network.EndpointSettings) []*RuntimeNetwork {
43 | rnList := make([]*RuntimeNetwork, 0, len(networks))
44 | for key, value := range networks {
45 | mask := net.CIDRMask(value.IPPrefixLen, ipv4Length)
46 | maskIP := net.IP(mask).String()
47 | rnNetwork := &RuntimeNetwork{
48 | Name: key,
49 | IP: value.IPAddress,
50 | Mask: maskIP,
51 | Gateway: value.Gateway,
52 | }
53 | rnList = append(rnList, rnNetwork)
54 | }
55 | return rnList
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/cluster/runtime_network_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/docker/docker/api/types/network"
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | func TestNewRuntimeNetworks(t *testing.T) {
27 | t.Run("Success", func(t *testing.T) {
28 | networks := map[string]*network.EndpointSettings{}
29 | networks["mynetwork"] = &network.EndpointSettings{
30 | Gateway: "172.17.0.1",
31 | IPAddress: "172.17.0.4",
32 | IPPrefixLen: 16,
33 | }
34 | res := NewRuntimeNetworks(networks)
35 |
36 | expectedRuntimeNetworks := []*RuntimeNetwork{
37 | &RuntimeNetwork{Name: "mynetwork", Gateway: "172.17.0.1", IP: "172.17.0.4", Mask: "255.255.0.0"}}
38 | assert.Equal(t, expectedRuntimeNetworks, res)
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/cluster/status.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package cluster
18 |
19 | import (
20 | "encoding/json"
21 | "fmt"
22 | "io"
23 | "strings"
24 | "text/tabwriter"
25 |
26 | "github.com/brightzheng100/vind/pkg/config"
27 | "github.com/mitchellh/go-homedir"
28 | "gopkg.in/yaml.v2"
29 | )
30 |
31 | const (
32 | // NotCreated status of a machine
33 | NotCreated = "Not created"
34 | // Stopped status of a machine
35 | Stopped = "Stopped"
36 | // Running status of a machine
37 | Running = "Running"
38 | )
39 |
40 | // MachineStatus is the runtime status of a Machine.
41 | type MachineStatus struct {
42 | Container string `json:"container"`
43 | State string `json:"state"`
44 | Spec *config.Machine `json:"spec,omitempty"`
45 | Ports []port `json:"ports"`
46 | MachineName string `json:"machineName"`
47 | Image string `json:"image"`
48 | Command string `json:"cmd"`
49 | IP string `json:"ip"`
50 | RuntimeNetworks []*RuntimeNetwork `json:"runtimeNetworks,omitempty"`
51 | }
52 |
53 | // Formatter formats a slice of machines and outputs the result
54 | // in a given format.
55 | type Formatter interface {
56 | Format(io.Writer, *cluster, []*Machine) error
57 | }
58 |
59 | // JSONFormatter formats a slice of machines into a JSON and
60 | // outputs it to stdout.
61 | type JSONFormatter struct{}
62 |
63 | // TableFormatter formats a slice of machines into a colored
64 | // table like output and prints that to stdout.
65 | type TableFormatter struct{}
66 |
67 | // AnsibleFormatter formats a slice of machines into YAML and
68 | // outputs it to stdout
69 | type AnsibleFormatter struct{}
70 |
71 | // SSHConfigFormatter formats a slice of machines into ssh_config and
72 | // outputs it to stdout
73 | type SSHConfigFormatter struct{}
74 |
75 | type port struct {
76 | Guest int `json:"guest"`
77 | Host int `json:"host"`
78 | }
79 |
80 | // Format will output to stdout in JSON format.
81 | func (JSONFormatter) Format(w io.Writer, _ *cluster, machines []*Machine) error {
82 | var statuses []MachineStatus
83 | for _, m := range machines {
84 | statuses = append(statuses, *m.Status())
85 | }
86 |
87 | m := struct {
88 | Machines []MachineStatus `json:"machines"`
89 | }{
90 | Machines: statuses,
91 | }
92 | ms, err := json.MarshalIndent(m, "", " ")
93 | if err != nil {
94 | return err
95 | }
96 | ms = append(ms, '\n')
97 | _, err = w.Write(ms)
98 | return err
99 | }
100 |
101 | // FormatSingle is a json formatter for a single machine.
102 | func (JSONFormatter) FormatSingle(w io.Writer, m *Machine) error {
103 | status, err := json.MarshalIndent(m.Status(), "", " ")
104 | if err != nil {
105 | return err
106 | }
107 | _, err = w.Write(status)
108 | return err
109 | }
110 |
111 | // writer contains writeColumns' error value to clean-up some error handling
112 | type writer struct {
113 | err error
114 | }
115 |
116 | // writerColumns is a no-op if there was an error already
117 | func (wr writer) writeColumns(w io.Writer, cols []string) {
118 | if wr.err != nil {
119 | return
120 | }
121 | _, err := fmt.Fprintln(w, strings.Join(cols, "\t"))
122 | wr.err = err
123 | }
124 |
125 | // Format will output to stdout in table format.
126 | func (TableFormatter) Format(w io.Writer, _ *cluster, machines []*Machine) error {
127 | const padding = 3
128 | wr := new(writer)
129 | var statuses []MachineStatus
130 | for _, m := range machines {
131 | statuses = append(statuses, *m.Status())
132 | }
133 |
134 | table := tabwriter.NewWriter(w, 0, 0, padding, ' ', 0)
135 | wr.writeColumns(table, []string{"CONTAINER NAME", "MACHINE NAME", "PORTS", "IP", "IMAGE", "CMD", "STATE"})
136 | // we bail early here if there was an error so we don't process the below loop
137 | if wr.err != nil {
138 | return wr.err
139 | }
140 | for _, s := range statuses {
141 | var ports []string
142 | for _, port := range s.Ports {
143 | p := fmt.Sprintf("%d->%d", port.Host, port.Guest)
144 | ports = append(ports, p)
145 | }
146 | if len(ports) < 1 {
147 | for _, p := range s.Spec.PortMappings {
148 | port := fmt.Sprintf("%d->%d", p.HostPort, p.ContainerPort)
149 | ports = append(ports, port)
150 | }
151 | }
152 | ps := strings.Join(ports, ",")
153 | wr.writeColumns(table, []string{s.Container, s.MachineName, ps, s.IP, s.Image, s.Command, s.State})
154 | }
155 |
156 | if wr.err != nil {
157 | return wr.err
158 | }
159 | return table.Flush()
160 | }
161 |
162 | func (AnsibleFormatter) Format(w io.Writer, c *cluster, machines []*Machine) error {
163 | var statuses []MachineStatus
164 | for _, m := range machines {
165 | statuses = append(statuses, *m.Status())
166 | }
167 | path, _ := homedir.Expand(c.config.Cluster.PrivateKey)
168 |
169 | args := []string{
170 | "-o", "UserKnownHostsFile=/dev/null",
171 | "-o", "StrictHostKeyChecking=no",
172 | }
173 |
174 | m := map[string]interface{}{}
175 | hosts := map[string]interface{}{}
176 | for _, s := range statuses {
177 | user := s.Spec.User
178 | if s.Spec.User == "" {
179 | user = defaultUser
180 | }
181 | port := 0
182 | if len(s.Ports) > 0 {
183 | port = s.Ports[0].Host
184 | }
185 | hosts[s.MachineName] = map[string]interface{}{
186 | "ansible_host": "localhost",
187 | "ansible_port": port,
188 | "ansible_user": user,
189 | "ansible_connection": "ssh",
190 | "ansible_ssh_private_key_file": path,
191 | "ansible_ssh_common_args": strings.Join(args, " "),
192 | }
193 | }
194 | group := map[string]interface{}{}
195 | group["hosts"] = hosts
196 | m[c.config.Cluster.Name] = group
197 | ms, err := yaml.Marshal(m)
198 | if err != nil {
199 | return err
200 | }
201 | _, err = w.Write(ms)
202 | return err
203 | }
204 |
205 | func (SSHConfigFormatter) Format(w io.Writer, c *cluster, machines []*Machine) error {
206 | var statuses []MachineStatus
207 | for _, m := range machines {
208 | statuses = append(statuses, *m.Status())
209 | }
210 | path, _ := homedir.Expand(c.config.Cluster.PrivateKey)
211 |
212 | args := map[string]interface{}{
213 | "UserKnownHostsFile": "/dev/null",
214 | "StrictHostKeyChecking": "no",
215 | }
216 |
217 | l := []string{}
218 | for _, s := range statuses {
219 | user := s.Spec.User
220 | if s.Spec.User == "" {
221 | user = defaultUser
222 | }
223 | port := 0
224 | if len(s.Ports) > 0 {
225 | port = s.Ports[0].Host
226 | }
227 | opts := map[string]interface{}{
228 | "Hostname": "localhost",
229 | "Port": port,
230 | "User": user,
231 | "IdentityFile": path,
232 | }
233 | for arg, val := range args {
234 | opts[arg] = val
235 | }
236 |
237 | h := fmt.Sprintf("Host %s\n", s.MachineName)
238 | for opt, val := range opts {
239 | h += fmt.Sprintf(" %s %v\n", opt, val)
240 | }
241 | l = append(l, h)
242 | }
243 | for _, s := range l {
244 | _, err := w.Write([]byte(s))
245 | if err != nil {
246 | return err
247 | }
248 | }
249 | return nil
250 | }
251 |
--------------------------------------------------------------------------------
/pkg/config/cluster.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package config
18 |
19 | import (
20 | "fmt"
21 | "os"
22 |
23 | "github.com/brightzheng100/vind/pkg/utils"
24 | "gopkg.in/yaml.v2"
25 | )
26 |
27 | // Config is the top level config object.
28 | type Config struct {
29 | // Cluster describes cluster-wide configuration.
30 | Cluster Cluster `json:"cluster"`
31 | // MachineSets describe the sets of machines we define in this cluster.
32 | MachineSets []MachineSet `json:"machineSets"`
33 | }
34 |
35 | // Cluster is a set of Machines.
36 | type Cluster struct {
37 | // Name is the cluster name. Defaults to "cluster".
38 | Name string `json:"name"`
39 | // PrivateKey is the path to the private SSH key used to login into the cluster
40 | // machines. Can be expanded to user homedir if ~ is found. Ex. ~/.ssh/id_rsa.
41 | //
42 | // This field is optional. If absent, machines are expected to have a public
43 | // key defined.
44 | PrivateKey string `json:"privateKey,omitempty"`
45 | }
46 |
47 | // MachineSet are a set of machines following the same specification.
48 | type MachineSet struct {
49 | // Name is the MachineSet's name. Defaults to "test"
50 | Name string `json:"name"`
51 | // Replicas is the number of machines within the MachineSet
52 | Replicas int `json:"replicas"`
53 | // Spec is the detailed specifications of the machines within the MachineSet
54 | Spec Machine `json:"spec"`
55 | }
56 |
57 | func NewConfigFromYAML(data []byte) (*Config, error) {
58 | config := Config{}
59 | if err := yaml.Unmarshal(data, &config); err != nil {
60 | return nil, err
61 | }
62 | return &config, nil
63 | }
64 |
65 | func NewConfigFromFile(path string) (*Config, error) {
66 | data, err := os.ReadFile(path)
67 | if err != nil {
68 | return nil, err
69 | }
70 | return NewConfigFromYAML(data)
71 | }
72 |
73 | // validate checks basic rules for MachineReplicas's fields
74 | func (conf MachineSet) validate() error {
75 | return conf.Spec.validate()
76 | }
77 |
78 | // Validate checks basic rules for Config's fields
79 | func (conf Config) Validate() error {
80 | valid := true
81 | for _, machine := range conf.MachineSets {
82 | err := machine.validate()
83 | if err != nil {
84 | valid = false
85 | utils.Logger.Fatalf(err.Error())
86 | }
87 | }
88 | if !valid {
89 | return fmt.Errorf("Configuration file non valid")
90 | }
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/config/get.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package config
18 |
19 | import (
20 | "fmt"
21 | "reflect"
22 | "strconv"
23 | "strings"
24 | )
25 |
26 | func pathSplit(r rune) bool {
27 | return r == '.' || r == '[' || r == ']' || r == '"'
28 | }
29 |
30 | // GetValueFromConfig returns specific value from object given a string path
31 | func GetValueFromConfig(stringPath string, object interface{}) (interface{}, error) {
32 | keyPath := strings.FieldsFunc(stringPath, pathSplit)
33 | v := reflect.ValueOf(object)
34 | for _, key := range keyPath {
35 | keyUpper := strings.Title(key)
36 | for v.Kind() == reflect.Ptr {
37 | v = v.Elem()
38 | }
39 | if v.Kind() == reflect.Struct {
40 | v = v.FieldByName(keyUpper)
41 | if !v.IsValid() {
42 | return nil, fmt.Errorf("%v key does not exist", keyUpper)
43 | }
44 | } else if v.Kind() == reflect.Slice {
45 | index, errConv := strconv.Atoi(keyUpper)
46 | if errConv != nil {
47 | return nil, fmt.Errorf("%v is not an index", key)
48 | }
49 | v = v.Index(index)
50 | } else {
51 | return nil, fmt.Errorf("%v is neither a slice or a struct", v)
52 | }
53 | }
54 | return v.Interface(), nil
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/config/get_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package config
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 | )
24 |
25 | func TestGetValueFromConfig(t *testing.T) {
26 | config := Config{
27 | Cluster: Cluster{Name: "clustername", PrivateKey: "privatekey"},
28 | MachineSets: []MachineSet{
29 | MachineSet{
30 | Name: "mySet",
31 | Replicas: 3,
32 | Spec: Machine{
33 | Image: "myImage",
34 | Name: "myName",
35 | Privileged: true,
36 | },
37 | },
38 | },
39 | }
40 |
41 | tests := []struct {
42 | name string
43 | stringPath string
44 | config Config
45 | expectedOutput interface{}
46 | }{
47 | {
48 | "simple path select string",
49 | "cluster.name",
50 | Config{
51 | Cluster: Cluster{Name: "clustername", PrivateKey: "privatekey"},
52 | MachineSets: []MachineSet{MachineSet{Name: "mySet", Replicas: 3, Spec: Machine{}}},
53 | },
54 | "clustername",
55 | },
56 | {
57 | "array path select global",
58 | "machines[0].spec",
59 | config,
60 | Machine{
61 | Image: "myImage",
62 | Name: "myName",
63 | Privileged: true,
64 | },
65 | },
66 | {
67 | "array path select bool",
68 | "machines[0].spec.Privileged",
69 | config,
70 | true,
71 | },
72 | }
73 |
74 | for _, utest := range tests {
75 | t.Run(utest.name, func(t *testing.T) {
76 | res, err := GetValueFromConfig(utest.stringPath, utest.config)
77 | assert.Nil(t, err)
78 | assert.Equal(t, utest.expectedOutput, res)
79 | })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/config/key.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package config
18 |
19 | // PublicKey is a public SSH key.
20 | type PublicKey struct {
21 | Name string `json:"name"`
22 | // Key is the public key textual representation. Begins with Begins with
23 | // 'ssh-rsa', 'ssh-dss', 'ssh-ed25519', 'ecdsa-sha2-nistp256',
24 | // 'ecdsa-sha2-nistp384', or 'ecdsa-sha2-nistp521'.
25 | Key string `json:"key"`
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/config/machine.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package config
18 |
19 | import (
20 | "fmt"
21 | "strings"
22 |
23 | "github.com/brightzheng100/vind/pkg/utils"
24 | )
25 |
26 | // Machine is the machine configuration.
27 | type Machine struct {
28 | // Name is the machine name.
29 | //
30 | // When used in a MachineReplicas object, eg. in vind.yaml config files,
31 | // this field a format string. This format string needs to have a '%d', which
32 | // is populated by the machine index, a number between 0 and N-1, N being the
33 | // Count field of MachineReplicas. Name will default to "node%d"
34 | //
35 | // This name will also be used as the machine hostname.
36 | Name string `json:"name"`
37 | // Image is the container image to use for this machine.
38 | Image string `json:"image"`
39 | // User is the machine user used for SSH login.
40 | User string `json:"user,omitempty"`
41 | // Privileged controls whether to start the Machine as a privileged container
42 | // or not. Defaults to false.
43 | Privileged bool `json:"privileged,omitempty"`
44 | // Volumes is the list of volumes attached to this machine.
45 | Volumes []Volume `json:"volumes,omitempty"`
46 | // Networks is the list of user-defined docker networks this machine is
47 | // attached to. These networks have to be created manually before creating the
48 | // containers via "docker network create mynetwork"
49 | Networks []string `json:"networks,omitempty"`
50 | // PortMappings is the list of ports to expose to the host.
51 | PortMappings []PortMapping `json:"portMappings,omitempty"`
52 | // Cmd is a cmd which will be run in the container.
53 | Cmd string `json:"cmd,omitempty"`
54 | // PublicKey is the name of the public key to upload onto the machine for root
55 | // SSH access.
56 | PublicKey string `json:"publicKey,omitempty"`
57 |
58 | // Backend specifies the runtime backend for this machine
59 | Backend string `json:"backend,omitempty"`
60 | }
61 |
62 | // Volume is a volume that can be attached to a Machine.
63 | type Volume struct {
64 | // Type is the volume type. One of "bind" or "volume".
65 | Type string `json:"type"`
66 | // Source is the volume source.
67 | // With type=bind, the volume source is a directory or a file in the host
68 | // filesystem.
69 | // With type=volume, source is either the name of a docker volume or "" for
70 | // anonymous volumes.
71 | Source string `json:"source"`
72 | // Destination is the mount point inside the container.
73 | Destination string `json:"destination"`
74 | // ReadOnly specifies if the volume should be read-only or not.
75 | ReadOnly bool `json:"readOnly"`
76 | }
77 |
78 | // PortMapping describes mapping a port from the machine onto the host.
79 | type PortMapping struct {
80 | // Protocol is the layer 4 protocol for this mapping. One of "tcp" or "udp".
81 | // Defaults to "tcp".
82 | Protocol string `json:"protocol,omitempty"`
83 | // Address is the host address to bind to. Defaults to "0.0.0.0".
84 | Address string `json:"address,omitempty"`
85 | // HostPort is the base host port to map the containers ports to. As we
86 | // configure a number of machine replicas, each machine will use HostPort+i
87 | // where i is between 0 and N-1, N being the number of machine replicas. If 0,
88 | // a local port will be automatically allocated.
89 | HostPort uint16 `json:"hostPort,omitempty"`
90 | // ContainerPort is the container port to map.
91 | ContainerPort uint16 `json:"containerPort"`
92 | }
93 |
94 | // validate checks basic rules for Machine's fields
95 | func (conf Machine) validate() error {
96 | validName := strings.Contains(conf.Name, "%d")
97 | if !validName {
98 | utils.Logger.Warnf("Machine conf validation: machine name %v is not valid, it should contains %%d", conf.Name)
99 | return fmt.Errorf("Machine configuration not valid")
100 | }
101 | return nil
102 | }
103 |
--------------------------------------------------------------------------------
/pkg/docker/cp.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | )
24 |
25 | // CopyTo copies the file at hostPath to the container at destPath
26 | func CopyTo(srcPath, containerNameOrID, destPath string) error {
27 | cmd := exec.Command(
28 | "docker", "cp",
29 | srcPath, // from the source file
30 | containerNameOrID+":"+destPath, // to the node, at dest
31 | )
32 | return cmd.Run()
33 | }
34 |
35 | // CopyFrom copies the file or dir in the container at srcPath to the host at hostPath
36 | func CopyFrom(containerNameOrID, srcPath, destPath string) error {
37 | cmd := exec.Command(
38 | "docker", "cp",
39 | containerNameOrID+":"+srcPath, // from the node, at src
40 | destPath, // to the host
41 | )
42 | return cmd.Run()
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/docker/create.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/pkg/errors"
23 |
24 | "github.com/brightzheng100/vind/pkg/exec"
25 | "github.com/brightzheng100/vind/pkg/utils"
26 | )
27 |
28 | // Create creates a container with "docker create", with some error handling
29 | // it will return the ID of the created container if any, even on error
30 | func Create(image string, runArgs []string, containerArgs []string) (id string, err error) {
31 | args := []string{"create"}
32 | args = append(args, runArgs...)
33 | args = append(args, image)
34 | args = append(args, containerArgs...)
35 |
36 | utils.Logger.Debug("Docker command: ", "docker", args)
37 | cmd := exec.Command("docker", args...)
38 |
39 | output, err := exec.CombinedOutputLines(cmd)
40 | if err != nil {
41 | // log error output if there was any
42 | for _, line := range output {
43 | utils.Logger.Error(line)
44 | }
45 | return "", err
46 | }
47 | // if docker created a container the id will be the first line and match
48 | // validate the output and get the id
49 | if len(output) < 1 {
50 | return "", errors.New("failed to get container id, received no output from docker run")
51 | }
52 | if !containerIDRegex.MatchString(output[0]) {
53 | return "", errors.Errorf("failed to get container id, output did not match: %v", output)
54 | }
55 | return output[0], nil
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/docker/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | // Package docker contains helpers for working with docker
20 | // This package has no stability guarantees whatsoever!
21 | package docker
22 |
--------------------------------------------------------------------------------
/pkg/docker/exec.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "io"
23 |
24 | "github.com/brightzheng100/vind/pkg/exec"
25 | )
26 |
27 | // containerCmder implements exec.Cmder for docker containers
28 | type containerCmder struct {
29 | nameOrID string
30 | }
31 |
32 | // ContainerCmder creates a new exec.Cmder against a docker container
33 | func ContainerCmder(containerNameOrID string) exec.Cmder {
34 | return &containerCmder{
35 | nameOrID: containerNameOrID,
36 | }
37 | }
38 |
39 | func (c *containerCmder) Command(command string, args ...string) exec.Cmd {
40 | return &containerCmd{
41 | nameOrID: c.nameOrID,
42 | command: command,
43 | args: args,
44 | }
45 | }
46 |
47 | // containerCmd implements exec.Cmd for docker containers
48 | type containerCmd struct {
49 | nameOrID string // the container name or ID
50 | command string
51 | args []string
52 | env []string
53 | stdin io.Reader
54 | stdout io.Writer
55 | stderr io.Writer
56 | }
57 |
58 | func (c *containerCmd) Run() error {
59 | args := []string{
60 | "exec",
61 | // run with privileges so we can remount etc..
62 | // this might not make sense in the most general sense, but it is
63 | // important to many kind commands
64 | "--privileged",
65 | }
66 | if c.stdin != nil {
67 | args = append(args,
68 | "-i", // interactive so we can supply input
69 | )
70 | }
71 | if c.stderr != nil || c.stdout != nil {
72 | args = append(args,
73 | "-t", // use a tty so we can get output
74 | )
75 | }
76 | // set env
77 | for _, env := range c.env {
78 | args = append(args, "-e", env)
79 | }
80 | // specify the container and command, after this everything will be
81 | // args the command in the container rather than to docker
82 | args = append(
83 | args,
84 | c.nameOrID, // ... against the container
85 | c.command, // with the command specified
86 | )
87 | args = append(
88 | args,
89 | // finally, with the caller args
90 | c.args...,
91 | )
92 | cmd := exec.Command("docker", args...)
93 | if c.stdin != nil {
94 | cmd.SetStdin(c.stdin)
95 | }
96 | if c.stderr != nil {
97 | cmd.SetStderr(c.stderr)
98 | }
99 | if c.stdout != nil {
100 | cmd.SetStdout(c.stdout)
101 | }
102 | return cmd.Run()
103 | }
104 |
105 | func (c *containerCmd) SetEnv(env ...string) {
106 | c.env = env
107 | }
108 |
109 | func (c *containerCmd) SetStdin(r io.Reader) {
110 | c.stdin = r
111 | }
112 |
113 | func (c *containerCmd) SetStdout(w io.Writer) {
114 | c.stdout = w
115 | }
116 |
117 | func (c *containerCmd) SetStderr(w io.Writer) {
118 | c.stderr = w
119 | }
120 |
--------------------------------------------------------------------------------
/pkg/docker/inspect.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "encoding/json"
23 | "fmt"
24 | "strings"
25 |
26 | "github.com/brightzheng100/vind/pkg/exec"
27 | )
28 |
29 | // Inspect return low-level information on containers
30 | func Inspect(containerNameOrID, format string) ([]string, error) {
31 | cmd := exec.Command("docker", "inspect",
32 | "-f", // format
33 | fmt.Sprintf("'%s'", format),
34 | containerNameOrID, // ... against the "node" container
35 | )
36 |
37 | return exec.CombinedOutputLines(cmd)
38 |
39 | }
40 |
41 | // InspectObject is similar to Inspect but deserializes the JSON output to a struct.
42 | func InspectObject(containerNameOrID, format string, out interface{}) error {
43 | res, err := Inspect(containerNameOrID, fmt.Sprintf("{{json %s}}", format))
44 | if err != nil {
45 | return err
46 | }
47 | data := []byte(strings.Trim(res[0], "'"))
48 | err = json.Unmarshal(data, out)
49 | if err != nil {
50 | return err
51 | }
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/docker/kill.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | )
24 |
25 | // Kill sends the named signal to the container
26 | func Kill(signal, containerNameOrID string) error {
27 | cmd := exec.Command(
28 | "docker", "kill",
29 | "-s", signal,
30 | containerNameOrID,
31 | )
32 | return cmd.Run()
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/docker/network_connect.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | )
24 |
25 | // ConnectNetwork connects network to container.
26 | func ConnectNetwork(container, network string) error {
27 | cmd := exec.Command("docker", "network", "connect", network, container)
28 | return runWithLogging(cmd)
29 | }
30 |
31 | // ConnectNetworkWithAlias connects network to container adding a network-scoped
32 | // alias for the container.
33 | func ConnectNetworkWithAlias(container, network, alias string) error {
34 | cmd := exec.Command("docker", "network", "connect", network, container, "--alias", alias)
35 | return runWithLogging(cmd)
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/docker/pull.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "os"
23 | "time"
24 |
25 | "github.com/brightzheng100/vind/pkg/exec"
26 | "github.com/brightzheng100/vind/pkg/utils"
27 | )
28 |
29 | // PullIfNotPresent will pull an image if it is not present locally
30 | // retrying up to retries times
31 | // it returns true if it attempted to pull, and any errors from pulling
32 | func PullIfNotPresent(image string, retries int) (pulled bool, err error) {
33 | // TODO(bentheelder): switch most (all) of the logging here to debug level
34 | // once we have configurable log levels
35 | // if this did not return an error, then the image exists locally
36 | cmd := exec.Command("docker", "inspect", "--type=image", image)
37 | if err := cmd.Run(); err == nil {
38 | utils.Logger.Infof("Docker Image: %s present locally", image)
39 | return false, nil
40 | }
41 | // otherwise try to pull it
42 | return true, Pull(image, retries)
43 | }
44 |
45 | // Pull pulls an image, retrying up to retries times
46 | func Pull(image string, retries int) error {
47 | utils.Logger.Infof("Pulling image: %s ...", image)
48 | err := setPullCmd(image).Run()
49 | // retry pulling up to retries times if necessary
50 | if err != nil {
51 | for i := 0; i < retries; i++ {
52 | time.Sleep(time.Second * time.Duration(i+1))
53 | utils.Logger.WithError(err).Infof("Trying again to pull image: %s ...", image)
54 | // TODO(bentheelder): add some backoff / sleep?
55 | if err = setPullCmd(image).Run(); err == nil {
56 | break
57 | }
58 | }
59 | }
60 | if err != nil {
61 | utils.Logger.WithError(err).Infof("Failed to pull image: %s", image)
62 | }
63 | return err
64 | }
65 |
66 | // IsRunning checks if Docker is running properly
67 | func IsRunning() error {
68 | cmd := exec.Command("docker", "version")
69 | if err := cmd.Run(); err != nil {
70 | utils.Logger.WithError(err).Infoln("Cannot connect to the Docker daemon. Is the docker daemon running?")
71 | return err
72 | }
73 | return nil
74 | }
75 |
76 | func setPullCmd(image string) exec.Cmd {
77 | cmd := exec.Command("docker", "pull", image)
78 | cmd.SetStderr(os.Stderr)
79 | return cmd
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/docker/run.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "regexp"
23 |
24 | "github.com/brightzheng100/vind/pkg/utils"
25 | "github.com/pkg/errors"
26 |
27 | "github.com/brightzheng100/vind/pkg/exec"
28 | )
29 |
30 | // Docker container IDs are hex, more than one character, and on their own line
31 | var containerIDRegex = regexp.MustCompile("^[a-f0-9]+$")
32 |
33 | // Run creates a container with "docker run", with some error handling
34 | // it will return the ID of the created container if any, even on error
35 | func Run(image string, runArgs []string, containerArgs []string) (id string, err error) {
36 | args := []string{"run"}
37 | args = append(args, runArgs...)
38 | args = append(args, image)
39 | args = append(args, containerArgs...)
40 | cmd := exec.Command("docker", args...)
41 | output, err := exec.CombinedOutputLines(cmd)
42 | if err != nil {
43 | // log error output if there was any
44 | for _, line := range output {
45 | utils.Logger.Error(line)
46 | }
47 | return "", err
48 | }
49 | // if docker created a container the id will be the first line and match
50 | // validate the output and get the id
51 | if len(output) < 1 {
52 | return "", errors.New("failed to get container id, received no output from docker run")
53 | }
54 | if !containerIDRegex.MatchString(output[0]) {
55 | return "", errors.Errorf("failed to get container id, output did not match: %v", output)
56 | }
57 | return output[0], nil
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/docker/save.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | )
24 |
25 | // Save saves image to dest, as in `docker save`
26 | func Save(image, dest string) error {
27 | return exec.Command("docker", "save", "-o", dest, image).Run()
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/docker/start.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | "github.com/brightzheng100/vind/pkg/utils"
24 | )
25 |
26 | func runWithLogging(cmd exec.Cmd) error {
27 | output, err := exec.CombinedOutputLines(cmd)
28 | if err != nil {
29 | // log error output if there was any
30 | for _, line := range output {
31 | utils.Logger.Error(line)
32 | }
33 | }
34 | return err
35 |
36 | }
37 |
38 | // Start starts a container.
39 | func Start(container string) error {
40 | cmd := exec.Command("docker", "start", container)
41 | return runWithLogging(cmd)
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/docker/stop.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "github.com/brightzheng100/vind/pkg/exec"
23 | )
24 |
25 | // Stop stops a container.
26 | func Stop(container string) error {
27 | cmd := exec.Command("docker", "stop", container)
28 | return runWithLogging(cmd)
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/docker/userns_remap.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package docker
20 |
21 | import (
22 | "strings"
23 |
24 | "github.com/brightzheng100/vind/pkg/exec"
25 | )
26 |
27 | // UsernsRemap checks if userns-remap is enabled in dockerd
28 | func UsernsRemap() bool {
29 | cmd := exec.Command("docker", "info", "--format", "'{{json .SecurityOptions}}'")
30 | lines, err := exec.CombinedOutputLines(cmd)
31 | if err != nil {
32 | return false
33 | }
34 | if len(lines) > 0 {
35 | if strings.Contains(lines[0], "name=userns") {
36 | return true
37 | }
38 | }
39 | return false
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/exec/exec.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package exec
20 |
21 | import (
22 | "bufio"
23 | "bytes"
24 | "io"
25 | "os"
26 |
27 | "github.com/brightzheng100/vind/pkg/utils"
28 | )
29 |
30 | // Cmd abstracts over running a command somewhere, this is useful for testing
31 | type Cmd interface {
32 | Run() error
33 | // Each entry should be of the form "key=value"
34 | SetEnv(...string)
35 | SetStdin(io.Reader)
36 | SetStdout(io.Writer)
37 | SetStderr(io.Writer)
38 | }
39 |
40 | // Cmder abstracts over creating commands
41 | type Cmder interface {
42 | // command, args..., just like os/exec.Cmd
43 | Command(string, ...string) Cmd
44 | }
45 |
46 | // DefaultCmder is a LocalCmder instance used for convienience, packages
47 | // originally using os/exec.Command can instead use pkg/kind/exec.Command
48 | // which forwards to this instance
49 | // TODO(bentheelder): swap this for testing
50 | // TODO(bentheelder): consider not using a global for this :^)
51 | var DefaultCmder = &LocalCmder{}
52 |
53 | // Command is a convience wrapper over DefaultCmder.Command
54 | func Command(command string, args ...string) Cmd {
55 | return DefaultCmder.Command(command, args...)
56 | }
57 |
58 | // CommandWithLogging is a convience wrapper over Command
59 | // display any errors received by the executed command
60 | func CommandWithLogging(command string, args ...string) error {
61 | cmd := Command(command, args...)
62 | output, err := CombinedOutputLines(cmd)
63 | if err != nil {
64 | // log error output if there was any
65 | for _, line := range output {
66 | utils.Logger.Error(line)
67 | }
68 | }
69 | return err
70 |
71 | }
72 |
73 | // CombinedOutputLines is like os/exec's cmd.CombinedOutput(),
74 | // but over our Cmd interface, and instead of returning the byte buffer of
75 | // stderr + stdout, it scans these for lines and returns a slice of output lines
76 | func CombinedOutputLines(cmd Cmd) (lines []string, err error) {
77 | var buff bytes.Buffer
78 | cmd.SetStdout(&buff)
79 | cmd.SetStderr(&buff)
80 | err = cmd.Run()
81 | scanner := bufio.NewScanner(&buff)
82 | for scanner.Scan() {
83 | lines = append(lines, scanner.Text())
84 | }
85 | return lines, err
86 | }
87 |
88 | // InheritOutput sets cmd's output to write to the current process's stdout and stderr
89 | func InheritOutput(cmd Cmd) {
90 | cmd.SetStderr(os.Stderr)
91 | cmd.SetStdout(os.Stdout)
92 | }
93 |
94 | // RunLoggingOutputOnFail runs the cmd, logging error output if Run returns an error
95 | func RunLoggingOutputOnFail(cmd Cmd) error {
96 | var buff bytes.Buffer
97 | cmd.SetStdout(&buff)
98 | cmd.SetStderr(&buff)
99 | err := cmd.Run()
100 | if err != nil {
101 | utils.Logger.Error("failed with:")
102 | scanner := bufio.NewScanner(&buff)
103 | for scanner.Scan() {
104 | utils.Logger.Error(scanner.Text())
105 | }
106 | }
107 | return err
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/exec/local.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2018 The Kubernetes Authors.
3 | Copyright © 2019-2023 footloose developers
4 | Copyright © 2024-2025 Bright Zheng
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | */
18 |
19 | package exec
20 |
21 | import (
22 | "fmt"
23 | "io"
24 | "os"
25 | osexec "os/exec"
26 | "strings"
27 |
28 | "github.com/brightzheng100/vind/pkg/utils"
29 | "github.com/pkg/errors"
30 | )
31 |
32 | // LocalCmd wraps os/exec.Cmd, implementing the kind/pkg/exec.Cmd interface
33 | type LocalCmd struct {
34 | *osexec.Cmd
35 | }
36 |
37 | var _ Cmd = &LocalCmd{}
38 |
39 | // LocalCmder is a factory for LocalCmd, implementing Cmder
40 | type LocalCmder struct{}
41 |
42 | var _ Cmder = &LocalCmder{}
43 |
44 | // Command returns a new exec.Cmd backed by Cmd
45 | func (c *LocalCmder) Command(name string, arg ...string) Cmd {
46 | return &LocalCmd{
47 | Cmd: osexec.Command(name, arg...),
48 | }
49 | }
50 |
51 | // SetEnv sets env
52 | func (cmd *LocalCmd) SetEnv(env ...string) {
53 | cmd.Env = env
54 | }
55 |
56 | // SetStdin sets stdin
57 | func (cmd *LocalCmd) SetStdin(r io.Reader) {
58 | cmd.Stdin = r
59 | }
60 |
61 | // SetStdout set stdout
62 | func (cmd *LocalCmd) SetStdout(w io.Writer) {
63 | cmd.Stdout = w
64 | }
65 |
66 | // SetStderr sets stderr
67 | func (cmd *LocalCmd) SetStderr(w io.Writer) {
68 | cmd.Stderr = w
69 | }
70 |
71 | // Run runs
72 | func (cmd *LocalCmd) Run() error {
73 | utils.Logger.Debugf("Running: %v %v", cmd.Path, cmd.Args)
74 | return cmd.Cmd.Run()
75 | }
76 |
77 | func ExecuteCommand(command string, args ...string) (string, error) {
78 | cmd := osexec.Command(command, args...)
79 | out, err := cmd.CombinedOutput()
80 | cmdArgs := strings.Join(cmd.Args, " ")
81 | //utils.Logger.Debugf("Command %q returned %q\n", cmdArgs, out)
82 | if err != nil {
83 | return "", errors.Wrapf(err, "command %q exited with %q", cmdArgs, out)
84 | }
85 |
86 | // TODO: strings.Builder?
87 | return strings.TrimSpace(string(out)), nil
88 | }
89 |
90 | func ExecForeground(command string, args ...string) (int, error) {
91 | cmd := osexec.Command(command, args...)
92 | cmd.Stdin = os.Stdin
93 | cmd.Stdout = os.Stdout
94 | cmd.Stderr = os.Stderr
95 | err := cmd.Run()
96 | cmdArgs := strings.Join(cmd.Args, " ")
97 |
98 | var cmdErr error
99 | var exitCode int
100 |
101 | if err != nil {
102 | cmdErr = fmt.Errorf("external command %q exited with an error: %v", cmdArgs, err)
103 |
104 | if exitError, ok := err.(*osexec.ExitError); ok {
105 | exitCode = exitError.ExitCode()
106 | } else {
107 | cmdErr = fmt.Errorf("failed to get exit code for external command %q", cmdArgs)
108 | }
109 | }
110 |
111 | return exitCode, cmdErr
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/utils/logging.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024-2025 Bright Zheng
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package utils
17 |
18 | import (
19 | "os"
20 |
21 | "github.com/sirupsen/logrus"
22 | )
23 |
24 | var Logger = logrus.New()
25 |
26 | func init() {
27 | Logger.SetFormatter(&logrus.TextFormatter{})
28 |
29 | // defaults to Info log level
30 | Logger.SetLevel(logrus.InfoLevel)
31 |
32 | // and log level is configurable by env vaiable $LOG_LEVEL
33 | config_log_level := os.Getenv("LOG_LEVEL")
34 | if config_log_level != "" {
35 | log_level, err := logrus.ParseLevel(config_log_level)
36 | if err != nil {
37 | Logger.Warnf("configured LOG_LEVEL is unparsable: %s, ignore and fall back to Info level", config_log_level)
38 | } else {
39 | Logger.Infof("log level is set to [%s]", log_level)
40 | Logger.SetLevel(log_level)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/version/release.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2019-2023 footloose developers
3 | Copyright © 2024-2025 Bright Zheng
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 | package release
18 |
19 | import (
20 | "context"
21 | "fmt"
22 |
23 | "github.com/google/go-github/v24/github"
24 | )
25 |
26 | const (
27 | owner = "brightzheng100"
28 | repo = "vind"
29 | )
30 |
31 | // FindLastRelease searches latest release of the project
32 | func FindLastRelease() (*github.RepositoryRelease, error) {
33 | githubclient := github.NewClient(nil)
34 | repoRelease, _, err := githubclient.Repositories.GetLatestRelease(context.Background(), owner, repo)
35 | if err != nil {
36 | return nil, fmt.Errorf("Failed to get latest release information")
37 | }
38 | return repoRelease, nil
39 | }
40 |
--------------------------------------------------------------------------------