mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-02 17:45:10 +00:00
Split off cli into separate binaries (#37)
* 🎨 Split off cli into separate binaries This commit splits off the cli into 3 binaries: - agent - cli - provider The provider now is a separate component that can be tested by itself and have its own lifecycle. This paves the way to a ligher c3os variant, HA support and other features that can be provided on runtime. This is working, but still there are low hanging fruit to care about. Fixes #14 * 🤖 Add provider bin to releases * ⚙️ Handle signals * ⚙️ Reduce buildsize footprint * 🎨 Scan for providers also in /system/providers * 🤖 Run goreleaser * 🎨 Refactoring
This commit is contained in:
committed by
Itxaka
parent
74bfd373db
commit
63cd28d1cb
66
internal/bus/bus.go
Normal file
66
internal/bus/bus.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/mudler/go-pluggable"
|
||||
)
|
||||
|
||||
// Manager is the bus instance manager, which subscribes plugins to events emitted
|
||||
var Manager *Bus = &Bus{
|
||||
Manager: pluggable.NewManager(
|
||||
[]pluggable.EventType{
|
||||
bus.EventBootstrap,
|
||||
bus.EventChallenge,
|
||||
bus.EventInstall,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
type Bus struct {
|
||||
*pluggable.Manager
|
||||
}
|
||||
|
||||
func (b *Bus) Initialize() {
|
||||
b.Manager.Autoload("agent-provider", "/system/providers").Register()
|
||||
|
||||
for i := range b.Manager.Events {
|
||||
e := b.Manager.Events[i]
|
||||
b.Manager.Response(e, func(p *pluggable.Plugin, r *pluggable.EventResponse) {
|
||||
if os.Getenv("BUS_DEBUG") == "true" {
|
||||
fmt.Println(
|
||||
fmt.Sprintf("[provider event: %s]", e),
|
||||
"received from",
|
||||
p.Name,
|
||||
"at",
|
||||
p.Executable,
|
||||
r,
|
||||
)
|
||||
}
|
||||
if r.Errored() {
|
||||
err := fmt.Sprintf("Provider %s at %s had an error: %s", p.Name, p.Executable, r.Error)
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
if r.State != "" {
|
||||
fmt.Println(fmt.Sprintf("[provider event: %s]", e), r.State)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RunHookScript(s string) error {
|
||||
_, err := os.Stat(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cmd := exec.Command(s)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd.Run()
|
||||
}
|
7
internal/c3os/branding.go
Normal file
7
internal/c3os/branding.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package c3os
|
||||
|
||||
import "path"
|
||||
|
||||
func BrandingFile(s string) string {
|
||||
return path.Join("/etc", "c3os", "branding", s)
|
||||
}
|
141
internal/cmd/commands.go
Normal file
141
internal/cmd/commands.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
//"fmt"
|
||||
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
config "github.com/c3os-io/c3os/pkg/config"
|
||||
edgeVPNClient "github.com/mudler/edgevpn/api/client"
|
||||
"github.com/mudler/edgevpn/api/client/service"
|
||||
"github.com/mudler/edgevpn/pkg/node"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func CommonCommand(cmds ...cli.Command) []cli.Command {
|
||||
return append(commonCommands, cmds...)
|
||||
}
|
||||
|
||||
var commonCommands = []cli.Command{
|
||||
{
|
||||
Name: "get-kubeconfig",
|
||||
Usage: "Return a deployment kubeconfig",
|
||||
UsageText: "Retrieve a c3os 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:
|
||||
|
||||
$ c3os get-kubeconfig --network-id c3os
|
||||
`,
|
||||
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: "c3os role set <UUID> 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 "c3os uuid".
|
||||
|
||||
Example:
|
||||
|
||||
$ (node A) c3os uuid
|
||||
$ (node B) c3os role set <UUID of node A> 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()[0], c.Args()[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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create-config",
|
||||
Aliases: []string{"c"},
|
||||
UsageText: "Create a config with a generated network token",
|
||||
|
||||
Usage: "Creates a pristine config file",
|
||||
Description: `
|
||||
Prints a vanilla YAML configuration on screen which can be used to bootstrap a c3os network.
|
||||
`,
|
||||
ArgsUsage: "Optionally takes a token rotation interval (seconds)",
|
||||
|
||||
Action: func(c *cli.Context) error {
|
||||
l := int(^uint(0) >> 1)
|
||||
args := c.Args()
|
||||
if len(args) > 0 {
|
||||
if i, err := strconv.Atoi(args[0]); err == nil {
|
||||
l = i
|
||||
}
|
||||
}
|
||||
cc := &config.Config{C3OS: &config.C3OS{NetworkToken: node.GenerateNewConnectionData(l).Base64()}}
|
||||
y, _ := yaml.Marshal(cc)
|
||||
fmt.Println(string(y))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "generate-token",
|
||||
Aliases: []string{"g"},
|
||||
UsageText: "Generate a network token",
|
||||
Usage: "Creates a new token",
|
||||
Description: `
|
||||
Generates a new token which can be used to bootstrap a c3os network.
|
||||
`,
|
||||
ArgsUsage: "Optionally takes a token rotation interval (seconds)",
|
||||
|
||||
Action: func(c *cli.Context) error {
|
||||
l := int(^uint(0) >> 1)
|
||||
args := c.Args()
|
||||
if len(args) > 0 {
|
||||
if i, err := strconv.Atoi(args[0]); err == nil {
|
||||
l = i
|
||||
}
|
||||
}
|
||||
fmt.Println(node.GenerateNewConnectionData(l).Base64())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
18
internal/cmd/flags.go
Normal file
18
internal/cmd/flags.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var networkAPI = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "API Address",
|
||||
Value: "http://localhost:8080",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "network-id",
|
||||
Value: "c3os",
|
||||
Usage: "Kubernetes Network Deployment ID",
|
||||
},
|
||||
}
|
33
internal/cmd/utils.go
Normal file
33
internal/cmd/utils.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/c3os"
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
func PrintTextFromFile(f string, banner string) {
|
||||
installText := ""
|
||||
text, err := ioutil.ReadFile(f)
|
||||
if err == nil {
|
||||
installText = string(text)
|
||||
}
|
||||
pterm.DefaultBox.WithTitle(banner).WithTitleBottomRight().WithRightPadding(0).WithBottomPadding(0).Println(
|
||||
installText)
|
||||
}
|
||||
|
||||
func PrintBranding(b []byte) {
|
||||
brandingFile := c3os.BrandingFile("banner")
|
||||
if _, err := os.Stat(brandingFile); err == nil {
|
||||
f, err := ioutil.ReadFile(brandingFile)
|
||||
if err == nil {
|
||||
fmt.Println(string(f))
|
||||
}
|
||||
|
||||
}
|
||||
utils.PrintBanner(b)
|
||||
}
|
49
internal/github/releases.go
Normal file
49
internal/github/releases.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v40/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func newHTTPClient(ctx context.Context, token string) *http.Client {
|
||||
if token == "" {
|
||||
return http.DefaultClient
|
||||
}
|
||||
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
||||
return oauth2.NewClient(ctx, src)
|
||||
}
|
||||
|
||||
func FindReleases(ctx context.Context, token, slug string) ([]string, error) {
|
||||
hc := newHTTPClient(ctx, token)
|
||||
cli := github.NewClient(hc)
|
||||
|
||||
repo := strings.Split(slug, "/")
|
||||
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
|
||||
return nil, fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
|
||||
}
|
||||
|
||||
rels, res, err := cli.Repositories.ListReleases(ctx, repo[0], repo[1], nil)
|
||||
if err != nil {
|
||||
log.Println("API returned an error response:", err)
|
||||
if res != nil && res.StatusCode == 404 {
|
||||
// 404 means repository not found or release not found. It's not an error here.
|
||||
err = nil
|
||||
log.Println("API returned 404. Repository or release not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions := []string{}
|
||||
for _, rel := range rels {
|
||||
if strings.HasPrefix(*rel.Name, "v") {
|
||||
versions = append(versions, *rel.Name)
|
||||
}
|
||||
}
|
||||
return versions, nil
|
||||
}
|
103
internal/machine/machine.go
Normal file
103
internal/machine/machine.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/machine/openrc"
|
||||
"github.com/c3os-io/c3os/internal/machine/systemd"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
WriteUnit() error
|
||||
Start() error
|
||||
OverrideCmd(string) error
|
||||
Enable() error
|
||||
Restart() error
|
||||
}
|
||||
|
||||
func EdgeVPN(instance, rootDir string) (Service, error) {
|
||||
if utils.IsOpenRCBased() {
|
||||
return openrc.NewService(
|
||||
openrc.WithName("edgevpn"),
|
||||
openrc.WithUnitContent(openrc.EdgevpnUnit),
|
||||
openrc.WithRoot(rootDir),
|
||||
)
|
||||
} else {
|
||||
return systemd.NewService(
|
||||
systemd.WithName("edgevpn"),
|
||||
systemd.WithInstance(instance),
|
||||
systemd.WithUnitContent(systemd.EdgevpnUnit),
|
||||
systemd.WithRoot(rootDir),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const EdgeVPNDefaultInstance string = "c3os"
|
||||
|
||||
type fakegetty struct{}
|
||||
|
||||
func (fakegetty) Restart() error { return nil }
|
||||
func (fakegetty) Enable() error { return nil }
|
||||
func (fakegetty) OverrideCmd(string) error { return nil }
|
||||
func (fakegetty) SetEnvFile(string) error { return nil }
|
||||
func (fakegetty) WriteUnit() error { return nil }
|
||||
func (fakegetty) Start() error {
|
||||
utils.SH("chvt 2")
|
||||
return nil
|
||||
}
|
||||
|
||||
func Getty(i int) (Service, error) {
|
||||
if utils.IsOpenRCBased() {
|
||||
return &fakegetty{}, nil
|
||||
} else {
|
||||
return systemd.NewService(
|
||||
systemd.WithName("getty"),
|
||||
systemd.WithInstance(fmt.Sprintf("tty%d", i)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func K3s() (Service, error) {
|
||||
if utils.IsOpenRCBased() {
|
||||
return openrc.NewService(
|
||||
openrc.WithName("k3s"),
|
||||
)
|
||||
} else {
|
||||
return systemd.NewService(
|
||||
systemd.WithName("k3s"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func K3sAgent() (Service, error) {
|
||||
if utils.IsOpenRCBased() {
|
||||
return openrc.NewService(
|
||||
openrc.WithName("k3s-agent"),
|
||||
)
|
||||
} else {
|
||||
return systemd.NewService(
|
||||
systemd.WithName("k3s-agent"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func K3sEnvUnit(unit string) string {
|
||||
if utils.IsOpenRCBased() {
|
||||
return fmt.Sprintf("/etc/rancher/k3s/%s.env", unit)
|
||||
} else {
|
||||
return fmt.Sprintf("/etc/sysconfig/%s", unit)
|
||||
}
|
||||
}
|
||||
|
||||
func UUID() string {
|
||||
if os.Getenv("UUID") != "" {
|
||||
return os.Getenv("UUID")
|
||||
}
|
||||
id, _ := machineid.ID()
|
||||
hostname, _ := os.Hostname()
|
||||
return fmt.Sprintf("%s-%s", id, hostname)
|
||||
}
|
19
internal/machine/openrc/edgevpn.go
Normal file
19
internal/machine/openrc/edgevpn.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package openrc
|
||||
|
||||
const EdgevpnUnit string = `#!/sbin/openrc-run
|
||||
|
||||
depend() {
|
||||
after net
|
||||
provide edgevpn
|
||||
}
|
||||
|
||||
supervisor=supervise-daemon
|
||||
name="edgevpn"
|
||||
command="edgevpn"
|
||||
supervise_daemon_args="--stdout /var/log/edgevpn.log --stderr /var/log/edgevpn.log"
|
||||
pidfile="/run/edgevpn.pid"
|
||||
respawn_delay=5
|
||||
set -o allexport
|
||||
if [ -f /etc/environment ]; then source /etc/environment; fi
|
||||
if [ -f /etc/systemd/system.conf.d/edgevpn-c3os.env ]; then source /etc/systemd/system.conf.d/edgevpn-c3os.env; fi
|
||||
set +o allexport`
|
86
internal/machine/openrc/unit.go
Normal file
86
internal/machine/openrc/unit.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package openrc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
)
|
||||
|
||||
type ServiceUnit struct {
|
||||
content string
|
||||
name string
|
||||
rootdir string
|
||||
}
|
||||
|
||||
type ServiceOpts func(*ServiceUnit) error
|
||||
|
||||
func WithRoot(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.rootdir = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithName(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.name = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithUnitContent(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.content = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(opts ...ServiceOpts) (ServiceUnit, error) {
|
||||
s := &ServiceUnit{}
|
||||
for _, o := range opts {
|
||||
if err := o(s); err != nil {
|
||||
return *s, err
|
||||
}
|
||||
}
|
||||
return *s, nil
|
||||
}
|
||||
|
||||
func (s ServiceUnit) WriteUnit() error {
|
||||
uname := s.name
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(s.rootdir, fmt.Sprintf("/etc/init.d/%s", uname)), []byte(s.content), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: This is too much k3s specific
|
||||
func (s ServiceUnit) OverrideCmd(cmd string) error {
|
||||
cmd = strings.ReplaceAll(cmd, "/usr/bin/k3s ", "")
|
||||
svcDir := filepath.Join(s.rootdir, fmt.Sprintf("/etc/rancher/k3s/%s.env", s.name))
|
||||
|
||||
return ioutil.WriteFile(svcDir, []byte(fmt.Sprintf(`command_args="%s >>/var/log/%s.log 2>&1"`, cmd, s.name)), 0600)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Start() error {
|
||||
_, err := utils.SH(fmt.Sprintf("/etc/init.d/%s start", s.name))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Restart() error {
|
||||
_, err := utils.SH(fmt.Sprintf("/etc/init.d/%s restart", s.name))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Enable() error {
|
||||
_, err := utils.SH(fmt.Sprintf("ln -sf /etc/init.d/%s /etc/runlevels/default/%s", s.name, s.name))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s ServiceUnit) StartBlocking() error {
|
||||
return s.Start()
|
||||
}
|
13
internal/machine/systemd/edgevpn.go
Normal file
13
internal/machine/systemd/edgevpn.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package systemd
|
||||
|
||||
const EdgevpnUnit string = `[Unit]
|
||||
Description=EdgeVPN Daemon
|
||||
After=network.target
|
||||
[Service]
|
||||
EnvironmentFile=/etc/systemd/system.conf.d/edgevpn-%i.env
|
||||
LimitNOFILE=49152
|
||||
ExecStartPre=-/bin/sh -c "sysctl -w net.core.rmem_max=2500000"
|
||||
ExecStart=edgevpn
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
115
internal/machine/systemd/unit.go
Normal file
115
internal/machine/systemd/unit.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package systemd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
)
|
||||
|
||||
type ServiceUnit struct {
|
||||
content string
|
||||
name, instance string
|
||||
rootdir string
|
||||
}
|
||||
|
||||
const overrideCmdTemplate string = `
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=%s
|
||||
`
|
||||
|
||||
type ServiceOpts func(*ServiceUnit) error
|
||||
|
||||
func WithRoot(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.rootdir = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithName(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.name = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithInstance(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.instance = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithUnitContent(n string) ServiceOpts {
|
||||
return func(su *ServiceUnit) error {
|
||||
su.content = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(opts ...ServiceOpts) (ServiceUnit, error) {
|
||||
s := &ServiceUnit{}
|
||||
for _, o := range opts {
|
||||
if err := o(s); err != nil {
|
||||
return *s, err
|
||||
}
|
||||
}
|
||||
return *s, nil
|
||||
}
|
||||
|
||||
func (s ServiceUnit) WriteUnit() error {
|
||||
uname := s.name
|
||||
if s.instance != "" {
|
||||
uname = fmt.Sprintf("%s@", s.name)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(s.rootdir, fmt.Sprintf("/etc/systemd/system/%s.service", uname)), []byte(s.content), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.SH("systemctl daemon-reload")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ServiceUnit) OverrideCmd(cmd string) error {
|
||||
svcDir := filepath.Join(s.rootdir, fmt.Sprintf("/etc/systemd/system/%s.service.d/", s.name))
|
||||
os.MkdirAll(svcDir, 0600)
|
||||
|
||||
return ioutil.WriteFile(filepath.Join(svcDir, "override.conf"), []byte(fmt.Sprintf(overrideCmdTemplate, cmd)), 0600)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Start() error {
|
||||
return s.systemctl("start", false)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Restart() error {
|
||||
return s.systemctl("restart", false)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) Enable() error {
|
||||
return s.systemctl("enable", false)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) StartBlocking() error {
|
||||
return s.systemctl("start", true)
|
||||
}
|
||||
|
||||
func (s ServiceUnit) systemctl(action string, blocking bool) error {
|
||||
uname := s.name
|
||||
if s.instance != "" {
|
||||
uname = fmt.Sprintf("%s@%s", s.name, s.instance)
|
||||
}
|
||||
args := []string{action}
|
||||
if !blocking {
|
||||
args = append(args, "--no-block")
|
||||
}
|
||||
args = append(args, uname)
|
||||
|
||||
_, err := utils.SH(fmt.Sprintf("systemctl %s", strings.Join(args, " ")))
|
||||
return err
|
||||
}
|
210
internal/provider/bootstrap.go
Normal file
210
internal/provider/bootstrap.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logging "github.com/ipfs/go-log"
|
||||
edgeVPNClient "github.com/mudler/edgevpn/api/client"
|
||||
"go.uber.org/zap"
|
||||
|
||||
eventBus "github.com/c3os-io/c3os/internal/bus"
|
||||
"github.com/c3os-io/c3os/internal/machine"
|
||||
"github.com/c3os-io/c3os/internal/machine/openrc"
|
||||
"github.com/c3os-io/c3os/internal/machine/systemd"
|
||||
"github.com/c3os-io/c3os/internal/role"
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
"github.com/c3os-io/c3os/internal/vpn"
|
||||
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
"github.com/mudler/edgevpn/api/client/service"
|
||||
"github.com/mudler/go-pluggable"
|
||||
)
|
||||
|
||||
func Bootstrap(e *pluggable.Event) pluggable.EventResponse {
|
||||
cfg := &bus.BootstrapPayload{}
|
||||
err := json.Unmarshal([]byte(e.Data), cfg)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), e.Data)}
|
||||
}
|
||||
|
||||
c := &config.Config{}
|
||||
err = config.FromString(cfg.Config, c)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), cfg.Config)}
|
||||
}
|
||||
|
||||
utils.SH("sysctl -w net.core.rmem_max=2500000")
|
||||
|
||||
tokenNotDefined := (c.C3OS == nil || c.C3OS.NetworkToken == "")
|
||||
|
||||
if c.C3OS == nil && !c.K3s.Enabled && !c.K3sAgent.Enabled {
|
||||
return pluggable.EventResponse{State: "No config file supplied"}
|
||||
}
|
||||
|
||||
utils.SH("elemental run-stage c3os-agent.bootstrap")
|
||||
eventBus.RunHookScript("/usr/bin/c3os-agent.bootstrap.hook")
|
||||
|
||||
logLevel := "debug"
|
||||
|
||||
if c.C3OS != nil && c.C3OS.LogLevel != "" {
|
||||
logLevel = c.C3OS.LogLevel
|
||||
}
|
||||
|
||||
lvl, err := logging.LevelFromString(logLevel)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())}
|
||||
}
|
||||
|
||||
// TODO: Fixup Logging to file
|
||||
loggerCfg := zap.NewProductionConfig()
|
||||
loggerCfg.OutputPaths = []string{
|
||||
cfg.Logfile,
|
||||
}
|
||||
logger, err := loggerCfg.Build()
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())}
|
||||
}
|
||||
|
||||
logging.SetAllLoggers(lvl)
|
||||
|
||||
log := &logging.ZapEventLogger{SugaredLogger: *logger.Sugar()}
|
||||
|
||||
// Do onetimebootstrap if K3s or K3s-agent are enabled.
|
||||
// Those blocks are not required to be enabled in case of a c3os
|
||||
// full automated setup. Otherwise, they must be explicitly enabled.
|
||||
if c.K3s.Enabled || c.K3sAgent.Enabled {
|
||||
err := oneTimeBootstrap(log, c, func() error { return vpn.Setup(machine.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, c) })
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup: %s", err.Error())}
|
||||
}
|
||||
return pluggable.EventResponse{}
|
||||
} else if tokenNotDefined {
|
||||
return pluggable.EventResponse{Error: "No network token provided, exiting"}
|
||||
}
|
||||
|
||||
logger.Info("Configuring VPN")
|
||||
|
||||
if err := vpn.Setup(machine.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, c); err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())}
|
||||
}
|
||||
|
||||
networkID := "c3os"
|
||||
|
||||
if c.C3OS != nil && c.C3OS.NetworkID != "" {
|
||||
networkID = c.C3OS.NetworkID
|
||||
}
|
||||
|
||||
cc := service.NewClient(
|
||||
networkID,
|
||||
edgeVPNClient.NewClient(edgeVPNClient.WithHost(cfg.APIAddress)))
|
||||
|
||||
nodeOpts := []service.Option{
|
||||
service.WithLogger(log),
|
||||
service.WithClient(cc),
|
||||
service.WithUUID(machine.UUID()),
|
||||
service.WithStateDir("/usr/local/.c3os/state"),
|
||||
service.WithNetworkToken(c.C3OS.NetworkToken),
|
||||
service.WithPersistentRoles("auto"),
|
||||
service.WithRoles(
|
||||
service.RoleKey{
|
||||
Role: "master",
|
||||
RoleHandler: role.Master(c),
|
||||
},
|
||||
service.RoleKey{
|
||||
Role: "worker",
|
||||
RoleHandler: role.Worker(c),
|
||||
},
|
||||
service.RoleKey{
|
||||
Role: "auto",
|
||||
RoleHandler: role.Auto(c),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
// Optionally set up a specific node role if the user has defined so
|
||||
if c.C3OS.Role != "" {
|
||||
nodeOpts = append(nodeOpts, service.WithDefaultRoles(c.C3OS.Role))
|
||||
}
|
||||
|
||||
k, err := service.NewNode(nodeOpts...)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed creating node: %s", err.Error())}
|
||||
}
|
||||
err = k.Start(context.Background())
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed start: %s", err.Error())}
|
||||
}
|
||||
|
||||
return pluggable.EventResponse{
|
||||
State: "",
|
||||
Data: "",
|
||||
Error: "shouldn't return here",
|
||||
}
|
||||
}
|
||||
|
||||
func oneTimeBootstrap(l logging.StandardLogger, c *config.Config, vpnSetupFN func() error) error {
|
||||
if role.SentinelExist() {
|
||||
l.Info("Sentinel exists, nothing to do. exiting.")
|
||||
return nil
|
||||
}
|
||||
l.Info("One time bootstrap starting")
|
||||
|
||||
var svc machine.Service
|
||||
k3sConfig := config.K3s{}
|
||||
svcName := "k3s"
|
||||
svcRole := "server"
|
||||
|
||||
if c.K3s.Enabled {
|
||||
k3sConfig = c.K3s
|
||||
} else if c.K3sAgent.Enabled {
|
||||
k3sConfig = c.K3sAgent
|
||||
svcName = "k3s-agent"
|
||||
svcRole = "agent"
|
||||
}
|
||||
|
||||
if utils.IsOpenRCBased() {
|
||||
svc, _ = openrc.NewService(
|
||||
openrc.WithName(svcName),
|
||||
)
|
||||
} else {
|
||||
svc, _ = systemd.NewService(
|
||||
systemd.WithName(svcName),
|
||||
)
|
||||
}
|
||||
|
||||
envFile := machine.K3sEnvUnit(svcName)
|
||||
if svc == nil {
|
||||
return fmt.Errorf("could not detect OS")
|
||||
}
|
||||
|
||||
// Setup systemd unit and starts it
|
||||
if err := utils.WriteEnv(envFile,
|
||||
k3sConfig.Env,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s %s %s", svcRole, strings.Join(k3sConfig.Args, " "))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Enable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.VPN) > 0 {
|
||||
if err := vpnSetupFN(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return role.CreateSentinel()
|
||||
}
|
53
internal/provider/bootstrap_test.go
Normal file
53
internal/provider/bootstrap_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
. "github.com/c3os-io/c3os/internal/provider"
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
"github.com/mudler/go-pluggable"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var _ = Describe("Bootstrap provider", func() {
|
||||
Context("logging", func() {
|
||||
e := &pluggable.Event{}
|
||||
|
||||
BeforeEach(func() {
|
||||
e = &pluggable.Event{}
|
||||
})
|
||||
|
||||
It("logs to file", func() {
|
||||
f, err := ioutil.TempFile(os.TempDir(), "tests")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(f.Name())
|
||||
|
||||
cfg := &config.Config{
|
||||
C3OS: &config.C3OS{
|
||||
NetworkToken: "foo",
|
||||
},
|
||||
}
|
||||
dat, err := yaml.Marshal(cfg)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
payload := &bus.BootstrapPayload{Logfile: f.Name(), Config: string(dat)}
|
||||
|
||||
dat, err = json.Marshal(payload)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
e.Data = string(dat)
|
||||
resp := Bootstrap(e)
|
||||
dat, _ = json.Marshal(resp)
|
||||
Expect(resp.Errored()).To(BeTrue(), string(dat))
|
||||
|
||||
data, err := ioutil.ReadFile(f.Name())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(string(data)).Should(ContainSubstring("Configuring VPN"))
|
||||
})
|
||||
})
|
||||
})
|
36
internal/provider/challenge.go
Normal file
36
internal/provider/challenge.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
"github.com/mudler/go-nodepair"
|
||||
"github.com/mudler/go-pluggable"
|
||||
)
|
||||
|
||||
func Challenge(e *pluggable.Event) pluggable.EventResponse {
|
||||
p := &bus.EventPayload{}
|
||||
err := json.Unmarshal([]byte(e.Data), p)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), e.Data)}
|
||||
}
|
||||
|
||||
cfg := &config.Config{}
|
||||
err = config.FromString(p.Config, cfg)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), p.Config)}
|
||||
}
|
||||
|
||||
tk := ""
|
||||
if cfg.C3OS != nil && cfg.C3OS.NetworkToken != "" {
|
||||
tk = cfg.C3OS.NetworkToken
|
||||
}
|
||||
if tk == "" {
|
||||
tk = nodepair.GenerateToken()
|
||||
}
|
||||
return pluggable.EventResponse{
|
||||
Data: tk,
|
||||
}
|
||||
}
|
64
internal/provider/challenge_test.go
Normal file
64
internal/provider/challenge_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
. "github.com/c3os-io/c3os/internal/provider"
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
"github.com/mudler/go-pluggable"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Challenge provider", func() {
|
||||
Context("network token", func() {
|
||||
e := &pluggable.Event{}
|
||||
|
||||
BeforeEach(func() {
|
||||
e = &pluggable.Event{}
|
||||
})
|
||||
|
||||
It("returns it if provided", func() {
|
||||
f, err := ioutil.TempFile(os.TempDir(), "tests")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(f.Name())
|
||||
|
||||
cfg := &config.Config{
|
||||
C3OS: &config.C3OS{
|
||||
NetworkToken: "foo",
|
||||
},
|
||||
}
|
||||
c := &bus.EventPayload{Config: cfg.String()}
|
||||
dat, err := json.Marshal(c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
e.Data = string(dat)
|
||||
resp := Challenge(e)
|
||||
|
||||
Expect(string(resp.Data)).Should(ContainSubstring("foo"))
|
||||
})
|
||||
|
||||
It("generates it if not provided", func() {
|
||||
f, err := ioutil.TempFile(os.TempDir(), "tests")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(f.Name())
|
||||
|
||||
cfg := &config.Config{
|
||||
C3OS: &config.C3OS{
|
||||
NetworkToken: "",
|
||||
},
|
||||
}
|
||||
c := &bus.EventPayload{Config: cfg.String()}
|
||||
dat, err := json.Marshal(c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
e.Data = string(dat)
|
||||
resp := Challenge(e)
|
||||
|
||||
Expect(len(string(resp.Data))).Should(BeNumerically(">", 12))
|
||||
})
|
||||
})
|
||||
})
|
36
internal/provider/install.go
Normal file
36
internal/provider/install.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/c3os-io/c3os/pkg/bus"
|
||||
"github.com/mudler/go-nodepair"
|
||||
"github.com/mudler/go-pluggable"
|
||||
)
|
||||
|
||||
func Install(e *pluggable.Event) pluggable.EventResponse {
|
||||
cfg := &bus.InstallPayload{}
|
||||
err := json.Unmarshal([]byte(e.Data), cfg)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s", err.Error())}
|
||||
}
|
||||
|
||||
r := map[string]string{}
|
||||
ctx := context.Background()
|
||||
if err := nodepair.Receive(ctx, &r, nodepair.WithToken(cfg.Token)); err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s", err.Error())}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return pluggable.EventResponse{Error: fmt.Sprintf("Failed marshalling JSON input: %s", err.Error())}
|
||||
}
|
||||
|
||||
return pluggable.EventResponse{
|
||||
State: "",
|
||||
Data: string(payload),
|
||||
Error: "",
|
||||
}
|
||||
}
|
13
internal/provider/provider_suite_test.go
Normal file
13
internal/provider/provider_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestInstaller(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Provider Suite")
|
||||
}
|
57
internal/role/auto.go
Normal file
57
internal/role/auto.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
utils "github.com/mudler/edgevpn/pkg/utils"
|
||||
|
||||
service "github.com/mudler/edgevpn/api/client/service"
|
||||
)
|
||||
|
||||
func contains(slice []string, elem string) bool {
|
||||
for _, s := range slice {
|
||||
if elem == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func Auto(cc *config.Config) Role {
|
||||
return func(c *service.RoleConfig) error {
|
||||
advertizing, _ := c.Client.AdvertizingNodes()
|
||||
actives, _ := c.Client.ActiveNodes()
|
||||
|
||||
c.Logger.Info("Active nodes:", actives)
|
||||
c.Logger.Info("Advertizing nodes:", advertizing)
|
||||
|
||||
if len(advertizing) < 2 {
|
||||
c.Logger.Info("Not enough nodes")
|
||||
return nil
|
||||
}
|
||||
|
||||
// first get available nodes
|
||||
nodes := advertizing
|
||||
shouldBeLeader := utils.Leader(advertizing)
|
||||
|
||||
lead, _ := c.Client.Get("auto", "leader")
|
||||
|
||||
// From now on, only the leader keeps processing
|
||||
// TODO: Make this more reliable with consensus
|
||||
if shouldBeLeader != c.UUID && lead != c.UUID {
|
||||
c.Logger.Infof("<%s> not a leader, leader is '%s', sleeping", c.UUID, shouldBeLeader)
|
||||
return nil
|
||||
}
|
||||
|
||||
if shouldBeLeader == c.UUID && (lead == "" || !contains(nodes, lead)) {
|
||||
c.Client.Set("auto", "leader", c.UUID)
|
||||
c.Logger.Info("Announcing ourselves as leader, backing off")
|
||||
return nil
|
||||
}
|
||||
|
||||
if lead != c.UUID {
|
||||
c.Logger.Info("Backing off, as we are not currently flagged as leader")
|
||||
return nil
|
||||
}
|
||||
|
||||
return scheduleRoles(nodes, c, cc)
|
||||
}
|
||||
}
|
21
internal/role/common.go
Normal file
21
internal/role/common.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
service "github.com/mudler/edgevpn/api/client/service"
|
||||
)
|
||||
|
||||
type Role func(*service.RoleConfig) error
|
||||
|
||||
func SentinelExist() bool {
|
||||
if _, err := os.Stat("/usr/local/.c3os/deployed"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CreateSentinel() error {
|
||||
return ioutil.WriteFile("/usr/local/.c3os/deployed", []byte{}, os.ModePerm)
|
||||
}
|
134
internal/role/master.go
Normal file
134
internal/role/master.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/machine"
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
|
||||
service "github.com/mudler/edgevpn/api/client/service"
|
||||
)
|
||||
|
||||
func propagateMasterData(ip string, c *service.RoleConfig) error {
|
||||
defer func() {
|
||||
// Avoid polluting the API.
|
||||
// The ledger already retries in the background to update the blockchain, but it has
|
||||
// a default timeout where it would stop trying afterwards.
|
||||
// Each request here would have it's own background announce, so that can become expensive
|
||||
// when network is having lot of changes on its way.
|
||||
time.Sleep(30 * time.Second)
|
||||
}()
|
||||
|
||||
// If we are configured as master, always signal our role
|
||||
c.Client.Set("role", c.UUID, "master")
|
||||
|
||||
tokenB, err := ioutil.ReadFile("/var/lib/rancher/k3s/server/node-token")
|
||||
if err != nil {
|
||||
c.Logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
nodeToken := string(tokenB)
|
||||
nodeToken = strings.TrimRight(nodeToken, "\n")
|
||||
if nodeToken != "" {
|
||||
c.Client.Set("nodetoken", "token", nodeToken)
|
||||
}
|
||||
|
||||
kubeB, err := ioutil.ReadFile("/etc/rancher/k3s/k3s.yaml")
|
||||
if err != nil {
|
||||
c.Logger.Error(err)
|
||||
return err
|
||||
}
|
||||
kubeconfig := string(kubeB)
|
||||
if kubeconfig != "" {
|
||||
c.Client.Set("kubeconfig", "master", base64.RawURLEncoding.EncodeToString(kubeB))
|
||||
}
|
||||
c.Client.Set("master", "ip", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Master(cc *config.Config) Role {
|
||||
return func(c *service.RoleConfig) error {
|
||||
|
||||
ip := utils.GetInterfaceIP("edgevpn0")
|
||||
if ip == "" {
|
||||
return errors.New("node doesn't have an ip yet")
|
||||
}
|
||||
|
||||
if cc.C3OS.Role != "" {
|
||||
// propagate role if we were forced by configuration
|
||||
// This unblocks eventual auto instances to try to assign roles
|
||||
c.Client.Set("role", c.UUID, cc.C3OS.Role)
|
||||
}
|
||||
|
||||
if SentinelExist() {
|
||||
c.Logger.Info("Node already configured, backing off")
|
||||
return propagateMasterData(ip, c)
|
||||
}
|
||||
|
||||
// Configure k3s service to start on edgevpn0
|
||||
c.Logger.Info("Configuring k3s")
|
||||
|
||||
svc, err := machine.K3s()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k3sConfig := config.K3s{}
|
||||
if cc.K3s.Enabled {
|
||||
k3sConfig = cc.K3s
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
if !k3sConfig.ReplaceEnv {
|
||||
// Override opts with user-supplied
|
||||
for k, v := range k3sConfig.Env {
|
||||
env[k] = v
|
||||
}
|
||||
} else {
|
||||
env = k3sConfig.Env
|
||||
}
|
||||
|
||||
if err := utils.WriteEnv(machine.K3sEnvUnit("k3s"),
|
||||
env,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{"--flannel-iface=edgevpn0"}
|
||||
if k3sConfig.ReplaceArgs {
|
||||
args = k3sConfig.Args
|
||||
} else {
|
||||
args = append(args, k3sConfig.Args...)
|
||||
}
|
||||
|
||||
if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s server %s", strings.Join(args, " "))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Enable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
propagateMasterData(ip, c)
|
||||
|
||||
CreateSentinel()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/
|
||||
func HAMaster(c *service.RoleConfig) {
|
||||
c.Logger.Info("HA Role not implemented yet")
|
||||
}
|
73
internal/role/schedule.go
Normal file
73
internal/role/schedule.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
service "github.com/mudler/edgevpn/api/client/service"
|
||||
)
|
||||
|
||||
// scheduleRoles assigns roles to nodes. Meant to be called only by leaders
|
||||
// TODO: HA-Auto
|
||||
func scheduleRoles(nodes []string, c *service.RoleConfig, cc *config.Config) error {
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
// Assign roles to nodes
|
||||
currentRoles := map[string]string{}
|
||||
|
||||
existsMaster := false
|
||||
unassignedNodes := []string{}
|
||||
for _, a := range nodes {
|
||||
role, _ := c.Client.Get("role", a)
|
||||
currentRoles[a] = role
|
||||
if role == "master" {
|
||||
existsMaster = true
|
||||
} else if role == "" {
|
||||
unassignedNodes = append(unassignedNodes, a)
|
||||
}
|
||||
}
|
||||
|
||||
c.Logger.Infof("I'm the leader. My UUID is: %s.\n Current assigned roles: %+v", c.UUID, currentRoles)
|
||||
c.Logger.Infof("Master already present: %t", existsMaster)
|
||||
c.Logger.Infof("Unassigned nodes: %+v", unassignedNodes)
|
||||
|
||||
if !existsMaster && len(unassignedNodes) > 0 {
|
||||
var selected string
|
||||
toSelect := unassignedNodes
|
||||
|
||||
// Avoid to schedule to ourselves if we have a static role
|
||||
if cc.C3OS.Role != "" {
|
||||
toSelect = []string{}
|
||||
for _, u := range unassignedNodes {
|
||||
if u != c.UUID {
|
||||
toSelect = append(toSelect, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select one node without roles to become master
|
||||
if len(toSelect) == 1 {
|
||||
selected = toSelect[0]
|
||||
} else {
|
||||
selected = toSelect[rand.Intn(len(toSelect)-1)]
|
||||
}
|
||||
|
||||
c.Client.Set("role", selected, "master")
|
||||
c.Logger.Info("-> Set master to", selected)
|
||||
currentRoles[selected] = "master"
|
||||
// Return here, so next time we get called
|
||||
// makes sure master is set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// cycle all empty roles and assign worker roles
|
||||
for _, uuid := range unassignedNodes {
|
||||
c.Client.Set("role", uuid, "worker")
|
||||
c.Logger.Info("-> Set worker to", uuid)
|
||||
}
|
||||
|
||||
c.Logger.Info("Done scheduling")
|
||||
|
||||
return nil
|
||||
}
|
108
internal/role/worker.go
Normal file
108
internal/role/worker.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/machine"
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
|
||||
service "github.com/mudler/edgevpn/api/client/service"
|
||||
)
|
||||
|
||||
func Worker(cc *config.Config) Role {
|
||||
return func(c *service.RoleConfig) error {
|
||||
|
||||
if cc.C3OS.Role != "" {
|
||||
// propagate role if we were forced by configuration
|
||||
// This unblocks eventual auto instances to try to assign roles
|
||||
c.Client.Set("role", c.UUID, cc.C3OS.Role)
|
||||
}
|
||||
|
||||
if SentinelExist() {
|
||||
c.Logger.Info("Node already configured, backing off")
|
||||
return nil
|
||||
}
|
||||
|
||||
masterIP, _ := c.Client.Get("master", "ip")
|
||||
if masterIP == "" {
|
||||
c.Logger.Info("MasterIP not there still..")
|
||||
return nil
|
||||
}
|
||||
|
||||
nodeToken, _ := c.Client.Get("nodetoken", "token")
|
||||
if masterIP == "" {
|
||||
c.Logger.Info("nodetoken not there still..")
|
||||
return nil
|
||||
}
|
||||
|
||||
nodeToken = strings.TrimRight(nodeToken, "\n")
|
||||
|
||||
ip := utils.GetInterfaceIP("edgevpn0")
|
||||
if ip == "" {
|
||||
return errors.New("node doesn't have an ip yet")
|
||||
}
|
||||
|
||||
c.Logger.Info("Configuring k3s-agent", ip, masterIP, nodeToken)
|
||||
|
||||
svc, err := machine.K3sAgent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k3sConfig := config.K3s{}
|
||||
if cc.K3sAgent.Enabled {
|
||||
k3sConfig = cc.K3sAgent
|
||||
}
|
||||
|
||||
env := map[string]string{
|
||||
"K3S_URL": fmt.Sprintf("https://%s:6443", masterIP),
|
||||
"K3S_TOKEN": nodeToken,
|
||||
}
|
||||
|
||||
if !k3sConfig.ReplaceEnv {
|
||||
// Override opts with user-supplied
|
||||
for k, v := range k3sConfig.Env {
|
||||
env[k] = v
|
||||
}
|
||||
} else {
|
||||
env = k3sConfig.Env
|
||||
}
|
||||
|
||||
// Setup systemd unit and starts it
|
||||
if err := utils.WriteEnv(machine.K3sEnvUnit("k3s-agent"),
|
||||
env,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--with-node-id",
|
||||
fmt.Sprintf("--node-ip %s", ip),
|
||||
"--flannel-iface=edgevpn0",
|
||||
}
|
||||
if k3sConfig.ReplaceArgs {
|
||||
args = k3sConfig.Args
|
||||
} else {
|
||||
args = append(args, k3sConfig.Args...)
|
||||
}
|
||||
|
||||
if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s agent %s", strings.Join(args, " "))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Enable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
CreateSentinel()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
37
internal/utils/console.go
Normal file
37
internal/utils/console.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/qeesung/image2ascii/convert"
|
||||
)
|
||||
|
||||
func Prompt(t string) (string, error) {
|
||||
if t != "" {
|
||||
pterm.Info.Println(t)
|
||||
}
|
||||
answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(answer), nil
|
||||
}
|
||||
|
||||
func PrintBanner(d []byte) {
|
||||
img, _, _ := image.Decode(bytes.NewReader(d))
|
||||
|
||||
convertOptions := convert.DefaultOptions
|
||||
convertOptions.FixedWidth = 100
|
||||
convertOptions.FixedHeight = 40
|
||||
|
||||
// Create the image converter
|
||||
converter := convert.NewImageConverter()
|
||||
fmt.Print(converter.Image2ASCIIString(img, &convertOptions))
|
||||
}
|
33
internal/utils/sh.go
Normal file
33
internal/utils/sh.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func SH(c string) (string, error) {
|
||||
o, err := exec.Command("/bin/sh", "-c", c).CombinedOutput()
|
||||
return string(o), err
|
||||
}
|
||||
|
||||
func WriteEnv(envFile string, config map[string]string) error {
|
||||
content, _ := ioutil.ReadFile(envFile)
|
||||
env, _ := godotenv.Unmarshal(string(content))
|
||||
|
||||
for key, val := range config {
|
||||
env[key] = val
|
||||
}
|
||||
|
||||
return godotenv.Write(env, envFile)
|
||||
}
|
||||
|
||||
func Shell() *exec.Cmd {
|
||||
cmd := exec.Command("/bin/sh")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd
|
||||
}
|
15
internal/utils/signal.go
Normal file
15
internal/utils/signal.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
func OnSignal(fn func(), sig ...os.Signal) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, sig...)
|
||||
go func() {
|
||||
<-sigs
|
||||
fn()
|
||||
}()
|
||||
}
|
20
internal/utils/strings.go
Normal file
20
internal/utils/strings.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func RandStringRunes(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
82
internal/utils/system.go
Normal file
82
internal/utils/system.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
func Reboot() {
|
||||
pterm.Info.Println("Rebooting node")
|
||||
SH("reboot")
|
||||
}
|
||||
|
||||
func PowerOFF() {
|
||||
pterm.Info.Println("Shutdown node")
|
||||
if IsOpenRCBased() {
|
||||
SH("poweroff")
|
||||
} else {
|
||||
SH("shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func Version() string {
|
||||
release, _ := godotenv.Read("/etc/os-release")
|
||||
v := release["VERSION"]
|
||||
v = strings.ReplaceAll(v, "+k3s1-c3OS", "-")
|
||||
return strings.ReplaceAll(v, "+k3s-c3OS", "-")
|
||||
}
|
||||
|
||||
func OSRelease(key string) (string, error) {
|
||||
release, err := godotenv.Read("/etc/os-release")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
v, exists := release[key]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("key not found")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func Flavor() string {
|
||||
release, _ := godotenv.Read("/etc/os-release")
|
||||
v := release["NAME"]
|
||||
return strings.ReplaceAll(v, "c3os-", "")
|
||||
}
|
||||
|
||||
func IsOpenRCBased() bool {
|
||||
f := Flavor()
|
||||
return f == "alpine" || f == "alpine-arm-rpi"
|
||||
}
|
||||
|
||||
func GetInterfaceIP(in string) string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
fmt.Println("failed getting system interfaces")
|
||||
return ""
|
||||
}
|
||||
for _, i := range ifaces {
|
||||
if i.Name == in {
|
||||
addrs, _ := i.Addrs()
|
||||
// handle err
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip != nil {
|
||||
return ip.String()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
13
internal/utils/token.go
Normal file
13
internal/utils/token.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
const sep = "_CREDENTIALS_"
|
||||
|
||||
func EncodeRecoveryToken(data ...string) string {
|
||||
return strings.Join(data, sep)
|
||||
}
|
||||
|
||||
func DecodeRecoveryToken(recoverytoken string) []string {
|
||||
return strings.Split(recoverytoken, sep)
|
||||
}
|
90
internal/vpn/setup.go
Normal file
90
internal/vpn/setup.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package vpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/c3os-io/c3os/internal/machine"
|
||||
"github.com/c3os-io/c3os/internal/machine/systemd"
|
||||
"github.com/c3os-io/c3os/internal/utils"
|
||||
"github.com/c3os-io/c3os/pkg/config"
|
||||
yip "github.com/mudler/yip/pkg/schema"
|
||||
)
|
||||
|
||||
func Setup(instance, apiAddress, rootDir string, start bool, c *config.Config) error {
|
||||
|
||||
if c.C3OS == nil || c.C3OS.NetworkToken == "" {
|
||||
return fmt.Errorf("no network token defined")
|
||||
}
|
||||
|
||||
svc, err := machine.EdgeVPN(instance, rootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiAddress = strings.ReplaceAll(apiAddress, "https://", "")
|
||||
apiAddress = strings.ReplaceAll(apiAddress, "http://", "")
|
||||
|
||||
vpnOpts := map[string]string{
|
||||
"EDGEVPNTOKEN": c.C3OS.NetworkToken,
|
||||
"API": "true",
|
||||
"APILISTEN": apiAddress,
|
||||
"EDGEVPNLOWPROFILEVPN": "true",
|
||||
"DHCP": "true",
|
||||
"DHCPLEASEDIR": "/usr/local/.c3os/lease",
|
||||
}
|
||||
// Override opts with user-supplied
|
||||
for k, v := range c.VPN {
|
||||
vpnOpts[k] = v
|
||||
}
|
||||
|
||||
if c.C3OS.DNS {
|
||||
vpnOpts["DNSADDRESS"] = "127.0.0.1:53"
|
||||
vpnOpts["DNSFORWARD"] = "true"
|
||||
if !utils.IsOpenRCBased() {
|
||||
if _, err := os.Stat("/etc/sysconfig/network/config"); err == nil {
|
||||
utils.WriteEnv("/etc/sysconfig/network/config", map[string]string{
|
||||
"NETCONFIG_DNS_STATIC_SERVERS": "127.0.0.1",
|
||||
})
|
||||
if utils.Flavor() == "opensuse" {
|
||||
// TODO: This is dependant on wickedd, move this out in its own network detection block
|
||||
svc, err := systemd.NewService(systemd.WithName("wickedd"))
|
||||
if err == nil {
|
||||
svc.Restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := config.SaveCloudConfig("dns", yip.YipConfig{
|
||||
Name: "DNS Configuration",
|
||||
Stages: map[string][]yip.Stage{
|
||||
config.NetworkStage.String(): {{Dns: yip.DNS{Nameservers: []string{"127.0.0.1"}}}}},
|
||||
}); err != nil {
|
||||
fmt.Println("Failed installing DNS")
|
||||
}
|
||||
}
|
||||
|
||||
os.MkdirAll("/etc/systemd/system.conf.d/", 0600)
|
||||
// Setup edgevpn instance
|
||||
err = utils.WriteEnv(filepath.Join(rootDir, "/etc/systemd/system.conf.d/edgevpn-c3os.env"), vpnOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = svc.WriteUnit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if start {
|
||||
err = svc.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return svc.Enable()
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user