Skip to content

Commit 7fc7ec1

Browse files
feat: enable token based node join process (#23)
* feat: enable token based node join process adds a new way of deploying clusters. now we also support deploying one controller node and then from there generate a token to add other nodes manually. this makes helmvm to behave closer to how kurl works. * bug: return error on unsupported installation method * feat: requiring root for localhost as single node deployment * chore: rolling back "only root can applies" policy this commit rolls back the policy that was restricting the "apply" command to be run only by "root" users when deploying a single node cluster using only localhost.
1 parent 48d1f88 commit 7fc7ec1

File tree

11 files changed

+546
-64
lines changed

11 files changed

+546
-64
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,63 @@ $ ./helmvm install --multi-node
3333

3434
In this case, it's not necessary to execute this command exclusively on a Linux x86_64 machine. You have the flexibility to use any architecture for the process.
3535

36+
## Deploying Individual Nodes
37+
38+
HelmVM also facilitates deploying individual nodes through the use of tokens, deviating from the centralized approach.
39+
To follow this path, you need to exclude yourself from the centralized management facilitated via SSH.
40+
41+
### Installing a Multi-Node Setup using Token-Based Deployment
42+
43+
All operations should be executed directly on the Linux servers and require root privileges.
44+
Begin by deploying the first node:
45+
46+
```
47+
server-0# helmvm install
48+
```
49+
50+
After the cluster is online, you can generate a token to enable the addition of other nodes:
51+
52+
```
53+
server-0# helmvm node token create --role controller
54+
INFO[0000] Creating node join token for role controller
55+
WARN[0000] You are opting out of the centralized cluster management.
56+
WARN[0000] Through the centralized management you can manage all your
57+
WARN[0000] cluster nodes from a single location. If you decide to move
58+
WARN[0000] on the centralized management won't be available anymore
59+
? Do you want to use continue ? Yes
60+
INFO[0002] Token created successfully.
61+
INFO[0002] This token is valid for 24h0m0s hours.
62+
INFO[0002] You can now run the following command in a remote node to add it
63+
INFO[0002] to the cluster as a "controller" node:
64+
helmvm node join --role "controller" "<token redacted>"
65+
server-0#
66+
```
67+
68+
Upon generating the token, you will be prompted to continue; press Enter to proceed (you will be opting out of the centralized management).
69+
The role in the command above can be either "controller" or "worker", with the generated token tailored to the selected role.
70+
Copy the command provided and run it on the server you wish to join to the cluster:
71+
72+
```
73+
server-1# helmvm node join --role "controller" "<token redacted>"
74+
```
75+
76+
For this to function, you must ensure that the HelmVM binary is present on all nodes within the cluster.
77+
78+
79+
### Upgrading clusters
80+
81+
If your installation employs centralized management, simply download the newer version of HelmVM and execute:
82+
83+
```
84+
$ helmvm apply
85+
```
86+
87+
For installations without centralized management, download HelmVM, upload it to each server in your cluster, and execute the following command as **root** on each server:
88+
89+
```
90+
# helmvm node upgrade
91+
```
92+
3693
## Interacting with the cluster
3794

3895
Once the cluster has been deployed you can open a new terminal to interact with it using `kubectl`:

cmd/helmvm/install.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func runPostApply(ctx context.Context) error {
3535
orig := log.Log
3636
rig.SetLogger(logger)
3737
defer func() {
38-
logger.Infof("post apply process finished")
38+
logger.Infof("Post apply process finished")
3939
close(logger)
4040
<-end
4141
log.Log = orig
@@ -152,7 +152,7 @@ func copyUserProvidedConfig(c *cli.Context) error {
152152
func ensureK0sctlConfig(c *cli.Context, nodes []infra.Node) error {
153153
bundledir := c.String("bundle-dir")
154154
bundledir = strings.TrimRight(bundledir, "/")
155-
multi := c.Bool("multi-node")
155+
multi := c.Bool("multi-node") || len(nodes) > 0
156156
cfgpath := defaults.PathToConfig("k0sctl.yaml")
157157
if usercfg := c.String("config"); usercfg != "" {
158158
logrus.Infof("Using %s config file", usercfg)
@@ -229,10 +229,15 @@ func runK0sctlKubeconfig(ctx context.Context) error {
229229
}
230230
defer fp.Close()
231231
cfgpath := defaults.PathToConfig("k0sctl.yaml")
232+
if _, err := os.Stat(cfgpath); err != nil {
233+
os.RemoveAll(kpath)
234+
return fmt.Errorf("cluster configuration not found")
235+
}
232236
kctl := exec.Command(bin, "kubeconfig", "-c", cfgpath, "--disable-telemetry")
233237
kctl.Stderr = fp
234238
kctl.Stdout = fp
235239
if err := kctl.Run(); err != nil {
240+
os.RemoveAll(kpath)
236241
return fmt.Errorf("unable to run kubeconfig: %w", err)
237242
}
238243
logrus.Infof("Kubeconfig saved to %s", kpath)
@@ -310,6 +315,12 @@ var installCommand = &cli.Command{
310315
},
311316
},
312317
Action: func(c *cli.Context) error {
318+
if defaults.DecentralizedInstall() {
319+
logrus.Warnf("Decentralized install was detected. To manage the cluster")
320+
logrus.Warnf("you have to use the '%s node' commands instead.", defaults.BinaryName())
321+
logrus.Warnf("Run '%s node --help' for more information.", defaults.BinaryName())
322+
return fmt.Errorf("decentralized install detected")
323+
}
313324
logrus.Infof("Materializing binaries")
314325
if err := goods.Materialize(); err != nil {
315326
return fmt.Errorf("unable to materialize binaries: %w", err)

cmd/helmvm/join.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"runtime"
10+
11+
"github.com/sirupsen/logrus"
12+
"github.com/urfave/cli/v2"
13+
14+
"github.com/replicatedhq/helmvm/pkg/defaults"
15+
"github.com/replicatedhq/helmvm/pkg/goods"
16+
)
17+
18+
var joinCommand = &cli.Command{
19+
Name: "join",
20+
Usage: "Join the current node to an existing cluster",
21+
Flags: []cli.Flag{
22+
&cli.StringFlag{
23+
Name: "role",
24+
Usage: "The role of the node (can be controller or worker)",
25+
Value: "worker",
26+
},
27+
},
28+
Action: func(c *cli.Context) error {
29+
if err := canRunJoin(c); err != nil {
30+
return err
31+
}
32+
logrus.Infof("Materializing binaries")
33+
if err := goods.Materialize(); err != nil {
34+
return fmt.Errorf("unable to materialize binaries: %w", err)
35+
}
36+
logrus.Infof("Saving token to disk")
37+
if err := saveTokenToDisk(c.Args().First()); err != nil {
38+
return fmt.Errorf("unable to save token to disk: %w", err)
39+
}
40+
logrus.Infof("Installing binary")
41+
if err := installK0sBinary(); err != nil {
42+
return fmt.Errorf("unable to install k0s binary: %w", err)
43+
}
44+
logrus.Infof("Joining node to cluster")
45+
if err := runK0sInstallCommand(c.String("role")); err != nil {
46+
return fmt.Errorf("unable to join node to cluster: %w", err)
47+
}
48+
logrus.Infof("Creating systemd unit file")
49+
if err := createSystemdUnitFile(c.String("role")); err != nil {
50+
return fmt.Errorf("unable to create systemd unit file: %w", err)
51+
}
52+
logrus.Infof("Starting service")
53+
if err := startK0sService(); err != nil {
54+
return fmt.Errorf("unable to start service: %w", err)
55+
}
56+
return nil
57+
},
58+
}
59+
60+
// saveTokenToDisk saves the provided token in "/etc/k0s/join-token".
61+
func saveTokenToDisk(token string) error {
62+
if err := os.MkdirAll("/etc/k0s", 0755); err != nil {
63+
return err
64+
}
65+
data := []byte(token)
66+
if err := os.WriteFile("/etc/k0s/join-token", data, 0644); err != nil {
67+
return err
68+
}
69+
return nil
70+
}
71+
72+
// installK0sBinary saves the embedded k0s binary to disk under /usr/local/bin.
73+
func installK0sBinary() error {
74+
in, err := os.Open(defaults.K0sBinaryPath())
75+
if err != nil {
76+
return fmt.Errorf("unable to open embedded k0s binary: %w", err)
77+
}
78+
defer in.Close()
79+
out, err := os.OpenFile("/usr/local/bin/k0s", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
80+
if err != nil {
81+
return fmt.Errorf("unable to open k0s binary: %w", err)
82+
}
83+
defer out.Close()
84+
if _, err := io.Copy(out, in); err != nil {
85+
return fmt.Errorf("unable to copy k0s binary: %w", err)
86+
}
87+
return nil
88+
}
89+
90+
// startK0sService starts the k0s service.
91+
func startK0sService() error {
92+
cmd := exec.Command("/usr/local/bin/k0s", "start")
93+
stdout := bytes.NewBuffer(nil)
94+
stderr := bytes.NewBuffer(nil)
95+
cmd.Stdout = stdout
96+
cmd.Stderr = stderr
97+
if err := cmd.Run(); err != nil {
98+
logrus.Errorf("service start failed:")
99+
fmt.Fprintf(os.Stderr, "%s\n", stderr.String())
100+
fmt.Fprintf(os.Stdout, "%s\n", stdout.String())
101+
return err
102+
}
103+
return nil
104+
}
105+
106+
// canRunJoin checks if we can run the join command. Checks if we are running on linux,
107+
// if we are root, and if a token has been provided through the command line.
108+
func canRunJoin(c *cli.Context) error {
109+
if runtime.GOOS != "linux" {
110+
return fmt.Errorf("join command is only supported on linux")
111+
}
112+
if os.Getuid() != 0 {
113+
return fmt.Errorf("join command must be run as root")
114+
}
115+
if c.Args().Len() != 1 {
116+
return fmt.Errorf("usage: %s node join <token>", defaults.BinaryName())
117+
}
118+
if role := c.String("role"); role != "controller" && role != "worker" {
119+
return fmt.Errorf("role must be either controller or worker")
120+
}
121+
return nil
122+
}
123+
124+
// createSystemdUnitFile links the k0s systemd unit file.
125+
func createSystemdUnitFile(role string) error {
126+
dst := fmt.Sprintf("/etc/systemd/system/%s.service", defaults.BinaryName())
127+
if _, err := os.Stat(dst); err == nil {
128+
if err := os.Remove(dst); err != nil {
129+
return err
130+
}
131+
}
132+
src := "/etc/systemd/system/k0scontroller.service"
133+
if role == "worker" {
134+
src = "/etc/systemd/system/k0sworker.service"
135+
}
136+
if err := os.Symlink(src, dst); err != nil {
137+
return err
138+
}
139+
cmd := exec.Command("systemctl", "daemon-reload")
140+
stdout := bytes.NewBuffer(nil)
141+
stderr := bytes.NewBuffer(nil)
142+
cmd.Stdout = stdout
143+
cmd.Stderr = stderr
144+
if err := cmd.Run(); err != nil {
145+
logrus.Errorf("systemctl reload failed:")
146+
fmt.Fprintf(os.Stderr, "%s\n", stderr.String())
147+
fmt.Fprintf(os.Stdout, "%s\n", stdout.String())
148+
return err
149+
}
150+
return nil
151+
}
152+
153+
// runK0sInstallCommand runs the 'k0s install' command using the provided role.
154+
func runK0sInstallCommand(role string) error {
155+
a := []string{"install", role, "--token-file", "/etc/k0s/join-token", "--force"}
156+
if role == "controller" {
157+
a = append(a, "--enable-worker")
158+
}
159+
cmd := exec.Command("/usr/local/bin/k0s", a...)
160+
stdout := bytes.NewBuffer(nil)
161+
stderr := bytes.NewBuffer(nil)
162+
cmd.Stdout = stdout
163+
cmd.Stderr = stderr
164+
if err := cmd.Run(); err != nil {
165+
logrus.Errorf("install failed:")
166+
fmt.Fprintf(os.Stderr, "%s\n", stderr.String())
167+
fmt.Fprintf(os.Stdout, "%s\n", stdout.String())
168+
return err
169+
}
170+
return nil
171+
}

cmd/helmvm/node.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ var nodeCommands = &cli.Command{
1616
nodeStopCommand,
1717
nodeStartCommand,
1818
nodeListCommand,
19+
tokenCommands,
20+
joinCommand,
21+
upgradeCommand,
1922
},
2023
}
2124

cmd/helmvm/shell.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ var shellCommand = &cli.Command{
3838
Name: "shell",
3939
Usage: "Starts a shell with access to the running cluster",
4040
Action: func(c *cli.Context) error {
41+
cfgpath := defaults.PathToConfig("kubeconfig")
42+
if _, err := os.Stat(cfgpath); err != nil {
43+
return fmt.Errorf("kubeconfig not found at %s", cfgpath)
44+
}
4145
shpath := os.Getenv("SHELL")
4246
if shpath == "" {
4347
shpath = "/bin/bash"

0 commit comments

Comments
 (0)