├── internal ├── cli │ ├── recovery_darwin.go │ ├── recovery_windows.go │ ├── config_suite_test.go │ ├── recovery_linux.go │ ├── get_kubeconfig.go │ ├── config_test.go │ ├── role.go │ ├── recovery.go │ ├── register.go │ ├── token │ │ └── rotate.go │ ├── start.go │ └── bridge.go ├── role │ ├── p2p │ │ ├── suite_test.go │ │ ├── common.go │ │ ├── worker.go │ │ ├── k3s_test.go │ │ ├── k8s.go │ │ ├── k8s_test.go │ │ ├── master.go │ │ ├── kubevip.go │ │ ├── k3s.go │ │ └── k0s.go │ ├── common.go │ ├── auto.go │ └── schedule.go ├── provider │ ├── common.go │ ├── provider_suite_test.go │ ├── assets │ │ └── cloudconfig.go │ ├── start.go │ ├── install.go │ ├── challenge.go │ ├── interactive-install.go │ ├── recovery.go │ ├── challenge_test.go │ ├── config │ │ └── config.go │ ├── p2p.go │ ├── bootstrap.go │ └── buildEvent.go ├── assets │ ├── assets.go │ └── static │ │ └── kube_vip_rbac.yaml └── services │ ├── edgevpn.go │ └── k0s.go ├── maintainers.md ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── file-issues-on-main-kairos-repo.md └── workflows │ ├── lint.yml │ ├── osv-scanner-pr.yaml │ ├── release.yaml │ ├── renovate_auto.yml │ ├── test.yml │ └── dependabot_auto.yml ├── osv-scanner.toml ├── main.go ├── .golangci.yml ├── .yamllint ├── cli └── kairosctl │ └── main.go ├── renovate.json ├── .goreleaser.yaml ├── README.md ├── LICENSE └── go.mod /internal/cli/recovery_darwin.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func setWinsize(f *os.File, w, h int) { 8 | } 9 | -------------------------------------------------------------------------------- /internal/cli/recovery_windows.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func setWinsize(f *os.File, w, h int) { 8 | } 9 | -------------------------------------------------------------------------------- /maintainers.md: -------------------------------------------------------------------------------- 1 | You can find Kairos' maintainers list on our [community repo](https://github.com/kairos-io/community/blob/main/MAINTAINERS.md) 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This CODEOWNERS file is used to automatically assign maintainers as reviewers for all pull requests. 2 | * @kairos-io/maintainers 3 | -------------------------------------------------------------------------------- /osv-scanner.toml: -------------------------------------------------------------------------------- 1 | [[IgnoredVulns]] 2 | id = "GHSA-q7pp-wcgr-pffx" 3 | reason = "No impact since we don't work with TIFF image files" 4 | 5 | [[IgnoredVulns]] 6 | id = "GO-2024-3218" 7 | reason = "We dont use the ipfs to serve content, so not vulnerable" -------------------------------------------------------------------------------- /internal/role/p2p/suite_test.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestP2P(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "P2P Suite") 13 | } 14 | -------------------------------------------------------------------------------- /internal/cli/config_suite_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Config Suite") 13 | } 14 | -------------------------------------------------------------------------------- /internal/provider/common.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mudler/go-pluggable" 7 | ) 8 | 9 | func ErrorEvent(format string, a ...interface{}) pluggable.EventResponse { 10 | return pluggable.EventResponse{Error: fmt.Sprintf(format, a...)} 11 | } 12 | -------------------------------------------------------------------------------- /internal/provider/provider_suite_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestInstaller(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Provider Suite") 13 | } 14 | -------------------------------------------------------------------------------- /internal/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | ) 7 | 8 | //go:embed static 9 | var staticFiles embed.FS 10 | 11 | func GetStaticFS() fs.FS { 12 | fsys, err := fs.Sub(staticFiles, "static") 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | return fs.FS(fsys) 18 | } 19 | -------------------------------------------------------------------------------- /internal/cli/recovery_linux.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | ) 8 | 9 | func setWinsize(f *os.File, w, h int) { 10 | syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), //nolint:errcheck 11 | uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) //nolint:errcheck 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/file-issues-on-main-kairos-repo.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: File issues on main Kairos repo 3 | about: Tell users to file their issues on the main Kairos repo 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :warning: All Kairos issues are tracked in our main repo, please file your issue there, thanks! :warning: 11 | 12 | https://github.com/kairos-io/kairos/issues 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: lint-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.repository }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | call-workflow: 14 | uses: kairos-io/linting-composite-action/.github/workflows/reusable-linting.yaml@main 15 | with: 16 | yamldirs: "." 17 | -------------------------------------------------------------------------------- /internal/provider/assets/cloudconfig.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | const LocalDNS = ` 4 | name: DNS Configuration 5 | stages: 6 | initramfs: 7 | - files: 8 | - path: /etc/systemd/resolved.conf 9 | permissions: 0644 10 | owner: 0 11 | group: 0 12 | content: | 13 | [Resolve] 14 | DNS=127.0.0.1 15 | - dns: 16 | nameservers: 17 | - 127.0.0.1 18 | ` 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kairos-io/kairos-sdk/bus" 8 | "github.com/kairos-io/provider-kairos/v2/internal/cli" 9 | "github.com/kairos-io/provider-kairos/v2/internal/provider" 10 | ) 11 | 12 | func checkErr(err error) { 13 | if err != nil { 14 | fmt.Println(err) 15 | os.Exit(1) 16 | } 17 | os.Exit(0) 18 | } 19 | 20 | func main() { 21 | if len(os.Args) >= 2 && bus.IsEventDefined(os.Args[1], "init.provider.info") { 22 | checkErr(provider.Start()) 23 | } 24 | 25 | checkErr(cli.Start()) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/osv-scanner-pr.yaml: -------------------------------------------------------------------------------- 1 | name: OSV-Scanner PR Scan 2 | 3 | # Change "main" to your default branch if you use a different name, i.e. "master" 4 | on: 5 | pull_request: 6 | branches: [main] 7 | merge_group: 8 | branches: [main] 9 | 10 | permissions: 11 | # Require writing security events to upload SARIF file to security tab 12 | security-events: write 13 | # Only need to read contents and actions 14 | contents: read 15 | actions: read 16 | 17 | jobs: 18 | scan-pr: 19 | uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.1" 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | linters: 5 | enable: 6 | - dupl 7 | - goconst 8 | - gocyclo 9 | - godot 10 | - goheader 11 | - misspell 12 | - revive 13 | exclusions: 14 | generated: lax 15 | presets: 16 | - comments 17 | - common-false-positives 18 | - legacy 19 | - std-error-handling 20 | paths: 21 | - tests/ 22 | - third_party$ 23 | - builtin$ 24 | - examples$ 25 | formatters: 26 | enable: 27 | - gofmt 28 | - goimports 29 | exclusions: 30 | generated: lax 31 | paths: 32 | - tests/ 33 | - third_party$ 34 | - builtin$ 35 | - examples$ 36 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | # 80 chars should be enough, but don't fail if a line is longer 5 | line-length: 6 | max: 150 7 | level: warning 8 | ignore: "tests/assets/qrcode.yaml" # tokens are big 9 | 10 | # accept both key: 11 | # - item 12 | # 13 | # and key: 14 | # - item 15 | indentation: 16 | indent-sequences: whatever 17 | 18 | truthy: 19 | check-keys: false 20 | 21 | document-start: 22 | present: false 23 | ignore: "*" # There are multiple yamls in the same file, we need this sometimes 24 | 25 | comments: 26 | ignore: "tests/assets/*" # our #cloud-config header doesn't take a space after the "#" 27 | -------------------------------------------------------------------------------- /internal/role/p2p/common.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 8 | ) 9 | 10 | const ( 11 | RoleWorker = "worker" 12 | RoleMaster = "master" 13 | RoleMasterHA = "master/ha" 14 | RoleMasterClusterInit = "master/clusterinit" 15 | RoleAuto = "auto" 16 | ) 17 | 18 | func guessInterface(pconfig *providerConfig.Config) string { 19 | if pconfig.KubeVIP.Interface != "" { 20 | return pconfig.KubeVIP.Interface 21 | } 22 | ifaces, err := net.Interfaces() 23 | if err != nil { 24 | fmt.Println("failed getting system interfaces") 25 | return "" 26 | } 27 | for _, i := range ifaces { 28 | if i.Name != "lo" { 29 | return i.Name 30 | } 31 | } 32 | return "" 33 | } 34 | -------------------------------------------------------------------------------- /internal/role/common.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "io/ioutil" // nolint 5 | "os" 6 | 7 | service "github.com/mudler/edgevpn/api/client/service" 8 | ) 9 | 10 | type Role func(*service.RoleConfig) error 11 | 12 | func SentinelExist() bool { 13 | if _, err := os.Stat("/usr/local/.kairos/deployed"); err == nil { 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | func CreateSentinel() error { 20 | return ioutil.WriteFile("/usr/local/.kairos/deployed", []byte{}, os.ModePerm) 21 | } 22 | 23 | func getRoles(client *service.Client, nodes []string) ([]string, map[string]string) { 24 | unassignedNodes := []string{} 25 | currentRoles := map[string]string{} 26 | for _, a := range nodes { 27 | role, _ := client.Get("role", a) 28 | currentRoles[a] = role 29 | if role == "" { 30 | unassignedNodes = append(unassignedNodes, a) 31 | } 32 | } 33 | return unassignedNodes, currentRoles 34 | } 35 | -------------------------------------------------------------------------------- /cli/kairosctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | iCli "github.com/kairos-io/provider-kairos/v2/internal/cli" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func checkErr(err error) { 12 | if err != nil { 13 | fmt.Println(err) 14 | os.Exit(1) 15 | } 16 | os.Exit(0) 17 | } 18 | 19 | func main() { 20 | checkErr(Start()) 21 | } 22 | 23 | func Start() error { 24 | toolName := "kairosctl" 25 | name := toolName 26 | app := &cli.App{ 27 | Name: name, 28 | Version: iCli.VERSION, 29 | Authors: []*cli.Author{ 30 | { 31 | Name: iCli.Author, 32 | }, 33 | }, 34 | Copyright: iCli.Author, 35 | Commands: []*cli.Command{ 36 | iCli.RegisterCMD(toolName), 37 | iCli.BridgeCMD(toolName), 38 | &iCli.GetKubeConfigCMD, 39 | &iCli.RoleCMD, 40 | &iCli.CreateConfigCMD, 41 | &iCli.GenerateTokenCMD, 42 | &iCli.ValidateSchemaCMD, 43 | }, 44 | } 45 | 46 | return app.Run(os.Args) 47 | } 48 | -------------------------------------------------------------------------------- /internal/provider/start.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/kairos-io/kairos-sdk/bus" 7 | 8 | "github.com/mudler/go-pluggable" 9 | ) 10 | 11 | func Start() error { 12 | factory := pluggable.NewPluginFactory() 13 | 14 | // Input: bus.EventInstallPayload 15 | // Expected output: map[string]string{} 16 | factory.Add(bus.EventInstall, Install) 17 | 18 | factory.Add(bus.EventBootstrap, Bootstrap) 19 | 20 | // Input: config 21 | // Expected output: string 22 | factory.Add(bus.EventChallenge, Challenge) 23 | 24 | factory.Add(bus.EventRecovery, Recovery) 25 | 26 | factory.Add(bus.EventRecoveryStop, RecoveryStop) 27 | 28 | factory.Add(bus.EventInteractiveInstall, InteractiveInstall) 29 | 30 | // Init build related events 31 | factory.Add(bus.InitProviderInstall, BuildEvent) 32 | factory.Add(bus.InitProviderInfo, InfoEvent) 33 | 34 | return factory.Run(pluggable.EventType(os.Args[1]), os.Stdin, os.Stdout) 35 | } 36 | -------------------------------------------------------------------------------- /internal/assets/static/kube_vip_rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: kube-vip 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | annotations: 11 | rbac.authorization.kubernetes.io/autoupdate: "true" 12 | name: system:kube-vip-role 13 | rules: 14 | - apiGroups: [""] 15 | resources: ["services", "services/status", "nodes", "endpoints"] 16 | verbs: ["list", "get", "watch", "update"] 17 | - apiGroups: ["coordination.k8s.io"] 18 | resources: ["leases"] 19 | verbs: ["list", "get", "watch", "update", "create"] 20 | --- 21 | kind: ClusterRoleBinding 22 | apiVersion: rbac.authorization.k8s.io/v1 23 | metadata: 24 | name: system:kube-vip-binding 25 | roleRef: 26 | apiGroup: rbac.authorization.k8s.io 27 | kind: ClusterRole 28 | name: system:kube-vip-role 29 | subjects: 30 | - kind: ServiceAccount 31 | name: kube-vip 32 | namespace: kube-system 33 | -------------------------------------------------------------------------------- /internal/provider/install.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/kairos-io/kairos-sdk/bus" 8 | 9 | "github.com/kairos-io/go-nodepair" 10 | "github.com/mudler/go-pluggable" 11 | ) 12 | 13 | func Install(e *pluggable.Event) pluggable.EventResponse { 14 | cfg := &bus.InstallPayload{} 15 | err := json.Unmarshal([]byte(e.Data), cfg) 16 | if err != nil { 17 | return ErrorEvent("Failed reading JSON input: %s", err.Error()) 18 | } 19 | 20 | r := map[string]string{} 21 | ctx := context.Background() 22 | if err := nodepair.Receive(ctx, &r, nodepair.WithToken(cfg.Token)); err != nil { 23 | return ErrorEvent("Failed reading JSON input: %s", err.Error()) 24 | } 25 | 26 | payload, err := json.Marshal(r) 27 | if err != nil { 28 | return ErrorEvent("Failed marshalling JSON input: %s", err.Error()) 29 | } 30 | 31 | return pluggable.EventResponse{ 32 | State: "", 33 | Data: string(payload), 34 | Error: "", 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "schedule": [ 7 | "after 11pm every weekday", 8 | "before 7am every weekday", 9 | "every weekend" 10 | ], 11 | "timezone": "Europe/Brussels", 12 | "packageRules": [ 13 | { 14 | "matchUpdateTypes": [ 15 | "patch" 16 | ], 17 | "automerge": true 18 | }, 19 | { 20 | "matchDepNames": [ 21 | "github.com/urfave/cli/v3" 22 | ], 23 | "enabled": false 24 | } 25 | ], 26 | "regexManagers": [ 27 | { 28 | "fileMatch": [ 29 | "^internal/role/p2p/kubevip\\.go$" 30 | ], 31 | "matchStrings": [ 32 | "DefaultKubeVIPVersion\\s*=\\s*\"(?v[0-9]+\\.[0-9]+\\.[0-9]+)\"" 33 | ], 34 | "datasourceTemplate": "github-releases", 35 | "depNameTemplate": "kube-vip/kube-vip", 36 | "versioningTemplate": "semver" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'test-goreleaser/**' 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - run: | 16 | git fetch --prune --unshallow 17 | - name: Set up Go 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version-file: 'go.mod' 21 | - name: (dry-run) GoReleaser 22 | if: startsWith(github.ref_name, 'test-goreleaser') 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | version: latest 26 | args: release --skip=validate,publish 27 | - name: Run GoReleaser 28 | if: startsWith(github.ref_name, 'test-goreleaser') != true 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /internal/provider/challenge.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/kairos-io/kairos-sdk/bus" 7 | 8 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 9 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 10 | 11 | "github.com/kairos-io/go-nodepair" 12 | "github.com/mudler/go-pluggable" 13 | ) 14 | 15 | func Challenge(e *pluggable.Event) pluggable.EventResponse { 16 | p := &bus.EventPayload{} 17 | err := json.Unmarshal([]byte(e.Data), p) 18 | if err != nil { 19 | return ErrorEvent("Failed reading JSON input: %s input '%s'", err.Error(), e.Data) 20 | } 21 | 22 | cfg := &providerConfig.Config{} 23 | err = config.FromString(p.Config, cfg) 24 | if err != nil { 25 | return ErrorEvent("Failed reading JSON input: %s input '%s'", err.Error(), p.Config) 26 | } 27 | 28 | tk := "" 29 | if cfg.P2P != nil && cfg.P2P.NetworkToken != "" { 30 | tk = cfg.P2P.NetworkToken 31 | } 32 | if tk == "" { 33 | tk = nodepair.GenerateToken() 34 | } 35 | return pluggable.EventResponse{ 36 | Data: tk, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/renovate_auto.yml: -------------------------------------------------------------------------------- 1 | name: Renovate auto-merge 2 | on: 3 | - pull_request_target 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | packages: read 9 | 10 | jobs: 11 | dependabot: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'renovate[bot]' }} 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v6 17 | 18 | - name: Approve a PR if not already approved 19 | run: | 20 | gh pr checkout "$PR_URL" 21 | if [ "$(gh pr status --json reviewDecision -q .currentBranch.reviewDecision)" != "APPROVED" ]; 22 | then 23 | gh pr review --approve "$PR_URL" 24 | else 25 | echo "PR already approved."; 26 | fi 27 | env: 28 | PR_URL: ${{github.event.pull_request.html_url}} 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | 31 | - name: Enable auto-merge for Renovate PRs 32 | run: gh pr merge --auto --squash "$PR_URL" 33 | env: 34 | PR_URL: ${{github.event.pull_request.html_url}} 35 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 36 | -------------------------------------------------------------------------------- /internal/cli/get_kubeconfig.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | 8 | edgeVPNClient "github.com/mudler/edgevpn/api/client" 9 | "github.com/mudler/edgevpn/api/client/service" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | var GetKubeConfigCMD = cli.Command{ 14 | Name: "get-kubeconfig", 15 | Usage: "Return a deployment kubeconfig", 16 | UsageText: "Retrieve a kairos network kubeconfig (only for automated deployments)", 17 | Description: ` 18 | Retrieve a network kubeconfig and prints out to screen. 19 | 20 | If a deployment was bootstrapped with a network token, you can use this command to retrieve the master node kubeconfig of a network id. 21 | 22 | For example: 23 | 24 | $ kairos get-kubeconfig --network-id kairos 25 | `, 26 | Flags: networkAPI, 27 | Action: func(c *cli.Context) error { 28 | cc := service.NewClient( 29 | c.String("network-id"), 30 | edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) 31 | str, _ := cc.Get("kubeconfig", "master") 32 | b, _ := base64.RawURLEncoding.DecodeString(str) 33 | masterIP, _ := cc.Get("master", "ip") 34 | fmt.Println(strings.ReplaceAll(string(b), "127.0.0.1", masterIP)) 35 | return nil 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: test-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.repository }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | unit-tests: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v6 18 | with: 19 | fetch-depth: 0 20 | - name: Install Go 21 | uses: actions/setup-go@v6 22 | with: 23 | go-version-file: go.mod 24 | cache-dependency-path: go.sum 25 | - name: Run tests 26 | run: | 27 | go run github.com/onsi/ginkgo/v2/ginkgo --fail-fast --covermode=atomic --coverprofile=coverage.out -p -r ./internal 28 | - name: Codecov 29 | uses: codecov/codecov-action@v5 30 | with: 31 | file: ./coverage.out 32 | - name: Generate version 33 | run: echo "VERSION=$(git describe --always --tags --dirty)" >> $GITHUB_ENV 34 | - name: GoReleaser 35 | uses: goreleaser/goreleaser-action@v6 36 | with: 37 | version: latest 38 | args: --snapshot --clean 39 | - uses: actions/upload-artifact@v6 40 | with: 41 | name: build.zip 42 | path: | 43 | dist/* 44 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_auto.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | - pull_request_target 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | packages: read 9 | 10 | jobs: 11 | dependabot: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'dependabot[bot]' }} 14 | steps: 15 | - name: Dependabot metadata 16 | id: metadata 17 | uses: dependabot/fetch-metadata@v2.4.0 18 | with: 19 | github-token: "${{ secrets.GITHUB_TOKEN }}" 20 | skip-commit-verification: true 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@v6 24 | 25 | - name: Approve a PR if not already approved 26 | run: | 27 | gh pr checkout "$PR_URL" 28 | if [ "$(gh pr status --json reviewDecision -q .currentBranch.reviewDecision)" != "APPROVED" ]; 29 | then 30 | gh pr review --approve "$PR_URL" 31 | else 32 | echo "PR already approved."; 33 | fi 34 | env: 35 | PR_URL: ${{github.event.pull_request.html_url}} 36 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 37 | 38 | - name: Enable auto-merge for Dependabot PRs 39 | run: gh pr merge --auto --squash "$PR_URL" 40 | env: 41 | PR_URL: ${{github.event.pull_request.html_url}} 42 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 43 | -------------------------------------------------------------------------------- /internal/provider/interactive-install.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/kairos-io/kairos-sdk/bus" 7 | "github.com/kairos-io/kairos-sdk/utils" 8 | 9 | "github.com/mudler/edgevpn/pkg/node" 10 | "github.com/mudler/go-pluggable" 11 | ) 12 | 13 | func InteractiveInstall(e *pluggable.Event) pluggable.EventResponse { //nolint:revive 14 | prompts := []bus.YAMLPrompt{ 15 | { 16 | YAMLSection: "p2p.network_token", 17 | Prompt: "Insert a network token, leave empty to autogenerate", 18 | AskFirst: true, 19 | AskPrompt: "Do you want to setup a full mesh-support?", 20 | IfEmpty: node.GenerateNewConnectionData().Base64(), 21 | }, 22 | } 23 | 24 | // Check which Kubernetes binary is installed 25 | if utils.K3sBin() != "" { 26 | prompts = append(prompts, bus.YAMLPrompt{ 27 | YAMLSection: "k3s.enabled", 28 | Bool: true, 29 | Prompt: "Do you want to enable k3s?", 30 | }) 31 | } else if utils.K0sBin() != "" { 32 | prompts = append(prompts, bus.YAMLPrompt{ 33 | YAMLSection: "k0s.enabled", 34 | Bool: true, 35 | Prompt: "Do you want to enable k0s?", 36 | }) 37 | } 38 | 39 | payload, err := json.Marshal(prompts) 40 | if err != nil { 41 | return ErrorEvent("Failed marshalling JSON input: %s", err.Error()) 42 | } 43 | 44 | return pluggable.EventResponse{ 45 | State: "", 46 | Data: string(payload), 47 | Error: "", 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/provider/recovery.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kairos-io/kairos-sdk/utils" 8 | 9 | nodepair "github.com/kairos-io/go-nodepair" 10 | "github.com/mudler/go-pluggable" 11 | process "github.com/mudler/go-processmanager" 12 | ) 13 | 14 | const recoveryAddr = "127.0.0.1:2222" 15 | const sshStateDir = "/tmp/.ssh_recovery" 16 | 17 | func Recovery(e *pluggable.Event) pluggable.EventResponse { //nolint:revive 18 | 19 | resp := &pluggable.EventResponse{} 20 | 21 | tk := nodepair.GenerateToken() 22 | 23 | serviceUUID := utils.RandStringRunes(10) 24 | generatedPassword := utils.RandStringRunes(7) 25 | resp.Data = utils.EncodeRecoveryToken(tk, serviceUUID, generatedPassword) 26 | resp.State = fmt.Sprintf( 27 | "starting ssh server on '%s', password: '%s' service: '%s' ", recoveryAddr, generatedPassword, serviceUUID) 28 | 29 | // start ssh server in a separate process 30 | 31 | sshServer := process.New( 32 | process.WithName(os.Args[0]), 33 | process.WithArgs("recovery-ssh-server"), 34 | process.WithEnvironment( 35 | fmt.Sprintf("TOKEN=%s", tk), 36 | fmt.Sprintf("SERVICE=%s", serviceUUID), 37 | fmt.Sprintf("LISTEN=%s", recoveryAddr), 38 | fmt.Sprintf("PASSWORD=%s", generatedPassword), 39 | ), 40 | process.WithStateDir(sshStateDir), 41 | ) 42 | 43 | err := sshServer.Run() 44 | if err != nil { 45 | resp.Error = err.Error() 46 | } 47 | return *resp 48 | } 49 | 50 | func RecoveryStop(e *pluggable.Event) pluggable.EventResponse { //nolint:revive 51 | resp := &pluggable.EventResponse{} 52 | 53 | sshServer := process.New( 54 | process.WithStateDir(sshStateDir), 55 | ) 56 | 57 | err := sshServer.Stop() 58 | if err != nil { 59 | resp.Error = err.Error() 60 | } else { 61 | os.RemoveAll(sshStateDir) 62 | } 63 | return *resp 64 | } 65 | -------------------------------------------------------------------------------- /internal/cli/config_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 9 | . "github.com/kairos-io/provider-kairos/v2/internal/cli/token" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | type TConfig struct { 16 | Kairos struct { 17 | NetworkToken string `yaml:"network_token"` 18 | P2P string `yaml:"p2p"` 19 | } `yaml:"p2p"` 20 | } 21 | 22 | var _ = Describe("Get config", func() { 23 | Context("directory", func() { 24 | 25 | It("replace token in config files", func() { 26 | 27 | var cc string = `#node-config 28 | p2p: 29 | network_token: "foo" 30 | 31 | bb: 32 | nothing: "foo" 33 | ` 34 | d, _ := ioutil.TempDir("", "xxxx") 35 | defer os.RemoveAll(d) 36 | 37 | err := ioutil.WriteFile(filepath.Join(d, "test"), []byte(cc), os.ModePerm) 38 | Expect(err).ToNot(HaveOccurred()) 39 | err = ioutil.WriteFile(filepath.Join(d, "b"), []byte(` 40 | fooz: "bar" 41 | `), os.ModePerm) 42 | Expect(err).ToNot(HaveOccurred()) 43 | 44 | err = ReplaceToken([]string{d, "/doesnotexist"}, "baz") 45 | Expect(err).ToNot(HaveOccurred()) 46 | 47 | content, err := ioutil.ReadFile(filepath.Join(d, "test")) 48 | Expect(err).ToNot(HaveOccurred()) 49 | 50 | res := map[interface{}]interface{}{} 51 | err = yaml.Unmarshal(content, &res) 52 | Expect(err).ToNot(HaveOccurred()) 53 | 54 | // Check by element as they can be unordered 55 | Expect(res["p2p"]).To(Equal(map[string]interface{}{"network_token": "baz"})) 56 | Expect(res["bb"]).To(Equal(map[string]interface{}{"nothing": "foo"})) 57 | 58 | hasHeader, _ := config.HasHeader(string(content), "#node-config") 59 | Expect(hasHeader).To(BeTrue(), string(content)) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /internal/cli/role.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | edgeVPNClient "github.com/mudler/edgevpn/api/client" 7 | "github.com/mudler/edgevpn/api/client/service" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | var RoleCMD = cli.Command{ 12 | Name: "role", 13 | Usage: "Set or list node roles", 14 | Subcommands: []*cli.Command{ 15 | { 16 | Flags: networkAPI, 17 | Name: "set", 18 | Usage: "Set a node role", 19 | UsageText: "kairos role set master", 20 | Description: ` 21 | Sets a node role propagating the setting to the network. 22 | 23 | A role must be set prior to the node joining a network. You can retrieve a node UUID by running "kairos uuid". 24 | 25 | Example: 26 | 27 | $ (node A) kairos uuid 28 | $ (node B) kairos role set master 29 | `, 30 | Action: func(c *cli.Context) error { 31 | cc := service.NewClient( 32 | c.String("network-id"), 33 | edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) 34 | return cc.Set("role", c.Args().Get(0), c.Args().Get(1)) 35 | }, 36 | }, 37 | { 38 | Flags: networkAPI, 39 | Name: "list", 40 | Description: "List node roles", 41 | Action: func(c *cli.Context) error { 42 | cc := service.NewClient( 43 | c.String("network-id"), 44 | edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) 45 | advertizing, _ := cc.AdvertizingNodes() 46 | fmt.Printf("%-47s %-30s %-15s\n", "Node", "Role", "IP") 47 | fmt.Printf("%s %s %s\n", 48 | "-----------------------------------------------", 49 | "------------------------------", 50 | "---------------") 51 | for _, a := range advertizing { 52 | role, _ := cc.Get("role", a) 53 | ip, _ := cc.Get("ip", a) 54 | fmt.Printf("%-47s %-30s %-15s\n", a, role, ip) 55 | } 56 | return nil 57 | }, 58 | }, 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /internal/provider/challenge_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/kairos-io/kairos-sdk/bus" 9 | 10 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 11 | 12 | . "github.com/kairos-io/provider-kairos/v2/internal/provider" 13 | "github.com/mudler/go-pluggable" 14 | . "github.com/onsi/ginkgo/v2" 15 | . "github.com/onsi/gomega" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | var _ = Describe("Challenge provider", func() { 20 | Context("network token", func() { 21 | e := &pluggable.Event{} 22 | 23 | BeforeEach(func() { 24 | e = &pluggable.Event{} 25 | }) 26 | 27 | It("returns it if provided", func() { 28 | f, err := ioutil.TempFile(os.TempDir(), "tests") 29 | Expect(err).ToNot(HaveOccurred()) 30 | defer os.RemoveAll(f.Name()) 31 | 32 | cfg := &providerConfig.Config{ 33 | P2P: &providerConfig.P2P{ 34 | NetworkToken: "foo", 35 | }, 36 | } 37 | d, err := yaml.Marshal(cfg) 38 | Expect(err).ToNot(HaveOccurred()) 39 | 40 | c := &bus.EventPayload{Config: string(d)} 41 | dat, err := json.Marshal(c) 42 | Expect(err).ToNot(HaveOccurred()) 43 | 44 | e.Data = string(dat) 45 | resp := Challenge(e) 46 | 47 | Expect(string(resp.Data)).Should(ContainSubstring("foo")) 48 | }) 49 | 50 | It("generates it if not provided", func() { 51 | f, err := ioutil.TempFile(os.TempDir(), "tests") 52 | Expect(err).ToNot(HaveOccurred()) 53 | defer os.RemoveAll(f.Name()) 54 | 55 | cfg := &providerConfig.Config{ 56 | P2P: &providerConfig.P2P{ 57 | NetworkToken: "", 58 | }, 59 | } 60 | d, err := yaml.Marshal(cfg) 61 | Expect(err).ToNot(HaveOccurred()) 62 | c := &bus.EventPayload{Config: string(d)} 63 | dat, err := json.Marshal(c) 64 | Expect(err).ToNot(HaveOccurred()) 65 | 66 | e.Data = string(dat) 67 | resp := Challenge(e) 68 | 69 | Expect(len(string(resp.Data))).Should(BeNumerically(">", 12)) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /internal/role/auto.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 5 | 6 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 7 | utils "github.com/mudler/edgevpn/pkg/utils" 8 | 9 | service "github.com/mudler/edgevpn/api/client/service" 10 | ) 11 | 12 | func contains(slice []string, elem string) bool { 13 | for _, s := range slice { 14 | if elem == s { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func Auto(cc *config.Config, pconfig *providerConfig.Config) Role { //nolint:revive 22 | return func(c *service.RoleConfig) error { 23 | advertizing, _ := c.Client.AdvertizingNodes() 24 | actives, _ := c.Client.ActiveNodes() 25 | 26 | minimumNodes := pconfig.P2P.MinimumNodes 27 | if minimumNodes == 0 { 28 | minimumNodes = 2 29 | } 30 | 31 | c.Logger.Info("Active nodes:", actives) 32 | c.Logger.Info("Advertizing nodes:", advertizing) 33 | 34 | if len(advertizing) < minimumNodes { 35 | c.Logger.Info("Not enough nodes") 36 | return nil 37 | } 38 | 39 | // first get available nodes 40 | nodes := advertizing 41 | shouldBeLeader := utils.Leader(advertizing) 42 | 43 | lead, _ := c.Client.Get("auto", "leader") 44 | 45 | // From now on, only the leader keeps processing 46 | // TODO: Make this more reliable with consensus 47 | if shouldBeLeader != c.UUID && lead != c.UUID { 48 | c.Logger.Infof("<%s> not a leader, leader is '%s', sleeping", c.UUID, shouldBeLeader) 49 | return nil 50 | } 51 | 52 | if shouldBeLeader == c.UUID && (lead == "" || !contains(nodes, lead)) { 53 | if err := c.Client.Set("auto", "leader", c.UUID); err != nil { 54 | c.Logger.Error(err) 55 | return err 56 | } 57 | c.Logger.Info("Announcing ourselves as leader, backing off") 58 | return nil 59 | } 60 | 61 | if lead != c.UUID { 62 | c.Logger.Info("Backing off, as we are not currently flagged as leader") 63 | return nil 64 | } 65 | 66 | return scheduleRoles(nodes, c, cc, pconfig) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/role/p2p/worker.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 8 | "github.com/kairos-io/kairos-sdk/utils" 9 | 10 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 11 | "github.com/kairos-io/provider-kairos/v2/internal/role" 12 | service "github.com/mudler/edgevpn/api/client/service" 13 | ) 14 | 15 | func Worker(cc *config.Config, pconfig *providerConfig.Config) role.Role { //nolint:revive 16 | return func(c *service.RoleConfig) error { 17 | c.Logger.Info("Starting Worker") 18 | 19 | if pconfig.P2P.Role != "" { 20 | // propagate role if we were forced by configuration 21 | // This unblocks eventual auto instances to try to assign roles 22 | if err := c.Client.Set("role", c.UUID, pconfig.P2P.Role); err != nil { 23 | return err 24 | } 25 | } 26 | 27 | if role.SentinelExist() { 28 | c.Logger.Info("Node already configured, backing off") 29 | return nil 30 | } 31 | 32 | masterIP, _ := c.Client.Get("master", "ip") 33 | if masterIP == "" { 34 | c.Logger.Info("MasterIP not there still..") 35 | return nil 36 | } 37 | 38 | node, err := NewK8sNode(pconfig) 39 | if err != nil { 40 | return fmt.Errorf("stopping Worker: %s", err.Error()) 41 | } 42 | 43 | ip := guessIP(pconfig) 44 | if ip != "" { 45 | if err := c.Client.Set("ip", c.UUID, ip); err != nil { 46 | c.Logger.Error(err) 47 | } 48 | } 49 | 50 | node.SetRole(RoleWorker) 51 | node.SetRoleConfig(c) 52 | node.SetIP(ip) 53 | 54 | nodeToken, _ := node.Token() 55 | if nodeToken == "" { 56 | c.Logger.Info("node token not there still..") 57 | return nil 58 | } 59 | 60 | utils.SH("kairos-agent run-stage provider-kairos.bootstrap.before.worker") //nolint:errcheck 61 | 62 | err = node.SetupWorker(masterIP, nodeToken) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | k8sBin := node.K8sBin() 68 | if k8sBin == "" { 69 | return fmt.Errorf("no %s binary found (?)", node.Distro()) 70 | } 71 | 72 | args, err := node.WorkerArgs() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | svc, err := node.Service() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | c.Logger.Info(fmt.Sprintf("Configuring %s worker", node.Distro())) 83 | if err := svc.OverrideCmd(fmt.Sprintf("%s %s %s", k8sBin, node.Role(), strings.Join(args, " "))); err != nil { 84 | return err 85 | } 86 | 87 | if err := svc.Start(); err != nil { 88 | return err 89 | } 90 | 91 | if err := svc.Enable(); err != nil { 92 | return err 93 | } 94 | 95 | utils.SH("kairos-agent run-stage provider-kairos.bootstrap.after.worker") //nolint:errcheck 96 | 97 | return role.CreateSentinel() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/cli/recovery.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/ipfs/go-log/v2" 11 | 12 | "github.com/creack/pty" 13 | "github.com/gliderlabs/ssh" 14 | "github.com/mudler/edgevpn/cmd" 15 | "github.com/mudler/edgevpn/pkg/logger" 16 | "github.com/mudler/edgevpn/pkg/node" 17 | "github.com/mudler/edgevpn/pkg/services" 18 | "github.com/pterm/pterm" 19 | cliV2 "github.com/urfave/cli/v2" 20 | ) 21 | 22 | func startRecoveryService(ctx context.Context, loglevel string, c *cliV2.Context) error { 23 | err := c.Set("log-level", loglevel) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | nc := cmd.ConfigFromContext(c) 29 | 30 | lvl, err := log.LevelFromString(loglevel) 31 | if err != nil { 32 | lvl = log.LevelError 33 | } 34 | llger := logger.New(lvl) 35 | 36 | o, _, err := nc.ToOpts(llger) 37 | if err != nil { 38 | llger.Fatal(err.Error()) 39 | } 40 | 41 | o = append(o, 42 | services.Alive( 43 | time.Duration(20)*time.Second, 44 | time.Duration(10)*time.Second, 45 | time.Duration(10)*time.Second)...) 46 | 47 | // opts, err := vpn.Register(vpnOpts...) 48 | // if err != nil { 49 | // return err 50 | // } 51 | o = append(o, services.RegisterService(llger, time.Duration(5*time.Second), c.String("service"), c.String("listen"))...) 52 | 53 | e, err := node.New(o...) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return e.Start(ctx) 59 | } 60 | 61 | func sshServer(listenAdddr, password string) { 62 | ssh.Handle(func(s ssh.Session) { 63 | cmd := exec.Command("/bin/bash") 64 | ptyReq, winCh, isPty := s.Pty() 65 | if isPty { 66 | cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) 67 | f, err := pty.Start(cmd) 68 | if err != nil { 69 | pterm.Warning.Println("Failed reserving tty") 70 | } 71 | go func() { 72 | for win := range winCh { 73 | setWinsize(f, win.Width, win.Height) 74 | } 75 | }() 76 | go func() { 77 | io.Copy(f, s) //nolint:errcheck 78 | }() 79 | io.Copy(s, f) //nolint:errcheck 80 | cmd.Wait() //nolint:errcheck 81 | } else { 82 | io.WriteString(s, "No PTY requested.\n") //nolint:errcheck 83 | s.Exit(1) //nolint:errcheck 84 | } 85 | }) 86 | 87 | pterm.Info.Println(ssh.ListenAndServe(listenAdddr, nil, ssh.PasswordAuth(func(_ ssh.Context, pass string) bool { 88 | return pass == password 89 | }), 90 | )) 91 | } 92 | 93 | func StartRecoveryService(c *cliV2.Context) error { 94 | ctx, cancel := context.WithCancel(context.Background()) 95 | defer cancel() 96 | if err := startRecoveryService(ctx, "fatal", c); err != nil { 97 | return err 98 | } 99 | 100 | sshServer(c.String("listen"), c.String("password")) 101 | 102 | return fmt.Errorf("should not return") 103 | } 104 | -------------------------------------------------------------------------------- /internal/role/p2p/k3s_test.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("K3sNode Args", func() { 10 | Context("embedded registry flag", func() { 11 | It("should include --embedded-registry flag for master when enabled", func() { 12 | enabled := true 13 | config := &providerConfig.Config{ 14 | K3s: providerConfig.K3s{ 15 | Enabled: &enabled, 16 | EmbeddedRegistry: true, 17 | Args: []string{"--existing-arg"}, 18 | }, 19 | } 20 | 21 | node := &K3sNode{ 22 | providerConfig: config, 23 | role: "master", 24 | } 25 | 26 | args := node.Args() 27 | 28 | Expect(args).To(ContainElement("--existing-arg")) 29 | Expect(args).To(ContainElement("--embedded-registry")) 30 | }) 31 | 32 | It("should not include --embedded-registry flag for worker when enabled", func() { 33 | enabled := true 34 | config := &providerConfig.Config{ 35 | K3s: providerConfig.K3s{ 36 | Enabled: &enabled, 37 | EmbeddedRegistry: true, 38 | Args: []string{"--existing-arg"}, 39 | }, 40 | K3sAgent: providerConfig.K3s{ 41 | Enabled: &enabled, 42 | Args: []string{"--worker-arg"}, 43 | }, 44 | } 45 | 46 | node := &K3sNode{ 47 | providerConfig: config, 48 | role: "worker", 49 | } 50 | 51 | args := node.Args() 52 | 53 | Expect(args).To(ContainElement("--worker-arg")) 54 | Expect(args).NotTo(ContainElement("--embedded-registry")) 55 | }) 56 | 57 | It("should not include --embedded-registry flag when disabled", func() { 58 | enabled := true 59 | config := &providerConfig.Config{ 60 | K3s: providerConfig.K3s{ 61 | Enabled: &enabled, 62 | EmbeddedRegistry: false, 63 | Args: []string{"--existing-arg"}, 64 | }, 65 | } 66 | 67 | node := &K3sNode{ 68 | providerConfig: config, 69 | role: "master", 70 | } 71 | 72 | args := node.Args() 73 | 74 | Expect(args).To(ContainElement("--existing-arg")) 75 | Expect(args).NotTo(ContainElement("--embedded-registry")) 76 | }) 77 | 78 | It("should preserve existing args when embedded registry is enabled", func() { 79 | enabled := true 80 | config := &providerConfig.Config{ 81 | K3s: providerConfig.K3s{ 82 | Enabled: &enabled, 83 | EmbeddedRegistry: true, 84 | Args: []string{"--arg1", "--arg2"}, 85 | }, 86 | } 87 | 88 | node := &K3sNode{ 89 | providerConfig: config, 90 | role: "master", 91 | } 92 | 93 | args := node.Args() 94 | 95 | Expect(args).To(ContainElement("--arg1")) 96 | Expect(args).To(ContainElement("--arg2")) 97 | Expect(args).To(ContainElement("--embedded-registry")) 98 | Expect(len(args)).To(Equal(3)) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /internal/services/edgevpn.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/kairos-io/kairos-sdk/machine" 5 | "github.com/kairos-io/kairos-sdk/machine/openrc" 6 | "github.com/kairos-io/kairos-sdk/machine/systemd" 7 | "github.com/kairos-io/kairos-sdk/utils" 8 | ) 9 | 10 | const edgevpnOpenRC string = `#!/sbin/openrc-run 11 | 12 | depend() { 13 | after net 14 | provide edgevpn 15 | } 16 | 17 | supervisor=supervise-daemon 18 | name="edgevpn" 19 | command="edgevpn" 20 | supervise_daemon_args="--stdout /var/log/edgevpn.log --stderr /var/log/edgevpn.log" 21 | pidfile="/run/edgevpn.pid" 22 | respawn_delay=5 23 | set -o allexport 24 | if [ -f /etc/environment ]; then source /etc/environment; fi 25 | if [ -f /etc/systemd/system.conf.d/edgevpn-kairos.env ]; then source /etc/systemd/system.conf.d/edgevpn-kairos.env; fi 26 | set +o allexport` 27 | 28 | const edgevpnAPIOpenRC string = `#!/sbin/openrc-run 29 | 30 | depend() { 31 | after net 32 | provide edgevpn 33 | } 34 | 35 | supervisor=supervise-daemon 36 | name="edgevpn" 37 | command="edgevpn" 38 | command_args="api --enable-healthchecks" 39 | supervise_daemon_args="--stdout /var/log/edgevpn.log --stderr /var/log/edgevpn.log" 40 | pidfile="/run/edgevpn.pid" 41 | respawn_delay=5 42 | set -o allexport 43 | if [ -f /etc/environment ]; then source /etc/environment; fi 44 | if [ -f /etc/systemd/system.conf.d/edgevpn-kairos.env ]; then source /etc/systemd/system.conf.d/edgevpn-kairos.env; fi 45 | set +o allexport` 46 | 47 | const edgevpnAPISystemd string = `[Unit] 48 | Description=P2P API Daemon 49 | After=network.target 50 | [Service] 51 | EnvironmentFile=/etc/systemd/system.conf.d/edgevpn-kairos.env 52 | LimitNOFILE=49152 53 | ExecStart=edgevpn api --enable-healthchecks 54 | Restart=always 55 | [Install] 56 | WantedBy=multi-user.target` 57 | 58 | const edgevpnSystemd string = `[Unit] 59 | Description=EdgeVPN Daemon 60 | After=network.target 61 | [Service] 62 | EnvironmentFile=/etc/systemd/system.conf.d/edgevpn-%i.env 63 | LimitNOFILE=49152 64 | ExecStart=edgevpn 65 | Restart=always 66 | [Install] 67 | WantedBy=multi-user.target` 68 | 69 | const EdgeVPNDefaultInstance string = "kairos" 70 | 71 | func EdgeVPN(instance, rootDir string) (machine.Service, error) { 72 | if utils.IsOpenRCBased() { 73 | return openrc.NewService( 74 | openrc.WithName("edgevpn"), 75 | openrc.WithUnitContent(edgevpnOpenRC), 76 | openrc.WithRoot(rootDir), 77 | ) 78 | } 79 | 80 | return systemd.NewService( 81 | systemd.WithName("edgevpn"), 82 | systemd.WithInstance(instance), 83 | systemd.WithUnitContent(edgevpnSystemd), 84 | systemd.WithRoot(rootDir), 85 | ) 86 | } 87 | 88 | func P2PAPI(rootDir string) (machine.Service, error) { 89 | if utils.IsOpenRCBased() { 90 | return openrc.NewService( 91 | openrc.WithName("edgevpn"), 92 | openrc.WithUnitContent(edgevpnAPIOpenRC), 93 | openrc.WithRoot(rootDir), 94 | ) 95 | } 96 | 97 | return systemd.NewService( 98 | systemd.WithName("edgevpn"), 99 | systemd.WithUnitContent(edgevpnAPISystemd), 100 | systemd.WithRoot(rootDir), 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Make sure to check the documentation at http://goreleaser.com 2 | version: 2 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | - CGO_LDFLAGS="-ldl" 7 | goos: 8 | - linux 9 | goarch: 10 | - amd64 11 | - arm64 12 | binary: "provider-kairos" 13 | id: provider-kairos 14 | main: ./main.go 15 | ldflags: 16 | - -w -s -X "github.com/kairos-io/provider-kairos/v2/internal/cli.VERSION={{.Tag}}" 17 | - env: 18 | - CGO_ENABLED=0 19 | - GOEXPERIMENT=boringcrypto 20 | - CGO_LDFLAGS="-ldl" 21 | goos: 22 | - linux 23 | goarch: 24 | - amd64 25 | binary: "provider-kairos" 26 | id: provider-kairos-fips-amd64 27 | main: ./main.go 28 | ldflags: 29 | - -w -s -X "github.com/kairos-io/provider-kairos/v2/internal/cli.VERSION={{.Tag}}" 30 | hooks: 31 | post: 32 | - bash -c 'set -e; go version {{.Path}} | grep boringcrypto || (echo "boringcrypto not found" && exit 1)' 33 | - env: 34 | - CGO_ENABLED=0 35 | - GOEXPERIMENT=boringcrypto 36 | - CC=aarch64-linux-gnu-gcc 37 | - CGO_LDFLAGS="-ldl" 38 | goos: 39 | - linux 40 | goarch: 41 | - arm64 42 | binary: "provider-kairos" 43 | id: provider-kairos-fips-arm64 44 | main: ./main.go 45 | ldflags: 46 | - -w -s -X "github.com/kairos-io/provider-kairos/v2/internal/cli.VERSION={{.Tag}}" 47 | hooks: 48 | post: 49 | - bash -c 'set -e; go version {{.Path}} | grep boringcrypto || (echo "boringcrypto not found" && exit 1)' 50 | - env: 51 | - CGO_ENABLED=0 52 | - CGO_LDFLAGS="-ldl" 53 | goos: 54 | - linux 55 | goarch: 56 | - amd64 57 | - arm64 58 | binary: "kairosctl" 59 | id: default-ctl 60 | main: ./cli/kairosctl 61 | ldflags: 62 | - -w -s -X "github.com/kairos-io/provider-kairos/v2/internal/cli.VERSION={{.Tag}}" 63 | source: 64 | enabled: true 65 | name_template: '{{ .ProjectName }}-{{ .Tag }}-source' 66 | archives: 67 | - id: default-archive-cli 68 | ids: 69 | - provider-kairos 70 | name_template: 'provider-kairos-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' 71 | - id: fips-archive 72 | ids: 73 | - provider-kairos-fips-arm64 74 | - provider-kairos-fips-amd64 75 | name_template: 'provider-kairos-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}-fips' 76 | - id: default-archive-ctl 77 | ids: 78 | - default-ctl 79 | name_template: 'kairosctl-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' 80 | checksum: 81 | name_template: '{{ .ProjectName }}-{{ .Tag }}-checksums.txt' 82 | snapshot: 83 | version_template: "{{ .Tag }}-next" 84 | changelog: 85 | sort: asc 86 | filters: 87 | exclude: 88 | - '^docs:' 89 | - '^test:' 90 | - '^Merge pull request' 91 | env: 92 | - GOSUMDB=sum.golang.org 93 | before: 94 | hooks: 95 | - go mod tidy 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | kairos-white-column 5bc2fe34
4 | Kairos standard provider 5 |
6 |

7 | 8 |

Kairos standard provider

9 |

10 | 11 | license 13 | 14 | 15 | docs 17 | 18 | go report card 19 |

20 | 21 | 22 | With Kairos you can build immutable, bootable Kubernetes and OS images for your edge devices as easily as writing a Dockerfile. Optional P2P mesh with distributed ledger automates node bootstrapping and coordination. Updating nodes is as easy as CI/CD: push a new image to your container registry and let secure, risk-free A/B atomic upgrades do the rest. 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 50 | 55 | 56 |
28 | 29 |

30 | 31 | Documentation 32 | 33 |

34 |
36 | 37 |

38 | 39 | Contribute 40 | 41 |

42 |
46 | 47 | 📚 [Getting started with Kairos](https://kairos.io/docs/getting-started)
:bulb: [Examples](https://kairos.io/docs/examples)
:movie_camera: [Video](https://kairos.io/docs/media/)
:open_hands:[Engage with the Community](https://kairos.io/community/) 48 | 49 |
51 | 52 | 🙌[ CONTRIBUTING.md ]( https://github.com/kairos-io/kairos/blob/master/CONTRIBUTING.md )
:raising_hand: [ GOVERNANCE ]( https://github.com/kairos-io/kairos/blob/master/GOVERNANCE.md )
:construction_worker:[Code of conduct](https://github.com/kairos-io/kairos/blob/master/CODE_OF_CONDUCT.md) 53 | 54 |
57 | 58 | ## Provider kairos 59 | 60 | This repository hosts the code for provider binary used in Kairos "standard" images which offer full-mesh support. 61 | full-mesh support currently is available only with k3s, and the provider follows strictly k3s releases. 62 | 63 | > [!NOTE] 64 | > The provider-kairos release pipelines have been merged with the kairos ones from version `2.4.0` onward. All image artifacts are released from the kairos repository, both core images and standard images (those with the provider). 65 | 66 | To use Kairos with mesh support, download the bootable medium form the [kairos releases](https://github.com/kairos-io/kairos/releases). 67 | 68 | Follow up the examples in our documentation on how to get started: 69 | - https://kairos.io/docs/examples/single-node/ 70 | - https://kairos.io/docs/examples/multi-node/ 71 | - https://kairos.io/docs/examples/multi-node-p2p-ha-kubevip/ 72 | 73 | ## Upgrades 74 | 75 | Upgrading can be done either via Kubernetes or manually with `kairos-agent upgrade --image `, or you can list available versions with `kairos-agent upgrade list-releases`. 76 | 77 | Container images available for upgrades are pushed to quay, you can check out the [image matrix in our documentation](https://kairos.io/docs/reference/image_matrix/). 78 | -------------------------------------------------------------------------------- /internal/role/p2p/k8s.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kairos-io/kairos-sdk/machine" 7 | "github.com/kairos-io/kairos-sdk/utils" 8 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 9 | service "github.com/mudler/edgevpn/api/client/service" 10 | ) 11 | 12 | // BinaryDetector interface for detecting k8s binaries. 13 | type BinaryDetector interface { 14 | K3sBin() string 15 | K0sBin() string 16 | } 17 | 18 | // DefaultBinaryDetector uses the utils package to detect binaries. 19 | type DefaultBinaryDetector struct{} 20 | 21 | func (d *DefaultBinaryDetector) K3sBin() string { 22 | return utils.K3sBin() 23 | } 24 | 25 | func (d *DefaultBinaryDetector) K0sBin() string { 26 | return utils.K0sBin() 27 | } 28 | 29 | type K8sNode interface { 30 | PropagateData() error 31 | IP() string 32 | ClusterInit() bool 33 | HA() bool 34 | ProviderConfig() *providerConfig.Config 35 | SetRoleConfig(c *service.RoleConfig) 36 | RoleConfig() *service.RoleConfig 37 | GenerateEnv() map[string]string 38 | Service() (machine.Service, error) 39 | EnvUnit() string 40 | GenArgs() ([]string, error) 41 | DeployKubeVIP() error 42 | Token() (string, error) 43 | K8sBin() string 44 | SetupWorker(masterIP, nodeToken string) error 45 | Role() string 46 | WorkerArgs() ([]string, error) 47 | ServiceName() string 48 | Env() map[string]string 49 | Args() []string 50 | EnvFile() string 51 | SetRole(role string) 52 | SetIP(ip string) 53 | GuessInterface() 54 | Distro() string 55 | } 56 | 57 | // NewK8sNodeWithDetector creates a new K8sNode with a custom binary detector. 58 | func NewK8sNodeWithDetector(c *providerConfig.Config, detector BinaryDetector) (K8sNode, error) { 59 | k3sBinAvailable := detector.K3sBin() != "" 60 | k0sBinAvailable := detector.K0sBin() != "" 61 | 62 | if !k3sBinAvailable && !k0sBinAvailable { 63 | return nil, errors.New("no k8s binary is available") 64 | } 65 | 66 | if c.K3s.IsEnabled() { 67 | return &K3sNode{providerConfig: c, role: RoleMaster}, nil 68 | } 69 | if c.K3sAgent.IsEnabled() { 70 | return &K3sNode{providerConfig: c, role: RoleWorker}, nil 71 | } 72 | if c.K0s.IsEnabled() { 73 | return &K0sNode{providerConfig: c, role: RoleMaster}, nil 74 | } 75 | if c.K0sWorker.IsEnabled() { 76 | return &K0sNode{providerConfig: c, role: RoleWorker}, nil 77 | } 78 | 79 | if !c.IsP2PConfigured() { 80 | return nil, errors.New("no k8s configuration found. To enable k8s, either: 1) explicitly enable k3s, k3s-agent, k0s, or k0s-worker or 2) configure p2p with a network token") 81 | } 82 | 83 | if c.P2P.Role != "" { 84 | if c.P2P.Role != RoleMaster && c.P2P.Role != RoleWorker { 85 | return nil, errors.New("invalid p2p.role specified, must be 'master' or 'worker'") 86 | } 87 | 88 | if k3sBinAvailable { 89 | return &K3sNode{providerConfig: c, role: c.P2P.Role}, nil 90 | } 91 | 92 | if k0sBinAvailable { 93 | return &K0sNode{providerConfig: c, role: c.P2P.Role}, nil 94 | } 95 | } 96 | 97 | if c.P2P.IsAutoEnabled() { 98 | if k3sBinAvailable { 99 | return &K3sNode{providerConfig: c}, nil // No role set, will be assigned automatically 100 | } 101 | if k0sBinAvailable { 102 | return &K0sNode{providerConfig: c}, nil // No role set, will be assigned automatically 103 | } 104 | } 105 | 106 | return nil, errors.New("no k8s configuration found but p2p is configured") 107 | } 108 | 109 | // NewK8sNode creates a new K8sNode using the default binary detector 110 | // This is the convenience function for production use. 111 | func NewK8sNode(c *providerConfig.Config) (K8sNode, error) { 112 | return NewK8sNodeWithDetector(c, &DefaultBinaryDetector{}) 113 | } 114 | -------------------------------------------------------------------------------- /internal/provider/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/kube-vip/kube-vip/pkg/kubevip" 4 | 5 | const ( 6 | K3sDistro = "k3s" 7 | K0sDistro = "k0s" 8 | ) 9 | 10 | type P2P struct { 11 | NetworkToken string `yaml:"network_token,omitempty"` 12 | NetworkID string `yaml:"network_id,omitempty"` 13 | Role string `yaml:"role,omitempty"` 14 | DNS bool `yaml:"dns,omitempty"` 15 | LogLevel string `yaml:"loglevel,omitempty"` 16 | VPN VPN `yaml:"vpn,omitempty"` 17 | 18 | MinimumNodes int `yaml:"minimum_nodes,omitempty"` 19 | DisableDHT bool `yaml:"disable_dht,omitempty"` 20 | Auto Auto `yaml:"auto,omitempty"` 21 | 22 | DynamicRoles bool `yaml:"dynamic_roles,omitempty"` 23 | } 24 | 25 | func (p P2P) IsAutoEnabled() bool { 26 | return p.Auto.Enable != nil && *p.Auto.Enable || p.NetworkToken != "" 27 | } 28 | 29 | type VPN struct { 30 | Create *bool `yaml:"create,omitempty"` 31 | Use *bool `yaml:"use,omitempty"` 32 | Env map[string]string `yaml:"env,omitempty"` 33 | } 34 | 35 | // If no setting is provided by the user, 36 | // we assume that we are going to create and use the VPN 37 | // for the network layer of our cluster. 38 | func (p P2P) UseVPNWithKubernetes() bool { 39 | return p.VPNNeedsCreation() && (p.VPN.Use == nil || *p.VPN.Use) 40 | } 41 | 42 | func (p P2P) VPNNeedsCreation() bool { 43 | return p.VPN.Create == nil || *p.VPN.Create 44 | } 45 | 46 | type Config struct { 47 | P2P *P2P `yaml:"p2p,omitempty"` 48 | K3sAgent K3s `yaml:"k3s-agent,omitempty"` 49 | K3s K3s `yaml:"k3s,omitempty"` 50 | KubeVIP KubeVIP `yaml:"kubevip,omitempty"` 51 | K0sWorker K0s `yaml:"k0s-worker,omitempty"` 52 | K0s K0s `yaml:"k0s,omitempty"` 53 | } 54 | 55 | func (c *Config) IsP2PConfigured() bool { 56 | return c.P2P != nil 57 | } 58 | 59 | func (c *Config) IsKubernetesConfigured() bool { 60 | return c.K3s.IsEnabled() || c.K3sAgent.IsEnabled() || c.K0s.IsEnabled() || c.K0sWorker.IsEnabled() 61 | } 62 | 63 | type KubeVIP struct { 64 | EIP string `yaml:"eip,omitempty"` 65 | ManifestURL string `yaml:"manifest_url,omitempty"` 66 | Interface string `yaml:"interface,omitempty"` 67 | Enable *bool `yaml:"enable,omitempty"` 68 | StaticPod bool `yaml:"static_pod,omitempty"` 69 | Version string `yaml:"version,omitempty"` 70 | kubevip.Config 71 | } 72 | 73 | func (k KubeVIP) IsEnabled() bool { 74 | return (k.Enable == nil && k.EIP != "") || (k.Enable != nil && *k.Enable) 75 | } 76 | 77 | type Auto struct { 78 | Enable *bool `yaml:"enable,omitempty"` 79 | HA HA `yaml:"ha,omitempty"` 80 | } 81 | 82 | func (a Auto) IsEnabled() bool { 83 | return a.Enable == nil || (a.Enable != nil && *a.Enable) 84 | } 85 | 86 | func (ha HA) IsEnabled() bool { 87 | return (ha.Enable != nil && *ha.Enable) || (ha.Enable == nil && ha.MasterNodes != nil) 88 | } 89 | 90 | type HA struct { 91 | Enable *bool `yaml:"enable,omitempty"` 92 | ExternalDB string `yaml:"external_db,omitempty"` 93 | MasterNodes *int `yaml:"master_nodes,omitempty"` 94 | } 95 | 96 | type K3s struct { 97 | Env map[string]string `yaml:"env,omitempty"` 98 | ReplaceEnv bool `yaml:"replace_env,omitempty"` 99 | ReplaceArgs bool `yaml:"replace_args,omitempty"` 100 | Args []string `yaml:"args,omitempty"` 101 | Enabled *bool `yaml:"enabled,omitempty"` 102 | EmbeddedRegistry bool `yaml:"embedded_registry,omitempty"` 103 | } 104 | 105 | func (k K3s) IsEnabled() bool { 106 | return k.Enabled != nil && *k.Enabled 107 | } 108 | 109 | type K0s struct { 110 | Env map[string]string `yaml:"env,omitempty"` 111 | ReplaceEnv bool `yaml:"replace_env,omitempty"` 112 | ReplaceArgs bool `yaml:"replace_args,omitempty"` 113 | Args []string `yaml:"args,omitempty"` 114 | Enabled *bool `yaml:"enabled,omitempty"` 115 | } 116 | 117 | func (k K0s) IsEnabled() bool { 118 | return k.Enabled != nil && *k.Enabled 119 | } 120 | -------------------------------------------------------------------------------- /internal/role/schedule.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 9 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 10 | "github.com/mudler/edgevpn/api/client/service" 11 | "github.com/samber/lo" 12 | ) 13 | 14 | // scheduleRoles assigns roles to nodes. Meant to be called only by leaders. 15 | func scheduleRoles(nodes []string, c *service.RoleConfig, cc *config.Config, pconfig *providerConfig.Config) error { //nolint:revive 16 | // From the golang docs: https://pkg.go.dev/math/rand#example-package-Rand 17 | // Create and seed the generator. 18 | // Typically a non-fixed seed should be used, such as time.Now().UnixNano(). 19 | // Using a fixed seed will produce the same output on every run. 20 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 21 | 22 | // Assign roles to nodes 23 | unassignedNodes, currentRoles := getRoles(c.Client, nodes) 24 | c.Logger.Infof("I'm the leader. My UUID is: %s.\n Current assigned roles: %+v", c.UUID, currentRoles) 25 | 26 | // Scan for dead nodes 27 | if pconfig.P2P.DynamicRoles { 28 | advertizing, _ := c.Client.AdvertizingNodes() 29 | for u, r := range currentRoles { 30 | if !lo.Contains(advertizing, u) { 31 | c.Logger.Infof("Role '%s' assigned to unreachable node '%s'. Unassigning.", u, r) 32 | if err := c.Client.Delete("role", u); err != nil { 33 | c.Logger.Warnf("Error announcing deletion %+v", err) 34 | } 35 | // Return here to propagate announces and wait until the map is pruned 36 | return nil 37 | } 38 | } 39 | } 40 | 41 | existsMaster := false 42 | 43 | masterRole := "master" 44 | workerRole := "worker" 45 | masterHA := "master/ha" 46 | 47 | if pconfig.P2P.Auto.HA.IsEnabled() { 48 | masterRole = "master/clusterinit" 49 | } 50 | mastersHA := 0 51 | 52 | for _, r := range currentRoles { 53 | switch r { 54 | case masterRole: 55 | existsMaster = true 56 | case masterHA: 57 | mastersHA++ 58 | } 59 | } 60 | 61 | c.Logger.Infof("Master already present: %t", existsMaster) 62 | c.Logger.Infof("Unassigned nodes: %+v", unassignedNodes) 63 | 64 | if !existsMaster && len(unassignedNodes) > 0 { 65 | var selected string 66 | toSelect := unassignedNodes 67 | 68 | // Avoid to schedule to ourselves if we have a static role 69 | if pconfig.P2P.Role != "" { 70 | toSelect = []string{} 71 | for _, u := range unassignedNodes { 72 | if u != c.UUID { 73 | toSelect = append(toSelect, u) 74 | } 75 | } 76 | } 77 | 78 | // select one node without roles to become master 79 | if len(toSelect) == 0 { 80 | // No nodes available for selection (all filtered out) 81 | c.Logger.Warnf("No nodes available for master selection after filtering") 82 | return nil 83 | } else if len(toSelect) == 1 { 84 | selected = toSelect[0] 85 | } else { 86 | selected = toSelect[r.Intn(len(toSelect))] 87 | } 88 | 89 | if err := c.Client.Set("role", selected, masterRole); err != nil { 90 | return err 91 | } 92 | c.Logger.Infof("-> Set %s to %s", masterRole, selected) 93 | currentRoles[selected] = masterRole 94 | // Return here, so next time we get called 95 | // makes sure master is set. 96 | return nil 97 | } 98 | 99 | if pconfig.P2P.Auto.HA.IsEnabled() && pconfig.P2P.Auto.HA.MasterNodes != nil && *pconfig.P2P.Auto.HA.MasterNodes != mastersHA { 100 | if len(unassignedNodes) > 0 { 101 | if err := c.Client.Set("role", unassignedNodes[0], masterHA); err != nil { 102 | c.Logger.Error(err) 103 | return err 104 | } 105 | // We want to keep scheduling in a second batch 106 | return nil 107 | } 108 | return fmt.Errorf("not enough nodes to create HA control plane") 109 | } 110 | 111 | // cycle all empty roles and assign worker roles 112 | for _, uuid := range unassignedNodes { 113 | if err := c.Client.Set("role", uuid, workerRole); err != nil { 114 | c.Logger.Error(err) 115 | return err 116 | } 117 | c.Logger.Infof("-> Set %s to %s", workerRole, uuid) 118 | } 119 | 120 | c.Logger.Info("Done scheduling") 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/provider/p2p.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" // nolint 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/kairos-io/provider-kairos/v2/internal/provider/assets" 11 | 12 | "github.com/kairos-io/kairos-sdk/machine" 13 | "github.com/kairos-io/kairos-sdk/machine/systemd" 14 | "github.com/kairos-io/kairos-sdk/utils" 15 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 16 | "github.com/kairos-io/provider-kairos/v2/internal/services" 17 | ) 18 | 19 | func SaveCloudConfig(name string, c []byte) error { 20 | return ioutil.WriteFile(filepath.Join("oem", fmt.Sprintf("%s.yaml", name)), c, 0700) 21 | } 22 | 23 | func SetupAPI(apiAddress, rootDir string, start bool, c *providerConfig.Config) error { 24 | if c.P2P == nil || c.P2P.NetworkToken == "" { 25 | return fmt.Errorf("no network token defined") 26 | } 27 | 28 | svc, err := services.P2PAPI(rootDir) 29 | if err != nil { 30 | return fmt.Errorf("could not create svc: %w", err) 31 | } 32 | 33 | apiAddress = strings.ReplaceAll(apiAddress, "https://", "") 34 | apiAddress = strings.ReplaceAll(apiAddress, "http://", "") 35 | 36 | vpnOpts := map[string]string{ 37 | "EDGEVPNTOKEN": c.P2P.NetworkToken, 38 | "APILISTEN": apiAddress, 39 | } 40 | // Override opts with user-supplied 41 | for k, v := range c.P2P.VPN.Env { 42 | vpnOpts[k] = v 43 | } 44 | 45 | if c.P2P.DisableDHT { 46 | vpnOpts["EDGEVPNDHT"] = "false" 47 | } 48 | 49 | os.MkdirAll("/etc/systemd/system.conf.d/", 0600) //nolint:errcheck 50 | // Setup edgevpn instance 51 | err = utils.WriteEnv(filepath.Join(rootDir, "/etc/systemd/system.conf.d/edgevpn-kairos.env"), vpnOpts) 52 | if err != nil { 53 | return fmt.Errorf("could not create write env file: %w", err) 54 | } 55 | 56 | err = svc.WriteUnit() 57 | if err != nil { 58 | return fmt.Errorf("could not create write unit file: %w", err) 59 | } 60 | 61 | if start { 62 | err = svc.Start() 63 | if err != nil { 64 | return fmt.Errorf("could not start svc: %w", err) 65 | } 66 | 67 | return svc.Enable() 68 | } 69 | return nil 70 | } 71 | 72 | func SetupVPN(instance, apiAddress, rootDir string, start bool, c *providerConfig.Config) error { 73 | token := "" 74 | if c.P2P != nil && c.P2P.NetworkToken != "" { 75 | token = c.P2P.NetworkToken 76 | } 77 | 78 | svc, err := services.EdgeVPN(instance, rootDir) 79 | if err != nil { 80 | return fmt.Errorf("could not create svc: %w", err) 81 | } 82 | 83 | apiAddress = strings.ReplaceAll(apiAddress, "https://", "") 84 | apiAddress = strings.ReplaceAll(apiAddress, "http://", "") 85 | 86 | vpnOpts := map[string]string{ 87 | "API": "true", 88 | "APILISTEN": apiAddress, 89 | "DHCP": "true", 90 | "DHCPLEASEDIR": "/usr/local/.kairos/lease", 91 | } 92 | if token != "" { 93 | vpnOpts["EDGEVPNTOKEN"] = c.P2P.NetworkToken 94 | } 95 | 96 | if c.P2P.DisableDHT { 97 | vpnOpts["EDGEVPNDHT"] = "false" 98 | } 99 | 100 | // Override opts with user-supplied 101 | for k, v := range c.P2P.VPN.Env { 102 | vpnOpts[k] = v 103 | } 104 | 105 | if c.P2P.DNS { 106 | vpnOpts["DNSADDRESS"] = "127.0.0.1:53" 107 | vpnOpts["DNSFORWARD"] = "true" 108 | 109 | _ = machine.ExecuteInlineCloudConfig(assets.LocalDNS, "initramfs") 110 | if !utils.IsOpenRCBased() { 111 | svc, err := systemd.NewService( 112 | systemd.WithName("systemd-resolved"), 113 | ) 114 | if err == nil { 115 | _ = svc.Restart() 116 | } 117 | } 118 | 119 | if err := SaveCloudConfig("vpn_dns", []byte(assets.LocalDNS)); err != nil { 120 | return fmt.Errorf("could not create dns config: %w", err) 121 | } 122 | } 123 | 124 | os.MkdirAll("/etc/systemd/system.conf.d/", 0600) //nolint:errcheck 125 | // Setup edgevpn instance 126 | err = utils.WriteEnv(filepath.Join(rootDir, "/etc/systemd/system.conf.d/edgevpn-kairos.env"), vpnOpts) 127 | if err != nil { 128 | return fmt.Errorf("could not create write env file: %w", err) 129 | } 130 | 131 | err = svc.WriteUnit() 132 | if err != nil { 133 | return fmt.Errorf("could not create write unit file: %w", err) 134 | } 135 | 136 | if start { 137 | err = svc.Start() 138 | if err != nil { 139 | return fmt.Errorf("could not start svc: %w", err) 140 | } 141 | 142 | return svc.Enable() 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /internal/cli/register.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/urfave/cli/v2" 9 | 10 | nodepair "github.com/kairos-io/go-nodepair" 11 | qr "github.com/kairos-io/go-nodepair/qrcode" 12 | ) 13 | 14 | // RegisterCMD is only used temporarily to avoid duplication while the kairosctl sub-command is deprecated. 15 | func RegisterCMD(toolName string) *cli.Command { 16 | subCommandName := "register" 17 | fullName := fmt.Sprintf("%s %s", toolName, subCommandName) 18 | usage := "Registers and bootstraps a node" 19 | description := fmt.Sprintf(` 20 | Bootstraps a node which is started in pairing mode. It can send over a configuration file used to install the kairos node. 21 | 22 | For example: 23 | $ %s --config config.yaml --device /dev/sda ~/Downloads/screenshot.png 24 | 25 | will decode the QR code from ~/Downloads/screenshot.png and bootstrap the node remotely. 26 | 27 | If the image is omitted, a screenshot will be taken and used to decode the QR code. 28 | 29 | See also https://kairos.io/docs/getting-started/ for documentation. 30 | `, fullName) 31 | if toolName != "kairosctl" { 32 | usage += " (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead)" 33 | description = "\t\tWARNING: This command will be deprecated in the next release. Please use the new kairosctl binary to register your nodes.\n" + description 34 | } 35 | 36 | return &cli.Command{ 37 | Name: subCommandName, 38 | UsageText: fmt.Sprintf("%s --reboot --device /dev/sda /image/snapshot.png", fullName), 39 | Usage: usage, 40 | Description: description, 41 | ArgsUsage: "Register optionally accepts an image. If nothing is passed will take a screenshot of the screen and try to decode the QR code", 42 | Flags: []cli.Flag{ 43 | &cli.StringFlag{ 44 | Name: "config", 45 | Usage: "Kairos YAML configuration file", 46 | Required: true, 47 | }, 48 | &cli.StringFlag{ 49 | Name: "device", 50 | Usage: "Device used for the installation target", 51 | }, 52 | &cli.BoolFlag{ 53 | Name: "reboot", 54 | Usage: "Reboot node after installation", 55 | }, 56 | &cli.BoolFlag{ 57 | Name: "poweroff", 58 | Usage: "Shutdown node after installation", 59 | }, 60 | &cli.StringFlag{ 61 | Name: "log-level", 62 | Usage: "Set log level", 63 | }, 64 | }, 65 | Action: func(c *cli.Context) error { 66 | var ref string 67 | if c.Args().Len() == 1 { 68 | ref = c.Args().First() 69 | } 70 | 71 | return register(c.String("log-level"), ref, c.String("config"), c.String("device"), c.Bool("reboot"), c.Bool("poweroff")) 72 | }, 73 | } 74 | } 75 | 76 | // isDirectory determines if a file represented 77 | // by `path` is a directory or not. 78 | func isDirectory(path string) (bool, error) { 79 | fileInfo, err := os.Stat(path) 80 | if err != nil { 81 | return false, err 82 | } 83 | 84 | return fileInfo.IsDir(), err 85 | } 86 | 87 | func isReadable(fileName string) bool { 88 | file, err := os.Open(fileName) 89 | if err != nil { 90 | if os.IsPermission(err) { 91 | return false 92 | } 93 | } 94 | file.Close() 95 | return true 96 | } 97 | 98 | func register(loglevel, arg, configFile, device string, reboot, poweroff bool) error { 99 | b, _ := os.ReadFile(configFile) 100 | ctx, cancel := context.WithCancel(context.Background()) 101 | defer cancel() 102 | 103 | if arg != "" { 104 | isDir, err := isDirectory(arg) 105 | if err == nil && isDir { 106 | return fmt.Errorf("cannot register with a directory, please pass a file") 107 | } else if err != nil { 108 | return err 109 | } 110 | if !isReadable(arg) { 111 | return fmt.Errorf("cannot register with a file that is not readable") 112 | } 113 | } 114 | // dmesg -D to suppress tty ev 115 | fmt.Println("Sending registration payload, please wait") 116 | 117 | config := map[string]string{ 118 | "device": device, 119 | "cc": string(b), 120 | } 121 | 122 | if reboot { 123 | config["reboot"] = "" 124 | } 125 | 126 | if poweroff { 127 | config["poweroff"] = "" 128 | } 129 | 130 | err := nodepair.Send( 131 | ctx, 132 | config, 133 | nodepair.WithReader(qr.Reader), 134 | nodepair.WithToken(arg), 135 | nodepair.WithLogLevel(loglevel), 136 | ) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | fmt.Println("Payload sent, installation will start on the machine briefly") 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /internal/cli/token/rotate.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" // nolint 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 11 | "github.com/kairos-io/kairos-sdk/collector" 12 | "github.com/kairos-io/kairos-sdk/unstructured" 13 | "github.com/kairos-io/provider-kairos/v2/internal/provider" 14 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 15 | "github.com/kairos-io/provider-kairos/v2/internal/services" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | func RotateToken(configDir []string, newToken, apiAddress, rootDir string, restart bool) error { 20 | if err := ReplaceToken(configDir, newToken); err != nil { 21 | return err 22 | } 23 | 24 | o := &collector.Options{} 25 | if err := o.Apply(collector.Directories(configDir...)); err != nil { 26 | return err 27 | } 28 | c, err := collector.Scan(o, config.FilterKeys) 29 | 30 | if err != nil { 31 | return err 32 | } 33 | 34 | providerCfg := &providerConfig.Config{} 35 | a, _ := c.String() 36 | err = yaml.Unmarshal([]byte(a), providerCfg) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | err = provider.SetupVPN(services.EdgeVPNDefaultInstance, apiAddress, rootDir, false, providerCfg) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if restart { 47 | svc, err := services.EdgeVPN(services.EdgeVPNDefaultInstance, rootDir) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return svc.Restart() 53 | } 54 | return nil 55 | } 56 | 57 | func ReplaceToken(dir []string, token string) (err error) { 58 | locations, err := FindYAMLWithKey("p2p.network_token", collector.Directories(dir...)) 59 | if err != nil { 60 | return err 61 | } 62 | for _, f := range locations { 63 | dat, err := os.ReadFile(f) 64 | if err != nil { 65 | fmt.Printf("warning: could not read %s '%s'\n", f, err.Error()) 66 | } 67 | 68 | header := config.DefaultHeader 69 | if hasHeader, head := config.HasHeader(string(dat), ""); hasHeader { 70 | header = head 71 | } 72 | content := map[interface{}]interface{}{} 73 | 74 | if err := yaml.Unmarshal(dat, &content); err != nil { 75 | return err 76 | } 77 | 78 | section, exists := content["p2p"] 79 | if !exists { 80 | return errors.New("no p2p section in config file") 81 | } 82 | 83 | dd, err := yaml.Marshal(section) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | piece := map[string]interface{}{} 89 | 90 | if err := yaml.Unmarshal(dd, &piece); err != nil { 91 | return err 92 | } 93 | 94 | piece["network_token"] = token 95 | content["p2p"] = piece 96 | 97 | d, err := yaml.Marshal(content) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | fi, err := os.Stat(f) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if err := ioutil.WriteFile(f, []byte(config.AddHeader(header, string(d))), fi.Mode().Perm()); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | // FindYAMLWithKey will find and return files that contain a given key in them. 116 | func FindYAMLWithKey(s string, opts ...collector.Option) ([]string, error) { 117 | o := &collector.Options{} 118 | 119 | var result []string 120 | if err := o.Apply(opts...); err != nil { 121 | return result, err 122 | } 123 | 124 | files := allFiles(o.ScanDir) 125 | 126 | for _, f := range files { 127 | dat, err := os.ReadFile(f) 128 | if err != nil { 129 | fmt.Printf("warning: skipping file '%s' - %s\n", f, err.Error()) 130 | } 131 | 132 | found, err := unstructured.YAMLHasKey(s, dat) 133 | if err != nil { 134 | fmt.Printf("warning: skipping file '%s' - %s\n", f, err.Error()) 135 | } 136 | 137 | if found { 138 | result = append(result, f) 139 | } 140 | 141 | } 142 | 143 | return result, nil 144 | } 145 | 146 | func allFiles(dir []string) []string { 147 | var files []string 148 | for _, d := range dir { 149 | if f, err := listFiles(d); err == nil { 150 | files = append(files, f...) 151 | } 152 | } 153 | return files 154 | } 155 | 156 | func listFiles(dir string) ([]string, error) { 157 | var content []string 158 | 159 | err := filepath.Walk(dir, 160 | func(path string, info os.FileInfo, err error) error { 161 | if err != nil { 162 | return nil 163 | } 164 | if !info.IsDir() { 165 | content = append(content, path) 166 | } 167 | 168 | return nil 169 | }) 170 | 171 | return content, err 172 | } 173 | -------------------------------------------------------------------------------- /internal/services/k0s.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/kairos-io/kairos-sdk/machine/openrc" 5 | "github.com/kairos-io/kairos-sdk/machine/systemd" 6 | "github.com/kairos-io/kairos-sdk/types" 7 | "github.com/kairos-io/kairos-sdk/utils" 8 | ) 9 | 10 | // K0s Services start here 11 | 12 | const K0sControllerSystemd = `[Unit] 13 | Description=k0s - Zero Friction Kubernetes 14 | Documentation=https://docs.k0sproject.io 15 | ConditionFileIsExecutable=/usr/bin/k0s 16 | 17 | After=network-online.target 18 | Wants=network-online.target 19 | 20 | [Service] 21 | StartLimitInterval=5 22 | StartLimitBurst=10 23 | ExecStart=/usr/bin/k0s controller 24 | 25 | RestartSec=10 26 | Delegate=yes 27 | KillMode=process 28 | LimitCORE=infinity 29 | TasksMax=infinity 30 | TimeoutStartSec=0 31 | LimitNOFILE=999999 32 | Restart=always 33 | 34 | [Install] 35 | WantedBy=multi-user.target` 36 | 37 | const K0sWorkerSystemd = `[Unit] 38 | Description=k0s - Zero Friction Kubernetes 39 | Documentation=https://docs.k0sproject.io 40 | ConditionFileIsExecutable=/usr/bin/k0s 41 | 42 | After=network-online.target 43 | Wants=network-online.target 44 | 45 | [Service] 46 | StartLimitInterval=5 47 | StartLimitBurst=10 48 | ExecStart=/usr/bin/k0s worker 49 | 50 | RestartSec=10 51 | Delegate=yes 52 | KillMode=process 53 | LimitCORE=infinity 54 | TasksMax=infinity 55 | TimeoutStartSec=0 56 | LimitNOFILE=999999 57 | Restart=always 58 | 59 | [Install] 60 | WantedBy=multi-user.target` 61 | 62 | const K0sControllerOpenrc = `#!/sbin/openrc-run 63 | supervisor=supervise-daemon 64 | description="k0s - Zero Friction Kubernetes" 65 | command=/usr/bin/k0s 66 | command_args="'controller' " 67 | name=$(basename $(readlink -f $command)) 68 | supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err" 69 | 70 | : "${rc_ulimit=-n 1048576 -u unlimited}" 71 | depend() { 72 | need cgroups 73 | need net 74 | use dns 75 | after firewall 76 | }` 77 | 78 | const K0sWorkerOpenrc = `#!/sbin/openrc-run 79 | supervisor=supervise-daemon 80 | description="k0s - Zero Friction Kubernetes" 81 | command=/usr/bin/k0s 82 | command_args="'worker' " 83 | name=$(basename $(readlink -f $command)) 84 | supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err" 85 | 86 | : "${rc_ulimit=-n 1048576 -u unlimited}" 87 | depend() { 88 | need cgroups 89 | need net 90 | use dns 91 | after firewall 92 | }` 93 | 94 | // K0s Services end here 95 | 96 | // K0sServices creates the k0s controller and worker services for openrc or systemd based systems. 97 | func K0sServices(logger types.KairosLogger) error { 98 | if utils.IsOpenRCBased() { 99 | controller, err := openrc.NewService( 100 | openrc.WithName("k0scontroller"), 101 | openrc.WithUnitContent(K0sControllerOpenrc), 102 | ) 103 | if err != nil { 104 | logger.Logger.Error().Err(err).Str("init", "openrc").Msg("Failed to create k0s controller service") 105 | return err 106 | } 107 | if err = controller.WriteUnit(); err != nil { 108 | logger.Logger.Error().Err(err).Str("init", "openrc").Msg("Failed to write k0s controller service unit") 109 | return err 110 | } 111 | worker, err := openrc.NewService( 112 | openrc.WithName("k0sworker"), 113 | openrc.WithUnitContent(K0sWorkerOpenrc), 114 | ) 115 | 116 | if err != nil { 117 | logger.Logger.Error().Err(err).Str("init", "openrc").Msg("Failed to create k0s worker service") 118 | return err 119 | } 120 | if err = worker.WriteUnit(); err != nil { 121 | logger.Logger.Error().Err(err).Str("init", "openrc").Msg("Failed to write k0s worker service unit") 122 | return err 123 | } 124 | 125 | } else { 126 | controller, err := systemd.NewService( 127 | systemd.WithName("k0scontroller"), 128 | systemd.WithUnitContent(K0sControllerSystemd), 129 | systemd.WithReload(false), // we are not in a running system, so we cant reload 130 | ) 131 | if err != nil { 132 | logger.Logger.Error().Err(err).Str("init", "systemd").Msg("Failed to create k0s controller service") 133 | return err 134 | } 135 | if err = controller.WriteUnit(); err != nil { 136 | logger.Logger.Error().Err(err).Str("init", "systemd").Msg("Failed to write k0s controller service unit") 137 | return err 138 | } 139 | worker, err := systemd.NewService( 140 | systemd.WithName("k0sworker"), 141 | systemd.WithUnitContent(K0sWorkerSystemd), 142 | systemd.WithReload(false), // we are not in a running system, so we cant reload 143 | ) 144 | if err != nil { 145 | logger.Logger.Error().Err(err).Str("init", "systemd").Msg("Failed to create k0s worker service") 146 | return err 147 | } 148 | if err = worker.WriteUnit(); err != nil { 149 | logger.Logger.Error().Err(err).Str("init", "systemd").Msg("Failed to write k0s worker service unit") 150 | return err 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /internal/cli/start.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strconv" 8 | 9 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 10 | 11 | "github.com/kairos-io/kairos-sdk/schema" 12 | "github.com/mudler/edgevpn/pkg/node" 13 | "github.com/urfave/cli/v2" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | // do not edit version here, it is set by LDFLAGS 18 | // -X 'github.com/kairos-io/provider-kairos/v2/internal/cli.VERSION=$VERSION' 19 | // see Earthlfile. 20 | var VERSION = "0.0.0" 21 | var Author = "Ettore Di Giacinto" 22 | 23 | var networkAPI = []cli.Flag{ 24 | &cli.StringFlag{ 25 | Name: "api", 26 | Usage: "API Address", 27 | Value: "http://localhost:8080", 28 | }, 29 | &cli.StringFlag{ 30 | Name: "network-id", 31 | Value: "kairos", 32 | Usage: "Kubernetes Network Deployment ID", 33 | }, 34 | } 35 | 36 | const recoveryAddr = "127.0.0.1:2222" 37 | 38 | var CreateConfigCMD = cli.Command{ 39 | Name: "create-config", 40 | Aliases: []string{"c"}, 41 | UsageText: "Create a config with a generated network token", 42 | 43 | Usage: "Creates a pristine config file", 44 | Description: ` 45 | Prints a vanilla YAML configuration on screen which can be used to bootstrap a kairos network. 46 | `, 47 | ArgsUsage: "Optionally takes a token rotation interval (seconds)", 48 | 49 | Action: func(c *cli.Context) error { 50 | l := int(^uint(0) >> 1) 51 | if c.Args().Present() { 52 | if i, err := strconv.Atoi(c.Args().Get(0)); err == nil { 53 | l = i 54 | } 55 | } 56 | cc := &providerConfig.Config{P2P: &providerConfig.P2P{NetworkToken: node.GenerateNewConnectionData(l).Base64()}} 57 | y, _ := yaml.Marshal(cc) 58 | fmt.Printf("#cloud-config\n\n%s", string(y)) 59 | return nil 60 | }, 61 | } 62 | 63 | var GenerateTokenCMD = cli.Command{ 64 | Name: "generate-token", 65 | Aliases: []string{"g"}, 66 | UsageText: "Generate a network token", 67 | Usage: "Creates a new token", 68 | Description: ` 69 | Generates a new token which can be used to bootstrap a kairos network. 70 | `, 71 | ArgsUsage: "Optionally takes a token rotation interval (seconds)", 72 | 73 | Action: func(c *cli.Context) error { 74 | l := int(^uint(0) >> 1) 75 | if c.Args().Present() { 76 | if i, err := strconv.Atoi(c.Args().Get(0)); err == nil { 77 | l = i 78 | } 79 | } 80 | fmt.Println(node.GenerateNewConnectionData(l).Base64()) 81 | return nil 82 | }, 83 | } 84 | 85 | var ValidateSchemaCMD = cli.Command{ 86 | Name: "validate", 87 | Action: func(c *cli.Context) error { 88 | config := c.Args().First() 89 | return schema.Validate(config) 90 | }, 91 | Usage: "Validates a cloud config file", 92 | Description: ` 93 | The validate command expects a configuration file as its only argument. Local files and URLs are accepted. 94 | `, 95 | } 96 | 97 | var VersionCMD = cli.Command{ 98 | Name: "version", 99 | Action: func(_ *cli.Context) error { 100 | printVersion() 101 | return nil 102 | }, 103 | Description: "Prints version information of this binary", 104 | } 105 | 106 | func printVersion() { 107 | fmt.Printf("version: %s, compiled with: %s\n", VERSION, runtime.Version()) 108 | } 109 | 110 | func Start() error { 111 | toolName := "kairos" 112 | 113 | cli.VersionPrinter = func(_ *cli.Context) { 114 | printVersion() 115 | } 116 | 117 | app := &cli.App{ 118 | Name: toolName, 119 | Version: VERSION, 120 | Authors: []*cli.Author{ 121 | { 122 | Name: Author, 123 | }, 124 | }, 125 | Usage: "kairos CLI to bootstrap, upgrade, connect and manage a kairos network", 126 | Description: ` 127 | The kairos CLI can be used to manage a kairos box and perform all day-two tasks, like: 128 | - register a node (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead) 129 | - connect to a node in recovery mode 130 | - to establish a VPN connection 131 | - set, list roles 132 | - interact with the network API 133 | 134 | and much more. 135 | 136 | For all the example cases, see: https://kairos.io/docs/ 137 | `, 138 | UsageText: ``, 139 | Copyright: Author, 140 | Commands: []*cli.Command{ 141 | { 142 | Name: "recovery-ssh-server", 143 | UsageText: "recovery-ssh-server", 144 | Usage: "Starts SSH recovery service", 145 | Description: ` 146 | Spawn up a simple standalone ssh server over p2p 147 | `, 148 | ArgsUsage: "Spawn up a simple standalone ssh server over p2p", 149 | Flags: []cli.Flag{ 150 | &cli.StringFlag{ 151 | Name: "token", 152 | EnvVars: []string{"TOKEN"}, 153 | }, 154 | &cli.StringFlag{ 155 | Name: "service", 156 | EnvVars: []string{"SERVICE"}, 157 | }, 158 | &cli.StringFlag{ 159 | Name: "password", 160 | EnvVars: []string{"PASSWORD"}, 161 | }, 162 | &cli.StringFlag{ 163 | Name: "listen", 164 | EnvVars: []string{"LISTEN"}, 165 | Value: recoveryAddr, 166 | }, 167 | }, 168 | Action: func(c *cli.Context) error { 169 | return StartRecoveryService(c) 170 | }, 171 | }, 172 | RegisterCMD(toolName), 173 | BridgeCMD(toolName), 174 | &GetKubeConfigCMD, 175 | &RoleCMD, 176 | &CreateConfigCMD, 177 | &GenerateTokenCMD, 178 | &ValidateSchemaCMD, 179 | &VersionCMD, 180 | }, 181 | } 182 | 183 | return app.Run(os.Args) 184 | } 185 | -------------------------------------------------------------------------------- /internal/role/p2p/k8s_test.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | // MockBinaryDetector for testing 10 | type MockBinaryDetector struct { 11 | k3sBin string 12 | k0sBin string 13 | } 14 | 15 | func (m *MockBinaryDetector) K3sBin() string { 16 | return m.k3sBin 17 | } 18 | 19 | func (m *MockBinaryDetector) K0sBin() string { 20 | return m.k0sBin 21 | } 22 | 23 | var _ = Describe("NewK8sNode", func() { 24 | Context("explicit k8s configuration", func() { 25 | It("should return error when k3s is explicitly disabled", func() { 26 | enabled := false 27 | config := &providerConfig.Config{ 28 | K3s: providerConfig.K3s{ 29 | Enabled: &enabled, 30 | }, 31 | } 32 | 33 | // Mock k3s binary as available so we can test configuration logic 34 | mock := &MockBinaryDetector{k3sBin: "/usr/bin/k3s", k0sBin: ""} 35 | _, err := NewK8sNodeWithDetector(config, mock) 36 | Expect(err).To(HaveOccurred()) 37 | Expect(err.Error()).To(ContainSubstring("no k8s configuration found")) 38 | }) 39 | 40 | It("should return error when k3s-agent is explicitly disabled", func() { 41 | enabled := false 42 | config := &providerConfig.Config{ 43 | K3sAgent: providerConfig.K3s{ 44 | Enabled: &enabled, 45 | }, 46 | } 47 | 48 | // Mock k3s binary as available so we can test configuration logic 49 | mock := &MockBinaryDetector{k3sBin: "/usr/bin/k3s", k0sBin: ""} 50 | _, err := NewK8sNodeWithDetector(config, mock) 51 | Expect(err).To(HaveOccurred()) 52 | Expect(err.Error()).To(ContainSubstring("no k8s configuration found")) 53 | }) 54 | 55 | It("should return error when k0s is explicitly disabled", func() { 56 | enabled := false 57 | config := &providerConfig.Config{ 58 | K0s: providerConfig.K0s{ 59 | Enabled: &enabled, 60 | }, 61 | } 62 | 63 | // Mock k0s binary as available so we can test configuration logic 64 | mock := &MockBinaryDetector{k3sBin: "", k0sBin: "/usr/bin/k0s"} 65 | _, err := NewK8sNodeWithDetector(config, mock) 66 | Expect(err).To(HaveOccurred()) 67 | Expect(err.Error()).To(ContainSubstring("no k8s configuration found")) 68 | }) 69 | 70 | It("should return error when k0s-worker is explicitly disabled", func() { 71 | enabled := false 72 | config := &providerConfig.Config{ 73 | K0sWorker: providerConfig.K0s{ 74 | Enabled: &enabled, 75 | }, 76 | } 77 | 78 | // Mock k0s binary as available so we can test configuration logic 79 | mock := &MockBinaryDetector{k3sBin: "", k0sBin: "/usr/bin/k0s"} 80 | _, err := NewK8sNodeWithDetector(config, mock) 81 | Expect(err).To(HaveOccurred()) 82 | Expect(err.Error()).To(ContainSubstring("no k8s configuration found")) 83 | }) 84 | }) 85 | 86 | Context("p2p configuration with specific role", func() { 87 | It("should return error for invalid p2p.role", func() { 88 | config := &providerConfig.Config{ 89 | P2P: &providerConfig.P2P{ 90 | Role: "invalid", 91 | }, 92 | } 93 | 94 | // Mock k3s binary as available so we can test configuration logic 95 | mock := &MockBinaryDetector{k3sBin: "/usr/bin/k3s", k0sBin: ""} 96 | _, err := NewK8sNodeWithDetector(config, mock) 97 | Expect(err).To(HaveOccurred()) 98 | Expect(err.Error()).To(ContainSubstring("invalid p2p.role specified")) 99 | }) 100 | }) 101 | 102 | Context("conflicting configurations", func() { 103 | It("should use p2p.role when both p2p.auto.enabled=true and p2p.role are specified", func() { 104 | enabled := true 105 | config := &providerConfig.Config{ 106 | P2P: &providerConfig.P2P{ 107 | Role: RoleMaster, 108 | Auto: providerConfig.Auto{ 109 | Enable: &enabled, 110 | }, 111 | }, 112 | } 113 | 114 | // Mock k3s binary as available so we can test configuration logic 115 | mock := &MockBinaryDetector{k3sBin: "/usr/bin/k3s", k0sBin: ""} 116 | node, err := NewK8sNodeWithDetector(config, mock) 117 | Expect(err).To(BeNil()) 118 | Expect(node).ToNot(BeNil()) 119 | // The role should be set to the explicit role, not auto-assigned 120 | Expect(node.(*K3sNode).role).To(Equal(RoleMaster)) 121 | }) 122 | }) 123 | 124 | Context("no k8s configuration", func() { 125 | It("should return error when no k8s configuration is provided", func() { 126 | config := &providerConfig.Config{} 127 | 128 | // Mock no binaries available to test the binary check 129 | mock := &MockBinaryDetector{k3sBin: "", k0sBin: ""} 130 | _, err := NewK8sNodeWithDetector(config, mock) 131 | Expect(err).To(HaveOccurred()) 132 | Expect(err.Error()).To(ContainSubstring("no k8s binary is available")) 133 | }) 134 | 135 | It("should create auto-mode node when p2p is configured with network token", func() { 136 | config := &providerConfig.Config{ 137 | P2P: &providerConfig.P2P{ 138 | NetworkToken: "fooblar", 139 | }, 140 | } 141 | 142 | // Mock k3s binary available to test configuration logic 143 | mock := &MockBinaryDetector{k3sBin: "/usr/bin/k3s", k0sBin: ""} 144 | node, err := NewK8sNodeWithDetector(config, mock) 145 | Expect(err).To(BeNil()) 146 | Expect(node).ToNot(BeNil()) 147 | // Node should be created in auto mode (no explicit role set) 148 | Expect(node.(*K3sNode).role).To(Equal("")) 149 | }) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /internal/role/p2p/master.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 10 | "github.com/kairos-io/kairos-sdk/utils" 11 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 12 | "github.com/kairos-io/provider-kairos/v2/internal/role" 13 | 14 | service "github.com/mudler/edgevpn/api/client/service" 15 | ) 16 | 17 | func propagateMasterData(role string, k K8sNode) error { 18 | c := k.RoleConfig() 19 | defer func() { 20 | // Avoid polluting the API. 21 | // The ledger already retries in the background to update the blockchain, but it has 22 | // a default timeout where it would stop trying afterwards. 23 | // Each request here would have it's own background announce, so that can become expensive 24 | // when network is having lot of changes on its way. 25 | time.Sleep(30 * time.Second) 26 | }() 27 | 28 | // If we are configured as master, always signal our role 29 | if err := c.Client.Set("role", c.UUID, role); err != nil { 30 | c.Logger.Error(err) 31 | return err 32 | } 33 | 34 | if k.HA() && !k.ClusterInit() { 35 | return nil 36 | } 37 | 38 | err := k.PropagateData() 39 | if err != nil { 40 | c.Logger.Error(err) 41 | } 42 | 43 | err = c.Client.Set("master", "ip", k.IP()) 44 | if err != nil { 45 | c.Logger.Error(err) 46 | } 47 | return nil 48 | } 49 | 50 | // we either return the ElasticIP or the IP from the edgevpn interface. 51 | func guessIP(pconfig *providerConfig.Config) string { 52 | if pconfig.KubeVIP.EIP != "" { 53 | return pconfig.KubeVIP.EIP 54 | } 55 | return utils.GetInterfaceIP("edgevpn0") 56 | } 57 | 58 | func waitForMasterHAInfo(m K8sNode) bool { 59 | var nodeToken string 60 | 61 | nodeToken, _ = m.Token() 62 | c := m.RoleConfig() 63 | 64 | if nodeToken == "" { 65 | c.Logger.Info("the nodetoken is not there yet..") 66 | return true 67 | } 68 | clusterInitIP, _ := c.Client.Get("master", "ip") 69 | if clusterInitIP == "" { 70 | c.Logger.Info("the clusterInitIP is not there yet..") 71 | return true 72 | } 73 | 74 | return false 75 | } 76 | 77 | func Master(cc *config.Config, pconfig *providerConfig.Config, roleName string) role.Role { //nolint:revive 78 | return func(c *service.RoleConfig) error { 79 | c.Logger.Info(fmt.Sprintf("Starting Master(%s)", roleName)) 80 | 81 | ip := guessIP(pconfig) 82 | // If we don't have an IP, we sit and wait 83 | if ip == "" { 84 | return errors.New("node doesn't have an ip yet") 85 | } 86 | if err := c.Client.Set("ip", c.UUID, ip); err != nil { 87 | c.Logger.Error(err) 88 | } 89 | 90 | c.Logger.Info("Checking role assignment") 91 | 92 | if pconfig.P2P.Role != "" { 93 | c.Logger.Info(fmt.Sprintf("Setting role from configuration: %s", pconfig.P2P.Role)) 94 | // propagate role if we were forced by configuration 95 | // This unblocks eventual auto instances to try to assign roles 96 | if err := c.Client.Set("role", c.UUID, pconfig.P2P.Role); err != nil { 97 | c.Logger.Error(err) 98 | } 99 | } 100 | 101 | c.Logger.Info("Determining K8s distro") 102 | node, err := NewK8sNode(pconfig) 103 | if err != nil { 104 | return fmt.Errorf("stopping Master: %s", err.Error()) 105 | } 106 | 107 | node.SetRole(roleName) 108 | node.SetRoleConfig(c) 109 | node.SetIP(ip) 110 | node.GuessInterface() 111 | 112 | c.Logger.Info("Verifying sentinel file") 113 | if role.SentinelExist() { 114 | c.Logger.Info("Node already configured, propagating master data and backing off") 115 | return propagateMasterData(roleName, node) 116 | } 117 | 118 | c.Logger.Info("Checking HA") 119 | if node.HA() && !node.ClusterInit() && waitForMasterHAInfo(node) { 120 | return nil 121 | } 122 | 123 | c.Logger.Info("Generating env") 124 | env := node.GenerateEnv() 125 | 126 | // Configure k8s service to start on edgevpn0 127 | c.Logger.Info(fmt.Sprintf("Configuring %s", node.Distro())) 128 | 129 | c.Logger.Info("Running bootstrap before stage") 130 | utils.SH(fmt.Sprintf("kairos-agent run-stage provider-kairos.bootstrap.before.%s", roleName)) //nolint:errcheck 131 | 132 | svc, err := node.Service() 133 | if err != nil { 134 | return fmt.Errorf("failed to get %s service: %w", node.Distro(), err) 135 | } 136 | 137 | c.Logger.Info("Writing service Env %s") 138 | envUnit := node.EnvUnit() 139 | if err := utils.WriteEnv(envUnit, 140 | env, 141 | ); err != nil { 142 | return fmt.Errorf("failed to write the %s service: %w", node.Distro(), err) 143 | } 144 | 145 | c.Logger.Info("Generating args") 146 | args, err := node.GenArgs() 147 | if err != nil { 148 | return fmt.Errorf("failed to generate %s args: %w", node.Distro(), err) 149 | } 150 | 151 | if node.ProviderConfig().KubeVIP.IsEnabled() { 152 | c.Logger.Info("Configuring KubeVIP") 153 | if err := node.DeployKubeVIP(); err != nil { 154 | return fmt.Errorf("failed KubeVIP setup: %w", err) 155 | } 156 | } 157 | 158 | k8sBin := node.K8sBin() 159 | if k8sBin == "" { 160 | return fmt.Errorf("no %s binary found (?)", node.Distro()) 161 | } 162 | 163 | c.Logger.Info("Writing service override") 164 | if err := svc.OverrideCmd(fmt.Sprintf("%s %s %s", k8sBin, node.Role(), strings.Join(args, " "))); err != nil { 165 | return fmt.Errorf("failed to override %s command: %w", node.Distro(), err) 166 | } 167 | 168 | c.Logger.Info("Starting service") 169 | if err := svc.Start(); err != nil { 170 | return fmt.Errorf("failed to start %s service: %w", node.Distro(), err) 171 | } 172 | 173 | c.Logger.Info("Enabling service") 174 | if err := svc.Enable(); err != nil { 175 | return fmt.Errorf("failed to enable %s service: %w", node.Distro(), err) 176 | } 177 | 178 | c.Logger.Info("Propagating master data") 179 | if err := propagateMasterData(roleName, node); err != nil { 180 | return fmt.Errorf("failed to propagate master data: %w", err) 181 | } 182 | 183 | c.Logger.Info("Running after bootstrap stage") 184 | utils.SH(fmt.Sprintf("kairos-agent run-stage provider-kairos.bootstrap.after.%s", roleName)) //nolint:errcheck 185 | 186 | c.Logger.Info("Creating sentinel") 187 | if err := role.CreateSentinel(); err != nil { 188 | return fmt.Errorf("failed to create sentinel: %w", err) 189 | } 190 | 191 | return nil 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /internal/cli/bridge.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/ipfs/go-log/v2" 10 | qr "github.com/kairos-io/go-nodepair/qrcode" 11 | "github.com/kairos-io/kairos-sdk/utils" 12 | "github.com/mudler/edgevpn/api" 13 | "github.com/mudler/edgevpn/cmd" 14 | "github.com/mudler/edgevpn/pkg/logger" 15 | "github.com/mudler/edgevpn/pkg/node" 16 | "github.com/mudler/edgevpn/pkg/services" 17 | "github.com/mudler/edgevpn/pkg/vpn" 18 | "github.com/urfave/cli/v2" 19 | ) 20 | 21 | func BridgeCMD(toolName string) *cli.Command { 22 | usage := "Connect to a kairos VPN network" 23 | description := ` 24 | Starts a bridge with a kairos network or a node. 25 | 26 | # With a network 27 | 28 | By default, "bridge" will create a VPN network connection to the node with the token supplied, thus it requires elevated permissions in order to work. 29 | 30 | For example: 31 | 32 | $ sudo %s bridge --token 33 | 34 | Will start a VPN, which local ip is fixed to 10.1.0.254 (tweakable with --address). 35 | 36 | The API will also be accessible at http://127.0.0.1:8080 37 | 38 | # With a node 39 | 40 | "%s bridge" can be used also to connect over to a node in recovery mode. When operating in this modality kairos bridge requires no specific permissions, indeed a tunnel 41 | will be created locally to connect to the machine remotely. 42 | 43 | For example: 44 | 45 | $ %s bridge --qr-code-image /path/to/image.png 46 | 47 | Will scan the QR code in the image and connect over. Further instructions on how to connect over will be printed out to the screen. 48 | 49 | See also: https://kairos.io/docs/reference/recovery_mode/ 50 | 51 | ` 52 | 53 | if toolName != "kairosctl" { 54 | usage += " (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead)" 55 | description = "\t\tWARNING: This command will be deprecated in the next release. Please use the new kairosctl binary instead.\n" + description 56 | } 57 | 58 | flags := []cli.Flag{ 59 | &cli.BoolFlag{ 60 | Name: "qr-code-snapshot", 61 | Required: false, 62 | Usage: "Bool to take a local snapshot instead of reading from an image file for recovery", 63 | EnvVars: []string{"QR_CODE_SNAPSHOT"}, 64 | }, 65 | &cli.StringFlag{ 66 | Name: "qr-code-image", 67 | Usage: "Path to an image containing a valid QR code for recovery mode", 68 | Required: false, 69 | EnvVars: []string{"QR_CODE_IMAGE"}, 70 | }, 71 | &cli.StringFlag{ 72 | Name: "api", 73 | Value: "127.0.0.1:8080", 74 | Usage: "Listening API url", 75 | }, 76 | &cli.BoolFlag{ 77 | Name: "dhcp", 78 | EnvVars: []string{"DHCP"}, 79 | Usage: "Enable DHCP", 80 | }, 81 | &cli.StringFlag{ 82 | Value: "10.1.0.254/24", 83 | Name: "address", 84 | EnvVars: []string{"ADDRESS"}, 85 | Usage: "Specify an address for the bridge", 86 | }, 87 | &cli.StringFlag{ 88 | Value: "/tmp/kairos", 89 | Name: "lease-dir", 90 | EnvVars: []string{"lease-dir"}, 91 | Usage: "DHCP Lease directory", 92 | }, 93 | &cli.StringFlag{ 94 | Name: "interface", 95 | Usage: "Interface name", 96 | Value: "kairos0", 97 | EnvVars: []string{"IFACE"}, 98 | }, 99 | } 100 | 101 | flags = append(flags, cmd.CommonFlags...) 102 | 103 | return &cli.Command{ 104 | Name: "bridge", 105 | UsageText: fmt.Sprintf("%s %s", toolName, "bridge --token XXX"), 106 | Usage: usage, 107 | Description: fmt.Sprintf(description, toolName, toolName, toolName), 108 | Flags: flags, 109 | Action: bridge, 110 | } 111 | } 112 | 113 | // bridge is just starting a VPN with edgevpn to the given network token. 114 | func bridge(c *cli.Context) error { 115 | qrCodePath := "" 116 | fromQRCode := false 117 | var serviceUUID, sshPassword string 118 | 119 | if c.String("qr-code-image") != "" { 120 | qrCodePath = c.String("qr-code-image") 121 | fromQRCode = true 122 | } 123 | if c.Bool("qr-code-snapshot") { 124 | qrCodePath = "" 125 | fromQRCode = true 126 | } 127 | 128 | if fromQRCode { 129 | recoveryToken := qr.Reader(qrCodePath) 130 | data := utils.DecodeRecoveryToken(recoveryToken) 131 | if len(data) != 3 { 132 | fmt.Println("Token not decoded correctly") 133 | return fmt.Errorf("invalid token") 134 | } 135 | token := data[0] 136 | serviceUUID = data[1] 137 | sshPassword = data[2] 138 | if serviceUUID == "" || sshPassword == "" || token == "" { 139 | return fmt.Errorf("decoded invalid values") 140 | } 141 | 142 | err := c.Set("token", token) 143 | if err != nil { 144 | return err 145 | } 146 | } 147 | 148 | ctx := context.Background() 149 | 150 | nc := cmd.ConfigFromContext(c) 151 | 152 | lvl, err := log.LevelFromString(nc.LogLevel) 153 | if err != nil { 154 | lvl = log.LevelError 155 | } 156 | llger := logger.New(lvl) 157 | 158 | o, vpnOpts, err := nc.ToOpts(llger) 159 | if err != nil { 160 | llger.Fatal(err.Error()) 161 | } 162 | 163 | opts := []node.Option{} 164 | 165 | if !fromQRCode { 166 | // We just connect to a VPN token 167 | o = append(o, 168 | services.Alive( 169 | time.Duration(20)*time.Second, 170 | time.Duration(10)*time.Second, 171 | time.Duration(10)*time.Second)...) 172 | 173 | if c.Bool("dhcp") { 174 | // Adds DHCP server 175 | address, _, err := net.ParseCIDR(c.String("address")) 176 | if err != nil { 177 | return err 178 | } 179 | nodeOpts, vO := vpn.DHCP(llger, 15*time.Minute, c.String("lease-dir"), address.String()) 180 | o = append(o, nodeOpts...) 181 | vpnOpts = append(vpnOpts, vO...) 182 | } 183 | 184 | opts, err = vpn.Register(vpnOpts...) 185 | if err != nil { 186 | return err 187 | } 188 | } else { 189 | // We hook into a service 190 | llger.Info("Connecting to service", serviceUUID) 191 | llger.Info("SSH access password is", sshPassword) 192 | llger.Info("SSH server reachable at 127.0.0.1:2200") 193 | opts = append(opts, node.WithNetworkService( 194 | services.ConnectNetworkService( 195 | 30*time.Second, 196 | serviceUUID, 197 | "127.0.0.1:2200", 198 | ), 199 | )) 200 | llger.Info("To connect, keep this terminal open and run in another terminal 'ssh 127.0.0.1 -p 2200' the password is ", sshPassword) 201 | llger.Info("Note: the connection might not be available instantly and first attempts will likely fail.") 202 | llger.Info(" Few attempts might be required before establishing a tunnel to the host.") 203 | } 204 | 205 | e, err := node.New(append(o, opts...)...) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | go api.API(ctx, c.String("api"), 5*time.Second, 20*time.Second, e, nil, false) //nolint:errcheck 211 | 212 | return e.Start(ctx) 213 | } 214 | -------------------------------------------------------------------------------- /internal/role/p2p/kubevip.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "os" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/kairos-io/provider-kairos/v2/internal/assets" 13 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 14 | "github.com/kube-vip/kube-vip/pkg/kubevip" 15 | ) 16 | 17 | var ( 18 | initConfig kubevip.Config 19 | initLoadBalancer kubevip.LoadBalancer 20 | ) 21 | 22 | const ( 23 | // DefaultKubeVIPVersion is the default version of kube-vip to use. 24 | // Should be automatically bumped by renovate as it uses this version to set the mage version to use in the generated manifest. 25 | DefaultKubeVIPVersion = "v1.0.3" 26 | ) 27 | 28 | // Generates the kube-vip manifest based on the command type. 29 | func generateKubeVIP(command string, iface, ip string, kConfig *providerConfig.Config) (string, error) { 30 | // Comand can be "manifest" or "daemonset" 31 | // iface is the interface name 32 | // ip is the VIP address 33 | var err error 34 | 35 | // Set the kube-vip config based on the provider config and what we loaded from config files 36 | applyKConfigToInitConfig(kConfig.KubeVIP, &initConfig) 37 | 38 | // Now set the values coming from env vars 39 | if err := kubevip.ParseEnvironment(&initConfig); err != nil { 40 | return "", fmt.Errorf("parsing environment: %w", err) 41 | } 42 | 43 | // Now the manual ones that are hardcoded by us 44 | initConfig.Interface = iface 45 | initConfig.Address = ip 46 | initConfig.EnableControlPlane = true 47 | initConfig.EnableARP = true 48 | initConfig.EnableLeaderElection = true 49 | initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer) 50 | 51 | // The control plane has a requirement for a VIP being specified. 52 | if initConfig.EnableControlPlane && (initConfig.VIP == "" && initConfig.Address == "" && !initConfig.DDNS) { 53 | return "", fmt.Errorf("no address is specified for kube-vip to expose services on") 54 | } 55 | 56 | // Ensure there is an address to generate the CIDR from. 57 | if initConfig.VIPSubnet == "" && initConfig.Address != "" { 58 | initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address) 59 | if err != nil { 60 | return "", fmt.Errorf("config parse: %w", err) 61 | } 62 | } 63 | var kubeVipVersion string 64 | if kConfig.KubeVIP.Version != "" { 65 | kubeVipVersion = kConfig.KubeVIP.Version 66 | } else { 67 | kubeVipVersion = DefaultKubeVIPVersion 68 | } 69 | 70 | // Some fixes for the default values if they are empty. 71 | if initConfig.LeaseDuration == 0 { 72 | initConfig.LeaseDuration = 5 73 | } 74 | if initConfig.RenewDeadline == 0 { 75 | initConfig.RenewDeadline = 3 76 | } 77 | if initConfig.RetryPeriod == 0 { 78 | initConfig.RetryPeriod = 1 79 | } 80 | if initConfig.PrometheusHTTPServer == "" { 81 | initConfig.PrometheusHTTPServer = ":2112" 82 | } 83 | if initConfig.Port == 0 { 84 | initConfig.Port = 6443 85 | } 86 | switch strings.ToLower(command) { 87 | case "daemonset": 88 | return kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, kubeVipVersion, true, true), nil 89 | case "pod": 90 | return kubevip.GeneratePodManifestFromConfig(&initConfig, kubeVipVersion, true), nil 91 | } 92 | return "", fmt.Errorf("unknown manifest type %s", command) 93 | } 94 | 95 | // applyKConfigToInitConfig applies the KubeVIP configuration to the initConfig . 96 | // by iterating over the fields of the KubeVIP struct and setting the corresponding 97 | // fields in the initConfig struct. It uses reflection to access the fields dynamically. 98 | // This allows us to replicate the kubevip.Config struct in our provider config directly. 99 | func applyKConfigToInitConfig(kConfig providerConfig.KubeVIP, initConfig *kubevip.Config) { 100 | kConfigValue := reflect.ValueOf(kConfig) 101 | kConfigType := reflect.TypeOf(kConfig) 102 | initConfigValue := reflect.ValueOf(initConfig).Elem() 103 | 104 | for i := 0; i < kConfigType.NumField(); i++ { 105 | kField := kConfigType.Field(i) 106 | kValue := kConfigValue.Field(i) 107 | 108 | // Check if the field exists in initConfig 109 | initField := initConfigValue.FieldByName(kField.Name) 110 | if initField.IsValid() && initField.Type() == kField.Type { 111 | // Set the value from kConfig to initConfig 112 | initField.Set(kValue) 113 | } 114 | } 115 | } 116 | 117 | func downloadFromURL(url, where string) error { 118 | output, err := os.Create(where) 119 | if err != nil { 120 | return err 121 | } 122 | defer output.Close() 123 | 124 | response, err := http.Get(url) 125 | if err != nil { 126 | return err 127 | 128 | } 129 | defer response.Body.Close() 130 | 131 | _, err = io.Copy(output, response.Body) 132 | return err 133 | } 134 | 135 | func deployKubeVIP(iface, ip string, pconfig *providerConfig.Config) error { 136 | manifestDirectory := "/var/lib/rancher/k3s/server/manifests/" 137 | if pconfig.K3sAgent.IsEnabled() { 138 | manifestDirectory = "/var/lib/rancher/k3s/agent/pod-manifests/" 139 | } 140 | if err := os.MkdirAll(manifestDirectory, 0650); err != nil { 141 | return fmt.Errorf("could not create manifest dir") 142 | } 143 | 144 | targetFile := manifestDirectory + "kubevip.yaml" 145 | targetCRDFile := manifestDirectory + "kubevipmanifest.yaml" 146 | 147 | command := "daemonset" 148 | if pconfig.KubeVIP.StaticPod { 149 | command = "pod" 150 | } 151 | 152 | if pconfig.KubeVIP.ManifestURL != "" { 153 | err := downloadFromURL(pconfig.KubeVIP.ManifestURL, targetCRDFile) 154 | if err != nil { 155 | return err 156 | } 157 | } else { 158 | f, err := assets.GetStaticFS().Open("kube_vip_rbac.yaml") 159 | if err != nil { 160 | return fmt.Errorf("could not find kube_vip in assets") 161 | } 162 | defer f.Close() 163 | 164 | destination, err := os.Create(targetCRDFile) 165 | if err != nil { 166 | return err 167 | } 168 | defer destination.Close() 169 | _, err = io.Copy(destination, f) 170 | if err != nil { 171 | return err 172 | } 173 | } 174 | 175 | content, err := generateKubeVIP(command, iface, ip, pconfig) 176 | if err != nil { 177 | return fmt.Errorf("could not generate kubevip %s", err.Error()) 178 | } 179 | 180 | f, err := os.Create(targetFile) 181 | if err != nil { 182 | return fmt.Errorf("could not open %s: %w", f.Name(), err) 183 | } 184 | defer f.Close() 185 | if _, err := f.WriteString(content); err != nil { 186 | return fmt.Errorf("could not write to %s: %w", f.Name(), err) 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func GenerateCidrRange(address string) (string, error) { 193 | var cidrs []string 194 | 195 | addresses := strings.Split(address, ",") 196 | for _, a := range addresses { 197 | ip := net.ParseIP(a) 198 | if ip == nil { 199 | ips, err := net.LookupIP(a) 200 | if len(ips) == 0 || err != nil { 201 | return "", fmt.Errorf("invalid IP address: %s from [%s], %v", a, address, err) 202 | } 203 | ip = ips[0] 204 | } 205 | 206 | if ip.To4() != nil { 207 | cidrs = append(cidrs, "32") 208 | } else { 209 | cidrs = append(cidrs, "128") 210 | } 211 | } 212 | 213 | return strings.Join(cidrs, ","), nil 214 | } 215 | -------------------------------------------------------------------------------- /internal/role/p2p/k3s.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/kairos-io/kairos-sdk/machine" 11 | "github.com/kairos-io/kairos-sdk/utils" 12 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 13 | service "github.com/mudler/edgevpn/api/client/service" 14 | ) 15 | 16 | const ( 17 | K3sDistroName = "k3s" 18 | K3sMasterName = "server" 19 | K3sWorkerName = "agent" 20 | K3sMasterServiceName = "k3s" 21 | K3sWorkerServiceName = "k3s-agent" 22 | ) 23 | 24 | type K3sNode struct { 25 | providerConfig *providerConfig.Config 26 | roleConfig *service.RoleConfig 27 | ip string 28 | iface string 29 | ifaceIP string 30 | role string 31 | } 32 | 33 | func (k *K3sNode) IsWorker() bool { 34 | return k.role == RoleWorker 35 | } 36 | 37 | func (k *K3sNode) K8sBin() string { 38 | return utils.K3sBin() 39 | } 40 | 41 | func (k *K3sNode) DeployKubeVIP() error { 42 | pconfig := k.ProviderConfig() 43 | if !pconfig.KubeVIP.IsEnabled() { 44 | return nil 45 | } 46 | 47 | return deployKubeVIP(k.iface, k.ip, pconfig) 48 | } 49 | 50 | func (k *K3sNode) GenArgs() ([]string, error) { 51 | var args []string 52 | pconfig := k.ProviderConfig() 53 | 54 | if pconfig.P2P.UseVPNWithKubernetes() { 55 | args = append(args, "--flannel-iface=edgevpn0") 56 | } 57 | 58 | if pconfig.KubeVIP.IsEnabled() { 59 | args = append(args, fmt.Sprintf("--tls-san=%s", k.ip), fmt.Sprintf("--node-ip=%s", k.ifaceIP)) 60 | } 61 | 62 | if pconfig.K3s.EmbeddedRegistry { 63 | args = append(args, "--embedded-registry") 64 | } 65 | 66 | if pconfig.P2P.Auto.HA.ExternalDB != "" { 67 | args = []string{fmt.Sprintf("--datastore-endpoint=%s", pconfig.P2P.Auto.HA.ExternalDB)} 68 | } 69 | 70 | if k.HA() && !k.ClusterInit() { 71 | clusterInitIP, _ := k.roleConfig.Client.Get("master", "ip") 72 | args = append(args, fmt.Sprintf("--server=https://%s:6443", clusterInitIP)) 73 | } 74 | // The --cluster-init flag changes the embedded SQLite DB to etcd. We don't 75 | // want to do this if we're using an external DB. 76 | if k.ClusterInit() && pconfig.P2P.Auto.HA.ExternalDB == "" { 77 | args = append(args, "--cluster-init") 78 | } 79 | 80 | args = k.AppendArgs(args) 81 | 82 | return args, nil 83 | } 84 | 85 | func (k *K3sNode) AppendArgs(other []string) []string { 86 | c := k.ProviderConfig() 87 | if c.K3s.ReplaceArgs { 88 | return c.K3s.Args 89 | } 90 | 91 | return append(other, c.K3s.Args...) 92 | } 93 | 94 | func (k *K3sNode) EnvUnit() string { 95 | return machine.K3sEnvUnit("k3s") 96 | } 97 | 98 | func (k *K3sNode) Service() (machine.Service, error) { 99 | if k.role == "worker" { 100 | return machine.K3sAgent() 101 | } 102 | 103 | return machine.K3s() 104 | } 105 | 106 | func (k *K3sNode) Token() (string, error) { 107 | return k.RoleConfig().Client.Get("nodetoken", "token") 108 | } 109 | 110 | func (k *K3sNode) GenerateEnv() (env map[string]string) { 111 | env = make(map[string]string) 112 | 113 | if k.HA() && !k.ClusterInit() { 114 | nodeToken, _ := k.Token() 115 | env["K3S_TOKEN"] = nodeToken 116 | } 117 | 118 | pConfig := k.ProviderConfig() 119 | 120 | if pConfig.K3s.ReplaceEnv { 121 | env = pConfig.K3s.Env 122 | } else { 123 | // Override opts with user-supplied 124 | for k, v := range pConfig.K3s.Env { 125 | env[k] = v 126 | } 127 | } 128 | 129 | return env 130 | } 131 | 132 | func (k *K3sNode) ProviderConfig() *providerConfig.Config { 133 | return k.providerConfig 134 | } 135 | 136 | func (k *K3sNode) SetRoleConfig(c *service.RoleConfig) { 137 | k.roleConfig = c 138 | } 139 | 140 | func (k *K3sNode) RoleConfig() *service.RoleConfig { 141 | return k.roleConfig 142 | } 143 | 144 | func (k *K3sNode) HA() bool { 145 | return k.role == "master/ha" 146 | } 147 | 148 | func (k *K3sNode) ClusterInit() bool { 149 | return k.role == "master/clusterinit" 150 | } 151 | 152 | func (k *K3sNode) IP() string { 153 | return k.ip 154 | } 155 | 156 | func (k *K3sNode) PropagateData() error { 157 | c := k.RoleConfig() 158 | tokenB, err := os.ReadFile("/var/lib/rancher/k3s/server/node-token") 159 | if err != nil { 160 | c.Logger.Error(err) 161 | return err 162 | } 163 | 164 | nodeToken := string(tokenB) 165 | nodeToken = strings.TrimRight(nodeToken, "\n") 166 | if nodeToken != "" { 167 | err := c.Client.Set("nodetoken", "token", nodeToken) 168 | if err != nil { 169 | c.Logger.Error(err) 170 | } 171 | } 172 | 173 | kubeB, err := os.ReadFile("/etc/rancher/k3s/k3s.yaml") 174 | if err != nil { 175 | c.Logger.Error(err) 176 | return err 177 | } 178 | kubeconfig := string(kubeB) 179 | if kubeconfig != "" { 180 | err := c.Client.Set("kubeconfig", "master", base64.RawURLEncoding.EncodeToString(kubeB)) 181 | if err != nil { 182 | c.Logger.Error(err) 183 | } 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func (k *K3sNode) WorkerArgs() ([]string, error) { 190 | pconfig := k.ProviderConfig() 191 | k3sConfig := providerConfig.K3s{} 192 | if pconfig.K3sAgent.IsEnabled() { 193 | k3sConfig = pconfig.K3sAgent 194 | } 195 | 196 | args := []string{ 197 | "--with-node-id", 198 | } 199 | 200 | if pconfig.P2P.UseVPNWithKubernetes() { 201 | ip := utils.GetInterfaceIP("edgevpn0") 202 | if ip == "" { 203 | return nil, errors.New("node doesn't have an ip yet") 204 | } 205 | args = append(args, 206 | fmt.Sprintf("--node-ip %s", ip), 207 | "--flannel-iface=edgevpn0") 208 | } else { 209 | iface := guessInterface(pconfig) 210 | ip := utils.GetInterfaceIP(iface) 211 | args = append(args, 212 | fmt.Sprintf("--node-ip %s", ip)) 213 | } 214 | 215 | if k3sConfig.ReplaceArgs { 216 | args = k3sConfig.Args 217 | } else { 218 | args = append(args, k3sConfig.Args...) 219 | } 220 | 221 | return args, nil 222 | } 223 | 224 | func (k *K3sNode) SetupWorker(masterIP, nodeToken string) error { 225 | pconfig := k.ProviderConfig() 226 | 227 | nodeToken = strings.TrimRight(nodeToken, "\n") 228 | 229 | k3sConfig := providerConfig.K3s{} 230 | if pconfig.K3sAgent.IsEnabled() { 231 | k3sConfig = pconfig.K3sAgent 232 | } 233 | 234 | env := map[string]string{ 235 | "K3S_URL": fmt.Sprintf("https://%s:6443", masterIP), 236 | "K3S_TOKEN": nodeToken, 237 | } 238 | 239 | if k3sConfig.ReplaceEnv { 240 | env = k3sConfig.Env 241 | } else { 242 | // Override opts with user-supplied 243 | for k, v := range k3sConfig.Env { 244 | env[k] = v 245 | } 246 | } 247 | 248 | if err := utils.WriteEnv(machine.K3sEnvUnit("k3s-agent"), 249 | env, 250 | ); err != nil { 251 | return err 252 | } 253 | 254 | return nil 255 | } 256 | 257 | func (k *K3sNode) Role() string { 258 | if k.IsWorker() { 259 | return K3sWorkerName 260 | } 261 | 262 | return K3sMasterName 263 | } 264 | 265 | func (k *K3sNode) ServiceName() string { 266 | if k.IsWorker() { 267 | return K3sWorkerServiceName 268 | } 269 | 270 | return K3sMasterServiceName 271 | } 272 | 273 | func (k *K3sNode) Env() map[string]string { 274 | c := k.ProviderConfig() 275 | if k.IsWorker() { 276 | return c.K3sAgent.Env 277 | } 278 | 279 | return c.K3s.Env 280 | } 281 | 282 | func (k *K3sNode) Args() []string { 283 | c := k.ProviderConfig() 284 | var args []string 285 | 286 | if !c.K3sAgent.IsEnabled() && !c.K3s.IsEnabled() { 287 | return []string{} 288 | } 289 | 290 | if k.IsWorker() { 291 | return c.K3sAgent.Args 292 | } 293 | 294 | args = c.K3s.Args 295 | // Add embedded registry flag if enabled (for non-p2p mode, server only) 296 | if c.K3s.EmbeddedRegistry { 297 | args = append(args, "--embedded-registry") 298 | } 299 | 300 | return args 301 | } 302 | 303 | func (k *K3sNode) EnvFile() string { 304 | return machine.K3sEnvUnit(k.ServiceName()) 305 | } 306 | 307 | func (k *K3sNode) SetRole(role string) { 308 | k.role = role 309 | } 310 | 311 | func (k *K3sNode) SetIP(ip string) { 312 | k.ip = ip 313 | } 314 | 315 | func (k *K3sNode) GuessInterface() { 316 | iface := guessInterface(k.ProviderConfig()) 317 | ifaceIP := utils.GetInterfaceIP(iface) 318 | 319 | k.iface = iface 320 | k.ifaceIP = ifaceIP 321 | } 322 | 323 | func (k *K3sNode) Distro() string { 324 | return K3sDistroName 325 | } 326 | -------------------------------------------------------------------------------- /internal/provider/bootstrap.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/kairos-io/kairos-sdk/bus" 10 | "github.com/kairos-io/kairos-sdk/machine" 11 | "github.com/kairos-io/kairos-sdk/machine/openrc" 12 | "github.com/kairos-io/kairos-sdk/machine/systemd" 13 | "github.com/kairos-io/kairos-sdk/types" 14 | "github.com/kairos-io/kairos-sdk/utils" 15 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 16 | "github.com/kairos-io/provider-kairos/v2/internal/role" 17 | p2p "github.com/kairos-io/provider-kairos/v2/internal/role/p2p" 18 | edgeVPNClient "github.com/mudler/edgevpn/api/client" 19 | 20 | "github.com/kairos-io/provider-kairos/v2/internal/services" 21 | 22 | "github.com/kairos-io/kairos-agent/v2/pkg/config" 23 | "github.com/mudler/edgevpn/api/client/service" 24 | "github.com/mudler/go-pluggable" 25 | ) 26 | 27 | func Bootstrap(e *pluggable.Event) pluggable.EventResponse { 28 | cfg := &bus.BootstrapPayload{} 29 | err := json.Unmarshal([]byte(e.Data), cfg) 30 | if err != nil { 31 | return ErrorEvent("Failed reading JSON input: %s input '%s'", err.Error(), e.Data) 32 | } 33 | 34 | c := &config.Config{} 35 | prvConfig := &providerConfig.Config{} 36 | err = config.FromString(cfg.Config, c) 37 | if err != nil { 38 | return ErrorEvent("Failed reading JSON input: %s input '%s'", err.Error(), cfg.Config) 39 | } 40 | 41 | err = config.FromString(cfg.Config, prvConfig) 42 | if err != nil { 43 | return ErrorEvent("Failed reading JSON input: %s input '%s'", err.Error(), cfg.Config) 44 | } 45 | // TODO: this belong to a systemd service that is started instead 46 | 47 | p2pBlockDefined := prvConfig.P2P != nil 48 | tokenNotDefined := (p2pBlockDefined && prvConfig.P2P.NetworkToken == "") || !p2pBlockDefined 49 | skipAuto := p2pBlockDefined && !prvConfig.P2P.Auto.IsEnabled() 50 | 51 | if !prvConfig.IsP2PConfigured() && !prvConfig.IsKubernetesConfigured() { 52 | return pluggable.EventResponse{State: "no P2P or kubernetes configured"} 53 | } 54 | 55 | utils.SH("kairos-agent run-stage kairos-agent.bootstrap") //nolint:errcheck 56 | bus.RunHookScript("/usr/bin/kairos-agent.bootstrap.hook") //nolint:errcheck 57 | 58 | logLevel := "debug" 59 | 60 | if p2pBlockDefined && prvConfig.P2P.LogLevel != "" { 61 | logLevel = prvConfig.P2P.LogLevel 62 | } 63 | 64 | logger := types.NewKairosLogger("provider", logLevel, false) 65 | 66 | // Do onetimebootstrap if a Kubernetes distribution is enabled. 67 | // Those blocks are not required to be enabled in case of a kairos 68 | // full automated setup. Otherwise, they must be explicitly enabled. 69 | if (tokenNotDefined && prvConfig.IsKubernetesConfigured()) || skipAuto { 70 | err := oneTimeBootstrap(logger, prvConfig, func() error { 71 | return SetupVPN(services.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, prvConfig) 72 | }) 73 | if err != nil { 74 | return ErrorEvent("Failed setup: %s", err.Error()) 75 | } 76 | return pluggable.EventResponse{} 77 | } 78 | 79 | if tokenNotDefined { 80 | return ErrorEvent("No network token provided, or kubernetes distribution (k3s, k0s) block configured. Exiting") 81 | } 82 | 83 | // We might still want a VPN, but not to route traffic into 84 | if prvConfig.P2P.VPNNeedsCreation() { 85 | logger.Info("Configuring VPN") 86 | if err := SetupVPN(services.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, prvConfig); err != nil { 87 | return ErrorEvent("Failed setup VPN: %s", err.Error()) 88 | } 89 | } else { // We need at least the API to co-ordinate 90 | logger.Info("Configuring API") 91 | if err := SetupAPI(cfg.APIAddress, "/", true, prvConfig); err != nil { 92 | return ErrorEvent("Failed setup VPN: %s", err.Error()) 93 | } 94 | } 95 | 96 | networkID := "kairos" 97 | 98 | if p2pBlockDefined && prvConfig.P2P.NetworkID != "" { 99 | networkID = prvConfig.P2P.NetworkID 100 | } 101 | 102 | cc := service.NewClient( 103 | networkID, 104 | edgeVPNClient.NewClient(edgeVPNClient.WithHost(cfg.APIAddress))) 105 | 106 | nodeOpts := []service.Option{ 107 | service.WithMinNodes(prvConfig.P2P.MinimumNodes), 108 | service.WithLogger(logger), 109 | service.WithClient(cc), 110 | service.WithUUID(machine.UUID()), 111 | service.WithStateDir("/usr/local/.kairos/state"), 112 | service.WithNetworkToken(prvConfig.P2P.NetworkToken), 113 | service.WithPersistentRoles(p2p.RoleAuto), 114 | service.WithRoles( 115 | service.RoleKey{ 116 | Role: p2p.RoleMaster, 117 | RoleHandler: p2p.Master(c, prvConfig, p2p.RoleMaster), 118 | }, 119 | service.RoleKey{ 120 | Role: p2p.RoleMasterClusterInit, 121 | RoleHandler: p2p.Master(c, prvConfig, p2p.RoleMasterClusterInit), 122 | }, 123 | service.RoleKey{ 124 | Role: p2p.RoleMasterHA, 125 | RoleHandler: p2p.Master(c, prvConfig, p2p.RoleMasterHA), 126 | }, 127 | service.RoleKey{ 128 | Role: p2p.RoleWorker, 129 | RoleHandler: p2p.Worker(c, prvConfig), 130 | }, 131 | service.RoleKey{ 132 | Role: p2p.RoleAuto, 133 | RoleHandler: role.Auto(c, prvConfig), 134 | }, 135 | ), 136 | } 137 | 138 | // Optionally set up a specific node role if the user has defined so 139 | if prvConfig.P2P.Role != "" { 140 | logger.Info("Setting default role from configuration: ", prvConfig.P2P.Role) 141 | nodeOpts = append(nodeOpts, service.WithDefaultRoles(prvConfig.P2P.Role)) 142 | } 143 | 144 | k, err := service.NewNode(nodeOpts...) 145 | if err != nil { 146 | return ErrorEvent("Failed creating node: %s", err.Error()) 147 | } 148 | err = k.Start(context.Background()) 149 | if err != nil { 150 | return ErrorEvent("Failed start: %s", err.Error()) 151 | } 152 | 153 | return pluggable.EventResponse{ 154 | State: "", 155 | Data: "", 156 | Error: "shouldn't return here", 157 | } 158 | } 159 | 160 | func oneTimeBootstrap(l types.KairosLogger, c *providerConfig.Config, vpnSetupFN func() error) error { 161 | var err error 162 | if role.SentinelExist() { 163 | l.Info("Sentinel exists, nothing to do. exiting.") 164 | return nil 165 | } 166 | l.Info("One time bootstrap starting") 167 | 168 | var svc machine.Service 169 | var svcName, svcRole, envFile, binPath, args string 170 | var svcEnv map[string]string 171 | 172 | node, err := p2p.NewK8sNode(c) 173 | if err != nil { 174 | l.Errorf("failed on one-time bootstrap: %s", err.Error()) 175 | return err 176 | } 177 | 178 | svcName = node.ServiceName() 179 | svcRole = node.Role() 180 | svcEnv = node.Env() 181 | args = strings.Join(node.Args(), " ") 182 | binPath = node.K8sBin() 183 | envFile = node.EnvFile() 184 | 185 | if binPath == "" { 186 | l.Errorf("no %s binary found", svcName) 187 | return fmt.Errorf("no %s binary found", svcName) 188 | } 189 | 190 | if err := utils.WriteEnv(envFile, svcEnv); err != nil { 191 | l.Errorf("Failed to write %s env file: %s", svcName, err.Error()) 192 | return err 193 | } 194 | 195 | // Initialize the service based on the system's init system 196 | if utils.IsOpenRCBased() { 197 | svc, err = openrc.NewService(openrc.WithName(svcName)) 198 | } else { 199 | svc, err = systemd.NewService(systemd.WithName(svcName)) 200 | } 201 | 202 | if err != nil { 203 | l.Errorf("Failed to instantiate service: %s", err.Error()) 204 | return err 205 | } 206 | if svc == nil { 207 | return fmt.Errorf("could not detect OS") 208 | } 209 | 210 | // Override the service command and start it 211 | if err := svc.OverrideCmd(fmt.Sprintf("%s %s %s", binPath, svcRole, args)); err != nil { 212 | l.Errorf("Failed to override service command: %s", err.Error()) 213 | return err 214 | } 215 | if err := svc.Start(); err != nil { 216 | l.Errorf("Failed to start service: %s", err.Error()) 217 | return err 218 | } 219 | 220 | // When this fails, it doesn't produce an error! 221 | if err := svc.Enable(); err != nil { 222 | l.Errorf("Failed to enable service: %s", err.Error()) 223 | return err 224 | } 225 | 226 | // Setup VPN if required 227 | if c.P2P != nil && c.P2P.VPNNeedsCreation() { 228 | if err := vpnSetupFN(); err != nil { 229 | l.Errorf("Failed to setup VPN: %s", err.Error()) 230 | return err 231 | } 232 | } 233 | 234 | return role.CreateSentinel() 235 | } 236 | -------------------------------------------------------------------------------- /internal/provider/buildEvent.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/kairos-io/kairos-sdk/bus" 13 | "github.com/kairos-io/kairos-sdk/types" 14 | "github.com/kairos-io/kairos-sdk/utils" 15 | "github.com/kairos-io/provider-kairos/v2/internal/services" 16 | "github.com/mudler/go-pluggable" 17 | ) 18 | 19 | const ( 20 | K3s = "k3s" 21 | K0s = "k0s" 22 | ) 23 | 24 | // BuildEvent handles the buildtime event for the provider. Called by kairos-init during the build process. 25 | func BuildEvent(e *pluggable.Event) pluggable.EventResponse { 26 | returnData := pluggable.EventResponse{ 27 | State: "", 28 | Data: "", 29 | Error: "", 30 | } 31 | l := types.NewKairosLogger("provider-kairos-build", "info", true) 32 | l.Logger.Info().Msg("Buildtime event received") 33 | l.Logger.Debug().Interface("event", e).Msg("Event details") 34 | // unmarshal the event data if needed 35 | p := &bus.ProviderPayload{} 36 | if e.Data != "" { 37 | err := json.Unmarshal([]byte(e.Data), p) 38 | if err != nil { 39 | l.Logger.Error().Err(err).Msg("Failed to unmarshal event data") 40 | returnData.Error = err.Error() 41 | returnData.State = bus.EventResponseError 42 | return returnData 43 | } 44 | } 45 | // Now move the logger to the requested log level 46 | l.SetLevel(p.LogLevel) 47 | l.Logger.Debug().Interface("payload", p).Msg("Payload details") 48 | // Download the installer script for the provider 49 | var url string 50 | switch p.Provider { 51 | case K3s: 52 | url = "https://get.k3s.io" 53 | case K0s: 54 | url = "https://get.k0s.sh" 55 | } 56 | 57 | installerFile := filepath.Join(os.TempDir(), "installer.sh") 58 | 59 | // Download the installer script 60 | switch p.Provider { 61 | case K3s, K0s: 62 | l.Logger.Info().Msgf("Downloading installer script for %s from %s", p.Provider, url) 63 | // TODO: Do it with golang instead of needing curl? 64 | out, err := exec.Command("curl", "-sfL", url, "-o", installerFile).CombinedOutput() 65 | if err != nil { 66 | l.Logger.Error().Err(err).Msgf("Failed to download installer script: %s", string(out)) 67 | returnData.Error = fmt.Sprintf("Failed to download installer script: %s", string(out)) 68 | returnData.State = bus.EventResponseError 69 | return returnData 70 | } 71 | default: 72 | // This is not for us, its for another provider or no provider was specified 73 | l.Logger.Info().Msg("No valid provider specified or unsupported provider. Skipping buildtime logic.") 74 | returnData.State = bus.EventResponseNotApplicable 75 | return returnData 76 | } 77 | // Make the installer script executable 78 | err := os.Chmod(installerFile, 0755) 79 | if err != nil { 80 | l.Logger.Error().Err(err).Msgf("Failed to make installer script executable: %s", installerFile) 81 | returnData.Error = fmt.Sprintf("Failed to make installer script executable: %s", err) 82 | returnData.State = bus.EventResponseError 83 | return returnData 84 | } 85 | 86 | // Install the binaries 87 | var out []byte 88 | switch p.Provider { 89 | case K3s: 90 | // Prepare environment variables 91 | env := os.Environ() 92 | env = append(env, "INSTALL_K3S_BIN_DIR=/usr/bin", "INSTALL_K3S_SKIP_ENABLE=true", "INSTALL_K3S_SKIP_SELINUX_RPM=true") 93 | if p.Version != "" { 94 | env = append(env, fmt.Sprintf("INSTALL_K3S_VERSION=%s", p.Version)) 95 | } 96 | 97 | l.Logger.Info().Msg("Running k3s installer script") 98 | cmd := exec.Command("sh", installerFile) 99 | cmd.Env = env 100 | out, err = cmd.CombinedOutput() 101 | if err != nil { 102 | l.Logger.Error().Err(err).Msgf("Failed to run k3s installer script: %s", string(out)) 103 | returnData.Error = fmt.Sprintf("Failed to run k3s installer script: %s", string(out)) 104 | returnData.State = bus.EventResponseError 105 | return returnData 106 | } 107 | 108 | // Now agent 109 | agentCmd := exec.Command("sh", installerFile, "agent") 110 | agentCmd.Env = env 111 | out2, err := agentCmd.CombinedOutput() 112 | if err != nil { 113 | l.Logger.Error().Err(err).Msgf("Failed to run k3s agent installer script: %s", string(out)) 114 | returnData.Error = fmt.Sprintf("Failed to run k3s agent installer script: %s", string(out)) 115 | returnData.State = bus.EventResponseError 116 | return returnData 117 | } 118 | out = append(out, out2...) 119 | case K0s: 120 | env := os.Environ() 121 | if p.Version != "" { 122 | env = append(env, fmt.Sprintf("K0S_VERSION=%s", p.Version)) 123 | } 124 | l.Logger.Info().Msg("Running k0s installer script") 125 | cmd := exec.Command("sh", installerFile) 126 | cmd.Env = env 127 | out, err = cmd.CombinedOutput() 128 | if err != nil { 129 | l.Logger.Error().Err(err).Msgf("Failed to run k0s installer script: %s", string(out)) 130 | returnData.Error = fmt.Sprintf("Failed to run k0s installer script: %s", string(out)) 131 | returnData.State = bus.EventResponseError 132 | return returnData 133 | } 134 | // move the binary to a decent location t avoid overwriting it with PERSISTENT 135 | err = os.Rename("/usr/local/bin/k0s", "/usr/bin/k0s") 136 | if err != nil { 137 | l.Logger.Error().Err(err).Msg("Failed to move k0s binary to /usr/bin") 138 | returnData.Error = fmt.Sprintf("Failed to move k0s binary to /usr/bin: %s", err) 139 | returnData.State = bus.EventResponseError 140 | return returnData 141 | } 142 | // Because we change the binary location, the installer script wont produce the proper services 143 | // also we are running in a dockerfile so the service manager identification does not work as expected 144 | l.Logger.Info().Msg("Creating k0s service file manually") 145 | err = services.K0sServices(l) 146 | if err != nil { 147 | l.Logger.Error().Err(err).Msg("Failed to create k0s service file") 148 | returnData.Error = fmt.Sprintf("Failed to create k0s service file: %s", err) 149 | returnData.State = bus.EventResponseError 150 | return returnData 151 | } 152 | 153 | } 154 | returnData.Data = string(out) 155 | returnData.State = bus.EventResponseSuccess 156 | l.Logger.Debug().Msg("Returning response for buildtime event") 157 | l.Logger.Debug().Interface("response", returnData).Msg("Response details") 158 | return returnData 159 | } 160 | 161 | // InfoEvent handles the info event for the provider. Called by kairos-init during the build process. 162 | // It returns the installed version of the provider if available. 163 | func InfoEvent(e *pluggable.Event) pluggable.EventResponse { 164 | l := types.NewKairosLogger("provider-kairos-info", "info", true) 165 | l.Logger.Info().Msg("Info event received") 166 | l.Logger.Debug().Interface("event", e).Msg("Event details") 167 | 168 | infoData := bus.ProviderInstalledVersionPayload{} 169 | 170 | if k3s := utils.K3sBin(); k3s != "" { 171 | infoData.Provider = K3s 172 | infoData.Version = k3sVersion(l) 173 | 174 | } 175 | if k0s := utils.K0sBin(); k0s != "" { 176 | infoData.Provider = K0s 177 | infoData.Version = k0sVersion(l) 178 | } 179 | 180 | // This is the returned data for the info event 181 | jsondata, err := json.Marshal(infoData) 182 | if err != nil { 183 | l.Logger.Error().Err(err).Msg("Failed to marshal info data") 184 | return pluggable.EventResponse{ 185 | State: bus.EventResponseError, 186 | Data: "", 187 | Error: err.Error(), 188 | } 189 | } 190 | // If no provider was found, we return an empty response with a not applicable state 191 | if infoData.Provider == "" { 192 | l.Logger.Info().Msg("No provider found, returning not applicable state") 193 | return pluggable.EventResponse{ 194 | State: bus.EventResponseNotApplicable, 195 | Data: "", 196 | Error: "", 197 | } 198 | } 199 | data := pluggable.EventResponse{ 200 | State: bus.EventResponseSuccess, 201 | Data: string(jsondata), 202 | Error: "", 203 | } 204 | 205 | l.Logger.Debug().Msg("Returning response for info event") 206 | l.Logger.Debug().Interface("response", data).Msg("Response details") 207 | 208 | return data 209 | } 210 | 211 | // k3sVersion retrieves the version of k3s installed on the system. 212 | func k3sVersion(logger types.KairosLogger) string { 213 | out, err := exec.Command(utils.K3sBin(), "--version").CombinedOutput() 214 | if err != nil { 215 | logger.Logger.Error().Msgf("Failed to get the k3s version: %s", err) 216 | return "" 217 | } 218 | // 2 lines in this format: 219 | // k3s version v1.21.4+k3s1 (3781f4b7) 220 | // go version go1.16.5 221 | // We need the first line 222 | re := regexp.MustCompile(`k3s version (v\d+\.\d+\.\d+\+k3s\d+)`) 223 | if re.MatchString(string(out)) { 224 | match := re.FindStringSubmatch(string(out)) 225 | return match[1] 226 | } 227 | logger.Logger.Error().Msgf("Failed to parse the k3s version: %s", string(out)) 228 | return "" 229 | } 230 | 231 | // k0sVersion retrieves the version of k0s installed on the system. 232 | func k0sVersion(logger types.KairosLogger) string { 233 | out, err := exec.Command(utils.K0sBin(), "version").CombinedOutput() 234 | if err != nil { 235 | logger.Logger.Error().Msgf("Failed to get the k0s version: %s", err) 236 | return "" 237 | } 238 | 239 | return strings.TrimSpace(string(out)) 240 | } 241 | -------------------------------------------------------------------------------- /internal/role/p2p/k0s.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "os" 7 | "strings" 8 | 9 | "github.com/kairos-io/kairos-sdk/machine" 10 | "github.com/kairos-io/kairos-sdk/utils" 11 | providerConfig "github.com/kairos-io/provider-kairos/v2/internal/provider/config" 12 | service "github.com/mudler/edgevpn/api/client/service" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | const ( 17 | K0sDistroName = "k0s" 18 | K0sMasterName = "controller" 19 | K0sWorkerName = "worker" 20 | K0sMasterServiceName = "k0scontroller" 21 | K0sWorkerServiceName = "k0sworker" 22 | ) 23 | 24 | type K0sNode struct { 25 | providerConfig *providerConfig.Config 26 | roleConfig *service.RoleConfig 27 | ip string 28 | role string 29 | } 30 | 31 | func (k *K0sNode) IsWorker() bool { 32 | return k.role == RoleWorker 33 | } 34 | 35 | func (k *K0sNode) K8sBin() string { 36 | return utils.K0sBin() 37 | } 38 | 39 | func (k *K0sNode) DeployKubeVIP() error { 40 | pconfig := k.ProviderConfig() 41 | if pconfig.KubeVIP.IsEnabled() { 42 | return errors.New("KubeVIP is not yet supported with k0s") 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (k *K0sNode) GenArgs() ([]string, error) { 49 | var args []string 50 | 51 | // Generate a new k0s config 52 | _, err := utils.SH("k0s config create > /etc/k0s/k0s.yaml") 53 | if err != nil { 54 | return args, err 55 | } 56 | args = append(args, "--config /etc/k0s/k0s.yaml") 57 | 58 | data, err := os.ReadFile("/etc/k0s/k0s.yaml") 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | var k0sConfig map[any]any 64 | err = yaml.Unmarshal(data, &k0sConfig) 65 | if err != nil { 66 | return args, err 67 | } 68 | 69 | // check if the k0s config has an api address 70 | spec, ok := k0sConfig["spec"].(map[any]any) 71 | if !ok { 72 | return args, errors.New("k0s config does not have a spec") 73 | } 74 | api, ok := spec["api"].(map[any]any) 75 | if !ok { 76 | return args, errors.New("k0s config does not have an api") 77 | } 78 | // by default k0s uses the first IP address of the machine as the api address, but we want to use the edgevpn IP 79 | api["address"] = k.IP() 80 | 81 | spec["api"] = api 82 | 83 | network, ok := spec["network"].(map[any]any) 84 | if !ok { 85 | return args, errors.New("k0s config does not have a network") 86 | } 87 | kubeRouter, ok := network["kuberouter"].(map[any]any) 88 | if !ok { 89 | return args, errors.New("k0s config does not have a kuberouter") 90 | } 91 | 92 | // by default k0s uses the port 8080 for the metrics but this conflicts with the edgevpn API port 93 | kubeRouter["metricsPort"] = 9090 94 | network["kuberouter"] = kubeRouter 95 | spec["network"] = network 96 | 97 | storage, ok := spec["storage"].(map[any]any) 98 | if !ok { 99 | return args, errors.New("k0s config does not have a storage") 100 | } 101 | etcd, ok := storage["etcd"].(map[any]any) 102 | if !ok { 103 | return args, errors.New("k0s config does not have a etcd") 104 | } 105 | // just like the api address, we want to use the edgevpn IP for the etcd peer address 106 | etcd["peerAddress"] = k.IP() 107 | 108 | storage["etcd"] = etcd 109 | spec["storage"] = storage 110 | 111 | k0sConfig["spec"] = spec 112 | 113 | // write the k0s config back to the file 114 | data, err = yaml.Marshal(k0sConfig) 115 | if err != nil { 116 | return args, err 117 | } 118 | err = os.WriteFile("/etc/k0s/k0s.yaml", data, 0644) 119 | if err != nil { 120 | return args, err 121 | } 122 | 123 | pconfig := k.ProviderConfig() 124 | if !pconfig.P2P.UseVPNWithKubernetes() { 125 | return args, errors.New("having a VPN but not using it for Kubernetes is not yet supported with k0s") 126 | } 127 | 128 | if pconfig.KubeVIP.IsEnabled() { 129 | return args, errors.New("KubeVIP is not yet supported with k0s") 130 | } 131 | 132 | if pconfig.P2P.Auto.HA.ExternalDB != "" { 133 | return args, errors.New("ExternalDB is not yet supported with k0s") 134 | } 135 | 136 | if k.HA() && !k.ClusterInit() { 137 | args = append(args, "--token-file /etc/k0s/token") 138 | } 139 | 140 | // when we start implementing this functionality, remember to use 141 | // AppendArgs, and not just return the args here, this is because the 142 | // function understands if it needs to append or replace the args 143 | 144 | return args, nil 145 | } 146 | 147 | func (k *K0sNode) EnvUnit() string { 148 | return machine.K0sEnvUnit("k0scontroller") 149 | } 150 | 151 | func (k *K0sNode) Service() (machine.Service, error) { 152 | if k.IsWorker() { 153 | return machine.K0sWorker() 154 | } 155 | 156 | return machine.K0s() 157 | } 158 | 159 | func (k *K0sNode) Token() (string, error) { 160 | if k.IsWorker() { 161 | return k.RoleConfig().Client.Get("workertoken", "token") 162 | } 163 | 164 | return k.RoleConfig().Client.Get("controllertoken", "token") 165 | } 166 | 167 | func (k *K0sNode) GenerateEnv() (env map[string]string) { 168 | env = make(map[string]string) 169 | 170 | if k.HA() && !k.ClusterInit() { 171 | nodeToken, _ := k.Token() 172 | env["K0S_TOKEN"] = nodeToken 173 | } 174 | 175 | pConfig := k.ProviderConfig() 176 | 177 | if pConfig.K0s.ReplaceEnv { 178 | env = pConfig.K0s.Env 179 | } else { 180 | // Override opts with user-supplied 181 | for k, v := range pConfig.K0s.Env { 182 | env[k] = v 183 | } 184 | } 185 | 186 | return env 187 | } 188 | 189 | func (k *K0sNode) ProviderConfig() *providerConfig.Config { 190 | return k.providerConfig 191 | } 192 | 193 | func (k *K0sNode) SetRoleConfig(c *service.RoleConfig) { 194 | k.roleConfig = c 195 | } 196 | 197 | func (k *K0sNode) RoleConfig() *service.RoleConfig { 198 | return k.roleConfig 199 | } 200 | 201 | func (k *K0sNode) HA() bool { 202 | return k.role == RoleMasterHA 203 | } 204 | 205 | func (k *K0sNode) ClusterInit() bool { 206 | // k0s does not have a cluster init role like k3s. Instead we should have a way to set in the config 207 | // if the user wants a single node cluster, multi-node cluster, or HA cluster 208 | return false 209 | } 210 | 211 | func (k *K0sNode) IP() string { 212 | return k.ip 213 | } 214 | 215 | func (k *K0sNode) PropagateData() error { 216 | c := k.RoleConfig() 217 | controllerToken, err := utils.SH("k0s token create --role=controller") //nolint:errcheck 218 | if err != nil { 219 | c.Logger.Errorf("failed to create controller token: %s", err) 220 | } 221 | 222 | // we don't want to set the output if there is an error 223 | if err == nil && controllerToken != "" { 224 | err := c.Client.Set("controllertoken", "token", strings.TrimSuffix(controllerToken, "\n")) 225 | if err != nil { 226 | c.Logger.Error(err) 227 | } 228 | } 229 | 230 | workerToken, err := utils.SH("k0s token create --role=worker") //nolint:errcheck 231 | if err != nil { 232 | c.Logger.Errorf("failed to create worker token: %s", err) 233 | } 234 | // we don't want to set the output if there is an error 235 | if err == nil && workerToken != "" { 236 | err := c.Client.Set("workertoken", "token", strings.TrimSuffix(workerToken, "\n")) 237 | if err != nil { 238 | c.Logger.Error(err) 239 | } 240 | } 241 | 242 | kubeconfig, err := utils.SH("k0s config create") //nolint:errcheck 243 | if err != nil { 244 | c.Logger.Error(err) 245 | return err 246 | } 247 | if kubeconfig != "" { 248 | err := c.Client.Set("kubeconfig", "master", base64.RawURLEncoding.EncodeToString([]byte(kubeconfig))) 249 | if err != nil { 250 | c.Logger.Error(err) 251 | } 252 | } 253 | 254 | return nil 255 | } 256 | 257 | func (k *K0sNode) WorkerArgs() ([]string, error) { 258 | pconfig := k.ProviderConfig() 259 | k0sConfig := pconfig.K0sWorker 260 | args := []string{"--token-file /etc/k0s/token"} 261 | 262 | if k0sConfig.ReplaceArgs { 263 | args = k0sConfig.Args 264 | } else { 265 | args = append(args, k0sConfig.Args...) 266 | } 267 | 268 | return args, nil 269 | } 270 | 271 | func (k *K0sNode) SetupWorker(_, nodeToken string) error { 272 | if err := os.WriteFile("/etc/k0s/token", []byte(nodeToken), 0644); err != nil { 273 | return err 274 | } 275 | 276 | return nil 277 | } 278 | 279 | func (k *K0sNode) Role() string { 280 | if k.IsWorker() { 281 | return K0sWorkerName 282 | } 283 | 284 | return K0sMasterName 285 | } 286 | 287 | func (k *K0sNode) ServiceName() string { 288 | if k.IsWorker() { 289 | return K0sWorkerServiceName 290 | } 291 | 292 | return K0sMasterServiceName 293 | } 294 | 295 | func (k *K0sNode) Env() map[string]string { 296 | c := k.ProviderConfig() 297 | if k.IsWorker() { 298 | return c.K0sWorker.Env 299 | } 300 | 301 | return c.K0s.Env 302 | } 303 | 304 | func (k *K0sNode) Args() []string { 305 | c := k.ProviderConfig() 306 | if !c.K0sWorker.IsEnabled() && !c.K0s.IsEnabled() { 307 | return []string{} 308 | } 309 | 310 | if k.IsWorker() { 311 | return c.K0sWorker.Args 312 | } 313 | 314 | return c.K0s.Args 315 | } 316 | 317 | func (k *K0sNode) EnvFile() string { 318 | return machine.K0sEnvUnit(k.ServiceName()) 319 | } 320 | 321 | func (k *K0sNode) SetRole(role string) { 322 | k.role = role 323 | } 324 | 325 | func (k *K0sNode) SetIP(ip string) { 326 | k.ip = ip 327 | } 328 | 329 | func (k *K0sNode) GuessInterface() { 330 | // not used in k0s 331 | } 332 | 333 | func (k *K0sNode) Distro() string { 334 | return K0sDistroName 335 | } 336 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kairos-io/provider-kairos/v2 2 | 3 | go 1.25.0 4 | 5 | replace github.com/elastic/gosigar => github.com/mudler/gosigar v0.14.3-0.20220502202347-34be910bdaaf 6 | 7 | require ( 8 | github.com/creack/pty v1.1.24 9 | github.com/gliderlabs/ssh v0.3.8 10 | github.com/ipfs/go-log/v2 v2.9.0 11 | github.com/kairos-io/go-nodepair v0.3.0 12 | github.com/kairos-io/kairos-agent/v2 v2.25.1 13 | github.com/kairos-io/kairos-sdk v0.13.0 14 | github.com/kube-vip/kube-vip v1.0.0 15 | github.com/mudler/edgevpn v0.31.1 16 | github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5 17 | github.com/mudler/go-processmanager v0.0.0-20240820160718-8b802d3ecf82 18 | github.com/onsi/ginkgo/v2 v2.27.3 19 | github.com/onsi/gomega v1.38.3 20 | github.com/pterm/pterm v0.12.82 21 | github.com/samber/lo v1.52.0 22 | github.com/urfave/cli/v2 v2.27.7 23 | gopkg.in/yaml.v3 v3.0.1 24 | ) 25 | 26 | require ( 27 | atomicgo.dev/cursor v0.2.0 // indirect 28 | atomicgo.dev/keyboard v0.2.9 // indirect 29 | atomicgo.dev/schedule v0.1.0 // indirect 30 | dario.cat/mergo v1.0.1 // indirect 31 | github.com/Masterminds/goutils v1.1.1 // indirect 32 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 33 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 34 | github.com/Microsoft/go-winio v0.6.2 // indirect 35 | github.com/Microsoft/hcsshim v0.13.0 // indirect 36 | github.com/ProtonMail/go-crypto v1.1.5 // indirect 37 | github.com/anchore/go-lzo v0.1.0 // indirect 38 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 39 | github.com/avast/retry-go v3.0.0+incompatible // indirect 40 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect 41 | github.com/benbjohnson/clock v1.3.5 // indirect 42 | github.com/beorn7/perks v1.0.1 // indirect 43 | github.com/c-robinson/iplib v1.0.8 // indirect 44 | github.com/cavaliergopher/grab v2.0.0+incompatible // indirect 45 | github.com/cavaliergopher/grab/v3 v3.0.1 // indirect 46 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 47 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 48 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 49 | github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 // indirect 50 | github.com/cloudflare/circl v1.6.1 // indirect 51 | github.com/containerd/cgroups/v3 v3.0.5 // indirect 52 | github.com/containerd/console v1.0.5 // indirect 53 | github.com/containerd/containerd v1.7.29 // indirect 54 | github.com/containerd/continuity v0.4.5 // indirect 55 | github.com/containerd/errdefs v1.0.0 // indirect 56 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 57 | github.com/containerd/log v0.1.0 // indirect 58 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 59 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 60 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 61 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 62 | github.com/creachadair/otp v0.5.0 // indirect 63 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 64 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 65 | github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect 66 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 67 | github.com/denisbrodbeck/machineid v1.0.1 // indirect 68 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect 69 | github.com/disintegration/imaging v1.6.2 // indirect 70 | github.com/diskfs/go-diskfs v1.7.0 // indirect 71 | github.com/distribution/reference v0.6.0 // indirect 72 | github.com/djherbis/times v1.6.0 // indirect 73 | github.com/docker/cli v28.2.2+incompatible // indirect 74 | github.com/docker/distribution v2.8.3+incompatible // indirect 75 | github.com/docker/docker v28.3.3+incompatible // indirect 76 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 77 | github.com/docker/go-connections v0.5.0 // indirect 78 | github.com/docker/go-units v0.5.0 // indirect 79 | github.com/eapache/channels v1.1.0 // indirect 80 | github.com/eapache/queue v1.1.0 // indirect 81 | github.com/edsrzf/mmap-go v1.2.0 // indirect 82 | github.com/eliukblau/pixterm v1.3.2 // indirect 83 | github.com/elliotwutingfeng/asciiset v0.0.0-20250912055424-93680c478db2 // indirect 84 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 85 | github.com/emirpasic/gods v1.18.1 // indirect 86 | github.com/felixge/httpsnoop v1.0.4 // indirect 87 | github.com/florianl/go-conntrack v0.4.0 // indirect 88 | github.com/flynn/noise v1.1.0 // indirect 89 | github.com/foxboron/go-uefi v0.0.0-20250207204325-69fb7dba244f // indirect 90 | github.com/francoispqt/gojay v1.2.13 // indirect 91 | github.com/fsnotify/fsnotify v1.9.0 // indirect 92 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 93 | github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 // indirect 94 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 95 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 96 | github.com/go-git/go-git/v5 v5.14.0 // indirect 97 | github.com/go-logr/logr v1.4.3 // indirect 98 | github.com/go-logr/stdr v1.2.2 // indirect 99 | github.com/go-ole/go-ole v1.2.6 // indirect 100 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 101 | github.com/go-openapi/jsonreference v0.21.0 // indirect 102 | github.com/go-openapi/swag v0.23.0 // indirect 103 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 104 | github.com/gofrs/flock v0.12.1 // indirect 105 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 106 | github.com/gogo/protobuf v1.3.2 // indirect 107 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 108 | github.com/google/btree v1.1.3 // indirect 109 | github.com/google/gnostic-models v0.6.9 // indirect 110 | github.com/google/go-cmp v0.7.0 // indirect 111 | github.com/google/go-containerregistry v0.20.6 // indirect 112 | github.com/google/gopacket v1.1.19 // indirect 113 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect 114 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 115 | github.com/google/uuid v1.6.0 // indirect 116 | github.com/gookit/color v1.5.4 // indirect 117 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 118 | github.com/hashicorp/errwrap v1.1.0 // indirect 119 | github.com/hashicorp/go-multierror v1.1.1 // indirect 120 | github.com/hashicorp/golang-lru v1.0.2 // indirect 121 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 122 | github.com/hashicorp/hcl v1.0.0 // indirect 123 | github.com/huandu/xstrings v1.5.0 // indirect 124 | github.com/huin/goupnp v1.3.0 // indirect 125 | github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect 126 | github.com/ipfs/boxo v0.30.0 // indirect 127 | github.com/ipfs/go-cid v0.5.0 // indirect 128 | github.com/ipfs/go-datastore v0.8.2 // indirect 129 | github.com/ipfs/go-log v1.0.5 // indirect 130 | github.com/ipld/go-ipld-prime v0.21.0 // indirect 131 | github.com/itchyny/gojq v0.12.17 // indirect 132 | github.com/itchyny/timefmt-go v0.1.6 // indirect 133 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 134 | github.com/jaypipes/ghw v0.19.1 // indirect 135 | github.com/jaypipes/pcidb v1.1.1 // indirect 136 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 137 | github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect 138 | github.com/jezek/xgb v1.1.0 // indirect 139 | github.com/joho/godotenv v1.5.1 // indirect 140 | github.com/josharian/intern v1.0.0 // indirect 141 | github.com/josharian/native v1.1.0 // indirect 142 | github.com/jpillora/backoff v1.0.0 // indirect 143 | github.com/json-iterator/go v1.1.12 // indirect 144 | github.com/k-sone/critbitgo v1.4.0 // indirect 145 | github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 // indirect 146 | github.com/kendru/darwin/go/depgraph v0.0.0-20230809052043-4d1c7e9d1767 // indirect 147 | github.com/kevinburke/ssh_config v1.2.0 // indirect 148 | github.com/klauspost/compress v1.18.0 // indirect 149 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 150 | github.com/koron/go-ssdp v0.0.6 // indirect 151 | github.com/labstack/echo/v4 v4.13.4 // indirect 152 | github.com/labstack/gommon v0.4.2 // indirect 153 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 154 | github.com/libp2p/go-cidranger v1.1.0 // indirect 155 | github.com/libp2p/go-flow-metrics v0.2.0 // indirect 156 | github.com/libp2p/go-libp2p v0.43.0 // indirect 157 | github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect 158 | github.com/libp2p/go-libp2p-kad-dht v0.33.1 // indirect 159 | github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect 160 | github.com/libp2p/go-libp2p-pubsub v0.14.2 // indirect 161 | github.com/libp2p/go-libp2p-record v0.3.1 // indirect 162 | github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect 163 | github.com/libp2p/go-msgio v0.3.0 // indirect 164 | github.com/libp2p/go-netroute v0.2.2 // indirect 165 | github.com/libp2p/go-reuseport v0.4.0 // indirect 166 | github.com/libp2p/go-yamux/v5 v5.0.1 // indirect 167 | github.com/libp2p/zeroconf/v2 v2.2.0 // indirect 168 | github.com/lithammer/fuzzysearch v1.1.8 // indirect 169 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 170 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 171 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect 172 | github.com/magiconair/properties v1.8.9 // indirect 173 | github.com/mailru/easyjson v0.9.0 // indirect 174 | github.com/makiuchi-d/gozxing v0.1.1 // indirect 175 | github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect 176 | github.com/mattn/go-colorable v0.1.14 // indirect 177 | github.com/mattn/go-isatty v0.0.20 // indirect 178 | github.com/mattn/go-runewidth v0.0.16 // indirect 179 | github.com/mauromorales/xpasswd v0.4.1 // indirect 180 | github.com/mdlayher/ndp v1.1.0 // indirect 181 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect 182 | github.com/mdlayher/packet v1.1.2 // indirect 183 | github.com/mdlayher/socket v0.5.1 // indirect 184 | github.com/miekg/dns v1.1.66 // indirect 185 | github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect 186 | github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect 187 | github.com/minio/sha256-simd v1.0.1 // indirect 188 | github.com/mitchellh/copystructure v1.2.0 // indirect 189 | github.com/mitchellh/go-homedir v1.1.0 // indirect 190 | github.com/mitchellh/mapstructure v1.5.0 // indirect 191 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 192 | github.com/moby/docker-image-spec v1.3.1 // indirect 193 | github.com/moby/sys/mountinfo v0.7.2 // indirect 194 | github.com/moby/sys/sequential v0.6.0 // indirect 195 | github.com/moby/sys/userns v0.1.0 // indirect 196 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 197 | github.com/modern-go/reflect2 v1.0.2 // indirect 198 | github.com/mr-tron/base58 v1.2.0 // indirect 199 | github.com/mudler/entities v0.8.2 // indirect 200 | github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 // indirect 201 | github.com/mudler/yip v1.18.1 // indirect 202 | github.com/multiformats/go-base32 v0.1.0 // indirect 203 | github.com/multiformats/go-base36 v0.2.0 // indirect 204 | github.com/multiformats/go-multiaddr v0.16.0 // indirect 205 | github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect 206 | github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect 207 | github.com/multiformats/go-multibase v0.2.0 // indirect 208 | github.com/multiformats/go-multicodec v0.9.1 // indirect 209 | github.com/multiformats/go-multihash v0.2.3 // indirect 210 | github.com/multiformats/go-multistream v0.6.1 // indirect 211 | github.com/multiformats/go-varint v0.0.7 // indirect 212 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 213 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 214 | github.com/opencontainers/go-digest v1.0.0 // indirect 215 | github.com/opencontainers/image-spec v1.1.1 // indirect 216 | github.com/opentracing/opentracing-go v1.2.0 // indirect 217 | github.com/osrg/gobgp/v3 v3.37.0 // indirect 218 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 219 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 220 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 221 | github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect 222 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 223 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 224 | github.com/pion/datachannel v1.5.10 // indirect 225 | github.com/pion/dtls/v2 v2.2.12 // indirect 226 | github.com/pion/dtls/v3 v3.0.6 // indirect 227 | github.com/pion/ice/v4 v4.0.10 // indirect 228 | github.com/pion/interceptor v0.1.40 // indirect 229 | github.com/pion/logging v0.2.3 // indirect 230 | github.com/pion/mdns/v2 v2.0.7 // indirect 231 | github.com/pion/randutil v0.1.0 // indirect 232 | github.com/pion/rtcp v1.2.15 // indirect 233 | github.com/pion/rtp v1.8.19 // indirect 234 | github.com/pion/sctp v1.8.39 // indirect 235 | github.com/pion/sdp/v3 v3.0.13 // indirect 236 | github.com/pion/srtp/v3 v3.0.6 // indirect 237 | github.com/pion/stun v0.6.1 // indirect 238 | github.com/pion/stun/v3 v3.0.0 // indirect 239 | github.com/pion/transport/v2 v2.2.10 // indirect 240 | github.com/pion/transport/v3 v3.0.7 // indirect 241 | github.com/pion/turn/v4 v4.0.2 // indirect 242 | github.com/pion/webrtc/v4 v4.1.2 // indirect 243 | github.com/pjbgf/sha1cd v0.3.2 // indirect 244 | github.com/pkg/errors v0.9.1 // indirect 245 | github.com/pkg/xattr v0.4.12 // indirect 246 | github.com/polydawn/refmt v0.89.0 // indirect 247 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 248 | github.com/prometheus/client_golang v1.22.0 // indirect 249 | github.com/prometheus/client_model v0.6.2 // indirect 250 | github.com/prometheus/common v0.64.0 // indirect 251 | github.com/prometheus/procfs v0.16.1 // indirect 252 | github.com/qeesung/image2ascii v1.0.1 // indirect 253 | github.com/quic-go/qpack v0.5.1 // indirect 254 | github.com/quic-go/quic-go v0.54.0 // indirect 255 | github.com/quic-go/webtransport-go v0.9.0 // indirect 256 | github.com/rivo/uniseg v0.4.7 // indirect 257 | github.com/rs/zerolog v1.34.0 // indirect 258 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 259 | github.com/saferwall/pe v1.5.7 // indirect 260 | github.com/sagikazarmark/locafero v0.6.0 // indirect 261 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 262 | github.com/sanity-io/litter v1.5.8 // indirect 263 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect 264 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 265 | github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect 266 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 267 | github.com/shirou/gopsutil/v4 v4.24.7 // indirect 268 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 269 | github.com/shopspring/decimal v1.4.0 // indirect 270 | github.com/sirupsen/logrus v1.9.4-0.20250804143300-cb253f3080f1 // indirect 271 | github.com/skeema/knownhosts v1.3.1 // indirect 272 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect 273 | github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 // indirect 274 | github.com/sourcegraph/conc v0.3.0 // indirect 275 | github.com/spaolacci/murmur3 v1.1.0 // indirect 276 | github.com/spectrocloud-labs/herd v0.4.2 // indirect 277 | github.com/spf13/afero v1.11.0 // indirect 278 | github.com/spf13/cast v1.7.1 // indirect 279 | github.com/spf13/pflag v1.0.6 // indirect 280 | github.com/spf13/viper v1.19.0 // indirect 281 | github.com/subosito/gotenv v1.6.0 // indirect 282 | github.com/swaggest/jsonschema-go v0.3.62 // indirect 283 | github.com/swaggest/refl v1.3.0 // indirect 284 | github.com/tklauser/go-sysconf v0.3.12 // indirect 285 | github.com/tklauser/numcpus v0.6.1 // indirect 286 | github.com/tredoe/osutil v1.5.0 // indirect 287 | github.com/twpayne/go-vfs/v4 v4.3.0 // indirect 288 | github.com/twpayne/go-vfs/v5 v5.0.5 // indirect 289 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect 290 | github.com/ulikunitz/xz v0.5.15 // indirect 291 | github.com/valyala/bytebufferpool v1.0.0 // indirect 292 | github.com/valyala/fasttemplate v1.2.2 // indirect 293 | github.com/vbatts/tar-split v0.12.1 // indirect 294 | github.com/vishvananda/netlink v1.3.1 // indirect 295 | github.com/vishvananda/netns v0.0.5 // indirect 296 | github.com/vmware/vmw-guestinfo v0.0.0-20220317130741-510905f0efa3 // indirect 297 | github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect 298 | github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect 299 | github.com/wlynxg/anet v0.0.5 // indirect 300 | github.com/x448/float16 v0.8.4 // indirect 301 | github.com/xanzy/ssh-agent v0.3.3 // indirect 302 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 303 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 304 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 305 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 306 | github.com/zcalusic/sysinfo v1.1.3 // indirect 307 | go.opencensus.io v0.24.0 // indirect 308 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 309 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 310 | go.opentelemetry.io/otel v1.37.0 // indirect 311 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 312 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 313 | go.uber.org/automaxprocs v1.6.0 // indirect 314 | go.uber.org/dig v1.19.0 // indirect 315 | go.uber.org/fx v1.24.0 // indirect 316 | go.uber.org/mock v0.5.2 // indirect 317 | go.uber.org/multierr v1.11.0 // indirect 318 | go.uber.org/zap v1.27.0 // indirect 319 | go.yaml.in/yaml/v3 v3.0.4 // indirect 320 | golang.org/x/crypto v0.43.0 // indirect 321 | golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect 322 | golang.org/x/image v0.25.0 // indirect 323 | golang.org/x/mod v0.28.0 // indirect 324 | golang.org/x/net v0.46.0 // indirect 325 | golang.org/x/oauth2 v0.32.0 // indirect 326 | golang.org/x/sync v0.17.0 // indirect 327 | golang.org/x/sys v0.37.0 // indirect 328 | golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect 329 | golang.org/x/term v0.36.0 // indirect 330 | golang.org/x/text v0.30.0 // indirect 331 | golang.org/x/time v0.12.0 // indirect 332 | golang.org/x/tools v0.37.0 // indirect 333 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 334 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 335 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect 336 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 337 | gonum.org/v1/gonum v0.16.0 // indirect 338 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect 339 | google.golang.org/grpc v1.73.0 // indirect 340 | google.golang.org/protobuf v1.36.7 // indirect 341 | gopkg.in/inf.v0 v0.9.1 // indirect 342 | gopkg.in/ini.v1 v1.67.0 // indirect 343 | gopkg.in/warnings.v0 v0.1.2 // indirect 344 | gopkg.in/yaml.v2 v2.4.0 // indirect 345 | howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect 346 | k8s.io/api v0.33.3 // indirect 347 | k8s.io/apimachinery v0.33.3 // indirect 348 | k8s.io/client-go v0.33.3 // indirect 349 | k8s.io/klog/v2 v2.130.1 // indirect 350 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 351 | k8s.io/mount-utils v0.34.1 // indirect 352 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 353 | lukechampine.com/blake3 v1.4.1 // indirect 354 | pault.ag/go/modprobe v0.2.0 // indirect 355 | pault.ag/go/topsort v0.1.1 // indirect 356 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 357 | sigs.k8s.io/randfill v1.0.0 // indirect 358 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 359 | sigs.k8s.io/yaml v1.4.0 // indirect 360 | ) 361 | --------------------------------------------------------------------------------