diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 49fbc91..bd8c68b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,18 +13,43 @@ builds: main: ./ id: "kairos-cli" binary: "kairos-cli" + - ldflags: + - -w -s + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + goarch: + - amd64 + - 386 + main: ./cli/kairosctl + id: "kairosctl" + binary: "kairosctl" source: + rlcp: true enabled: true name_template: 'kairos-cli-{{ .Tag }}-source' archives: # Default template uses underscores instead of - - - name_template: "kairos-cli-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 + - name_template: >- + kairos-cli- + {{ .Tag }}- + {{ .Os }}- + {{ .Arch }} + {{ if .Arm }}v{{ .Arm }}{{ end }} + id: kairos-id + builds: + - "kairos-id" + - name_template: >- + kairosctl- + {{ .Tag }}- + {{ .Os }}- + {{ .Arch }} + {{ if .Arm }}v{{ .Arm }}{{ end }} + id: kairosctl + builds: + - "kairosctl" checksum: name_template: 'kairos-cli-{{ .Tag }}-checksums.txt' snapshot: diff --git a/Earthfile b/Earthfile index 6994e81..1465b68 100644 --- a/Earthfile +++ b/Earthfile @@ -94,8 +94,13 @@ build-kairos-agent-provider: FROM +go-deps DO +BUILD_GOLANG --BIN=agent-provider-kairos --SRC=./ --CGO_ENABLED=$CGO_ENABLED +build-kairosctl: + FROM +go-deps + DO +BUILD_GOLANG --BIN=kairosctl --SRC=./cli/kairosctl --CGO_ENABLED=$CGO_ENABLED + build: BUILD +build-kairos-agent-provider + BUILD +build-kairosctl version: FROM alpine diff --git a/cli/kairosctl/main.go b/cli/kairosctl/main.go new file mode 100644 index 0000000..8aee057 --- /dev/null +++ b/cli/kairosctl/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + + "github.com/kairos-io/kairos-sdk/bus" + iCli "github.com/kairos-io/provider-kairos/internal/cli" + "github.com/kairos-io/provider-kairos/internal/provider" + "github.com/urfave/cli/v2" +) + +func checkErr(err error) { + if err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) +} + +func main() { + if len(os.Args) >= 2 && bus.IsEventDefined(os.Args[1]) { + checkErr(provider.Start()) + } + + checkErr(Start()) +} + +func Start() error { + toolName := "kairosctl" + name := toolName + app := &cli.App{ + Name: name, + Version: iCli.VERSION, + Authors: []*cli.Author{ + { + Name: iCli.Author, + }, + }, + Copyright: iCli.Author, + Commands: []*cli.Command{ + iCli.RegisterCMD(toolName), + iCli.BridgeCMD(toolName), + &iCli.GetKubeConfigCMD, + &iCli.RoleCMD, + }, + } + + return app.Run(os.Args) +} diff --git a/internal/cli/bridge.go b/internal/cli/bridge.go index ab277ed..4145447 100644 --- a/internal/cli/bridge.go +++ b/internal/cli/bridge.go @@ -17,6 +17,101 @@ import ( "github.com/urfave/cli/v2" ) +func BridgeCMD(toolName string) *cli.Command { + usage := "Connect to a kairos VPN network" + description := ` + Starts a bridge with a kairos network or a node. + + # With a network + + 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. + + For example: + + $ sudo %s bridge --network-token + + Will start a VPN, which local ip is fixed to 10.1.0.254 (tweakable with --address). + + The API will also be accessible at http://127.0.0.1:8080 + + # With a node + + "%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 + will be created locally to connect to the machine remotely. + + For example: + + $ %s bridge --qr-code-image /path/to/image.png + + 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. + + See also: https://kairos.io/docs/reference/recovery_mode/ + + ` + + if toolName != "kairosctl" { + usage += " (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead)" + description = "\t\tWARNING: This command will be deprecated in the next release. Please use the new kairosctl binary instead.\n" + description + } + + return &cli.Command{ + Name: "bridge", + UsageText: fmt.Sprintf("%s %s", toolName, "bridge --network-token XXX"), + Usage: usage, + Description: fmt.Sprintf(description, toolName, toolName, toolName), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "network-token", + Required: false, + EnvVars: []string{"NETWORK_TOKEN"}, + Usage: "Network token to connect over", + }, + &cli.StringFlag{ + Name: "log-level", + Required: false, + EnvVars: []string{"LOGLEVEL"}, + Value: "info", + Usage: "Bridge log level", + }, + &cli.BoolFlag{ + Name: "qr-code-snapshot", + Required: false, + Usage: "Bool to take a local snapshot instead of reading from an image file for recovery", + EnvVars: []string{"QR_CODE_SNAPSHOT"}, + }, + &cli.StringFlag{ + Name: "qr-code-image", + Usage: "Path to an image containing a valid QR code for recovery mode", + Required: false, + EnvVars: []string{"QR_CODE_IMAGE"}, + }, + &cli.StringFlag{ + Name: "api", + Value: "127.0.0.1:8080", + Usage: "Listening API url", + }, + &cli.BoolFlag{ + Name: "dhcp", + EnvVars: []string{"DHCP"}, + Usage: "Enable DHCP", + }, + &cli.StringFlag{ + Value: "10.1.0.254/24", + Name: "address", + EnvVars: []string{"ADDRESS"}, + Usage: "Specify an address for the bridge", + }, + &cli.StringFlag{ + Value: "/tmp/kairos", + Name: "lease-dir", + EnvVars: []string{"lease-dir"}, + Usage: "DHCP Lease directory", + }, + }, + Action: bridge, + } +} + // bridge is just starting a VPN with edgevpn to the given network token. func bridge(c *cli.Context) error { qrCodePath := "" diff --git a/internal/cli/get_kubeconfig.go b/internal/cli/get_kubeconfig.go new file mode 100644 index 0000000..0fde726 --- /dev/null +++ b/internal/cli/get_kubeconfig.go @@ -0,0 +1,37 @@ +package cli + +import ( + "encoding/base64" + "fmt" + "strings" + + edgeVPNClient "github.com/mudler/edgevpn/api/client" + "github.com/mudler/edgevpn/api/client/service" + "github.com/urfave/cli/v2" +) + +var GetKubeConfigCMD = cli.Command{ + Name: "get-kubeconfig", + Usage: "Return a deployment kubeconfig", + UsageText: "Retrieve a kairos network kubeconfig (only for automated deployments)", + Description: ` + Retrieve a network kubeconfig and prints out to screen. + + If a deployment was bootstrapped with a network token, you can use this command to retrieve the master node kubeconfig of a network id. + + For example: + + $ kairos get-kubeconfig --network-id kairos + `, + Flags: networkAPI, + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + str, _ := cc.Get("kubeconfig", "master") + b, _ := base64.RawURLEncoding.DecodeString(str) + masterIP, _ := cc.Get("master", "ip") + fmt.Println(strings.ReplaceAll(string(b), "127.0.0.1", masterIP)) + return nil + }, +} diff --git a/internal/cli/register.go b/internal/cli/register.go index 01d5568..db8922a 100644 --- a/internal/cli/register.go +++ b/internal/cli/register.go @@ -3,13 +3,76 @@ package cli import ( "context" "fmt" - "io/ioutil" // nolint "os" + "github.com/urfave/cli/v2" + nodepair "github.com/mudler/go-nodepair" qr "github.com/mudler/go-nodepair/qrcode" ) +// RegisterCMD is only used temporarily to avoid duplication while the kairosctl sub-command is deprecated. +func RegisterCMD(toolName string) *cli.Command { + subCommandName := "register" + fullName := fmt.Sprintf("%s %s", toolName, subCommandName) + usage := "Registers and bootstraps a node" + description := fmt.Sprintf(` + Bootstraps a node which is started in pairing mode. It can send over a configuration file used to install the kairos node. + + For example: + $ %s --config config.yaml --device /dev/sda ~/Downloads/screenshot.png + + will decode the QR code from ~/Downloads/screenshot.png and bootstrap the node remotely. + + If the image is omitted, a screenshot will be taken and used to decode the QR code. + + See also https://kairos.io/docs/getting-started/ for documentation. + `, fullName) + if toolName != "kairosctl" { + usage += " (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead)" + description = "\t\tWARNING: This command will be deprecated in the next release. Please use the new kairosctl binary to register your nodes.\n" + description + } + + return &cli.Command{ + Name: subCommandName, + UsageText: fmt.Sprintf("%s --reboot --device /dev/sda /image/snapshot.png", fullName), + Usage: usage, + Description: description, + ArgsUsage: "Register optionally accepts an image. If nothing is passed will take a screenshot of the screen and try to decode the QR code", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "Kairos YAML configuration file", + Required: true, + }, + &cli.StringFlag{ + Name: "device", + Usage: "Device used for the installation target", + }, + &cli.BoolFlag{ + Name: "reboot", + Usage: "Reboot node after installation", + }, + &cli.BoolFlag{ + Name: "poweroff", + Usage: "Shutdown node after installation", + }, + &cli.StringFlag{ + Name: "log-level", + Usage: "Set log level", + }, + }, + Action: func(c *cli.Context) error { + var ref string + if c.Args().Len() == 1 { + ref = c.Args().First() + } + + return register(c.String("log-level"), ref, c.String("config"), c.String("device"), c.Bool("reboot"), c.Bool("poweroff")) + }, + } +} + // isDirectory determines if a file represented // by `path` is a directory or not. func isDirectory(path string) (bool, error) { @@ -33,7 +96,7 @@ func isReadable(fileName string) bool { } func register(loglevel, arg, configFile, device string, reboot, poweroff bool) error { - b, _ := ioutil.ReadFile(configFile) + b, _ := os.ReadFile(configFile) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/internal/cli/role.go b/internal/cli/role.go new file mode 100644 index 0000000..bb956f3 --- /dev/null +++ b/internal/cli/role.go @@ -0,0 +1,55 @@ +package cli + +import ( + "fmt" + + edgeVPNClient "github.com/mudler/edgevpn/api/client" + "github.com/mudler/edgevpn/api/client/service" + "github.com/urfave/cli/v2" +) + +var RoleCMD = cli.Command{ + Name: "role", + Usage: "Set or list node roles", + Subcommands: []*cli.Command{ + { + Flags: networkAPI, + Name: "set", + Usage: "Set a node role", + UsageText: "kairos role set master", + Description: ` + Sets a node role propagating the setting to the network. + + A role must be set prior to the node joining a network. You can retrieve a node UUID by running "kairos uuid". + + Example: + + $ (node A) kairos uuid + $ (node B) kairos role set master + `, + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + return cc.Set("role", c.Args().Get(0), c.Args().Get(1)) + }, + }, + { + Flags: networkAPI, + Name: "list", + Description: "List node roles", + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + advertizing, _ := cc.AdvertizingNodes() + fmt.Println("Node\tRole") + for _, a := range advertizing { + role, _ := cc.Get("role", a) + fmt.Printf("%s\t%s\n", a, role) + } + return nil + }, + }, + }, +} diff --git a/internal/cli/start.go b/internal/cli/start.go index c761d96..3bc66f4 100644 --- a/internal/cli/start.go +++ b/internal/cli/start.go @@ -1,19 +1,14 @@ package cli import ( - "encoding/base64" "fmt" "os" "strconv" - "strings" - - edgeVPNClient "github.com/mudler/edgevpn/api/client" providerConfig "github.com/kairos-io/provider-kairos/internal/provider/config" "github.com/urfave/cli/v2" "gopkg.in/yaml.v1" - "github.com/mudler/edgevpn/api/client/service" "github.com/mudler/edgevpn/pkg/node" ) @@ -21,6 +16,7 @@ import ( // -X 'github.com/kairos-io/provider-kairos/internal/cli.VERSION=$VERSION' // see Earthlfile. var VERSION = "0.0.0" +var Author = "Ettore Di Giacinto" var networkAPI = []cli.Flag{ &cli.StringFlag{ @@ -38,18 +34,19 @@ var networkAPI = []cli.Flag{ const recoveryAddr = "127.0.0.1:2222" func Start() error { + toolName := "kairos" app := &cli.App{ - Name: "kairos-agent-provider", + Name: toolName, Version: VERSION, Authors: []*cli.Author{ { - Name: "Ettore Di Giacinto", + Name: Author, }, }, Usage: "kairos CLI to bootstrap, upgrade, connect and manage a kairos network", Description: ` The kairos CLI can be used to manage a kairos box and perform all day-two tasks, like: -- register a node +- register a node (WARNING: this command will be deprecated in the next release, use the kairosctl binary instead) - connect to a node in recovery mode - to establish a VPN connection - set, list roles @@ -57,10 +54,10 @@ The kairos CLI can be used to manage a kairos box and perform all day-two tasks, and much more. -For all the example cases, see: https://docs.kairos.io . +For all the example cases, see: https://kairos.io/docs/ `, UsageText: ``, - Copyright: "Ettore Di Giacinto", + Copyright: Author, Commands: []*cli.Command{ { Name: "recovery-ssh-server", @@ -93,210 +90,10 @@ For all the example cases, see: https://docs.kairos.io . return StartRecoveryService(c.String("token"), c.String("service"), c.String("password"), c.String("listen")) }, }, - { - Name: "register", - UsageText: "register --reboot --device /dev/sda /image/snapshot.png", - Usage: "Registers and bootstraps a node", - Description: ` - Bootstraps a node which is started in pairing mode. It can send over a configuration file used to install the kairos node. - - For example: - $ kairos register --config config.yaml --device /dev/sda ~/Downloads/screenshot.png - - will decode the QR code from ~/Downloads/screenshot.png and bootstrap the node remotely. - - If the image is omitted, a screenshot will be taken and used to decode the QR code. - - See also https://kairos.io/docs/getting-started/ for documentation. - `, - ArgsUsage: "Register optionally accepts an image. If nothing is passed will take a screenshot of the screen and try to decode the QR code", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Usage: "Kairos YAML configuration file", - Required: true, - }, - &cli.StringFlag{ - Name: "device", - Usage: "Device used for the installation target", - }, - &cli.BoolFlag{ - Name: "reboot", - Usage: "Reboot node after installation", - }, - &cli.BoolFlag{ - Name: "poweroff", - Usage: "Shutdown node after installation", - }, - &cli.StringFlag{ - Name: "log-level", - Usage: "Set log level", - }, - }, - - Action: func(c *cli.Context) error { - var ref string - if c.Args().Len() == 1 { - ref = c.Args().First() - } - - return register(c.String("log-level"), ref, c.String("config"), c.String("device"), c.Bool("reboot"), c.Bool("poweroff")) - }, - }, - { - Name: "bridge", - UsageText: "bridge --network-token XXX", - Usage: "Connect to a kairos VPN network", - Description: ` - Starts a bridge with a kairos network or a node. - - # With a network - - 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. - - For example: - - $ sudo kairos bridge --network-token - - Will start a VPN, which local ip is fixed to 10.1.0.254 (tweakable with --address). - - The API will also be accessible at http://127.0.0.1:8080 - - # With a node - - "kairos 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 - will be created locally to connect to the machine remotely. - - For example: - - $ kairos bridge --qr-code-image /path/to/image.png - - 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. - - See also: https://docs.kairos.io/after_install/troubleshooting/#connect-to-the-cluster-network and https://docs.kairos.io/after_install/recovery_mode/ - - `, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "network-token", - Required: false, - EnvVars: []string{"NETWORK_TOKEN"}, - Usage: "Network token to connect over", - }, - &cli.StringFlag{ - Name: "log-level", - Required: false, - EnvVars: []string{"LOGLEVEL"}, - Value: "info", - Usage: "Bridge log level", - }, - &cli.BoolFlag{ - Name: "qr-code-snapshot", - Required: false, - Usage: "Bool to take a local snapshot instead of reading from an image file for recovery", - EnvVars: []string{"QR_CODE_SNAPSHOT"}, - }, - &cli.StringFlag{ - Name: "qr-code-image", - Usage: "Path to an image containing a valid QR code for recovery mode", - Required: false, - EnvVars: []string{"QR_CODE_IMAGE"}, - }, - &cli.StringFlag{ - Name: "api", - Value: "127.0.0.1:8080", - Usage: "Listening API url", - }, - &cli.BoolFlag{ - Name: "dhcp", - EnvVars: []string{"DHCP"}, - Usage: "Enable DHCP", - }, - &cli.StringFlag{ - Value: "10.1.0.254/24", - Name: "address", - EnvVars: []string{"ADDRESS"}, - Usage: "Specify an address for the bridge", - }, - &cli.StringFlag{ - Value: "/tmp/kairos", - Name: "lease-dir", - EnvVars: []string{"lease-dir"}, - Usage: "DHCP Lease directory", - }, - }, - Action: bridge, - }, - { - Name: "get-kubeconfig", - Usage: "Return a deployment kubeconfig", - UsageText: "Retrieve a kairos network kubeconfig (only for automated deployments)", - Description: ` - Retrieve a network kubeconfig and prints out to screen. - - If a deployment was bootstrapped with a network token, you can use this command to retrieve the master node kubeconfig of a network id. - - For example: - - $ kairos get-kubeconfig --network-id kairos - `, - Flags: networkAPI, - Action: func(c *cli.Context) error { - cc := service.NewClient( - c.String("network-id"), - edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) - str, _ := cc.Get("kubeconfig", "master") - b, _ := base64.RawURLEncoding.DecodeString(str) - masterIP, _ := cc.Get("master", "ip") - fmt.Println(strings.ReplaceAll(string(b), "127.0.0.1", masterIP)) - return nil - }, - }, - { - Name: "role", - Usage: "Set or list node roles", - Subcommands: []*cli.Command{ - { - Flags: networkAPI, - Name: "set", - Usage: "Set a node role", - UsageText: "kairos role set master", - Description: ` - Sets a node role propagating the setting to the network. - - A role must be set prior to the node joining a network. You can retrieve a node UUID by running "kairos uuid". - - Example: - - $ (node A) kairos uuid - $ (node B) kairos role set master - `, - Action: func(c *cli.Context) error { - cc := service.NewClient( - c.String("network-id"), - edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) - return cc.Set("role", c.Args().Get(0), c.Args().Get(1)) - }, - }, - { - Flags: networkAPI, - Name: "list", - Description: "List node roles", - Action: func(c *cli.Context) error { - cc := service.NewClient( - c.String("network-id"), - edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) - advertizing, _ := cc.AdvertizingNodes() - fmt.Println("Node\tRole") - for _, a := range advertizing { - role, _ := cc.Get("role", a) - fmt.Printf("%s\t%s\n", a, role) - } - return nil - }, - }, - }, - }, + RegisterCMD(toolName), + BridgeCMD(toolName), + &GetKubeConfigCMD, + &RoleCMD, { Name: "create-config", Aliases: []string{"c"}, diff --git a/tests/register_qrcode_test.go b/tests/register_qrcode_test.go index adf38f2..b4667b2 100644 --- a/tests/register_qrcode_test.go +++ b/tests/register_qrcode_test.go @@ -23,7 +23,7 @@ var _ = Describe("kairos qr code register", Label("qrcode-register"), func() { return err } - out, err = utils.SH(fmt.Sprintf("kairos register --device /dev/sda --config %s %s", os.Getenv("CLOUD_INIT"), "screenshot.png")) + out, err = utils.SH(fmt.Sprintf("kairosctl register--device /dev/sda --config %s %s", os.Getenv("CLOUD_INIT"), "screenshot.png")) fmt.Println(out) return err