├── .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 | [![asciicast](https://asciinema.org/a/697321.svg)](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 | [![asciicast](https://asciinema.org/a/697321.svg)](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 | --------------------------------------------------------------------------------