diff --git a/go.mod b/go.mod index 2e4792d838..fd83ecad4c 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/henvic/httpretty v0.1.3 + github.com/kardianos/service v1.2.2 github.com/kubescape/go-git-url v0.0.30 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 @@ -160,7 +161,6 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kardianos/service v1.2.2 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/internal/cli/kraft/run/run.go b/internal/cli/kraft/run/run.go index 587c547d66..74a35de5d8 100644 --- a/internal/cli/kraft/run/run.go +++ b/internal/cli/kraft/run/run.go @@ -8,8 +8,10 @@ import ( "context" "errors" "fmt" + "strings" "github.com/MakeNowJust/heredoc" + "github.com/kardianos/service" "github.com/sirupsen/logrus" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -19,11 +21,13 @@ import ( machineapi "kraftkit.sh/api/machine/v1alpha1" "kraftkit.sh/cmdfactory" "kraftkit.sh/config" + "kraftkit.sh/internal/cli/kraft/cloud/instance/stop" "kraftkit.sh/internal/cli/kraft/start" "kraftkit.sh/internal/set" "kraftkit.sh/iostreams" "kraftkit.sh/log" mplatform "kraftkit.sh/machine/platform" + "kraftkit.sh/machine/platform/systemd" "kraftkit.sh/packmanager" "kraftkit.sh/tui/selection" ukarch "kraftkit.sh/unikraft/arch" @@ -49,6 +53,7 @@ type RunOptions struct { Remove bool `long:"rm" usage:"Automatically remove the unikernel when it shutsdown"` Rootfs string `long:"rootfs" usage:"Specify a path to use as root file system (can be volume or initramfs)"` RunAs string `long:"as" usage:"Force a specific runner"` + Systemd bool `long:"systemd" usage:"runs unikernel as systemd process"` Target string `long:"target" short:"t" usage:"Explicitly use the defined project target"` Volumes []string `long:"volume" short:"v" usage:"Bind a volume to the instance"` WithKernelDbg bool `long:"symbolic" usage:"Use the debuggable (symbolic) unikernel"` @@ -374,15 +379,72 @@ func (opts *RunOptions) Run(ctx context.Context, args []string) error { return err } - if opts.NoStart { + if opts.NoStart && !opts.Systemd { // Output the name of the instance such that it can be piped fmt.Fprintf(iostreams.G(ctx).Out, "%s\n", machine.Name) return nil } - return start.Start(ctx, &start.StartOptions{ + err = start.Start(ctx, &start.StartOptions{ Detach: opts.Detach, Platform: opts.platform.String(), Remove: opts.Remove, }, machine.Name) + + // Install systemd serivce that runs a new unikernel instance on each start. + if err == nil && opts.Systemd { + // Stops the created testing instance if that is still runnning. + if err = stop.Stop(ctx, &stop.StopOptions{ + Force: true, + }, machine.Name); err != nil { + log.G(ctx).Debugf("instance %s is already stopped", machine.Name) + } + + boolArgs := "" + if opts.Detach { + boolArgs += "-d" + } + if opts.DisableAccel { + boolArgs += " -W" + } + if opts.PrefixName { + boolArgs += " --prefix-name" + } + if opts.WithKernelDbg { + boolArgs += " --symbolic" + } + + svcConfig, err := systemd.NewMachineV1alpha1ServiceSystemdWrapper( + ctx, + systemd.WithName(machine.Name), + systemd.WithDescription("created by kraftkit"), + systemd.WithArguments([]string{"run", "-m", opts.Architecture, "--as", opts.RunAs, "--kernel-arg", strings.Join(opts.KernelArgs, " "), + "-K", opts.Kraftfile, "--mac", opts.MacAddress, "--memory", opts.Memory, "--network", strings.Join(opts.Networks, " "), + "--plat", opts.Platform, "--port", strings.Join(opts.Ports, " "), "--prefix", opts.Prefix, "--rootfs", opts.Rootfs, "--target", opts.Target, + "--volume", strings.Join(opts.Volumes, " "), "--rm", boolArgs, machine.Name, + }), + systemd.WithOptions(service.KeyValue{ + "Restart": "never", + }), + ) + if err != nil { + return err + } + + machine, err = svcConfig.Create(ctx, machine) + if err != nil { + return err + } + log.G(ctx).Infof("created a systemd process named %s ", svcConfig.Name) + + _, err = svcConfig.Start(ctx, machine) + if err != nil { + return err + } + log.G(ctx).Infof("started running %s as systemd process", svcConfig.Name) + + opts.NoStart = true + } + + return err } diff --git a/machine/platform/systemd/manage.go b/machine/platform/systemd/manage.go index 660afa86d6..6ba07ee507 100644 --- a/machine/platform/systemd/manage.go +++ b/machine/platform/systemd/manage.go @@ -9,9 +9,12 @@ func (p *startStop) Start(s service.Service) error { go p.run() return nil } -func (p *startStop) run() { + +func (p *startStop) run() error { // Do work here + return nil } + func (p *startStop) Stop(s service.Service) error { // Stop should not block. Return with a few seconds. return nil diff --git a/machine/platform/systemd/options.go b/machine/platform/systemd/options.go index d4902c6f40..c64bed2462 100644 --- a/machine/platform/systemd/options.go +++ b/machine/platform/systemd/options.go @@ -4,42 +4,58 @@ import "github.com/kardianos/service" type ServiceConfigOption func(*ServiceConfig) error -// WithName sets the name of the systemd process. +// WithName sets the name of systemd service. func WithName(name string) ServiceConfigOption { return func(config *ServiceConfig) error { - config.name = name + config.Name = name return nil } } -// WithDisplayName sets the display-name of the systemd process. +// WithDisplayName sets the display-name of systemd service. func WithDisplayName(dName string) ServiceConfigOption { return func(config *ServiceConfig) error { - config.displayName = dName + config.DisplayName = dName return nil } } -// WithDescription sets the description of the systemd process. +// WithDescription sets the description/heading of systemd service. func WithDescription(desc string) ServiceConfigOption { return func(config *ServiceConfig) error { - config.description = desc + config.Description = desc return nil } } -// WithDependencies sets the dependencies of the systemd process. +// WithDependencies sets the dependencies of systemd service. func WithDependencies(deps []string) ServiceConfigOption { return func(config *ServiceConfig) error { - config.dependencies = deps + config.Dependencies = deps return nil } } -// WithOptions sets the options of the systemd process. +// WithEnvVars sets the environment variables for systemd service. +func WithEnvVars(envVars map[string]string) ServiceConfigOption { + return func(config *ServiceConfig) error { + config.EnvVars = envVars + return nil + } +} + +// WithArguments sets the arguments to the command executed by systemd service. +func WithArguments(args []string) ServiceConfigOption { + return func(config *ServiceConfig) error { + config.Arguments = args + return nil + } +} + +// WithOptions sets the options of systemd service. func WithOptions(opts service.KeyValue) ServiceConfigOption { return func(config *ServiceConfig) error { - config.option = opts + config.Option = opts return nil } } diff --git a/machine/platform/systemd/systemd.go b/machine/platform/systemd/systemd.go index cb81bad547..b07327fc4f 100644 --- a/machine/platform/systemd/systemd.go +++ b/machine/platform/systemd/systemd.go @@ -2,63 +2,122 @@ package systemd import ( "context" + "encoding/json" "fmt" "os" + "path/filepath" "github.com/kardianos/service" machinev1alpha1 "kraftkit.sh/api/machine/v1alpha1" + "kraftkit.sh/config" ) type ServiceConfig struct { - name string - displayName string - description string - dependencies []string - option service.KeyValue + Name string `json:"Name"` + DisplayName string `json:"DisplayName,omitempty"` + Description string `json:"Description"` + Dependencies []string `json:"Dependencies,omitempty"` + Arguments []string `json:"Arguments"` + Option service.KeyValue `json:"Option,omitempty"` + EnvVars map[string]string `json:"EnvVars,omitempty"` service service.Service logger service.Logger } +// NewMachineV1alpha1ServiceSystemdWrapper creates a new systemd service. func NewMachineV1alpha1ServiceSystemdWrapper(ctx context.Context, opts ...ServiceConfigOption) (ServiceConfig, error) { - config := ServiceConfig{} + svcConfig := ServiceConfig{} + + if uid := os.Getuid(); uid != 0 { + return svcConfig, fmt.Errorf("requires root permission") + } for _, opt := range opts { - if err := opt(&config); err != nil { - return config, err + if err := opt(&svcConfig); err != nil { + return svcConfig, err } } - return config, nil + // Creates service config file at `$HOME/.local/share/kraftkit/runtime/systemd/`. + byteJson, err := json.Marshal(svcConfig) + if err != nil { + return svcConfig, err + } + + systemdDir := filepath.Join(config.G[config.KraftKit](ctx).RuntimeDir, "systemd") + if err = os.MkdirAll(systemdDir, 0700); err != nil { + return svcConfig, err + } + + if err = os.WriteFile( + filepath.Join(systemdDir, svcConfig.Name+".json"), + byteJson, + 0644, + ); err != nil { + return svcConfig, err + } + + err = svcConfig.initService() + return svcConfig, err } -func (sc ServiceConfig) Create(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { - var err error - uid := os.Getuid() - if uid != 0 { - return machine, fmt.Errorf("requires root permission") +// GetMachineV1alpha1ServiceSystemdWrapper returns existing systemd service. +func GetMachineV1alpha1ServiceSystemdWrapper(ctx context.Context, name string) (ServiceConfig, error) { + svcConfig := ServiceConfig{} + + if uid := os.Getuid(); uid != 0 { + return svcConfig, fmt.Errorf("requires root permission") + } + + byteJson, err := os.ReadFile(filepath.Join(config.G[config.KraftKit](ctx).RuntimeDir, "systemd", name+".json")) + if err != nil { + // Return `Service is not pre-configured` + return svcConfig, err } - svcConfig := &service.Config{ - Name: sc.name, - DisplayName: sc.displayName, - Description: sc.description, - Dependencies: sc.dependencies, - Option: sc.option, + if err = json.Unmarshal(byteJson, &svcConfig); err != nil { + return svcConfig, err } + + err = svcConfig.initService() + return svcConfig, err +} + +// initService create systemd service. +func (sc *ServiceConfig) initService() error { + var err error sys := service.ChosenSystem() - sc.service, err = sys.New(&startStop{}, svcConfig) + sc.service, err = sys.New(&startStop{}, &service.Config{ + Name: sc.Name, + DisplayName: sc.DisplayName, + Description: sc.Description, + Dependencies: sc.Dependencies, + Arguments: sc.Arguments, + EnvVars: sc.EnvVars, + Option: sc.Option, + }) if err != nil { - return machine, err + return err } errs := make(chan error, 5) - sc.logger, err = sc.service.Logger(errs) + sc.logger, err = sc.service.SystemLogger(errs) if err != nil { - return machine, err + return err + } + + return nil +} + +// Create Install setups up the given service in the OS service manager. +// This may require greater rights. Will return an error if it is already installed. +func (sc ServiceConfig) Create(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + if uid := os.Getuid(); uid != 0 { + return machine, fmt.Errorf("requires root permission") } - err = sc.service.Install() + err := sc.service.Install() if err != nil { return machine, err } @@ -66,37 +125,67 @@ func (sc ServiceConfig) Create(ctx context.Context, machine *machinev1alpha1.Mac return machine, nil } +// Start signals to the OS service manager the given service should start. func (sc ServiceConfig) Start(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { // Implement `Start()` -> to start running systemd process, It also checks for the user permission same as above first. - return &machinev1alpha1.Machine{}, nil + if uid := os.Getuid(); uid != 0 { + return machine, fmt.Errorf("requires root permission") + } + + if err := sc.service.Start(); err != nil { + return machine, nil + } + + return machine, nil } func (sc ServiceConfig) Pause(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { - return &machinev1alpha1.Machine{}, nil + // This fuction can be upgraded in future as per need + // But right now it does same as Stop(). + return sc.Stop(ctx, machine) } +// Stop signals to the OS service manager the given service should stop. func (sc ServiceConfig) Stop(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { // Implement `Stop()` -> to stop running systemd process, It also checks for the user permission same as above first. - return &machinev1alpha1.Machine{}, nil + if uid := os.Getuid(); uid != 0 { + return machine, fmt.Errorf("requires root permission") + } + + if err := sc.service.Stop(); err != nil { + return machine, nil + } + return machine, nil } func (sc ServiceConfig) Update(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { - return &machinev1alpha1.Machine{}, nil + // Stop & Delete the existing service if possible and Create & Start (in case service was running) + // using Stop(), Delete(), Create() & Start() respectively. + return machine, nil } +// Delete removes the given service from the OS service manager. +// This may require greater rights. Will return an error if the service is not present. func (sc ServiceConfig) Delete(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { // Implement `Delete()` -> to uninstall systemd process, // It also checks for the user permission same as above first & stop the process if it's in running state. - return &machinev1alpha1.Machine{}, nil + if uid := os.Getuid(); uid != 0 { + return machine, fmt.Errorf("requires root permission") + } + + if err := sc.service.Uninstall(); err != nil { + return machine, nil + } + return machine, nil } func (sc ServiceConfig) Get(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { // Implement `Get()` -> to return the systemd process with following info: `name`, `status` & etc. - return &machinev1alpha1.Machine{}, nil + return machine, nil } func (sc ServiceConfig) List(ctx context.Context, machineList *machinev1alpha1.MachineList) (*machinev1alpha1.MachineList, error) { - return &machinev1alpha1.MachineList{}, nil + return machineList, nil } func (sc ServiceConfig) Watch(ctx context.Context, machine *machinev1alpha1.Machine) (chan *machinev1alpha1.Machine, chan error, error) {