diff --git a/sdk/bundles/bundle_test.go b/sdk/bundles/bundle_test.go new file mode 100644 index 0000000..061f81d --- /dev/null +++ b/sdk/bundles/bundle_test.go @@ -0,0 +1,34 @@ +package bundles_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/c3os-io/c3os/sdk/bundles" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Bundle", func() { + Context("install", func() { + PIt("installs packages from luet repos", func() { + dir, err := ioutil.TempDir("", "test") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(dir) + os.MkdirAll(filepath.Join(dir, "var", "tmp", "luet"), os.ModePerm) + err = RunBundles([]BundleOption{WithDBPath(dir), WithRootFS(dir), WithTarget("package://utils/edgevpn")}) + Expect(err).ToNot(HaveOccurred()) + Expect(filepath.Join(dir, "usr", "bin", "edgevpn")).To(BeARegularFile()) + }) + + It("installs from container images", func() { + dir, err := ioutil.TempDir("", "test") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(dir) + err = RunBundles([]BundleOption{WithDBPath(dir), WithRootFS(dir), WithTarget("container://quay.io/mocaccino/extra:edgevpn-utils-0.15.0")}) + Expect(err).ToNot(HaveOccurred()) + Expect(filepath.Join(dir, "usr", "bin", "edgevpn")).To(BeARegularFile()) + }) + }) +}) diff --git a/sdk/bundles/bundles.go b/sdk/bundles/bundles.go new file mode 100644 index 0000000..a94457d --- /dev/null +++ b/sdk/bundles/bundles.go @@ -0,0 +1,229 @@ +package bundles + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/pkg/utils" + "github.com/hashicorp/go-multierror" +) + +type BundleConfig struct { + Target string + Repository string + DBPath string + RootPath string +} + +// BundleOption defines a configuration option for a bundle. +type BundleOption func(bc *BundleConfig) error + +// Apply applies bundle options to the config. +func (bc *BundleConfig) Apply(opts ...BundleOption) error { + for _, o := range opts { + if err := o(bc); err != nil { + return err + } + } + return nil +} + +// WithDBPath sets the DB path for package installs. +// In case of luet packages will contain the db of the installed packages. +func WithDBPath(r string) BundleOption { + return func(bc *BundleConfig) error { + bc.DBPath = r + return nil + } +} + +func WithRootFS(r string) BundleOption { + return func(bc *BundleConfig) error { + bc.RootPath = r + return nil + } +} + +func WithRepository(r string) BundleOption { + return func(bc *BundleConfig) error { + bc.Repository = r + return nil + } +} + +func WithTarget(p string) BundleOption { + return func(bc *BundleConfig) error { + bc.Target = p + return nil + } +} + +func (bc *BundleConfig) extractRepo() (string, string, error) { + s := strings.Split(bc.Repository, "://") + if len(s) != 2 { + return "", "", fmt.Errorf("invalid repo schema") + } + return s[0], s[1], nil +} + +func defaultConfig() *BundleConfig { + return &BundleConfig{ + DBPath: "/usr/local/.c3os/db", + RootPath: "/", + Repository: "docker://quay.io/c3os/packages", + } +} + +type BundleInstaller interface { + Install(*BundleConfig) error +} + +// RunBundles runs bundles in a system. +// Accept a list of bundles options, which gets applied based on the bundle configuration. +func RunBundles(bundles ...[]BundleOption) error { + + // TODO: + // - Make provider consume bundles when bins are not detected in the rootfs + // - Default bundles preset in case of no binaries detected and version specified via config. + + var resErr error + for _, b := range bundles { + config := defaultConfig() + if err := config.Apply(b...); err != nil { + resErr = multierror.Append(err) + continue + } + + installer, err := NewBundleInstaller(*config) + if err != nil { + resErr = multierror.Append(err) + continue + } + dat := strings.Split(config.Target, "://") + if len(dat) != 2 { + resErr = multierror.Append(fmt.Errorf("invalid target")) + continue + } + config.Target = dat[1] + + err = installer.Install(config) + if err != nil { + resErr = multierror.Append(err) + continue + } + } + + return resErr +} + +func NewBundleInstaller(bc BundleConfig) (BundleInstaller, error) { + + dat := strings.Split(bc.Target, "://") + if len(dat) != 2 { + return nil, fmt.Errorf("could not decode scheme") + } + switch strings.ToLower(dat[0]) { + case "container": + return &ContainerInstaller{}, nil + case "run": + return &ContainerRunner{}, nil + case "package": + return &LuetInstaller{}, nil + + } + + return &LuetInstaller{}, nil +} + +// BundleInstall installs a bundle from a luet repo or a container image. +type ContainerRunner struct{} + +func (l *ContainerRunner) Install(config *BundleConfig) error { + + tempDir, err := ioutil.TempDir("", "containerrunner") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + out, err := utils.SH( + fmt.Sprintf( + `luet util unpack %s %s`, + config.Target, + tempDir, + ), + ) + if err != nil { + return fmt.Errorf("could not unpack container: %w - %s", err, out) + } + + out, err = utils.SH(fmt.Sprintf("CONTAINERDIR=%s %s/run.sh", tempDir, tempDir)) + if err != nil { + return fmt.Errorf("could not execute container: %w - %s", err, out) + } + return nil +} + +type ContainerInstaller struct{} + +func (l *ContainerInstaller) Install(config *BundleConfig) error { + + //mkdir -p test/etc/luet/repos.conf.d + out, err := utils.SH( + fmt.Sprintf( + `luet util unpack %s %s`, + config.Target, + config.RootPath, + ), + ) + if err != nil { + return fmt.Errorf("could not unpack bundle: %w - %s", err, out) + } + + return nil +} + +type LuetInstaller struct{} + +func (l *LuetInstaller) Install(config *BundleConfig) error { + + t, repo, err := config.extractRepo() + if err != nil { + return err + } + + err = os.MkdirAll(filepath.Join(config.RootPath, "etc/luet/repos.conf.d/"), os.ModePerm) + if err != nil { + return err + } + out, err := utils.SH( + fmt.Sprintf( + `LUET_CONFIG_FROM_HOST=false luet repo add --system-dbpath %s --system-target %s c3os-system -y --description "Automatically generated c3os-system" --url "%s" --type "%s"`, + config.DBPath, + config.RootPath, + repo, + t, + ), + ) + if err != nil { + return fmt.Errorf("could not add repository: %w - %s", err, out) + } + + out, err = utils.SH( + fmt.Sprintf( + `LUET_CONFIG_FROM_HOST=false luet install -y --system-dbpath %s --system-target %s %s`, + config.DBPath, + config.RootPath, + config.Target, + ), + ) + if err != nil { + return fmt.Errorf("could not install bundle: %w - %s", err, out) + } + + // copy bins to /usr/local/bin + return nil +} diff --git a/sdk/bus/events.go b/sdk/bus/events.go index 27f3814..10e87bf 100644 --- a/sdk/bus/events.go +++ b/sdk/bus/events.go @@ -18,6 +18,9 @@ const ( // EventBootstrap is issued to run any initial cluster configuration. EventBootstrap pluggable.EventType = "agent.bootstrap" + + // EventInstallPrompt is issued to request which config are required to ask to the user + EventInstallPrompt pluggable.EventType = "agent.installprompt" ) type InstallPayload struct { @@ -34,3 +37,35 @@ type BootstrapPayload struct { type EventPayload struct { Config string `json:"config"` } + +// AllEvents is a convenience list of all the events streamed from the bus. +var AllEvents = []pluggable.EventType{ + EventBootstrap, + EventChallenge, + EventBoot, + EventInstall, +} + +// IsEventDefined checks wether an event is defined in the bus. +// It accepts strings or EventType, returns a boolean indicating that +// the event was defined among the events emitted by the bus. +func IsEventDefined(i interface{}) bool { + checkEvent := func(e pluggable.EventType) bool { + for _, ee := range AllEvents { + if ee == e { + return true + } + } + + return false + } + + switch f := i.(type) { + case string: + return checkEvent(pluggable.EventType(f)) + case pluggable.EventType: + return checkEvent(f) + default: + return false + } +} diff --git a/sdk/bus/hooks.go b/sdk/bus/hooks.go new file mode 100644 index 0000000..b87bc47 --- /dev/null +++ b/sdk/bus/hooks.go @@ -0,0 +1,18 @@ +package bus + +import ( + "os" + "os/exec" +) + +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() +}