Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Factory
k0sctl
k0sctl-0.19.4.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File k0sctl-0.19.4.obscpio of Package k0sctl
07070100000000000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001600000000k0sctl-0.19.4/.github07070100000001000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001E00000000k0sctl-0.19.4/.github/actions07070100000002000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002F00000000k0sctl-0.19.4/.github/actions/smoke-test-cache07070100000003000081A40000000000000000000000016733442800000363000000000000000000000000000000000000003B00000000k0sctl-0.19.4/.github/actions/smoke-test-cache/action.yamlname: Smoke test cache steps description: Cache smoke test binaries runs: using: composite steps: - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Download compiled binary artifact uses: actions/download-artifact@v4 with: name: k0sctl path: . - name: k0sctl cache uses: actions/cache@v3 with: key: k0sctl-cache path: | /var/cache/k0sctl/k0s ~/.cache/k0sctl/k0s - name: kubectl cache uses: actions/cache@v3 with: path: | smoke-test/kubectl key: "kubectl-${{ hashFiles('smoke-test/smoke.common.sh') }}" - name: Make binaries executable shell: bash run: | chmod +x k0sctl || true chmod +x smoke-test/kubectl || true 07070100000004000081A400000000000000000000000167334428000000EE000000000000000000000000000000000000002500000000k0sctl-0.19.4/.github/dependabot.ymlversion: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 5 - package-ecosystem: github-actions directory: / schedule: interval: daily 07070100000005000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002000000000k0sctl-0.19.4/.github/workflows07070100000006000081A40000000000000000000000016733442800000145000000000000000000000000000000000000002F00000000k0sctl-0.19.4/.github/workflows/actionlint.ymlname: Lint (actionlint) on: pull_request: paths: - .github/** jobs: actionlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: reviewdog/action-actionlint@v1 with: fail_on_error: true level: warning github_token: ${{ github.token }} 07070100000007000081A40000000000000000000000016733442800000BB3000000000000000000000000000000000000003400000000k0sctl-0.19.4/.github/workflows/codeql-analysis.yml# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '40 16 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v4 # Added as suggested in https://github.com/github/codeql-action/issues/1842 to bring in a newer go than exists # preinstalled on the runners - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # âšī¸ Command-line programs to run using the OS shell. # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 07070100000008000081A40000000000000000000000016733442800000227000000000000000000000000000000000000003C00000000k0sctl-0.19.4/.github/workflows/dependabot-auto-approve.ymlname: Dependabot auto-approve on: pull_request permissions: pull-requests: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve PR run: gh pr review --approve "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 07070100000009000081A400000000000000000000000167334428000002EA000000000000000000000000000000000000002700000000k0sctl-0.19.4/.github/workflows/go.ymlname: Go unit tests on: pull_request: paths: - '**.go' - go.mod - go.sum - Makefile jobs: unit-test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Test run: go test -v ./... build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Verify all binaries can be built run: make build-all 0707010000000A000081A40000000000000000000000016733442800000264000000000000000000000000000000000000003200000000k0sctl-0.19.4/.github/workflows/golangci-lint.ymlname: Lint (golangci-lint) on: pull_request: paths: - '**.go' jobs: golangci-lint: name: Run golangci-lint runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Run golangci-lint uses: golangci/golangci-lint-action@v4 with: version: latest skip-cache: true only-new-issues: false args: --verbose --timeout=10m 0707010000000B000081A40000000000000000000000016733442800000211000000000000000000000000000000000000002F00000000k0sctl-0.19.4/.github/workflows/gomod-lint.ymlname: Lint (go.mod/go.sum) on: pull_request: paths: - 'go.mod' - 'go.sum' jobs: gomod-lint: name: Validate go module file consistency runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Check go.mod/go.sum to be consistent run: go mod tidy -v && git diff --exit-code 0707010000000C000081A400000000000000000000000167334428000004F4000000000000000000000000000000000000002D00000000k0sctl-0.19.4/.github/workflows/release.yamlname: Release on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: release: name: release runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Tag name id: tag-name run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Build binaries id: build_bins env: SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }} TAG_NAME: ${{ steps.tag-name.outputs.tag }} run: make build-all - name: Create release and upload binaries uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: | bin/k0sctl-* bin/checksums.txt body_path: bin/checksums.md tag_name: ${{ steps.tag-name.outputs.tag }} name: ${{ steps.tag-name.outputs.tag }} draft: true # So we can manually edit before publishing prerelease: ${{ contains(steps.tag-name.outputs.tag, '-') }} # v0.1.2-beta1, 1.2.3-rc1 0707010000000D000081A40000000000000000000000016733442800001C0D000000000000000000000000000000000000002A00000000k0sctl-0.19.4/.github/workflows/smoke.ymlname: Smoke tests on: pull_request: paths: - '**.go' - go.mod - go.sum - Makefile - .github/workflows/smoke.yml - smoke-test/** jobs: build: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Build run: make k0sctl - name: Stash the compiled binary for further testing uses: actions/upload-artifact@v4 with: name: k0sctl path: k0sctl retention-days: 2 smoke-basic: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 - quay.io/k0sproject/bootloose-amazonlinux2023 - quay.io/k0sproject/bootloose-debian12 - quay.io/k0sproject/bootloose-fedora38 - quay.io/k0sproject/bootloose-rockylinux9 - quay.io/k0sproject/bootloose-ubuntu20.04 name: Basic 1+1 smoke needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-basic smoke-basic-rootless: strategy: matrix: image: - quay.io/k0sproject/bootloose-debian12 - quay.io/k0sproject/bootloose-ubuntu20.04 name: Basic 1+1 smoke (regular user login) needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-basic-rootless smoke-basic-idlike: name: Basic 1+1 smoke (ID_LIKE fallback) needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Build image run: | make -C smoke-test kalilinux.iid echo "LINUX_IMAGE=$(cat smoke-test/kalilinux.iid)" >> "$GITHUB_ENV" - name: Run smoke tests run: make smoke-basic smoke-basic-openssh: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 name: Basic 1+1 smoke using openssh client needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-basic-openssh smoke-files: strategy: matrix: image: - quay.io/k0sproject/bootloose-ubuntu20.04 - quay.io/k0sproject/bootloose-alpine3.18 name: Basic file upload smoke needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests run: make smoke-files smoke-dynamic: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 name: Basic dynamic config smoke needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests run: make smoke-dynamic smoke-os-override: name: OS override smoke test needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run OS override smoke test run: make smoke-os-override smoke-downloadurl: name: k0sDownloadURL smoke test needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run k0sDownloadURL smoke test run: make smoke-downloadurl smoke-upgrade: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 - quay.io/k0sproject/bootloose-amazonlinux2023 - quay.io/k0sproject/bootloose-rockylinux9 - quay.io/k0sproject/bootloose-ubuntu20.04 k0s_from: - v1.21.6+k0s.0 name: Upgrade needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} K0S_FROM: ${{ matrix.k0s_from }} run: make smoke-upgrade smoke-dryrun: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 - quay.io/k0sproject/bootloose-ubuntu20.04 k0s_from: - v1.21.6+k0s.0 name: Dry run needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} K0S_FROM: ${{ matrix.k0s_from }} run: make smoke-dryrun smoke-reset: strategy: matrix: image: - quay.io/k0sproject/bootloose-rockylinux9 - quay.io/k0sproject/bootloose-ubuntu20.04 name: Apply + reset needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-reset smoke-backup-restore: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 - quay.io/k0sproject/bootloose-rockylinux9 - quay.io/k0sproject/bootloose-ubuntu20.04 name: Apply + backup + reset + restore needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-backup-restore smoke-controller-swap: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 name: Controller swap needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-controller-swap smoke-reinstall: strategy: matrix: image: - quay.io/k0sproject/bootloose-alpine3.18 - quay.io/k0sproject/bootloose-ubuntu20.04 name: Reinstall (modify install flags) needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run smoke tests env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-reinstall smoke-init: name: Init sub-command smoke test needs: build runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/smoke-test-cache - name: Run init smoke test run: make smoke-init 0707010000000E000081A4000000000000000000000001673344280000000C000000000000000000000000000000000000001900000000k0sctl-0.19.4/.gitignorebin/ k0sctl 0707010000000F000081A40000000000000000000000016733442800002E10000000000000000000000000000000000000001600000000k0sctl-0.19.4/LICENSEPortions of this software are licensed as follows: * All content residing under the "docs/" directory of this repository is licensed under "Creative Commons Attribution Share Alike 4.0 International" (CC-BY-SA-4.0). See docs/LICENCE for details. * Content outside of the above mentioned directories or restrictions above is available under the "Apache License 2.0" as defined below. The k0sctl binary will send anonymous telemetry when used. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023, k0sctl authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 07070100000010000081A40000000000000000000000016733442800000ADC000000000000000000000000000000000000001700000000k0sctl-0.19.4/MakefileGO_SRCS := $(shell find . -type f -name '*.go' -a ! \( -name 'zz_generated*' -o -name '*_test.go' \)) GO_TESTS := $(shell find . -type f -name '*_test.go') TAG_NAME = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) GIT_COMMIT = $(shell git rev-parse --short=7 HEAD) ifdef TAG_NAME ENVIRONMENT = production endif ENVIRONMENT ?= development PREFIX = /usr/local LD_FLAGS = -s -w -X github.com/k0sproject/k0sctl/version.Environment=$(ENVIRONMENT) -X github.com/carlmjohnson/versioninfo.Revision=$(GIT_COMMIT) -X github.com/carlmjohnson/versioninfo.Version=$(TAG_NAME) BUILD_FLAGS = -trimpath -a -tags "netgo,osusergo,static_build" -installsuffix netgo -ldflags "$(LD_FLAGS) -extldflags '-static'" k0sctl: $(GO_SRCS) go build $(BUILD_FLAGS) -o k0sctl main.go bin/k0sctl-linux-amd64: $(GO_SRCS) GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o bin/k0sctl-linux-amd64 main.go bin/k0sctl-linux-arm64: $(GO_SRCS) GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o bin/k0sctl-linux-arm64 main.go bin/k0sctl-linux-arm: $(GO_SRCS) GOOS=linux GOARCH=arm CGO_ENABLED=0 go build $(BUILD_FLAGS) -o bin/k0sctl-linux-arm main.go bin/k0sctl-win-amd64.exe: $(GO_SRCS) GOOS=windows GOARCH=amd64 go build $(BUILD_FLAGS) -o bin/k0sctl-win-amd64.exe main.go bin/k0sctl-darwin-amd64: $(GO_SRCS) GOOS=darwin GOARCH=amd64 go build $(BUILD_FLAGS) -o bin/k0sctl-darwin-amd64 main.go bin/k0sctl-darwin-arm64: $(GO_SRCS) GOOS=darwin GOARCH=arm64 go build $(BUILD_FLAGS) -o bin/k0sctl-darwin-arm64 main.go bins := k0sctl-linux-amd64 k0sctl-linux-arm64 k0sctl-linux-arm k0sctl-win-amd64.exe k0sctl-darwin-amd64 k0sctl-darwin-arm64 bin/checksums.txt: $(addprefix bin/,$(bins)) sha256sum -b $(addprefix bin/,$(bins)) | sed 's/bin\///' > $@ bin/checksums.md: bin/checksums.txt @echo "### SHA256 Checksums" > $@ @echo >> $@ @echo "\`\`\`" >> $@ @cat $< >> $@ @echo "\`\`\`" >> $@ .PHONY: build-all build-all: $(addprefix bin/,$(bins)) bin/checksums.md .PHONY: clean clean: rm -rf bin/ k0sctl smoketests := smoke-basic smoke-basic-rootless smoke-files smoke-upgrade smoke-reset smoke-os-override smoke-init smoke-backup-restore smoke-dynamic smoke-basic-openssh smoke-dryrun smoke-downloadurl smoke-controller-swap smoke-reinstall .PHONY: $(smoketests) $(smoketests): k0sctl $(MAKE) -C smoke-test $@ golint := $(shell which golangci-lint 2>/dev/null) ifeq ($(golint),) golint := $(shell go env GOPATH)/bin/golangci-lint endif $(golint): go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest .PHONY: lint lint: $(golint) $(golint) run ./... .PHONY: test test: $(GO_SRCS) $(GO_TESTS) go test -v ./... .PHONY: install install: k0sctl install -d $(DESTDIR)$(PREFIX)/bin/ install -m 755 k0sctl $(DESTDIR)$(PREFIX)/bin/ 07070100000011000081A40000000000000000000000016733442800005143000000000000000000000000000000000000001800000000k0sctl-0.19.4/README.md# k0sctl *A command-line bootstrapping and management tool for [k0s zero friction kubernetes](https://k0sproject.io/) clusters.* - [Installation](#installation) - [Development status](#development-status) - [Usage](#usage) - [Configuration](#configuration-file) Example output of k0sctl deploying a k0s cluster: ```text INFO ==> Running phase: Connect to hosts INFO ==> Running phase: Detect host operating systems INFO [ssh] 10.0.0.1:22: is running Ubuntu 20.10 INFO [ssh] 10.0.0.2:22: is running Ubuntu 20.10 INFO ==> Running phase: Prepare hosts INFO ==> Running phase: Gather host facts INFO [ssh] 10.0.0.1:22: discovered 10.12.18.133 as private address INFO ==> Running phase: Validate hosts INFO ==> Running phase: Gather k0s facts INFO ==> Running phase: Download k0s binaries on hosts INFO ==> Running phase: Configure k0s INFO ==> Running phase: Initialize the k0s cluster INFO [ssh] 10.0.0.1:22: installing k0s controller INFO ==> Running phase: Install workers INFO [ssh] 10.0.0.1:22: generating token INFO [ssh] 10.0.0.2:22: installing k0s worker INFO [ssh] 10.0.0.2:22: waiting for node to become ready INFO ==> Running phase: Disconnect from hosts INFO ==> Finished in 2m2s INFO k0s cluster version 1.22.3+k0s.0 is now installed INFO Tip: To access the cluster you can now fetch the admin kubeconfig using: INFO k0sctl kubeconfig ``` You can find example Terraform and [bootloose](https://github.com/k0sproject/bootloose) configurations in the [examples/](examples/) directory. ## Installation ### Install from the released binaries Download the desired version for your operating system and processor architecture from the [k0sctl releases page](https://github.com/k0sproject/k0sctl/releases). Make the file executable and place it in a directory available in your `$PATH`. As the released binaries aren't signed yet, on macOS and Windows, you must first run the executable via "Open" in the context menu and allow running it. ### Install from the sources If you have a working Go toolchain, you can use `go install` to install k0sctl to your `$GOPATH/bin`. ```sh go install github.com/k0sproject/k0sctl@latest ``` ### Package managers #### [Homebrew](https://brew.sh/) (macOS, Linux) ```sh brew install k0sproject/tap/k0sctl ``` #### [Chocolatey](https://chocolatey.org/) (Windows) Note: The [chocolatey package](https://community.chocolatey.org/packages/k0sctl) is community maintained, any issues should be reported to the maintainer of the package. ```sh choco install k0sctl ``` #### Shell auto-completions ##### Bash ```sh k0sctl completion > /etc/bash_completion.d/k0sctl ``` ##### Zsh ```sh k0sctl completion > /usr/local/share/zsh/site-functions/_k0sctl ``` ##### Fish ```sh k0sctl completion > ~/.config/fish/completions/k0sctl.fish ``` ## Anonymous telemetry K0sctl sends anonymized telemetry data when it is used. This can be disabled via the `--disable-telemetry` flag or by setting the environment variable `DISABLE_TELEMETRY=true`. The telemetry data includes: - K0sctl version - Operating system + CPU architecture ("linux x86", "darwin arm64", ...) - An anonymous machine ID generated by [denisbrodbeck/machineid](https://github.com/denisbrodbeck/machineid) or if that fails, an md5 sum of the hostname - Event information: * Phase name ("Connecting to hosts", "Gathering facts", ...) and the duration how long it took to finish * Cluster UUID (`kubectl get -n kube-system namespace kube-system -o template={{.metadata.uid}}`) * Was k0s dynamic config enabled (true/false) * Was a custom or the default k0s configuration used (true/false) * In case of a crash, a backtrace with source filenames and line numbers only The data is used to estimate the number of users and to identify failure hotspots. ## Development status K0sctl is ready for use and in continuous development. ## Usage ### `k0sctl apply` The main function of k0sctl is the `k0sctl apply` subcommand. Provided a configuration file describing the desired cluster state, k0sctl will connect to the listed hosts, determines the current state of the hosts and configures them as needed to form a k0s cluster. The default location for the configuration file is `k0sctl.yaml` in the current working directory. To load a configuration from a different location, use: ```sh k0sctl apply --config path/to/k0sctl.yaml ``` If the configuration cluster version `spec.k0s.version` is greater than the version detected on the cluster, a cluster upgrade will be performed. If the configuration lists hosts that are not part of the cluster, they will be configured to run k0s and will be joined to the cluster. ### `k0sctl init` Generate a configuration template. Use `--k0s` to include an example `spec.k0s.config` k0s configuration block. You can also supply a list of host addresses via arguments or stdin. Output a minimal configuration template: ```sh k0sctl init > k0sctl.yaml ``` Output an example configuration with a default k0s config: ```sh k0sctl init --k0s > k0sctl.yaml ``` Create a configuration from a list of host addresses and pipe it to k0sctl apply: ```sh k0sctl init 10.0.0.1 10.0.0.2 ubuntu@10.0.0.3:8022 | k0sctl apply --config - ``` ### `k0sctl backup & restore` Takes a [backup](https://docs.k0sproject.io/stable/backup/) of the cluster control plane state into the current working directory. The files are currently named with a running (unix epoch) timestamp, e.g. `k0s_backup_1623220591.tar.gz`. Restoring a backup can be done as part of the [k0sctl apply](#k0sctl-apply) command using `--restore-from k0s_backup_1623220591.tar.gz` flag. Restoring the cluster state is a full restoration of the cluster control plane state, including: - Etcd datastore content - Certificates - Keys In general restore is intended to be used as a disaster recovery mechanism and thus it expects that no k0s components actually exist on the controllers. Known limitations in the current restore process: - The control plane address (`externalAddress`) needs to remain the same between backup and restore. This is caused by the fact that all worker node components connect to this address and cannot currently be re-configured. ### `k0sctl reset` Uninstall k0s from the hosts listed in the configuration. ### `k0sctl kubeconfig` Connects to the cluster and outputs a kubeconfig file that can be used with `kubectl` or `kubeadm` to manage the kubernetes cluster. Example: ```sh $ k0sctl kubeconfig --config path/to/k0sctl.yaml > k0s.config $ kubectl get node --kubeconfig k0s.config NAME STATUS ROLES AGE VERSION worker0 NotReady <none> 10s v1.20.2-k0s1 ``` ## Configuration file The configuration file is in YAML format and loosely resembles the syntax used in Kubernetes. YAML anchors and aliases can be used. To generate a simple skeleton configuration file, you can use the `k0sctl init` subcommand. Configuration example: ```yaml apiVersion: k0sctl.k0sproject.io/v1beta1 kind: Cluster metadata: name: my-k0s-cluster spec: hosts: - role: controller installFlags: - --debug ssh: address: 10.0.0.1 user: root port: 22 keyPath: ~/.ssh/id_rsa - role: worker installFlags: - --debug ssh: address: 10.0.0.2 k0s: version: 0.10.0 config: apiVersion: k0s.k0sproject.io/v1beta1 kind: ClusterConfig metadata: name: my-k0s-cluster spec: images: calico: cni: image: calico/cni version: v3.16.2 ``` ### Environment variable substitution Simple bash-like expressions are supported in the configuration for environment variable substition. - `$VAR` or `${VAR}` value of `VAR` environment variable - `${var:-DEFAULT_VALUE}` will use `VAR` if non-empty, otherwise `DEFAULT_VALUE` - `$$var` - escape, result will be `$var`. - And [several other expressions](https://github.com/a8m/envsubst#docs) ### Configuration Header Fields ###### `apiVersion` <string> (required) The configuration file syntax version. Currently the only supported version is `k0sctl.k0sproject.io/v1beta1`. ###### `kind` <string> (required) In the future, some of the configuration APIs can support multiple types of objects. For now, the only supported kind is `Cluster`. ###### `spec` <mapping> (required) The main object definition, see [below](#spec-fields) ###### `metadata` <mapping> (optional) Information that can be used to uniquely identify the object. Example: ```yaml metadata: name: k0s-cluster-name ``` ### Spec Fields ##### `spec.hosts` <sequence> (required) A list of cluster hosts. Host requirements: * Currently only linux targets are supported * The user must either be root or have passwordless `sudo` access. * The host must fulfill the k0s system requirements See [host object documentation](#host-fields) below. ##### `spec.k0s` <mapping> (optional) Settings related to the k0s cluster. See [k0s object documentation](#k0s-fields) below. ### Host Fields ###### `spec.hosts[*].role` <string> (required) One of: - `controller` - a controller host - `controller+worker` - a controller host that will also run workloads - `single` - a [single-node cluster](https://docs.k0sproject.io/stable/k0s-single-node/) host, the configuration can only contain one host - `worker` - a worker host ###### `spec.hosts[*].noTaints` <boolean> (optional) (default: `false`) When `true` and used in conjuction with the `controller+worker` role, the default taints are disabled making regular workloads schedulable on the node. By default, k0s sets a node-role.kubernetes.io/master:NoSchedule taint on controller+worker nodes and only workloads with toleration for it will be scheduled. ###### `spec.hosts[*].uploadBinary` <boolean> (optional) (default: `false`) When `true`, the k0s binaries for target host will be downloaded and cached on the local host and uploaded to the target. When `false`, the k0s binary downloading is performed on the target host itself ###### `spec.hosts[*].k0sBinaryPath` <string> (optional) A path to a file on the local host that contains a k0s binary to be uploaded to the host. Can be used to test drive a custom development build of k0s. ###### `spec.hosts[*].k0sDownloadURL` <string> (optional) A URL to download the k0s binary from. The default is to download from the [k0s repository](https://github.com/k0sproject/k0s). The URL can contain '%'-prefixed tokens that will be replaced with the host's information, see [tokens](#tokens). ###### `spec.hosts[*].hostname` <string> (optional) Override host's hostname. When not set, the hostname reported by the operating system is used. ###### `spec.hosts[*].dataDir` <string> (optional) (default: `/var/lib/k0s`) Set host's k0s data-dir. ###### `spec.hosts[*].installFlags` <sequence> (optional) Extra flags passed to the `k0s install` command on the target host. See `k0s install --help` for a list of options. ###### `spec.hosts[*].environment` <mapping> (optional) List of key-value pairs to set to the target host's environment variables. Example: ```yaml environment: HTTP_PROXY: 10.0.0.1:443 ``` ###### `spec.hosts[*].files` <sequence> (optional) List of files to be uploaded to the host. Example: ```yaml - name: image-bundle src: airgap-images.tgz dstDir: /var/lib/k0s/images/ perm: 0600 ``` * `name`: name of the file "bundle", used only for logging purposes (optional) * `src`: File path, an URL or [Glob pattern](https://golang.org/pkg/path/filepath/#Match) to match files to be uploaded. URL sources will be directly downloaded using the target host. If the value is a URL, '%'-prefixed tokens can be used, see [tokens](#tokens). (required) * `dstDir`: Destination directory for the file(s). `k0sctl` will create full directory structure if it does not already exist on the host (default: user home) * `dst`: Destination filename for the file. Only usable for single file uploads (default: basename of file) * `perm`: File permission mode for uploaded file(s) (default: same as local) * `dirPerm`: Directory permission mode for created directories (default: 0755) * `user`: User name of file/directory owner, must exist on the host (optional) * `group`: Group name of file/directory owner, must exist on the host (optional) ###### `spec.hosts[*].hooks` <mapping> (optional) Run a set of commands on the remote host during k0sctl operations. Example: ```yaml hooks: apply: before: - date >> k0sctl-apply.log after: - echo "apply success" >> k0sctl-apply.log ``` The currently available "hook points" are: * `apply`: Runs during `k0sctl apply` - `before`: Runs after configuration and host validation, right before configuring k0s on the host - `after`: Runs before disconnecting from the host after a successful apply operation * `backup`: Runs during `k0s backup` - `before`: Runs before k0sctl runs the `k0s backup` command - `after`: Runs before disconnecting from the host after successfully taking a backup * `reset`: Runs during `k0sctl reset` - `before`: Runs after gathering information about the cluster, right before starting to remove the k0s installation. - `after`: Runs before disconnecting from the host after a successful reset operation ##### `spec.hosts[*].os` <string> (optional) (default: ``) Override OS distribution auto-detection. By default `k0sctl` detects the OS by reading `/etc/os-release` or `/usr/lib/os-release` files. In case your system is based on e.g. Debian but the OS release info has something else configured you can override `k0sctl` to use Debian based functionality for the node with: ```yaml - role: worker os: debian ssh: address: 10.0.0.2 ``` ##### `spec.hosts[*].privateInterface` <string> (optional) (default: ``) Override private network interface selected by host fact gathering. Useful in case fact gathering picks the wrong private network interface. ```yaml - role: worker os: debian privateInterface: eth1 ``` ##### `spec.hosts[*].privateAddress` <string> (optional) (default: ``) Override private IP address selected by host fact gathering. Useful in case fact gathering picks the wrong IPAddress. ```yaml - role: worker os: debian privateAddress: 10.0.0.2 ``` ##### `spec.hosts[*].ssh` <mapping> (optional) SSH connection options. Example: ```yaml spec: hosts: - role: controller ssh: address: 10.0.0.2 user: ubuntu keyPath: ~/.ssh/id_rsa ``` It's also possible to tunnel connections through a bastion host. The bastion configuration has all the same fields as any SSH connection: ```yaml spec: hosts: - role: controller ssh: address: 10.0.0.2 user: ubuntu keyPath: ~/.ssh/id_rsa bastion: address: 10.0.0.1 user: root keyPath: ~/.ssh/id_rsa2 ``` SSH agent and auth forwarding are also supported, a host without a keyfile: ```yaml spec: hosts: - role: controller ssh: address: 10.0.0.2 user: ubuntu ``` ```shell $ ssh-add ~/.ssh/aws.pem $ ssh -A user@jumphost user@jumphost ~ $ k0sctl apply ``` Pageant or openssh-agent can be used on Windows. ###### `spec.hosts[*].ssh.address` <string> (required) IP address of the host ###### `spec.hosts[*].ssh.user` <string> (optional) (default: `root`) Username to log in as. ###### `spec.hosts[*].ssh.port` <number> (required) TCP port of the SSH service on the host. ###### `spec.hosts[*].ssh.keyPath` <string> (optional) (default: `~/.ssh/identity ~/.ssh/id_rsa ~/.ssh/id_dsa`) Path to an SSH key file. If a public key is used, ssh-agent is required. When left empty, the default value will first be looked for from the ssh configuration (default `~/.ssh/config`) `IdentityFile` parameter. ##### `spec.hosts[*].localhost` <mapping> (optional) Localhost connection options. Can be used to use the local host running k0sctl as a node in the cluster. ###### `spec.hosts[*].localhost.enabled` <boolean> (optional) (default: `false`) This must be set `true` to enable the localhost connection. ##### `spec.hosts[*].openSSH` <mapping> (optional) An alternative SSH client protocol that uses the system's openssh client for connections. Example: ```yaml spec: hosts: - role: controller openSSH: address: 10.0.0.2 ``` The only required field is the `address` and it can also be a hostname that is found in the ssh config. All other options such as user, port and keypath will use the same defaults as if running `ssh` from the command-line or will use values found from the ssh config. An example SSH config: ``` Host controller1 Hostname 10.0.0.1 Port 2222 IdentityFile ~/.ssh/id_cluster_esa ``` If this is in your `~/.ssh/config`, you can simply use the host alias as the address in your k0sctl config: ```yaml spec: hosts: - role: controller openSSH: address: controller1 # if the ssh configuration is in a different file, you can use: # configPath: /path/to/config ``` ###### `spec.hosts[*].openSSH.address` <string> (required) IP address, hostname or ssh config host alias of the host ###### `spec.hosts[*].openSSH.user` <string> (optional) Username to connect as. ###### `spec.hosts[*].openSSH.port` <number> (optional) Remote port. ###### `spec.hosts[*].openSSH.keyPath` <string> (optional) Path to private key. ###### `spec.hosts[*].openSSH.configPath` <string> (optional) Path to ssh config, defaults to ~/.ssh/config with fallback to /etc/ssh/ssh_config. ###### `spec.hosts[*].openSSH.disableMultiplexing` <boolean> (optional) The default mode of operation is to use connection multiplexing where a ControlMaster connection is opened and the subsequent connections to the same host use the master connection over a socket to communicate to the host. If this is disabled by setting `disableMultiplexing: true`, running every remote command will require reconnecting and reauthenticating to the host. ###### `spec.hosts[*].openSSH.options` <mapping> (optional) Additional options as key/value pairs to use when running the ssh client. Example: ```yaml openSSH: address: host options: ForwardAgent: true # -o ForwardAgent=yes StrictHostkeyChecking: false # -o StrictHostkeyChecking: no ``` ###### `spec.hosts[*].reset` <boolean> (optional) (default: `false`) If set to `true` k0sctl will remove the node from kubernetes and reset k0s on the host. ### K0s Fields ##### `spec.k0s.version` <string> (optional) (default: auto-discovery) The version of k0s to deploy. When left out, k0sctl will default to using the latest released version of k0s or the version already running on the cluster. ##### `spec.k0s.versionChannel` <string> (optional) (default: `stable`) Possible values are `stable` and `latest`. When `spec.k0s.version` is left undefined, this setting can be set to `latest` to allow k0sctl to include k0s pre-releases when looking for the latest version. The default is to only look for stable releases. ##### `spec.k0s.dynamicConfig` <boolean> (optional) (default: false) Enable k0s dynamic config. The setting will be automatically set to true if: * Any controller node has `--enable-dynamic-config` in `installFlags` * Any existing controller node has `--enable-dynamic-config` in run arguments (`k0s status -o json`) **Note:** When running k0s in dynamic config mode, k0sctl will ONLY configure the cluster-wide configuration during the first time initialization, after that the configuration has to be managed via `k0s config edit` or `k0sctl config edit`. The node specific configuration will be updated on each apply. See also: * [k0s Dynamic Configuration](https://docs.k0sproject.io/stable/dynamic-configuration/) ##### `spec.k0s.config` <mapping> (optional) (default: auto-generated) Embedded k0s cluster configuration. See [k0s configuration documentation](https://docs.k0sproject.io/stable/configuration/) for details. When left out, the output of `k0s config create` will be used. #### Tokens The following tokens can be used in the `k0sDownloadURL` and `files.[*].src` fields: - `%%` - literal `%` - `%p` - host architecture (arm, arm64, amd64) - `%v` - k0s version (v1.21.0+k0s.0) - `%x` - k0s binary extension (currently always empty) Any other tokens will be output as-is including the `%` character. Example: ```yaml - role: controller k0sDownloadURL: https://files.example.com/k0s%20files/k0s-%v-%p%x # Expands to https://files.example.com/k0s%20files/k0s-v1.21.0+k0s.0-amd64 ``` 07070100000012000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001500000000k0sctl-0.19.4/action07070100000013000081A40000000000000000000000016733442800001466000000000000000000000000000000000000001E00000000k0sctl-0.19.4/action/apply.gopackage action import ( "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" log "github.com/sirupsen/logrus" ) type ApplyOptions struct { // Manager is the phase manager Manager *phase.Manager // DisableDowngradeCheck skips the downgrade check DisableDowngradeCheck bool // Force allows forced installation in case of certain failures Force bool // NoWait skips waiting for the cluster to be ready NoWait bool // NoDrain skips draining worker nodes NoDrain bool // RestoreFrom is the path to a cluster backup archive to restore the state from RestoreFrom string // KubeconfigOut is a writer to write the kubeconfig to KubeconfigOut io.Writer // KubeconfigAPIAddress is the API address to use in the kubeconfig KubeconfigAPIAddress string // ConfigPath is the path to the configuration file (used for kubeconfig command tip on success) ConfigPath string } type Apply struct { ApplyOptions Phases phase.Phases } // NewApply creates a new Apply action. The list of phases can be modified via the Phases field, for example: // // apply := NewApply(opts) // gatherK0sFacts := &phase.GatherK0sFacts{} // advisable to get the title from the phase itself instead of hardcoding the title. // apply.Phases.InsertBefore(gatherK0sFacts.Title(), &myCustomPhase{}) // insert a custom phase before the GatherK0sFacts phase func NewApply(opts ApplyOptions) *Apply { lockPhase := &phase.Lock{} unlockPhase := lockPhase.UnlockPhase() apply := &Apply{ ApplyOptions: opts, Phases: phase.Phases{ &phase.DefaultK0sVersion{}, &phase.Connect{}, &phase.DetectOS{}, lockPhase, &phase.PrepareHosts{}, &phase.GatherFacts{}, &phase.ValidateHosts{}, &phase.GatherK0sFacts{}, &phase.ValidateFacts{SkipDowngradeCheck: opts.DisableDowngradeCheck}, &phase.ValidateEtcdMembers{}, // if UploadBinaries: true &phase.DownloadBinaries{}, // downloads k0s binaries to local cache &phase.UploadK0s{}, // uploads k0s binaries to hosts from cache // if UploadBinaries: false &phase.DownloadK0s{}, // downloads k0s binaries directly from hosts &phase.UploadFiles{}, &phase.InstallBinaries{}, &phase.PrepareArm{}, &phase.ConfigureK0s{}, &phase.Restore{ RestoreFrom: opts.RestoreFrom, }, &phase.RunHooks{Stage: "before", Action: "apply"}, &phase.InitializeK0s{}, &phase.InstallControllers{}, &phase.InstallWorkers{}, &phase.UpgradeControllers{}, &phase.UpgradeWorkers{NoDrain: opts.NoDrain}, &phase.Reinstall{}, &phase.ResetWorkers{NoDrain: opts.NoDrain}, &phase.ResetControllers{NoDrain: opts.NoDrain}, &phase.RunHooks{Stage: "after", Action: "apply"}, unlockPhase, &phase.Disconnect{}, }, } if opts.KubeconfigOut != nil { apply.Phases.InsertBefore(unlockPhase.Title(), &phase.GetKubeconfig{APIAddress: opts.KubeconfigAPIAddress}) } return apply } // Run the Apply action func (a Apply) Run() error { if len(a.Phases) == 0 { // for backwards compatibility with the old Apply struct without NewApply(..) tmpApply := NewApply(a.ApplyOptions) a.Phases = tmpApply.Phases } start := time.Now() phase.NoWait = a.NoWait phase.Force = a.Force a.Manager.SetPhases(a.Phases) analytics.Client.Publish("apply-start", map[string]interface{}{}) var result error if result = a.Manager.Run(); result != nil { analytics.Client.Publish("apply-failure", map[string]interface{}{"clusterID": a.Manager.Config.Spec.K0s.Metadata.ClusterID}) log.Info(phase.Colorize.Red("==> Apply failed").String()) return result } analytics.Client.Publish("apply-success", map[string]interface{}{"duration": time.Since(start), "clusterID": a.Manager.Config.Spec.K0s.Metadata.ClusterID}) if a.KubeconfigOut != nil { if _, err := a.KubeconfigOut.Write([]byte(a.Manager.Config.Metadata.Kubeconfig)); err != nil { log.Warnf("failed to write kubeconfig to %s: %v", a.KubeconfigOut, err) } } duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) log.Info(phase.Colorize.Green(text).String()) for _, host := range a.Manager.Config.Spec.Hosts { if host.Reset { log.Info("There were nodes that got uninstalled during the apply phase. Please remove them from your k0sctl config file") break } } if !a.Manager.DryRun { log.Infof("k0s cluster version %s is now installed", a.Manager.Config.Spec.K0s.Version) } if a.KubeconfigOut == nil { cmd := &strings.Builder{} executable, err := os.Executable() if err != nil { executable = "k0sctl" } else { // check if the basename of executable is in the PATH, if so, just use the basename if _, err := exec.LookPath(filepath.Base(executable)); err == nil { executable = filepath.Base(executable) } } cmd.WriteString(executable) cmd.WriteString(" kubeconfig") if a.ConfigPath != "" && a.ConfigPath != "-" && a.ConfigPath != "k0sctl.yaml" { cmd.WriteString(" --config ") cmd.WriteString(a.ConfigPath) } log.Info("Tip: To access the cluster you can now fetch the admin kubeconfig using:") log.Info(" " + phase.Colorize.Cyan(cmd.String()).String()) } return nil } 07070100000014000081A400000000000000000000000167334428000004F4000000000000000000000000000000000000001F00000000k0sctl-0.19.4/action/backup.gopackage action import ( "fmt" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" log "github.com/sirupsen/logrus" ) type Backup struct { // Manager is the phase manager Manager *phase.Manager } func (b Backup) Run() error { start := time.Now() lockPhase := &phase.Lock{} b.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, lockPhase, &phase.PrepareHosts{}, &phase.GatherFacts{SkipMachineIDs: true}, &phase.GatherK0sFacts{}, &phase.RunHooks{Stage: "before", Action: "backup"}, &phase.Backup{}, &phase.RunHooks{Stage: "after", Action: "backup"}, &phase.Unlock{Cancel: lockPhase.Cancel}, &phase.Disconnect{}, ) analytics.Client.Publish("backup-start", map[string]interface{}{}) if err := b.Manager.Run(); err != nil { analytics.Client.Publish("backup-failure", map[string]interface{}{"clusterID": b.Manager.Config.Spec.K0s.Metadata.ClusterID}) return err } analytics.Client.Publish("backup-success", map[string]interface{}{"duration": time.Since(start), "clusterID": b.Manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) log.Info(phase.Colorize.Green(text).String()) return nil } 07070100000015000081A400000000000000000000000167334428000008CB000000000000000000000000000000000000002400000000k0sctl-0.19.4/action/config_edit.gopackage action import ( "fmt" "io" "os" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/rig/exec" osexec "os/exec" "github.com/mattn/go-isatty" ) type ConfigEdit struct { Config *v1beta1.Cluster Stdout io.Writer Stderr io.Writer Stdin io.Reader } func (c ConfigEdit) Run() error { stdoutFile, ok := c.Stdout.(*os.File) if !ok || !isatty.IsTerminal(stdoutFile.Fd()) { return fmt.Errorf("output is not a terminal") } analytics.Client.Publish("config-edit-start", map[string]interface{}{}) editor, err := shellEditor() if err != nil { return err } h := c.Config.Spec.K0sLeader() if err := h.Connect(); err != nil { return fmt.Errorf("failed to connect: %w", err) } defer h.Disconnect() if err := h.ResolveConfigurer(); err != nil { return err } oldCfg, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get clusterconfig k0s -o yaml"), exec.Sudo(h)) if err != nil { return fmt.Errorf("%s: %w", h, err) } tmpFile, err := os.CreateTemp("", "k0s-config.*.yaml") if err != nil { return err } defer func() { _ = os.Remove(tmpFile.Name()) }() if _, err := tmpFile.WriteString(oldCfg); err != nil { return err } if err := tmpFile.Close(); err != nil { return err } cmd := osexec.Command(editor, tmpFile.Name()) cmd.Stdin = c.Stdin cmd.Stdout = c.Stdout cmd.Stderr = c.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to start editor (%s): %w", cmd.String(), err) } newCfgBytes, err := os.ReadFile(tmpFile.Name()) if err != nil { return err } newCfg := string(newCfgBytes) if newCfg == oldCfg { return fmt.Errorf("configuration was not changed, aborting") } if err := h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "apply -n kube-system -f -"), exec.Stdin(newCfg), exec.Sudo(h)); err != nil { return err } return nil } func shellEditor() (string, error) { if v := os.Getenv("VISUAL"); v != "" { return v, nil } if v := os.Getenv("EDITOR"); v != "" { return v, nil } if path, err := osexec.LookPath("vi"); err == nil { return path, nil } return "", fmt.Errorf("could not detect shell editor ($VISUAL, $EDITOR)") } 07070100000016000081A400000000000000000000000167334428000003CB000000000000000000000000000000000000002600000000k0sctl-0.19.4/action/config_status.gopackage action import ( "fmt" "io" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/rig/exec" ) type ConfigStatus struct { Config *v1beta1.Cluster Concurrency int Format string Writer io.Writer } func (c ConfigStatus) Run() error { analytics.Client.Publish("config-status-start", map[string]interface{}{}) h := c.Config.Spec.K0sLeader() if err := h.Connect(); err != nil { return fmt.Errorf("failed to connect: %w", err) } defer h.Disconnect() if err := h.ResolveConfigurer(); err != nil { return err } format := c.Format if format != "" { format = "-o " + format } output, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get event --field-selector involvedObject.name=k0s %s", format), exec.Sudo(h)) if err != nil { return fmt.Errorf("%s: %w", h, err) } fmt.Fprintln(c.Writer, output) return nil } 07070100000017000081A400000000000000000000000167334428000002BB000000000000000000000000000000000000002300000000k0sctl-0.19.4/action/kubeconfig.gopackage action import ( "github.com/k0sproject/k0sctl/phase" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) type Kubeconfig struct { // Manager is the phase manager Manager *phase.Manager KubeconfigAPIAddress string Kubeconfig string } func (k *Kubeconfig) Run() error { // Change so that the internal config has only single controller host as we // do not need to connect to all nodes k.Manager.Config.Spec.Hosts = cluster.Hosts{k.Manager.Config.Spec.K0sLeader()} k.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, &phase.GetKubeconfig{APIAddress: k.KubeconfigAPIAddress}, &phase.Disconnect{}, ) return k.Manager.Run() } 07070100000018000081A4000000000000000000000001673344280000081F000000000000000000000000000000000000001E00000000k0sctl-0.19.4/action/reset.gopackage action import ( "fmt" "io" "os" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" log "github.com/sirupsen/logrus" "github.com/AlecAivazis/survey/v2" "github.com/mattn/go-isatty" ) type Reset struct { // Manager is the phase manager Manager *phase.Manager Stdout io.Writer Force bool } func (r Reset) Run() error { if !r.Force { if stdoutFile, ok := r.Stdout.(*os.File); ok && !isatty.IsTerminal(stdoutFile.Fd()) { return fmt.Errorf("reset requires --force") } confirmed := false prompt := &survey.Confirm{ Message: "Going to reset all of the hosts, which will destroy all configuration and data, Are you sure?", } _ = survey.AskOne(prompt, &confirmed) if !confirmed { return fmt.Errorf("confirmation or --force required to proceed") } } start := time.Now() for _, h := range r.Manager.Config.Spec.Hosts { h.Reset = true } lockPhase := &phase.Lock{} r.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, lockPhase, &phase.PrepareHosts{}, &phase.GatherFacts{SkipMachineIDs: true}, &phase.GatherK0sFacts{}, &phase.RunHooks{Stage: "before", Action: "reset"}, &phase.ResetWorkers{ NoDrain: true, NoDelete: true, }, &phase.ResetControllers{ NoDrain: true, NoDelete: true, NoLeave: true, }, &phase.ResetLeader{}, &phase.DaemonReload{}, &phase.RunHooks{Stage: "after", Action: "reset"}, &phase.Unlock{Cancel: lockPhase.Cancel}, &phase.Disconnect{}, ) analytics.Client.Publish("reset-start", map[string]interface{}{}) if err := r.Manager.Run(); err != nil { analytics.Client.Publish("reset-failure", map[string]interface{}{"clusterID": r.Manager.Config.Spec.K0s.Metadata.ClusterID}) return err } analytics.Client.Publish("reset-success", map[string]interface{}{"duration": time.Since(start), "clusterID": r.Manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) log.Info(phase.Colorize.Green(text).String()) return nil } 07070100000019000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001800000000k0sctl-0.19.4/analytics0707010000001A000081A40000000000000000000000016733442800000245000000000000000000000000000000000000002500000000k0sctl-0.19.4/analytics/analytics.gopackage analytics type publisher interface { Publish(string, map[string]interface{}) Close() } // Client is an analytics client that implements the publisher interface var Client publisher // NullClient is a drop in non-functional analytics publisher type NullClient struct{} // Initialize does nothing func (c *NullClient) Initialize() error { return nil } // Publish would send a tracking event func (c *NullClient) Publish(_ string, _ map[string]interface{}) {} // Close the analytics connection func (c *NullClient) Close() {} func init() { Client = &NullClient{} } 0707010000001B000081A4000000000000000000000001673344280000023E000000000000000000000000000000000000002500000000k0sctl-0.19.4/analytics/machineid.gopackage analytics import ( "crypto/md5" "encoding/hex" "os" "github.com/denisbrodbeck/machineid" ) // MachineID returns protected id for the current machine func MachineID() (string, error) { id, err := machineid.ProtectedID("k0sproject-k0s") if err != nil { return MachineIDFromHostname() } return id, err } // MachineIDFromHostname generates a machine id hash from hostname func MachineIDFromHostname() (string, error) { name, err := os.Hostname() if err != nil { return "", err } sum := md5.Sum([]byte(name)) return hex.EncodeToString(sum[:]), nil } 0707010000001C000081A40000000000000000000000016733442800000483000000000000000000000000000000000000002100000000k0sctl-0.19.4/analytics/phase.gopackage analytics import ( "sync" "time" ) // Phase is a stub implementation of a phase with analytics reporting type Phase struct { props map[string]interface{} start time.Time propmutex sync.Mutex } // IncProp increases a numeric data point, creating one if it didn't exist func (p *Phase) IncProp(key string) { p.propmutex.Lock() defer p.propmutex.Unlock() var val uint32 if v, ok := p.props[key].(uint32); ok { val = v } val++ p.props[key] = val } // SetProp sets a value to a datapoint by key func (p *Phase) SetProp(key string, value interface{}) { p.propmutex.Lock() defer p.propmutex.Unlock() p.props[key] = value } // Before prepares the analytics properties and sets the start time func (p *Phase) Before(title string) error { p.props = make(map[string]interface{}) p.props["name"] = title p.start = time.Now() return nil } // After enqueues the sending of analytics func (p *Phase) After(result error) error { p.props["duration"] = time.Since(p.start) var event string if result == nil { event = "phase-success" } else { event = "phase-failure" } Client.Publish(event, p.props) return nil } 0707010000001D000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001200000000k0sctl-0.19.4/cmd0707010000001E000081A400000000000000000000000167334428000009F8000000000000000000000000000000000000001B00000000k0sctl-0.19.4/cmd/apply.gopackage cmd import ( "fmt" "io" "os" "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) var applyCommand = &cli.Command{ Name: "apply", Usage: "Apply a k0sctl configuration", Flags: []cli.Flag{ configFlag, concurrencyFlag, concurrentUploadsFlag, dryRunFlag, &cli.BoolFlag{ Name: "no-wait", Usage: "Do not wait for worker nodes to join", }, &cli.BoolFlag{ Name: "no-drain", Usage: "Do not drain worker nodes when upgrading", }, &cli.StringFlag{ Name: "restore-from", Usage: "Path to cluster backup archive to restore the state from", TakesFile: true, }, &cli.StringFlag{ Name: "kubeconfig-out", Usage: "Write kubeconfig to given path after a successful apply", TakesFile: true, }, &cli.StringFlag{ Name: "kubeconfig-api-address", Usage: "Override the API address in the kubeconfig when kubeconfig-out is set", }, &cli.BoolFlag{ Name: "disable-downgrade-check", Usage: "Skip downgrade check", Hidden: true, }, &cli.BoolFlag{ Name: "force", Usage: "Attempt a forced installation in case of certain failures", }, debugFlag, traceFlag, redactFlag, retryIntervalFlag, retryTimeoutFlag, ignoredUpgradeCheckFlag, analyticsFlag, }, Before: actions(initLogging, initConfig, initManager, displayLogo, initAnalytics, displayCopyright, warnOldCache), After: actions(closeAnalytics), Action: func(ctx *cli.Context) error { var kubeconfigOut io.Writer if kc := ctx.String("kubeconfig-out"); kc != "" { out, err := os.OpenFile(kc, os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("failed to open kubeconfig-out file: %w", err) } defer out.Close() kubeconfigOut = out } applyOpts := action.ApplyOptions{ Force: ctx.Bool("force"), Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), KubeconfigOut: kubeconfigOut, KubeconfigAPIAddress: ctx.String("kubeconfig-api-address"), NoWait: ctx.Bool("no-wait"), NoDrain: ctx.Bool("no-drain"), DisableDowngradeCheck: ctx.Bool("disable-downgrade-check"), RestoreFrom: ctx.String("restore-from"), ConfigPath: ctx.String("config"), } applyAction := action.NewApply(applyOpts) if err := applyAction.Run(); err != nil { return fmt.Errorf("apply failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } return nil }, } 0707010000001F000081A40000000000000000000000016733442800000368000000000000000000000000000000000000001C00000000k0sctl-0.19.4/cmd/backup.gopackage cmd import ( "fmt" "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) var backupCommand = &cli.Command{ Name: "backup", Usage: "Take backup of existing clusters state", Flags: []cli.Flag{ configFlag, dryRunFlag, concurrencyFlag, debugFlag, traceFlag, redactFlag, retryIntervalFlag, retryTimeoutFlag, analyticsFlag, }, Before: actions(initLogging, initConfig, initManager, displayLogo, initAnalytics, displayCopyright), After: actions(closeAnalytics), Action: func(ctx *cli.Context) error { backupAction := action.Backup{ Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), } if err := backupAction.Run(); err != nil { return fmt.Errorf("backup failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } return nil }, } 07070100000020000081A40000000000000000000000016733442800000934000000000000000000000000000000000000002000000000k0sctl-0.19.4/cmd/completion.gopackage cmd import ( "fmt" "os" "path" "strings" "github.com/urfave/cli/v2" ) var completionCommand = &cli.Command{ Name: "completion", Hidden: false, Description: `Generates a shell auto-completion script. Typical locations for the generated output are: - Bash: /etc/bash_completion.d/k0sctl - Zsh: /usr/local/share/zsh/site-functions/_k0sctl - Fish: ~/.config/fish/completions/k0sctl.fish`, Flags: []cli.Flag{ &cli.StringFlag{ Name: "shell", Usage: "Shell to generate the script for", Value: "bash", Aliases: []string{"s"}, EnvVars: []string{"SHELL"}, }, }, Action: func(ctx *cli.Context) error { switch path.Base(ctx.String("shell")) { case "bash": fmt.Print(bashTemplate()) case "zsh": fmt.Print(zshTemplate()) case "fish": t, err := ctx.App.ToFishCompletion() if err != nil { return err } fmt.Print(t) default: return fmt.Errorf("no completion script available for %s", ctx.String("shell")) } return nil }, } func prog() string { p, err := os.Executable() if err != nil || strings.HasSuffix(p, "main") { return "k0sctl" } return path.Base(p) } func bashTemplate() string { return fmt.Sprintf(`#! /bin/bash _k0sctl_bash_autocomplete() { if [[ "${COMP_WORDS[0]}" != "source" ]]; then local cur opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" if [[ "$cur" == "-"* ]]; then opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) else opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) fi COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 fi } complete -o bashdefault -o default -o nospace -F _k0sctl_bash_autocomplete %s `, prog()) } // zshTemplate returns a completion script for zsh func zshTemplate() string { p := prog() return fmt.Sprintf(`#compdef %s _k0sctl_zsh_autocomplete() { local -a opts local cur cur=${words[-1]} if [[ "$cur" == "-"* ]]; then opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") else opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") fi if [[ "${opts[1]}" != "" ]]; then _describe 'values' opts else _files fi return } compdef _k0sctl_zsh_autocomplete %s `, p, p) } 07070100000021000081A400000000000000000000000167334428000002E2000000000000000000000000000000000000002100000000k0sctl-0.19.4/cmd/config_edit.gopackage cmd import ( "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/urfave/cli/v2" ) var configEditCommand = &cli.Command{ Name: "edit", Usage: "Edit k0s dynamic config in SHELL's default editor", Flags: []cli.Flag{ configFlag, debugFlag, traceFlag, redactFlag, analyticsFlag, }, Before: actions(initLogging, initConfig, initAnalytics), After: actions(closeAnalytics), Action: func(ctx *cli.Context) error { configEditAction := action.ConfigEdit{ Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), Stdout: ctx.App.Writer, Stderr: ctx.App.ErrWriter, Stdin: ctx.App.Reader, } return configEditAction.Run() }, } 07070100000022000081A40000000000000000000000016733442800000342000000000000000000000000000000000000002300000000k0sctl-0.19.4/cmd/config_status.gopackage cmd import ( "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/urfave/cli/v2" ) var configStatusCommand = &cli.Command{ Name: "status", Usage: "Show k0s dynamic config reconciliation events", Flags: []cli.Flag{ configFlag, debugFlag, traceFlag, redactFlag, analyticsFlag, &cli.StringFlag{ Name: "output", Usage: "kubectl output formatting", Aliases: []string{"o"}, }, }, Before: actions(initLogging, initConfig, initAnalytics), After: actions(closeAnalytics), Action: func(ctx *cli.Context) error { configStatusAction := action.ConfigStatus{ Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), Format: ctx.String("output"), Writer: ctx.App.Writer, } return configStatusAction.Run() }, } 07070100000023000081A400000000000000000000000167334428000025D8000000000000000000000000000000000000001B00000000k0sctl-0.19.4/cmd/flags.gopackage cmd import ( "context" "fmt" "io" "os" "path" "path/filepath" "runtime" "time" "github.com/a8m/envsubst" "github.com/adrg/xdg" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/integration/segment" "github.com/k0sproject/k0sctl/phase" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/retry" k0sctl "github.com/k0sproject/k0sctl/version" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/logrusorgru/aurora" "github.com/shiena/ansicolor" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "gopkg.in/yaml.v2" ) type ( ctxConfigKey struct{} ctxManagerKey struct{} ctxLogFileKey struct{} ) var ( debugFlag = &cli.BoolFlag{ Name: "debug", Usage: "Enable debug logging", Aliases: []string{"d"}, EnvVars: []string{"DEBUG"}, } dryRunFlag = &cli.BoolFlag{ Name: "dry-run", Usage: "Do not alter cluster state, just print what would be done (EXPERIMENTAL)", EnvVars: []string{"DRY_RUN"}, } traceFlag = &cli.BoolFlag{ Name: "trace", Usage: "Enable trace logging", EnvVars: []string{"TRACE"}, Hidden: false, } redactFlag = &cli.BoolFlag{ Name: "no-redact", Usage: "Do not hide sensitive information in the output", Value: false, } configFlag = &cli.StringFlag{ Name: "config", Usage: "Path to cluster config yaml. Use '-' to read from stdin.", Aliases: []string{"c"}, Value: "k0sctl.yaml", TakesFile: true, } analyticsFlag = &cli.BoolFlag{ Name: "disable-telemetry", Usage: "Do not send anonymous telemetry", EnvVars: []string{"DISABLE_TELEMETRY"}, } ignoredUpgradeCheckFlag = &cli.BoolFlag{ Name: "disable-upgrade-check", Usage: "Do not check for a k0sctl upgrade", EnvVars: []string{"DISABLE_UPGRADE_CHECK"}, Hidden: true, } concurrencyFlag = &cli.IntFlag{ Name: "concurrency", Usage: "Maximum number of hosts to configure in parallel, set to 0 for unlimited", Value: 30, } concurrentUploadsFlag = &cli.IntFlag{ Name: "concurrent-uploads", Usage: "Maximum number of files to upload in parallel, set to 0 for unlimited", Value: 5, } retryTimeoutFlag = &cli.DurationFlag{ Name: "default-timeout", Usage: "Default timeout when waiting for node state changes", Value: retry.DefaultTimeout, Action: func(_ *cli.Context, d time.Duration) error { retry.DefaultTimeout = d return nil }, } retryIntervalFlag = &cli.DurationFlag{ Name: "retry-interval", Usage: "Retry interval when waiting for node state changes", Value: retry.Interval, Action: func(_ *cli.Context, d time.Duration) error { retry.Interval = d return nil }, } Colorize = aurora.NewAurora(false) ) // actions can be used to chain action functions (for urfave/cli's Before, After, etc) func actions(funcs ...func(*cli.Context) error) func(*cli.Context) error { return func(ctx *cli.Context) error { for _, f := range funcs { if err := f(ctx); err != nil { return err } } return nil } } // initConfig takes the config flag, does some magic and replaces the value with the file contents func initConfig(ctx *cli.Context) error { f := ctx.String("config") if f == "" { return nil } file, err := configReader(f) if err != nil { return err } defer file.Close() content, err := io.ReadAll(file) if err != nil { return err } subst, err := envsubst.Bytes(content) if err != nil { return err } log.Debugf("Loaded configuration:\n%s", subst) c := &v1beta1.Cluster{} if err := yaml.UnmarshalStrict(subst, c); err != nil { return err } m, err := yaml.Marshal(c) if err == nil { log.Tracef("unmarshaled configuration:\n%s", m) } if err := c.Validate(); err != nil { return fmt.Errorf("configuration validation failed: %w", err) } ctx.Context = context.WithValue(ctx.Context, ctxConfigKey{}, c) return nil } func displayCopyright(ctx *cli.Context) error { fmt.Printf("k0sctl %s Copyright 2023, k0sctl authors.\n", k0sctl.Version) if !ctx.Bool("disable-telemetry") { fmt.Println("Anonymized telemetry of usage will be sent to the authors.") } fmt.Println("By continuing to use k0sctl you agree to these terms:") fmt.Println("https://k0sproject.io/licenses/eula") return nil } func warnOldCache(_ *cli.Context) error { var olds []string home, err := os.UserHomeDir() if err == nil { olds = append(olds, path.Join(home, ".k0sctl", "cache")) } if runtime.GOOS == "linux" { olds = append(olds, "/var/cache/k0sctl") } for _, p := range olds { if _, err := os.Stat(p); err == nil { log.Warnf("An old cache directory still exists at %s, k0sctl now uses %s", p, path.Join(xdg.CacheHome, "k0sctl")) } } return nil } const segmentWriteKey string = "oU2iC4shRUBfEboaO0FDuDIUk49Ime92" func initAnalytics(ctx *cli.Context) error { if ctx.Bool("disable-telemetry") { log.Tracef("disabling telemetry") return nil } client, err := segment.NewClient(segmentWriteKey) if err != nil { return err } analytics.Client = client return nil } func closeAnalytics(_ *cli.Context) error { analytics.Client.Close() return nil } func initManager(ctx *cli.Context) error { c, ok := ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster) if c == nil || !ok { return fmt.Errorf("cluster config not available in context") } manager, err := phase.NewManager(c) if err != nil { return fmt.Errorf("failed to initialize phase manager: %w", err) } manager.Concurrency = ctx.Int("concurrency") manager.ConcurrentUploads = ctx.Int("concurrent-uploads") manager.DryRun = ctx.Bool("dry-run") ctx.Context = context.WithValue(ctx.Context, ctxManagerKey{}, manager) return nil } // initLogging initializes the logger func initLogging(ctx *cli.Context) error { log.SetLevel(log.TraceLevel) log.SetOutput(io.Discard) initScreenLogger(logLevelFromCtx(ctx, log.InfoLevel)) exec.DisableRedact = ctx.Bool("no-redact") rig.SetLogger(log.StandardLogger()) return initFileLogger(ctx) } // initSilentLogging initializes the logger in silent mode // TODO too similar to initLogging func initSilentLogging(ctx *cli.Context) error { log.SetLevel(log.TraceLevel) log.SetOutput(io.Discard) exec.DisableRedact = ctx.Bool("no-redact") initScreenLogger(logLevelFromCtx(ctx, log.FatalLevel)) rig.SetLogger(log.StandardLogger()) return initFileLogger(ctx) } func logLevelFromCtx(ctx *cli.Context, defaultLevel log.Level) log.Level { if ctx.Bool("trace") { return log.TraceLevel } else if ctx.Bool("debug") { return log.DebugLevel } else { return defaultLevel } } func initScreenLogger(lvl log.Level) { log.AddHook(screenLoggerHook(lvl)) } func initFileLogger(ctx *cli.Context) error { lf, err := LogFile() if err != nil { return err } log.AddHook(fileLoggerHook(lf)) ctx.Context = context.WithValue(ctx.Context, ctxLogFileKey{}, lf.Name()) return nil } const logPath = "k0sctl/k0sctl.log" func LogFile() (*os.File, error) { fn, err := xdg.SearchCacheFile(logPath) if err != nil { fn, err = xdg.CacheFile(logPath) if err != nil { return nil, err } } logFile, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_SYNC, 0o600) if err != nil { return nil, fmt.Errorf("Failed to open log %s: %s", fn, err.Error()) } _, _ = fmt.Fprintf(logFile, "time=\"%s\" level=info msg=\"###### New session ######\"\n", time.Now().Format(time.RFC822)) return logFile, nil } func configReader(f string) (io.ReadCloser, error) { if f == "-" { stat, err := os.Stdin.Stat() if err != nil { return nil, fmt.Errorf("can't stat stdin: %s", err.Error()) } if (stat.Mode() & os.ModeCharDevice) == 0 { return os.Stdin, nil } return nil, fmt.Errorf("can't read stdin") } variants := []string{f} // add .yml to default value lookup if f == "k0sctl.yaml" { variants = append(variants, "k0sctl.yml") } for _, fn := range variants { if _, err := os.Stat(fn); err != nil { continue } fp, err := filepath.Abs(fn) if err != nil { return nil, err } file, err := os.Open(fp) if err != nil { return nil, err } return file, nil } return nil, fmt.Errorf("failed to locate configuration") } type loghook struct { Writer io.Writer Formatter log.Formatter levels []log.Level } func (h *loghook) SetLevel(level log.Level) { h.levels = []log.Level{} for _, l := range log.AllLevels { if level >= l { h.levels = append(h.levels, l) } } } func (h *loghook) Levels() []log.Level { return h.levels } func (h *loghook) Fire(entry *log.Entry) error { line, err := h.Formatter.Format(entry) if err != nil { fmt.Fprintf(os.Stderr, "Unable to format log entry: %v", err) return err } _, err = h.Writer.Write(line) return err } func screenLoggerHook(lvl log.Level) *loghook { var forceColors bool var writer io.Writer if runtime.GOOS == "windows" { writer = ansicolor.NewAnsiColorWriter(os.Stdout) forceColors = true } else { writer = os.Stdout if fi, _ := os.Stdout.Stat(); (fi.Mode() & os.ModeCharDevice) != 0 { forceColors = true } } if forceColors { Colorize = aurora.NewAurora(true) phase.Colorize = Colorize } l := &loghook{ Writer: writer, Formatter: &log.TextFormatter{DisableTimestamp: lvl < log.DebugLevel, ForceColors: forceColors}, } l.SetLevel(lvl) return l } func fileLoggerHook(logFile io.Writer) *loghook { l := &loghook{ Formatter: &log.TextFormatter{ FullTimestamp: true, TimestampFormat: time.RFC822, DisableLevelTruncation: true, }, Writer: logFile, } l.SetLevel(log.DebugLevel) return l } func displayLogo(_ *cli.Context) error { fmt.Print(logo) return nil } 07070100000024000081A40000000000000000000000016733442800001343000000000000000000000000000000000000001A00000000k0sctl-0.19.4/cmd/init.gopackage cmd import ( "bufio" "os" "strconv" "strings" "github.com/creasty/defaults" "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig" "github.com/urfave/cli/v2" "gopkg.in/yaml.v2" ) // DefaultK0sYaml is pretty much what "k0s default-config" outputs var DefaultK0sYaml = []byte(`apiVersion: k0s.k0sproject.io/v1beta1 kind: Cluster metadata: name: k0s spec: api: port: 6443 k0sApiPort: 9443 storage: type: etcd network: podCIDR: 10.244.0.0/16 serviceCIDR: 10.96.0.0/12 provider: kuberouter kuberouter: mtu: 0 peerRouterIPs: "" peerRouterASNs: "" autoMTU: true kubeProxy: disabled: false mode: iptables podSecurityPolicy: defaultPolicy: 00-k0s-privileged telemetry: enabled: true installConfig: users: etcdUser: etcd kineUser: kube-apiserver konnectivityUser: konnectivity-server kubeAPIserverUser: kube-apiserver kubeSchedulerUser: kube-scheduler konnectivity: agentPort: 8132 adminPort: 8133 `) var defaultHosts = cluster.Hosts{ &cluster.Host{ Connection: rig.Connection{ SSH: &rig.SSH{ Address: "10.0.0.1", }, }, Role: "controller", }, &cluster.Host{ Connection: rig.Connection{ SSH: &rig.SSH{ Address: "10.0.0.2", }, }, Role: "worker", }, } func hostFromAddress(addr, role, user, keypath string) *cluster.Host { port := 22 if idx := strings.Index(addr, "@"); idx > 0 { user = addr[:idx] addr = addr[idx+1:] } if idx := strings.Index(addr, ":"); idx > 0 { pstr := addr[idx+1:] if p, err := strconv.Atoi(pstr); err == nil { port = p } addr = addr[:idx] } host := &cluster.Host{ Connection: rig.Connection{ SSH: &rig.SSH{ Address: addr, Port: port, }, }, } if role != "" { host.Role = role } else { host.Role = "worker" } if user != "" { host.SSH.User = user } _ = defaults.Set(host) if keypath == "" { host.SSH.KeyPath = nil } else { host.SSH.KeyPath = &keypath } return host } func buildHosts(addresses []string, ccount int, user, keypath string) cluster.Hosts { var hosts cluster.Hosts role := "controller" for _, a := range addresses { // strip trailing comments if idx := strings.Index(a, "#"); idx > 0 { a = a[:idx] } a = strings.TrimSpace(a) if a == "" || strings.HasPrefix(a, "#") { // skip empty and comment lines continue } if len(hosts) >= ccount { role = "worker" } hosts = append(hosts, hostFromAddress(a, role, user, keypath)) } if len(hosts) == 0 { return defaultHosts } return hosts } var initCommand = &cli.Command{ Name: "init", Usage: "Create a configuration template", Description: "Outputs a new k0sctl configuration. When a list of addresses are provided, hosts are generated into the configuration. The list of addresses can also be provided via stdin.", ArgsUsage: "[[user@]address[:port] ...]", Before: actions(initLogging), Flags: []cli.Flag{ &cli.BoolFlag{ Name: "k0s", Usage: "Include a skeleton k0s config section", }, &cli.StringFlag{ Name: "cluster-name", Usage: "Cluster name", Aliases: []string{"n"}, Value: "k0s-cluster", }, &cli.IntFlag{ Name: "controller-count", Usage: "The number of controllers to create when addresses are given", Aliases: []string{"C"}, Value: 1, }, &cli.StringFlag{ Name: "user", Usage: "Host user when addresses given", Aliases: []string{"u"}, }, &cli.StringFlag{ Name: "key-path", Usage: "Host key path when addresses given", Aliases: []string{"i"}, }, }, Action: func(ctx *cli.Context) error { var addresses []string // Read addresses from stdin stat, err := os.Stdin.Stat() if err == nil { if (stat.Mode() & os.ModeCharDevice) == 0 { rd := bufio.NewReader(os.Stdin) for { row, _, err := rd.ReadLine() if err != nil { break } addresses = append(addresses, string(row)) } if err != nil { return err } } } // Read addresses from args addresses = append(addresses, ctx.Args().Slice()...) cfg := v1beta1.Cluster{ APIVersion: v1beta1.APIVersion, Kind: "Cluster", Metadata: &v1beta1.ClusterMetadata{Name: ctx.String("cluster-name")}, Spec: &cluster.Spec{ Hosts: buildHosts(addresses, ctx.Int("controller-count"), ctx.String("user"), ctx.String("key-path")), K0s: &cluster.K0s{}, }, } if err := defaults.Set(&cfg); err != nil { return err } if ctx.Bool("k0s") { cfg.Spec.K0s.Config = dig.Mapping{} if err := yaml.Unmarshal(DefaultK0sYaml, &cfg.Spec.K0s.Config); err != nil { return err } } encoder := yaml.NewEncoder(os.Stdout) return encoder.Encode(&cfg) }, } 07070100000025000081A40000000000000000000000016733442800000488000000000000000000000000000000000000001F00000000k0sctl-0.19.4/cmd/init_test.gopackage cmd import ( "testing" "github.com/stretchr/testify/require" ) func TestBuildHosts(t *testing.T) { addresses := []string{ "10.0.0.1", "", "10.0.0.2", "10.0.0.3", } hosts := buildHosts(addresses, 1, "test", "foo") require.Len(t, hosts, 3) require.Len(t, hosts.Controllers(), 1) require.Len(t, hosts.Workers(), 2) require.Equal(t, "test", hosts.First().SSH.User) require.Equal(t, "foo", *hosts.First().SSH.KeyPath) hosts = buildHosts(addresses, 2, "", "") require.Len(t, hosts, 3) require.Len(t, hosts.Controllers(), 2) require.Len(t, hosts.Workers(), 1) require.Equal(t, "root", hosts.First().SSH.User) require.Nil(t, hosts.First().SSH.KeyPath) } func TestBuildHostsWithComments(t *testing.T) { addresses := []string{ "# controllers", "10.0.0.1", "# workers", "10.0.0.2# second worker", "10.0.0.3 # last worker", } hosts := buildHosts(addresses, 1, "", "") require.Len(t, hosts, 3) require.Len(t, hosts.Controllers(), 1) require.Len(t, hosts.Workers(), 2) require.Equal(t, "10.0.0.1", hosts[0].Address()) require.Equal(t, "10.0.0.2", hosts[1].Address()) require.Equal(t, "10.0.0.3", hosts[2].Address()) } 07070100000026000081A400000000000000000000000167334428000004D5000000000000000000000000000000000000002000000000k0sctl-0.19.4/cmd/kubeconfig.gopackage cmd import ( "fmt" "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) var kubeconfigCommand = &cli.Command{ Name: "kubeconfig", Usage: "Output the admin kubeconfig of the cluster", Flags: []cli.Flag{ &cli.StringFlag{ Name: "address", Usage: "Set kubernetes API address (default: auto-detect)", Value: "", }, configFlag, dryRunFlag, debugFlag, traceFlag, redactFlag, retryIntervalFlag, retryTimeoutFlag, analyticsFlag, }, Before: actions(initSilentLogging, initConfig, initManager, initAnalytics), After: func(_ *cli.Context) error { analytics.Client.Close() return nil }, Action: func(ctx *cli.Context) error { kubeconfigAction := action.Kubeconfig{ Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), KubeconfigAPIAddress: ctx.String("address"), } if err := kubeconfigAction.Run(); err != nil { return fmt.Errorf("getting kubeconfig failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } _, err := fmt.Fprintf(ctx.App.Writer, "%s\n", kubeconfigAction.Manager.Config.Metadata.Kubeconfig) return err }, } 07070100000027000081A40000000000000000000000016733442800000354000000000000000000000000000000000000001A00000000k0sctl-0.19.4/cmd/logo.gopackage cmd const logo = ` â âŖŋâŖŋâĄâ â âĸâŖ´âŖžâŖŋâ â âĸ¸âŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâĄŋâ â â âĸ¸âŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâ âââââââââ âââââââââ âââ â âŖŋâŖŋâĄâŖ âŖļâŖŋâĄŋâ â â â âĸ¸âŖŋâĄâ â â âŖ â â âĸâŖ âĄâĸ¸âŖŋâŖŋâ â â â â â â â â â âââ âââ âââ â âŖŋâŖŋâŖŋâŖŋâŖâ â â â â â âĸ¸âŖŋâĄâ âĸ°âŖžâŖŋâ â âŖŋâŖŋâĄâĸ¸âŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâŖŋâ âââ âââ âââ â âŖŋâŖŋâĄâ ģâŖŋâŖˇâŖ¤âĄâ â â â ¸â â â â ¸â â â â âŖŋâŖŋâĄâ â â â â â â â â âĸšâŖŋâŖŋâ âââ âââ âââ â âŖŋâŖŋâĄâ â â âĸŋâŖŋâŖĻâŖâ â â âŖ âŖļâŖļâŖļâŖļâŖļâŖļâŖŋâŖŋâĄâĸ°âŖļâŖļâŖļâŖļâŖļâŖļâŖļâŖļâŖžâŖŋâŖŋâ âââââââââ âââ ââââââââââ ` 07070100000028000081A40000000000000000000000016733442800000420000000000000000000000000000000000000001B00000000k0sctl-0.19.4/cmd/reset.gopackage cmd import ( "fmt" "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) var resetCommand = &cli.Command{ Name: "reset", Usage: "Remove traces of k0s from all of the hosts", Flags: []cli.Flag{ configFlag, concurrencyFlag, dryRunFlag, debugFlag, traceFlag, redactFlag, retryIntervalFlag, retryTimeoutFlag, ignoredUpgradeCheckFlag, analyticsFlag, &cli.BoolFlag{ Name: "force", Usage: "Don't ask for confirmation", Aliases: []string{"f"}, }, }, Before: actions(initLogging, initConfig, initManager, initAnalytics, displayCopyright), After: actions(closeAnalytics), Action: func(ctx *cli.Context) error { resetAction := action.Reset{ Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), Force: ctx.Bool("force"), Stdout: ctx.App.Writer, } if err := resetAction.Run(); err != nil { return fmt.Errorf("reset failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } return nil }, } 07070100000029000081A4000000000000000000000001673344280000024C000000000000000000000000000000000000001A00000000k0sctl-0.19.4/cmd/root.gopackage cmd import ( "github.com/urfave/cli/v2" ) // App is the main urfave/cli.App for k0sctl var App = &cli.App{ Name: "k0sctl", Usage: "k0s cluster management tool", Flags: []cli.Flag{ debugFlag, traceFlag, redactFlag, }, Commands: []*cli.Command{ versionCommand, applyCommand, kubeconfigCommand, initCommand, resetCommand, backupCommand, { Name: "config", Usage: "Configuration related sub-commands", Subcommands: []*cli.Command{ configEditCommand, configStatusCommand, }, }, completionCommand, }, EnableBashCompletion: true, } 0707010000002A000081A40000000000000000000000016733442800000571000000000000000000000000000000000000001D00000000k0sctl-0.19.4/cmd/version.gopackage cmd import ( "fmt" "os" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/integration/github" "github.com/k0sproject/k0sctl/version" "github.com/urfave/cli/v2" ) var versionCommand = &cli.Command{ Name: "version", Usage: "Output k0sctl version", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "machine-id", Hidden: true, }, &cli.BoolFlag{ Name: "k0s", Usage: "Retrieve the latest k0s version number", }, &cli.BoolFlag{ Name: "k0sctl", Usage: "Retrieve the latest k0sctl version number", }, &cli.BoolFlag{ Name: "pre", Usage: "When used in conjunction with --k0s, a pre release is accepted as the latest version", }, }, Before: func(ctx *cli.Context) error { if ctx.Bool("k0s") { v, err := github.LatestK0sVersion(ctx.Bool("pre")) if err != nil { return err } fmt.Println(v) os.Exit(0) } if ctx.Bool("k0sctl") { v, err := github.LatestRelease(ctx.Bool("pre")) if err != nil { return err } fmt.Println(v.TagName) os.Exit(0) } if ctx.Bool("machine-id") { id, err := analytics.MachineID() if err != nil { println(err.Error()) os.Exit(1) } fmt.Println(id) os.Exit(0) } return nil }, Action: func(ctx *cli.Context) error { fmt.Printf("version: %s\n", version.Version) fmt.Printf("commit: %s\n", version.GitCommit) return nil }, } 0707010000002B000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001900000000k0sctl-0.19.4/configurer0707010000002C000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001F00000000k0sctl-0.19.4/configurer/linux0707010000002D000081A400000000000000000000000167334428000023EF000000000000000000000000000000000000002200000000k0sctl-0.19.4/configurer/linux.gopackage configurer import ( "fmt" "path" "regexp" "strconv" "strings" "sync" "github.com/alessio/shellescape" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" "github.com/k0sproject/version" ) // Linux is a base module for various linux OS support packages type Linux struct { paths map[string]string pathMu sync.Mutex } // NOTE The Linux struct does not embed rig/os.Linux because it will confuse // go as the distro-configurers' parents embed it too. This means you can't // add functions to base Linux package that call functions in the rig/os.Linux package, // you can however write those functions in the distro-configurers. // An example of this problem is the ReplaceK0sTokenPath function, which would like to // call `l.ServiceScriptPath("kos")`, which was worked around here by getting the // path as a parameter. func (l *Linux) initPaths() { if l.paths != nil { return } l.paths = map[string]string{ "K0sBinaryPath": "/usr/local/bin/k0s", "K0sConfigPath": "/etc/k0s/k0s.yaml", "K0sJoinTokenPath": "/etc/k0s/k0stoken", "DataDirDefaultPath": "/var/lib/k0s", } } // K0sBinaryPath returns the path to the k0s binary on the host func (l *Linux) K0sBinaryPath() string { l.pathMu.Lock() defer l.pathMu.Unlock() l.initPaths() return l.paths["K0sBinaryPath"] } // K0sConfigPath returns the path to the k0s config file on the host func (l *Linux) K0sConfigPath() string { l.pathMu.Lock() defer l.pathMu.Unlock() l.initPaths() return l.paths["K0sConfigPath"] } // K0sJoinTokenPath returns the path to the k0s join token file on the host func (l *Linux) K0sJoinTokenPath() string { l.pathMu.Lock() defer l.pathMu.Unlock() l.initPaths() return l.paths["K0sJoinTokenPath"] } // DataDirDefaultPath returns the path to the k0s data dir on the host func (l *Linux) DataDirDefaultPath() string { l.pathMu.Lock() defer l.pathMu.Unlock() l.initPaths() return l.paths["DataDirDefaultPath"] } // SetPath sets a path for a key func (l *Linux) SetPath(key, value string) { l.pathMu.Lock() defer l.pathMu.Unlock() l.initPaths() l.paths[key] = value } // Arch returns the host processor architecture in the format k0s expects it func (l *Linux) Arch(h os.Host) (string, error) { arch, err := h.ExecOutput("uname -m") if err != nil { return "", err } switch arch { case "x86_64": return "amd64", nil case "aarch64": return "arm64", nil case "armv7l", "armv8l", "aarch32", "arm32", "armhfp", "arm-32": return "arm", nil default: return arch, nil } } // K0sCmdf can be used to construct k0s commands in sprintf style. func (l *Linux) K0sCmdf(template string, args ...interface{}) string { return fmt.Sprintf("%s %s", l.K0sBinaryPath(), fmt.Sprintf(template, args...)) } func (l *Linux) K0sBinaryVersion(h os.Host) (*version.Version, error) { k0sVersionCmd := l.K0sCmdf("version") output, err := h.ExecOutput(k0sVersionCmd, exec.Sudo(h)) if err != nil { return nil, err } version, err := version.NewVersion(output) if err != nil { return nil, err } return version, nil } // K0sctlLockFilePath returns a path to a lock file func (l *Linux) K0sctlLockFilePath(h os.Host) string { if h.Exec("test -d /run/lock", exec.Sudo(h)) == nil { return "/run/lock/k0sctl" } return "/tmp/k0sctl.lock" } // TempFile returns a temp file path func (l *Linux) TempFile(h os.Host) (string, error) { return h.ExecOutput("mktemp") } // TempDir returns a temp dir path func (l *Linux) TempDir(h os.Host) (string, error) { return h.ExecOutput("mktemp -d") } var trailingNumberRegex = regexp.MustCompile(`(\d+)$`) func trailingNumber(s string) (int, bool) { match := trailingNumberRegex.FindStringSubmatch(s) if len(match) > 0 { i, err := strconv.Atoi(match[1]) if err == nil { return i, true } } return 0, false } // DownloadURL performs a download from a URL on the host func (l *Linux) DownloadURL(h os.Host, url, destination string, opts ...exec.Option) error { err := h.Exec(fmt.Sprintf(`curl -sSLf -o %s %s`, shellescape.Quote(destination), shellescape.Quote(url)), opts...) if err != nil { if exitCode, ok := trailingNumber(err.Error()); ok && exitCode == 22 { return fmt.Errorf("download failed: http 404 - not found: %w", err) } return fmt.Errorf("download failed: %w", err) } return nil } // DownloadK0s performs k0s binary download from github on the host func (l *Linux) DownloadK0s(h os.Host, path string, version *version.Version, arch string, opts ...exec.Option) error { v := strings.ReplaceAll(strings.TrimPrefix(version.String(), "v"), "+", "%2B") url := fmt.Sprintf("https://github.com/k0sproject/k0s/releases/download/v%[1]s/k0s-v%[1]s-%[2]s", v, arch) if err := l.DownloadURL(h, url, path, opts...); err != nil { return fmt.Errorf("failed to download k0s - check connectivity and k0s version validity: %w", err) } return nil } // ReplaceK0sTokenPath replaces the config path in the service stub func (l *Linux) ReplaceK0sTokenPath(h os.Host, spath string) error { return h.Exec(fmt.Sprintf("sed -i 's^REPLACEME^%s^g' %s", l.K0sJoinTokenPath(), spath)) } // FileContains returns true if a file contains the substring func (l *Linux) FileContains(h os.Host, path, s string) bool { return h.Execf(`grep -q "%s" "%s"`, s, path, exec.Sudo(h)) == nil } // MoveFile moves a file on the host func (l *Linux) MoveFile(h os.Host, src, dst string) error { return h.Execf(`mv "%s" "%s"`, src, dst, exec.Sudo(h)) } // KubeconfigPath returns the path to a kubeconfig on the host func (l *Linux) KubeconfigPath(h os.Host, dataDir string) string { linux := &os.Linux{} // if admin.conf exists, use that adminConfPath := path.Join(dataDir, "pki/admin.conf") if linux.FileExist(h, adminConfPath) { return adminConfPath } return path.Join(dataDir, "kubelet.conf") } // KubectlCmdf returns a command line in sprintf manner for running kubectl on the host using the kubeconfig from KubeconfigPath func (l *Linux) KubectlCmdf(h os.Host, dataDir, s string, args ...interface{}) string { return fmt.Sprintf(`env "KUBECONFIG=%s" %s`, l.KubeconfigPath(h, dataDir), l.K0sCmdf(`kubectl %s`, fmt.Sprintf(s, args...))) } // HTTPStatus makes a HTTP GET request to the url and returns the status code or an error func (l *Linux) HTTPStatus(h os.Host, url string) (int, error) { output, err := h.ExecOutput(fmt.Sprintf(`curl -kso /dev/null --connect-timeout 20 -w "%%{http_code}" "%s"`, url)) if err != nil { return -1, err } status, err := strconv.Atoi(output) if err != nil { return -1, fmt.Errorf("invalid response: %s", err.Error()) } return status, nil } const sbinPath = `PATH=/usr/local/sbin:/usr/sbin:/sbin:$PATH` // PrivateInterface tries to find a private network interface func (l *Linux) PrivateInterface(h os.Host) (string, error) { output, err := h.ExecOutput(fmt.Sprintf(`%s; (ip route list scope global | grep -E "\b(172|10|192\.168)\.") || (ip route list | grep -m1 default)`, sbinPath)) if err == nil { re := regexp.MustCompile(`\bdev (\w+)`) match := re.FindSubmatch([]byte(output)) if len(match) > 0 { return string(match[1]), nil } err = fmt.Errorf("can't find 'dev' in output") } return "", fmt.Errorf("failed to detect a private network interface, define the host privateInterface manually (%s)", err.Error()) } // PrivateAddress resolves internal ip from private interface func (l *Linux) PrivateAddress(h os.Host, iface, publicip string) (string, error) { output, err := h.ExecOutput(fmt.Sprintf("%s ip -o addr show dev %s scope global", sbinPath, iface)) if err != nil { return "", fmt.Errorf("failed to find private interface with name %s: %s. Make sure you've set correct 'privateInterface' for the host in config", iface, output) } lines := strings.Split(output, "\n") for _, line := range lines { items := strings.Fields(line) if len(items) < 4 { continue } // When subnet mask is 255.255.255.255, CIDR notation is not /32, but it is omitted instead. index := strings.Index(items[3], "/") addr := items[3] if index >= 0 { addr = items[3][:index] } if len(strings.Split(addr, ".")) == 4 { if publicip != addr { return addr, nil } } } return "", fmt.Errorf("not found") } // UpsertFile creates a file in path with content only if the file does not exist already func (l *Linux) UpsertFile(h os.Host, path, content string) error { tmpf, err := l.TempFile(h) if err != nil { return err } if err := h.Execf(`cat > "%s"`, tmpf, exec.Stdin(content), exec.Sudo(h)); err != nil { return err } defer func() { _ = h.Execf(`rm -f "%s"`, tmpf, exec.Sudo(h)) }() // mv -n is atomic if err := h.Execf(`mv -n "%s" "%s"`, tmpf, path, exec.Sudo(h)); err != nil { return fmt.Errorf("upsert failed: %w", err) } // if original tempfile still exists, error out if h.Execf(`test -f "%s"`, tmpf) == nil { return fmt.Errorf("upsert failed") } return nil } func (l *Linux) DeleteDir(h os.Host, path string, opts ...exec.Option) error { return h.Exec(fmt.Sprintf(`rmdir %s`, shellescape.Quote(path)), opts...) } func (l *Linux) MachineID(h os.Host) (string, error) { return h.ExecOutput(`cat /etc/machine-id || cat /var/lib/dbus/machine-id`) } 0707010000002E000081A40000000000000000000000016733442800000353000000000000000000000000000000000000002900000000k0sctl-0.19.4/configurer/linux/alpine.gopackage linux import ( "strings" "github.com/k0sproject/k0sctl/configurer" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) // BaseLinux for tricking go interfaces type BaseLinux struct { configurer.Linux } // Alpine provides OS support for Alpine Linux type Alpine struct { os.Linux BaseLinux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "alpine" }, func() interface{} { return &Alpine{} }, ) } // InstallPackage installs packages via slackpkg func (l *Alpine) InstallPackage(h os.Host, pkg ...string) error { return h.Execf("apk add --update %s", strings.Join(pkg, " "), exec.Sudo(h)) } func (l *Alpine) Prepare(h os.Host) error { return l.InstallPackage(h, "findutils", "coreutils") } 0707010000002F000081A400000000000000000000000167334428000000D9000000000000000000000000000000000000002E00000000k0sctl-0.19.4/configurer/linux/alpine_test.gopackage linux import ( "testing" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) func TestAlpineConfigurerInterface(t *testing.T) { h := cluster.Host{} h.Configurer = &Alpine{} } 07070100000030000081A400000000000000000000000167334428000001DF000000000000000000000000000000000000002C00000000k0sctl-0.19.4/configurer/linux/archlinux.gopackage linux import ( "github.com/k0sproject/k0sctl/configurer" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/linux" "github.com/k0sproject/rig/os/registry" ) // Archlinux provides OS support for Archlinux systems type Archlinux struct { linux.Archlinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "arch" || os.IDLike == "arch" }, func() interface{} { return &Archlinux{} }, ) } 07070100000031000081A40000000000000000000000016733442800000275000000000000000000000000000000000000002900000000k0sctl-0.19.4/configurer/linux/coreos.gopackage linux import ( "errors" "strings" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) // CoreOS provides OS support for ostree based Fedora & RHEL systems type CoreOS struct { os.Linux BaseLinux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return strings.Contains(os.Name, "CoreOS") && (os.ID == "fedora" || os.ID == "rhel") }, func() interface{} { return &CoreOS{} }, ) } func (l *CoreOS) InstallPackage(h os.Host, pkg ...string) error { return errors.New("CoreOS does not support installing packages manually") } 07070100000032000081A400000000000000000000000167334428000001BB000000000000000000000000000000000000002900000000k0sctl-0.19.4/configurer/linux/debian.gopackage linux import ( "github.com/k0sproject/k0sctl/configurer" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/linux" "github.com/k0sproject/rig/os/registry" ) // Debian provides OS support for Debian systems type Debian struct { linux.Ubuntu configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "debian" }, func() interface{} { return &Debian{} }, ) } 07070100000033000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002F00000000k0sctl-0.19.4/configurer/linux/enterpriselinux07070100000034000081A4000000000000000000000001673344280000010F000000000000000000000000000000000000003200000000k0sctl-0.19.4/configurer/linux/enterpriselinux.gopackage linux import ( "github.com/k0sproject/k0sctl/configurer" "github.com/k0sproject/rig/os/linux" ) // EnterpriseLinux is a base package for several RHEL-like enterprise linux distributions type EnterpriseLinux struct { linux.EnterpriseLinux configurer.Linux } 07070100000035000081A400000000000000000000000167334428000001EC000000000000000000000000000000000000003C00000000k0sctl-0.19.4/configurer/linux/enterpriselinux/almalinux.gopackage enterpriselinux import ( "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // AlmaLinux provides OS support for AlmaLinux type AlmaLinux struct { k0slinux.EnterpriseLinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "almalinux" }, func() interface{} { return &AlmaLinux{} }, ) } 07070100000036000081A400000000000000000000000167334428000002BA000000000000000000000000000000000000003900000000k0sctl-0.19.4/configurer/linux/enterpriselinux/amazon.gopackage enterpriselinux import ( "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) // AmazonLinux provides OS support for AmazonLinux type AmazonLinux struct { k0slinux.EnterpriseLinux configurer.Linux } // Hostname on amazon linux will return the full hostname func (l *AmazonLinux) Hostname(h os.Host) string { hostname, _ := h.ExecOutput("hostname") return hostname } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "amzn" }, func() interface{} { return &AmazonLinux{} }, ) } 07070100000037000081A400000000000000000000000167334428000001DD000000000000000000000000000000000000003900000000k0sctl-0.19.4/configurer/linux/enterpriselinux/centos.gopackage enterpriselinux import ( "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // CentOS provides OS support for CentOS type CentOS struct { k0slinux.EnterpriseLinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "centos" }, func() interface{} { return &CentOS{} }, ) } 07070100000038000081A40000000000000000000000016733442800000211000000000000000000000000000000000000003900000000k0sctl-0.19.4/configurer/linux/enterpriselinux/fedora.gopackage enterpriselinux import ( "strings" "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // Fedora provides OS support for Fedora type Fedora struct { k0slinux.EnterpriseLinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "fedora" && !strings.Contains(os.Name, "CoreOS") }, func() interface{} { return &Fedora{} }, ) } 07070100000039000081A400000000000000000000000167334428000001EE000000000000000000000000000000000000003900000000k0sctl-0.19.4/configurer/linux/enterpriselinux/oracle.gopackage enterpriselinux import ( "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // OracleLinux provides OS support for Oracle Linuc type OracleLinux struct { k0slinux.EnterpriseLinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "ol" }, func() interface{} { return &OracleLinux{} }, ) } 0707010000003A000081A400000000000000000000000167334428000001DD000000000000000000000000000000000000003700000000k0sctl-0.19.4/configurer/linux/enterpriselinux/rhel.gopackage enterpriselinux import ( "strings" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // RHEL provides OS support for RedHat Enterprise Linux type RHEL struct { k0slinux.EnterpriseLinux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "rhel" && !strings.Contains(os.Name, "CoreOS") }, func() interface{} { return &RHEL{} }, ) } 0707010000003B000081A400000000000000000000000167334428000001EC000000000000000000000000000000000000003800000000k0sctl-0.19.4/configurer/linux/enterpriselinux/rocky.gopackage enterpriselinux import ( "github.com/k0sproject/k0sctl/configurer" k0slinux "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // RockyLinux provides OS support for RockyLinux type RockyLinux struct { k0slinux.EnterpriseLinux configurer.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "rocky" }, func() interface{} { return &RockyLinux{} }, ) } 0707010000003C000081A40000000000000000000000016733442800000237000000000000000000000000000000000000002A00000000k0sctl-0.19.4/configurer/linux/flatcar.gopackage linux import ( "errors" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) type Flatcar struct { BaseLinux os.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "flatcar" }, func() interface{} { fc := &Flatcar{} fc.SetPath("K0sBinaryPath", "/opt/bin/k0s") return fc }, ) } func (l *Flatcar) InstallPackage(h os.Host, pkg ...string) error { return errors.New("FlatcarContainerLinux does not support installing packages manually") } 0707010000003D000081A400000000000000000000000167334428000006F3000000000000000000000000000000000000002D00000000k0sctl-0.19.4/configurer/linux/linux_test.gopackage linux import ( "fmt" "io" "testing" "github.com/k0sproject/rig/exec" "github.com/stretchr/testify/require" ) type mockHost struct { ExecFError bool } func (m mockHost) Upload(source, destination string, opts ...exec.Option) error { return nil } func (m mockHost) Exec(string, ...exec.Option) error { return nil } func (m mockHost) ExecOutput(string, ...exec.Option) (string, error) { return "", nil } func (m mockHost) Execf(string, ...interface{}) error { if m.ExecFError { return fmt.Errorf("error") } return nil } func (m mockHost) ExecOutputf(string, ...interface{}) (string, error) { return "", nil } func (m mockHost) ExecStreams(cmd string, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, opts ...exec.Option) (exec.Waiter, error) { return nil, nil } func (m mockHost) String() string { return "" } func (m mockHost) Sudo(string) (string, error) { return "", nil } func TestPaths(t *testing.T) { fc := &Flatcar{} fc.SetPath("K0sBinaryPath", "/opt/bin/k0s") ubuntu := &Ubuntu{} h1 := &mockHost{ ExecFError: false, } h2 := &mockHost{ ExecFError: true, } require.Equal(t, "/opt/bin/k0s", fc.K0sBinaryPath()) require.Equal(t, "/usr/local/bin/k0s", ubuntu.K0sBinaryPath()) require.Equal(t, "/opt/bin/k0s --help", fc.K0sCmdf("--help")) require.Equal(t, "/usr/local/bin/k0s --help", ubuntu.K0sCmdf("--help")) require.Equal(t, "/var/lib/k0s/pki/admin.conf", fc.KubeconfigPath(h1, fc.DataDirDefaultPath())) require.Equal(t, "/var/lib/k0s/pki/admin.conf", ubuntu.KubeconfigPath(h1, ubuntu.DataDirDefaultPath())) require.Equal(t, "/var/lib/k0s/kubelet.conf", fc.KubeconfigPath(h2, fc.DataDirDefaultPath())) require.Equal(t, "/var/lib/k0s/kubelet.conf", ubuntu.KubeconfigPath(h2, ubuntu.DataDirDefaultPath())) } 0707010000003E000081A40000000000000000000000016733442800000171000000000000000000000000000000000000002B00000000k0sctl-0.19.4/configurer/linux/opensuse.gopackage linux import ( "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // OpenSUSE provides OS support for OpenSUSE type OpenSUSE struct { SLES } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "opensuse" || os.ID == "opensuse-microos" }, func() interface{} { return &OpenSUSE{} }, ) } 0707010000003F000081A40000000000000000000000016733442800000321000000000000000000000000000000000000002C00000000k0sctl-0.19.4/configurer/linux/slackware.gopackage linux import ( "fmt" "strings" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) // Slackware provides OS support for Slackware Linux type Slackware struct { BaseLinux os.Linux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "slackware" }, func() interface{} { return &Slackware{} }, ) } // InstallPackage installs packages via slackpkg func (l *Slackware) InstallPackage(h os.Host, pkg ...string) error { updatecmd, err := h.Sudo("slackpkg update") if err != nil { return err } installcmd, err := h.Sudo(fmt.Sprintf("slackpkg install --priority ADD %s", strings.Join(pkg, " "))) if err != nil { return err } return h.Execf("%s && %s", updatecmd, installcmd) } 07070100000040000081A400000000000000000000000167334428000001BC000000000000000000000000000000000000002700000000k0sctl-0.19.4/configurer/linux/sles.gopackage linux import ( "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/linux" "github.com/k0sproject/rig/os/registry" ) // SLES provides OS support for Suse SUSE Linux Enterprise Server type SLES struct { linux.SLES os.Linux BaseLinux } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "sles" }, func() interface{} { return &SLES{} }, ) } 07070100000041000081A40000000000000000000000016733442800000152000000000000000000000000000000000000002900000000k0sctl-0.19.4/configurer/linux/ubuntu.gopackage linux import ( "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" ) // Ubuntu provides OS support for Ubuntu systems type Ubuntu struct { Debian } func init() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "ubuntu" }, func() interface{} { return &Ubuntu{} }, ) } 07070100000042000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001300000000k0sctl-0.19.4/docs07070100000043000081A40000000000000000000000016733442800000000000000000000000000000000000000000000001C00000000k0sctl-0.19.4/docs/.gitkeep07070100000044000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001700000000k0sctl-0.19.4/examples07070100000045000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001E00000000k0sctl-0.19.4/examples/aws-tf07070100000046000081A4000000000000000000000001673344280000005D000000000000000000000000000000000000002900000000k0sctl-0.19.4/examples/aws-tf/.gitignoreterraform.tfvars .terraform terraform.tfstate* aws_private.pem .terraform.lock.hcl kubeconfig07070100000047000081A4000000000000000000000001673344280000010B000000000000000000000000000000000000002700000000k0sctl-0.19.4/examples/aws-tf/Makefile apply: terraform init terraform apply -auto-approve terraform output -raw k0s_cluster | go run ../../main.go apply --config - destroy: terraform destroy -auto-approve kubeconfig: terraform output -raw k0s_cluster | go run ../../main.go kubeconfig --config - 07070100000048000081A400000000000000000000000167334428000003E4000000000000000000000000000000000000002800000000k0sctl-0.19.4/examples/aws-tf/README.md# Bootstrapping a k0s cluster on AWS using Terraform This directory provides an example flow with `k0sctl` tool together with Terraform using AWS as the cloud provider. ## Prerequisites - You need an account and AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN for AWS - Terraform >=v0.14.3 installed - You will need the `k0sctl` binary in `PATH` ## TF Steps - `terraform init` - `terraform apply` - `terraform output -raw k0s_cluster | k0sctl apply --config -` NOTE: this assumes that `k0sctl` binary is available in the `PATH` This will create a cluster with single controller and worker nodes. If you want to override the default behaviour. Create a `terraform.tfvars` file with the needed details. You can use the provided `terraform.tfvars.example` as a template. ## Makefile steps In case you don't want to do all those steps you can use the Makefile. To deploy a k0s cluster with k0sctl: - `make apply` Get kubeconfig: - `make kubeconfig` Teardown: - `make destroy` 07070100000049000081A400000000000000000000000167334428000001C6000000000000000000000000000000000000002C00000000k0sctl-0.19.4/examples/aws-tf/controller.tfresource "aws_instance" "cluster-controller" { count = var.controller_count ami = data.aws_ami.ubuntu.id instance_type = var.cluster_flavor tags = { Name = "controller" } key_name = aws_key_pair.cluster-key.key_name vpc_security_group_ids = [aws_security_group.cluster_allow_ssh.id] associate_public_ip_address = true root_block_device { volume_type = "gp2" volume_size = 10 } }0707010000004A000081A400000000000000000000000167334428000007B9000000000000000000000000000000000000002600000000k0sctl-0.19.4/examples/aws-tf/main.tfterraform { required_version = ">= 0.14.3" } provider "aws" { region = "eu-north-1" } resource "tls_private_key" "k0sctl" { algorithm = "RSA" rsa_bits = 4096 } resource "aws_key_pair" "cluster-key" { key_name = format("%s_key", var.cluster_name) public_key = tls_private_key.k0sctl.public_key_openssh } // Save the private key to filesystem resource "local_file" "aws_private_pem" { file_permission = "600" filename = format("%s/%s", path.module, "aws_private.pem") content = tls_private_key.k0sctl.private_key_pem } resource "aws_security_group" "cluster_allow_ssh" { name = format("%s-allow-ssh", var.cluster_name) description = "Allow ssh inbound traffic" // vpc_id = aws_vpc.cluster-vpc.id // Allow all incoming and outgoing ports. // TODO: need to create a more restrictive policy ingress { description = "SSH from VPC" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = format("%s-allow-ssh", var.cluster_name) } } data "aws_ami" "ubuntu" { most_recent = true filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] } filter { name = "virtualization-type" values = ["hvm"] } owners = ["099720109477"] } locals { k0s_tmpl = { apiVersion = "k0sctl.k0sproject.io/v1beta1" kind = "cluster" spec = { hosts = [ for host in concat(aws_instance.cluster-controller, aws_instance.cluster-workers) : { ssh = { address = host.public_ip user = "ubuntu" keyPath = "./aws_private.pem" } role = host.tags["Name"] } ] k0s = { version = "0.13.1" } } } } output "k0s_cluster" { value = yamlencode(local.k0s_tmpl) } 0707010000004B000081A4000000000000000000000001673344280000005E000000000000000000000000000000000000003700000000k0sctl-0.19.4/examples/aws-tf/terraform.tfvars.examplecluster_name = "pick_a_name" controller_count = 1 worker_count = 2 cluster_flavor = "t2.large"0707010000004C000081A4000000000000000000000001673344280000010D000000000000000000000000000000000000002B00000000k0sctl-0.19.4/examples/aws-tf/variables.tfvariable "cluster_name" { type = string default = "k0sctl" } variable "controller_count" { type = number default = 1 } variable "worker_count" { type = number default = 1 } variable "cluster_flavor" { type = string default = "t3.large" } 0707010000004D000081A400000000000000000000000167334428000001D7000000000000000000000000000000000000002800000000k0sctl-0.19.4/examples/aws-tf/worker.tfresource "aws_instance" "cluster-workers" { count = var.worker_count ami = data.aws_ami.ubuntu.id instance_type = var.cluster_flavor tags = { Name = "worker" } key_name = aws_key_pair.cluster-key.key_name vpc_security_group_ids = [aws_security_group.cluster_allow_ssh.id] associate_public_ip_address = true source_dest_check = false root_block_device { volume_type = "gp2" volume_size = 20 } } 0707010000004E000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002100000000k0sctl-0.19.4/examples/bootloose0707010000004F000081A40000000000000000000000016733442800000016000000000000000000000000000000000000002C00000000k0sctl-0.19.4/examples/bootloose/.gitignorek0sctl.yaml binaries/ 07070100000050000081A4000000000000000000000001673344280000030C000000000000000000000000000000000000003000000000k0sctl-0.19.4/examples/bootloose/bootloose.yamlcluster: name: k0s privateKey: ~/.ssh/id_rsa machines: - count: 1 backend: ignite spec: image: weaveworks/ignite-ubuntu:18.04 name: controller%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 443 - containerPort: 6443 - count: 1 backend: ignite spec: image: weaveworks/ignite-centos:7 name: worker%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 6443 07070100000051000081A400000000000000000000000167334428000000FD000000000000000000000000000000000000003500000000k0sctl-0.19.4/examples/bootloose/k0sctl.yaml.exampleapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller ssh: address: 127.0.0.1 port: 9022 - role: worker ssh: address: 127.0.0.1 port: 9023 k0s: version: 0.12.1 07070100000052000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002200000000k0sctl-0.19.4/examples/hetzner-tf07070100000053000081A4000000000000000000000001673344280000002F000000000000000000000000000000000000002D00000000k0sctl-0.19.4/examples/hetzner-tf/.gitignoreterraform.tfvars .terraform terraform.tfstate* 07070100000054000081A400000000000000000000000167334428000001EF000000000000000000000000000000000000002C00000000k0sctl-0.19.4/examples/hetzner-tf/README.md# Bootstrapping k0s cluster on Hetzner This directory provides an example flow with `k0sctl` tool together with Terraform using Hetzner as the cloud provider. ## Prerequisites - You need an account and API token for Hetzner - Terraform installed - k0sctl installed ## Steps Create terraform.tfvars file with needed details. You can use the provided terraform.tfvars.example as a baseline. - `terraform init` - `terraform apply` - `terraform output -raw k0s_cluster | k0sctl apply --config -` 07070100000055000081A40000000000000000000000016733442800000E01000000000000000000000000000000000000002A00000000k0sctl-0.19.4/examples/hetzner-tf/main.tfterraform { required_providers { hcloud = { source = "hetznercloud/hcloud" version = "~> 1.24" } } } variable "hcloud_token" { description = "Hetzner API token" } provider "hcloud" { token = var.hcloud_token } variable "ssh_keys" { default = [] } variable "ssh_user" { default = "root" } variable "cluster_name" { default = "k0s" } variable "location" { default = "hel1" } variable "image" { default = "ubuntu-18.04" } variable "controller_type" { default = "cx31" } variable "controller_count" { default = 1 } variable "worker_count" { default = 1 } variable "worker_type" { default = "cx31" } resource "hcloud_server" "controller" { count = var.controller_count name = "${var.cluster_name}-controller-${count.index}" image = var.image server_type = var.controller_type ssh_keys = var.ssh_keys location = var.location labels = { role = "controller" } } resource "hcloud_server" "worker" { count = var.worker_count name = "${var.cluster_name}-worker-${count.index}" image = var.image server_type = var.worker_type ssh_keys = var.ssh_keys location = var.location labels = { role = "worker" } } resource "hcloud_load_balancer" "load_balancer" { name = "${var.cluster_name}-balancer" load_balancer_type = "lb11" location = var.location } resource "hcloud_load_balancer_target" "load_balancer_target" { type = "label_selector" load_balancer_id = hcloud_load_balancer.load_balancer.id label_selector = "role=controller" } resource "hcloud_load_balancer_service" "load_balancer_service_6443" { load_balancer_id = hcloud_load_balancer.load_balancer.id protocol = "tcp" listen_port = 6443 destination_port = 6443 } resource "hcloud_load_balancer_service" "load_balancer_service_9443" { load_balancer_id = hcloud_load_balancer.load_balancer.id protocol = "tcp" listen_port = 9443 destination_port = 9443 } resource "hcloud_load_balancer_service" "load_balancer_service_8132" { load_balancer_id = hcloud_load_balancer.load_balancer.id protocol = "tcp" listen_port = 8132 destination_port = 8132 } resource "hcloud_load_balancer_service" "load_balancer_service_8133" { load_balancer_id = hcloud_load_balancer.load_balancer.id protocol = "tcp" listen_port = 8133 destination_port = 8133 } locals { k0s_tmpl = { apiVersion = "k0sctl.k0sproject.io/v1beta1" kind = "cluster" spec = { hosts = [ for host in concat(hcloud_server.controller, hcloud_server.worker) : { ssh = { address = host.ipv4_address user = "root" } role = host.labels.role } ] k0s = { version = "0.12.1" "config" = { "apiVersion" = "k0s.k0sproject.io/v1beta1" "kind" = "Cluster" "metadata" = { "name" = var.cluster_name } "spec" = { "api" = { "externalAddress" = hcloud_load_balancer.load_balancer.ipv4 "sans" = [hcloud_load_balancer.load_balancer.ipv4] } } } } } } } output "k0s_cluster" { value = yamlencode(local.k0s_tmpl) } 07070100000056000081A400000000000000000000000167334428000000C7000000000000000000000000000000000000003B00000000k0sctl-0.19.4/examples/hetzner-tf/terraform.tfvars.examplessh_keys = ["you@domain.com"] hcloud_token = "your-api-token" cluster_name = "k0s" location = "hel1" image = "ubuntu-18.04" master_type = "cx31" master_count = 1 worker_type = "cx31" worker_count = 207070100000057000081A4000000000000000000000001673344280000117A000000000000000000000000000000000000001500000000k0sctl-0.19.4/go.modmodule github.com/k0sproject/k0sctl go 1.23.2 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect github.com/a8m/envsubst v1.4.2 github.com/adrg/xdg v0.5.3 github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/creasty/defaults v1.8.0 github.com/denisbrodbeck/machineid v1.0.1 github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/k0sproject/dig v0.3.1 github.com/k0sproject/rig v0.19.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/masterzen/winrm v0.0.0-20240702205601-3fad6e106085 // indirect github.com/mattn/go-isatty v0.0.20 github.com/segmentio/analytics-go v3.1.0+incompatible github.com/segmentio/backo-go v1.1.0 // indirect github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.5 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect golang.org/x/crypto v0.29.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/alessio/shellescape v1.4.2 github.com/carlmjohnson/versioninfo v0.22.5 github.com/go-playground/validator/v10 v10.22.1 github.com/jellydator/validation v1.1.0 github.com/k0sproject/version v0.6.0 github.com/sergi/go-diff v1.3.1 k8s.io/client-go v0.31.2 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/goidentity/v6 v6.0.1 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/time v0.7.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.31.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) 07070100000058000081A40000000000000000000000016733442800006FC1000000000000000000000000000000000000001500000000k0sctl-0.19.4/go.sumgithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b h1:baFN6AnR0SeC194X2D292IUZcHDs4JjStpqtE70fjXE= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b/go.mod h1:Ram6ngyPDmP+0t6+4T2rymv0w0BS9N8Ch5vvUJccw5o= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jellydator/validation v1.1.0 h1:TBkx56y6dd0By2AhtStRdTIhDjtcuoSE9w6G6z7wQ4o= github.com/jellydator/validation v1.1.0/go.mod h1:AaCjfkQ4Ykdcb+YCwqCtaI3wDsf2UAGhJ06lJs0VgOw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/k0sproject/dig v0.3.1 h1:/QK40lXQ/HEE3LMT3r/kST1ANhMVZiajNDXI+spbL9o= github.com/k0sproject/dig v0.3.1/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4= github.com/k0sproject/rig v0.19.0 h1:aF/wJDfK45Ho2Z75Uap+u4Q4jHgr/1WfrHcOg2U9/n0= github.com/k0sproject/rig v0.19.0/go.mod h1:SNa9+xeVA6zQVYx+SINaa4ZihFPWrmo/6crHcdvJRFI= github.com/k0sproject/version v0.6.0 h1:Wi8wu9j+H36+okIQA47o/YHbzNpKeIYj8IjGdJOdqsI= github.com/k0sproject/version v0.6.0/go.mod h1:5/7Js62gDCLBP6mEs0mUcYEEkYneM5qXDKN/hyFlQTM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20240702205601-3fad6e106085 h1:PiQLLKX4vMYlJImDzJYtQScF2BbQ0GAjPIHCDqzHHHs= github.com/masterzen/winrm v0.0.0-20240702205601-3fad6e106085/go.mod h1:JajVhkiG2bYSNYYPYuWG7WZHr42CTjMTcCjfInRNCqc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs= github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 07070100000059000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001A00000000k0sctl-0.19.4/integration0707010000005A000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002100000000k0sctl-0.19.4/integration/github0707010000005B000081A40000000000000000000000016733442800000D12000000000000000000000000000000000000002B00000000k0sctl-0.19.4/integration/github/github.gopackage github import ( "encoding/json" "fmt" "io" "net/http" "sort" "strings" "time" k0sversion "github.com/k0sproject/version" ) const timeOut = time.Second * 10 // Asset describes a github asset type Asset struct { Name string `json:"name"` URL string `json:"browser_download_url"` } // Release describes a github release type Release struct { URL string `json:"html_url"` TagName string `json:"tag_name"` PreRelease bool `json:"prerelease"` Assets []Asset `json:"assets"` } func (r *Release) IsNewer(b string) bool { this, err := k0sversion.NewVersion(r.TagName) if err != nil { return false } other, err := k0sversion.NewVersion(b) if err != nil { return false } return this.GreaterThan(other) } // LatestK0sBinaryURL returns the url for the latest k0s release by arch and os func LatestK0sBinaryURL(arch, osKind string, preok bool) (string, error) { r, err := k0sversion.LatestByPrerelease(preok) if err != nil { return "", err } return r.DownloadURL(osKind, arch), nil } // LatestK0sVersion returns the latest k0s version number (without v prefix) func LatestK0sVersion(preok bool) (string, error) { r, err := k0sversion.LatestByPrerelease(preok) if err != nil { return "", err } return strings.TrimPrefix(r.String(), "v"), nil } // LatestRelease returns the latest k0sctl version from github func LatestRelease(preok bool) (Release, error) { latestRelease, err := fetchLatestRelease() if err != nil { return Release{}, fmt.Errorf("failed to fetch the latest release: %w", err) } if latestRelease.PreRelease && !preok { latestRelease, err = fetchLatestNonPrereleaseRelease() if err != nil { return Release{}, fmt.Errorf("failed to fetch the latest non-prerelease release: %w", err) } } return latestRelease, nil } // fetchLatestRelease fetches the latest release from the GitHub API func fetchLatestRelease() (Release, error) { var release Release if err := unmarshalURLBody("https://api.github.com/repos/k0sproject/k0sctl/releases/latest", &release); err != nil { return Release{}, err } return release, nil } func unmarshalURLBody(url string, o interface{}) error { client := &http.Client{ Timeout: timeOut, } resp, err := client.Get(url) if err != nil { return err } if resp.Body == nil { return fmt.Errorf("nil body") } if resp.StatusCode != 200 { return fmt.Errorf("backend returned http %d for %s", resp.StatusCode, url) } body, err := io.ReadAll(resp.Body) if err != nil { return err } if err := resp.Body.Close(); err != nil { return err } return json.Unmarshal(body, o) } // fetchLatestNonPrereleaseRelease fetches the latest non-prerelease from the GitHub API func fetchLatestNonPrereleaseRelease() (Release, error) { var releases []Release if err := unmarshalURLBody("https://api.github.com/repos/k0sproject/k0sctl/releases", &releases); err != nil { return Release{}, err } var versions k0sversion.Collection for _, v := range releases { if v.PreRelease { continue } if version, err := k0sversion.NewVersion(v.TagName); err == nil { versions = append(versions, version) } } sort.Sort(versions) latest := versions[len(versions)-1].String() for _, v := range releases { if v.TagName == latest { return v, nil } } return Release{}, fmt.Errorf("no release found") } 0707010000005C000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002200000000k0sctl-0.19.4/integration/segment0707010000005D000081A400000000000000000000000167334428000005EC000000000000000000000000000000000000002D00000000k0sctl-0.19.4/integration/segment/segment.gopackage segment import ( "runtime" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/version" segment "github.com/segmentio/analytics-go" log "github.com/sirupsen/logrus" ) // Verbose controls the verbosity of segment analytics client var Verbose bool var ctx = &segment.Context{ App: segment.AppInfo{ Name: "k0sctl", Version: version.Version, Build: version.GitCommit, Namespace: "k0s", }, OS: segment.OSInfo{ Name: runtime.GOOS + " " + runtime.GOARCH, }, Extra: map[string]interface{}{"direct": true}, } // Client for the Segment.io analytics service type Client struct { client segment.Client machineID string } // NewClient returns a new segment analytics client func NewClient(writeKey string) (*Client, error) { client, err := segment.NewWithConfig(writeKey, segment.Config{Verbose: Verbose}) if err != nil { return nil, err } id, err := analytics.MachineID() if err != nil { return nil, err } return &Client{ client: client, machineID: id, }, nil } // Publish enqueues the sending of a tracking event func (c Client) Publish(event string, props map[string]interface{}) { log.Tracef("segment event %s - properties: %+v", event, props) err := c.client.Enqueue(segment.Track{ Context: ctx, AnonymousId: c.machineID, Event: event, Properties: props, }) if err != nil { log.Debugf("failed to submit telemetry: %s", err) } } // Close the analytics connection func (c Client) Close() { c.client.Close() } 0707010000005E000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001700000000k0sctl-0.19.4/internal0707010000005F000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001D00000000k0sctl-0.19.4/internal/shell07070100000060000081A400000000000000000000000167334428000005F3000000000000000000000000000000000000002600000000k0sctl-0.19.4/internal/shell/split.gopackage shell // this is borrowed as-is from rig v2 until k0sctl is updated to use it import ( "fmt" "strings" ) // Split splits the input string respecting shell-like quoted segments. func Split(input string) ([]string, error) { //nolint:cyclop var segments []string currentSegment, ok := builderPool.Get().(*strings.Builder) if !ok { currentSegment = &strings.Builder{} } defer builderPool.Put(currentSegment) defer currentSegment.Reset() var inDoubleQuotes, inSingleQuotes, isEscaped bool for i := range len(input) { currentChar := input[i] if isEscaped { currentSegment.WriteByte(currentChar) isEscaped = false continue } switch { case currentChar == '\\' && !inSingleQuotes: isEscaped = true case currentChar == '"' && !inSingleQuotes: inDoubleQuotes = !inDoubleQuotes case currentChar == '\'' && !inDoubleQuotes: inSingleQuotes = !inSingleQuotes case currentChar == ' ' && !inDoubleQuotes && !inSingleQuotes: // Space outside quotes; delimiter for a new segment segments = append(segments, currentSegment.String()) currentSegment.Reset() default: currentSegment.WriteByte(currentChar) } } if inDoubleQuotes || inSingleQuotes { return nil, fmt.Errorf("split `%q`: %w", input, ErrMismatchedQuotes) } if isEscaped { return nil, fmt.Errorf("split `%q`: %w", input, ErrTrailingBackslash) } // Add the last segment if present if currentSegment.Len() > 0 { segments = append(segments, currentSegment.String()) } return segments, nil } 07070100000061000081A40000000000000000000000016733442800000802000000000000000000000000000000000000002800000000k0sctl-0.19.4/internal/shell/unquote.gopackage shell import ( "errors" "fmt" "strings" "sync" ) // This is borrowed from rig v2 until k0sctl is updated to use it var ( builderPool = sync.Pool{ New: func() interface{} { return &strings.Builder{} }, } // ErrMismatchedQuotes is returned when the input string has mismatched quotes when unquoting. ErrMismatchedQuotes = errors.New("mismatched quotes") // ErrTrailingBackslash is returned when the input string ends with a trailing backslash. ErrTrailingBackslash = errors.New("trailing backslash") ) // Unquote is a mostly POSIX compliant implementation of unquoting a string the same way a shell would. // Variables and command substitutions are not handled. func Unquote(input string) (string, error) { //nolint:cyclop sb, ok := builderPool.Get().(*strings.Builder) if !ok { sb = &strings.Builder{} } defer builderPool.Put(sb) defer sb.Reset() var inDoubleQuotes, inSingleQuotes, isEscaped bool for i := range len(input) { currentChar := input[i] if isEscaped { sb.WriteByte(currentChar) isEscaped = false continue } switch currentChar { case '\\': if !inSingleQuotes { // Escape works in double quotes or outside any quotes isEscaped = true } else { sb.WriteByte(currentChar) // Treat as a regular character within single quotes } case '"': if !inSingleQuotes { // Toggle double quotes only if not in single quotes inDoubleQuotes = !inDoubleQuotes } else { sb.WriteByte(currentChar) // Treat as a regular character within single quotes } case '\'': if !inDoubleQuotes { // Toggle single quotes only if not in double quotes inSingleQuotes = !inSingleQuotes } else { sb.WriteByte(currentChar) // Treat as a regular character within double quotes } default: sb.WriteByte(currentChar) } } if inDoubleQuotes || inSingleQuotes { return "", fmt.Errorf("unquote `%q`: %w", input, ErrMismatchedQuotes) } if isEscaped { return "", fmt.Errorf("unquote `%q`: %w", input, ErrTrailingBackslash) } return sb.String(), nil } 07070100000062000081A400000000000000000000000167334428000003AE000000000000000000000000000000000000002D00000000k0sctl-0.19.4/internal/shell/unquote_test.gopackage shell_test import ( "testing" "github.com/k0sproject/k0sctl/internal/shell" "github.com/stretchr/testify/require" ) func TestUnquote(t *testing.T) { t.Run("no quotes", func(t *testing.T) { out, err := shell.Unquote("foo bar") require.NoError(t, err) require.Equal(t, "foo bar", out) }) t.Run("simple quotes", func(t *testing.T) { out, err := shell.Unquote("\"foo\" 'bar'") require.NoError(t, err) require.Equal(t, "foo bar", out) }) t.Run("mid-word quotes", func(t *testing.T) { out, err := shell.Unquote("f\"o\"o b'a'r") require.NoError(t, err) require.Equal(t, "foo bar", out) }) t.Run("complex quotes", func(t *testing.T) { out, err := shell.Unquote(`'"'"'foo'"'"'`) require.NoError(t, err) require.Equal(t, `"'foo'"`, out) }) t.Run("escaped quotes", func(t *testing.T) { out, err := shell.Unquote("\\'foo\\' 'bar'") require.NoError(t, err) require.Equal(t, "'foo' bar", out) }) } 07070100000063000081A4000000000000000000000001673344280000041F000000000000000000000000000000000000001600000000k0sctl-0.19.4/main.gopackage main import ( "os" "runtime" "strings" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/cmd" log "github.com/sirupsen/logrus" // blank import to make sure versioninfo is included in the binary _ "github.com/carlmjohnson/versioninfo" // blank import to make sure versioninfo is included in the binary _ "github.com/k0sproject/k0sctl/version" ) func handlepanic() { if err := recover(); err != nil { buf := make([]byte, 1<<16) ss := runtime.Stack(buf, true) msg := string(buf[:ss]) var bt []string for _, row := range strings.Split(msg, "\n") { if !strings.HasPrefix(row, "\t") { continue } if strings.Contains(row, "main.") { continue } if strings.Contains(row, "panic") { continue } bt = append(bt, strings.TrimSpace(row)) } analytics.Client.Publish("panic", map[string]interface{}{"backtrace": strings.Join(bt, "\n")}) log.Fatalf("PANIC: %v\n", err) } } func main() { defer handlepanic() err := cmd.App.Run(os.Args) if err != nil { log.Fatal(err) } } 07070100000064000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001400000000k0sctl-0.19.4/phase07070100000065000081A40000000000000000000000016733442800000686000000000000000000000000000000000000002300000000k0sctl-0.19.4/phase/arm_prepare.gopackage phase import ( "strings" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) var etcdSupportedArchArm64Since = version.MustConstraint(">= v1.22.1+k0s.0") // PrepareArm implements a phase which fixes arm quirks type PrepareArm struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *PrepareArm) Title() string { return "Prepare ARM nodes" } // Prepare the phase func (p *PrepareArm) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { if h.Reset { return false } if h.Role == "worker" { return false } arch := h.Metadata.Arch if !strings.HasPrefix(arch, "arm") && !strings.HasPrefix(arch, "aarch") { return false } if strings.HasSuffix(arch, "64") { // 64-bit arm is supported on etcd 3.5.0+ which is included in k0s v1.22.1+k0s.0 and newer if etcdSupportedArchArm64Since.Check(p.Config.Spec.K0s.Version) { return false } } return true }) return nil } // ShouldRun is true when there are arm controllers func (p *PrepareArm) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *PrepareArm) Run() error { return p.parallelDo(p.hosts, p.etcdUnsupportedArch) } func (p *PrepareArm) etcdUnsupportedArch(h *cluster.Host) error { log.Warnf("%s: enabling ETCD_UNSUPPORTED_ARCH=%s override - you may encounter problems with etcd", h, h.Metadata.Arch) h.Environment["ETCD_UNSUPPORTED_ARCH"] = h.Metadata.Arch return nil } 07070100000066000081A40000000000000000000000016733442800000C33000000000000000000000000000000000000001E00000000k0sctl-0.19.4/phase/backup.gopackage phase import ( "fmt" "os" "path" "path/filepath" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) var _ Phase = &Backup{} var backupSinceVersion = version.MustConstraint(">= v1.21.0-rc.1+k0s.0") // Backup connect to one of the controllers and takes a backup type Backup struct { GenericPhase leader *cluster.Host } // Title returns the title for the phase func (p *Backup) Title() string { return "Take backup" } // Prepare the phase func (p *Backup) Prepare(config *v1beta1.Cluster) error { p.Config = config if !backupSinceVersion.Check(p.Config.Spec.K0s.Version) { return fmt.Errorf("the version of k0s on the host does not support taking backups") } leader := p.Config.Spec.K0sLeader() if leader.Metadata.K0sRunningVersion == nil { return fmt.Errorf("failed to find a running controller") } p.leader = leader p.leader.Metadata.IsK0sLeader = true return nil } // ShouldRun is true when there is a leader host func (p *Backup) ShouldRun() bool { return p.leader != nil } // Run the phase func (p *Backup) Run() error { h := p.leader log.Infof("%s: backing up", h) var backupDir string err := p.Wet(h, "create a tempdir using `mktemp -d`", func() error { b, err := h.Configurer.TempDir(h) if err != nil { return err } backupDir = b return nil }, func() error { backupDir = "/tmp/k0s_backup.dryrun" return nil }) if err != nil { return err } cmd := h.K0sBackupCommand(backupDir) err = p.Wet(h, fmt.Sprintf("create backup using `%s`", cmd), func() error { return h.Exec(h.K0sBackupCommand(backupDir), exec.Sudo(h)) }) if err != nil { return err } // get the name of the backup file var remoteFile string if p.IsWet() { r, err := h.ExecOutputf(`ls "%s"`, backupDir) if err != nil { return err } remoteFile = r } else { remoteFile = "k0s_backup.dryrun.tar.gz" } remotePath := path.Join(backupDir, remoteFile) defer func() { if p.IsWet() { log.Debugf("%s: cleaning up %s", h, remotePath) if err := h.Configurer.DeleteFile(h, remotePath); err != nil { log.Warnf("%s: failed to clean up backup temp file %s: %s", h, remotePath, err) } if err := h.Configurer.DeleteDir(h, backupDir, exec.Sudo(h)); err != nil { log.Warnf("%s: failed to clean up backup temp directory %s: %s", h, backupDir, err) } } else { p.DryMsg(h, "delete the tempdir") } }() localFile, err := filepath.Abs(fmt.Sprintf("k0s_backup_%d.tar.gz", time.Now().Unix())) if err != nil { return err } if p.IsWet() { // Download the file f, err := os.OpenFile(localFile, os.O_RDWR|os.O_CREATE|os.O_SYNC, 0600) if err != nil { return err } defer f.Close() if err := h.Execf(`cat "%s"`, remotePath, exec.Writer(f)); err != nil { return err } log.Infof("backup file written to %s", localFile) } else { p.DryMsgf(nil, "download the backup file to local host as %s", localFile) } return nil } 07070100000067000081A40000000000000000000000016733442800002ABA000000000000000000000000000000000000002500000000k0sctl-0.19.4/phase/configure_k0s.gopackage phase import ( "bytes" "context" "fmt" gopath "path" "slices" "time" "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" "github.com/k0sproject/version" "github.com/sergi/go-diff/diffmatchpatch" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) // "k0s default-config" was replaced with "k0s config create" in v1.23.1+k0s.0 var configCreateSinceVersion = version.MustConstraint(">= v1.23.1+k0s.0") const ( configSourceExisting int = iota configSourceDefault configSourceProvided configSourceNodeConfig ) // ConfigureK0s writes the k0s configuration to host k0s config dir type ConfigureK0s struct { GenericPhase leader *cluster.Host configSource int newBaseConfig dig.Mapping hosts cluster.Hosts } // Title returns the phase title func (p *ConfigureK0s) Title() string { return "Configure k0s" } // Prepare the phase func (p *ConfigureK0s) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() if len(p.Config.Spec.K0s.Config) > 0 { log.Debug("using provided k0s config") p.configSource = configSourceProvided p.newBaseConfig = p.Config.Spec.K0s.Config.Dup() } else if p.leader.Metadata.K0sExistingConfig != "" { log.Debug("using existing k0s config") p.configSource = configSourceExisting p.newBaseConfig = make(dig.Mapping) err := yaml.Unmarshal([]byte(p.leader.Metadata.K0sExistingConfig), &p.newBaseConfig) if err != nil { return fmt.Errorf("failed to unmarshal existing k0s config: %w", err) } } else { log.Debug("using generated default k0s config") p.configSource = configSourceDefault cfg, err := p.generateDefaultConfig() if err != nil { return fmt.Errorf("failed to generate default k0s config: %w", err) } p.newBaseConfig = make(dig.Mapping) err = yaml.Unmarshal([]byte(cfg), &p.newBaseConfig) if err != nil { return fmt.Errorf("failed to unmarshal default k0s config: %w", err) } } // convert sans from unmarshaled config into []string var sans []string oldsans := p.newBaseConfig.Dig("spec", "api", "sans") switch oldsans := oldsans.(type) { case []interface{}: for _, v := range oldsans { if s, ok := v.(string); ok { sans = append(sans, s) } } log.Tracef("converted sans from %T to []string", oldsans) case []string: sans = append(sans, oldsans...) log.Tracef("sans was readily %T", oldsans) default: // do nothing - base k0s config does not contain any existing SANs } // populate SANs with all controller addresses for i, c := range p.Config.Spec.Hosts.Controllers() { if c.Reset { continue } if !slices.Contains(sans, c.Address()) { sans = append(sans, c.Address()) log.Debugf("added controller %d address %s to spec.api.sans", i+1, c.Address()) } if c.PrivateAddress != "" && !slices.Contains(sans, c.PrivateAddress) { sans = append(sans, c.PrivateAddress) log.Debugf("added controller %d private address %s to spec.api.sans", i+1, c.PrivateAddress) } } // assign populated sans to the base config p.newBaseConfig.DigMapping("spec", "api")["sans"] = sans for _, h := range p.Config.Spec.Hosts.Controllers() { if h.Reset { continue } cfgNew, err := p.configFor(h) if err != nil { return fmt.Errorf("failed to build k0s config for %s: %w", h, err) } tempConfigPath, err := h.Configurer.TempFile(h) if err != nil { return fmt.Errorf("failed to create temporary file for config: %w", err) } defer func() { if err := h.Configurer.DeleteFile(h, tempConfigPath); err != nil { log.Warnf("%s: failed to delete temporary file %s: %s", h, tempConfigPath, err) } }() if err := h.Configurer.WriteFile(h, tempConfigPath, cfgNew, "0600"); err != nil { return err } if err := p.validateConfig(h, tempConfigPath); err != nil { return err } cfgA := make(map[string]any) cfgB := make(map[string]any) if err := yaml.Unmarshal([]byte(cfgNew), &cfgA); err != nil { return fmt.Errorf("failed to unmarshal new config: %w", err) } if err := yaml.Unmarshal([]byte(h.Metadata.K0sExistingConfig), &cfgB); err != nil { return fmt.Errorf("failed to unmarshal existing config: %w", err) } cfgAString, err := yaml.Marshal(cfgA) if err != nil { return fmt.Errorf("failed to marshal new config: %w", err) } cfgBString, err := yaml.Marshal(cfgB) if err != nil { return fmt.Errorf("failed to marshal existing config: %w", err) } if bytes.Equal(cfgAString, cfgBString) { log.Debugf("%s: configuration will not change", h) continue } log.Debugf("%s: configuration will change", h) h.Metadata.K0sNewConfig = cfgNew p.hosts = append(p.hosts, h) } return nil } // DryRun prints the actions that would be taken func (p *ConfigureK0s) DryRun() error { for _, h := range p.hosts { p.DryMsgf(h, "write k0s configuration to %s", h.Configurer.K0sConfigPath()) switch p.configSource { case configSourceDefault: p.DryMsg(h, "k0s configuration is based on a generated k0s default configuration") case configSourceExisting: p.DryMsgf(h, "k0s configuration is based on an existing k0s configuration found on %s", p.Config.Spec.K0sLeader()) case configSourceProvided: p.DryMsg(h, "k0s configuration is based on spec.k0s.config in k0sctl config") case configSourceNodeConfig: p.DryMsg(h, "k0s configuration is a generated node specific config for dynamic config clusters") } dmp := diffmatchpatch.New() diffs := dmp.DiffMain(h.Metadata.K0sExistingConfig, h.Metadata.K0sNewConfig, false) p.DryMsgf(h, "configuration changes:\n%s", dmp.DiffPrettyText(diffs)) if h.Metadata.K0sRunningVersion != nil && !h.Metadata.NeedsUpgrade { p.DryMsg(h, Colorize.BrightRed("restart the k0s service").String()) } } return nil } // ShouldRun is true when there are controllers to configure func (p *ConfigureK0s) ShouldRun() bool { return len(p.hosts) > 0 } func (p *ConfigureK0s) generateDefaultConfig() (string, error) { log.Debugf("%s: generating default configuration", p.leader) var cmd string if configCreateSinceVersion.Check(p.leader.Metadata.K0sBinaryVersion) { cmd = p.leader.Configurer.K0sCmdf("config create --data-dir=%s", p.leader.K0sDataDir()) } else { cmd = p.leader.Configurer.K0sCmdf("default-config") } cfg, err := p.leader.ExecOutput(cmd, exec.Sudo(p.leader)) if err != nil { return "", err } return cfg, nil } // Run the phase func (p *ConfigureK0s) Run() error { controllers := p.Config.Spec.Hosts.Controllers().Filter(func(h *cluster.Host) bool { return !h.Reset && len(h.Metadata.K0sNewConfig) > 0 }) return p.parallelDo(controllers, p.configureK0s) } func (p *ConfigureK0s) validateConfig(h *cluster.Host, configPath string) error { log.Infof("%s: validating configuration", h) var cmd string log.Debugf("%s: comparing k0s version %s with %s", h, p.Config.Spec.K0s.Version, configCreateSinceVersion) if h.Metadata.K0sBinaryTempFile != "" { oldK0sBinaryPath := h.Configurer.K0sBinaryPath() h.Configurer.SetPath("K0sBinaryPath", h.Metadata.K0sBinaryTempFile) defer func() { h.Configurer.SetPath("K0sBinaryPath", oldK0sBinaryPath) }() } if configCreateSinceVersion.Check(p.Config.Spec.K0s.Version) { log.Debugf("%s: comparison result true", h) cmd = h.Configurer.K0sCmdf(`config validate --config "%s"`, configPath) } else { log.Debugf("%s: comparison result false", h) cmd = h.Configurer.K0sCmdf(`validate config --config "%s"`, configPath) } var stderrBuf bytes.Buffer command, err := h.ExecStreams(cmd, nil, nil, &stderrBuf, exec.Sudo(h)) if err != nil { return fmt.Errorf("can't run spec.k0s.config validation: %w", err) } if err := command.Wait(); err != nil { return fmt.Errorf("spec.k0s.config validation failed:: %w (%s)", err, stderrBuf.String()) } return nil } func (p *ConfigureK0s) configureK0s(h *cluster.Host) error { path := h.K0sConfigPath() if h.Configurer.FileExist(h, path) { if !h.Configurer.FileContains(h, path, " generated-by-k0sctl") { newpath := path + ".old" log.Warnf("%s: an existing config was found and will be backed up as %s", h, newpath) if err := h.Configurer.MoveFile(h, path, newpath); err != nil { return err } } } log.Debugf("%s: writing k0s configuration", h) tempConfigPath, err := h.Configurer.TempFile(h) if err != nil { return fmt.Errorf("failed to create temporary file for config: %w", err) } if err := h.Configurer.WriteFile(h, tempConfigPath, h.Metadata.K0sNewConfig, "0600"); err != nil { return err } log.Infof("%s: installing new configuration", h) configPath := h.K0sConfigPath() configDir := gopath.Dir(configPath) if !h.Configurer.FileExist(h, configDir) { if err := h.Execf(`install -m 0750 -o root -g root -d "%s"`, configDir, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to create k0s configuration directory: %w", err) } } if err := h.Execf(`install -m 0600 -o root -g root "%s" "%s"`, tempConfigPath, configPath, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to install k0s configuration: %w", err) } if h.Metadata.K0sRunningVersion != nil && !h.Metadata.NeedsUpgrade { log.Infof("%s: restarting k0s service", h) if err := h.Configurer.RestartService(h, h.K0sServiceName()); err != nil { return err } log.Infof("%s: waiting for k0s service to start", h) return retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceRunningFunc(h, h.K0sServiceName())) } return nil } func (p *ConfigureK0s) configFor(h *cluster.Host) (string, error) { var cfg dig.Mapping if p.Config.Spec.K0s.DynamicConfig { if h == p.leader && h.Metadata.K0sRunningVersion == nil { log.Debugf("%s: leader will get a full config on initialize ", h) cfg = p.newBaseConfig.Dup() } else { log.Debugf("%s: using a stripped down config for dynamic config", h) cfg = p.Config.Spec.K0s.NodeConfig() } } else { cfg = p.newBaseConfig.Dup() } var addr string if h.PrivateAddress != "" { addr = h.PrivateAddress } else { addr = h.Address() } if cfg.DigString("spec", "api", "address") == "" { if onlyBindAddr, ok := cfg.Dig("spec", "api", "onlyBindToAddress").(bool); ok && onlyBindAddr { cfg.DigMapping("spec", "api")["address"] = addr } } if cfg.Dig("spec", "storage", "etcd", "peerAddress") != nil || h.PrivateAddress != "" { cfg.DigMapping("spec", "storage", "etcd")["peerAddress"] = addr } if _, ok := cfg["apiVersion"]; !ok { cfg["apiVersion"] = "k0s.k0sproject.io/v1beta1" } if _, ok := cfg["kind"]; !ok { cfg["kind"] = "ClusterConfig" } c, err := yaml.Marshal(cfg) if err != nil { return "", err } return fmt.Sprintf("# generated-by-k0sctl %s\n%s", time.Now().Format(time.RFC3339), c), nil } 07070100000068000081A40000000000000000000000016733442800000380000000000000000000000000000000000000001F00000000k0sctl-0.19.4/phase/connect.gopackage phase import ( "context" "errors" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig" log "github.com/sirupsen/logrus" ) // Connect connects to each of the hosts type Connect struct { GenericPhase } // Title for the phase func (p *Connect) Title() string { return "Connect to hosts" } // Run the phase func (p *Connect) Run() error { return p.parallelDo(p.Config.Spec.Hosts, func(h *cluster.Host) error { return retry.Timeout(context.TODO(), 10*time.Minute, func(_ context.Context) error { if err := h.Connect(); err != nil { if errors.Is(err, rig.ErrCantConnect) || strings.Contains(err.Error(), "host key mismatch") { return errors.Join(retry.ErrAbort, err) } return err } log.Infof("%s: connected", h) return nil }) }) } 07070100000069000081A40000000000000000000000016733442800000349000000000000000000000000000000000000002500000000k0sctl-0.19.4/phase/daemon_reload.gopackage phase import ( "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" log "github.com/sirupsen/logrus" ) // DaemonReload phase runs `systemctl daemon-reload` or equivalent on all hosts. type DaemonReload struct { GenericPhase } // Title for the phase func (p *DaemonReload) Title() string { return "Reload service manager" } // ShouldRun is true when there are controllers that needs to be reset func (p *DaemonReload) ShouldRun() bool { return len(p.Config.Spec.Hosts) > 0 } // Run the phase func (p *DaemonReload) Run() error { return p.parallelDo(p.Config.Spec.Hosts, func(h *cluster.Host) error { log.Infof("%s: reloading service manager", h) if err := h.Configurer.DaemonReload(h); err != nil { log.Warnf("%s: failed to reload service manager: %s", h, err.Error()) } return nil }) } 0707010000006A000081A400000000000000000000000167334428000003DE000000000000000000000000000000000000002B00000000k0sctl-0.19.4/phase/default_k0s_version.gopackage phase import ( "fmt" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) type DefaultK0sVersion struct { GenericPhase } func (p *DefaultK0sVersion) ShouldRun() bool { return p.Config.Spec.K0s.Version == nil || p.Config.Spec.K0s.Version.IsZero() } func (p *DefaultK0sVersion) Title() string { return "Set k0s version" } func (p *DefaultK0sVersion) Run() error { isStable := p.Config.Spec.K0s.VersionChannel == "" || p.Config.Spec.K0s.VersionChannel == "stable" var msg string if isStable { msg = "latest stable k0s version" } else { msg = "latest k0s version including pre-releases" } log.Info("Looking up ", msg) latest, err := version.LatestByPrerelease(!isStable) if err != nil { return fmt.Errorf("failed to look up k0s version online - try setting spec.k0s.version manually: %w", err) } log.Infof("Using k0s version %s", latest) p.Config.Spec.K0s.Version = latest p.Config.Spec.K0s.Metadata.VersionDefaulted = true return nil } 0707010000006B000081A40000000000000000000000016733442800000626000000000000000000000000000000000000002100000000k0sctl-0.19.4/phase/detect_os.gopackage phase import ( "strings" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" // anonymous import is needed to load the os configurers _ "github.com/k0sproject/k0sctl/configurer" // anonymous import is needed to load the os configurers _ "github.com/k0sproject/k0sctl/configurer/linux" // anonymous import is needed to load the os configurers _ "github.com/k0sproject/k0sctl/configurer/linux/enterpriselinux" log "github.com/sirupsen/logrus" ) // DetectOS performs remote OS detection type DetectOS struct { GenericPhase } // Title for the phase func (p *DetectOS) Title() string { return "Detect host operating systems" } // Run the phase func (p *DetectOS) Run() error { return p.parallelDo(p.Config.Spec.Hosts, func(h *cluster.Host) error { if h.OSIDOverride != "" { log.Infof("%s: OS ID has been manually set to %s", h, h.OSIDOverride) } if err := h.ResolveConfigurer(); err != nil { p.SetProp("missing-support", h.OSVersion.String()) if h.OSVersion.IDLike != "" { log.Debugf("%s: trying to find a fallback OS support module for %s using os-release ID_LIKE '%s'", h, h.OSVersion.String(), h.OSVersion.IDLike) for _, id := range strings.Split(h.OSVersion.IDLike, " ") { h.OSVersion.ID = id if err := h.ResolveConfigurer(); err == nil { log.Warnf("%s: using '%s' as OS support fallback for %s", h, id, h.OSVersion.String()) return nil } } } return err } os := h.OSVersion.String() p.IncProp(os) log.Infof("%s: is running %s", h, os) return nil }) } 0707010000006C000081A4000000000000000000000001673344280000032F000000000000000000000000000000000000002200000000k0sctl-0.19.4/phase/disconnect.gopackage phase import ( "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) // Disconnect disconnects from the hosts type Disconnect struct { GenericPhase } // Title for the phase func (p *Disconnect) Title() string { return "Disconnect from hosts" } // DryRun cleans up the temporary k0s binary from the hosts func (p *Disconnect) DryRun() error { _ = p.Config.Spec.Hosts.ParallelEach(func(h *cluster.Host) error { if h.Metadata.K0sBinaryTempFile != "" && h.Configurer.FileExist(h, h.Metadata.K0sBinaryTempFile) { _ = h.Configurer.DeleteFile(h, h.Metadata.K0sBinaryTempFile) } return nil }) return p.Run() } // Run the phase func (p *Disconnect) Run() error { return p.Config.Spec.Hosts.ParallelEach(func(h *cluster.Host) error { h.Disconnect() return nil }) } 0707010000006D000081A40000000000000000000000016733442800000EB1000000000000000000000000000000000000002900000000k0sctl-0.19.4/phase/download_binaries.gopackage phase import ( "fmt" "io" "net/http" "os" "path" "strings" "github.com/adrg/xdg" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) // DownloadBinaries downloads k0s binaries to localohost temp files type DownloadBinaries struct { GenericPhase hosts []*cluster.Host } // Title for the phase func (p *DownloadBinaries) Title() string { return "Download k0s binaries to local host" } // Prepare the phase func (p *DownloadBinaries) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return !h.Reset && h.UploadBinary && !h.Metadata.K0sBinaryVersion.Equal(config.Spec.K0s.Version) }) return nil } // ShouldRun is true when the phase should be run func (p *DownloadBinaries) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *DownloadBinaries) Run() error { var bins binaries for _, h := range p.hosts { if bin := bins.find(h.Configurer.Kind(), h.Metadata.Arch); bin != nil { continue } bin := &binary{arch: h.Metadata.Arch, os: h.Configurer.Kind(), version: p.Config.Spec.K0s.Version} // find configuration defined binpaths and use instead of downloading a new one for _, v := range p.hosts { if v.Metadata.Arch == bin.arch && v.Configurer.Kind() == bin.os && v.K0sBinaryPath != "" { bin.path = h.K0sBinaryPath } } bins = append(bins, bin) } for _, bin := range bins { if bin.path != "" { continue } if err := bin.download(); err != nil { return err } } for _, h := range p.hosts { if h.K0sBinaryPath == "" { if bin := bins.find(h.Configurer.Kind(), h.Metadata.Arch); bin != nil { h.UploadBinaryPath = bin.path } } else { h.UploadBinaryPath = h.K0sBinaryPath } } return nil } type binary struct { arch string os string version *version.Version path string } func (b *binary) download() error { fn := path.Join("k0sctl", "k0s", b.os, b.arch, "k0s-"+strings.TrimPrefix(b.version.String(), "v")+b.ext()) p, err := xdg.SearchCacheFile(fn) if err == nil { b.path = p return nil } p, err = xdg.CacheFile(fn) if err != nil { return err } if err := b.downloadTo(p); err != nil { return err } b.path = p log.Infof("using k0s binary from %s for %s-%s", b.path, b.os, b.arch) return nil } func (b binary) ext() string { if b.os == "windows" { return ".exe" } return "" } func (b binary) url() string { v := strings.ReplaceAll(strings.TrimPrefix(b.version.String(), "v"), "+", "%2B") return fmt.Sprintf("https://github.com/k0sproject/k0s/releases/download/v%[1]s/k0s-v%[1]s-%[2]s%[3]s", v, b.arch, b.ext()) } func (b binary) downloadTo(path string) error { log.Infof("downloading k0s version %s binary for %s-%s from %s", b.version, b.os, b.arch, b.url()) var err error f, err := os.Create(path) if err != nil { return err } defer func() { if err != nil { err = os.Remove(path) if err != nil { log.Warnf("failed to remove broken download at %s: %s", path, err.Error()) } } }() resp, err := http.Get(b.url()) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to get k0s binary (http %d)", resp.StatusCode) } _, err = io.Copy(f, resp.Body) if err != nil { return err } if err = f.Close(); err == nil { return err } log.Infof("cached k0s binary to %s", path) return nil } type binaries []*binary func (b binaries) find(os, arch string) *binary { for _, v := range b { if v.arch == arch && v.os == os { return v } } return nil } 0707010000006E000081A40000000000000000000000016733442800000890000000000000000000000000000000000000002400000000k0sctl-0.19.4/phase/download_k0s.gopackage phase import ( "fmt" "strconv" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // DownloadK0s performs k0s online download on the hosts type DownloadK0s struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *DownloadK0s) Title() string { return "Download k0s on hosts" } // Prepare the phase func (p *DownloadK0s) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { // Nothing to download if h.UploadBinary { return false } // No need to download, host is going to be reset if h.Reset { return false } // The version on host is already same as the target version if p.Config.Spec.K0s.Version.Equal(h.Metadata.K0sBinaryVersion) { log.Debugf("%s: k0s version on target host is already %s", h, h.Metadata.K0sBinaryVersion) return false } return true }) return nil } // ShouldRun is true when the phase should be run func (p *DownloadK0s) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *DownloadK0s) Run() error { return p.parallelDo(p.hosts, p.downloadK0s) } func (p *DownloadK0s) downloadK0s(h *cluster.Host) error { tmp := h.Configurer.K0sBinaryPath() + ".tmp." + strconv.Itoa(int(time.Now().UnixNano())) log.Infof("%s: downloading k0s %s", h, p.Config.Spec.K0s.Version) if h.K0sDownloadURL != "" { expandedURL := h.ExpandTokens(h.K0sDownloadURL, p.Config.Spec.K0s.Version) log.Infof("%s: downloading k0s binary from %s", h, expandedURL) if err := h.Configurer.DownloadURL(h, expandedURL, tmp, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to download k0s binary: %w", err) } } else if err := h.Configurer.DownloadK0s(h, tmp, p.Config.Spec.K0s.Version, h.Metadata.Arch, exec.Sudo(h)); err != nil { return err } if err := h.Execf(`chmod +x "%s"`, tmp, exec.Sudo(h)); err != nil { log.Warnf("%s: failed to chmod k0s temp binary: %v", h, err.Error()) } h.Metadata.K0sBinaryTempFile = tmp return nil } 0707010000006F000081A4000000000000000000000001673344280000093F000000000000000000000000000000000000002400000000k0sctl-0.19.4/phase/gather_facts.gopackage phase import ( "fmt" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) // Note: Passwordless sudo has not yet been confirmed when this runs // GatherFacts gathers information about hosts, such as if k0s is already up and running type GatherFacts struct { GenericPhase SkipMachineIDs bool } // K0s doesn't rely on unique machine IDs anymore since v1.30. var uniqueMachineIDVersion = version.MustConstraint("< v1.30") // Title for the phase func (p *GatherFacts) Title() string { return "Gather host facts" } // Run the phase func (p *GatherFacts) Run() error { return p.parallelDo(p.Config.Spec.Hosts, p.investigateHost) } func (p *GatherFacts) investigateHost(h *cluster.Host) error { p.IncProp(h.Role) output, err := h.Configurer.Arch(h) if err != nil { return err } h.Metadata.Arch = output if !p.SkipMachineIDs && uniqueMachineIDVersion.Check(p.Config.Spec.K0s.Version) { id, err := h.Configurer.MachineID(h) if err != nil { return err } h.Metadata.MachineID = id } p.IncProp(h.Metadata.Arch) if extra := h.InstallFlags.GetValue("--kubelet-extra-args"); extra != "" { ef := cluster.Flags{extra} if over := ef.GetValue("--hostname-override"); over != "" { if h.HostnameOverride != "" && h.HostnameOverride != over { return fmt.Errorf("hostname and installFlags kubelet-extra-args hostname-override mismatch, only define either one") } h.HostnameOverride = over } } if h.HostnameOverride != "" { log.Infof("%s: using %s from configuration as hostname", h, h.HostnameOverride) h.Metadata.Hostname = h.HostnameOverride } else { n := h.Configurer.Hostname(h) if n == "" { return fmt.Errorf("%s: failed to resolve a hostname", h) } h.Metadata.Hostname = n log.Infof("%s: using %s as hostname", h, n) } if h.PrivateAddress == "" { if h.PrivateInterface == "" { if iface, err := h.Configurer.PrivateInterface(h); err == nil { h.PrivateInterface = iface log.Infof("%s: discovered %s as private interface", h, iface) } } if h.PrivateInterface != "" { if addr, err := h.Configurer.PrivateAddress(h, h.PrivateInterface, h.Address()); err == nil { h.PrivateAddress = addr log.Infof("%s: discovered %s as private address", h, addr) } } } return nil } 07070100000070000081A40000000000000000000000016733442800002AC3000000000000000000000000000000000000002800000000k0sctl-0.19.4/phase/gather_k0s_facts.gopackage phase import ( "context" "encoding/json" "fmt" "net" "net/url" "path" "strings" "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/rig/exec" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) type k0sstatus struct { Version *version.Version `json:"Version"` Pid int `json:"Pid"` PPid int `json:"PPid"` Role string `json:"Role"` SysInit string `json:"SysInit"` StubFile string `json:"StubFile"` Workloads bool `json:"Workloads"` Args []string `json:"Args"` ClusterConfig dig.Mapping `json:"ClusterConfig"` K0sVars dig.Mapping `json:"K0sVars"` } func (k *k0sstatus) isSingle() bool { for _, a := range k.Args { if a == "--single=true" { return true } } return false } // GatherK0sFacts gathers information about hosts, such as if k0s is already up and running type GatherK0sFacts struct { GenericPhase leader *cluster.Host hosts cluster.Hosts } // Title for the phase func (p *GatherK0sFacts) Title() string { return "Gather k0s facts" } // Prepare finds hosts with k0s installed func (p *GatherK0sFacts) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return h.Exec(h.Configurer.K0sCmdf("version"), exec.Sudo(h)) == nil }) return nil } // ShouldRun is true when there are hosts that need to be connected func (p *GatherK0sFacts) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *GatherK0sFacts) Run() error { var controllers cluster.Hosts = p.hosts.Controllers() if err := p.parallelDo(controllers, p.investigateK0s); err != nil { return err } p.leader = p.Config.Spec.K0sLeader() p.leader.Metadata.IsK0sLeader = true if id, err := p.Config.Spec.K0s.GetClusterID(p.leader); err == nil { p.Config.Spec.K0s.Metadata.ClusterID = id p.SetProp("clusterID", id) } if err := p.investigateEtcd(); err != nil { return err } var workers cluster.Hosts = p.hosts.Workers() if err := p.parallelDo(workers, p.investigateK0s); err != nil { return err } return nil } func (p *GatherK0sFacts) isInternalEtcd() bool { if p.leader.Role != "controller" && p.leader.Role != "controller+worker" { return false } if p.leader.Metadata.K0sRunningVersion == nil { return false } if p.Config.Spec.K0s == nil || p.Config.Spec.K0s.Config == nil { log.Debugf("%s: k0s config not found, expecting default internal etcd", p.leader) return true } log.Debugf("%s: checking storage config for etcd", p.leader) if storageConfig, ok := p.Config.Spec.K0s.Config.Dig("spec", "storage").(dig.Mapping); ok { storageType := storageConfig.DigString("type") switch storageType { case "etcd": if _, ok := storageConfig.Dig("etcd", "externalCluster").(dig.Mapping); ok { log.Debugf("%s: storage is configured with external etcd", p.leader) return false } log.Debugf("%s: storage type is etcd", p.leader) return true case "": log.Debugf("%s: storage type is default", p.leader) return true default: log.Debugf("%s: storage type is %s", p.leader, storageType) return false } } log.Debugf("%s: storage config not found, expecting default internal etcd", p.leader) return true } func (p *GatherK0sFacts) investigateEtcd() error { if !p.isInternalEtcd() { log.Debugf("%s: skipping etcd member list", p.leader) return nil } if err := p.listEtcdMembers(p.leader); err != nil { return err } return nil } func (p *GatherK0sFacts) listEtcdMembers(h *cluster.Host) error { log.Infof("%s: listing etcd members", h) // etcd member-list outputs json like: // {"members":{"controller0":"https://172.17.0.2:2380","controller1":"https://172.17.0.3:2380"}} // on versions like ~1.21.x etcd member-list outputs to stderr with extra fields (from logrus). output, err := h.ExecOutput(h.Configurer.K0sCmdf("etcd member-list --data-dir=%s 2>&1", h.K0sDataDir()), exec.Sudo(h)) if err != nil { return fmt.Errorf("failed to run list etcd members command: %w", err) } result := make(map[string]any) if err := json.Unmarshal([]byte(output), &result); err != nil { return fmt.Errorf("failed to decode etcd member-list output: %w", err) } etcdMembers := []string{} if members, ok := result["members"].(map[string]any); ok { for _, urlField := range members { urlFieldStr, ok := urlField.(string) if ok { memberURL, err := url.Parse(urlFieldStr) if err != nil { return fmt.Errorf("failed to parse etcd member URL: %w", err) } memberHost, _, err := net.SplitHostPort(memberURL.Host) if err != nil { return fmt.Errorf("failed to split etcd member URL: %w", err) } log.Debugf("%s: detected etcd member %s", h, memberHost) etcdMembers = append(etcdMembers, memberHost) } } } p.Config.Metadata.EtcdMembers = etcdMembers return nil } func (p *GatherK0sFacts) investigateK0s(h *cluster.Host) error { output, err := h.ExecOutput(h.Configurer.K0sCmdf("version"), exec.Sudo(h)) if err != nil { log.Debugf("%s: no 'k0s' binary in PATH", h) return nil } binVersion, err := version.NewVersion(strings.TrimSpace(output)) if err != nil { return fmt.Errorf("failed to parse installed k0s version: %w", err) } h.Metadata.K0sBinaryVersion = binVersion log.Debugf("%s: has k0s binary version %s", h, h.Metadata.K0sBinaryVersion) if h.IsController() && h.Configurer.FileExist(h, h.K0sConfigPath()) { cfg, err := h.Configurer.ReadFile(h, h.K0sConfigPath()) if cfg != "" && err == nil { log.Infof("%s: found existing configuration", h) h.Metadata.K0sExistingConfig = cfg } } var existingServiceScript string for _, svc := range []string{"k0scontroller", "k0sworker", "k0sserver"} { if path, err := h.Configurer.ServiceScriptPath(h, svc); err == nil && path != "" { existingServiceScript = path break } } output, err = h.ExecOutput(h.Configurer.K0sCmdf("status -o json"), exec.Sudo(h)) if err != nil { if existingServiceScript == "" { log.Debugf("%s: an existing k0s instance is not running and does not seem to have been installed as a service", h) return nil } if Force { log.Warnf("%s: an existing k0s instance is not running but has been installed as a service at %s - ignoring because --force was given", h, existingServiceScript) return nil } return fmt.Errorf("k0s doesn't appear to be running but has been installed as a service at %s - please remove it or start the service", existingServiceScript) } if existingServiceScript == "" { return fmt.Errorf("k0s is running but has not been installed as a service, possibly a non-k0sctl managed host or a broken installation - you can try to reset the host by setting `reset: true` on it") } status := k0sstatus{} if err := json.Unmarshal([]byte(output), &status); err != nil { log.Warnf("%s: failed to decode k0s status output: %s", h, err.Error()) return nil } if status.Version == nil || status.Role == "" || status.Pid == 0 { log.Debugf("%s: k0s is not running", h) return nil } switch status.Role { case "server": status.Role = "controller" case "server+worker": status.Role = "controller+worker" case "controller": if status.Workloads { if status.isSingle() { status.Role = "single" } else { status.Role = "controller+worker" } } } if status.Role != h.Role { return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported", h, h.Role, status.Role) } h.Metadata.K0sRunningVersion = status.Version if p.Config.Spec.K0s.Version == nil { p.Config.Spec.K0s.Version = status.Version } h.Metadata.NeedsUpgrade = p.needsUpgrade(h) var args cluster.Flags if len(status.Args) > 2 { // status.Args contains the binary path and the role as the first two elements, which we can ignore here. for _, a := range status.Args[2:] { args.Add(a) } } h.Metadata.K0sStatusArgs = args log.Infof("%s: is running k0s %s version %s", h, h.Role, h.Metadata.K0sRunningVersion) if h.IsController() { for _, a := range h.Metadata.K0sStatusArgs { if strings.HasPrefix(a, "--enable-dynamic-config") && !strings.HasSuffix(a, "false") { if !p.Config.Spec.K0s.DynamicConfig { log.Warnf("%s: controller has dynamic config enabled, but spec.k0s.dynamicConfig was not set in configuration, proceeding in dynamic config mode", h) p.Config.Spec.K0s.DynamicConfig = true } } } if h.InstallFlags.Include("--enable-dynamic-config") { if val := h.InstallFlags.GetValue("--enable-dynamic-config"); val != "false" { if !p.Config.Spec.K0s.DynamicConfig { log.Warnf("%s: controller has --enable-dynamic-config in installFlags, but spec.k0s.dynamicConfig was not set in configuration, proceeding in dynamic config mode", h) } p.Config.Spec.K0s.DynamicConfig = true } } if p.Config.Spec.K0s.DynamicConfig { h.InstallFlags.AddOrReplace("--enable-dynamic-config") } } if h.Role == "controller+worker" && !h.NoTaints { log.Warnf("%s: the controller+worker node will not schedule regular workloads without toleration for node-role.kubernetes.io/master:NoSchedule unless 'noTaints: true' is set", h) } if h.Metadata.NeedsUpgrade { log.Warnf("%s: k0s will be upgraded", h) } if !h.IsController() { log.Infof("%s: checking if worker %s has joined", p.leader, h.Metadata.Hostname) if err := node.KubeNodeReadyFunc(h)(context.Background()); err != nil { log.Debugf("%s: failed to get ready status: %s", h, err.Error()) } else { h.Metadata.Ready = true } } return nil } func (p *GatherK0sFacts) needsUpgrade(h *cluster.Host) bool { if h.Reset { return false } // If supplimental files or a k0s binary have been specified explicitly, // always upgrade. This covers the scenario where a user moves from a // default-install cluster to one fed by OCI image bundles (ie. airgap) for _, f := range h.Files { if f.IsURL() { log.Debugf("%s: marked for upgrade because there are URL source file uploads for the host", h) return true } for _, s := range f.Sources { dest := f.DestinationFile if dest == "" { dest = path.Join(f.DestinationDir, s.Path) } src := path.Join(f.Base, s.Path) if h.FileChanged(src, dest) { log.Debugf("%s: marked for upgrade because file was changed for upload %s", h, src) return true } } } if h.K0sBinaryPath != "" && h.FileChanged(h.K0sBinaryPath, h.Configurer.K0sBinaryPath()) { log.Debugf("%s: marked for upgrade because of a static k0s binary path %s", h, h.K0sBinaryPath) return true } return p.Config.Spec.K0s.Version.GreaterThan(h.Metadata.K0sRunningVersion) } 07070100000071000081A4000000000000000000000001673344280000035A000000000000000000000000000000000000002D00000000k0sctl-0.19.4/phase/gather_k0s_facts_test.gopackage phase import ( "testing" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/version" "github.com/stretchr/testify/require" ) func TestNeedsUpgrade(t *testing.T) { cfg := &v1beta1.Cluster{ Spec: &cluster.Spec{ K0s: &cluster.K0s{ Version: version.MustParse("1.23.3+k0s.1"), }, }, } h := &cluster.Host{ Metadata: cluster.HostMetadata{ K0sRunningVersion: version.MustParse("1.23.3+k0s.1"), }, } p := GatherK0sFacts{GenericPhase: GenericPhase{Config: cfg}} require.False(t, p.needsUpgrade(h)) h.Metadata.K0sRunningVersion = version.MustParse("1.23.3+k0s.2") require.False(t, p.needsUpgrade(h)) h.Metadata.K0sRunningVersion = version.MustParse("1.23.3+k0s.0") require.True(t, p.needsUpgrade(h)) } 07070100000072000081A4000000000000000000000001673344280000074C000000000000000000000000000000000000002500000000k0sctl-0.19.4/phase/generic_phase.gopackage phase import ( "fmt" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) // GenericPhase is a basic phase which gets a config via prepare, sets it into p.Config type GenericPhase struct { analytics.Phase Config *v1beta1.Cluster manager *Manager } // GetConfig is an accessor to phase Config func (p *GenericPhase) GetConfig() *v1beta1.Cluster { return p.Config } // Prepare the phase func (p *GenericPhase) Prepare(c *v1beta1.Cluster) error { p.Config = c return nil } // Wet is a shorthand for manager.Wet func (p *GenericPhase) Wet(host fmt.Stringer, msg string, funcs ...errorfunc) error { return p.manager.Wet(host, msg, funcs...) } // IsWet returns true if manager is in dry-run mode func (p *GenericPhase) IsWet() bool { return !p.manager.DryRun } // DryMsg is a shorthand for manager.DryMsg func (p *GenericPhase) DryMsg(host fmt.Stringer, msg string) { p.manager.DryMsg(host, msg) } // DryMsgf is a shorthand for manager.DryMsg + fmt.Sprintf func (p *GenericPhase) DryMsgf(host fmt.Stringer, msg string, args ...any) { p.manager.DryMsg(host, fmt.Sprintf(msg, args...)) } // SetManager adds a reference to the phase manager func (p *GenericPhase) SetManager(m *Manager) { p.manager = m } func (p *GenericPhase) parallelDo(hosts cluster.Hosts, funcs ...func(h *cluster.Host) error) error { if p.manager.Concurrency == 0 { return hosts.ParallelEach(funcs...) } return hosts.BatchedParallelEach(p.manager.Concurrency, funcs...) } func (p *GenericPhase) parallelDoUpload(hosts cluster.Hosts, funcs ...func(h *cluster.Host) error) error { if p.manager.Concurrency == 0 { return hosts.ParallelEach(funcs...) } return hosts.BatchedParallelEach(p.manager.ConcurrentUploads, funcs...) } 07070100000073000081A40000000000000000000000016733442800000818000000000000000000000000000000000000002600000000k0sctl-0.19.4/phase/get_kubeconfig.gopackage phase import ( "fmt" "github.com/alessio/shellescape" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" "k8s.io/client-go/tools/clientcmd" ) // GetKubeconfig is a phase to get and dump the admin kubeconfig type GetKubeconfig struct { GenericPhase APIAddress string } // Title for the phase func (p *GetKubeconfig) Title() string { return "Get admin kubeconfig" } var readKubeconfig = func(h *cluster.Host) (string, error) { output, err := h.ExecOutput(h.Configurer.K0sCmdf("kubeconfig admin --data-dir=%s", shellescape.Quote(h.K0sDataDir())), exec.Sudo(h), exec.HideOutput()) if err != nil { return "", fmt.Errorf("get kubeconfig from host: %w", err) } return output, nil } func (p *GetKubeconfig) DryRun() error { p.DryMsg(p.Config.Spec.Hosts.Controllers()[0], "get admin kubeconfig") return nil } // Run the phase func (p *GetKubeconfig) Run() error { h := p.Config.Spec.Hosts.Controllers()[0] output, err := readKubeconfig(h) if err != nil { return fmt.Errorf("read kubeconfig from host: %w", err) } if p.APIAddress == "" { p.APIAddress = p.Config.Spec.KubeAPIURL() } cfgString, err := kubeConfig(output, p.Config.Metadata.Name, p.APIAddress) if err != nil { return err } p.Config.Metadata.Kubeconfig = cfgString return nil } // kubeConfig reads in the raw kubeconfig and changes the given address // and cluster name into it func kubeConfig(raw string, name string, address string) (string, error) { cfg, err := clientcmd.Load([]byte(raw)) if err != nil { return "", err } cfg.Clusters[name] = cfg.Clusters["local"] delete(cfg.Clusters, "local") cfg.Clusters[name].Server = address cfg.Contexts[name] = cfg.Contexts["Default"] delete(cfg.Contexts, "Default") cfg.Contexts[name].Cluster = name cfg.Contexts[name].AuthInfo = "admin" cfg.CurrentContext = name cfg.AuthInfos["admin"] = cfg.AuthInfos["user"] delete(cfg.AuthInfos, "user") out, err := clientcmd.Write(*cfg) if err != nil { return "", err } return string(out), nil } 07070100000074000081A40000000000000000000000016733442800000682000000000000000000000000000000000000002B00000000k0sctl-0.19.4/phase/get_kubeconfig_test.gopackage phase import ( "strings" "testing" "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig" "github.com/stretchr/testify/require" "k8s.io/client-go/tools/clientcmd" ) func fakeReader(h *cluster.Host) (string, error) { return strings.ReplaceAll(`apiVersion: v1 clusters: - cluster: server: https://localhost:6443 name: local contexts: - context: cluster: local user: user name: Default current-context: Default kind: Config preferences: {} users: - name: user user: `, "\t", " "), nil } func TestGetKubeconfig(t *testing.T) { cfg := &v1beta1.Cluster{ Metadata: &v1beta1.ClusterMetadata{ Name: "k0s", }, Spec: &cluster.Spec{ K0s: &cluster.K0s{Config: dig.Mapping{}}, Hosts: []*cluster.Host{ {Role: "controller", Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.1", Port: 22}}}, }, }, } origReadKubeconfig := readKubeconfig defer func() { readKubeconfig = origReadKubeconfig }() readKubeconfig = fakeReader p := GetKubeconfig{GenericPhase: GenericPhase{Config: cfg}} require.NoError(t, p.Run()) conf, err := clientcmd.Load([]byte(cfg.Metadata.Kubeconfig)) require.NoError(t, err) require.Equal(t, "https://10.0.0.1:6443", conf.Clusters["k0s"].Server) cfg.Spec.Hosts[0].Connection.SSH.Address = "abcd:efgh:ijkl:mnop" p.APIAddress = "" require.NoError(t, p.Run()) conf, err = clientcmd.Load([]byte(cfg.Metadata.Kubeconfig)) require.NoError(t, err) require.Equal(t, "https://[abcd:efgh:ijkl:mnop]:6443", conf.Clusters["k0s"].Server) } 07070100000075000081A40000000000000000000000016733442800000FC1000000000000000000000000000000000000002600000000k0sctl-0.19.4/phase/initialize_k0s.gopackage phase import ( "context" "fmt" "strings" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // InitializeK0s sets up the "initial" k0s controller type InitializeK0s struct { GenericPhase leader *cluster.Host } // Title for the phase func (p *InitializeK0s) Title() string { return "Initialize the k0s cluster" } // Prepare the phase func (p *InitializeK0s) Prepare(config *v1beta1.Cluster) error { p.Config = config leader := p.Config.Spec.K0sLeader() if leader.Metadata.K0sRunningVersion == nil { p.leader = leader } return nil } // ShouldRun is true when there is a leader host func (p *InitializeK0s) ShouldRun() bool { return p.leader != nil && !p.leader.Reset } // CleanUp cleans up the environment override file func (p *InitializeK0s) CleanUp() { h := p.leader log.Infof("%s: cleaning up", h) if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", h, err.Error()) } } if h.Metadata.K0sInstalled { if err := h.Exec(h.Configurer.K0sCmdf("reset --data-dir=%s", h.K0sDataDir()), exec.Sudo(h)); err != nil { log.Warnf("%s: k0s reset failed", h) } } } // Run the phase func (p *InitializeK0s) Run() error { h := p.leader h.Metadata.IsK0sLeader = true if p.Config.Spec.K0s.DynamicConfig || (h.InstallFlags.Include("--enable-dynamic-config") && h.InstallFlags.GetValue("--enable-dynamic-config") != "false") { p.Config.Spec.K0s.DynamicConfig = true h.InstallFlags.AddOrReplace("--enable-dynamic-config") p.SetProp("dynamic-config", true) } if Force { log.Warnf("%s: --force given, using k0s install with --force", h) h.InstallFlags.AddOrReplace("--force=true") } log.Infof("%s: installing k0s controller", h) cmd, err := h.K0sInstallCommand() if err != nil { return err } err = p.Wet(p.leader, fmt.Sprintf("install first k0s controller using `%s`", strings.ReplaceAll(cmd, p.leader.Configurer.K0sBinaryPath(), "k0s")), func() error { return h.Exec(cmd, exec.Sudo(h)) }, func() error { p.leader.Metadata.DryRunFakeLeader = true return nil }) if err != nil { return err } h.Metadata.K0sInstalled = true if len(h.Environment) > 0 { err = p.Wet(h, "configure k0s service environment variables", func() error { log.Infof("%s: updating service environment", h) return h.Configurer.UpdateServiceEnvironment(h, h.K0sServiceName(), h.Environment) }, func() error { for k, v := range h.Environment { p.DryMsgf(h, "%s=<%d characters>", k, len(v)) } return nil }) if err != nil { return err } } err = p.Wet(h, "start k0s service", func() error { if err := h.Configurer.StartService(h, h.K0sServiceName()); err != nil { return err } log.Infof("%s: waiting for the k0s service to start", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceRunningFunc(h, h.K0sServiceName())); err != nil { return err } log.Infof("%s: waiting for kubernetes api to respond", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.KubeAPIReadyFunc(h, p.Config)); err != nil { return err } return nil }) if err != nil { return err } if p.IsWet() && p.Config.Spec.K0s.DynamicConfig { if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.K0sDynamicConfigReadyFunc(h)); err != nil { return fmt.Errorf("dynamic config reconciliation failed: %w", err) } } h.Metadata.K0sRunningVersion = p.Config.Spec.K0s.Version h.Metadata.K0sBinaryVersion = p.Config.Spec.K0s.Version h.Metadata.Ready = true if p.IsWet() { if id, err := p.Config.Spec.K0s.GetClusterID(h); err == nil { p.Config.Spec.K0s.Metadata.ClusterID = id p.SetProp("clusterID", id) } } return nil } 07070100000076000081A40000000000000000000000016733442800000A51000000000000000000000000000000000000002800000000k0sctl-0.19.4/phase/install_binaries.gopackage phase import ( "fmt" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" "github.com/sirupsen/logrus" ) // InstallBinaries installs the k0s binaries from the temp location of UploadBinaries or InstallBinaries type InstallBinaries struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *InstallBinaries) Title() string { return "Install k0s binaries on hosts" } // Prepare the phase func (p *InstallBinaries) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { if h.Reset && h.Metadata.K0sBinaryVersion != nil { return false } // Upgrade is handled in UpgradeControllers/UpgradeWorkers phases if h.Metadata.NeedsUpgrade { return false } return h.Metadata.K0sBinaryTempFile != "" }) return nil } // ShouldRun is true when the phase should be run func (p *InstallBinaries) ShouldRun() bool { return len(p.hosts) > 0 || !p.IsWet() } // DryRun reports what would happen if Run is called. func (p *InstallBinaries) DryRun() error { return p.parallelDo( p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return h.Metadata.K0sBinaryTempFile != "" }), func(h *cluster.Host) error { p.DryMsgf(h, "install k0s %s binary from %s to %s", p.Config.Spec.K0s.Version, h.Metadata.K0sBinaryTempFile, h.Configurer.K0sBinaryPath()) if err := h.Execf(`chmod +x "%s"`, h.Metadata.K0sBinaryTempFile, exec.Sudo(h)); err != nil { logrus.Warnf("%s: failed to chmod k0s temp binary for dry-run: %s", h, err.Error()) } h.Configurer.SetPath("K0sBinaryPath", h.Metadata.K0sBinaryTempFile) h.Metadata.K0sBinaryVersion = p.Config.Spec.K0s.Version return nil }, ) } // Run the phase func (p *InstallBinaries) Run() error { return p.parallelDo(p.hosts, p.installBinary) } func (p *InstallBinaries) installBinary(h *cluster.Host) error { if err := h.UpdateK0sBinary(h.Metadata.K0sBinaryTempFile, p.Config.Spec.K0s.Version); err != nil { return fmt.Errorf("failed to install k0s binary: %w", err) } return nil } func (p *InstallBinaries) CleanUp() { err := p.parallelDo(p.hosts, func(h *cluster.Host) error { if h.Metadata.K0sBinaryTempFile == "" { return nil } logrus.Infof("%s: cleaning up k0s binary tempfile", h) if err := h.Configurer.DeleteFile(h, h.Metadata.K0sBinaryTempFile); err != nil { return fmt.Errorf("clean up tempfile: %w", err) } return nil }) if err != nil { logrus.Debugf("failed to clean up tempfiles: %v", err) } } 07070100000077000081A4000000000000000000000001673344280000171B000000000000000000000000000000000000002B00000000k0sctl-0.19.4/phase/install_controllers.gopackage phase import ( "context" "fmt" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // InstallControllers installs k0s controllers and joins them to the cluster type InstallControllers struct { GenericPhase hosts cluster.Hosts leader *cluster.Host } // Title for the phase func (p *InstallControllers) Title() string { return "Install controllers" } // Prepare the phase func (p *InstallControllers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() p.hosts = p.Config.Spec.Hosts.Controllers().Filter(func(h *cluster.Host) bool { return !h.Reset && !h.Metadata.NeedsUpgrade && (h != p.leader && h.Metadata.K0sRunningVersion == nil) }) return nil } // ShouldRun is true when there are controllers func (p *InstallControllers) ShouldRun() bool { return len(p.hosts) > 0 } // CleanUp cleans up the environment override files on hosts func (p *InstallControllers) CleanUp() { _ = p.After() _ = p.hosts.Filter(func(h *cluster.Host) bool { return !h.Metadata.Ready }).ParallelEach(func(h *cluster.Host) error { log.Infof("%s: cleaning up", h) if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %v", h, err) } } if h.Metadata.K0sInstalled && p.IsWet() { if err := h.Exec(h.Configurer.K0sCmdf("reset --data-dir=%s", h.K0sDataDir()), exec.Sudo(h)); err != nil { log.Warnf("%s: k0s reset failed", h) } } return nil }) } func (p *InstallControllers) After() error { for i, h := range p.hosts { if h.Metadata.K0sJoinTokenID == "" { continue } h.Metadata.K0sJoinToken = "" err := p.Wet(p.leader, fmt.Sprintf("invalidate k0s join token for controller %s", h), func() error { log.Debugf("%s: invalidating join token for controller %d", p.leader, i+1) return p.leader.Exec(p.leader.Configurer.K0sCmdf("token invalidate --data-dir=%s %s", p.leader.K0sDataDir(), h.Metadata.K0sJoinTokenID), exec.Sudo(p.leader)) }) if err != nil { log.Warnf("%s: failed to invalidate worker join token: %v", p.leader, err) } _ = p.Wet(h, "overwrite k0s join token file", func() error { if err := h.Configurer.WriteFile(h, h.K0sJoinTokenPath(), "# overwritten by k0sctl after join\n", "0600"); err != nil { log.Warnf("%s: failed to overwrite the join token file at %s", h, h.K0sJoinTokenPath()) } return nil }) } return nil } // Run the phase func (p *InstallControllers) Run() error { url := p.Config.Spec.InternalKubeAPIURL() healthz := fmt.Sprintf("%s/healthz", url) err := p.parallelDo(p.hosts, func(h *cluster.Host) error { if p.IsWet() || !p.leader.Metadata.DryRunFakeLeader { log.Infof("%s: validating api connection to %s", h, url) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := retry.Context(ctx, node.HTTPStatusFunc(h, healthz, 200, 401)); err != nil { return fmt.Errorf("failed to connect from controller to kubernetes api at %s - check networking", url) } } else { log.Warnf("%s: dry-run: skipping api connection validation to %s because cluster is not running", h, url) } return nil }) if err != nil { return err } for _, h := range p.hosts { var token string var tokenID string if p.IsWet() { log.Infof("%s: generating token", p.leader) token, err = p.Config.Spec.K0s.GenerateToken( p.leader, "controller", time.Duration(10)*time.Minute, ) if err != nil { return err } h.Metadata.K0sJoinToken = token tokenID, err = cluster.TokenID(token) if err != nil { return err } log.Debugf("%s: join token ID: %s", p.leader, tokenID) h.Metadata.K0sJoinTokenID = tokenID } else { p.DryMsgf(p.leader, "generate a k0s join token for controller %s", h) h.Metadata.K0sJoinTokenID = "dry-run" } log.Infof("%s: writing join token", h) if err := h.Configurer.WriteFile(h, h.K0sJoinTokenPath(), h.Metadata.K0sJoinToken, "0640"); err != nil { return err } if p.Config.Spec.K0s.DynamicConfig { h.InstallFlags.AddOrReplace("--enable-dynamic-config") } if Force { log.Warnf("%s: --force given, using k0s install with --force", h) h.InstallFlags.AddOrReplace("--force=true") } cmd, err := h.K0sInstallCommand() if err != nil { return err } log.Infof("%s: installing k0s controller", h) err = p.Wet(h, fmt.Sprintf("install k0s controller using `%s", strings.ReplaceAll(cmd, h.Configurer.K0sBinaryPath(), "k0s")), func() error { return h.Exec(cmd, exec.Sudo(h)) }) if err != nil { return err } h.Metadata.K0sInstalled = true h.Metadata.K0sRunningVersion = p.Config.Spec.K0s.Version if p.IsWet() { if len(h.Environment) > 0 { log.Infof("%s: updating service environment", h) if err := h.Configurer.UpdateServiceEnvironment(h, h.K0sServiceName(), h.Environment); err != nil { return err } } log.Infof("%s: starting service", h) if err := h.Configurer.StartService(h, h.K0sServiceName()); err != nil { return err } log.Infof("%s: waiting for the k0s service to start", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceRunningFunc(h, h.K0sServiceName())); err != nil { return err } if err := p.waitJoined(h); err != nil { return err } } h.Metadata.Ready = true } return nil } func (p *InstallControllers) waitJoined(h *cluster.Host) error { log.Infof("%s: waiting for kubernetes api to respond", h) return retry.Timeout(context.TODO(), retry.DefaultTimeout, node.KubeAPIReadyFunc(h, p.Config)) } 07070100000078000081A40000000000000000000000016733442800001A12000000000000000000000000000000000000002700000000k0sctl-0.19.4/phase/install_workers.gopackage phase import ( "context" "fmt" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // InstallWorkers installs k0s on worker hosts and joins them to the cluster type InstallWorkers struct { GenericPhase hosts cluster.Hosts leader *cluster.Host } // Title for the phase func (p *InstallWorkers) Title() string { return "Install workers" } // Prepare the phase func (p *InstallWorkers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Workers().Filter(func(h *cluster.Host) bool { return !h.Reset && !h.Metadata.NeedsUpgrade && (h.Metadata.K0sRunningVersion == nil || !h.Metadata.Ready) }) p.leader = p.Config.Spec.K0sLeader() return nil } // ShouldRun is true when there are workers func (p *InstallWorkers) ShouldRun() bool { return len(p.hosts) > 0 } // CleanUp attempts to clean up any changes after a failed install func (p *InstallWorkers) CleanUp() { _ = p.After() _ = p.hosts.Filter(func(h *cluster.Host) bool { return !h.Metadata.Ready }).ParallelEach(func(h *cluster.Host) error { log.Infof("%s: cleaning up", h) if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %v", h, err) } } if h.Metadata.K0sInstalled && p.IsWet() { if err := h.Exec(h.Configurer.K0sCmdf("reset --data-dir=%s", h.K0sDataDir()), exec.Sudo(h)); err != nil { log.Warnf("%s: k0s reset failed", h) } } return nil }) } func (p *InstallWorkers) After() error { if NoWait { for _, h := range p.hosts { if h.Metadata.K0sJoinToken != "" { log.Warnf("%s: --no-wait given, created join tokens will remain valid for 10 minutes", p.leader) break } } return nil } for i, h := range p.hosts { if h.Metadata.K0sJoinTokenID == "" { continue } h.Metadata.K0sJoinToken = "" err := p.Wet(p.leader, fmt.Sprintf("invalidate k0s join token for worker %s", h), func() error { log.Debugf("%s: invalidating join token for worker %d", p.leader, i+1) return p.leader.Exec(p.leader.Configurer.K0sCmdf("token invalidate --data-dir=%s %s", p.leader.K0sDataDir(), h.Metadata.K0sJoinTokenID), exec.Sudo(p.leader)) }) if err != nil { log.Warnf("%s: failed to invalidate worker join token: %v", p.leader, err) } _ = p.Wet(h, "overwrite k0s join token file", func() error { if err := h.Configurer.WriteFile(h, h.K0sJoinTokenPath(), "# overwritten by k0sctl after join\n", "0600"); err != nil { log.Warnf("%s: failed to overwrite the join token file at %s", h, h.K0sJoinTokenPath()) } return nil }) } return nil } // Run the phase func (p *InstallWorkers) Run() error { url := p.Config.Spec.InternalKubeAPIURL() healthz := fmt.Sprintf("%s/healthz", url) err := p.parallelDo(p.hosts, func(h *cluster.Host) error { if p.IsWet() || !p.leader.Metadata.DryRunFakeLeader { log.Infof("%s: validating api connection to %s", h, url) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := retry.Context(ctx, node.HTTPStatusFunc(h, healthz, 200, 401)); err != nil { return fmt.Errorf("failed to connect from worker to kubernetes api at %s - check networking", url) } } else { log.Warnf("%s: dry-run: skipping api connection validation to %s because cluster is not running", h, url) } return nil }) if err != nil { return err } for i, h := range p.hosts { log.Infof("%s: generating a join token for worker %d", p.leader, i+1) err = p.Wet(p.leader, fmt.Sprintf("generate a k0s join token for worker %s", h), func() error { t, err := p.Config.Spec.K0s.GenerateToken( p.leader, "worker", time.Duration(10*time.Minute), ) if err != nil { return err } h.Metadata.K0sJoinToken = t ti, err := cluster.TokenID(t) if err != nil { return err } h.Metadata.K0sJoinTokenID = ti log.Debugf("%s: join token ID: %s", h, ti) return nil }, func() error { h.Metadata.K0sJoinTokenID = "dry-run" return nil }) if err != nil { return err } } return p.parallelDo(p.hosts, func(h *cluster.Host) error { err := p.Wet(h, fmt.Sprintf("write k0s join token to %s", h.K0sJoinTokenPath()), func() error { log.Infof("%s: writing join token", h) return h.Configurer.WriteFile(h, h.K0sJoinTokenPath(), h.Metadata.K0sJoinToken, "0640") }) if err != nil { return err } if sp, err := h.Configurer.ServiceScriptPath(h, h.K0sServiceName()); err == nil { if h.Configurer.ServiceIsRunning(h, h.K0sServiceName()) { err := p.Wet(h, "stop existing k0s service", func() error { log.Infof("%s: stopping service", h) return h.Configurer.StopService(h, h.K0sServiceName()) }) if err != nil { return err } } if h.Configurer.FileExist(h, sp) { err := p.Wet(h, "remove existing k0s service file", func() error { return h.Configurer.DeleteFile(h, sp) }) if err != nil { return err } } } log.Infof("%s: installing k0s worker", h) if Force { log.Warnf("%s: --force given, using k0s install with --force", h) h.InstallFlags.AddOrReplace("--force=true") } cmd, err := h.K0sInstallCommand() if err != nil { return err } err = p.Wet(h, fmt.Sprintf("install k0s worker with `%s`", strings.ReplaceAll(cmd, h.Configurer.K0sBinaryPath(), "k0s")), func() error { return h.Exec(cmd, exec.Sudo(h)) }) if err != nil { return err } h.Metadata.K0sInstalled = true if len(h.Environment) > 0 { err := p.Wet(h, "update service environment variables", func() error { log.Infof("%s: updating service environment", h) return h.Configurer.UpdateServiceEnvironment(h, h.K0sServiceName(), h.Environment) }) if err != nil { return err } } if p.IsWet() { log.Infof("%s: starting service", h) if err := h.Configurer.StartService(h, h.K0sServiceName()); err != nil { return err } } if NoWait { log.Debugf("%s: not waiting because --no-wait given", h) h.Metadata.Ready = true } else { log.Infof("%s: waiting for node to become ready", h) if p.IsWet() { if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.KubeNodeReadyFunc(h)); err != nil { return err } h.Metadata.Ready = true } } h.Metadata.K0sRunningVersion = p.Config.Spec.K0s.Version return nil }) } 07070100000079000081A40000000000000000000000016733442800000CDA000000000000000000000000000000000000001C00000000k0sctl-0.19.4/phase/lock.gopackage phase import ( "context" "fmt" gos "os" "sync" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // Lock acquires an exclusive k0sctl lock on hosts type Lock struct { GenericPhase cfs []func() instanceID string m sync.Mutex wg sync.WaitGroup } // Prepare the phase func (p *Lock) Prepare(c *v1beta1.Cluster) error { p.Config = c mid, _ := analytics.MachineID() p.instanceID = fmt.Sprintf("%s-%d", mid, gos.Getpid()) return nil } // Title for the phase func (p *Lock) Title() string { return "Acquire exclusive host lock" } // Cancel releases the lock func (p *Lock) Cancel() { p.m.Lock() defer p.m.Unlock() for _, f := range p.cfs { f() } p.wg.Wait() } // CleanUp calls Cancel to release the lock func (p *Lock) CleanUp() { p.Cancel() } // UnlockPhase returns an unlock phase for this lock phase func (p *Lock) UnlockPhase() Phase { return &Unlock{Cancel: p.Cancel} } // Run the phase func (p *Lock) Run() error { if err := p.parallelDo(p.Config.Spec.Hosts, p.startLock); err != nil { return err } return p.Config.Spec.Hosts.ParallelEach(p.startTicker) } func (p *Lock) startTicker(h *cluster.Host) error { p.wg.Add(1) lfp := h.Configurer.K0sctlLockFilePath(h) ticker := time.NewTicker(10 * time.Second) ctx, cancel := context.WithCancel(context.Background()) p.m.Lock() p.cfs = append(p.cfs, cancel) p.m.Unlock() go func() { log.Tracef("%s: started periodic update of lock file %s timestamp", h, lfp) for { select { case <-ticker.C: if err := h.Configurer.Touch(h, lfp, time.Now(), exec.Sudo(h), exec.HideCommand()); err != nil { log.Debugf("%s: failed to touch lock file: %s", h, err) } case <-ctx.Done(): log.Tracef("%s: stopped lock cycle, removing file", h) if err := h.Configurer.DeleteFile(h, lfp); err != nil { log.Debugf("%s: failed to remove host lock file, k0sctl may have been previously aborted or crashed. the start of next invocation may be delayed until it expires: %s", h, err) } p.wg.Done() return } } }() return nil } func (p *Lock) startLock(h *cluster.Host) error { return retry.Times(context.TODO(), 10, func(_ context.Context) error { return p.tryLock(h) }) } func (p *Lock) tryLock(h *cluster.Host) error { lfp := h.Configurer.K0sctlLockFilePath(h) if err := h.Configurer.UpsertFile(h, lfp, p.instanceID); err != nil { stat, err := h.Configurer.Stat(h, lfp, exec.Sudo(h), exec.HideCommand()) if err != nil { return fmt.Errorf("lock file disappeared: %w", err) } content, err := h.Configurer.ReadFile(h, lfp) if err != nil { return fmt.Errorf("failed to read lock file: %w", err) } if content != p.instanceID { if time.Since(stat.ModTime()) < 30*time.Second { return fmt.Errorf("another instance of k0sctl is currently operating on the host, delete %s or wait 30 seconds for it to expire", lfp) } _ = h.Configurer.DeleteFile(h, lfp) return fmt.Errorf("removed existing expired lock file, will retry") } } return nil } 0707010000007A000081A400000000000000000000000167334428000017FD000000000000000000000000000000000000001F00000000k0sctl-0.19.4/phase/manager.gopackage phase import ( "fmt" "sync" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/logrusorgru/aurora" log "github.com/sirupsen/logrus" ) // NoWait is used by various phases to decide if node ready state should be waited for or not var NoWait bool // Force is used by various phases to attempt a forced installation var Force bool // Colorize is an instance of "aurora", used to colorize the output var Colorize = aurora.NewAurora(false) // Phase represents a runnable phase which can be added to Manager. type Phase interface { Run() error Title() string } // Phases is a slice of Phases type Phases []Phase // Index returns the index of the first occurrence matching the given phase title or -1 if not found func (p Phases) Index(title string) int { for i, phase := range p { if phase.Title() == title { return i } } return -1 } // Remove removes the first occurrence of a phase with the given title func (p *Phases) Remove(title string) { i := p.Index(title) if i == -1 { return } *p = append((*p)[:i], (*p)[i+1:]...) } // InsertAfter inserts a phase after the first occurrence of a phase with the given title func (p *Phases) InsertAfter(title string, phase Phase) { i := p.Index(title) if i == -1 { return } *p = append((*p)[:i+1], append(Phases{phase}, (*p)[i+1:]...)...) } // InsertBefore inserts a phase before the first occurrence of a phase with the given title func (p *Phases) InsertBefore(title string, phase Phase) { i := p.Index(title) if i == -1 { return } *p = append((*p)[:i], append(Phases{phase}, (*p)[i:]...)...) } // Replace replaces the first occurrence of a phase with the given title func (p *Phases) Replace(title string, phase Phase) { i := p.Index(title) if i == -1 { return } (*p)[i] = phase } type withconfig interface { Title() string Prepare(*v1beta1.Cluster) error } type conditional interface { ShouldRun() bool } // beforehook receives the phase title as an argument because of reasons. type beforehook interface { Before(string) error } type afterhook interface { After(error) error } type propsetter interface { SetProp(string, interface{}) } type withcleanup interface { CleanUp() } type withmanager interface { SetManager(*Manager) } type withDryRun interface { DryRun() error } // Manager executes phases to construct the cluster type Manager struct { phases Phases Config *v1beta1.Cluster Concurrency int ConcurrentUploads int DryRun bool dryMessages map[string][]string dryMu sync.Mutex } // NewManager creates a new Manager func NewManager(config *v1beta1.Cluster) (*Manager, error) { if config == nil { return nil, fmt.Errorf("config is nil") } return &Manager{Config: config}, nil } // AddPhase adds a Phase to Manager func (m *Manager) AddPhase(p ...Phase) { m.phases = append(m.phases, p...) } // SetPhases sets the list of phases func (m *Manager) SetPhases(p Phases) { m.phases = p } type errorfunc func() error // DryMsg prints a message in dry-run mode func (m *Manager) DryMsg(host fmt.Stringer, msg string) { m.dryMu.Lock() defer m.dryMu.Unlock() if m.dryMessages == nil { m.dryMessages = make(map[string][]string) } var key string if host == nil { key = "local" } else { key = host.String() } m.dryMessages[key] = append(m.dryMessages[key], msg) } // Wet runs the first given function when not in dry-run mode. The second function will be // run when in dry-mode and the message will be displayed. Any error returned from the // functions will be returned and will halt the operation. func (m *Manager) Wet(host fmt.Stringer, msg string, funcs ...errorfunc) error { if !m.DryRun { if len(funcs) > 0 && funcs[0] != nil { return funcs[0]() } return nil } m.DryMsg(host, msg) if m.DryRun && len(funcs) == 2 && funcs[1] != nil { return funcs[1]() } return nil } // Run executes all the added Phases in order func (m *Manager) Run() error { var ran []Phase var result error defer func() { if m.DryRun { if len(m.dryMessages) == 0 { fmt.Println(Colorize.BrightGreen("dry-run: no cluster state altering actions would be performed")) return } fmt.Println(Colorize.BrightRed("dry-run: cluster state altering actions would be performed:")) for host, msgs := range m.dryMessages { fmt.Println(Colorize.BrightRed("dry-run:"), Colorize.Bold(fmt.Sprintf("* %s :", host))) for _, msg := range msgs { fmt.Println(Colorize.BrightRed("dry-run:"), Colorize.Red(" -"), msg) } } return } if result != nil { for _, p := range ran { if c, ok := p.(withcleanup); ok { log.Infof(Colorize.Red("* Running clean-up for phase: %s").String(), p.Title()) c.CleanUp() } } } }() for _, p := range m.phases { title := p.Title() if p, ok := p.(withmanager); ok { p.SetManager(m) } if p, ok := p.(withconfig); ok { log.Debugf("Preparing phase '%s'", p.Title()) if err := p.Prepare(m.Config); err != nil { return err } } if p, ok := p.(conditional); ok { if !p.ShouldRun() { continue } } if p, ok := p.(beforehook); ok { if err := p.Before(title); err != nil { log.Debugf("before hook failed '%s'", err.Error()) return err } } if p, ok := p.(propsetter); ok { if m.Config.Spec.K0s == nil { m.Config.Spec.K0s = &cluster.K0s{Metadata: cluster.K0sMetadata{}} } if m.Config.Spec.K0s.Metadata.ClusterID != "" { p.SetProp("clusterID", m.Config.Spec.K0s.Metadata.ClusterID) } } text := Colorize.Green("==> Running phase: %s").String() log.Infof(text, title) if dp, ok := p.(withDryRun); ok && m.DryRun { if err := dp.DryRun(); err != nil { return err } continue } result = p.Run() ran = append(ran, p) if p, ok := p.(afterhook); ok { if err := p.After(result); err != nil { log.Debugf("after hook failed: '%s' (phase result: %s)", err.Error(), result) return err } } if result != nil { return result } } return nil } 0707010000007B000081A40000000000000000000000016733442800000823000000000000000000000000000000000000002400000000k0sctl-0.19.4/phase/manager_test.gopackage phase import ( "fmt" "testing" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/stretchr/testify/require" ) type conditionalPhase struct { shouldrunCalled bool runCalled bool } func (p *conditionalPhase) Title() string { return "conditional phase" } func (p *conditionalPhase) ShouldRun() bool { p.shouldrunCalled = true return false } func (p *conditionalPhase) Run() error { p.runCalled = true return nil } func TestConditionalPhase(t *testing.T) { m := Manager{Config: &v1beta1.Cluster{Spec: &cluster.Spec{}}} p := &conditionalPhase{} m.AddPhase(p) require.NoError(t, m.Run()) require.False(t, p.runCalled, "run was not called") require.True(t, p.shouldrunCalled, "shouldrun was not called") } type configPhase struct { receivedConfig bool } func (p *configPhase) Title() string { return "config phase" } func (p *configPhase) Prepare(c *v1beta1.Cluster) error { p.receivedConfig = c != nil return nil } func (p *configPhase) Run() error { return nil } func TestConfigPhase(t *testing.T) { m := Manager{Config: &v1beta1.Cluster{Spec: &cluster.Spec{}}} p := &configPhase{} m.AddPhase(p) require.NoError(t, m.Run()) require.True(t, p.receivedConfig, "config was not received") } type hookedPhase struct { beforeCalled bool afterCalled bool err error } func (p *hookedPhase) Title() string { return "hooked phase" } func (p *hookedPhase) Before(_ string) error { p.beforeCalled = true return nil } func (p *hookedPhase) After(err error) error { p.afterCalled = true p.err = err return nil } func (p *hookedPhase) Run() error { return fmt.Errorf("run failed") } func TestHookedPhase(t *testing.T) { m := Manager{Config: &v1beta1.Cluster{Spec: &cluster.Spec{}}} p := &hookedPhase{} m.AddPhase(p) require.Error(t, m.Run()) require.True(t, p.beforeCalled, "before hook was not called") require.True(t, p.afterCalled, "after hook was not called") require.EqualError(t, p.err, "run failed") } 0707010000007C000081A40000000000000000000000016733442800000C24000000000000000000000000000000000000002500000000k0sctl-0.19.4/phase/prepare_hosts.gopackage phase import ( "context" "errors" "fmt" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) var iptablesEmbeddedSince = version.MustConstraint(">= v1.22.1+k0s.0") // PrepareHosts installs required packages and so on on the hosts. type PrepareHosts struct { GenericPhase } // Title for the phase func (p *PrepareHosts) Title() string { return "Prepare hosts" } // Run the phase func (p *PrepareHosts) Run() error { return p.parallelDo(p.Config.Spec.Hosts, p.prepareHost) } type prepare interface { Prepare(os.Host) error } // updateEnvironment updates the environment variables on the host and reconnects to // it if necessary. func (p *PrepareHosts) updateEnvironment(h *cluster.Host) error { if err := h.Configurer.UpdateEnvironment(h, h.Environment); err != nil { return err } if h.Connection.Protocol() != "SSH" { return nil } // XXX: this is a workaround. UpdateEnvironment on rig's os/linux.go writes // the environment to /etc/environment and then exports the same variables // using 'export' command. This is not enough for the environment to be // preserved across multiple ssh sessions. We need to write the environment // and then reopen the ssh session. Go's ssh client.Setenv() depends on ssh // server configuration (sshd only accepts LC_* variables by default). log.Infof("%s: reconnecting to apply new environment", h) h.Disconnect() return retry.Timeout(context.TODO(), 10*time.Minute, func(_ context.Context) error { if err := h.Connect(); err != nil { if errors.Is(err, rig.ErrCantConnect) || strings.Contains(err.Error(), "host key mismatch") { return errors.Join(retry.ErrAbort, err) } return fmt.Errorf("failed to reconnect to %s: %w", h, err) } return nil }) } func (p *PrepareHosts) prepareHost(h *cluster.Host) error { if c, ok := h.Configurer.(prepare); ok { if err := c.Prepare(h); err != nil { return err } } if len(h.Environment) > 0 { log.Infof("%s: updating environment", h) if err := p.updateEnvironment(h); err != nil { return fmt.Errorf("failed to updated environment: %w", err) } } var pkgs []string if h.NeedCurl() { pkgs = append(pkgs, "curl") } // iptables is only required for very old versions of k0s if p.Config.Spec.K0s.Version != nil && !iptablesEmbeddedSince.Check(p.Config.Spec.K0s.Version) && h.NeedIPTables() { //nolint:staticcheck pkgs = append(pkgs, "iptables") } if h.NeedInetUtils() { pkgs = append(pkgs, "inetutils") } for _, pkg := range pkgs { err := p.Wet(h, fmt.Sprintf("install package %s", pkg), func() error { log.Infof("%s: installing package %s", h, pkg) return h.Configurer.InstallPackage(h, pkg) }) if err != nil { return err } } if h.Configurer.IsContainer(h) { log.Infof("%s: is a container, applying a fix", h) if err := h.Configurer.FixContainer(h); err != nil { return err } } return nil } 0707010000007D000081A40000000000000000000000016733442800000FEA000000000000000000000000000000000000002100000000k0sctl-0.19.4/phase/reinstall.gopackage phase import ( "context" "fmt" "math" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) type Reinstall struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *Reinstall) Title() string { return "Reinstall" } // Prepare the phase func (p *Reinstall) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return !h.Metadata.K0sInstalled && h.Metadata.K0sRunningVersion != nil && !h.Reset && h.FlagsChanged() }) return nil } // ShouldRun is true when there are hosts that needs to be reinstalled func (p *Reinstall) ShouldRun() bool { return cluster.K0sForceFlagSince.Check(p.Config.Spec.K0s.Version) && len(p.hosts) > 0 } // Run the phase func (p *Reinstall) Run() error { if !cluster.K0sForceFlagSince.Check(p.Config.Spec.K0s.Version) { log.Warnf("k0s version %s does not support install --force flag, installFlags won't be reconfigured", p.Config.Spec.K0s.Version) return nil } controllers := p.hosts.Controllers() if len(controllers) > 0 { log.Infof("Reinstalling %d controllers sequentially", len(controllers)) err := controllers.Each(func(h *cluster.Host) error { return p.reinstall(h) }) if err != nil { return err } } workers := p.hosts.Workers() if len(workers) == 0 { return nil } concurrentReinstalls := int(math.Floor(float64(len(p.hosts)) * 0.10)) if concurrentReinstalls == 0 { concurrentReinstalls = 1 } log.Infof("Reinstalling max %d workers in parallel", concurrentReinstalls) return p.hosts.BatchedParallelEach(concurrentReinstalls, p.reinstall) } func (p *Reinstall) reinstall(h *cluster.Host) error { if p.Config.Spec.K0s.DynamicConfig && h.Role != "worker" { h.InstallFlags.AddOrReplace("--enable-dynamic-config") } h.InstallFlags.AddOrReplace("--force=true") cmd, err := h.K0sInstallCommand() if err != nil { return err } log.Infof("%s: reinstalling k0s", h) err = p.Wet(h, fmt.Sprintf("reinstall k0s using `%s", strings.ReplaceAll(cmd, h.Configurer.K0sBinaryPath(), "k0s")), func() error { if err := h.Exec(cmd, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to reinstall k0s: %w", err) } return nil }) if err != nil { return err } err = p.Wet(h, "restart k0s service", func() error { if err := h.Configurer.RestartService(h, h.K0sServiceName()); err != nil { return fmt.Errorf("failed to restart k0s: %w", err) } log.Infof("%s: waiting for the k0s service to start", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceRunningFunc(h, h.K0sServiceName())); err != nil { return fmt.Errorf("k0s did not restart: %w", err) } return nil }) if err != nil { return fmt.Errorf("restart after reinstall: %w", err) } if h != p.Config.Spec.K0sLeader() { return nil } if NoWait || !p.IsWet() { log.Warnf("%s: skipping scheduler and system pod checks because --no-wait given", h) return nil } log.Infof("%s: waiting for the scheduler to become ready", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ScheduledEventsAfterFunc(h, time.Now())); err != nil { if !Force { return fmt.Errorf("failed to observe scheduling events after api start-up, you can ignore this check by using --force: %w", err) } log.Warnf("%s: failed to observe scheduling events after api start-up: %s", h, err) } log.Infof("%s: waiting for system pods to become ready", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.SystemPodsRunningFunc(h)); err != nil { if !Force { return fmt.Errorf("all system pods not running after api start-up, you can ignore this check by using --force: %w", err) } log.Warnf("%s: failed to observe system pods running after api start-up: %s", h, err) } return nil } 0707010000007E000081A40000000000000000000000016733442800000EDA000000000000000000000000000000000000002900000000k0sctl-0.19.4/phase/reset_controllers.gopackage phase import ( "context" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // ResetControllers phase removes controllers marked for reset from the kubernetes and etcd clusters // and resets k0s on the host type ResetControllers struct { GenericPhase NoDrain bool NoDelete bool NoLeave bool hosts cluster.Hosts leader *cluster.Host } // Title for the phase func (p *ResetControllers) Title() string { return "Reset controllers" } // Prepare the phase func (p *ResetControllers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() var controllers cluster.Hosts = p.Config.Spec.Hosts.Controllers() log.Debugf("%d controllers in total", len(controllers)) p.hosts = controllers.Filter(func(h *cluster.Host) bool { return h.Reset }) log.Debugf("ResetControllers phase prepared, %d controllers will be reset", len(p.hosts)) return nil } // ShouldRun is true when there are controllers that needs to be reset func (p *ResetControllers) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *ResetControllers) Run() error { for _, h := range p.hosts { log.Debugf("%s: draining node", h) if !p.NoDrain && h.Role != "controller" { if err := p.leader.DrainNode(&cluster.Host{ Metadata: cluster.HostMetadata{ Hostname: h.Metadata.Hostname, }, }); err != nil { log.Warnf("%s: failed to drain node: %s", h, err.Error()) } } log.Debugf("%s: draining node completed", h) log.Debugf("%s: deleting node...", h) if !p.NoDelete && h.Role != "controller" { if err := p.leader.DeleteNode(&cluster.Host{ Metadata: cluster.HostMetadata{ Hostname: h.Metadata.Hostname, }, }); err != nil { log.Warnf("%s: failed to delete node: %s", h, err.Error()) } } log.Debugf("%s: deleting node", h) if h.Configurer.ServiceIsRunning(h, h.K0sServiceName()) { log.Debugf("%s: stopping k0s...", h) if err := h.Configurer.StopService(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to stop k0s: %s", h, err.Error()) } log.Debugf("%s: waiting for k0s to stop", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceStoppedFunc(h, h.K0sServiceName())); err != nil { log.Warnf("%s: failed to wait for k0s to stop: %v", h, err) } log.Debugf("%s: stopping k0s completed", h) } if !p.NoLeave { log.Debugf("%s: leaving etcd...", h) if err := h.Exec(h.Configurer.K0sCmdf("etcd leave --peer-address %s --datadir %s", h.PrivateAddress, h.K0sDataDir()), exec.Sudo(h)); err != nil { log.Warnf("%s: failed to leave etcd: %s", h, err.Error()) } log.Debugf("%s: leaving etcd completed", h) } log.Debugf("%s: resetting k0s...", h) out, err := h.ExecOutput(h.Configurer.K0sCmdf("reset --data-dir=%s", h.K0sDataDir()), exec.Sudo(h)) if err != nil { log.Debugf("%s: k0s reset failed: %s", h, out) log.Warnf("%s: k0s reported failure: %v", h, err) } log.Debugf("%s: resetting k0s completed", h) log.Debugf("%s: removing config...", h) if dErr := h.Configurer.DeleteFile(h, h.Configurer.K0sConfigPath()); dErr != nil { log.Warnf("%s: failed to remove existing configuration %s: %s", h, h.Configurer.K0sConfigPath(), dErr) } log.Debugf("%s: removing config completed", h) if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", h, err.Error()) } } log.Infof("%s: reset", h) } return nil } 0707010000007F000081A4000000000000000000000001673344280000094C000000000000000000000000000000000000002400000000k0sctl-0.19.4/phase/reset_leader.gopackage phase import ( "context" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // ResetLeader phase removes the leader from the cluster and thus destroys the cluster type ResetLeader struct { GenericPhase leader *cluster.Host } // Title for the phase func (p *ResetLeader) Title() string { return "Reset leader" } // Prepare the phase func (p *ResetLeader) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() return nil } // Run the phase func (p *ResetLeader) Run() error { if p.leader.Configurer.ServiceIsRunning(p.leader, p.leader.K0sServiceName()) { log.Debugf("%s: stopping k0s...", p.leader) if err := p.leader.Configurer.StopService(p.leader, p.leader.K0sServiceName()); err != nil { log.Warnf("%s: failed to stop k0s: %s", p.leader, err.Error()) } log.Debugf("%s: waiting for k0s to stop", p.leader) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceStoppedFunc(p.leader, p.leader.K0sServiceName())); err != nil { log.Warnf("%s: k0s service stop: %s", p.leader, err.Error()) } log.Debugf("%s: stopping k0s completed", p.leader) } log.Debugf("%s: resetting k0s...", p.leader) out, err := p.leader.ExecOutput(p.leader.Configurer.K0sCmdf("reset --data-dir=%s", p.leader.K0sDataDir()), exec.Sudo(p.leader)) if err != nil { log.Debugf("%s: k0s reset failed: %s", p.leader, out) log.Warnf("%s: k0s reported failure: %v", p.leader, err) } log.Debugf("%s: resetting k0s completed", p.leader) log.Debugf("%s: removing config...", p.leader) if dErr := p.leader.Configurer.DeleteFile(p.leader, p.leader.Configurer.K0sConfigPath()); dErr != nil { log.Warnf("%s: failed to remove existing configuration %s: %s", p.leader, p.leader.Configurer.K0sConfigPath(), dErr) } log.Debugf("%s: removing config completed", p.leader) if len(p.leader.Environment) > 0 { if err := p.leader.Configurer.CleanupServiceEnvironment(p.leader, p.leader.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", p.leader, err.Error()) } } log.Infof("%s: reset", p.leader) return nil } 07070100000080000081A40000000000000000000000016733442800000D35000000000000000000000000000000000000002500000000k0sctl-0.19.4/phase/reset_workers.gopackage phase import ( "context" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // ResetControllers phase removes workers marked for reset from the kubernetes cluster // and resets k0s on the host type ResetWorkers struct { GenericPhase NoDrain bool NoDelete bool hosts cluster.Hosts leader *cluster.Host } // Title for the phase func (p *ResetWorkers) Title() string { return "Reset workers" } // Prepare the phase func (p *ResetWorkers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() var workers cluster.Hosts = p.Config.Spec.Hosts.Workers() log.Debugf("%d workers in total", len(workers)) p.hosts = workers.Filter(func(h *cluster.Host) bool { return h.Reset }) log.Debugf("ResetWorkers phase prepared, %d workers will be reset", len(p.hosts)) return nil } // ShouldRun is true when there are workers that needs to be reset func (p *ResetWorkers) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *ResetWorkers) Run() error { return p.parallelDo(p.hosts, func(h *cluster.Host) error { log.Debugf("%s: draining node", h) if !p.NoDrain { if err := p.leader.DrainNode(&cluster.Host{ Metadata: cluster.HostMetadata{ Hostname: h.Metadata.Hostname, }, }); err != nil { log.Warnf("%s: failed to drain node: %s", h, err.Error()) } } log.Debugf("%s: draining node completed", h) log.Debugf("%s: deleting node...", h) if !p.NoDelete { if err := p.leader.DeleteNode(&cluster.Host{ Metadata: cluster.HostMetadata{ Hostname: h.Metadata.Hostname, }, }); err != nil { log.Warnf("%s: failed to delete node: %s", h, err.Error()) } } log.Debugf("%s: deleting node", h) if h.Configurer.ServiceIsRunning(h, h.K0sServiceName()) { log.Debugf("%s: stopping k0s...", h) if err := h.Configurer.StopService(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to stop k0s: %s", h, err.Error()) } log.Debugf("%s: waiting for k0s to stop", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceStoppedFunc(h, h.K0sServiceName())); err != nil { log.Warnf("%s: failed to wait for k0s to stop: %s", h, err.Error()) } log.Debugf("%s: stopping k0s completed", h) } log.Debugf("%s: resetting k0s...", h) out, err := h.ExecOutput(h.Configurer.K0sCmdf("reset --data-dir=%s", h.K0sDataDir()), exec.Sudo(h)) if err != nil { log.Debugf("%s: k0s reset failed: %s", h, out) log.Warnf("%s: k0s reported failure: %v", h, err) } log.Debugf("%s: resetting k0s completed", h) log.Debugf("%s: removing config...", h) if dErr := h.Configurer.DeleteFile(h, h.Configurer.K0sConfigPath()); dErr != nil { log.Warnf("%s: failed to remove existing configuration %s: %s", h, h.Configurer.K0sConfigPath(), dErr) } log.Debugf("%s: removing config completed", h) if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", h, err.Error()) } } log.Infof("%s: reset", h) return err }) } 07070100000081000081A40000000000000000000000016733442800000775000000000000000000000000000000000000001F00000000k0sctl-0.19.4/phase/restore.gopackage phase import ( "fmt" "path" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) type Restore struct { GenericPhase RestoreFrom string leader *cluster.Host } // Title for the phase func (p *Restore) Title() string { return "Restore cluster state" } // ShouldRun is true when there path to backup file func (p *Restore) ShouldRun() bool { return p.RestoreFrom != "" && p.leader.Metadata.K0sRunningVersion == nil && !p.leader.Reset } // Prepare the phase func (p *Restore) Prepare(config *v1beta1.Cluster) error { p.Config = config if p.RestoreFrom == "" { return nil } // defined in backup.go if !backupSinceVersion.Check(p.Config.Spec.K0s.Version) { return fmt.Errorf("the version of k0s on the host does not support restoring backups") } p.leader = p.Config.Spec.K0sLeader() log.Tracef("restore leader: %s", p.leader) log.Tracef("restore leader state: %+v", p.leader.Metadata) return nil } // Run the phase func (p *Restore) Run() error { // Push the backup file to controller h := p.leader tmpDir, err := h.Configurer.TempDir(h) if err != nil { return err } dstFile := path.Join(tmpDir, "k0s_backup.tar.gz") if err := h.Upload(p.RestoreFrom, dstFile, exec.LogError(true)); err != nil { return err } defer func() { if err := h.Configurer.DeleteFile(h, dstFile); err != nil { log.Warnf("%s: failed to remove backup file %s: %s", h, dstFile, err) } if err := h.Configurer.DeleteDir(h, tmpDir, exec.Sudo(h)); err != nil { log.Warnf("%s: failed to remove backup temp dir %s: %s", h, tmpDir, err) } }() // Run restore log.Infof("%s: restoring cluster state", h) if err := h.Exec(h.K0sRestoreCommand(dstFile), exec.Sudo(h)); err != nil { return err } return nil } 07070100000082000081A400000000000000000000000167334428000005A6000000000000000000000000000000000000002000000000k0sctl-0.19.4/phase/runhooks.gopackage phase import ( "fmt" "golang.org/x/text/cases" "golang.org/x/text/language" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) var _ Phase = &RunHooks{} // RunHooks phase runs a set of hooks configured for the host type RunHooks struct { GenericPhase Action string Stage string hosts cluster.Hosts } // Title for the phase func (p *RunHooks) Title() string { titler := cases.Title(language.AmericanEnglish) return fmt.Sprintf("Run %s %s Hooks", titler.String(p.Stage), titler.String(p.Action)) } // Prepare digs out the hosts with steps from the config func (p *RunHooks) Prepare(config *v1beta1.Cluster) error { p.hosts = config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return len(h.Hooks.ForActionAndStage(p.Action, p.Stage)) > 0 }) return nil } // ShouldRun is true when there are hosts that need to be connected func (p *RunHooks) ShouldRun() bool { return len(p.hosts) > 0 } // Run does all the prep work on the hosts in parallel func (p *RunHooks) Run() error { return p.hosts.ParallelEach(p.runHooksForHost) } func (p *RunHooks) runHooksForHost(h *cluster.Host) error { steps := h.Hooks.ForActionAndStage(p.Action, p.Stage) for _, s := range steps { err := p.Wet(h, fmt.Sprintf("run hook: `%s`", s), func() error { return h.Exec(s) }) if err != nil { return err } } return nil } 07070100000083000081A40000000000000000000000016733442800000260000000000000000000000000000000000000001E00000000k0sctl-0.19.4/phase/unlock.gopackage phase import ( "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" log "github.com/sirupsen/logrus" ) // Unlock acquires an exclusive k0sctl lock on hosts type Unlock struct { GenericPhase Cancel func() } // Prepare the phase func (p *Unlock) Prepare(c *v1beta1.Cluster) error { p.Config = c if p.Cancel == nil { p.Cancel = func() { log.Fatalf("cancel function not defined") } } return nil } // Title for the phase func (p *Unlock) Title() string { return "Release exclusive host lock" } // Run the phase func (p *Unlock) Run() error { p.Cancel() return nil } 07070100000084000081A4000000000000000000000001673344280000141A000000000000000000000000000000000000002B00000000k0sctl-0.19.4/phase/upgrade_controllers.gopackage phase import ( "context" "fmt" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // UpgradeControllers upgrades the controllers one-by-one type UpgradeControllers struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *UpgradeControllers) Title() string { return "Upgrade controllers" } // Prepare the phase func (p *UpgradeControllers) Prepare(config *v1beta1.Cluster) error { log.Debugf("UpgradeControllers phase prep starting") p.Config = config var controllers cluster.Hosts = p.Config.Spec.Hosts.Controllers() log.Debugf("%d controllers in total", len(controllers)) p.hosts = controllers.Filter(func(h *cluster.Host) bool { if h.Metadata.K0sBinaryTempFile == "" { return false } return !h.Reset && h.Metadata.NeedsUpgrade }) log.Debugf("UpgradeControllers phase prepared, %d controllers needs upgrade", len(p.hosts)) return nil } // ShouldRun is true when there are controllers that needs to be upgraded func (p *UpgradeControllers) ShouldRun() bool { return len(p.hosts) > 0 } // CleanUp cleans up the environment override files on hosts func (p *UpgradeControllers) CleanUp() { for _, h := range p.hosts { if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", h, err.Error()) } } } } // Run the phase func (p *UpgradeControllers) Run() error { for _, h := range p.hosts { if !h.Configurer.FileExist(h, h.Metadata.K0sBinaryTempFile) { return fmt.Errorf("k0s binary tempfile not found on host") } log.Infof("%s: starting upgrade", h) log.Debugf("%s: stop service", h) err := p.Wet(h, "stop k0s service", func() error { if err := h.Configurer.StopService(h, h.K0sServiceName()); err != nil { return err } if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceStoppedFunc(h, h.K0sServiceName())); err != nil { return fmt.Errorf("wait for k0s service stop: %w", err) } return nil }) if err != nil { return err } log.Debugf("%s: update binary", h) err = p.Wet(h, "replace k0s binary", func() error { return h.UpdateK0sBinary(h.Metadata.K0sBinaryTempFile, p.Config.Spec.K0s.Version) }) if err != nil { return err } if len(h.Environment) > 0 { log.Infof("%s: updating service environment", h) err := p.Wet(h, "update service environment", func() error { return h.Configurer.UpdateServiceEnvironment(h, h.K0sServiceName(), h.Environment) }) if err != nil { return err } } err = p.Wet(h, "reinstall k0s service", func() error { if p.Config.Spec.K0s.DynamicConfig { h.InstallFlags.AddOrReplace("--enable-dynamic-config") } h.InstallFlags.AddOrReplace("--force") cmd, err := h.K0sInstallCommand() if err != nil { return err } if err := h.Exec(cmd, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to reinstall k0s: %w", err) } return nil }) if err != nil { return err } h.Metadata.K0sInstalled = true log.Debugf("%s: restart service", h) err = p.Wet(h, "start k0s service with the new binary", func() error { if err := h.Configurer.StartService(h, h.K0sServiceName()); err != nil { return err } log.Infof("%s: waiting for the k0s service to start", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceRunningFunc(h, h.K0sServiceName())); err != nil { return fmt.Errorf("k0s service start: %w", err) } return nil }) if err != nil { return err } if p.IsWet() { if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.KubeAPIReadyFunc(h, p.Config)); err != nil { return fmt.Errorf("kube api did not become ready: %w", err) } } h.Metadata.K0sRunningVersion = p.Config.Spec.K0s.Version } leader := p.Config.Spec.K0sLeader() if NoWait || !p.IsWet() { log.Warnf("%s: skipping scheduler and system pod checks because --no-wait given", leader) return nil } log.Infof("%s: waiting for the scheduler to become ready", leader) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ScheduledEventsAfterFunc(leader, time.Now())); err != nil { if !Force { return fmt.Errorf("failed to observe scheduling events after api start-up, you can ignore this check by using --force: %w", err) } log.Warnf("%s: failed to observe scheduling events after api start-up: %s", leader, err) } log.Infof("%s: waiting for system pods to become ready", leader) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.SystemPodsRunningFunc(leader)); err != nil { if !Force { return fmt.Errorf("all system pods not running after api start-up, you can ignore this check by using --force: %w", err) } log.Warnf("%s: failed to observe system pods running after api start-up: %s", leader, err) } return nil } 07070100000085000081A4000000000000000000000001673344280000157A000000000000000000000000000000000000002700000000k0sctl-0.19.4/phase/upgrade_workers.gopackage phase import ( "context" "fmt" "math" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/k0sctl/pkg/node" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // UpgradeWorkers upgrades workers in batches type UpgradeWorkers struct { GenericPhase NoDrain bool hosts cluster.Hosts leader *cluster.Host } // Title for the phase func (p *UpgradeWorkers) Title() string { return "Upgrade workers" } // Prepare the phase func (p *UpgradeWorkers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.leader = p.Config.Spec.K0sLeader() var workers cluster.Hosts = p.Config.Spec.Hosts.Workers() log.Debugf("%d workers in total", len(workers)) p.hosts = workers.Filter(func(h *cluster.Host) bool { if h.Metadata.K0sBinaryTempFile == "" { return false } return !h.Reset && h.Metadata.NeedsUpgrade }) err := p.parallelDo(p.hosts, func(h *cluster.Host) error { if !h.Configurer.FileExist(h, h.Metadata.K0sBinaryTempFile) { return fmt.Errorf("k0s binary tempfile not found on host") } return nil }) if err != nil { return err } log.Debugf("UpgradeWorkers phase prepared, %d workers needs upgrade", len(p.hosts)) return nil } // ShouldRun is true when there are workers that needs to be upgraded func (p *UpgradeWorkers) ShouldRun() bool { return len(p.hosts) > 0 } // CleanUp cleans up the environment override files on hosts func (p *UpgradeWorkers) CleanUp() { if !p.IsWet() { return } _ = p.parallelDo(p.hosts, func(h *cluster.Host) error { if len(h.Environment) > 0 { if err := h.Configurer.CleanupServiceEnvironment(h, h.K0sServiceName()); err != nil { log.Warnf("%s: failed to clean up service environment: %s", h, err.Error()) } } _ = p.leader.UncordonNode(h) return nil }) } // Run the phase func (p *UpgradeWorkers) Run() error { // Upgrade worker hosts parallelly in 10% chunks concurrentUpgrades := int(math.Floor(float64(len(p.hosts)) * 0.10)) if concurrentUpgrades == 0 { concurrentUpgrades = 1 } log.Infof("Upgrading max %d workers in parallel", concurrentUpgrades) return p.hosts.BatchedParallelEach(concurrentUpgrades, p.start, p.cordonWorker, p.drainWorker, p.upgradeWorker, p.uncordonWorker, p.finish, ) } func (p *UpgradeWorkers) cordonWorker(h *cluster.Host) error { if p.NoDrain { log.Debugf("%s: not cordoning because --no-drain given", h) return nil } if !p.IsWet() { p.DryMsg(h, "cordon node") return nil } log.Debugf("%s: cordon", h) if err := p.leader.CordonNode(h); err != nil { return fmt.Errorf("cordon node: %w", err) } return nil } func (p *UpgradeWorkers) uncordonWorker(h *cluster.Host) error { if !p.IsWet() { p.DryMsg(h, "uncordon node") return nil } log.Debugf("%s: uncordon", h) if err := p.leader.UncordonNode(h); err != nil { return fmt.Errorf("uncordon node: %w", err) } return nil } func (p *UpgradeWorkers) drainWorker(h *cluster.Host) error { if p.NoDrain { log.Debugf("%s: not draining because --no-drain given", h) return nil } if !p.IsWet() { p.DryMsg(h, "drain node") return nil } log.Debugf("%s: drain", h) if err := p.leader.DrainNode(h); err != nil { return fmt.Errorf("drain node: %w", err) } return nil } func (p *UpgradeWorkers) start(h *cluster.Host) error { log.Infof("%s: starting upgrade", h) return nil } func (p *UpgradeWorkers) finish(h *cluster.Host) error { log.Infof("%s: upgrade finished", h) return nil } func (p *UpgradeWorkers) upgradeWorker(h *cluster.Host) error { log.Debugf("%s: stop service", h) err := p.Wet(h, "stop k0s service", func() error { if err := h.Configurer.StopService(h, h.K0sServiceName()); err != nil { return err } if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.ServiceStoppedFunc(h, h.K0sServiceName())); err != nil { return err } return nil }) if err != nil { return err } log.Debugf("%s: update binary", h) err = p.Wet(h, "replace k0s binary", func() error { return h.UpdateK0sBinary(h.Metadata.K0sBinaryTempFile, p.Config.Spec.K0s.Version) }) if err != nil { return err } if len(h.Environment) > 0 { log.Infof("%s: updating service environment", h) err := p.Wet(h, "update service environment", func() error { return h.Configurer.UpdateServiceEnvironment(h, h.K0sServiceName(), h.Environment) }) if err != nil { return err } } err = p.Wet(h, "reinstall k0s service", func() error { h.InstallFlags.AddOrReplace("--force") cmd, err := h.K0sInstallCommand() if err != nil { return err } if err := h.Exec(cmd, exec.Sudo(h)); err != nil { return fmt.Errorf("failed to reinstall k0s: %w", err) } return nil }) if err != nil { return err } h.Metadata.K0sInstalled = true log.Debugf("%s: restart service", h) err = p.Wet(h, "restart k0s service", func() error { if err := h.Configurer.StartService(h, h.K0sServiceName()); err != nil { return err } if NoWait { log.Debugf("%s: not waiting because --no-wait given", h) } else { log.Infof("%s: waiting for node to become ready again", h) if err := retry.Timeout(context.TODO(), retry.DefaultTimeout, node.KubeNodeReadyFunc(h)); err != nil { return fmt.Errorf("node did not become ready: %w", err) } } return nil }) if err != nil { return err } h.Metadata.Ready = true return nil } 07070100000086000081A400000000000000000000000167334428000008C5000000000000000000000000000000000000002200000000k0sctl-0.19.4/phase/upload_k0s.gopackage phase import ( "fmt" "os" "strconv" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // UploadK0s uploads k0s binaries from localhost to target type UploadK0s struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *UploadK0s) Title() string { return "Upload k0s binaries to hosts" } // Prepare the phase func (p *UploadK0s) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { // Nothing to upload if h.UploadBinaryPath == "" { return false } // No need to upload, host is going to be reset if h.Reset { return false } if !p.Config.Spec.K0s.Version.Equal(h.Metadata.K0sBinaryVersion) { log.Debugf("%s: k0s version on host is '%s'", h, h.Metadata.K0sBinaryVersion) return true } // If the file has been changed compared to local, re-upload and replace return h.FileChanged(h.UploadBinaryPath, h.Configurer.K0sBinaryPath()) }) return nil } // ShouldRun is true when there are hosts that need binary uploading func (p *UploadK0s) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *UploadK0s) Run() error { return p.parallelDoUpload(p.hosts, p.uploadBinary) } func (p *UploadK0s) uploadBinary(h *cluster.Host) error { tmp := h.Configurer.K0sBinaryPath() + ".tmp." + strconv.Itoa(int(time.Now().UnixNano())) stat, err := os.Stat(h.UploadBinaryPath) if err != nil { return fmt.Errorf("stat %s: %w", h.UploadBinaryPath, err) } log.Infof("%s: uploading k0s binary from %s to %s", h, h.UploadBinaryPath, tmp) if err := h.Upload(h.UploadBinaryPath, tmp, exec.Sudo(h), exec.LogError(true)); err != nil { return fmt.Errorf("upload k0s binary: %w", err) } if err := h.Configurer.Touch(h, tmp, stat.ModTime(), exec.Sudo(h)); err != nil { return fmt.Errorf("failed to touch %s: %w", tmp, err) } if err := h.Execf(`chmod +x "%s"`, tmp, exec.Sudo(h)); err != nil { log.Warnf("%s: failed to chmod k0s temp binary: %v", h, err.Error()) } h.Metadata.K0sBinaryTempFile = tmp return nil } 07070100000087000081A4000000000000000000000001673344280000149B000000000000000000000000000000000000002300000000k0sctl-0.19.4/phase/uploadfiles.gopackage phase import ( "fmt" "os" "path" "github.com/alessio/shellescape" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // UploadFiles implements a phase which upload files to hosts type UploadFiles struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *UploadFiles) Title() string { return "Upload files to hosts" } // Prepare the phase func (p *UploadFiles) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Filter(func(h *cluster.Host) bool { return !h.Reset && len(h.Files) > 0 }) return nil } // ShouldRun is true when there are workers func (p *UploadFiles) ShouldRun() bool { return len(p.hosts) > 0 } // Run the phase func (p *UploadFiles) Run() error { return p.parallelDoUpload(p.Config.Spec.Hosts, p.uploadFiles) } func (p *UploadFiles) uploadFiles(h *cluster.Host) error { for _, f := range h.Files { var err error if f.IsURL() { err = p.uploadURL(h, f) } else { err = p.uploadFile(h, f) } if err != nil { return err } } return nil } func (p *UploadFiles) ensureDir(h *cluster.Host, dir, perm, owner string) error { log.Debugf("%s: ensuring directory %s", h, dir) if h.Configurer.FileExist(h, dir) { return nil } err := p.Wet(h, fmt.Sprintf("create a directory for uploading: `mkdir -p \"%s\"`", dir), func() error { return h.Configurer.MkDir(h, dir, exec.Sudo(h)) }) if err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } if perm == "" { perm = "0755" } err = p.Wet(h, fmt.Sprintf("set permissions for directory %s to %s", dir, perm), func() error { return h.Configurer.Chmod(h, dir, perm, exec.Sudo(h)) }) if err != nil { return fmt.Errorf("failed to set permissions for directory %s: %w", dir, err) } if owner != "" { err = p.Wet(h, fmt.Sprintf("set owner for directory %s to %s", dir, owner), func() error { return h.Execf(`chown "%s" "%s"`, owner, dir, exec.Sudo(h)) }) if err != nil { return err } } return nil } func (p *UploadFiles) uploadFile(h *cluster.Host, f *cluster.UploadFile) error { log.Infof("%s: uploading %s", h, f) numfiles := len(f.Sources) for i, s := range f.Sources { dest := f.DestinationFile if dest == "" { dest = path.Join(f.DestinationDir, s.Path) } src := path.Join(f.Base, s.Path) if numfiles > 1 { log.Infof("%s: uploading file %s => %s (%d of %d)", h, src, dest, i+1, numfiles) } owner := f.Owner() if err := p.ensureDir(h, path.Dir(dest), f.DirPermString, owner); err != nil { return err } if h.FileChanged(src, dest) { err := p.Wet(h, fmt.Sprintf("upload file %s => %s", src, dest), func() error { return h.Upload(path.Join(f.Base, s.Path), dest, exec.Sudo(h), exec.LogError(true)) }) if err != nil { return err } } else { log.Infof("%s: file already exists and hasn't been changed, skipping upload", h) } if owner != "" { err := p.Wet(h, fmt.Sprintf("set owner for %s to %s", dest, owner), func() error { log.Debugf("%s: setting owner %s for %s", h, owner, dest) return h.Execf(`chown %s %s`, shellescape.Quote(owner), shellescape.Quote(dest), exec.Sudo(h)) }) if err != nil { return err } } err := p.Wet(h, fmt.Sprintf("set permissions for %s to %s", dest, s.PermMode), func() error { log.Debugf("%s: setting permissions %s for %s", h, s.PermMode, dest) return h.Configurer.Chmod(h, dest, s.PermMode, exec.Sudo(h)) }) if err != nil { return err } stat, err := os.Stat(src) if err != nil { return fmt.Errorf("failed to stat %s: %s", src, err) } err = p.Wet(h, fmt.Sprintf("set timestamp for %s to %s", dest, stat.ModTime()), func() error { log.Debugf("%s: touching %s", h, dest) return h.Configurer.Touch(h, dest, stat.ModTime(), exec.Sudo(h)) }) if err != nil { return fmt.Errorf("failed to touch %s: %w", dest, err) } } return nil } func (p *UploadFiles) uploadURL(h *cluster.Host, f *cluster.UploadFile) error { log.Infof("%s: downloading %s to host %s", h, f, f.DestinationFile) owner := f.Owner() if err := p.ensureDir(h, path.Dir(f.DestinationFile), f.DirPermString, owner); err != nil { return err } expandedURL := h.ExpandTokens(f.Source, p.Config.Spec.K0s.Version) err := p.Wet(h, fmt.Sprintf("download file %s => %s", expandedURL, f.DestinationFile), func() error { return h.Configurer.DownloadURL(h, expandedURL, f.DestinationFile, exec.Sudo(h)) }) if err != nil { return err } if f.PermString != "" { err := p.Wet(h, fmt.Sprintf("set permissions for %s to %s", f.DestinationFile, f.PermString), func() error { return h.Configurer.Chmod(h, f.DestinationFile, f.PermString, exec.Sudo(h)) }) if err != nil { return err } } if owner != "" { err := p.Wet(h, fmt.Sprintf("set owner for %s to %s", f.DestinationFile, owner), func() error { log.Debugf("%s: setting owner %s for %s", h, owner, f.DestinationFile) return h.Execf(`chown %s %s`, shellescape.Quote(owner), shellescape.Quote(f.DestinationFile), exec.Sudo(h)) }) if err != nil { return err } } return nil } 07070100000088000081A40000000000000000000000016733442800000C51000000000000000000000000000000000000002D00000000k0sctl-0.19.4/phase/validate_etcd_members.gopackage phase import ( "fmt" "slices" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" log "github.com/sirupsen/logrus" ) // ValidateEtcdMembers checks for existing etcd members with the same IP as a new controller type ValidateEtcdMembers struct { GenericPhase hosts cluster.Hosts } // Title for the phase func (p *ValidateEtcdMembers) Title() string { return "Validate etcd members" } // Prepare the phase func (p *ValidateEtcdMembers) Prepare(config *v1beta1.Cluster) error { p.Config = config p.hosts = p.Config.Spec.Hosts.Controllers().Filter(func(h *cluster.Host) bool { return h.Metadata.K0sRunningVersion == nil // only check new controllers }) return nil } // ShouldRun is true when there are new controllers and etcd func (p *ValidateEtcdMembers) ShouldRun() bool { if p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion == nil { log.Debugf("%s: leader has no k0s running, assuming a fresh cluster", p.Config.Spec.K0sLeader()) return false } if p.Config.Spec.K0sLeader().Role == "single" { log.Debugf("%s: leader is a single node, assuming no etcd", p.Config.Spec.K0sLeader()) return false } if len(p.Config.Spec.K0s.Config) > 0 { storageType := p.Config.Spec.K0s.Config.DigString("spec", "storage", "type") if storageType != "" && storageType != "etcd" { log.Debugf("%s: storage type is %q, not k0s managed etcd", p.Config.Spec.K0sLeader(), storageType) return false } } return len(p.hosts) > 0 } // Run the phase func (p *ValidateEtcdMembers) Run() error { if err := p.validateControllerSwap(); err != nil { return err } return nil } func (p *ValidateEtcdMembers) validateControllerSwap() error { if len(p.Config.Metadata.EtcdMembers) > len(p.Config.Spec.Hosts.Controllers()) { log.Warnf("there are more etcd members in the cluster than controllers listed in the configuration") } for _, h := range p.hosts { log.Debugf("%s: host is new, checking if etcd members list already contains %s", h, h.PrivateAddress) if slices.Contains(p.Config.Metadata.EtcdMembers, h.PrivateAddress) { if Force { log.Infof("%s: force used, running 'k0s etcd leave' for the host", h) leader := p.Config.Spec.K0sLeader() leaveCommand := leader.Configurer.K0sCmdf("etcd leave --peer-address %s", h.PrivateAddress) err := p.Wet(h, fmt.Sprintf("remove host from etcd using %v", leaveCommand), func() error { return leader.Exec(leaveCommand) }) if err != nil { return fmt.Errorf("controller %s is listed as an existing etcd member but k0s is not found installed on it, the host may have been replaced. attempted etcd leave for the address %s but it failed: %w", h, h.PrivateAddress, err) } continue } return fmt.Errorf("controller %s is listed as an existing etcd member but k0s is not found installed on it, the host may have been replaced. check the host and use `k0s etcd leave --peer-address %s on a controller or re-run apply with --force", h, h.PrivateAddress) } log.Debugf("%s: no match, assuming its safe to install", h) } return nil } 07070100000089000081A4000000000000000000000001673344280000074D000000000000000000000000000000000000002600000000k0sctl-0.19.4/phase/validate_facts.gopackage phase import ( "fmt" log "github.com/sirupsen/logrus" ) // ValidateFacts performs remote OS detection type ValidateFacts struct { GenericPhase SkipDowngradeCheck bool } // Title for the phase func (p *ValidateFacts) Title() string { return "Validate facts" } // Run the phase func (p *ValidateFacts) Run() error { if err := p.validateDowngrade(); err != nil { return err } if err := p.validateDefaultVersion(); err != nil { return err } return nil } func (p *ValidateFacts) validateDowngrade() error { if p.SkipDowngradeCheck { return nil } if p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion == nil || p.Config.Spec.K0s.Version == nil { return nil } if p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion.GreaterThan(p.Config.Spec.K0s.Version) { return fmt.Errorf("can't perform a downgrade: %s > %s", p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion, p.Config.Spec.K0s.Version) } return nil } func (p *ValidateFacts) validateDefaultVersion() error { // Only check when running with a defaulted version if !p.Config.Spec.K0s.Metadata.VersionDefaulted { return nil } // Installing a fresh latest is ok if p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion == nil { return nil } // Upgrading should not be performed if the config version was defaulted if p.Config.Spec.K0s.Version.GreaterThan(p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion) { log.Warnf("spec.k0s.version was automatically defaulted to %s but the cluster is running %s", p.Config.Spec.K0s.Version, p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion) log.Warnf("to perform an upgrade, set the k0s version in the configuration explicitly") p.Config.Spec.K0s.Version = p.Config.Spec.K0sLeader().Metadata.K0sRunningVersion for _, h := range p.Config.Spec.Hosts { h.Metadata.NeedsUpgrade = false } } return nil } 0707010000008A000081A40000000000000000000000016733442800000A4D000000000000000000000000000000000000002600000000k0sctl-0.19.4/phase/validate_hosts.gopackage phase import ( "fmt" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" log "github.com/sirupsen/logrus" ) // ValidateHosts performs remote OS detection type ValidateHosts struct { GenericPhase hncount map[string]int machineidcount map[string]int privateaddrcount map[string]int } // Title for the phase func (p *ValidateHosts) Title() string { return "Validate hosts" } // Run the phase func (p *ValidateHosts) Run() error { p.hncount = make(map[string]int, len(p.Config.Spec.Hosts)) if uniqueMachineIDVersion.Check(p.Config.Spec.K0s.Version) { p.machineidcount = make(map[string]int, len(p.Config.Spec.Hosts)) } p.privateaddrcount = make(map[string]int, len(p.Config.Spec.Hosts)) controllerCount := len(p.Config.Spec.Hosts.Controllers()) var resetControllerCount int for _, h := range p.Config.Spec.Hosts { p.hncount[h.Metadata.Hostname]++ if p.machineidcount != nil { p.machineidcount[h.Metadata.MachineID]++ } if h.PrivateAddress != "" { p.privateaddrcount[h.PrivateAddress]++ } if h.IsController() && h.Reset { resetControllerCount++ } } if resetControllerCount >= controllerCount { return fmt.Errorf("all controllers are marked to be reset - this will break the cluster. use `k0sctl reset` instead if that is intentional") } return p.parallelDo( p.Config.Spec.Hosts, p.warnK0sBinaryPath, p.validateUniqueHostname, p.validateUniqueMachineID, p.validateUniquePrivateAddress, p.validateSudo, ) } func (p *ValidateHosts) warnK0sBinaryPath(h *cluster.Host) error { if h.K0sBinaryPath != "" { log.Warnf("%s: k0s binary path is set to %q, version checking for the host is disabled. The k0s version for other hosts is %s.", h, h.K0sBinaryPath, p.Config.Spec.K0s.Version) } return nil } func (p *ValidateHosts) validateUniqueHostname(h *cluster.Host) error { if p.hncount[h.Metadata.Hostname] > 1 { return fmt.Errorf("hostname is not unique: %s", h.Metadata.Hostname) } return nil } func (p *ValidateHosts) validateUniquePrivateAddress(h *cluster.Host) error { if p.privateaddrcount[h.PrivateAddress] > 1 { return fmt.Errorf("privateAddress %q is not unique: %s", h.PrivateAddress, h.Metadata.Hostname) } return nil } func (p *ValidateHosts) validateUniqueMachineID(h *cluster.Host) error { if p.machineidcount[h.Metadata.MachineID] > 1 { return fmt.Errorf("machine id %s is not unique: %s", h.Metadata.MachineID, h.Metadata.Hostname) } return nil } func (p *ValidateHosts) validateSudo(h *cluster.Host) error { if err := h.Configurer.CheckPrivilege(h); err != nil { return err } return nil } 0707010000008B000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001200000000k0sctl-0.19.4/pkg0707010000008C000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001700000000k0sctl-0.19.4/pkg/apis0707010000008D000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000002C00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io0707010000008E000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000003400000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta10707010000008F000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000003C00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster07070100000090000081A40000000000000000000000016733442800000667000000000000000000000000000000000000003F00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.gopackage v1beta1 import ( "fmt" "github.com/creasty/defaults" "github.com/jellydator/validation" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) // APIVersion is the current api version const APIVersion = "k0sctl.k0sproject.io/v1beta1" // ClusterMetadata defines cluster metadata type ClusterMetadata struct { Name string `yaml:"name" validate:"required" default:"k0s-cluster"` Kubeconfig string `yaml:"-"` EtcdMembers []string `yaml:"-"` } // Cluster describes launchpad.yaml configuration type Cluster struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata *ClusterMetadata `yaml:"metadata"` Spec *cluster.Spec `yaml:"spec"` } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml func (c *Cluster) UnmarshalYAML(unmarshal func(interface{}) error) error { c.Metadata = &ClusterMetadata{ Name: "k0s-cluster", } c.Spec = &cluster.Spec{} type clusterConfig Cluster yc := (*clusterConfig)(c) if err := unmarshal(yc); err != nil { return err } if err := defaults.Set(c); err != nil { return fmt.Errorf("failed to set defaults: %w", err) } return nil } // Validate performs a configuration sanity check func (c *Cluster) Validate() error { validation.ErrorTag = "yaml" return validation.ValidateStruct(c, validation.Field(&c.APIVersion, validation.Required, validation.In(APIVersion).Error("must equal "+APIVersion)), validation.Field(&c.Kind, validation.Required, validation.In("cluster", "Cluster").Error("must equal Cluster")), validation.Field(&c.Spec), ) } 07070100000091000081A40000000000000000000000016733442800001290000000000000000000000000000000000000004500000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags.gopackage cluster import ( "fmt" "strconv" "strings" "github.com/alessio/shellescape" "github.com/k0sproject/k0sctl/internal/shell" ) // Flags is a slice of strings with added functions to ease manipulating lists of command-line flags type Flags []string // Add adds a flag regardless if it exists already or not func (f *Flags) Add(s string) { if ns, err := shell.Unquote(s); err == nil { s = ns } *f = append(*f, s) } // Add a flag with a value func (f *Flags) AddWithValue(key, value string) { if nv, err := shell.Unquote(value); err == nil { value = nv } *f = append(*f, key+"="+value) } // AddUnlessExist adds a flag unless one with the same prefix exists func (f *Flags) AddUnlessExist(s string) { if ns, err := shell.Unquote(s); err == nil { s = ns } if f.Include(s) { return } f.Add(s) } // AddOrReplace replaces a flag with the same prefix or adds a new one if one does not exist func (f *Flags) AddOrReplace(s string) { if ns, err := shell.Unquote(s); err == nil { s = ns } idx := f.Index(s) if idx > -1 { (*f)[idx] = s return } f.Add(s) } // Include returns true if a flag with a matching prefix can be found func (f Flags) Include(s string) bool { return f.Index(s) > -1 } // Index returns an index to a flag with a matching prefix func (f Flags) Index(s string) int { if ns, err := shell.Unquote(s); err == nil { s = ns } var flag string sepidx := strings.IndexAny(s, "= ") if sepidx < 0 { flag = s } else { flag = s[:sepidx] } for i, v := range f { if v == s || strings.HasPrefix(v, flag+"=") || strings.HasPrefix(v, flag+" ") { return i } } return -1 } // Get returns the full flag with the possible value such as "--san=10.0.0.1" or "" when not found func (f Flags) Get(s string) string { idx := f.Index(s) if idx < 0 { return "" } return f[idx] } // GetValue returns the value part of a flag such as "10.0.0.1" for a flag like "--san=10.0.0.1" func (f Flags) GetValue(s string) string { fl := f.Get(s) if fl == "" { return "" } if nfl, err := shell.Unquote(fl); err == nil { fl = nfl } idx := strings.IndexAny(fl, "= ") if idx < 0 { return "" } val := fl[idx+1:] return val } // GetValue returns the boolean value part of a flag such as true for a flag like "--san" // If the flag is not defined returns false. If the flag is defined without a value, returns true // If no value is set, returns true func (f Flags) GetBoolean(s string) (bool, error) { idx := f.Index(s) if idx < 0 { return false, nil } fl := f.GetValue(s) if fl == "" { return true, nil } return strconv.ParseBool(fl) } // Delete removes a matching flag from the list func (f *Flags) Delete(s string) { idx := f.Index(s) if idx < 0 { return } *f = append((*f)[:idx], (*f)[idx+1:]...) } // Merge takes the flags from another Flags and adds them to this one unless this already has that flag set func (f *Flags) Merge(b Flags) { for _, flag := range b { f.AddUnlessExist(flag) } } // MergeOverwrite takes the flags from another Flags and adds or replaces them into this one func (f *Flags) MergeOverwrite(b Flags) { for _, flag := range b { f.AddOrReplace(flag) } } // MergeAdd takes the flags from another Flags and adds them into this one even if they exist func (f *Flags) MergeAdd(b Flags) { for _, flag := range b { f.Add(flag) } } // Join creates a string separated by spaces func (f *Flags) Join() string { var parts []string f.Each(func(k, v string) { if v == "" && k != "" { parts = append(parts, shellescape.Quote(k)) } else { parts = append(parts, fmt.Sprintf("%s=%s", k, shellescape.Quote(v))) } }) return strings.Join(parts, " ") } // Each iterates over each flag and calls the function with the flag key and value as arguments func (f Flags) Each(fn func(string, string)) { for _, flag := range f { sepidx := strings.IndexAny(flag, "= ") if sepidx < 0 { if flag == "" { continue } fn(flag, "") } else { key, value := flag[:sepidx], flag[sepidx+1:] if unq, err := shell.Unquote(value); err == nil { value = unq } fn(key, value) } } } // Map returns a map[string]string of the flags where the key is the flag and the value is the value func (f Flags) Map() map[string]string { res := make(map[string]string) f.Each(func(k, v string) { res[k] = v }) return res } // Equals compares the flags with another Flags and returns true if they have the same flags and values, ignoring order func (f Flags) Equals(b Flags) bool { if len(f) != len(b) { return false } for _, flag := range f { if !b.Include(flag) { return false } ourValue := f.GetValue(flag) theirValue := b.GetValue(flag) if ourValue != theirValue { return false } } return true } 07070100000092000081A40000000000000000000000016733442800000EE2000000000000000000000000000000000000004A00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags_test.gopackage cluster import ( "testing" "github.com/stretchr/testify/require" ) func TestFlags(t *testing.T) { flags := Flags{"--admin-username=foofoo", "--san foo", "--ucp-insecure-tls"} require.Equal(t, "--ucp-insecure-tls", flags[2]) require.Equal(t, 0, flags.Index("--admin-username")) require.Equal(t, 1, flags.Index("--san")) require.Equal(t, 2, flags.Index("--ucp-insecure-tls")) require.True(t, flags.Include("--san")) flags.Delete("--san") require.Equal(t, 1, flags.Index("--ucp-insecure-tls")) require.False(t, flags.Include("--san")) flags.AddOrReplace("--san 10.0.0.1") require.Equal(t, 2, flags.Index("--san")) require.Equal(t, "--san 10.0.0.1", flags.Get("--san")) require.Equal(t, "10.0.0.1", flags.GetValue("--san")) require.Equal(t, "foofoo", flags.GetValue("--admin-username")) require.Len(t, flags, 3) flags.AddOrReplace("--admin-password=barbar") require.Equal(t, 3, flags.Index("--admin-password")) require.Equal(t, "barbar", flags.GetValue("--admin-password")) require.Len(t, flags, 4) flags.AddUnlessExist("--admin-password=borbor") require.Len(t, flags, 4) require.Equal(t, "barbar", flags.GetValue("--admin-password")) flags.AddUnlessExist("--help") require.Len(t, flags, 5) require.True(t, flags.Include("--help")) } func TestFlagsWithQuotes(t *testing.T) { flags := Flags{"--admin-username \"foofoo\"", "--admin-password=\"foobar\""} require.Equal(t, "foofoo", flags.GetValue("--admin-username")) require.Equal(t, "foobar", flags.GetValue("--admin-password")) } func TestString(t *testing.T) { flags := Flags{"--help", "--setting=false"} require.Equal(t, "--help --setting=false", flags.Join()) } func TestGetBoolean(t *testing.T) { t.Run("Valid flags", func(t *testing.T) { testsValid := []struct { flag string expect bool }{ {"--flag", true}, {"--flag=true", true}, {"--flag=false", false}, {"--flag=1", true}, {"--flag=TRUE", true}, } for _, test := range testsValid { flags := Flags{test.flag} result, err := flags.GetBoolean(test.flag) require.NoError(t, err) require.Equal(t, test.expect, result) flags = Flags{"--unrelated-flag1", "--unrelated-flag2=foo", test.flag} result, err = flags.GetBoolean(test.flag) require.NoError(t, err) require.Equal(t, test.expect, result) } }) t.Run("Invalid flags", func(t *testing.T) { testsInvalid := []string{ "--flag=foo", "--flag=2", "--flag=TrUe", "--flag=-4", "--flag=FalSe", } for _, test := range testsInvalid { flags := Flags{test} _, err := flags.GetBoolean(test) require.Error(t, err) flags = Flags{"--unrelated-flag1", "--unrelated-flag2=foo", test} _, err = flags.GetBoolean(test) require.Error(t, err) } }) t.Run("Unknown flags", func(t *testing.T) { flags := Flags{"--flag1=1", "--flag2"} result, err := flags.GetBoolean("--flag3") require.NoError(t, err) require.Equal(t, result, false) }) } func TestEach(t *testing.T) { flags := Flags{"--flag1", "--flag2=foo", "--flag3=bar"} var countF, countV int flags.Each(func(flag string, value string) { countF++ if value != "" { countV++ } }) require.Equal(t, 3, countF) require.Equal(t, 2, countV) } func TestMap(t *testing.T) { flags := Flags{"--flag1", "--flag2=foo", "--flag3=bar"} m := flags.Map() require.Len(t, m, 3) require.Equal(t, "", m["--flag1"]) require.Equal(t, "foo", m["--flag2"]) require.Equal(t, "bar", m["--flag3"]) } func TestEquals(t *testing.T) { flags1 := Flags{"--flag1", "--flag2=foo", "--flag3=bar"} flags2 := Flags{"--flag1", "--flag2=foo", "--flag3=bar"} require.True(t, flags1.Equals(flags2)) flags2 = Flags{"--flag1", "--flag2=foo"} require.False(t, flags1.Equals(flags2)) flags2 = Flags{"-f", "--flag2=foo", "--flag3=baz"} require.False(t, flags1.Equals(flags2)) } 07070100000093000081A4000000000000000000000001673344280000015B000000000000000000000000000000000000004400000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/hook.gopackage cluster // Hooks define a list of hooks such as hooks["apply"]["before"] = ["ls -al", "rm foo.txt"] type Hooks map[string]map[string][]string // ForActionAndStage return hooks for given action and stage func (h Hooks) ForActionAndStage(action, stage string) []string { if len(h[action]) > 0 { return h[action][stage] } return nil } 07070100000094000081A40000000000000000000000016733442800004709000000000000000000000000000000000000004400000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.gopackage cluster import ( "fmt" "net/url" gos "os" gopath "path" "strings" "time" "github.com/alessio/shellescape" "github.com/creasty/defaults" "github.com/go-playground/validator/v10" "github.com/jellydator/validation" "github.com/jellydator/validation/is" "github.com/k0sproject/k0sctl/internal/shell" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" "github.com/k0sproject/version" log "github.com/sirupsen/logrus" ) var K0sForceFlagSince = version.MustConstraint(">= v1.27.4+k0s.0") // Host contains all the needed details to work with hosts type Host struct { rig.Connection `yaml:",inline"` Role string `yaml:"role"` Reset bool `yaml:"reset,omitempty"` PrivateInterface string `yaml:"privateInterface,omitempty"` PrivateAddress string `yaml:"privateAddress,omitempty"` DataDir string `yaml:"dataDir,omitempty"` Environment map[string]string `yaml:"environment,flow,omitempty"` UploadBinary bool `yaml:"uploadBinary,omitempty"` K0sBinaryPath string `yaml:"k0sBinaryPath,omitempty"` K0sDownloadURL string `yaml:"k0sDownloadURL,omitempty"` InstallFlags Flags `yaml:"installFlags,omitempty"` Files []*UploadFile `yaml:"files,omitempty"` OSIDOverride string `yaml:"os,omitempty"` HostnameOverride string `yaml:"hostname,omitempty"` NoTaints bool `yaml:"noTaints,omitempty"` Hooks Hooks `yaml:"hooks,omitempty"` UploadBinaryPath string `yaml:"-"` Metadata HostMetadata `yaml:"-"` Configurer configurer `yaml:"-"` } func (h *Host) SetDefaults() { if h.OSIDOverride != "" { h.OSVersion = &rig.OSVersion{ID: h.OSIDOverride} } _ = defaults.Set(h.Connection) if h.InstallFlags.Get("--single") != "" && h.InstallFlags.GetValue("--single") != "false" && h.Role != "single" { log.Debugf("%s: changed role from '%s' to 'single' because of --single installFlag", h, h.Role) h.Role = "single" } if h.InstallFlags.Get("--enable-worker") != "" && h.InstallFlags.GetValue("--enable-worker") != "false" && h.Role != "controller+worker" { log.Debugf("%s: changed role from '%s' to 'controller+worker' because of --enable-worker installFlag", h, h.Role) h.Role = "controller+worker" } if h.InstallFlags.Get("--no-taints") != "" && h.InstallFlags.GetValue("--no-taints") != "false" { h.NoTaints = true } if dd := h.InstallFlags.GetValue("--data-dir"); dd != "" { if h.DataDir != "" { log.Debugf("%s: changed dataDir from '%s' to '%s' because of --data-dir installFlag", h, h.DataDir, dd) } h.InstallFlags.Delete("--data-dir") h.DataDir = dd } } func validateBalancedQuotes(val any) error { s, ok := val.(string) if !ok { return fmt.Errorf("invalid type") } quoteCount := make(map[rune]int) for i, ch := range s { if i > 0 && s[i-1] == '\\' { continue } if ch == '\'' || ch == '"' { quoteCount[ch]++ } } for _, count := range quoteCount { if count%2 != 0 { return fmt.Errorf("unbalanced quotes in %s", s) } } return nil } func (h *Host) Validate() error { // For rig validation v := validator.New() if err := v.Struct(h); err != nil { return err } return validation.ValidateStruct(h, validation.Field(&h.Role, validation.In("controller", "worker", "controller+worker", "single").Error("unknown role "+h.Role)), validation.Field(&h.PrivateAddress, is.IP), validation.Field(&h.Files), validation.Field(&h.NoTaints, validation.When(h.Role != "controller+worker", validation.NotIn(true).Error("noTaints can only be true for controller+worker role"))), validation.Field(&h.InstallFlags, validation.Each(validation.By(validateBalancedQuotes))), ) } type configurer interface { Kind() string CheckPrivilege(os.Host) error StartService(os.Host, string) error StopService(os.Host, string) error RestartService(os.Host, string) error ServiceIsRunning(os.Host, string) bool Arch(os.Host) (string, error) K0sCmdf(string, ...interface{}) string K0sBinaryPath() string K0sBinaryVersion(os.Host) (*version.Version, error) K0sConfigPath() string DataDirDefaultPath() string K0sJoinTokenPath() string WriteFile(os.Host, string, string, string) error UpdateEnvironment(os.Host, map[string]string) error DaemonReload(os.Host) error ReplaceK0sTokenPath(os.Host, string) error ServiceScriptPath(os.Host, string) (string, error) ReadFile(os.Host, string) (string, error) FileExist(os.Host, string) bool Chmod(os.Host, string, string, ...exec.Option) error DownloadK0s(os.Host, string, *version.Version, string, ...exec.Option) error DownloadURL(os.Host, string, string, ...exec.Option) error InstallPackage(os.Host, ...string) error FileContains(os.Host, string, string) bool MoveFile(os.Host, string, string) error MkDir(os.Host, string, ...exec.Option) error DeleteFile(os.Host, string) error CommandExist(os.Host, string) bool Hostname(os.Host) string KubectlCmdf(os.Host, string, string, ...interface{}) string KubeconfigPath(os.Host, string) string IsContainer(os.Host) bool FixContainer(os.Host) error HTTPStatus(os.Host, string) (int, error) PrivateInterface(os.Host) (string, error) PrivateAddress(os.Host, string, string) (string, error) TempDir(os.Host) (string, error) TempFile(os.Host) (string, error) UpdateServiceEnvironment(os.Host, string, map[string]string) error CleanupServiceEnvironment(os.Host, string) error Stat(os.Host, string, ...exec.Option) (*os.FileInfo, error) Touch(os.Host, string, time.Time, ...exec.Option) error DeleteDir(os.Host, string, ...exec.Option) error K0sctlLockFilePath(os.Host) string UpsertFile(os.Host, string, string) error MachineID(os.Host) (string, error) SetPath(string, string) } // HostMetadata resolved metadata for host type HostMetadata struct { K0sBinaryVersion *version.Version K0sBinaryTempFile string K0sRunningVersion *version.Version K0sInstalled bool K0sExistingConfig string K0sNewConfig string K0sJoinToken string K0sJoinTokenID string K0sStatusArgs Flags Arch string IsK0sLeader bool Hostname string Ready bool NeedsUpgrade bool MachineID string DryRunFakeLeader bool } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml func (h *Host) UnmarshalYAML(unmarshal func(interface{}) error) error { type host Host yh := (*host)(h) yh.Environment = make(map[string]string) if err := unmarshal(yh); err != nil { return err } if h.SSH != nil && h.SSH.HostKey != "" { log.Warnf("%s: host.ssh.hostKey is deprecated, use a ssh known hosts file instead", h) } return defaults.Set(h) } // Address returns an address for the host func (h *Host) Address() string { if addr := h.Connection.Address(); addr != "" { return addr } return "127.0.0.1" } // Protocol returns host communication protocol func (h *Host) Protocol() string { if h.SSH != nil { return "ssh" } if h.WinRM != nil { return "winrm" } if h.Localhost != nil { return "local" } return "nil" } // ResolveConfigurer assigns a rig-style configurer to the Host (see configurer/) func (h *Host) ResolveConfigurer() error { bf, err := registry.GetOSModuleBuilder(*h.OSVersion) if err != nil { return err } if c, ok := bf().(configurer); ok { h.Configurer = c return nil } return fmt.Errorf("unsupported OS") } // K0sJoinTokenPath returns the token file path from install flags or configurer func (h *Host) K0sJoinTokenPath() string { if path := h.InstallFlags.GetValue("--token-file"); path != "" { return path } return h.Configurer.K0sJoinTokenPath() } // K0sConfigPath returns the config file path from install flags or configurer func (h *Host) K0sConfigPath() string { if path := h.InstallFlags.GetValue("--config"); path != "" { return path } if path := h.InstallFlags.GetValue("-c"); path != "" { return path } return h.Configurer.K0sConfigPath() } func (h *Host) K0sRole() string { switch h.Role { case "controller+worker", "single": return "controller" default: return h.Role } } func (h *Host) K0sInstallFlags() (Flags, error) { flags := Flags(h.InstallFlags) flags.AddOrReplace(fmt.Sprintf("--data-dir=%s", shellescape.Quote(h.K0sDataDir()))) switch h.Role { case "controller+worker": flags.AddUnlessExist("--enable-worker") if h.NoTaints { flags.AddUnlessExist("--no-taints") } case "single": flags.AddUnlessExist("--single=true") } if !h.Metadata.IsK0sLeader { flags.AddUnlessExist(fmt.Sprintf(`--token-file=%s`, shellescape.Quote(h.K0sJoinTokenPath()))) } if h.IsController() { flags.AddUnlessExist(fmt.Sprintf(`--config=%s`, shellescape.Quote(h.K0sConfigPath()))) } if strings.HasSuffix(h.Role, "worker") { var extra Flags if old := flags.GetValue("--kubelet-extra-args"); old != "" { parts, err := shell.Split(old) if err != nil { return flags, fmt.Errorf("failed to split kubelet-extra-args: %w", err) } for _, part := range parts { extra.Add(part) } } // set worker's private address to --node-ip in --extra-kubelet-args if cloud ins't enabled enableCloudProvider, err := h.InstallFlags.GetBoolean("--enable-cloud-provider") if err != nil { return flags, fmt.Errorf("--enable-cloud-provider flag is set to invalid value: %s. (%v)", h.InstallFlags.GetValue("--enable-cloud-provider"), err) } if !enableCloudProvider && h.PrivateAddress != "" { extra.AddUnlessExist("--node-ip=" + h.PrivateAddress) } if h.HostnameOverride != "" { extra.AddOrReplace("--hostname-override=" + h.HostnameOverride) } if extra != nil { flags.AddOrReplace(fmt.Sprintf("--kubelet-extra-args=%s", shellescape.Quote(extra.Join()))) } } if flags.Include("--force") && h.Metadata.K0sBinaryVersion != nil && !K0sForceFlagSince.Check(h.Metadata.K0sBinaryVersion) { log.Warnf("%s: k0s version %s does not support the --force flag, ignoring it", h, h.Metadata.K0sBinaryVersion) flags.Delete("--force") } return flags, nil } // K0sInstallCommand returns a full command that will install k0s service with necessary flags func (h *Host) K0sInstallCommand() (string, error) { flags, err := h.K0sInstallFlags() if err != nil { return "", err } return h.Configurer.K0sCmdf("install %s %s", h.K0sRole(), flags.Join()), nil } // K0sBackupCommand returns a full command to be used as run k0s backup func (h *Host) K0sBackupCommand(targetDir string) string { return h.Configurer.K0sCmdf("backup --save-path %s --data-dir %s", shellescape.Quote(targetDir), h.K0sDataDir()) } // K0sRestoreCommand returns a full command to restore cluster state from a backup func (h *Host) K0sRestoreCommand(backupfile string) string { return h.Configurer.K0sCmdf("restore --data-dir=%s %s", h.K0sDataDir(), shellescape.Quote(backupfile)) } // IsController returns true for controller and controller+worker roles func (h *Host) IsController() bool { return h.Role == "controller" || h.Role == "controller+worker" || h.Role == "single" } // K0sServiceName returns correct service name func (h *Host) K0sServiceName() string { switch h.Role { case "controller", "controller+worker", "single": return "k0scontroller" default: return "k0sworker" } } func (h *Host) k0sBinaryPathDir() string { return gopath.Dir(h.Configurer.K0sBinaryPath()) } // InstallK0sBinary installs the k0s binary from the provided file path to K0sBinaryPath func (h *Host) InstallK0sBinary(path string) error { if !h.Configurer.FileExist(h, path) { return fmt.Errorf("k0s binary tempfile not found") } dir := h.k0sBinaryPathDir() if err := h.Execf(`install -m 0755 -o root -g root -d "%s"`, dir, exec.Sudo(h)); err != nil { return fmt.Errorf("create k0s binary dir: %w", err) } if err := h.Execf(`install -m 0750 -o root -g root "%s" "%s"`, path, h.Configurer.K0sBinaryPath(), exec.Sudo(h)); err != nil { return fmt.Errorf("install k0s binary: %w", err) } return nil } // UpdateK0sBinary updates the binary on the host from the provided file path func (h *Host) UpdateK0sBinary(path string, version *version.Version) error { if err := h.InstallK0sBinary(path); err != nil { return fmt.Errorf("update k0s binary: %w", err) } updatedVersion, err := h.Configurer.K0sBinaryVersion(h) if err != nil { return fmt.Errorf("failed to get updated k0s binary version: %w", err) } // verify the installed version matches the expected version, unless a custom k0sbinarypath is used if h.K0sBinaryPath == "" && !version.Equal(updatedVersion) { return fmt.Errorf("updated k0s binary version is %s not %s", updatedVersion, version) } h.Metadata.K0sBinaryVersion = version return nil } // K0sDataDir returns the data dir for the host either from host.DataDir or the default from configurer's DataDirDefaultPath func (h *Host) K0sDataDir() string { if h.DataDir == "" { return h.Configurer.DataDirDefaultPath() } return h.DataDir } // DrainNode drains the given node func (h *Host) DrainNode(node *Host) error { return h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "drain --grace-period=120 --force --timeout=5m --ignore-daemonsets --delete-emptydir-data %s", node.Metadata.Hostname), exec.Sudo(h)) } // CordonNode marks the node unschedulable func (h *Host) CordonNode(node *Host) error { return h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "cordon %s", node.Metadata.Hostname), exec.Sudo(h)) } // UncordonNode marks the node schedulable func (h *Host) UncordonNode(node *Host) error { return h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "uncordon %s", node.Metadata.Hostname), exec.Sudo(h)) } // DeleteNode deletes the given node from kubernetes func (h *Host) DeleteNode(node *Host) error { return h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "delete node %s", node.Metadata.Hostname), exec.Sudo(h)) } // CheckHTTPStatus will perform a web request to the url and return an error if the http status is not the expected func (h *Host) CheckHTTPStatus(url string, expected ...int) error { status, err := h.Configurer.HTTPStatus(h, url) if err != nil { return err } for _, e := range expected { if status == e { return nil } } return fmt.Errorf("expected response code %d but received %d", expected, status) } // NeedCurl returns true when the curl package is needed on the host func (h *Host) NeedCurl() bool { // Windows does not need any packages for web requests if h.Configurer.Kind() == "windows" { return false } return !h.Configurer.CommandExist(h, "curl") } // NeedIPTables returns true when the iptables package is needed on the host // // Deprecated: iptables is only required for k0s versions that are unsupported // for a long time already (< v1.22.1+k0s.0). func (h *Host) NeedIPTables() bool { // Windows does not need iptables if h.Configurer.Kind() == "windows" { return false } // Controllers do not need iptables if h.IsController() { return false } return !h.Configurer.CommandExist(h, "iptables") } // NeedInetUtils returns true when the inetutils package is needed on the host to run `hostname`. func (h *Host) NeedInetUtils() bool { // Windows does not need inetutils if h.Configurer.Kind() == "windows" { return false } return !h.Configurer.CommandExist(h, "hostname") } // FileChanged returns true when a remote file has different size or mtime compared to local // or if an error occurs func (h *Host) FileChanged(lpath, rpath string) bool { lstat, err := gos.Stat(lpath) if err != nil { log.Debugf("%s: local stat failed: %s", h, err) return true } rstat, err := h.Configurer.Stat(h, rpath, exec.Sudo(h)) if err != nil { log.Debugf("%s: remote stat failed: %s", h, err) return true } if lstat.Size() != rstat.Size() { log.Debugf("%s: file sizes for %s differ (%d vs %d)", h, lpath, lstat.Size(), rstat.Size()) return true } if !lstat.ModTime().Equal(rstat.ModTime()) { log.Debugf("%s: file modtimes for %s differ (%s vs %s)", h, lpath, lstat.ModTime(), rstat.ModTime()) return true } return false } // ExpandTokens expands percent-sign prefixed tokens in a string, mainly for the download URLs. // The supported tokens are: // // - %% - literal % // - %p - host architecture (arm, arm64, amd64) // - %v - k0s version (v1.21.0+k0s.0) // - %x - k0s binary extension (.exe on Windows) // // Any unknown token is output as-is with the leading % included. func (h *Host) ExpandTokens(input string, k0sVersion *version.Version) string { if input == "" { return "" } builder := strings.Builder{} var inPercent bool for i := 0; i < len(input); i++ { currCh := input[i] if inPercent { inPercent = false switch currCh { case '%': // Literal %. builder.WriteByte('%') case 'p': // Host architecture (arm, arm64, amd64). builder.WriteString(h.Metadata.Arch) case 'v': // K0s version (v1.21.0+k0s.0) builder.WriteString(url.QueryEscape(k0sVersion.String())) case 'x': // K0s binary extension (.exe on Windows). if h.IsConnected() && h.IsWindows() { builder.WriteString(".exe") } default: // Unknown token, just output it with the leading %. builder.WriteByte('%') builder.WriteByte(currCh) } } else if currCh == '%' { inPercent = true } else { builder.WriteByte(currCh) } } if inPercent { // Trailing %. builder.WriteByte('%') } return builder.String() } // FlagsChanged returns true when the flags have changed by comparing the host.Metadata.K0sStatusArgs to what host.InstallFlags would produce func (h *Host) FlagsChanged() bool { installFlags, err := h.K0sInstallFlags() if err != nil { log.Warnf("%s: could not get install flags: %s", h, err) installFlags = Flags{} } if installFlags.Equals(h.Metadata.K0sStatusArgs) { log.Debugf("%s: installFlags have not changed", h) return false } log.Debugf("%s: installFlags seem to have changed. existing: %+v new: %+v", h, h.Metadata.K0sStatusArgs.Map(), installFlags.Map()) return true } 07070100000095000081A40000000000000000000000016733442800001D19000000000000000000000000000000000000004900000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host_test.gopackage cluster import ( "fmt" "testing" cfg "github.com/k0sproject/k0sctl/configurer" "github.com/k0sproject/k0sctl/configurer/linux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" "github.com/k0sproject/version" "github.com/stretchr/testify/require" ) func TestHostK0sServiceName(t *testing.T) { h := Host{Role: "worker"} require.Equal(t, "k0sworker", h.K0sServiceName()) h.Role = "controller" require.Equal(t, "k0scontroller", h.K0sServiceName()) h.Role = "controller+worker" require.Equal(t, "k0scontroller", h.K0sServiceName()) } type mockconfigurer struct { cfg.Linux linux.Ubuntu } func (c *mockconfigurer) Chmod(_ os.Host, _, _ string, _ ...exec.Option) error { return nil } func (c *mockconfigurer) MkDir(_ os.Host, _ string, _ ...exec.Option) error { return nil } func (c *mockconfigurer) K0sCmdf(s string, args ...interface{}) string { return fmt.Sprintf("k0s %s", fmt.Sprintf(s, args...)) } func TestK0sJoinTokenPath(t *testing.T) { h := Host{} h.Configurer = &mockconfigurer{} h.Configurer.SetPath("K0sJoinTokenPath", "from-configurer") require.Equal(t, "from-configurer", h.K0sJoinTokenPath()) h.InstallFlags.Add("--token-file from-install-flags") require.Equal(t, "from-install-flags", h.K0sJoinTokenPath()) } func TestK0sConfigPath(t *testing.T) { h := Host{} h.Configurer = &mockconfigurer{} h.Configurer.SetPath("K0sConfigPath", "from-configurer") require.Equal(t, "from-configurer", h.K0sConfigPath()) h.InstallFlags.Add("--config from-install-long-flag") require.Equal(t, "from-install-long-flag", h.K0sConfigPath()) h.InstallFlags.Delete("--config") h.InstallFlags.Add("-c from-install-short-flag") require.Equal(t, "from-install-short-flag", h.K0sConfigPath()) } func TestK0sInstallCommand(t *testing.T) { h := Host{Role: "worker", DataDir: "/tmp/k0s", Connection: rig.Connection{Localhost: &rig.Localhost{Enabled: true}}} _ = h.Connect() h.Configurer = &mockconfigurer{} h.Configurer.SetPath("K0sConfigPath", "from-configurer") h.Configurer.SetPath("K0sJoinTokenPath", "from-configurer") cmd, err := h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install worker --data-dir=/tmp/k0s --token-file=from-configurer`, cmd) h.Role = "controller" h.Metadata.IsK0sLeader = true cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install controller --data-dir=/tmp/k0s --config=from-configurer`, cmd) h.Metadata.IsK0sLeader = false cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install controller --data-dir=/tmp/k0s --token-file=from-configurer --config=from-configurer`, cmd) h.Role = "controller+worker" h.Metadata.IsK0sLeader = true cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install controller --data-dir=/tmp/k0s --enable-worker --config=from-configurer`, cmd) h.Metadata.IsK0sLeader = false cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install controller --data-dir=/tmp/k0s --enable-worker --token-file=from-configurer --config=from-configurer`, cmd) h.Role = "worker" h.PrivateAddress = "10.0.0.9" cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install worker --data-dir=/tmp/k0s --token-file=from-configurer --kubelet-extra-args=--node-ip=10.0.0.9`, cmd) h.InstallFlags = []string{`--kubelet-extra-args="--foo bar"`} cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install worker --kubelet-extra-args='--foo bar --node-ip=10.0.0.9' --data-dir=/tmp/k0s --token-file=from-configurer`, cmd) // Verify that K0sInstallCommand does not modify InstallFlags" require.Equal(t, `--kubelet-extra-args='--foo bar'`, h.InstallFlags.Join()) h.InstallFlags = []string{`--enable-cloud-provider=true`} cmd, err = h.K0sInstallCommand() require.NoError(t, err) require.Equal(t, `k0s install worker --enable-cloud-provider=true --data-dir=/tmp/k0s --token-file=from-configurer`, cmd) } func TestValidation(t *testing.T) { t.Run("installFlags", func(t *testing.T) { h := Host{ Role: "worker", InstallFlags: []string{"--foo"}, } require.NoError(t, h.Validate()) h.InstallFlags = []string{`--foo=""`, `--bar=''`} require.NoError(t, h.Validate()) h.InstallFlags = []string{`--foo="`, "--bar"} require.ErrorContains(t, h.Validate(), "unbalanced quotes") h.InstallFlags = []string{"--bar='"} require.ErrorContains(t, h.Validate(), "unbalanced quotes") }) } func TestBinaryPath(t *testing.T) { h := Host{} h.Configurer = &mockconfigurer{} h.Configurer.SetPath("K0sBinaryPath", "/foo/bar/k0s") require.Equal(t, "/foo/bar", h.k0sBinaryPathDir()) } func TestExpandTokens(t *testing.T) { h := Host{ Metadata: HostMetadata{ Arch: "amd64", }, } ver, err := version.NewVersion("v1.0.0+k0s.0") require.NoError(t, err) require.Equal(t, "test%20expand/k0s-v1.0.0%2Bk0s.0-amd64", h.ExpandTokens("test%20expand/k0s-%v-%p%x", ver)) } func TestFlagsChanged(t *testing.T) { cfg := &mockconfigurer{} cfg.SetPath("K0sConfigPath", "/tmp/foo.yaml") cfg.SetPath("K0sJoinTokenPath", "/tmp/token") t.Run("simple", func(t *testing.T) { h := Host{ Configurer: cfg, DataDir: "/tmp/data", Role: "controller", PrivateAddress: "10.0.0.1", InstallFlags: []string{"--foo"}, Metadata: HostMetadata{ K0sStatusArgs: []string{"--foo", "--data-dir=/tmp/data", "--token-file=/tmp/token", "--config=/tmp/foo.yaml"}, }, } require.False(t, h.FlagsChanged()) h.InstallFlags = []string{"--bar"} require.True(t, h.FlagsChanged()) }) t.Run("quoted values", func(t *testing.T) { h := Host{ Configurer: cfg, DataDir: "/tmp/data", Role: "controller", PrivateAddress: "10.0.0.1", InstallFlags: []string{"--foo='bar'", "--bar=foo"}, Metadata: HostMetadata{ K0sStatusArgs: []string{"--foo=bar", `--bar="foo"`, "--data-dir=/tmp/data", "--token-file=/tmp/token", "--config=/tmp/foo.yaml"}, }, } require.False(t, h.FlagsChanged()) h.InstallFlags = []string{"--foo=bar", `--bar="foo"`} require.False(t, h.FlagsChanged()) h.InstallFlags = []string{"--foo=baz", `--bar="foo"`} require.True(t, h.FlagsChanged()) }) t.Run("kubelet-extra-args and single", func(t *testing.T) { h := Host{ Configurer: cfg, DataDir: "/tmp/data", Role: "single", PrivateAddress: "10.0.0.1", InstallFlags: []string{"--foo='bar'", `--kubelet-extra-args="--bar=foo --foo='bar'"`}, Metadata: HostMetadata{ K0sStatusArgs: []string{"--foo=bar", `--kubelet-extra-args="--bar=foo --foo='bar'"`, "--data-dir=/tmp/data", "--single=true", "--token-file=/tmp/token", "--config=/tmp/foo.yaml"}, }, } flags, err := h.K0sInstallFlags() require.NoError(t, err) require.Equal(t, `--foo=bar --kubelet-extra-args='--bar=foo --foo='"'"'bar'"'"'' --data-dir=/tmp/data --single=true --token-file=/tmp/token --config=/tmp/foo.yaml`, flags.Join()) require.False(t, h.FlagsChanged()) h.InstallFlags = []string{"--foo='baz'", `--kubelet-extra-args='--bar=baz --foo="bar"'`} flags, err = h.K0sInstallFlags() require.NoError(t, err) require.Equal(t, `--foo=baz --kubelet-extra-args='--bar=baz --foo="bar"' --data-dir=/tmp/data --single=true --token-file=/tmp/token --config=/tmp/foo.yaml`, flags.Join()) require.True(t, h.FlagsChanged()) }) } 07070100000096000081A40000000000000000000000016733442800000DC6000000000000000000000000000000000000004500000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/hosts.gopackage cluster import ( "fmt" "strings" "sync" ) // Hosts are destnation hosts type Hosts []*Host func (hosts Hosts) Validate() error { if len(hosts) == 0 { return fmt.Errorf("at least one host required") } if len(hosts) > 1 { hostmap := make(map[string]struct{}, len(hosts)) for idx, h := range hosts { if err := h.Validate(); err != nil { return fmt.Errorf("host #%d: %v", idx+1, err) } if h.Role == "single" { return fmt.Errorf("%d hosts defined but includes a host with role 'single': %s", len(hosts), h) } if _, ok := hostmap[h.String()]; ok { return fmt.Errorf("%s: is not unique", h) } hostmap[h.String()] = struct{}{} } } if len(hosts.Controllers()) < 1 { return fmt.Errorf("no hosts with a controller role defined") } return nil } // First returns the first host func (hosts Hosts) First() *Host { if len(hosts) == 0 { return nil } return (hosts)[0] } // Last returns the last host func (hosts Hosts) Last() *Host { c := len(hosts) - 1 if c < 0 { return nil } return hosts[c] } // Find returns the first matching Host. The finder function should return true for a Host matching the criteria. func (hosts Hosts) Find(filter func(h *Host) bool) *Host { for _, h := range hosts { if filter(h) { return (h) } } return nil } // Filter returns a filtered list of Hosts. The filter function should return true for hosts matching the criteria. func (hosts Hosts) Filter(filter func(h *Host) bool) Hosts { result := make(Hosts, 0, len(hosts)) for _, h := range hosts { if filter(h) { result = append(result, h) } } return result } // WithRole returns a ltered list of Hosts that have the given role func (hosts Hosts) WithRole(s string) Hosts { return hosts.Filter(func(h *Host) bool { return h.Role == s }) } // Controllers returns hosts with the role "controller" func (hosts Hosts) Controllers() Hosts { return hosts.Filter(func(h *Host) bool { return h.IsController() }) } // Workers returns hosts with the role "worker" func (hosts Hosts) Workers() Hosts { return hosts.WithRole("worker") } // Each runs a function (or multiple functions chained) on every Host. func (hosts Hosts) Each(filters ...func(h *Host) error) error { for _, filter := range filters { for _, h := range hosts { if err := filter(h); err != nil { return err } } } return nil } // ParallelEach runs a function (or multiple functions chained) on every Host parallelly. // Any errors will be concatenated and returned. func (hosts Hosts) ParallelEach(filters ...func(h *Host) error) error { var wg sync.WaitGroup var mu sync.Mutex var errors []string for _, filter := range filters { for _, h := range hosts { wg.Add(1) go func(h *Host) { defer wg.Done() if err := filter(h); err != nil { mu.Lock() errors = append(errors, fmt.Sprintf("%s: %s", h.String(), err.Error())) mu.Unlock() } }(h) } wg.Wait() } if len(errors) > 0 { return fmt.Errorf("failed on %d hosts:\n - %s", len(errors), strings.Join(errors, "\n - ")) } return nil } // BatchedParallelEach runs a function (or multiple functions chained) on every Host parallelly in groups of batchSize hosts. func (hosts Hosts) BatchedParallelEach(batchSize int, filter ...func(h *Host) error) error { for i := 0; i < len(hosts); i += batchSize { end := i + batchSize if end > len(hosts) { end = len(hosts) } if err := hosts[i:end].ParallelEach(filter...); err != nil { return err } } return nil } 07070100000097000081A40000000000000000000000016733442800001708000000000000000000000000000000000000004300000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/k0s.gopackage cluster import ( "compress/gzip" "context" "encoding/base64" "fmt" "io" "strings" "time" "github.com/jellydator/validation" "github.com/alessio/shellescape" "github.com/creasty/defaults" "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/pkg/retry" "github.com/k0sproject/rig/exec" "github.com/k0sproject/version" "gopkg.in/yaml.v2" ) // K0sMinVersion is the minimum supported k0s version const K0sMinVersion = "0.11.0-rc1" var ( k0sSupportedVersion = version.MustConstraint(">= " + K0sMinVersion) k0sDynamicConfigSince = version.MustConstraint(">= 1.22.2+k0s.2") k0sTokenCreateConfigFlagUntil = version.MustConstraint("< v1.23.4-rc.1+k0s.0") ) // K0s holds configuration for bootstraping a k0s cluster type K0s struct { Version *version.Version `yaml:"version,omitempty"` VersionChannel string `yaml:"versionChannel,omitempty"` DynamicConfig bool `yaml:"dynamicConfig,omitempty" default:"false"` Config dig.Mapping `yaml:"config,omitempty"` Metadata K0sMetadata `yaml:"-"` } // K0sMetadata contains gathered information about k0s cluster type K0sMetadata struct { ClusterID string VersionDefaulted bool } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml func (k *K0s) UnmarshalYAML(unmarshal func(interface{}) error) error { type k0s K0s yk := (*k0s)(k) if err := unmarshal(yk); err != nil { return err } return defaults.Set(k) } // MarshalYAML implements yaml.Marshaler interface func (k *K0s) MarshalYAML() (interface{}, error) { if k == nil { return nil, nil } type k0s K0s yk := (*k0s)(k) yml, err := yaml.Marshal(yk) if err != nil { return nil, fmt.Errorf("marshal k0s: %w", err) } if string(yml) == "{}\n" { return nil, nil } return yk, nil } // SetDefaults sets default values func (k *K0s) SetDefaults() { if k.Version == nil { return } if k.Version.IsZero() { k.Version = nil } } func validateVersion(value interface{}) error { v, ok := value.(*version.Version) if !ok { return fmt.Errorf("not a version") } if v == nil || v.IsZero() { return nil } if !k0sSupportedVersion.Check(v) { return fmt.Errorf("minimum supported k0s version is %s", k0sSupportedVersion) } return nil } func (k *K0s) Validate() error { return validation.ValidateStruct(k, validation.Field(&k.Version, validation.By(validateVersion)), validation.Field(&k.DynamicConfig, validation.By(k.validateMinDynamic())), validation.Field(&k.VersionChannel, validation.In("stable", "latest"), validation.When(k.VersionChannel != "")), ) } func (k *K0s) validateMinDynamic() func(interface{}) error { return func(value interface{}) error { dc, ok := value.(bool) if !ok { return fmt.Errorf("not a boolean") } if !dc { return nil } if k.Version != nil && !k.Version.IsZero() && !k0sDynamicConfigSince.Check(k.Version) { return fmt.Errorf("dynamic config only available since k0s version %s", k0sDynamicConfigSince) } return nil } } func (k *K0s) NodeConfig() dig.Mapping { return dig.Mapping{ "apiVersion": k.Config.DigString("apiVersion"), "kind": k.Config.DigString("kind"), "Metadata": dig.Mapping{ "name": k.Config.DigMapping("metadata")["name"], }, "spec": dig.Mapping{ "api": k.Config.DigMapping("spec", "api"), "network": k.Config.DigMapping("spec", "network"), "storage": k.Config.DigMapping("spec", "storage"), }, } } // GenerateToken runs the k0s token create command func (k *K0s) GenerateToken(h *Host, role string, expiry time.Duration) (string, error) { var k0sFlags Flags k0sFlags.Add(fmt.Sprintf("--role %s", role)) k0sFlags.Add(fmt.Sprintf("--expiry %s", expiry)) k0sFlags.AddOrReplace(fmt.Sprintf("--data-dir=%s", h.K0sDataDir())) if k0sTokenCreateConfigFlagUntil.Check(k.Version) { k0sFlags.Add(fmt.Sprintf("--config %s", shellescape.Quote(h.K0sConfigPath()))) } var token string err := retry.Timeout(context.TODO(), retry.DefaultTimeout, func(_ context.Context) error { output, err := h.ExecOutput(h.Configurer.K0sCmdf("token create %s", k0sFlags.Join()), exec.HideOutput(), exec.Sudo(h)) if err != nil { return fmt.Errorf("create token: %w", err) } token = output return nil }) return token, err } // GetClusterID uses kubectl to fetch the kube-system namespace uid func (k *K0s) GetClusterID(h *Host) (string, error) { return h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "get -n kube-system namespace kube-system -o template={{.metadata.uid}}"), exec.Sudo(h)) } // TokenID returns a token id from a token string that can be used to invalidate the token func TokenID(s string) (string, error) { b64 := make([]byte, base64.StdEncoding.DecodedLen(len(s))) _, err := base64.StdEncoding.Decode(b64, []byte(s)) if err != nil { return "", fmt.Errorf("failed to decode token: %w", err) } sr := strings.NewReader(s) b64r := base64.NewDecoder(base64.StdEncoding, sr) gzr, err := gzip.NewReader(b64r) if err != nil { return "", fmt.Errorf("failed to create a reader for token: %w", err) } defer gzr.Close() c, err := io.ReadAll(gzr) if err != nil { return "", fmt.Errorf("failed to uncompress token: %w", err) } cfg := dig.Mapping{} err = yaml.Unmarshal(c, &cfg) if err != nil { return "", fmt.Errorf("failed to unmarshal token: %w", err) } users, ok := cfg.Dig("users").([]interface{}) if !ok || len(users) < 1 { return "", fmt.Errorf("failed to find users in token") } user, ok := users[0].(dig.Mapping) if !ok { return "", fmt.Errorf("failed to find user in token") } token, ok := user.Dig("user", "token").(string) if !ok { return "", fmt.Errorf("failed to find user token in token") } idx := strings.IndexRune(token, '.') if idx < 0 { return "", fmt.Errorf("failed to find separator in token") } return token[0:idx], nil } 07070100000098000081A40000000000000000000000016733442800000AE0000000000000000000000000000000000000004800000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/k0s_test.gopackage cluster import ( "testing" "github.com/creasty/defaults" "github.com/k0sproject/version" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) func TestTokenID(t *testing.T) { token := "H4sIAAAAAAAC/2xVXY/iOBZ9r1/BH6geO4GeAWkfKiEmGGLKjn1N/BbidAFOgjuk+Frtf18V3SPtSvN2fc/ROdaVfc9L6Q9Q9+fDqZuNLvilaj7PQ92fZy+vo9/17GU0Go3OdX+p+9loPwz+PPvjD/xn8A3/+Q19C2bfx+Pwyanqfjj8OFTlUL+Wn8P+1B+G+6sth3I2WudoWOc4FspSeYjmAqjKlaEcESWeGBpih2muRCQSNucavEEkzBWNDGoApDV1t19W6uNSbJsyRzS1mPc7TVdiDknV0qNFQmjl1zvsaZmao3RECHVd8YZEFtlEgGW8ISmXBIQiY6km+wwbr5v9yoIvVHs71pL81CAio0yYpQ2DJMFSe1InWHEZMZHQveiqa/3hf2Eg+v/FpKJdnZifHCA2aKK5IwwSsbVzYnZgJkWLdUZ8IbfCZA5CE1hSKhxliZ2rkKRxw2hxZIlSEHMgwFWCckUTi8iTmyNy+ZqJUtktO2Y9C8Wpuk8DsTUT7ehnjt9uBTQ0T7yDB9nyw+A4Tlb5wt2NbHgB5LSJpwvR2Ytpp6oKm/lG2ZvUZoDERjs9vubzamxJcZEaX6vDwLKWFeUWIoOqi7z/hWx7c2q77DfcJ5BkQQFAyxYw6xix8BZILAar8Ha3GM7l420ssZ/UZE/rrQtUytSus4ssXGKOissKkdgiOskw1fowPKRqxnFLPy0hj1pPvV6IC0t4AOhGgZDlZjFdGYdXLBVZBozKrUccW6Ra2mQNm5sF9bsHXRVqv8lB7E3XmNyZjKHTSm7Jp82HyxoJDom56HY8zgFa6/xCoOtdIL8qF8t71rDUYBZAI247ZHnpiluZn+9WNu8GsvEusFuOpvNS20J/+GUN1aN2U2kfpFQouVaBj3PsW6VgXwXVeJfSd4DlLdN2JR+gqoAed8hEBcB7OXc4J3Dl2jLuSCQCL0pHo9jhiCU2ygCcSC3hh2moFEQWNTFvfaQS2snGLJXDMdfFWCiquBKRUh8XqZZXgZIbaJEYTLbcUQnBtLDkY8VbWuzmMAhH97ka1tWWKN1lvQFLICEb3tq+0vu+VNXEPqKvN/gQjkQSsejLv3BsUjTRNk8mpNbMF46d1Ju/SURPRWihBOJtS5eVwp9ZQhvIB8+UCo1ksSXg7IPcS2wNc35cphHKVKNE4rebbSR2ODpxd5uYAA/VfH+JW9Jt1GRv231eJ9mj1uao2+Z7pRrB2ulP4+xF5kOxDtUF3PLKJXmXCb4XgQmzuRFVmmGZnCaA/nrIBdCvuRduvMpVs8lcNi7UcDVhRG0A93JLYpP66yqYgJoLoZumlQ9x2xFD8znIkux77oacdWqSdZSVyjCWnkKmb+9WDz/Nh5+b9O1SIDIUHaC6bW5V4qFsYSnSRmUIloXCuV1MaE7IsQAxBkR5ndqASRZtFDVGm7VszHGzwEfhJqzUzTV2tMi1iG369dfsmjVvkxKKfhMPgjsccEUPLMmCTcJCsTDrfGHGdXsOJcBpo4ezQd7sQroC3EQrdLtVD+Z16lZCY58rEO8SrX7vZiId/+AIckiaRa5YBIl67uU1P/3rZTTqyraejRw6v1Snbqhvw6+U+FX/Som/I+PJ+mp8np+nz13d1MPr7nQazkNf+v9X++z7uhte/1Z6Nt2hs7NRfOp+HD5efF//qPu6q+rzbPTv/7x8qT7Nf4v8g/zT+HmF4eTqbjY6fD+E949vVzeZ7vHx8mM6uPCATi//DQAA//+MVAsnAgcAAA==" id, err := TokenID(token) require.NoError(t, err) require.Equal(t, "i6i3yg", id) } func TestUnmarshal(t *testing.T) { t.Run("version given", func(t *testing.T) { k0s := &K0s{} err := yaml.Unmarshal([]byte("version: 0.11.0-rc1\ndynamicConfig: false\n"), k0s) require.NoError(t, err) require.Equal(t, "v0.11.0-rc1", k0s.Version.String()) require.NoError(t, k0s.Validate()) }) t.Run("version not given", func(t *testing.T) { k0s := &K0s{} err := yaml.Unmarshal([]byte("dynamicConfig: false\n"), k0s) require.NoError(t, err) require.NoError(t, k0s.Validate()) }) } func TestVersionDefaulting(t *testing.T) { t.Run("version given", func(t *testing.T) { k0s := &K0s{Version: version.MustParse("v0.11.0-rc1")} require.NoError(t, defaults.Set(k0s)) require.NoError(t, k0s.Validate()) }) } 07070100000099000081A40000000000000000000000016733442800001135000000000000000000000000000000000000004400000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.gopackage cluster import ( "fmt" "strings" "github.com/creasty/defaults" "github.com/jellydator/validation" "gopkg.in/yaml.v2" ) // Spec defines cluster config spec section type Spec struct { Hosts Hosts `yaml:"hosts,omitempty"` K0s *K0s `yaml:"k0s,omitempty"` k0sLeader *Host } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml func (s *Spec) UnmarshalYAML(unmarshal func(interface{}) error) error { type spec Spec ys := (*spec)(s) ys.K0s = &K0s{} if err := unmarshal(ys); err != nil { return err } return defaults.Set(s) } // MarshalYAML implements yaml.Marshaler interface func (s *Spec) MarshalYAML() (interface{}, error) { k0s, err := s.K0s.MarshalYAML() if err != nil { return nil, err } if k0s == nil { return Spec{Hosts: s.Hosts}, nil } return s, nil } // SetDefaults sets defaults func (s *Spec) SetDefaults() { if s.K0s == nil { s.K0s = &K0s{} _ = defaults.Set(s.K0s) } } // K0sLeader returns a controller host that is selected to be a "leader", // or an initial node, a node that creates join tokens for other controllers. func (s *Spec) K0sLeader() *Host { if s.k0sLeader == nil { controllers := s.Hosts.Controllers() // Pick the first controller that reports to be running and persist the choice for _, h := range controllers { if !h.Reset && h.Metadata.K0sBinaryVersion != nil && h.Metadata.K0sRunningVersion != nil { s.k0sLeader = h break } } // Still nil? Fall back to first "controller" host, do not persist selection. if s.k0sLeader == nil { return controllers.First() } } return s.k0sLeader } func (s *Spec) Validate() error { return validation.ValidateStruct(s, validation.Field(&s.Hosts, validation.Required), validation.Field(&s.Hosts), validation.Field(&s.K0s), ) } type k0sCPLBConfig struct { Spec struct { Network struct { ControlPlaneLoadBalancing struct { Enabled bool `yaml:"enabled"` Type string `yaml:"type"` Keepalived struct { VirtualServers []struct { IPAddress string `yaml:"ipAddress"` } `yaml:"virtualServers"` } `yaml:"keepalived"` } `yaml:"controlPlaneLoadBalancing"` } `yaml:"network"` } `yaml:"spec"` } func (s *Spec) clusterExternalAddress() string { if s.K0s != nil { if a := s.K0s.Config.DigString("spec", "api", "externalAddress"); a != "" { return a } if cfg, err := yaml.Marshal(s.K0s.Config); err == nil { k0scfg := k0sCPLBConfig{} if err := yaml.Unmarshal(cfg, &k0scfg); err == nil { cplb := k0scfg.Spec.Network.ControlPlaneLoadBalancing if cplb.Enabled && cplb.Type == "Keepalived" { for _, vs := range cplb.Keepalived.VirtualServers { if addr := vs.IPAddress; addr != "" { return addr } } } } } } if leader := s.K0sLeader(); leader != nil { return leader.Address() } return "" } func (s *Spec) clusterInternalAddress() string { leader := s.K0sLeader() if leader.PrivateAddress != "" { return leader.PrivateAddress } else { return leader.Address() } } const defaultAPIPort = 6443 func (s *Spec) APIPort() int { if s.K0s != nil { if p, ok := s.K0s.Config.Dig("spec", "api", "port").(int); ok { return p } } return defaultAPIPort } // KubeAPIURL returns an external url to the cluster's kube API func (s *Spec) KubeAPIURL() string { return fmt.Sprintf("https://%s:%d", formatIPV6(s.clusterExternalAddress()), s.APIPort()) } // InternalKubeAPIURL returns a cluster internal url to the cluster's kube API func (s *Spec) InternalKubeAPIURL() string { return fmt.Sprintf("https://%s:%d", formatIPV6(s.clusterInternalAddress()), s.APIPort()) } // NodeInternalKubeAPIURL returns a cluster internal url to the node's kube API func (s *Spec) NodeInternalKubeAPIURL(h *Host) string { addr := "127.0.0.1" // spec.api.onlyBindToAddress was introduced in k0s 1.30. Setting it to true will make the API server only // listen on the IP address configured by the `address` option. if onlyBindAddr, ok := s.K0s.Config.Dig("spec", "api", "onlyBindToAddress").(bool); ok && onlyBindAddr { if h.PrivateAddress != "" { addr = h.PrivateAddress } else { addr = h.Address() } } return fmt.Sprintf("https://%s:%d", formatIPV6(addr), s.APIPort()) } func formatIPV6(address string) string { if strings.Contains(address, ":") { return fmt.Sprintf("[%s]", address) } return address } 0707010000009A000081A400000000000000000000000167334428000006D5000000000000000000000000000000000000004900000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec_test.gopackage cluster import ( "testing" "github.com/k0sproject/dig" "github.com/k0sproject/rig" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) func TestKubeAPIURL(t *testing.T) { t.Run("with external address and port", func(t *testing.T) { spec := &Spec{ K0s: &K0s{ Config: dig.Mapping(map[string]any{ "spec": dig.Mapping(map[string]any{ "api": dig.Mapping(map[string]any{ "port": 6444, "externalAddress": "test.example.com", }), }), }), }, Hosts: Hosts{ &Host{ Role: "controller", Connection: rig.Connection{ SSH: &rig.SSH{ Address: "10.0.0.1", }, }, }, }, } require.Equal(t, "https://test.example.com:6444", spec.KubeAPIURL()) }) t.Run("without k0s config", func(t *testing.T) { spec := &Spec{ Hosts: Hosts{ &Host{ Role: "controller", PrivateAddress: "10.0.0.1", Connection: rig.Connection{ SSH: &rig.SSH{ Address: "192.168.0.1", }, }, }, }, } require.Equal(t, "https://192.168.0.1:6443", spec.KubeAPIURL()) }) t.Run("with CPLB", func(t *testing.T) { specYaml := []byte(` hosts: - role: controller ssh: address: 192.168.0.1 privateAddress: 10.0.0.1 k0s: config: spec: network: controlPlaneLoadBalancing: enabled: true type: Keepalived keepalived: vrrpInstances: - virtualIPs: ["192.168.0.10/24"] authPass: CPLB virtualServers: - ipAddress: 192.168.0.10`) spec := &Spec{} err := yaml.Unmarshal(specYaml, spec) require.NoError(t, err) require.Equal(t, "https://192.168.0.10:6443", spec.KubeAPIURL()) }) } 0707010000009B000081A4000000000000000000000001673344280000142A000000000000000000000000000000000000004A00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile.gopackage cluster import ( "fmt" "os" "path" "strconv" "strings" "github.com/bmatcuk/doublestar/v4" "github.com/jellydator/validation" log "github.com/sirupsen/logrus" ) type LocalFile struct { Path string PermMode string } // UploadFile describes a file to be uploaded for the host type UploadFile struct { Name string `yaml:"name,omitempty"` Source string `yaml:"src"` DestinationDir string `yaml:"dstDir"` DestinationFile string `yaml:"dst"` PermMode interface{} `yaml:"perm"` DirPermMode interface{} `yaml:"dirPerm"` User string `yaml:"user"` Group string `yaml:"group"` PermString string `yaml:"-"` DirPermString string `yaml:"-"` Sources []*LocalFile `yaml:"-"` Base string `yaml:"-"` } func (u UploadFile) Validate() error { return validation.ValidateStruct(&u, validation.Field(&u.Source, validation.Required), validation.Field(&u.DestinationFile, validation.Required.When(u.DestinationDir == "").Error("dst or dstdir required")), validation.Field(&u.DestinationDir, validation.Required.When(u.DestinationFile == "").Error("dst or dstdir required")), ) } // converts string or integer value to octal string for chmod func permToString(val interface{}) (string, error) { var s string switch t := val.(type) { case int, float64: var num int if n, ok := t.(float64); ok { num = int(n) } else { num = t.(int) } if num < 0 { return s, fmt.Errorf("invalid permission: %d: must be a positive value", num) } if num == 0 { return s, fmt.Errorf("invalid nil permission") } s = fmt.Sprintf("%#o", num) case string: s = t default: return "", nil } for i, c := range s { n, err := strconv.Atoi(string(c)) if err != nil { return s, fmt.Errorf("failed to parse permission %s: %w", s, err) } // These could catch some weird octal conversion mistakes if i == 1 && n < 4 { return s, fmt.Errorf("invalid permission %s: owner would have unconventional access", s) } if n > 7 { return s, fmt.Errorf("invalid permission %s: octal value can't have numbers over 7", s) } } return s, nil } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml func (u *UploadFile) UnmarshalYAML(unmarshal func(interface{}) error) error { type uploadFile UploadFile yu := (*uploadFile)(u) if err := unmarshal(yu); err != nil { return err } fp, err := permToString(u.PermMode) if err != nil { return err } u.PermString = fp dp, err := permToString(u.DirPermMode) if err != nil { return err } u.DirPermString = dp return u.resolve() } // String returns the file bundle name or if it is empty, the source. func (u *UploadFile) String() string { if u.Name == "" { return u.Source } return u.Name } // Owner returns a chown compatible user:group string from User and Group, or empty when neither are set. func (u *UploadFile) Owner() string { return strings.TrimSuffix(fmt.Sprintf("%s:%s", u.User, u.Group), ":") } // returns true if the string contains any glob characters func isGlob(s string) bool { return strings.ContainsAny(s, "*%?[]{}") } // sets the destination and resolves any globs/local paths into u.Sources func (u *UploadFile) resolve() error { if u.IsURL() { if u.DestinationFile == "" { if u.DestinationDir != "" { u.DestinationFile = path.Join(u.DestinationDir, path.Base(u.Source)) } else { u.DestinationFile = path.Base(u.Source) } } return nil } if isGlob(u.Source) { return u.glob(u.Source) } stat, err := os.Stat(u.Source) if err != nil { return fmt.Errorf("failed to stat local path for %s: %w", u, err) } if stat.IsDir() { log.Tracef("source %s is a directory, assuming %s/**/*", u.Source, u.Source) return u.glob(path.Join(u.Source, "**/*")) } perm := u.PermString if perm == "" { perm = fmt.Sprintf("%o", stat.Mode()) } u.Base = path.Dir(u.Source) u.Sources = []*LocalFile{ {Path: path.Base(u.Source), PermMode: perm}, } return nil } // finds files based on a glob pattern func (u *UploadFile) glob(src string) error { base, pattern := doublestar.SplitPattern(src) u.Base = base fsys := os.DirFS(base) sources, err := doublestar.Glob(fsys, pattern) if err != nil { return err } for _, s := range sources { abs := path.Join(base, s) log.Tracef("glob %s found: %s", abs, s) stat, err := os.Stat(abs) if err != nil { return fmt.Errorf("failed to stat file %s: %w", u, err) } if stat.IsDir() { log.Tracef("%s is a directory", abs) continue } perm := u.PermString if perm == "" { perm = fmt.Sprintf("%o", stat.Mode()) } u.Sources = append(u.Sources, &LocalFile{Path: s, PermMode: perm}) } if len(u.Sources) == 0 { return fmt.Errorf("no files found for %s", u) } if u.DestinationFile != "" && len(u.Sources) > 1 { return fmt.Errorf("found multiple files for %s but single file dst %s defined", u, u.DestinationFile) } return nil } // IsURL returns true if the source is a URL func (u *UploadFile) IsURL() bool { return strings.Contains(u.Source, "://") } 0707010000009C000081A4000000000000000000000001673344280000040F000000000000000000000000000000000000004F00000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.gopackage cluster import ( "testing" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) func TestPermStringUnmarshalWithOctal(t *testing.T) { u := UploadFile{} yml := []byte(` src: . dstDir: . perm: 0755 `) require.NoError(t, yaml.Unmarshal(yml, &u)) require.Equal(t, "0755", u.PermString) } func TestPermStringUnmarshalWithString(t *testing.T) { u := UploadFile{} yml := []byte(` src: . dstDir: . perm: "0755" `) require.NoError(t, yaml.Unmarshal(yml, &u)) require.Equal(t, "0755", u.PermString) } func TestPermStringUnmarshalWithInvalidString(t *testing.T) { u := UploadFile{} yml := []byte(` src: . dstDir: . perm: u+rwx `) require.Error(t, yaml.Unmarshal(yml, &u)) } func TestPermStringUnmarshalWithInvalidNumber(t *testing.T) { u := UploadFile{} yml := []byte(` src: . dstDir: . perm: 0800 `) require.Error(t, yaml.Unmarshal(yml, &u)) } func TestPermStringUnmarshalWithZero(t *testing.T) { u := UploadFile{} yml := []byte(` src: . dstDir: . perm: 0 `) require.Error(t, yaml.Unmarshal(yml, &u)) } 0707010000009D000081A400000000000000000000000167334428000003D1000000000000000000000000000000000000004400000000k0sctl-0.19.4/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster_test.gopackage v1beta1 import ( "testing" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/version" "github.com/stretchr/testify/require" ) func TestAPIVersionValidation(t *testing.T) { cfg := Cluster{ APIVersion: "wrongversion", Kind: "cluster", } require.EqualError(t, cfg.Validate(), "apiVersion: must equal k0sctl.k0sproject.io/v1beta1.") cfg.APIVersion = APIVersion require.NoError(t, cfg.Validate()) } func TestK0sVersionValidation(t *testing.T) { cfg := Cluster{ APIVersion: APIVersion, Kind: "cluster", Spec: &cluster.Spec{ K0s: &cluster.K0s{ Version: version.MustParse("0.1.0"), }, Hosts: cluster.Hosts{ &cluster.Host{Role: "controller"}, }, }, } err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "minimum supported k0s version") cfg.Spec.K0s.Version = version.MustParse(cluster.K0sMinVersion) require.NoError(t, cfg.Validate()) } 0707010000009E000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001700000000k0sctl-0.19.4/pkg/node0707010000009F000081A40000000000000000000000016733442800001B8C000000000000000000000000000000000000002500000000k0sctl-0.19.4/pkg/node/statusfunc.gopackage node import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" ) // this file contains functions that return functions that can be used with pkg/retry to wait on certain // status conditions of nodes type retryFunc func(context.Context) error // kubectl get node -o json type kubeNodeStatus struct { Items []struct { Status struct { Conditions []struct { Status string `json:"status"` Type string `json:"type"` } `json:"conditions"` } `json:"status"` } `json:"items"` } // kubectl get events -o json type statusEvents struct { Items []struct { InvolvedObject struct { Name string `json:"name"` } `json:"involvedObject"` Reason string `json:"reason"` EventTime time.Time `json:"eventTime"` } `json:"items"` } // kubectl get pods -o json type podStatusList struct { Items []struct { Status struct { ContainerStatuses []struct { ContainerID string `json:"containerID"` Name string `json:"name"` Ready bool `json:"ready"` } `json:"containerStatuses"` Phase string `json:"phase"` } `json:"status"` } `json:"items"` } // KubeNodeReady returns a function that returns an error unless the node is ready according to "kubectl get node" func KubeNodeReadyFunc(h *cluster.Host) retryFunc { return func(_ context.Context) error { output, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "get node -l kubernetes.io/hostname=%s -o json", strings.ToLower(h.Metadata.Hostname)), exec.HideOutput(), exec.Sudo(h)) if err != nil { return fmt.Errorf("failed to get node status: %w", err) } status := &kubeNodeStatus{} if err := json.Unmarshal([]byte(output), status); err != nil { return fmt.Errorf("failed to decode kubectl get node status output: %w", err) } for _, i := range status.Items { for _, c := range i.Status.Conditions { if c.Type == "Ready" { if c.Status == "True" { return nil } return fmt.Errorf("node %s is not ready", h.Metadata.Hostname) } } } return fmt.Errorf("node %s 'Ready' condition not found", h.Metadata.Hostname) } } // K0sDynamicConfigReadyFunc returns a function that returns an error unless the k0s dynamic config has been reconciled func K0sDynamicConfigReadyFunc(h *cluster.Host) retryFunc { return func(_ context.Context) error { output, err := h.ExecOutput(h.Configurer.K0sCmdf("kubectl --data-dir=%s -n kube-system get event --field-selector involvedObject.name=k0s -o json", h.K0sDataDir()), exec.Sudo(h)) if err != nil { return fmt.Errorf("failed to get k0s config status events: %w", err) } events := &statusEvents{} if err := json.Unmarshal([]byte(output), &events); err != nil { return fmt.Errorf("failed to decode kubectl output: %w", err) } for _, e := range events.Items { if e.Reason == "SuccessfulReconcile" { return nil } } return fmt.Errorf("dynamic config not ready") } } // ScheduledEventsAfterFunc returns a function that returns an error unless a kube-system 'Scheduled' event has occurred after the given time // The returned function is intended to be used with pkg/retry. func ScheduledEventsAfterFunc(h *cluster.Host, since time.Time) retryFunc { return func(_ context.Context) error { output, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get events --field-selector reason=Scheduled -o json"), exec.HideOutput(), exec.Sudo(h)) if err != nil { return fmt.Errorf("failed to get kube system events: %w", err) } events := &statusEvents{} if err := json.Unmarshal([]byte(output), &events); err != nil { return fmt.Errorf("failed to decode kubectl output for kube-system events: %w", err) } for _, e := range events.Items { if e.EventTime.Before(since) { log.Tracef("%s: skipping prior event for %s: %s < %s", h, e.InvolvedObject.Name, e.EventTime.Format(time.RFC3339), since.Format(time.RFC3339)) continue } log.Debugf("%s: found a 'Scheduled' event occuring after %s", h, since) return nil } return fmt.Errorf("didn't find any 'Scheduled' kube-system events after %s", since) } } // SystemPodsRunningFunc returns a function that returns an error unless all kube-system pods are running func SystemPodsRunningFunc(h *cluster.Host) retryFunc { return func(_ context.Context) error { output, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get pods -o json"), exec.HideOutput(), exec.Sudo(h)) if err != nil { return fmt.Errorf("failed to get kube-system pods: %w", err) } pods := &podStatusList{} if err := json.Unmarshal([]byte(output), &pods); err != nil { return fmt.Errorf("failed to decode kubectl output for get kube-system pods: %w", err) } var running int var notReady int for _, p := range pods.Items { if p.Status.Phase != "Running" { log.Tracef("%s: pod phase '%s' - container statuses: %+v", h, p.Status.Phase, p.Status.ContainerStatuses) continue } running++ for _, c := range p.Status.ContainerStatuses { if !c.Ready { log.Debugf("%s: container %s not ready", h, c.Name) notReady++ } } } if running == 0 { return fmt.Errorf("no kube-system pods running") } if notReady > 0 { return fmt.Errorf("%d kube-system containers not ready", notReady) } log.Debugf("%s: all (%d) system pods running", h, running) return nil } } // HTTPStatus returns a function that returns an error unless the expected status code is returned for a HTTP get to the url func HTTPStatusFunc(h *cluster.Host, url string, expected ...int) retryFunc { return func(_ context.Context) error { return h.CheckHTTPStatus(url, expected...) } } // ServiceRunningFunc returns a function that returns an error until the service is running on the host func ServiceRunningFunc(h *cluster.Host, service string) retryFunc { return func(_ context.Context) error { if !h.Configurer.ServiceIsRunning(h, service) { return fmt.Errorf("service %s is not running", service) } return nil } } // ServiceStoppedFunc returns a function that returns an error if the service is not running on the host func ServiceStoppedFunc(h *cluster.Host, service string) retryFunc { return func(_ context.Context) error { if h.Configurer.ServiceIsRunning(h, service) { return fmt.Errorf("service %s is still running", service) } return nil } } // KubeAPIReadyFunc returns a function that returns an error unless the host's local kube api responds to /version func KubeAPIReadyFunc(h *cluster.Host, config *v1beta1.Cluster) retryFunc { // If the anon-auth is disabled on kube api the version endpoint will give 401 // thus we need to accept both 200 and 401 as valid statuses when checking kube api return HTTPStatusFunc(h, fmt.Sprintf("%s/version", config.Spec.NodeInternalKubeAPIURL(h)), 200, 401) } 070701000000A0000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001800000000k0sctl-0.19.4/pkg/retry070701000000A1000081A40000000000000000000000016733442800000C2E000000000000000000000000000000000000002100000000k0sctl-0.19.4/pkg/retry/retry.go// Package retry provides simple retry wrappers for functions that return an error package retry import ( "context" "errors" "fmt" "time" log "github.com/sirupsen/logrus" ) var ( // DefaultTimeout is a default timeout for retry operations DefaultTimeout = 2 * time.Minute // Interval is the time to wait between retry attempts Interval = 5 * time.Second // ErrAbort should be returned when an error occurs on which retrying should be aborted ErrAbort = errors.New("retrying aborted") ) // Context is a retry wrapper that will retry the given function until it succeeds or the context is cancelled func Context(ctx context.Context, f func(ctx context.Context) error) error { var lastErr error if ctx.Err() != nil { return ctx.Err() } // Execute the function immediately for the first try lastErr = f(ctx) if lastErr == nil || errors.Is(lastErr, ErrAbort) { return lastErr } ticker := time.NewTicker(Interval) defer ticker.Stop() attempt := 0 for { select { case <-ctx.Done(): log.Tracef("retry.Context: context cancelled after %d attempts", attempt) return errors.Join(ctx.Err(), lastErr) case <-ticker.C: attempt++ if lastErr != nil { log.Debugf("retrying, attempt %d - last error: %v", attempt, lastErr) } lastErr = f(ctx) if errors.Is(lastErr, ErrAbort) { log.Tracef("retry.Context: aborted after %d attempts", attempt) return lastErr } if lastErr == nil { log.Tracef("retry.Context: succeeded after %d attempts", attempt) return nil } else { log.Tracef("retry.Context: attempt %d failed: %s", attempt, lastErr) } } } } // Timeout is a retry wrapper that will retry the given function until it succeeds, the context // is cancelled, or the timeout is reached func Timeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) error) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() return Context(ctx, f) } // Times is a retry wrapper that will retry the given function until it succeeds or the given number of // attempts have been made func Times(ctx context.Context, times int, f func(context.Context) error) error { var lastErr error // Execute the function immediately for the first try lastErr = f(ctx) if lastErr == nil || errors.Is(lastErr, ErrAbort) { return lastErr } i := 1 ticker := time.NewTicker(Interval) defer ticker.Stop() for { select { case <-ctx.Done(): log.Tracef("retry.Times: context cancelled after %d attempts", i) return errors.Join(ctx.Err(), lastErr) case <-ticker.C: if lastErr != nil { log.Debugf("retrying: attempt %d of %d (previous error: %v)", i+1, times, lastErr) } lastErr = f(ctx) if errors.Is(lastErr, ErrAbort) { log.Tracef("retry.Times: aborted after %d attempts", i) return lastErr } if lastErr == nil { log.Tracef("retry.Times: succeeded on attempt %d", i) return nil } i++ if i >= times { log.Tracef("retry.Times: exceeded %d attempts", times) return fmt.Errorf("retry limit exceeded after %d attempts: %w", times, lastErr) } } } } 070701000000A2000081A40000000000000000000000016733442800000AC1000000000000000000000000000000000000002600000000k0sctl-0.19.4/pkg/retry/retry_test.gopackage retry import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { oldInterval := Interval Interval = 1 * time.Millisecond defer func() { Interval = oldInterval }() m.Run() } func TestContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() t.Run("succeeds on first try", func(t *testing.T) { err := Context(ctx, func(_ context.Context) error { return nil }) require.NoError(t, err) }) t.Run("fails when context is canceled between tries", func(t *testing.T) { var counter int err := Context(ctx, func(_ context.Context) error { counter++ if counter == 2 { cancel() } return errors.New("some error") }) assert.Error(t, err, "foo") }) t.Run("fails with a canceled context", func(t *testing.T) { err := Context(ctx, func(_ context.Context) error { return errors.New("some error") }) assert.Error(t, err, "some error") }) } func TestTimeout(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() t.Run("succeeds before timeout", func(t *testing.T) { err := Timeout(ctx, 10*time.Second, func(_ context.Context) error { return nil }) require.NoError(t, err) }) t.Run("fails on timeout", func(t *testing.T) { err := Timeout(ctx, 1*time.Millisecond, func(_ context.Context) error { time.Sleep(2 * time.Millisecond) return errors.New("some error") }) assert.Error(t, err, "foo") }) t.Run("stops retrying on ErrAbort", func(t *testing.T) { var counter int err := Timeout(ctx, 10*time.Second, func(_ context.Context) error { counter++ if counter == 2 { return errors.Join(ErrAbort, errors.New("some error")) } return errors.New("some error") }) assert.Error(t, err, "foo") }) } func TestTimes(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() t.Run("succeeds within limit", func(t *testing.T) { counter := 0 err := Times(ctx, 3, func(_ context.Context) error { counter++ if counter == 2 { return nil } return errors.New("some error") }) require.NoError(t, err) assert.Equal(t, 2, counter) }) t.Run("fails on reaching limit", func(t *testing.T) { var tries int err := Times(ctx, 2, func(_ context.Context) error { tries++ return errors.New("some error") }) assert.Error(t, err, "foo") assert.Equal(t, 2, tries) }) t.Run("stops retrying on ErrAbort", func(t *testing.T) { var tries int err := Times(ctx, 2, func(_ context.Context) error { tries++ return errors.Join(ErrAbort, errors.New("some error")) }) assert.Error(t, err, "foo") assert.Equal(t, 1, tries) }) } 070701000000A3000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001900000000k0sctl-0.19.4/smoke-test070701000000A4000081A40000000000000000000000016733442800000031000000000000000000000000000000000000002400000000k0sctl-0.19.4/smoke-test/.gitignorebootloose.yaml id_rsa* k0sctl_040 *.tar.gz *.iid 070701000000A5000081A40000000000000000000000016733442800000582000000000000000000000000000000000000002E00000000k0sctl-0.19.4/smoke-test/Dockerfile.kalilinuxFROM kalilinux/kali-rolling:latest ENV container docker # Don't start any optional services except for the few we need. RUN find /etc/systemd/system \ /lib/systemd/system \ -path '*.wants/*' \ -not -name '*journald*' \ -not -name '*systemd-tmpfiles*' \ -not -name '*systemd-user-sessions*' \ -exec rm \{} \; RUN apt-get update && \ apt-get install -y \ dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny sudo && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Truncate machine ID files to trigger regeneration. RUN >/etc/machine-id RUN >/var/lib/dbus/machine-id EXPOSE 22 RUN systemctl set-default multi-user.target RUN systemctl mask \ dev-hugepages.mount \ sys-fs-fuse-connections.mount \ systemd-update-utmp.service \ systemd-tmpfiles-setup.service \ console-getty.service # This container image doesn't have locales installed. Disable forwarding the # user locale env variables or we get warnings such as: # bash: warning: setlocale: LC_ALL: cannot change locale RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config RUN systemctl enable ssh # This may be needed for some systemd services to start properly. RUN echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d # https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/ STOPSIGNAL SIGRTMIN+3 CMD ["/bin/bash"] 070701000000A6000081A400000000000000000000000167334428000006EF000000000000000000000000000000000000002200000000k0sctl-0.19.4/smoke-test/Makefile bootloose := $(shell which bootloose) ifeq ($(bootloose),) bootloose := $(shell go env GOPATH)/bin/bootloose endif envsubst := $(shell which envsubst) ifeq ($(envsubst),) $(error 'envsubst' NOT found in path, please install it and re-run) endif .PHONY: k0sctl k0sctl: $(MAKE) -C .. k0sctl $(bootloose): go install github.com/k0sproject/bootloose@latest id_rsa_k0s: ssh-keygen -t rsa -f ./id_rsa_k0s -N "" smoke-basic: $(bootloose) id_rsa_k0s k0sctl ./smoke-basic.sh smoke-basic-rootless: $(bootloose) id_rsa_k0s k0sctl ./smoke-basic-rootless.sh smoke-basic-openssh: $(bootloose) id_rsa_k0s k0sctl ./smoke-basic-openssh.sh smoke-dynamic: $(bootloose) id_rsa_k0s k0sctl ./smoke-dynamic.sh smoke-reinstall: $(bootloose) id_rsa_k0s k0sctl ./smoke-reinstall.sh smoke-files: $(bootloose) id_rsa_k0s k0sctl ./smoke-files.sh smoke-init: $(bootloose) id_rsa_k0s k0sctl ./smoke-init.sh smoke-upgrade: $(bootloose) id_rsa_k0s k0sctl ./smoke-upgrade.sh smoke-dryrun: $(bootloose) id_rsa_k0s k0sctl ./smoke-dryrun.sh smoke-reset: $(bootloose) id_rsa_k0s k0sctl ./smoke-reset.sh smoke-os-override: $(bootloose) id_rsa_k0s k0sctl BOOTLOOSE_TEMPLATE=bootloose.yaml.osoverride.tpl K0SCTL_CONFIG=k0sctl-single.yaml OS_RELEASE_PATH=$(realpath os-release) OS_OVERRIDE="ubuntu" ./smoke-basic.sh smoke-downloadurl: $(bootloose) id_rsa_k0s k0sctl BOOTLOOSE_TEMPLATE=bootloose.yaml.single.tpl K0SCTL_CONFIG=k0sctl-downloadurl.yaml ./smoke-basic.sh smoke-backup-restore: $(bootloose) id_rsa_k0s k0sctl ./smoke-backup-restore.sh smoke-controller-swap: $(bootloose) id_rsa_k0s k0sctl BOOTLOOSE_TEMPLATE=bootloose-controller-swap.yaml.tpl K0SCTL_CONFIG=k0sctl-controller-swap.yaml ./smoke-controller-swap.sh %.iid: Dockerfile.% docker build --iidfile '$@' - < '$<' 070701000000A7000081A400000000000000000000000167334428000001C7000000000000000000000000000000000000003C00000000k0sctl-0.19.4/smoke-test/bootloose-controller-swap.yaml.tplcluster: name: k0s privateKey: ./id_rsa_k0s machines: - count: 3 backend: docker spec: image: $LINUX_IMAGE name: manager%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 443 hostPort: 443 - containerPort: 6443 hostPort: 6443 070701000000A8000081A40000000000000000000000016733442800000236000000000000000000000000000000000000003700000000k0sctl-0.19.4/smoke-test/bootloose.yaml.osoverride.tplcluster: name: k0s privateKey: ./id_rsa_k0s machines: - count: 1 backend: docker spec: image: quay.io/k0sproject/bootloose-ubuntu20.04 name: manager%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s - type: bind source: $OS_RELEASE_PATH destination: /etc/os-release portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 443 hostPort: 443 - containerPort: 6443 hostPort: 6443 070701000000A9000081A400000000000000000000000167334428000001E3000000000000000000000000000000000000003300000000k0sctl-0.19.4/smoke-test/bootloose.yaml.single.tplcluster: name: k0s privateKey: ./id_rsa_k0s machines: - count: 1 backend: docker spec: image: quay.io/k0sproject/bootloose-ubuntu20.04 name: manager%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 443 hostPort: 443 - containerPort: 6443 hostPort: 6443 070701000000AA000081A400000000000000000000000167334428000002F6000000000000000000000000000000000000002C00000000k0sctl-0.19.4/smoke-test/bootloose.yaml.tplcluster: name: k0s privateKey: ./id_rsa_k0s machines: - count: 1 backend: docker spec: image: $LINUX_IMAGE name: manager%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022 - containerPort: 443 hostPort: 443 - containerPort: 6443 hostPort: 6443 - count: 1 backend: docker spec: image: $LINUX_IMAGE name: worker%d privileged: true volumes: - type: bind source: /lib/modules destination: /lib/modules - type: volume destination: /var/lib/k0s portMappings: - containerPort: 22 hostPort: 9022070701000000AB000081A4000000000000000000000001673344280000024B000000000000000000000000000000000000003500000000k0sctl-0.19.4/smoke-test/k0sctl-controller-swap.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s - role: controller uploadBinary: true ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s - role: controller uploadBinary: true ssh: address: "127.0.0.1" port: 9024 keyPath: ./id_rsa_k0s k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000AC000081A400000000000000000000000167334428000001E4000000000000000000000000000000000000003100000000k0sctl-0.19.4/smoke-test/k0sctl-downloadurl.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: single k0sDownloadURL: https://github.com/k0sproject/k0s/releases/download/%v/k0s-%v-%p ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s hooks: apply: before: - "echo hello > apply.hook" after: - "grep -q hello apply.hook" k0s: config: spec: telemetry: enabled: false 070701000000AD000081A400000000000000000000000167334428000001EF000000000000000000000000000000000000002C00000000k0sctl-0.19.4/smoke-test/k0sctl-dryrun.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s - role: worker uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000AE000081A400000000000000000000000167334428000001EE000000000000000000000000000000000000002D00000000k0sctl-0.19.4/smoke-test/k0sctl-dynamic.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s - role: worker uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s k0s: version: "${K0S_VERSION}" dynamicConfig: true config: spec: telemetry: enabled: false 070701000000AF000081A4000000000000000000000001673344280000054F000000000000000000000000000000000000002F00000000k0sctl-0.19.4/smoke-test/k0sctl-files.yaml.tplapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s files: - name: single file src: ./upload/toplevel.txt dst: /root/singlefile/renamed.txt user: test group: test - name: dest_dir src: ./upload/toplevel.txt dstDir: /root/destdir - name: perm644 src: ./upload_chmod/script.sh dstDir: /root/chmod perm: 0644 - name: permtransfer src: ./upload_chmod/script.sh dstDir: /root/chmod_exec - name: dir src: ./upload dstDir: /root/dir - name: glob src: ./upload/**/*.txt dstDir: /root/glob dirPerm: 0700 - name: url src: https://api.github.com/repos/k0sproject/k0s/releases dst: /root/url/releases.json - name: url-destdir src: https://api.github.com/repos/k0sproject/k0s/releases dstDir: /root/url_destdir - role: worker uploadBinary: true ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s k0s: version: "$K0S_VERSION" config: spec: telemetry: enabled: false 070701000000B0000081A40000000000000000000000016733442800000227000000000000000000000000000000000000003600000000k0sctl-0.19.4/smoke-test/k0sctl-installflags.yaml.tplapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true installFlags: - "${K0S_CONTROLLER_FLAG}" ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s - role: worker uploadBinary: true installFlags: - "${K0S_WORKER_FLAG}" ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000B1000081A4000000000000000000000001673344280000019B000000000000000000000000000000000000002D00000000k0sctl-0.19.4/smoke-test/k0sctl-openssh.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true openSSH: address: controller configPath: ssh/config - role: worker uploadBinary: true openSSH: address: worker configPath: ssh/config k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000B2000081A400000000000000000000000167334428000002B3000000000000000000000000000000000000003200000000k0sctl-0.19.4/smoke-test/k0sctl-rootless.yaml.tplapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s user: ${SSH_USER} hooks: apply: before: - "echo hello > apply.hook" after: - "grep -q hello apply.hook" - role: worker uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s user: ${SSH_USER} k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000B3000081A400000000000000000000000167334428000001DA000000000000000000000000000000000000002C00000000k0sctl-0.19.4/smoke-test/k0sctl-single.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: single uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s hooks: apply: before: - "echo hello > apply.hook" after: - "grep -q hello apply.hook" k0s: version: "$K0S_VERSION" config: spec: telemetry: enabled: false070701000000B4000081A4000000000000000000000001673344280000027F000000000000000000000000000000000000002500000000k0sctl-0.19.4/smoke-test/k0sctl.yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: cluster spec: hosts: - role: controller uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s hooks: apply: before: - "echo hello > apply.hook" after: - "grep -q hello apply.hook" - role: worker uploadBinary: true os: "$OS_OVERRIDE" ssh: address: "127.0.0.1" port: 9023 keyPath: ./id_rsa_k0s k0s: version: "${K0S_VERSION}" config: spec: telemetry: enabled: false 070701000000B5000081A400000000000000000000000167334428000001A7000000000000000000000000000000000000002400000000k0sctl-0.19.4/smoke-test/os-releaseNAME="Ubuntu-override-test" VERSION="18.04.3 LTS (Bionic Beaver)" ID=override-test ID_LIKE=debian PRETTY_NAME="Override-test -- Ubuntu 18.04.3 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic070701000000B6000081ED0000000000000000000000016733442800000655000000000000000000000000000000000000003100000000k0sctl-0.19.4/smoke-test/smoke-backup-restore.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl.yaml"} set -e . ./smoke.common.sh trap runCleanup EXIT # custom exit trap to cleanup the backup archives runCleanup() { cleanup rm k0s_backup*.tar.gz } deleteCluster createCluster ../k0sctl init ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug # Collect some facts so we can validate restore actually did full restore system_ns_uid=$(bootloose ssh root@manager0 -- k0s kubectl --kubeconfig "/var/lib/k0s/pki/admin.conf" get -n kube-system namespace kube-system -o template='{{.metadata.uid}}') node_uid=$(bootloose ssh root@manager0 -- k0s kubectl --kubeconfig "/var/lib/k0s/pki/admin.conf" get node worker0 -o template='{{.metadata.uid}}') ../k0sctl backup --config "${K0SCTL_CONFIG}" --debug # Reset the controller bootloose ssh root@manager0 -- k0s stop bootloose ssh root@manager0 -- k0s reset ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug --restore-from "$(ls k0s_backup*.tar.gz)" # Verify kube object UIDs match so we know we did full restore of the API objects new_system_ns_uid=$(bootloose ssh root@manager0 -- k0s kubectl --kubeconfig "/var/lib/k0s/pki/admin.conf" get -n kube-system namespace kube-system -o template='{{.metadata.uid}}') if [ "$system_ns_uid" != "$new_system_ns_uid" ]; then echo "kube-system UIDs do not match after restore!!!" exit 1 fi new_node_uid=$(bootloose ssh root@manager0 -- k0s kubectl --kubeconfig "/var/lib/k0s/pki/admin.conf" get node worker0 -o template='{{.metadata.uid}}') if [ "$node_uid" != "$new_node_uid" ]; then echo "worker0 UIDs do not match after restore!!!" exit 1 fi 070701000000B7000081ED0000000000000000000000016733442800000273000000000000000000000000000000000000003000000000k0sctl-0.19.4/smoke-test/smoke-basic-openssh.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl-openssh.yaml"} set -e . ./smoke.common.sh trap cleanup_openssh EXIT cleanup_openssh() { cleanup [ -f "ssh/id_rsa_k0s" ] && rm -rf .ssh } deleteCluster createCluster echo "* Create SSH config" mkdir -p ~/.ssh mkdir -p ssh cp id_rsa_k0s ssh/ cat <<EOF > ssh/config Host * StrictHostKeyChecking no UserKnownHostsFile /dev/null IdentityFile id_rsa_k0s User root Host controller Hostname 127.0.0.1 Port 9022 Host worker Hostname 127.0.0.1 Port 9023 EOF echo "* Starting apply" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug echo "* Apply OK" 070701000000B8000081ED00000000000000000000000167334428000006C1000000000000000000000000000000000000003100000000k0sctl-0.19.4/smoke-test/smoke-basic-rootless.sh#!/usr/bin/env sh export SSH_USER=${SSH_USER:-"k0sctl-user"} K0SCTL_CONFIG="k0sctl-rootless.yaml" envsubst < "k0sctl-rootless.yaml.tpl" > "${K0SCTL_CONFIG}" set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster for host in manager0 worker0; do echo "* Creating ${SSH_USER} on ${host}" bootloose ssh "root@${host}" -- groupadd --system k0sctl-admin bootloose ssh "root@${host}" -- useradd -m -G k0sctl-admin -p '*' "${SSH_USER}" bootloose ssh "root@${host}" -- echo "'%k0sctl-admin ALL=(ALL)NOPASSWD:ALL'" '>/etc/sudoers.d/k0sctl-admin' bootloose ssh "root@${host}" -- chmod 0440 /etc/sudoers.d/k0sctl-admin bootloose ssh "root@${host}" -- mkdir -p "/home/${SSH_USER}/.ssh" bootloose ssh "root@${host}" -- cp '/root/.ssh/*' "/home/${SSH_USER}/.ssh/" bootloose ssh "root@${host}" -- chown -R "${SSH_USER}:${SSH_USER}" "/home/${SSH_USER}/.ssh" done echo "* Starting apply" ../k0sctl apply --config "${K0SCTL_CONFIG}" --kubeconfig-out applykubeconfig --debug echo "* Apply OK" echo "* Verify hooks were executed on the host" bootloose ssh root@manager0 -- grep -q hello "~${SSH_USER}/apply.hook" echo "* Verify 'k0sctl kubeconfig' output includes 'data' block" ../k0sctl kubeconfig --config k0sctl.yaml | grep -v -- "-data" echo "* Run kubectl on controller" bootloose ssh root@manager0 -- k0s kubectl get nodes echo "* Downloading kubectl for local test" downloadKubectl echo "* Using the kubectl from apply" ./kubectl --kubeconfig applykubeconfig get nodes echo "* Using k0sctl kubecofig locally" ../k0sctl kubeconfig --config k0sctl.yaml > kubeconfig echo "* Output:" grep -v -- -data kubeconfig echo "* Running kubectl" ./kubectl --kubeconfig kubeconfig get nodes echo "* Done" 070701000000B9000081ED00000000000000000000000167334428000003C8000000000000000000000000000000000000002800000000k0sctl-0.19.4/smoke-test/smoke-basic.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl.yaml"} set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster echo "* Starting apply" ../k0sctl apply --config "${K0SCTL_CONFIG}" --kubeconfig-out applykubeconfig --debug echo "* Apply OK" echo "* Verify hooks were executed on the host" bootloose ssh root@manager0 -- grep -q hello apply.hook echo "* Verify 'k0sctl kubeconfig' output includes 'data' block" ../k0sctl kubeconfig --config k0sctl.yaml | grep -v -- "-data" echo "* Run kubectl on controller" bootloose ssh root@manager0 -- k0s kubectl get nodes echo "* Downloading kubectl for local test" downloadKubectl echo "* Using the kubectl from apply" ./kubectl --kubeconfig applykubeconfig get nodes echo "* Using k0sctl kubecofig locally" ../k0sctl kubeconfig --config k0sctl.yaml > kubeconfig echo "* Output:" grep -v -- -data kubeconfig echo "* Running kubectl" ./kubectl --kubeconfig kubeconfig get nodes echo "* Done" 070701000000BA000081ED0000000000000000000000016733442800000630000000000000000000000000000000000000003200000000k0sctl-0.19.4/smoke-test/smoke-controller-swap.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl-controller-swap.yaml"} set -ex . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster echo "* Starting apply" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug echo "* Apply OK" echo "* Get the ip of the last controller" controllerip=$(bootloose show manager2 -o json | grep '"ip"' | head -1 | cut -d'"' -f4) echo "* Wipe controller 3" docker rm -fv "$(bootloose show manager2 -o json | grep '"container"' | head -1 | cut -d'"' -f4)" echo "* Verify its gone" bootloose show manager2 | grep "Not created" echo "* Recreate controller2" createCluster echo "* Verify its back and IP is the same" bootloose show manager2 | grep "Running" newip=$(bootloose show manager2 -o json | grep '"ip"' | head -1 | cut -d'"' -f4) if [ "$controllerip" != "$newip" ]; then echo "IP mismatch: $controllerip != $newip - ip should get reused" exit 1 fi echo "* Re-apply should fail because of known hosts" if ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug; then echo "Re-apply should have failed because of known hosts" exit 1 fi echo "* Clear known hosts" truncate -s 0 ~/.ssh/known_hosts echo "* Re-apply should fail because of replaced controller" if ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug; then echo "Re-apply should have failed because of replaced controller" exit 1 fi echo "* Perform etcd member removal" bootloose ssh root@manager0 -- k0s etcd leave --peer-address "$controllerip" echo "* Re-apply should succeed" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug echo "* Done" 070701000000BB000081ED0000000000000000000000016733442800000CBB000000000000000000000000000000000000002900000000k0sctl-0.19.4/smoke-test/smoke-dryrun.sh#!/usr/bin/env bash # Default values for environment variables K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl-dryrun.yaml"} K0S_FROM=${K0S_FROM:-"v1.21.6+k0s.0"} K0S_TO=${K0S_TO:-"$(curl -s "https://docs.k0sproject.io/stable.txt")"} log="smoke-dryrun.log" # Source common functions . ./smoke.common.sh trap cleanup EXIT # Define functions remoteCommand() { local userhost="$1" shift bootloose ssh "${userhost}" -- "$*" } colorEcho() { local color=$1 shift echo -e "\033[1;3${color}m************************************************************\033[0m" echo -e "\033[1;3${color}m$*\033[0m" echo -e "\033[1;3${color}m************************************************************\033[0m" } checkDryRunLines() { local mode=$1 local expected=$2 local count count=$(grep -c "dry-run" "${log}") case "${mode}" in min) if [ "${count}" -lt "${expected}" ]; then colorEcho 1 "Expected at least ${expected} dry-run lines, got ${count}" exit 1 fi ;; none) if [ "${count}" -ne 0 ]; then colorEcho 1 "Expected zero dry-run lines, got ${count}" exit 1 fi ;; *) echo "Unknown mode for checkDryRunLines" exit 1 ;; esac } dryRunNoChanges() { if ! grep -q "no cluster state altering actions" "${log}"; then colorEcho 1 "Expected dry-run to have no changes" exit 1 fi } dumpDryRunLines() { colorEcho 2 "Dry-run filtered log:" grep "dry-run" "${log}" } expectK0sVersion() { local expected=$1 local remote remote=$(remoteCommand "root@manager0" "k0s version") if [ "${remote}" != "${expected}" ]; then colorEcho 1 "Expected k0s version ${expected}, got ${remote}" exit 1 fi } expectNoK0s() { echo "Expecting no k0s on controller" if remoteCommand "root@manager0" "test -d /etc/k0s"; then colorEcho 1 "Expected no /etc/k0s on controller" exit 1 fi if remoteCommand "root@manager0" "test -f /etc/k0s/k0s.yaml"; then colorEcho 1 "Expected no /etc/k0s/k0s.yaml on controller" exit 1 fi if remoteCommand "root@manager0" "ps -ef" | grep -q "k0s controller"; then colorEcho 1 "Expected no k0s controller process on controller" exit 1 fi } applyConfig() { local extra_flag=$1 ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug "${extra_flag}" | tee "${log}" } deleteCluster createCluster K0S_VERSION="${K0S_FROM}" colorEcho 3 "Installing ${K0S_VERSION} with --dry-run" applyConfig "--dry-run" expectNoK0s checkDryRunLines min 3 dumpDryRunLines colorEcho 3 "Installing ${K0S_VERSION}" applyConfig expectK0sVersion "${K0S_FROM}" checkDryRunLines none colorEcho 3 "Installing ${K0S_VERSION} with --dry-run again" applyConfig "--dry-run" expectK0sVersion "${K0S_FROM}" dryRunNoChanges colorEcho 4 "Succesfully installed ${K0S_FROM}, moving on to upgrade to ${K0S_TO}" K0S_VERSION="${K0S_TO}" colorEcho 3 "Upgrading to ${K0S_VERSION} with --dry-run" applyConfig "--dry-run" expectK0sVersion "${K0S_FROM}" checkDryRunLines min 3 dumpDryRunLines colorEcho 3 "Upgrading to ${K0S_VERSION}" applyConfig expectK0sVersion "${K0S_TO}" checkDryRunLines none colorEcho 3 "Upgrading to ${K0S_VERSION} with --dry-run again" applyConfig "--dry-run" expectK0sVersion "${K0S_TO}" dryRunNoChanges 070701000000BC000081ED00000000000000000000000167334428000002CA000000000000000000000000000000000000002A00000000k0sctl-0.19.4/smoke-test/smoke-dynamic.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl-dynamic.yaml"} set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster echo "* Starting apply" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug echo "* Apply OK" max_retry=5 counter=0 echo "* Verifying dynamic config reconciliation was a success" until ../k0sctl config status -o json --config "${K0SCTL_CONFIG}" | grep -q "SuccessfulReconcile" do [ $counter -eq $max_retry ] && echo "Failed!" && exit 1 echo "* Waiting for a couple of seconds to retry" sleep 5 counter=$((counter+1)) done echo "* OK" echo "* Dynamic config reconciliation status:" ../k0sctl config status --config "${K0SCTL_CONFIG}" echo "* Done" 070701000000BD000081ED0000000000000000000000016733442800000CF4000000000000000000000000000000000000002800000000k0sctl-0.19.4/smoke-test/smoke-files.sh#!/usr/bin/env sh K0SCTL_TEMPLATE=${K0SCTL_TEMPLATE:-"k0sctl.yaml.tpl"} set -e . ./smoke.common.sh trap cleanup EXIT envsubst < k0sctl-files.yaml.tpl > k0sctl.yaml deleteCluster createCluster remoteCommand() { local userhost="$1" shift bootloose ssh "${userhost}" -- "$@" } remoteFileExist() { local userhost="$1" local path="$2" remoteCommand "${userhost}" test -e "${path}" } remoteFileContent() { local userhost="$1" local path="$2" remoteCommand "${userhost}" cat "${path}" } echo "* Creating random files" mkdir -p upload mkdir -p upload/nested mkdir -p upload_chmod head -c 8192 </dev/urandom > upload/toplevel.txt head -c 8192 </dev/urandom > upload/nested/nested.txt head -c 8192 </dev/urandom > upload/nested/exclude-on-glob cat << EOF > upload_chmod/script.sh #!/bin/sh echo hello EOF chmod 0744 upload_chmod/script.sh echo "* Creating test user" remoteCommand root@manager0 useradd test echo "* Starting apply" ../k0sctl apply --config k0sctl.yaml --debug echo "* Verifying uploads" remoteCommand root@manager0 "apt-get update > /dev/null && apt-get install tree > /dev/null && tree -fp" printf %s " - Single file using destination file path and user:group .. " remoteFileExist root@manager0 /root/singlefile/renamed.txt printf %s "[exist]" remoteCommand root@manager0 stat -c '%U:%G' /root/singlefile/renamed.txt | grep -q test:test printf %s "[stat]" echo "OK" printf %s " - Single file using destination dir .. " remoteFileExist root@manager0 /root/destdir/toplevel.txt echo "OK" printf %s " - PermMode 644 .. " remoteFileExist root@manager0 /root/chmod/script.sh printf %s "[exist]" remoteCommand root@manager0 stat -c '%a' /root/chmod/script.sh | grep -q 644 printf %s "[stat] " echo "OK" printf %s " - PermMode transfer .." remoteFileExist root@manager0 /root/chmod_exec/script.sh printf %s "[exist] " remoteCommand root@manager0 stat -c '%a' /root/chmod_exec/script.sh | grep -q 744 printf %s "[stat] " remoteCommand root@manager0 /root/chmod_exec/script.sh | grep -q hello printf %s "[run] " echo "OK" printf %s " - Directory using destination dir .. " remoteFileExist root@manager0 /root/dir/toplevel.txt printf %s "[1] " remoteFileExist root@manager0 /root/dir/nested/nested.txt printf %s "[2] " remoteFileExist root@manager0 /root/dir/nested/exclude-on-glob printf %s "[3] " echo "OK" printf %s " - Glob using destination dir .. " remoteFileExist root@manager0 /root/glob/toplevel.txt printf %s "[1] " remoteFileExist root@manager0 /root/glob/nested/nested.txt printf %s "[2] " if remoteFileExist root@manager0 /root/glob/nested/exclude-on-glob; then exit 1; fi printf %s "[3] " remoteCommand root@manager0 stat -c '%a' /root/glob | grep -q 700 printf %s "[stat1]" remoteCommand root@manager0 stat -c '%a' /root/glob/nested | grep -q 700 printf %s "[stat2]" echo "OK" printf %s " - URL using destination file .. " remoteFileExist root@manager0 /root/url/releases.json printf %s "[exist] " remoteFileContent root@manager0 /root/url/releases.json | grep -q html_url printf %s "[content] " echo "OK" printf %s " - URL using destination dir .. " remoteFileExist root@manager0 /root/url_destdir/releases printf %s "[exist] " remoteFileContent root@manager0 /root/url_destdir/releases | grep -q html_url printf %s "[content] " echo "OK" echo "* Done" 070701000000BE000081ED00000000000000000000000167334428000000CD000000000000000000000000000000000000002700000000k0sctl-0.19.4/smoke-test/smoke-init.sh#!/usr/bin/env sh set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster ../k0sctl init --key-path ./id_rsa_k0s 127.0.0.1:9022 root@127.0.0.1:9023 | ../k0sctl apply --config - --debug 070701000000BF000081ED0000000000000000000000016733442800000475000000000000000000000000000000000000002C00000000k0sctl-0.19.4/smoke-test/smoke-reinstall.sh#!/usr/bin/env bash K0SCTL_CONFIG="k0sctl-installflags.yaml" export K0S_CONTROLLER_FLAG="--labels=smoke-stage=1" export K0S_WORKER_FLAG="--labels=smoke-stage=1" envsubst < "k0sctl-installflags.yaml.tpl" > "${K0SCTL_CONFIG}" set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster remoteCommand() { local userhost="$1" shift echo "* Running command on ${userhost}: $*" bootloose ssh "${userhost}" -- "$*" } echo "Installing ${K0S_VERSION}" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug remoteCommand "root@manager0" "k0s status -o json | grep -q -- ${K0S_CONTROLLER_FLAG}" remoteCommand "root@worker0" "k0s status -o json | grep -q -- ${K0S_WORKER_FLAG}" export K0S_CONTROLLER_FLAG="--labels=smoke-stage=2" export K0S_WORKER_FLAG="--labels=smoke-stage=2" envsubst < "k0sctl-installflags.yaml.tpl" > "${K0SCTL_CONFIG}" echo "Re-applying ${K0S_VERSION} with modified installFlags" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug remoteCommand "root@manager0" "k0s status -o json | grep -q -- ${K0S_CONTROLLER_FLAG}" remoteCommand "root@worker0" "k0s status -o json | grep -q -- ${K0S_WORKER_FLAG}" 070701000000C0000081ED000000000000000000000001673344280000013D000000000000000000000000000000000000002800000000k0sctl-0.19.4/smoke-test/smoke-reset.sh#!/usr/bin/env sh K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl.yaml"} set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster echo "* Applying" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug echo "* Resetting" ../k0sctl reset --config "${K0SCTL_CONFIG}" --debug --force echo "* Done, cleaning up" 070701000000C1000081ED0000000000000000000000016733442800000310000000000000000000000000000000000000002A00000000k0sctl-0.19.4/smoke-test/smoke-upgrade.sh#!/usr/bin/env bash K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl.yaml"} set -e . ./smoke.common.sh trap cleanup EXIT deleteCluster createCluster remoteCommand() { local userhost="$1" shift echo "* Running command on ${userhost}: $*" bootloose ssh "${userhost}" -- "$*" } # Create config with older version and apply K0S_VERSION="${K0S_FROM}" echo "Installing ${K0S_VERSION}" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug remoteCommand "root@manager0" "k0s version | grep -q ${K0S_FROM}" K0S_VERSION=$(curl -s "https://docs.k0sproject.io/stable.txt") # Create config with latest version and apply as upgrade echo "Upgrading to k0s ${K0S_VERSION}" ../k0sctl apply --config "${K0SCTL_CONFIG}" --debug remoteCommand "root@manager0" "k0s version | grep -q ${K0S_VERSION}" 070701000000C2000081A400000000000000000000000167334428000003B2000000000000000000000000000000000000002900000000k0sctl-0.19.4/smoke-test/smoke.common.shBOOTLOOSE_TEMPLATE=${BOOTLOOSE_TEMPLATE:-"bootloose.yaml.tpl"} export LINUX_IMAGE="${LINUX_IMAGE:-"quay.io/k0sproject/bootloose-ubuntu20.04"}" export PRESERVE_CLUSTER="${PRESERVE_CLUSTER:-""}" export DISABLE_TELEMETRY=true export K0S_VERSION createCluster() { envsubst < "${BOOTLOOSE_TEMPLATE}" > bootloose.yaml bootloose create } deleteCluster() { # cleanup any existing cluster envsubst < "${BOOTLOOSE_TEMPLATE}" > bootloose.yaml bootloose delete && docker volume prune -f } cleanup() { echo "Cleaning up..." if [ -z "${PRESERVE_CLUSTER}" ]; then deleteCluster fi } downloadKubectl() { OS=$(uname | tr '[:upper:]' '[:lower:]') ARCH="amd64" case $(uname -m) in arm,arm64) ARCH="arm64" ;; esac [ -f kubectl ] || (curl -L https://storage.googleapis.com/kubernetes-release/release/v1.28.2/bin/"${OS}"/${ARCH}/kubectl > ./kubectl && chmod +x ./kubectl) ./kubectl version --client } 070701000000C3000041ED0000000000000000000000026733442800000000000000000000000000000000000000000000001600000000k0sctl-0.19.4/version070701000000C4000081A400000000000000000000000167334428000001C0000000000000000000000000000000000000002100000000k0sctl-0.19.4/version/version.gopackage version import ( "strings" "github.com/carlmjohnson/versioninfo" ) var ( // Version of the product, is set during the build Version = versioninfo.Version // GitCommit is set during the build GitCommit = versioninfo.Revision // Environment of the product, is set during the build Environment = "development" ) // IsPre is true when the current version is a prerelease func IsPre() bool { return strings.Contains(Version, "-") } 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!832 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor