diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 21f0d8e..9ec1c9e 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -1,44 +1,69 @@ package main import ( - "os" "log" - "ebs-bootstrap/internal/config" - "ebs-bootstrap/internal/service" - "ebs-bootstrap/internal/utils" - "ebs-bootstrap/internal/state" + "os" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/layer" + "github.com/reecetech/ebs-bootstrap/internal/service" + "github.com/reecetech/ebs-bootstrap/internal/utils" ) func main() { - // Disable Timetamp log.SetFlags(0) - e := utils.NewExecRunner() - ds := &service.LinuxDeviceService{Runner: e} - ns := &service.AwsNVMeService{} - fs := &service.UnixFileService{} - dts := &service.EbsDeviceTranslator{DeviceService: ds, NVMeService: ns} - dt, err := dts.GetTranslator() - if err != nil { - log.Fatal(err) - } - config, err := config.New(os.Args, dt, fs) + // Services + rc := utils.NewRunnerCache() + ufs := service.NewUnixFileService() + lds := service.NewLinuxDeviceService(rc) + uos := service.NewUnixOwnerService() + ans := service.NewAwsNitroNVMeService() + + // Config + Flags + c, f, err := config.Parse(os.Args) + checkError(err) + + // Service + Config Consumers + db := backend.NewLinuxDeviceBackend(lds) + fb := backend.NewLinuxFileBackend(ufs) + ub := backend.NewLinuxOwnerBackend(uos) + ae := action.NewActionExecutor(rc, c) + + // Modify Config + bm := config.NewBatchModifier([]config.Modifier{ + config.NewOverridesModifier(f), + config.NewAwsNVMeDriverModifier(ans, lds), + }) + checkError(bm.Modify(c)) + + // Validate Config + bv := config.NewBatchValidator([]config.Validator{ + config.NewDeviceValidator(lds), + config.NewFileSystemValidator(), + config.NewModeValidator(), + config.NewMountPointValidator(), + config.NewPermissionsValidator(), + config.NewOwnerValidator(uos), + }) + checkError(bv.Validate(c)) + // Layers + layers := layer.NewLayerExecutor([]layer.Layer{ + layer.NewFormatDeviceLayer(db), + layer.NewLabelDeviceLayer(db), + layer.NewUnmountDeviceLayer(db, fb), + layer.NewCreateDirectoryLayer(db, fb), + layer.NewMountDeviceLayer(db, fb), + layer.NewChangeOwnerLayer(ub, fb), + layer.NewChangePermissionsLayer(fb), + }) + checkError(layers.Execute(c, ae)) +} + +func checkError(err error) { if err != nil { log.Fatal(err) } - - for name, device := range config.Devices { - d, err := state.NewDevice(name, ds, fs) - if err != nil { - log.Fatal(err) - } - err = d.Diff(config) - if err == nil { - log.Printf("🟢 %s: No changes detected", name) - continue - } - if device.Mode == "healthcheck" { - log.Fatal(err) - } - } } diff --git a/configs/ebs-bootstrap.yml b/configs/ebs-bootstrap.yml deleted file mode 100644 index e6df7ff..0000000 --- a/configs/ebs-bootstrap.yml +++ /dev/null @@ -1,11 +0,0 @@ -global: - mode: healthcheck -devices: - /dev/xvdf: - fs: "xfs" - mount_point: "/mnt/app" - owner: 1000 - group: 1000 - permissions: 755 - label: "external-vol" - mode: healthcheck diff --git a/configs/ubuntu.yml b/configs/ubuntu.yml new file mode 100644 index 0000000..cbe8919 --- /dev/null +++ b/configs/ubuntu.yml @@ -0,0 +1,11 @@ +defaults: + mode: healthcheck +devices: + /dev/vdb: + fs: ext4 + label: lasith-rules + mountPoint: /ifmx/dev/james + mountOptions: defaults + group: ubuntu + user: ubuntu + permissions: 644 \ No newline at end of file diff --git a/go.mod b/go.mod index 88788c8..a14c7c3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module ebs-bootstrap +module "github.com/reecetech/ebs-bootstrap" go 1.21 diff --git a/internal/action/action.go b/internal/action/action.go new file mode 100644 index 0000000..efb41cc --- /dev/null +++ b/internal/action/action.go @@ -0,0 +1,110 @@ +package action + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +const ( + // Device operations like mounting and formatting are + // delegeated to respective C-based tools like `mount` and `mkfs`. + // From experience, we need to introduce a slight delay to ensure + // that the file-system is eventually consistent with any changes + // that were performed + DefaultDeviceActionDelay = 100 * time.Millisecond + // File changes like os.Chown and os.Chmod are performed + // natively through golang standard libraries. Since these standard + // libraries are making direct syscalls, changes are reflected almost + // immidiately on the file-system. Therefore a delay is not required + // for actions that peform file changes + DefaultFileActionDelay = 0 +) + +type Action interface { + Execute(rc *utils.RunnerCache) error + GetdeviceName() string + Success() string + Prompt() string + Refuse() string + IsTrusted() bool + GetDelay() time.Duration +} + +type ActionExecutor struct { + runnerCache *utils.RunnerCache + config *config.Config +} + +func NewActionExecutor(rc *utils.RunnerCache, c *config.Config) *ActionExecutor { + return &ActionExecutor{ + runnerCache: rc, + config: c, + } +} + +func (ae *ActionExecutor) ExecuteAction(action Action) error { + name := action.GetdeviceName() + mode, err := ae.config.GetMode(name) + if err != nil { + return err + } + + switch mode { + case config.Prompt: + if !ae.ShouldProceed(action) { + return fmt.Errorf("🔴 Action rejected. %s", action.Refuse()) + } + case config.Healtcheck: + // Special handling for trusted actions when device is under + // healthcheck mode + if action.IsTrusted() { + if ae.config.GetSkipTrustedActions() { + log.Printf("🙅 Skipped trusted action. %s", action.Refuse()) + return nil + } + break + } + return fmt.Errorf("🔴%s: Healthcheck mode enabled. %s", name, action.Refuse()) + } + return ae.executeAction(action) +} + +func (ae *ActionExecutor) executeAction(action Action) error { + if err := action.Execute(ae.runnerCache); err != nil { + return err + } + log.Printf("⭐ %s: %s", action.GetdeviceName(), action.Success()) + // Add a delay because from experience, the operating system needs some time + // to catch up to device changes performed through C tools like e2label and mkfs.ext4 + time.Sleep(action.GetDelay()) + return nil +} + +func (ae *ActionExecutor) ExecuteActions(actions []Action) error { + for _, action := range actions { + err := ae.ExecuteAction(action) + if err != nil { + return err + } + } + return nil +} + +func (ae *ActionExecutor) ShouldProceed(action Action) bool { + prompt := action.Prompt() + + fmt.Printf("🟣 %s? (y/n): ", prompt) + var response string + fmt.Scanln(&response) + + response = strings.ToLower(response) + if response == "y" || response == "yes" { + return true + } + return false +} diff --git a/internal/action/file.go b/internal/action/file.go new file mode 100644 index 0000000..ecfc137 --- /dev/null +++ b/internal/action/file.go @@ -0,0 +1,144 @@ +package action + +import ( + "fmt" + "os" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +const ( + DefaultDirectoryPermissions = os.FileMode(0755) +) + +type CreateDirectoryAction struct { + deviceName string + path string +} + +func NewCreateDirectoryAction(dn string, p string) *CreateDirectoryAction { + return &CreateDirectoryAction{ + deviceName: dn, + path: p, + } +} + +func (a *CreateDirectoryAction) Execute(rc *utils.RunnerCache) error { + return os.MkdirAll(a.path, DefaultDirectoryPermissions) +} + +func (a *CreateDirectoryAction) Prompt() string { + return fmt.Sprintf("Would you like to recursively create directory %s", a.path) +} + +func (a *CreateDirectoryAction) GetdeviceName() string { + return a.deviceName +} + +func (a *CreateDirectoryAction) Refuse() string { + return fmt.Sprintf("Refused to create directory %s", a.path) +} + +func (a *CreateDirectoryAction) Success() string { + return fmt.Sprintf("Successfully created directory %s", a.path) +} + +func (a *CreateDirectoryAction) IsTrusted() bool { + return false +} + +func (a *CreateDirectoryAction) GetDelay() time.Duration { + return DefaultFileActionDelay +} + +type ChangeOwnerAction struct { + deviceName string + path string + uid int + gid int +} + +func NewChangeOwnerAction(dn string, p string, uid int, gid int) *ChangeOwnerAction { + return &ChangeOwnerAction{ + deviceName: dn, + path: p, + uid: uid, + gid: gid, + } +} + +func (a *ChangeOwnerAction) Execute(rc *utils.RunnerCache) error { + return os.Chown(a.path, a.uid, a.gid) +} + +func (a *ChangeOwnerAction) Prompt() string { + return fmt.Sprintf("Would you like to change ownership (%d:%d) of %s", a.uid, a.gid, a.path) +} + +func (a *ChangeOwnerAction) GetdeviceName() string { + return a.deviceName +} + +func (a *ChangeOwnerAction) Refuse() string { + return fmt.Sprintf("Refused to to change ownership (%d:%d) of %s", a.uid, a.gid, a.path) +} + +func (a *ChangeOwnerAction) Success() string { + return fmt.Sprintf("Successfully changed ownership (%d:%d) of %s", a.uid, a.gid, a.path) +} + +func (a *ChangeOwnerAction) IsTrusted() bool { + return false +} + +func (a *ChangeOwnerAction) GetDelay() time.Duration { + return DefaultFileActionDelay +} + +type ChangePermissionsAction struct { + deviceName string + path string + perms model.Permissions +} + +func NewChangePermissions(dn string, p string, perms model.Permissions) *ChangePermissionsAction { + return &ChangePermissionsAction{ + deviceName: dn, + path: p, + perms: perms, + } +} + +func (a *ChangePermissionsAction) Execute(rc *utils.RunnerCache) error { + mode, err := a.perms.ToFileMode() + if err != nil { + return err + } + return os.Chmod(a.path, mode) +} + +func (a *ChangePermissionsAction) Prompt() string { + return fmt.Sprintf("Would you like to change permissions of %s to %s", a.path, a.perms) +} + +func (a *ChangePermissionsAction) GetdeviceName() string { + return a.deviceName +} + +func (a *ChangePermissionsAction) Refuse() string { + return fmt.Sprintf("Refused to to change permissions of %s to %s", a.path, a.perms) +} + +func (a *ChangePermissionsAction) Success() string { + return fmt.Sprintf("Successfully change permissions of %s to %s", a.path, a.perms) +} + +func (a *ChangePermissionsAction) IsTrusted() bool { + return false +} + +func (a *ChangePermissionsAction) GetDelay() time.Duration { + return DefaultFileActionDelay +} diff --git a/internal/action/format.go b/internal/action/format.go new file mode 100644 index 0000000..9205a5e --- /dev/null +++ b/internal/action/format.go @@ -0,0 +1,94 @@ +package action + +import ( + "fmt" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +type FormatExt4Action struct { + deviceName string +} + +func NewFormatExt4Action(dn string) *FormatExt4Action { + return &FormatExt4Action{ + deviceName: dn, + } +} + +func (a *FormatExt4Action) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.MkfsExt4) + _, err := r.Command(a.deviceName) + if err != nil { + return err + } + return nil +} + +func (a *FormatExt4Action) Prompt() string { + return fmt.Sprintf("Would you like to format %s to ext4", a.deviceName) +} + +func (a *FormatExt4Action) GetdeviceName() string { + return a.deviceName +} + +func (a *FormatExt4Action) Refuse() string { + return "Refused to format to ext4" +} + +func (a *FormatExt4Action) Success() string { + return "Successfully formated to ext4" +} + +func (a *FormatExt4Action) IsTrusted() bool { + return false +} + +func (a *FormatExt4Action) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} + +type FormatXfsAction struct { + deviceName string +} + +func NewFormatXfsAction(dn string) *FormatXfsAction { + return &FormatXfsAction{ + deviceName: dn, + } +} + +func (a *FormatXfsAction) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.MkfsXfs) + _, err := r.Command(a.deviceName) + if err != nil { + return err + } + return nil +} + +func (a *FormatXfsAction) Prompt() string { + return fmt.Sprintf("Would you like to format %s to XFS?", a.deviceName) +} + +func (a *FormatXfsAction) GetdeviceName() string { + return a.deviceName +} + +func (a *FormatXfsAction) Refuse() string { + return "Refused to format to XFS" +} + +func (a *FormatXfsAction) Success() string { + return "Successfully formated to XFS" +} + +func (a *FormatXfsAction) IsTrusted() bool { + return false +} + +func (a *FormatXfsAction) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} diff --git a/internal/action/label.go b/internal/action/label.go new file mode 100644 index 0000000..5af7182 --- /dev/null +++ b/internal/action/label.go @@ -0,0 +1,104 @@ +package action + +import ( + "fmt" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +type LabelExt4Action struct { + deviceName string + label string +} + +func NewLabelExt4Action(dn string, label string) (*LabelExt4Action, error) { + if len(label) > 16 { + return nil, fmt.Errorf("🔴 %s: Label cannot exceed 16 characters for the ext4 file system", label) + } + return &LabelExt4Action{ + deviceName: dn, + label: label, + }, nil +} + +func (a *LabelExt4Action) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.E2Label) + _, err := r.Command(a.deviceName, a.label) + if err != nil { + return err + } + return nil +} + +func (a *LabelExt4Action) Prompt() string { + return fmt.Sprintf("Would you like to label device %s to %s", a.deviceName, a.label) +} + +func (a *LabelExt4Action) GetdeviceName() string { + return a.deviceName +} + +func (a *LabelExt4Action) Refuse() string { + return fmt.Sprintf("Refused to label to %s", a.label) +} + +func (a *LabelExt4Action) Success() string { + return fmt.Sprintf("Successfully labelled to %s", a.label) +} + +func (a *LabelExt4Action) IsTrusted() bool { + return false +} + +func (a *LabelExt4Action) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} + +type LabelXfsAction struct { + deviceName string + label string +} + +func NewLabelXfsAction(dn string, label string) (*LabelXfsAction, error) { + if len(label) > 12 { + return nil, fmt.Errorf("🔴 %s: Label cannot exceed 12 characters for the XFS file system", label) + } + return &LabelXfsAction{ + deviceName: dn, + label: label, + }, nil +} + +func (a *LabelXfsAction) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.XfsAdmin) + _, err := r.Command("-L", a.label, a.deviceName) + if err != nil { + return err + } + return nil +} + +func (a *LabelXfsAction) Prompt() string { + return fmt.Sprintf("Would you like to label device %s to %s", a.deviceName, a.label) +} + +func (a *LabelXfsAction) GetdeviceName() string { + return a.deviceName +} + +func (a *LabelXfsAction) Refuse() string { + return fmt.Sprintf("Refused to label to %s", a.label) +} + +func (a *LabelXfsAction) Success() string { + return fmt.Sprintf("Successfully labelled to %s", a.label) +} + +func (a *LabelXfsAction) IsTrusted() bool { + return false +} + +func (a *LabelXfsAction) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} diff --git a/internal/action/mount.go b/internal/action/mount.go new file mode 100644 index 0000000..8b6f51e --- /dev/null +++ b/internal/action/mount.go @@ -0,0 +1,152 @@ +package action + +import ( + "fmt" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +type MountDeviceAction struct { + source string + target string + fileSystem model.FileSystem + options model.MountOptions +} + +func NewMountDeviceAction(source string, target string, fs model.FileSystem, options model.MountOptions) *MountDeviceAction { + return &MountDeviceAction{ + source: source, + target: target, + fileSystem: fs, + options: options.Remount(false), + } +} + +func (a *MountDeviceAction) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.Mount) + _, err := r.Command(a.source, "-t", string(a.fileSystem), "-o", string(a.options), a.target) + if err != nil { + return err + } + return nil +} + +func (a *MountDeviceAction) Prompt() string { + return fmt.Sprintf("Would you like to mount %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *MountDeviceAction) GetdeviceName() string { + return a.source +} + +func (a *MountDeviceAction) Refuse() string { + return fmt.Sprintf("Refused to mount %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *MountDeviceAction) Success() string { + return fmt.Sprintf("Successfully mounted %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *MountDeviceAction) IsTrusted() bool { + return false +} + +func (a *MountDeviceAction) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} + +type RemountDeviceAction struct { + source string + target string + fileSystem model.FileSystem + options model.MountOptions +} + +func NewRemountDeviceAction(source string, target string, fs model.FileSystem, options model.MountOptions) *RemountDeviceAction { + return &RemountDeviceAction{ + source: source, + target: target, + fileSystem: fs, + options: options.Remount(true), + } +} + +func (a *RemountDeviceAction) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.Mount) + _, err := r.Command(a.source, "-t", string(a.fileSystem), "-o", string(a.options), a.target) + if err != nil { + return err + } + return nil +} + +func (a *RemountDeviceAction) Prompt() string { + return fmt.Sprintf("Would you like to remount %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *RemountDeviceAction) GetdeviceName() string { + return a.source +} + +func (a *RemountDeviceAction) Refuse() string { + return fmt.Sprintf("Refused to remount %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *RemountDeviceAction) Success() string { + return fmt.Sprintf("Successfully remounted %s to %s (%s)", a.source, a.target, a.options) +} + +func (a *RemountDeviceAction) IsTrusted() bool { + return true +} + +func (a *RemountDeviceAction) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} + +type UnmountDeviceAction struct { + source string + target string +} + +func NewUnmountDeviceAction(source string, target string) *UnmountDeviceAction { + return &UnmountDeviceAction{ + source: source, + target: target, + } +} + +func (a *UnmountDeviceAction) Execute(rc *utils.RunnerCache) error { + r := rc.GetRunner(utils.Umount) + _, err := r.Command(a.target) + if err != nil { + return err + } + return nil +} + +func (a *UnmountDeviceAction) Prompt() string { + return fmt.Sprintf("Would you like to unmount %s from %s", a.source, a.target) +} + +func (a *UnmountDeviceAction) GetdeviceName() string { + return a.source +} + +func (a *UnmountDeviceAction) Refuse() string { + return fmt.Sprintf("Refused to unmount %s from %s", a.source, a.target) +} + +func (a *UnmountDeviceAction) Success() string { + return fmt.Sprintf("Successfully unmounted %s from %s", a.source, a.target) +} + +func (a *UnmountDeviceAction) IsTrusted() bool { + return false +} + +func (a *UnmountDeviceAction) GetDelay() time.Duration { + return DefaultDeviceActionDelay +} diff --git a/internal/backend/device.go b/internal/backend/device.go new file mode 100644 index 0000000..8a45702 --- /dev/null +++ b/internal/backend/device.go @@ -0,0 +1,90 @@ +package backend + +import ( + "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type DeviceBackend interface { + GetBlockDevice(device string) (*model.BlockDevice, error) + Label(bd *model.BlockDevice, label string) (action.Action, error) + Format(bd *model.BlockDevice, fileSystem model.FileSystem) (action.Action, error) + Mount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action + Remount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action + Umount(bd *model.BlockDevice) action.Action + From(config *config.Config) error +} + +type LinuxDeviceBackend struct { + blockDevices map[string]*model.BlockDevice + deviceService service.DeviceService +} + +func NewLinuxDeviceBackend(ds service.DeviceService) *LinuxDeviceBackend { + return &LinuxDeviceBackend{ + blockDevices: map[string]*model.BlockDevice{}, + deviceService: ds, + } +} + +func (db *LinuxDeviceBackend) GetBlockDevice(device string) (*model.BlockDevice, error) { + blockDevice, exists := db.blockDevices[device] + if !exists { + return nil, fmt.Errorf("🔴 %s: Could not find block device", device) + } + return blockDevice, nil +} + +func (db *LinuxDeviceBackend) Label(bd *model.BlockDevice, label string) (action.Action, error) { + switch bd.FileSystem { + case model.Ext4: + return action.NewLabelExt4Action(bd.Name, label) + case model.Xfs: + return action.NewLabelXfsAction(bd.Name, label) + default: + return nil, fmt.Errorf("🔴 %s: Can not label a device with no file system", bd.Name) + } +} + +func (db *LinuxDeviceBackend) Format(bd *model.BlockDevice, fs model.FileSystem) (action.Action, error) { + switch fs { + case model.Ext4: + return action.NewFormatExt4Action(bd.Name), nil + case model.Xfs: + return action.NewFormatXfsAction(bd.Name), nil + default: + return nil, fmt.Errorf("🔴 %s: Can not erase the file system of a device", bd.Name) + } +} + +func (db *LinuxDeviceBackend) Mount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action { + return action.NewMountDeviceAction(bd.Name, target, bd.FileSystem, options) +} + +func (db *LinuxDeviceBackend) Remount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action { + return action.NewRemountDeviceAction(bd.Name, target, bd.FileSystem, options) +} + +func (db *LinuxDeviceBackend) Umount(bd *model.BlockDevice) action.Action { + return action.NewUnmountDeviceAction(bd.Name, bd.MountPoint) +} + +func (db *LinuxDeviceBackend) From(config *config.Config) error { + // Clear in memory representation of devices + for k := range db.blockDevices { + delete(db.blockDevices, k) + } + + for name := range config.Devices { + d, err := db.deviceService.GetBlockDevice(name) + if err != nil { + return err + } + db.blockDevices[d.Name] = d + } + return nil +} diff --git a/internal/backend/file.go b/internal/backend/file.go new file mode 100644 index 0000000..5793e9c --- /dev/null +++ b/internal/backend/file.go @@ -0,0 +1,105 @@ +package backend + +import ( + "fmt" + "os" + "path" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type FileBackend interface { + CreateDirectory(device string, p string) action.Action + ChangeOwner(device string, p string, uid int, gid int) action.Action + ChangePermissions(device string, p string, perms model.Permissions) action.Action + GetDirectory(p string) (*model.File, error) + IsMount(p string) bool + From(config *config.Config) error +} + +type LinuxFileBackend struct { + files map[string]*model.File + fileService service.FileService +} + +func NewLinuxFileBackend(fs service.FileService) *LinuxFileBackend { + return &LinuxFileBackend{ + files: map[string]*model.File{}, + fileService: fs, + } +} + +func (lfb *LinuxFileBackend) CreateDirectory(device string, p string) action.Action { + return action.NewCreateDirectoryAction(device, p) +} + +func (lfb *LinuxFileBackend) ChangeOwner(device string, p string, uid int, gid int) action.Action { + return action.NewChangeOwnerAction(device, p, uid, gid) +} + +func (lfb *LinuxFileBackend) ChangePermissions(device string, p string, perms model.Permissions) action.Action { + return action.NewChangePermissions(device, p, perms) +} + +func (lfb *LinuxFileBackend) GetDirectory(p string) (*model.File, error) { + f, exists := lfb.files[p] + if !exists { + return nil, os.ErrNotExist + } + if f.Type != model.Directory { + return nil, fmt.Errorf("🔴 %s: File is not a directory", p) + } + return f, nil +} + +func (lfb *LinuxFileBackend) IsMount(p string) bool { + child, exists := lfb.files[p] + if !exists { + return false + } + if child.Type != model.Directory { + return false + } + parent, exists := lfb.files[path.Dir(p)] + if !exists { + return false + } + if child.DeviceId != parent.DeviceId { + return true + } + return child.InodeNo == parent.InodeNo +} + +func (lfb *LinuxFileBackend) From(config *config.Config) error { + // Clear in memory representation of files + for k := range lfb.files { + delete(lfb.files, k) + } + + for _, cd := range config.Devices { + // For certain file operations, it is essential that we + // can query the parent directory. Therefore, lets pull the + // state of the parent directory + files := []string{cd.MountPoint, path.Dir(cd.MountPoint)} + for _, file := range files { + _, exists := lfb.files[file] + if exists { + continue + } + f, err := lfb.fileService.GetFile(file) + if err != nil { + // Gracefully continue loop if the error is because + // the file does not exist. + if os.IsNotExist(err) { + continue + } + return err + } + lfb.files[f.Path] = f + } + } + return nil +} diff --git a/internal/backend/owner.go b/internal/backend/owner.go new file mode 100644 index 0000000..fa1c08d --- /dev/null +++ b/internal/backend/owner.go @@ -0,0 +1,73 @@ +package backend + +import ( + "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type OwnerBackend interface { + GetUser(user string) (*model.User, error) + GetGroup(group string) (*model.Group, error) + From(config *config.Config) error +} + +type LinuxOwnerBackend struct { + users map[string]*model.User + groups map[string]*model.Group + ownerService service.OwnerService +} + +func NewLinuxOwnerBackend(ows service.OwnerService) *LinuxOwnerBackend { + return &LinuxOwnerBackend{ + users: map[string]*model.User{}, + groups: map[string]*model.Group{}, + ownerService: ows, + } +} + +func (lfb *LinuxOwnerBackend) GetUser(user string) (*model.User, error) { + o, exists := lfb.users[user] + if !exists { + return nil, fmt.Errorf("🔴 User %s does not exist", user) + } + return o, nil +} + +func (lfb *LinuxOwnerBackend) GetGroup(group string) (*model.Group, error) { + g, exists := lfb.groups[group] + if !exists { + return nil, fmt.Errorf("🔴 Group %s does not exist", group) + } + return g, nil +} + +func (lfb *LinuxOwnerBackend) From(config *config.Config) error { + // Clear in memory representation of files + for k := range lfb.users { + delete(lfb.users, k) + } + for k := range lfb.groups { + delete(lfb.groups, k) + } + + for _, cd := range config.Devices { + if cd.User != "" { + o, err := lfb.ownerService.GetUser(cd.User) + if err != nil { + return err + } + lfb.users[cd.User] = o + } + if cd.Group != "" { + g, err := lfb.ownerService.GetGroup(cd.Group) + if err != nil { + return err + } + lfb.groups[cd.Group] = g + } + } + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index ed37803..2a1634e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,99 +1,144 @@ package config import ( - "os" - "fmt" - "flag" "bytes" - "gopkg.in/yaml.v2" - "ebs-bootstrap/internal/service" + "flag" + "fmt" + "os" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "gopkg.in/yaml.v2" +) + +type Mode string + +const ( + Empty Mode = "" + Healtcheck Mode = "healthcheck" + Prompt Mode = "prompt" + Force Mode = "force" ) -type ConfigDevice struct { - Fs string `yaml:"fs"` - MountPoint string `yaml:"mount_point"` - Owner string `yaml:"owner"` - Group string `yaml:"group"` - Label string `yaml:"label"` - Permissions string `yaml:"permissions"` - Mode string `yaml:"mode"` +func (m Mode) String() string { + return string(m) +} + +func ParseMode(s string) (Mode, error) { + Modes := map[Mode]struct{}{ + Empty: {}, + Healtcheck: {}, + Prompt: {}, + Force: {}, + } + fst := Mode(s) + _, ok := Modes[fst] + if !ok { + return fst, fmt.Errorf("🔴 %s: Mode is not supported", s) + } + return fst, nil +} + +type Flag struct { + Config string + Mode string + SkipTrustedActions bool +} + +type Device struct { + Fs model.FileSystem `yaml:"fs"` + MountPoint string `yaml:"mountPoint"` + MountOptions model.MountOptions `yaml:"mountOptions"` + User string `yaml:"user"` + Group string `yaml:"group"` + Label string `yaml:"label"` + Permissions model.Permissions `yaml:"permissions"` + Mode Mode `yaml:"mode"` } -type ConfigGlobal struct { - Mode string `yaml:"mode"` +type Defaults struct { + Mode Mode `yaml:"mode"` + SkipTrustedActions bool `yaml:"skipTrustedActions"` +} + +type Overrides struct { + Mode Mode `yaml:"mode"` + SkipTrustedActions bool `yaml:"skipTrustedActions"` } type Config struct { - Global ConfigGlobal `yaml:"global"` - Devices map[string]ConfigDevice `yaml:"devices"` + Defaults Defaults `yaml:"defaults"` + Overrides Overrides `yaml:"overrides"` + Devices map[string]Device `yaml:"devices"` } -func New(args []string, dt *service.DeviceTranslator, fs service.FileService) (*Config, error) { - // Generate path of config - cp, err := parseFlags(args[0], args[1:]) +func Parse(args []string) (*Config, *Flag, error) { + // Generate config path + flag, err := parseFlags(args[0], args[1:]) if err != nil { fmt.Fprint(os.Stderr, err) - return nil, fmt.Errorf("🔴 Failed to parse provided flags") - } - - // Validate the path first - if err := fs.ValidateFile(cp); err != nil { - return nil, err - } + return nil, nil, fmt.Errorf("🔴 Failed to parse provided flags") + } - // Create config structure - config := &Config{} + // Create config structure + config := &Config{} - // Load config file into memory as byte[] - file, err := os.ReadFile(cp) - if err != nil { - return nil, err - } + // Load config file into memory + file, err := os.ReadFile(flag.Config) + if err != nil { + return nil, nil, err + } - // Unmarshal YAML file from memory into struct + // Unmarshal YAML file from memory into struct err = yaml.UnmarshalStrict(file, config) - if err != nil { + if err != nil { fmt.Fprintln(os.Stderr, err) - return nil, fmt.Errorf("🔴 Failed to ingest malformed config") - } - - // Layer modifications to the Config. These modifiers will incrementally - // transform the Config until it reaches a desired state - modifiers := []Modifiers{ - &OwnerModifier{}, - &DeviceModifier{ - DeviceTranslator: dt, - }, - &GroupModifier{}, - &DeviceModeModifier{}, + return nil, nil, fmt.Errorf("🔴 %s: Failed to ingest malformed config", flag.Config) } - for _, modifier := range modifiers { - err = modifier.Modify(config) - if err != nil { - return nil, err - } - } - return config, nil + + return config, flag, nil } -func parseFlags(program string, args []string) (string, error) { +func parseFlags(program string, args []string) (*Flag, error) { flags := flag.NewFlagSet(program, flag.ContinueOnError) var buf bytes.Buffer flags.SetOutput(&buf) - // String that contains the configured configuration path - var cp string + // String that contains the configured configuration path + flag := &Flag{} // String that contains the mode of bootstrap process - // Set up a CLI flag called "-config" to allow users - // to supply the configuration file - flags.StringVar(&cp, "config", "/etc/ebs-bootstrap/config.yml", "path to config file") + // Set up a CLI flag called "-config" to allow users + // to supply the configuration file + flags.StringVar(&flag.Config, "config", "/etc/ebs-bootstrap/config.yml", "path to config file") + flags.StringVar(&flag.Mode, "mode", "", "override for mode") + flags.BoolVar(&flag.SkipTrustedActions, "skip-trusted-actions", false, "skip trusted actions") - // Actually parse the flag - err := flags.Parse(args) + // Actually parse the flag + err := flags.Parse(args) if err != nil { - return "", fmt.Errorf(buf.String()) + return nil, fmt.Errorf(buf.String()) } - // Return the configuration path - return cp, nil + // Return the configuration path + return flag, nil +} + +func (c *Config) GetMode(name string) (Mode, error) { + cd, found := c.Devices[name] + if !found { + return Empty, fmt.Errorf("🔴 %s: Could not find device in config", name) + } + if c.Overrides.Mode != Empty { + return c.Overrides.Mode, nil + } + if cd.Mode == Empty && c.Defaults.Mode != Empty { + return c.Defaults.Mode, nil + } + if cd.Mode != Empty { + return cd.Mode, nil + } + return Empty, fmt.Errorf("🔴 %s: Ensure that you have provided a supported mode locally or globally", name) +} + +func (c *Config) GetSkipTrustedActions() bool { + return c.Overrides.SkipTrustedActions || c.Defaults.SkipTrustedActions } diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index dc03b3a..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package config - -import ( - "fmt" - "os" - "testing" - "ebs-bootstrap/internal/utils" - "ebs-bootstrap/internal/service" - "github.com/google/go-cmp/cmp" -) - -var dt = &service.DeviceTranslator{ - Table: map[string]string{ - "/dev/xvdf": "/dev/nvme0n1", - "/dev/nvme0n1": "/dev/nvme0n1", - }, -} - -var fs = &service.UnixFileService{} - -func TestConfigParsing(t *testing.T) { - u, g, err := utils.GetCurrentUserGroup() - if err != nil { - t.Error(err) - return - } - subtests := []struct{ - Name string - Data []byte - ExpectedOutput *Config - ExpectedErr error - }{ - { - Name: "Valid Config", - Data: []byte(fmt.Sprintf(`--- -global: - mode: healthcheck -devices: - /dev/xvdf: - fs: "xfs" - mount_point: "/ifmx/dev/root" - owner: "%s" - group: "%s" - permissions: 755 - label: "external-vol"`,u.Name, g.Name)), - ExpectedOutput: &Config{ - Global: ConfigGlobal{ - Mode: "healthcheck", - }, - Devices: map[string]ConfigDevice{ - "/dev/nvme0n1": ConfigDevice{ - Fs: "xfs", - MountPoint: "/ifmx/dev/root", - Owner: u.Uid, - Group: g.Gid, - Permissions: "755", - Label: "external-vol", - Mode: "healthcheck", - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Malformed Config", - Data: []byte(`--- -global: - mode: healthcheck -devices:: - /dev/xvdf: - bad_attribute: false`), - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("🔴 Failed to ingest malformed config"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - configPath, err := createConfigFile(subtest.Data) - if err != nil { - t.Errorf("createConfigFile() [error] %s", err) - } - defer os.Remove(configPath) - - c, err := New( - []string{"ebs-bootstrap-test", "-config", configPath}, - dt, - fs, - ) - if !cmp.Equal(c, subtest.ExpectedOutput) { - t.Errorf("Modify() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, c) - } - utils.CheckError("config.New()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestFlagParsing(t *testing.T) { - u, g, err := utils.GetCurrentUserGroup() - if err != nil { - t.Error(err) - return - } - // Create a variable to the path of a valid config - c, err := createConfigFile([]byte(fmt.Sprintf(`--- -global: - mode: healthcheck -devices: - /dev/xvdf: - fs: "xfs" - mount_point: "/ifmx/dev/root" - owner: "%s" - group: "%s" - permissions: 755 - label: "external-vol"`,u.Uid, g.Gid))) - if err != nil { - t.Errorf("createConfigFile() [error] %s", err) - return - } - - // Create a variable to the current working directory - d, err := os.Getwd() - if err != nil { - t.Errorf("os.Getwd() [error] %s", err) - } - - subtests := []struct{ - Name string - Args []string - ExpectedErr error - }{ - { - Name: "Valid Config", - Args: []string{"ebs-bootstrap-test","-config",c}, - ExpectedErr: nil, - }, - { - Name: "Invalid Config (Directory)", - Args: []string{"ebs-bootstrap-test","-config",d}, - ExpectedErr: fmt.Errorf("🔴 %s is not a regular file", d), - }, - { - Name: "Invalid Config (Non-existent File)", - Args: []string{"ebs-bootstrap-test","-config","/doesnt-exist"}, - ExpectedErr: fmt.Errorf("🔴 /doesnt-exist does not exist"), - }, - { - Name: "Unsupported Flag", - Args: []string{"ebs-bootstrap-test","-unsupported-flag"}, - ExpectedErr: fmt.Errorf("🔴 Failed to parse provided flags"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - _, err := New( - subtest.Args, - dt, - fs, - ) - utils.CheckError("config.New()", t, subtest.ExpectedErr, err) - }) - } -} - -func createConfigFile(data []byte) (string, error) { - f, err := os.CreateTemp("", "config_test_*.yml") - if err != nil { - return "", fmt.Errorf("🔴 Failed to create temporary config file: %v", err) - } - defer f.Close() - if _, err := f.Write(data); err != nil { - return "", fmt.Errorf("🔴 Failed to write to temporary config file: %v", err) - } - return f.Name(), nil -} diff --git a/internal/config/modifers_test.go b/internal/config/modifers_test.go deleted file mode 100644 index c73abb7..0000000 --- a/internal/config/modifers_test.go +++ /dev/null @@ -1,335 +0,0 @@ -package config - -import ( - "testing" - "fmt" - "ebs-bootstrap/internal/utils" - "ebs-bootstrap/internal/service" - "github.com/google/go-cmp/cmp" -) - -func TestOwnerModifier(t *testing.T) { - subtests := []struct{ - Name string - Config *Config - ExpectedOutput *Config - ExpectedErr error - }{ - { - Name: "Existing Owner (Non-Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "root", - }, - }, - }, - ExpectedOutput: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "0", - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Existing Owner (Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "0", - }, - }, - }, - ExpectedOutput: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "0", - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Non-existent Owner (Non-Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "non-existent-user", - }, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("user: unknown user non-existent-user"), - }, - { - Name: "Non-existent Owner (Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Owner: "-1", - }, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("user: unknown userid -1"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - om := &OwnerModifier{} - err := om.Modify(subtest.Config) - if subtest.ExpectedOutput != nil { - if !cmp.Equal(subtest.Config, subtest.ExpectedOutput) { - t.Errorf("Modify() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, subtest.Config) - } - } - utils.CheckError("Modify()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestGroupModifier(t *testing.T) { - _, g, err := utils.GetCurrentUserGroup() - if err != nil { - t.Error(err) - return - } - subtests := []struct{ - Name string - Config *Config - ExpectedOutput *Config - ExpectedErr error - }{ - { - Name: "Existing Group (Non-Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: g.Name, - }, - }, - }, - ExpectedOutput: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: g.Gid, - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Existing Group (Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: g.Gid, - }, - }, - }, - ExpectedOutput: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: g.Gid, - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Non-existent Group (Non-Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: "non-existent-group", - }, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("group: unknown group non-existent-group"), - }, - { - Name: "Non-existent Group (Int)", - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Group: "-1", - }, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("group: unknown groupid -1"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - gm := &GroupModifier{} - err := gm.Modify(subtest.Config) - if subtest.ExpectedOutput != nil { - if !cmp.Equal(subtest.Config, subtest.ExpectedOutput) { - t.Errorf("Modify() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, subtest.Config) - } - } - utils.CheckError("Modify()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestDeviceModifier(t *testing.T) { - subtests := []struct{ - Name string - DeviceTranslator *service.DeviceTranslator - Config *Config - ExpectedOutput *Config - ExpectedErr error - }{ - { - Name: "DeviceTranslator() Hit", - DeviceTranslator: &service.DeviceTranslator{ - Table: map[string]string{ - "/dev/nvme0n1": "/dev/nvme0n1", - "/dev/xvdf": "/dev/nvme0n1", - }, - }, - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvdf": ConfigDevice{}, - }, - }, - ExpectedOutput: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/nvme0n1": ConfigDevice{}, - }, - }, - ExpectedErr: nil, - }, - { - Name: "DeviceTranslator() Miss", - DeviceTranslator: &service.DeviceTranslator{ - Table: map[string]string{}, - }, - Config: &Config{ - Devices: map[string]ConfigDevice{ - "/dev/xvdf": ConfigDevice{}, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("🔴 Could not identify a device with an alias /dev/xvdf"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - dm := &DeviceModifier{DeviceTranslator: subtest.DeviceTranslator} - err := dm.Modify(subtest.Config) - if subtest.ExpectedOutput != nil { - if !cmp.Equal(subtest.Config, subtest.ExpectedOutput) { - t.Errorf("Modify() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, subtest.Config) - } - } - utils.CheckError("Modify()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestDeviceModeModifier(t *testing.T) { - subtests := []struct{ - Name string - Config *Config - ExpectedOutput *Config - ExpectedErr error - }{ - { - Name: "Valid Global Mode, Empty Local Mode", - Config: &Config{ - Global: ConfigGlobal{ - Mode: ValidDeviceModes[0], - }, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{}, - }, - }, - ExpectedOutput: &Config{ - Global: ConfigGlobal{ - Mode: ValidDeviceModes[0], - }, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Mode: ValidDeviceModes[0], - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Empty Global Mode, Valid Local Mode", - Config: &Config{ - Global: ConfigGlobal{}, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Mode: ValidDeviceModes[0], - }, - }, - }, - ExpectedOutput: &Config{ - Global: ConfigGlobal{}, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Mode: ValidDeviceModes[0], - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Empty Global Mode, Empty Local Mode", - Config: &Config{ - Global: ConfigGlobal{}, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{}, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("🔴 /dev/xvda: If mode is not provided locally, it must be provided globally"), - }, - { - Name: "Invalid Global Mode, Empty Local Mode", - Config: &Config{ - Global: ConfigGlobal{ - Mode: "non-supported-mode", - }, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{}, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("🔴 A valid global mode was not provided: Expected=%s Provided=non-supported-mode", ValidDeviceModes), - }, - { - Name: "Empty Global Mode, Invalid Local Mode", - Config: &Config{ - Global: ConfigGlobal{}, - Devices: map[string]ConfigDevice{ - "/dev/xvda": ConfigDevice{ - Mode: "non-supported-mode", - }, - }, - }, - ExpectedOutput: nil, - ExpectedErr: fmt.Errorf("🔴 /dev/xvda: A valid mode was not provided: Expected=%s Provided=non-supported-mode", ValidDeviceModes), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - gm := &DeviceModeModifier{} - err := gm.Modify(subtest.Config) - if subtest.ExpectedOutput != nil { - if !cmp.Equal(subtest.Config, subtest.ExpectedOutput) { - t.Errorf("Modify() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, subtest.Config) - } - } - utils.CheckError("Modify()", t, subtest.ExpectedErr, err) - }) - } -} diff --git a/internal/config/modifier.go b/internal/config/modifier.go new file mode 100644 index 0000000..69a6e95 --- /dev/null +++ b/internal/config/modifier.go @@ -0,0 +1,83 @@ +package config + +import ( + "log" + "strings" + + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type Modifier interface { + Modify(c *Config) error +} + +type BatchModifier struct { + modifiers []Modifier +} + +func NewBatchModifier(modifiers []Modifier) *BatchModifier { + return &BatchModifier{ + modifiers: modifiers, + } +} + +func (bv *BatchModifier) Modify(c *Config) error { + for _, modifier := range bv.modifiers { + err := modifier.Modify(c) + if err != nil { + return err + } + } + return nil +} + +type OverridesModifier struct { + flag *Flag +} + +func NewOverridesModifier(flag *Flag) *OverridesModifier { + return &OverridesModifier{ + flag: flag, + } +} + +func (om *OverridesModifier) Modify(c *Config) error { + mode, err := ParseMode(om.flag.Mode) + if err != nil { + return err + } + c.Overrides.Mode = mode + c.Overrides.SkipTrustedActions = om.flag.SkipTrustedActions + return nil +} + +type AwsNitroNVMeModifier struct { + nvmeService service.NVMeService + deviceService service.DeviceService +} + +func NewAwsNVMeDriverModifier(nvmeService service.NVMeService, deviceService service.DeviceService) *AwsNitroNVMeModifier { + return &AwsNitroNVMeModifier{ + nvmeService: nvmeService, + deviceService: deviceService, + } +} + +func (andm *AwsNitroNVMeModifier) Modify(c *Config) error { + for name, cd := range c.Devices { + if _, err := andm.deviceService.GetBlockDevice(name); err == nil { + continue + } + if !strings.HasPrefix(name, "/dev/nvme") { + continue + } + bdm, err := andm.nvmeService.GetBlockDeviceMapping(name) + if err != nil { + continue + } + log.Printf("🔵 Detected that %s is a nitro-based AWS NVMe device, originally mapped to %s", name, bdm) + c.Devices[bdm] = cd + delete(c.Devices, name) + } + return nil +} diff --git a/internal/config/modifiers.go b/internal/config/modifiers.go deleted file mode 100644 index bde2e96..0000000 --- a/internal/config/modifiers.go +++ /dev/null @@ -1,97 +0,0 @@ -package config - -import ( - "fmt" - "os/user" - "strconv" - "slices" - "ebs-bootstrap/internal/service" -) - -var ( - ValidDeviceModes = []string{"healthcheck"} -) - -type Modifiers interface { - Modify(config *Config) (error) -} - -type OwnerModifier struct {} - -func (om *OwnerModifier) Modify(config *Config) (error) { - for key, device := range config.Devices { - var u *user.User; - var err error; - if _, atoiErr := strconv.Atoi(device.Owner); atoiErr != nil { - u, err = user.Lookup(device.Owner) - } else { - u, err = user.LookupId(device.Owner) - } - if err != nil { - return err - } - device.Owner = u.Uid - config.Devices[key] = device - } - return nil -} - -type DeviceModifier struct { - DeviceTranslator *service.DeviceTranslator -} - -func (dm *DeviceModifier) Modify(config *Config) (error) { - for key, device := range config.Devices { - alias, found := dm.DeviceTranslator.Table[key] - if !found { - return fmt.Errorf("🔴 Could not identify a device with an alias %s", key) - } - delete(config.Devices, key) - config.Devices[alias] = device - } - return nil -} - -type GroupModifier struct {} - -func (gm *GroupModifier) Modify(config *Config) (error) { - for key, device := range config.Devices { - var g *user.Group; - var err error; - if _, atoiErr := strconv.Atoi(device.Group); atoiErr != nil { - g, err = user.LookupGroup(device.Group) - } else { - g, err = user.LookupGroupId(device.Group) - } - if err != nil { - return err - } - device.Group = g.Gid - config.Devices[key] = device - } - return nil -} - -type DeviceModeModifier struct {} - -func (dm *DeviceModeModifier) Modify(config *Config) (error) { - if config.Global.Mode != "" && !slices.Contains(ValidDeviceModes, config.Global.Mode) { - return fmt.Errorf("🔴 A valid global mode was not provided: Expected=%s Provided=%s", ValidDeviceModes, config.Global.Mode) - } - - for key, device := range config.Devices { - if device.Mode == "" && config.Global.Mode == "" { - return fmt.Errorf("🔴 %s: If mode is not provided locally, it must be provided globally", key) - } - - if device.Mode != "" && !slices.Contains(ValidDeviceModes, device.Mode) { - return fmt.Errorf("🔴 %s: A valid mode was not provided: Expected=%s Provided=%s", key, ValidDeviceModes, device.Mode) - } - - if device.Mode == "" { - device.Mode = config.Global.Mode - } - config.Devices[key] = device - } - return nil -} diff --git a/internal/config/validator.go b/internal/config/validator.go new file mode 100644 index 0000000..e5b420a --- /dev/null +++ b/internal/config/validator.go @@ -0,0 +1,166 @@ +package config + +import ( + "fmt" + "path" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type Validator interface { + Validate(c *Config) error +} + +type BatchValidator struct { + validators []Validator +} + +func NewBatchValidator(validators []Validator) *BatchValidator { + return &BatchValidator{ + validators: validators, + } +} + +func (bv *BatchValidator) Validate(c *Config) error { + for _, validator := range bv.validators { + err := validator.Validate(c) + if err != nil { + return err + } + } + return nil +} + +type DeviceValidator struct { + deviceService service.DeviceService +} + +func NewDeviceValidator(ds service.DeviceService) *DeviceValidator { + return &DeviceValidator{ + deviceService: ds, + } +} + +func (dv *DeviceValidator) Validate(c *Config) error { + for name := range c.Devices { + _, err := dv.deviceService.GetBlockDevice(name) + if err != nil { + return fmt.Errorf("🔴 %s is not a block device", name) + } + } + return nil +} + +type FileSystemValidator struct{} + +func NewFileSystemValidator() *FileSystemValidator { + return &FileSystemValidator{} +} + +func (fsv *FileSystemValidator) Validate(c *Config) error { + for name, device := range c.Devices { + fs, err := model.ParseFileSystem(string(device.Fs)) + if err != nil { + return fmt.Errorf("🔴 %s: %s is not a supported file system", name, fs) + } + if fs == model.Unformatted { + return fmt.Errorf("🔴 %s: Must provide a supported file system", name) + } + } + return nil +} + +type ModeValidator struct{} + +func NewModeValidator() *ModeValidator { + return &ModeValidator{} +} + +func (fsv *ModeValidator) Validate(c *Config) error { + mode := string(c.Defaults.Mode) + _, err := ParseMode(mode) + if err != nil { + return fmt.Errorf("🔴 %s is not a supported global mode", mode) + } + + mode = string(c.Overrides.Mode) + _, err = ParseMode(mode) + if err != nil { + return fmt.Errorf("🔴 %s is not a supported overrides mode", mode) + } + + for name, device := range c.Devices { + mode := string(device.Mode) + _, err := ParseMode(mode) + if err != nil { + return fmt.Errorf("🔴 %s: %s is not a supported mode", name, mode) + } + } + return nil +} + +type MountPointValidator struct{} + +func NewMountPointValidator() *MountPointValidator { + return &MountPointValidator{} +} + +func (apv *MountPointValidator) Validate(c *Config) error { + for name, device := range c.Devices { + if device.MountPoint == "" { + continue + } + if !path.IsAbs(device.MountPoint) { + return fmt.Errorf("🔴 %s: %s is not an absolute path", name, device.MountPoint) + } + if device.MountPoint == "/" { + return fmt.Errorf("🔴 %s: Can not be mounted to the root directory", name) + } + } + return nil +} + +type PermissionsValidator struct{} + +func NewPermissionsValidator() *PermissionsValidator { + return &PermissionsValidator{} +} + +func (apv *PermissionsValidator) Validate(c *Config) error { + for _, device := range c.Devices { + _, err := device.Permissions.ToFileMode() + if err != nil { + return err + } + } + return nil +} + +type OwnerValidator struct { + ownerService service.OwnerService +} + +func NewOwnerValidator(ows service.OwnerService) *OwnerValidator { + return &OwnerValidator{ + ownerService: ows, + } +} + +func (ov *OwnerValidator) Validate(c *Config) error { + for _, device := range c.Devices { + if device.User != "" { + _, err := ov.ownerService.GetUser(device.User) + if err != nil { + return err + } + } + if device.Group != "" { + _, err := ov.ownerService.GetGroup(device.Group) + if err != nil { + return err + } + } + } + return nil +} diff --git a/internal/layer/directory.go b/internal/layer/directory.go new file mode 100644 index 0000000..070a5be --- /dev/null +++ b/internal/layer/directory.go @@ -0,0 +1,70 @@ +package layer + +import ( + "fmt" + "log" + "os" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type CreateDirectoryLayer struct { + deviceBackend backend.DeviceBackend + fileBackend backend.FileBackend +} + +func NewCreateDirectoryLayer(db backend.DeviceBackend, fb backend.FileBackend) *CreateDirectoryLayer { + return &CreateDirectoryLayer{ + deviceBackend: db, + fileBackend: fb, + } +} + +func (fdl *CreateDirectoryLayer) From(c *config.Config) error { + err := fdl.deviceBackend.From(c) + if err != nil { + return err + } + return fdl.fileBackend.From(c) +} + +func (fdl *CreateDirectoryLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + if cd.MountPoint == "" { + continue + } + d, err := fdl.fileBackend.GetDirectory((cd.MountPoint)) + if err != nil && !os.IsNotExist(err) { + // Since this layer is concerned with creating a directory + // if it does not exist. We will not produce an error if + // a file does not exist at the path + return nil, fmt.Errorf("🔴 %s: %s must be a directory for a device to be mounted to it", name, cd.MountPoint) + } + if d != nil { + continue + } + action := fdl.fileBackend.CreateDirectory(bd.Name, cd.MountPoint) + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *CreateDirectoryLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + if _, err := fdl.fileBackend.GetDirectory(cd.MountPoint); err != nil { + return fmt.Errorf("🔴 %s: Failed directory validation checks. %s does not exist or is not a directory", name, cd.MountPoint) + } + } + log.Println("🟢 Passed directory validation checks") + return nil +} diff --git a/internal/layer/format.go b/internal/layer/format.go new file mode 100644 index 0000000..5ebcf7f --- /dev/null +++ b/internal/layer/format.go @@ -0,0 +1,64 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/model" +) + +type FormatDeviceLayer struct { + deviceBackend backend.DeviceBackend +} + +func NewFormatDeviceLayer(db backend.DeviceBackend) *FormatDeviceLayer { + return &FormatDeviceLayer{ + deviceBackend: db, + } +} + +func (fdl *FormatDeviceLayer) From(c *config.Config) error { + return fdl.deviceBackend.From(c) +} + +func (fdl *FormatDeviceLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + if bd.FileSystem == cd.Fs { + continue + } + if bd.FileSystem != model.Unformatted { + return nil, fmt.Errorf("🔴 %s: Can not format a device that already has a %s file system", bd.Name, bd.FileSystem) + } + if bd.MountPoint != "" { + return nil, fmt.Errorf("🔴 %s: Can not format a device that is already mounted to %s", bd.Name, bd.MountPoint) + } + action, err := fdl.deviceBackend.Format(bd, cd.Fs) + if err != nil { + return nil, err + } + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *FormatDeviceLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + d, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return err + } + if d.FileSystem != cd.Fs { + return fmt.Errorf("🔴 %s: Failed file system validation checks. Expected=%s, Actual=%s", name, cd.Fs, d.FileSystem) + } + } + log.Println("🟢 Passed file system validation checks") + return nil +} diff --git a/internal/layer/label.go b/internal/layer/label.go new file mode 100644 index 0000000..9ef62ff --- /dev/null +++ b/internal/layer/label.go @@ -0,0 +1,63 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type LabelDeviceLayer struct { + deviceBackend backend.DeviceBackend +} + +func NewLabelDeviceLayer(db backend.DeviceBackend) *LabelDeviceLayer { + return &LabelDeviceLayer{ + deviceBackend: db, + } +} + +func (fdl *LabelDeviceLayer) From(c *config.Config) error { + return fdl.deviceBackend.From(c) +} + +func (fdl *LabelDeviceLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + if cd.Label == "" { + continue + } + if bd.Label == cd.Label { + continue + } + action, err := fdl.deviceBackend.Label(bd, cd.Label) + if err != nil { + return nil, err + } + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *LabelDeviceLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if cd.Label == "" { + continue + } + d, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return err + } + if d.Label != cd.Label { + return fmt.Errorf("🔴 %s: Failed label validation checks. Expected=%s, Actual=%s", name, cd.Label, d.Label) + } + } + log.Println("🟢 Passed label validation checks") + return nil +} diff --git a/internal/layer/layer.go b/internal/layer/layer.go new file mode 100644 index 0000000..a646b9b --- /dev/null +++ b/internal/layer/layer.go @@ -0,0 +1,48 @@ +package layer + +import ( + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type Layer interface { + From(config *config.Config) error + Modify(c *config.Config) ([]action.Action, error) + Validate(config *config.Config) error +} + +type LayerExecutor struct { + layers []Layer +} + +func NewLayerExecutor(layers []Layer) *LayerExecutor { + return &LayerExecutor{ + layers: layers, + } +} + +func (le *LayerExecutor) Execute(config *config.Config, ae *action.ActionExecutor) error { + for _, layer := range le.layers { + err := layer.From(config) + if err != nil { + return err + } + actions, err := layer.Modify(config) + if err != nil { + return err + } + err = ae.ExecuteActions(actions) + if err != nil { + return err + } + err = layer.From(config) + if err != nil { + return err + } + err = layer.Validate(config) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/layer/mount.go b/internal/layer/mount.go new file mode 100644 index 0000000..1296084 --- /dev/null +++ b/internal/layer/mount.go @@ -0,0 +1,83 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type MountDeviceLayer struct { + deviceBackend backend.DeviceBackend + fileBackend backend.FileBackend +} + +func NewMountDeviceLayer(db backend.DeviceBackend, fb backend.FileBackend) *MountDeviceLayer { + return &MountDeviceLayer{ + deviceBackend: db, + fileBackend: fb, + } +} + +func (fdl *MountDeviceLayer) From(c *config.Config) error { + err := fdl.deviceBackend.From(c) + if err != nil { + return err + } + return fdl.fileBackend.From(c) +} + +func (fdl *MountDeviceLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + if cd.MountPoint == "" { + continue + } + if bd.MountPoint != "" && bd.MountPoint != cd.MountPoint { + return nil, fmt.Errorf("🔴 %s: Device is already mounted to %s. Thus cannot be mounted to %s", name, bd.MountPoint, cd.MountPoint) + } + + var a action.Action + if bd.MountPoint == cd.MountPoint { + a = fdl.deviceBackend.Remount(bd, cd.MountPoint, cd.MountOptions) + } else { + _, err = fdl.fileBackend.GetDirectory(cd.MountPoint) + if err != nil { + // We prevent users from mounting devices to symbolic links as this would cause + // validation checks to consistently fail. We could evaluate symbolic links + // prior, but this just adds complexity we do not need right now + return nil, fmt.Errorf("🔴 %s: %s must exist as a directory before it can be mounted", name, cd.MountPoint) + } + if fdl.fileBackend.IsMount(cd.MountPoint) { + return nil, fmt.Errorf("🔴 %s: %s is already mounted by another device", name, cd.MountPoint) + } + a = fdl.deviceBackend.Mount(bd, cd.MountPoint, cd.MountOptions) + } + + actions = append(actions, a) + } + return actions, nil +} + +func (fdl *MountDeviceLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return err + } + if bd.MountPoint != cd.MountPoint { + return fmt.Errorf("🔴 %s: Failed mountpoint validation checks. Device not mounted to %s", name, bd.MountPoint) + } + } + log.Printf("🟢 Passed mountpoint validation checks") + return nil +} diff --git a/internal/layer/owner.go b/internal/layer/owner.go new file mode 100644 index 0000000..4367234 --- /dev/null +++ b/internal/layer/owner.go @@ -0,0 +1,121 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type ChangeOwnerLayer struct { + ownerBackend backend.OwnerBackend + fileBackend backend.FileBackend +} + +func NewChangeOwnerLayer(ub backend.OwnerBackend, fb backend.FileBackend) *ChangeOwnerLayer { + return &ChangeOwnerLayer{ + ownerBackend: ub, + fileBackend: fb, + } +} + +func (fdl *ChangeOwnerLayer) From(c *config.Config) error { + err := fdl.ownerBackend.From(c) + if err != nil { + return err + } + return fdl.fileBackend.From(c) +} + +func (fdl *ChangeOwnerLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + if cd.User == "" && cd.Group == "" { + continue + } + + d, err := fdl.fileBackend.GetDirectory(cd.MountPoint) + if err != nil { + return nil, fmt.Errorf("🔴 %s is either not a directory or does not exist", cd.MountPoint) + } + + // Default the values of User ID and Group ID to that + // of the directory + uid := d.Uid + gid := d.Gid + + if cd.User != "" { + u, err := fdl.ownerBackend.GetUser(cd.User) + if err != nil { + return nil, err + } + uid = u.Uid + } + + if cd.Group != "" { + g, err := fdl.ownerBackend.GetGroup(cd.Group) + if err != nil { + return nil, err + } + gid = g.Gid + } + + if d.Uid == uid && d.Gid == gid { + continue + } + action := fdl.fileBackend.ChangeOwner(name, cd.MountPoint, uid, gid) + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *ChangeOwnerLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + if cd.User == "" && cd.Group == "" { + continue + } + + d, err := fdl.fileBackend.GetDirectory(cd.MountPoint) + if err != nil { + return fmt.Errorf("🔴 %s: Failed ownership validation checks. %s is either not a directory or does not exist", name, cd.MountPoint) + } + + // Default the values of User ID and Group ID to that + // of the directory + uid := d.Uid + gid := d.Gid + + if cd.User != "" { + u, err := fdl.ownerBackend.GetUser(cd.User) + if err != nil { + return err + } + uid = u.Uid + } + + if cd.Group != "" { + g, err := fdl.ownerBackend.GetGroup(cd.Group) + if err != nil { + return err + } + gid = g.Gid + } + + if d.Uid != uid { + return fmt.Errorf("🔴 %s: Failed ownership validation checks. %s User Expected=%d, Actual=%d", name, cd.MountPoint, d.Uid, uid) + } + if d.Gid != gid { + return fmt.Errorf("🔴 %s: Failed ownership validation checks. %s Group Expected=%d, Actual=%d", name, cd.MountPoint, d.Gid, gid) + } + } + log.Printf("🟢 Passed ownership validation checks") + return nil +} diff --git a/internal/layer/permissions.go b/internal/layer/permissions.go new file mode 100644 index 0000000..5eda5fd --- /dev/null +++ b/internal/layer/permissions.go @@ -0,0 +1,72 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type ChangePermissionsLayer struct { + fileBackend backend.FileBackend +} + +func NewChangePermissionsLayer(fb backend.FileBackend) *ChangePermissionsLayer { + return &ChangePermissionsLayer{ + fileBackend: fb, + } +} + +func (fdl *ChangePermissionsLayer) From(c *config.Config) error { + return fdl.fileBackend.From(c) +} + +func (fdl *ChangePermissionsLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + if cd.Permissions == "" { + continue + } + + d, err := fdl.fileBackend.GetDirectory(cd.MountPoint) + if err != nil { + return nil, fmt.Errorf("🔴 %s is either not a directory or does not exist", cd.MountPoint) + } + + if d.Permissions.Equals(cd.Permissions) { + continue + } + + action := fdl.fileBackend.ChangePermissions(name, cd.MountPoint, cd.Permissions) + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *ChangePermissionsLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if cd.MountPoint == "" { + continue + } + // When permissions is not provided + if cd.Permissions == "" { + continue + } + + d, err := fdl.fileBackend.GetDirectory(cd.MountPoint) + if err != nil { + return fmt.Errorf("🔴 %s: Failed ownership validation checks. %s is either not a directory or does not exist", name, cd.MountPoint) + } + + if d.Permissions.NotEquals(cd.Permissions) { + return fmt.Errorf("🔴 %s: Failed permissions validation checks. %s Permissions Expected=%s, Actual=%s", name, cd.MountPoint, cd.Permissions, d.Permissions) + } + } + log.Printf("🟢 Passed permissions validation checks") + return nil +} diff --git a/internal/layer/umount.go b/internal/layer/umount.go new file mode 100644 index 0000000..9c36d27 --- /dev/null +++ b/internal/layer/umount.go @@ -0,0 +1,60 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type UnmountDeviceLayer struct { + deviceBackend backend.DeviceBackend + fileBackend backend.FileBackend +} + +func NewUnmountDeviceLayer(db backend.DeviceBackend, fb backend.FileBackend) *UnmountDeviceLayer { + return &UnmountDeviceLayer{ + deviceBackend: db, + fileBackend: fb, + } +} + +func (fdl *UnmountDeviceLayer) From(c *config.Config) error { + err := fdl.deviceBackend.From(c) + if err != nil { + return err + } + return fdl.fileBackend.From(c) +} + +func (fdl *UnmountDeviceLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + if bd.MountPoint == "" || bd.MountPoint == cd.MountPoint { + continue + } + action := fdl.deviceBackend.Umount(bd) + actions = append(actions, action) + } + return actions, nil +} + +func (fdl *UnmountDeviceLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + bd, err := fdl.deviceBackend.GetBlockDevice(name) + if err != nil { + return err + } + if bd.MountPoint != "" && bd.MountPoint != cd.MountPoint { + return fmt.Errorf("🔴 %s: Failed unmount validation checks. Device mounted to %s", name, bd.MountPoint) + } + } + log.Printf("🟢 Passed unmount validation checks") + return nil +} diff --git a/internal/model/device.go b/internal/model/device.go new file mode 100644 index 0000000..4aeb622 --- /dev/null +++ b/internal/model/device.go @@ -0,0 +1,54 @@ +package model + +import ( + "strings" +) + +type BlockDevice struct { + Name string + Uuid string + MountPoint string + FileSystem FileSystem + Label string +} + +type MountOptions string + +func (mop MountOptions) String() string { + if mop == "" { + return "defaults" + } + return string(mop) +} + +func (mop MountOptions) Remount(enabled bool) MountOptions { + mops := strings.Split(string(mop), ",") + if enabled { + index := -1 + for i, op := range mops { + if op == "remount" { + index = i + break + } + } + // remount flag not found (add it) + if index < 0 { + mops = append(mops, "remount") + } + } else { + index := -1 + for i, op := range mops { + if op == "remount" { + index = i + break + } + } + // remount flag found (remove it) + if index >= 0 { + copy(mops[index:], mops[index+1:]) // Shift a[i+1:] left one index. + mops[len(mops)-1] = "" // Erase last element (write zero value). + mops = mops[:len(mops)-1] // Truncate slice. + } + } + return MountOptions(strings.Join(mops, ",")) +} diff --git a/internal/model/file.go b/internal/model/file.go new file mode 100644 index 0000000..de05737 --- /dev/null +++ b/internal/model/file.go @@ -0,0 +1,60 @@ +package model + +import ( + "fmt" + "os" + "strconv" +) + +type FileType int + +const ( + RegularFile FileType = 0 + Directory FileType = 1 + Special FileType = 2 +) + +type File struct { + Path string + Type FileType + DeviceId int + InodeNo int + Uid int // User ID + Gid int // Group ID + Permissions Permissions +} + +type Permissions string + +func FromFileMode(fm os.FileMode) Permissions { + return Permissions(fmt.Sprintf("%o", fm)) +} + +func (p Permissions) ToFileMode() (os.FileMode, error) { + if p == "" { + return os.FileMode(0), nil + } + // Base: 8 + // Width: 32 bits + mode, err := strconv.ParseUint(string(p), 8, 32) + if err != nil { + return os.FileMode(0), fmt.Errorf("🔴 %s is not a valid permissions option", string(p)) + } + return os.FileMode(mode), nil +} + +func (pa Permissions) Equals(pb Permissions) bool { + fma, err := pa.ToFileMode() + if err != nil { + return false + } + fmb, err := pb.ToFileMode() + if err != nil { + return false + } + return fma == fmb +} + +func (p Permissions) NotEquals(pb Permissions) bool { + return !p.Equals(pb) +} diff --git a/internal/model/filesystem.go b/internal/model/filesystem.go new file mode 100644 index 0000000..a92e0f6 --- /dev/null +++ b/internal/model/filesystem.go @@ -0,0 +1,29 @@ +package model + +import "fmt" + +type FileSystem string + +const ( + Unformatted FileSystem = "" + Ext4 FileSystem = "ext4" + Xfs FileSystem = "xfs" +) + +func (c FileSystem) String() string { + return string(c) +} + +func ParseFileSystem(s string) (FileSystem, error) { + FileSystems := map[FileSystem]struct{}{ + Unformatted: {}, + Ext4: {}, + Xfs: {}, + } + fst := FileSystem(s) + _, ok := FileSystems[fst] + if !ok { + return fst, fmt.Errorf("🔴 %s: File system is not supported", s) + } + return fst, nil +} diff --git a/internal/model/owner.go b/internal/model/owner.go new file mode 100644 index 0000000..e6bd3a2 --- /dev/null +++ b/internal/model/owner.go @@ -0,0 +1,11 @@ +package model + +type User struct { + Name string + Uid int +} + +type Group struct { + Name string + Gid int +} diff --git a/internal/service/device.go b/internal/service/device.go index 9908772..bfe83ad 100644 --- a/internal/service/device.go +++ b/internal/service/device.go @@ -3,113 +3,64 @@ package service import ( "encoding/json" "fmt" - "strings" - "ebs-bootstrap/internal/utils" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/utils" ) // Device Service Interface [START] -type DeviceInfo struct { - Name string - Label string - Fs string - MountPoint string -} - type DeviceService interface { - GetBlockDevices() ([]string, error) - GetDeviceInfo(device string) (*DeviceInfo, error) + GetBlockDevice(name string) (*model.BlockDevice, error) } // Device Service Interface [END] type LinuxDeviceService struct { - Runner utils.Runner + runnerCache *utils.RunnerCache } type LsblkBlockDeviceResponse struct { - BlockDevices []LsblkBlockDevice `json:"blockdevices"` + BlockDevices []LsblkBlockDevice `json:"blockdevices"` } type LsblkBlockDevice struct { - Name string `json:"name"` - Label string `json:"label"` - FsType string `json:"fstype"` - MountPoint string `json:"mountpoint"` + Name *string `json:"name"` + Uuid *string `json:"uuid"` + Label *string `json:"label"` + FsType *string `json:"fstype"` + MountPoint *string `json:"mountpoint"` } -func (du *LinuxDeviceService) GetBlockDevices() ([]string, error) { - output, err := du.Runner.Command("lsblk", "--nodeps", "-o", "NAME,LABEL,FSTYPE,MOUNTPOINT", "-J") - if err != nil { - return nil, err +func NewLinuxDeviceService(rc *utils.RunnerCache) *LinuxDeviceService { + return &LinuxDeviceService{ + runnerCache: rc, } - lbd := &LsblkBlockDeviceResponse{} - err = json.Unmarshal([]byte(output), lbd) - if err != nil { - return nil, err - } - d := make([]string,len(lbd.BlockDevices)) - for i, _ := range d { - d[i] = "/dev/" + lbd.BlockDevices[i].Name - } - return d, nil } -func (du *LinuxDeviceService) GetDeviceInfo(device string) (*DeviceInfo, error) { - output, err := du.Runner.Command("lsblk", "--nodeps", "-o", "NAME,LABEL,FSTYPE,MOUNTPOINT", "-J", device) +func (du *LinuxDeviceService) GetBlockDevice(name string) (*model.BlockDevice, error) { + r := du.runnerCache.GetRunner(utils.Lsblk) + output, err := r.Command("--nodeps", "-o", "NAME,UUID,LABEL,FSTYPE,MOUNTPOINT", "-J", name) if err != nil { return nil, err } - bd := &LsblkBlockDeviceResponse{} - err = json.Unmarshal([]byte(output), bd) + lbd := &LsblkBlockDeviceResponse{} + err = json.Unmarshal([]byte(output), lbd) if err != nil { return nil, err } - if len(bd.BlockDevices) != 1 { - return nil, fmt.Errorf("🔴 [%s] An unexpected number of block devices were returned: Expected=1 Actual=%d", device, len(bd.BlockDevices)) + if len(lbd.BlockDevices) != 1 { + return nil, fmt.Errorf("🔴 %s: An unexpected number of block devices were returned: Expected=1 Actual=%d", name, len(lbd.BlockDevices)) } - return &DeviceInfo{ - Name: "/dev/" + bd.BlockDevices[0].Name, - Label: bd.BlockDevices[0].Label, - Fs: bd.BlockDevices[0].FsType, - MountPoint: bd.BlockDevices[0].MountPoint, - }, nil -} - -// Device Translator Service Interface [START] - -type DeviceTranslator struct { - Table map[string]string -} - -type DeviceTranslatorService interface { - GetTranslator() *DeviceTranslator -} - -type EbsDeviceTranslator struct { - DeviceService DeviceService - NVMeService NVMeService -} - -// Device Translator Service Interface [END] - -func (edt *EbsDeviceTranslator) GetTranslator() (*DeviceTranslator, error) { - dt := &DeviceTranslator{} - dt.Table = make(map[string]string) - devices, err := edt.DeviceService.GetBlockDevices() + fst, err := model.ParseFileSystem(utils.SafeString(lbd.BlockDevices[0].FsType)) if err != nil { return nil, err } - for _, device := range(devices) { - alias := device - if strings.HasPrefix(device, "/dev/nvme") { - alias, err = edt.NVMeService.GetBlockDeviceMapping(device) - if err != nil { - return nil, err - } - } - dt.Table[alias] = device - dt.Table[device] = alias - } - return dt, nil + return &model.BlockDevice{ + Name: "/dev/" + utils.SafeString(lbd.BlockDevices[0].Name), + Uuid: utils.SafeString(lbd.BlockDevices[0].Uuid), + Label: utils.SafeString(lbd.BlockDevices[0].Label), + FileSystem: fst, + MountPoint: utils.SafeString(lbd.BlockDevices[0].MountPoint), + }, nil } diff --git a/internal/service/device_test.go b/internal/service/device_test.go deleted file mode 100644 index e84cebe..0000000 --- a/internal/service/device_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package service - -import ( - "testing" - "fmt" - "ebs-bootstrap/internal/utils" - "github.com/google/go-cmp/cmp" -) - -type deviceMockRunner struct { - Output string - Error error -} - -func (mr *deviceMockRunner) Command(name string, arg ...string) (string, error) { - return mr.Output, mr.Error -} - -func TestGetBlockDevices(t *testing.T) { - mr := &deviceMockRunner{ - Output: `{"blockdevices": [ - {"name":"nvme1n1", "label":"external-vol", "fstype":"xfs", "mountpoint":"/ifmx/dev/root"}, - {"name":"nvme0n1", "label":null, "fstype":null, "mountpoint":null} - ]}`, - Error: nil, - } - expectedOutput := []string{"/dev/nvme1n1", "/dev/nvme0n1"} - - t.Run("Get Block Devices", func(t *testing.T) { - du := &LinuxDeviceService{mr} - d, err := du.GetBlockDevices() - if !cmp.Equal(d, expectedOutput) { - t.Errorf("GetBlockDevices() [output] mismatch: Expected=%v Actual=%v", expectedOutput, d) - } - utils.CheckError("GetBlockDevices()", t, nil, err) - }) -} - -func TestGetDeviceInfo(t *testing.T) { - deviceNotFoundErr := fmt.Errorf("🔴 /dev/nvme0n1 not a block device") - - subtests := []struct { - Name string - Device string - MockRunner *deviceMockRunner - ExpectedOutput *DeviceInfo - ExpectedErr error - }{ - { - Name: "Get Device Info for /dev/nvme0n1", - Device: "/dev/nvme0n1", - MockRunner: &deviceMockRunner{ - Output: `{"blockdevices":[{"name":"nvme0n1","label":"external-vol","fstype":"xfs","mountpoint":"/mnt/app"}]}`, - Error: nil, - }, - ExpectedOutput: &DeviceInfo{ - Name: "/dev/nvme0n1", - Label: "external-vol", - Fs: "xfs", - MountPoint: "/mnt/app", - }, - ExpectedErr: nil, - }, - { - Name: "Get Device Info for /dev/nvme0n1 (No Fs,Label,Mountpoint)", - Device: "/dev/nvme0n1", - MockRunner: &deviceMockRunner{ - Output: `{"blockdevices":[{"name":"nvme0n1","label":null,"fstype":null,"mountpoint":null}]}`, - Error: nil, - }, - ExpectedOutput: &DeviceInfo{ - Name: "/dev/nvme0n1", - Label: "", - Fs: "", - MountPoint: "", - }, - ExpectedErr: nil, - }, - { - Name: "Get Device Info for Missing Device", - Device: "/dev/nvme0n1", - MockRunner: &deviceMockRunner{ - Output: "", - Error: deviceNotFoundErr, - }, - ExpectedOutput: nil, - ExpectedErr: deviceNotFoundErr, - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - du := &LinuxDeviceService{subtest.MockRunner} - di, err := du.GetDeviceInfo(subtest.Device) - if !cmp.Equal(di, subtest.ExpectedOutput) { - t.Errorf("GetDeviceInfo() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, di) - } - utils.CheckError("GetDeviceInfo()", t, subtest.ExpectedErr, err) - }) - } -} - -type mockDeviceService struct { - getBlockDevices func() ([]string, error) -} - -func (ds *mockDeviceService) GetBlockDevices() ([]string, error) { - return ds.getBlockDevices() -} - -func (ds *mockDeviceService) GetDeviceInfo(device string) (*DeviceInfo, error) { - return nil, fmt.Errorf("🔴 GetDeviceInfo() not implemented") -} - -type mockNVMeService struct { - getBlockDeviceMapping func(device string) (string, error) -} - -func (ns *mockNVMeService) GetBlockDeviceMapping(device string) (string, error) { - return ns.getBlockDeviceMapping(device) -} - -func TestEbsDeviceTranslator(t *testing.T) { - subtests := []struct{ - Name string - DeviceService DeviceService - NVMeService NVMeService - ExpectedOutput *DeviceTranslator - ExpectedErr error - }{ - { - Name: "Get DeviceTranslator for EBS NVME Device", - DeviceService: &mockDeviceService{ - getBlockDevices: func() ([]string, error) { - return []string{"/dev/nvme0n1"}, nil - }, - }, - NVMeService: &mockNVMeService { - getBlockDeviceMapping: func(device string) (string, error) { - return "/dev/xvdf", nil - }, - }, - ExpectedOutput: &DeviceTranslator{ - Table: map[string]string{ - "/dev/nvme0n1" : "/dev/xvdf", - "/dev/xvdf": "/dev/nvme0n1", - }, - }, - ExpectedErr: nil, - }, - { - Name: "Get DeviceTranslator for Traditional EBS Device", - DeviceService: &mockDeviceService{ - getBlockDevices: func() ([]string, error) { - return []string{"/dev/xvdf"}, nil - }, - }, - NVMeService: &mockNVMeService{ - getBlockDeviceMapping: func(device string) (string, error) { - return "", fmt.Errorf("🔴 getBlockDeviceMapping() should not be called") - }, - }, - ExpectedOutput: &DeviceTranslator{ - Table: map[string]string{ - "/dev/xvdf": "/dev/xvdf", - }, - }, - ExpectedErr: nil, - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - dts := &EbsDeviceTranslator{ - DeviceService: subtest.DeviceService, - NVMeService: subtest.NVMeService, - } - dt, err := dts.GetTranslator() - if !cmp.Equal(dt, subtest.ExpectedOutput) { - t.Errorf("GetTranslator() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, dt) - } - utils.CheckError("GetTranslator()", t, subtest.ExpectedErr, err) - }) - } -} diff --git a/internal/service/file.go b/internal/service/file.go index b5f68df..ee98466 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -1,72 +1,51 @@ package service import ( + "fmt" "os" "syscall" - "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/model" ) // File Service Interface [START] -type FileInfo struct { - Owner string - Group string - Permissions string - Exists bool -} - type FileService interface { - GetStats(file string) (*FileInfo, error) - ValidateFile(path string) (error) + GetFile(file string) (*model.File, error) } // File Service Interface [END] -type UnixFileService struct {} +type UnixFileService struct{} + +func NewUnixFileService() *UnixFileService { + return &UnixFileService{} +} -func (ds *UnixFileService) GetStats(file string) (*FileInfo, error) { - info, err := os.Stat(file) +func (ds *UnixFileService) GetFile(file string) (*model.File, error) { + info, err := os.Lstat(file) if err != nil { - if os.IsNotExist(err) { - return &FileInfo{Exists: false}, nil - } - return nil, err + return nil, err } if stat, ok := info.Sys().(*syscall.Stat_t); ok { - return &FileInfo{ - Owner: fmt.Sprintf("%d", stat.Uid), - Group: fmt.Sprintf("%d", stat.Gid), - Permissions: fmt.Sprintf("%o", info.Mode().Perm()), - Exists: true, + var ft model.FileType + switch mode := info.Mode(); { + case mode.IsRegular(): + ft = model.RegularFile + case mode.IsDir(): + ft = model.Directory + default: + ft = model.Special + } + return &model.File{ + Path: file, + DeviceId: int(stat.Dev), + InodeNo: int(stat.Ino), + Uid: int(stat.Uid), + Gid: int(stat.Gid), + Permissions: model.FromFileMode(info.Mode().Perm()), + Type: ft, }, nil } return nil, fmt.Errorf("🔴 %s: Failed to get stats", file) } - -func (ds *UnixFileService) ValidateFile(path string) (error) { - s, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("🔴 %s does not exist", path) - } - return err - } - if !s.Mode().IsRegular() { - return fmt.Errorf("🔴 %s is not a regular file", path) - } - return nil -} - -func (ds *UnixFileService) ValidateDirectory(path string) (error) { - s, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("🔴 %s does not exist", path) - } - return err - } - if !s.Mode().IsDir() { - return fmt.Errorf("🔴 %s is not a directory", path) - } - return nil -} diff --git a/internal/service/file_test.go b/internal/service/file_test.go deleted file mode 100644 index a80d90d..0000000 --- a/internal/service/file_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package service - -import ( - "fmt" - "os" - "testing" - "ebs-bootstrap/internal/utils" - "github.com/google/go-cmp/cmp" -) - -func TestGetStats(t *testing.T) { - fs := &UnixFileService{} - t.Run("Get File Stats (Existing File)", func(t *testing.T) { - owner, group, permissions := os.Getuid(), os.Getgid(), os.FileMode(0644) - f, err := os.CreateTemp("", "sample") - utils.CheckError("CreateTemp()", t, nil, err) - defer os.Remove(f.Name()) - - err = os.Chown(f.Name(), owner, group) - utils.CheckError("Chown()", t, nil, err) - - err = os.Chmod(f.Name(), permissions) - utils.CheckError("Chmod()", t, nil, err) - - actual, err := fs.GetStats(f.Name()) - utils.CheckError("GetStats()", t, nil, err) - - expected := &FileInfo{ - Owner: fmt.Sprintf("%d", owner), - Group: fmt.Sprintf("%d", group), - Permissions: fmt.Sprintf("%o", permissions), - Exists: true, - } - if !cmp.Equal(actual, expected) { - t.Errorf("GetStats() [output] mismatch: Expected=%+v Actual=%+v", expected, actual) - } - }) - t.Run("Get File Stats (Non-Existent File)", func(t *testing.T) { - expected := &FileInfo{Exists: false} - actual, err := fs.GetStats("/non-existent-file/file.txt") - if !cmp.Equal(actual, expected) { - t.Errorf("GetStats() [output] mismatch: Expected=%+v Actual=%+v", expected, actual) - } - utils.CheckError("GetStats()", t, nil, err) - }) -} - -func TestValidateFile(t *testing.T) { - fs := &UnixFileService{} - - // Create a variable to the current working directory - d, err := os.Getwd() - if err != nil { - t.Errorf("os.Getwd() [error] %s", err) - return - } - - // Create a temporary file - f, err := os.CreateTemp("", "validate-file") - if err != nil { - t.Errorf("os.CreateTemp() [error] %s", err) - return - } - defer os.Remove(f.Name()) - - subtests := []struct{ - Name string - Path string - ExpectedErr error - }{ - { - Name: "Valid (Existing File)", - Path: f.Name(), - ExpectedErr: nil, - }, - { - Name: "Invalid (Existing Directory)", - Path: d, - ExpectedErr: fmt.Errorf("🔴 %s is not a regular file", d), - }, - { - Name: "Invalid: (Non-existing File)", - Path: "/doesnt-exist", - ExpectedErr: fmt.Errorf("🔴 /doesnt-exist does not exist"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - err := fs.ValidateFile(subtest.Path) - utils.CheckError("ValidateFile()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestValidateDirectory(t *testing.T) { - fs := &UnixFileService{} - - // Create a variable to the current working directory - d, err := os.Getwd() - if err != nil { - t.Errorf("os.Getwd() [error] %s", err) - return - } - - // Create a temporary file - f, err := os.CreateTemp("", "validate-directory") - if err != nil { - t.Errorf("os.CreateTemp() [error] %s", err) - return - } - defer os.Remove(f.Name()) - - subtests := []struct{ - Name string - Path string - ExpectedErr error - }{ - { - Name: "Valid (Existing Directory)", - Path: d, - ExpectedErr: nil, - }, - { - Name: "Invalid (Existing File)", - Path: f.Name(), - ExpectedErr: fmt.Errorf("🔴 %s is not a directory", f.Name()), - }, - { - Name: "Invalid (Non-existing Directory)", - Path: "/doesnt-exist", - ExpectedErr: fmt.Errorf("🔴 /doesnt-exist does not exist"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - err := fs.ValidateDirectory(subtest.Path) - utils.CheckError("ValidateDirectory()", t, subtest.ExpectedErr, err) - }) - } -} diff --git a/internal/service/nvme.go b/internal/service/nvme.go index 10cd361..e125aa1 100644 --- a/internal/service/nvme.go +++ b/internal/service/nvme.go @@ -1,166 +1,169 @@ package service import ( - "fmt" - "os" - "syscall" - "unsafe" - "strings" - "unicode" + "fmt" + "os" + "strings" + "syscall" + "unicode" + "unsafe" ) const ( - NVME_ADMIN_IDENTIFY = 0x06 - NVME_IOCTL_ADMIN_CMD = 0xC0484E41 - AMZN_NVME_VID = 0x1D0F - AMZN_NVME_EBS_MN = "Amazon Elastic Block Store" + NVME_ADMIN_IDENTIFY = 0x06 + NVME_IOCTL_ADMIN_CMD = 0xC0484E41 + AMZN_NVME_VID = 0x1D0F + AMZN_NVME_EBS_MN = "Amazon Elastic Block Store" ) type nvmeAdminCommand struct { - Opcode uint8 - Flags uint8 - Cid uint16 - Nsid uint32 - Reserved0 uint64 - Mptr uint64 - Addr uint64 - Mlen uint32 - Alen uint32 - Cdw10 uint32 - Cdw11 uint32 - Cdw12 uint32 - Cdw13 uint32 - Cdw14 uint32 - Cdw15 uint32 - Reserved1 uint64 + Opcode uint8 + Flags uint8 + Cid uint16 + Nsid uint32 + Reserved0 uint64 + Mptr uint64 + Addr uint64 + Mlen uint32 + Alen uint32 + Cdw10 uint32 + Cdw11 uint32 + Cdw12 uint32 + Cdw13 uint32 + Cdw14 uint32 + Cdw15 uint32 + Reserved1 uint64 } type nvmeIdentifyControllerAmznVS struct { - Bdev [32]byte - Reserved0 [1024 - 32]byte + Bdev [32]byte + Reserved0 [1024 - 32]byte } type nvmeIdentifyControllerPSD struct { - Mp uint16 - Reserved0 uint16 - Enlat uint32 - Exlat uint32 - Rrt uint8 - Rrl uint8 - Rwt uint8 - Rwl uint8 - Reserved1 [16]byte + Mp uint16 + Reserved0 uint16 + Enlat uint32 + Exlat uint32 + Rrt uint8 + Rrl uint8 + Rwt uint8 + Rwl uint8 + Reserved1 [16]byte } type nvmeIdentifyController struct { - Vid uint16 - Ssvid uint16 - Sn [20]byte - Mn [40]byte - Fr [8]byte - Rab uint8 - Ieee [3]uint8 - Mic uint8 - Mdts uint8 - Reserved0 [256 - 78]byte - Oacs uint16 - Acl uint8 - Aerl uint8 - Frmw uint8 - Lpa uint8 - Elpe uint8 - Npss uint8 - Avscc uint8 - Reserved1 [512 - 265]byte - Sqes uint8 - Cqes uint8 - Reserved2 uint16 - Nn uint32 - Oncs uint16 - Fuses uint16 - Fna uint8 - Vwc uint8 - Awun uint16 - Awupf uint16 - Nvscc uint8 - Reserved3 [704 - 531]byte - Reserved4 [2048 - 704]byte - Psd [32]nvmeIdentifyControllerPSD - Vs nvmeIdentifyControllerAmznVS + Vid uint16 + Ssvid uint16 + Sn [20]byte + Mn [40]byte + Fr [8]byte + Rab uint8 + Ieee [3]uint8 + Mic uint8 + Mdts uint8 + Reserved0 [256 - 78]byte + Oacs uint16 + Acl uint8 + Aerl uint8 + Frmw uint8 + Lpa uint8 + Elpe uint8 + Npss uint8 + Avscc uint8 + Reserved1 [512 - 265]byte + Sqes uint8 + Cqes uint8 + Reserved2 uint16 + Nn uint32 + Oncs uint16 + Fuses uint16 + Fna uint8 + Vwc uint8 + Awun uint16 + Awupf uint16 + Nvscc uint8 + Reserved3 [704 - 531]byte + Reserved4 [2048 - 704]byte + Psd [32]nvmeIdentifyControllerPSD + Vs nvmeIdentifyControllerAmznVS } type NVMeDevice struct { - Name string - IdCtrl nvmeIdentifyController + Name string + IdCtrl nvmeIdentifyController } func NewNVMeDevice(name string) (*NVMeDevice, error) { - d := &NVMeDevice{Name: name} - if err := d.nvmeIOctl(); err != nil { - return nil, err - } - return d, nil + d := &NVMeDevice{Name: name} + if err := d.NVMeIOctl(); err != nil { + return nil, err + } + return d, nil } -func (d *NVMeDevice) nvmeIOctl() error { - idResponse := uintptr(unsafe.Pointer(&d.IdCtrl)) - idLen := unsafe.Sizeof(d.IdCtrl) - - adminCmd := nvmeAdminCommand{ - Opcode: NVME_ADMIN_IDENTIFY, - Addr: uint64(idResponse), - Alen: uint32(idLen), - Cdw10: 1, - } - - nvmeFile, err := os.OpenFile(d.Name, os.O_RDONLY, 0) - if err != nil { - return err - } - defer nvmeFile.Close() - - _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, nvmeFile.Fd(), NVME_IOCTL_ADMIN_CMD, uintptr(unsafe.Pointer(&adminCmd))) - if errno != 0 { - return fmt.Errorf("🔴 ioctl error: %v", errno) - } - - return nil +func (d *NVMeDevice) NVMeIOctl() error { + idResponse := uintptr(unsafe.Pointer(&d.IdCtrl)) + idLen := unsafe.Sizeof(d.IdCtrl) + + adminCmd := nvmeAdminCommand{ + Opcode: NVME_ADMIN_IDENTIFY, + Addr: uint64(idResponse), + Alen: uint32(idLen), + Cdw10: 1, + } + + nvmeFile, err := os.OpenFile(d.Name, os.O_RDONLY, 0) + if err != nil { + return err + } + defer nvmeFile.Close() + + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, nvmeFile.Fd(), NVME_IOCTL_ADMIN_CMD, uintptr(unsafe.Pointer(&adminCmd))) + if errno != 0 { + return fmt.Errorf("🔴 ioctl error: %v", errno) + } + + return nil } // NVMe Service [Start] - type NVMeService interface { - GetBlockDeviceMapping(device string) (string, error) + GetBlockDeviceMapping(device string) (string, error) } // NVMe Service [END] -type AwsNVMeService struct {} +type AwsNitroNVMeService struct{} + +func NewAwsNitroNVMeService() *AwsNitroNVMeService { + return &AwsNitroNVMeService{} +} -func (ns *AwsNVMeService) GetBlockDeviceMapping(device string) (string, error) { - nd, err := NewNVMeDevice(device); - if err != nil { - return "", err - } - return ns.getBlockDeviceMapping(nd) +func (ns *AwsNitroNVMeService) GetBlockDeviceMapping(device string) (string, error) { + nd, err := NewNVMeDevice(device) + if err != nil { + return "", err + } + return ns.getBlockDeviceMapping(nd) } -func (ns *AwsNVMeService) isEBSVolume(nd *NVMeDevice) bool { - vid := nd.IdCtrl.Vid - mn := strings.TrimRightFunc(string(nd.IdCtrl.Mn[:]), unicode.IsSpace) - return vid == AMZN_NVME_VID && mn == AMZN_NVME_EBS_MN +func (ns *AwsNitroNVMeService) isEBSVolume(nd *NVMeDevice) bool { + vid := nd.IdCtrl.Vid + mn := strings.TrimRightFunc(string(nd.IdCtrl.Mn[:]), unicode.IsSpace) + return vid == AMZN_NVME_VID && mn == AMZN_NVME_EBS_MN } -func (ns *AwsNVMeService) getBlockDeviceMapping(nd *NVMeDevice) (string, error) { - var bdm string; - if ns.isEBSVolume(nd) { - bdm = strings.TrimRightFunc(string(nd.IdCtrl.Vs.Bdev[:]), unicode.IsSpace) - } - if bdm == "" { - return "", fmt.Errorf("🔴 %s is not an AWS-managed NVME device", nd.Name) - } - if !strings.HasPrefix(bdm, "/dev/") { - bdm = "/dev/" + bdm - } - return bdm, nil +func (ns *AwsNitroNVMeService) getBlockDeviceMapping(nd *NVMeDevice) (string, error) { + var bdm string + if ns.isEBSVolume(nd) { + bdm = strings.TrimRightFunc(string(nd.IdCtrl.Vs.Bdev[:]), unicode.IsSpace) + } + if bdm == "" { + return "", fmt.Errorf("🔴 %s is not an AWS-managed NVME device", nd.Name) + } + if !strings.HasPrefix(bdm, "/dev/") { + bdm = "/dev/" + bdm + } + return bdm, nil } diff --git a/internal/service/nvme_test.go b/internal/service/nvme_test.go deleted file mode 100644 index 237d00e..0000000 --- a/internal/service/nvme_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package service - -import ( - "fmt" - "testing" - "ebs-bootstrap/internal/utils" -) - -const ( - UNSUPPORTED_NVME_VID = 0xFFFF - UNSUPPORTED_NVME_MN = "External NVME Manufacturer" -) - -func TestAwsNVMeService(t *testing.T) { - subtests := []struct{ - Name string - Device string - VendorId uint16 - ModelNumber string - BlockDevice string - ExpectedOutput string - ExpectedErr error - }{ - { - Name: "EBS NVMe Device (Partial Block Device)", - Device: "/dev/nvme1n1", - VendorId: AMZN_NVME_VID, - ModelNumber: AMZN_NVME_EBS_MN, - BlockDevice: "sdb", - ExpectedOutput: "/dev/sdb", - ExpectedErr: nil, - }, - { - Name: "EBS NVMe Device (Complete Block Device)", - Device: "/dev/nvme1n1", - VendorId: AMZN_NVME_VID, - ModelNumber: AMZN_NVME_EBS_MN, - BlockDevice: "/dev/sdb", - ExpectedOutput: "/dev/sdb", - ExpectedErr: nil, - }, - { - Name: "Invalid NVMe Device (Unsupported Vendor ID)", - Device: "/dev/nvme1n1", - VendorId: UNSUPPORTED_NVME_VID, - ModelNumber: AMZN_NVME_EBS_MN, - BlockDevice: "", - ExpectedOutput: "", - ExpectedErr: fmt.Errorf("🔴 /dev/nvme1n1 is not an AWS-managed NVME device"), - }, - { - Name: "Invalid NVMe Device (Unsupported Model Number)", - Device: "/dev/nvme1n1", - VendorId: AMZN_NVME_VID, - ModelNumber: UNSUPPORTED_NVME_MN, - BlockDevice: "", - ExpectedOutput: "", - ExpectedErr: fmt.Errorf("🔴 /dev/nvme1n1 is not an AWS-managed NVME device"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - nd := &NVMeDevice{ - Name: subtest.Device, - IdCtrl: nvmeIdentifyController{ - Vid: subtest.VendorId, - Mn: parseModelNumber(subtest.ModelNumber), - Vs: nvmeIdentifyControllerAmznVS{ - Bdev: parseBlockDevice(subtest.BlockDevice), - }, - }, - } - ns := &AwsNVMeService{} - bdm, err := ns.getBlockDeviceMapping(nd) - if bdm != subtest.ExpectedOutput { - t.Errorf("getBlockDeviceMapping() [output] mismatch: Expected=%+v Actual=%+v", subtest.ExpectedOutput, bdm) - } - utils.CheckError("getBlockDeviceMapping()", t, subtest.ExpectedErr, err) - }) - } -} - -func parseModelNumber(input string) [40]byte { - var mn [40]byte - copy(mn[:], input) - if len(input) < 40 { - for i := len(input); i < 40; i++ { - mn[i] = ' ' - } - } - return mn -} - -func parseBlockDevice(input string) [32]byte { - var bd [32]byte - copy(bd[:], input) - if len(input) < 32 { - for i := len(input); i < 32; i++ { - bd[i] = ' ' - } - } - return bd -} diff --git a/internal/service/owner.go b/internal/service/owner.go new file mode 100644 index 0000000..d57c7cf --- /dev/null +++ b/internal/service/owner.go @@ -0,0 +1,66 @@ +package service + +import ( + "fmt" + "os/user" + "strconv" + + "github.com/reecetech/ebs-bootstrap/internal/model" +) + +type OwnerService interface { + GetUser(owner string) (*model.User, error) + GetGroup(owner string) (*model.Group, error) +} + +type UnixOwnerService struct{} + +func NewUnixOwnerService() *UnixOwnerService { + return &UnixOwnerService{} +} + +func (s *UnixOwnerService) GetUser(owner string) (*model.User, error) { + var u *user.User + if _, err := strconv.Atoi(owner); err != nil { + // If not a valid integer, try to look up by username + u, err = user.Lookup(owner) + if err != nil { + return nil, fmt.Errorf("🔴 Owner (name) %s does not exist", owner) + } + } else { + u, err = user.LookupId(owner) + if err != nil { + return nil, fmt.Errorf("🔴 Owner (id) %s does not exist", owner) + } + } + + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast owner id to integer") + } + + return &model.User{Name: u.Username, Uid: uid}, nil +} + +func (s *UnixOwnerService) GetGroup(group string) (*model.Group, error) { + var g *user.Group + if _, err := strconv.Atoi(group); err != nil { + // If not a valid integer, try to look up by group name + g, err = user.LookupGroup(group) + if err != nil { + return nil, fmt.Errorf("🔴 Group (name) %s does not exist", group) + } + } else { + g, err = user.LookupGroupId(group) + if err != nil { + return nil, fmt.Errorf("🔴 Group (id) %s does not exist", group) + } + } + + gid, err := strconv.Atoi(g.Gid) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast group id to integer") + } + + return &model.Group{Name: g.Name, Gid: gid}, nil +} diff --git a/internal/state/device.go b/internal/state/device.go deleted file mode 100644 index 100b781..0000000 --- a/internal/state/device.go +++ /dev/null @@ -1,110 +0,0 @@ -package state - -import ( - "fmt" - "log" - "ebs-bootstrap/internal/config" - "ebs-bootstrap/internal/service" -) - -type deviceProperties struct { - Name string - Fs string - MountPoint string - Owner string - Group string - Label string - Permissions string -} - -type Device struct { - Properties deviceProperties - DeviceService service.DeviceService - FileService service.FileService -} - -func NewDevice(name string, ds service.DeviceService, fs service.FileService) (*Device, error) { - s := &Device{ - DeviceService: ds, - FileService: fs, - Properties: deviceProperties{Name: name}, - } - err := s.Pull() - if err != nil { - return nil, err - } - return s, nil -} - -func (d *Device) Pull() (error) { - name := d.Properties.Name - di, err := d.DeviceService.GetDeviceInfo(name) - if err != nil { - return err - } - p := deviceProperties{ - Name: name, - Fs: di.Fs, - Label: di.Label, - MountPoint: di.MountPoint, - } - - if p.MountPoint == "" { - log.Printf("🟡 %s: No mount-point detected. Skip further checks...", name) - d.Properties = p - return nil - } - - fi, err := d.FileService.GetStats(p.MountPoint) - if err != nil { - return err - } - if fi.Exists { - p.Owner = fi.Owner - p.Group = fi.Group - p.Permissions = fi.Permissions - } - - d.Properties = p - return nil -} - -func (d *Device) Diff(c *config.Config) (error) { - name := d.Properties.Name - if name == "" { - return fmt.Errorf("🔴 An unexpected error occured") - } - desired, found := c.Devices[name] - if !found { - return fmt.Errorf("🔴 %s: Couldn't find device in config", name) - } - - if d.Properties.Fs != string(desired.Fs) { - return fmt.Errorf("🔴 File System [%s]: Expected=%s", d.Properties.Name, desired.Fs) - } - - if d.Properties.Label != string(desired.Label) { - return fmt.Errorf("🔴 Label [%s]: Expected=%s", d.Properties.Name, desired.Label) - } - - if d.Properties.MountPoint != string(desired.MountPoint) { - return fmt.Errorf("🔴 Mount Point [%s]: Expected=%s", d.Properties.Name, desired.MountPoint) - } - - if d.Properties.Owner != string(desired.Owner) { - return fmt.Errorf("🔴 Owner [%s]: Expected=%s", d.Properties.MountPoint, desired.Owner) - } - - if d.Properties.Group != string(desired.Group) { - return fmt.Errorf("🔴 Group: [%s]: Expected=%s", d.Properties.MountPoint, desired.Group) - } - - if d.Properties.Permissions != string(desired.Permissions) { - return fmt.Errorf("🔴 Permissions [%s]: Expected=%s", d.Properties.MountPoint, desired.Permissions) - } - return nil -} - -func (d *Device) Push(c *config.Config) (error) { - return nil -} diff --git a/internal/state/device_test.go b/internal/state/device_test.go deleted file mode 100644 index 31ce0b7..0000000 --- a/internal/state/device_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package state - -import ( - "fmt" - "testing" - "ebs-bootstrap/internal/utils" - "ebs-bootstrap/internal/service" - "ebs-bootstrap/internal/config" -) - -type mockDeviceService struct { - getDeviceInfo func(device string) (*service.DeviceInfo, error) -} - -func (ds *mockDeviceService) GetBlockDevices() ([]string, error) { - return nil, fmt.Errorf("🔴 GetBlockDevices() not implemented") -} - -func (ds *mockDeviceService) GetDeviceInfo(device string) (*service.DeviceInfo, error) { - return ds.getDeviceInfo(device) -} - -type mockFileService struct { - getStats func(file string) (*service.FileInfo, error) -} - -func (fs *mockFileService) GetStats(file string) (*service.FileInfo, error) { - return fs.getStats(file) -} - -func (fs *mockFileService) ValidateFile(path string) (error) { - return fmt.Errorf("🔴 ValidateFile() not implemented") -} - -func TestDevice(t *testing.T) { - subtests := []struct { - Name string - DeviceName string - DeviceService service.DeviceService - FileService service.FileService - ExpectedErr error - }{ - { - Name: "Non-Existent Device", - DeviceName: "/dev/doesnt-exist", - DeviceService: &mockDeviceService{ - getDeviceInfo: func(device string) (*service.DeviceInfo, error) { - return nil, fmt.Errorf("🔴 /dev/doesnt-exist not a block device") - }, - }, - FileService: &mockFileService{ - getStats: func(file string) (*service.FileInfo, error) { - return nil, fmt.Errorf("🔴 getStats() should not be called") - }, - }, - ExpectedErr: fmt.Errorf("🔴 /dev/doesnt-exist not a block device"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - _, err := NewDevice(subtest.DeviceName, - subtest.DeviceService, - subtest.FileService) - utils.CheckError("NewDevice()", t, subtest.ExpectedErr, err) - }) - } -} - -func TestDeviceDiff(t *testing.T) { - subtests := []struct { - Name string - DeviceName string - DeviceService service.DeviceService - FileService service.FileService - Config *config.Config - ExpectedErr error - }{ - { - Name: "No Diff Expected With Mount-Point", - DeviceName: "/dev/nvme0n1", - DeviceService: &mockDeviceService{ - getDeviceInfo: func(device string) (*service.DeviceInfo, error) { - return &service.DeviceInfo{ - Name: "/dev/nvme0n1", - Label: "external-vol", - Fs: "xfs", - MountPoint: "/mnt/app", - }, nil - }, - }, - FileService: &mockFileService{ - getStats: func(file string) (*service.FileInfo, error) { - return &service.FileInfo{ - Owner: "100", - Group: "100", - Permissions: "755", - Exists: true, - }, nil - }, - }, - Config: &config.Config{ - Devices: map[string]config.ConfigDevice{ - "/dev/nvme0n1": config.ConfigDevice{ - Fs: "xfs", - MountPoint: "/mnt/app", - Owner: "100", - Group: "100", - Label: "external-vol", - Permissions: "755", - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "No Diff Expected Without Mount-Point", - DeviceName: "/dev/nvme0n1", - DeviceService: &mockDeviceService{ - getDeviceInfo: func(device string) (*service.DeviceInfo, error) { - return &service.DeviceInfo{ - Name: "/dev/nvme0n1", - Label: "external-vol", - Fs: "xfs", - MountPoint: "", - }, nil - }, - }, - FileService: &mockFileService{ - getStats: func(file string) (*service.FileInfo, error) { - return nil, fmt.Errorf("🔴 getStats() should not be called") - }, - }, - Config: &config.Config{ - Devices: map[string]config.ConfigDevice{ - "/dev/nvme0n1": config.ConfigDevice{ - Fs: "xfs", - Label: "external-vol", - }, - }, - }, - ExpectedErr: nil, - }, - { - Name: "Diff Suggesting Fs Change (xfs->ext4)", - DeviceName: "/dev/nvme0n1", - DeviceService: &mockDeviceService{ - getDeviceInfo: func(device string) (*service.DeviceInfo, error) { - return &service.DeviceInfo{ - Name: "/dev/nvme0n1", - Label: "external-vol", - Fs: "xfs", - MountPoint: "/mnt/app", - }, nil - }, - }, - FileService: &mockFileService{ - getStats: func(file string) (*service.FileInfo, error) { - return &service.FileInfo{ - Owner: "100", - Group: "100", - Permissions: "755", - Exists: true, - }, nil - }, - }, - Config: &config.Config{ - Devices: map[string]config.ConfigDevice{ - "/dev/nvme0n1": config.ConfigDevice{ - Fs: "ext4", - MountPoint: "/mnt/app", - Owner: "100", - Group: "100", - Label: "external-vol", - Permissions: "755", - }, - }, - }, - ExpectedErr: fmt.Errorf("🔴 File System [/dev/nvme0n1]: Expected=ext4"), - }, - } - for _, subtest := range subtests { - t.Run(subtest.Name, func(t *testing.T) { - d, err := NewDevice(subtest.DeviceName, - subtest.DeviceService, - subtest.FileService) - if err != nil { - t.Errorf("NewDevice() [error] %s", err) - } - err = d.Diff(subtest.Config) - utils.CheckError("Diff()", t, subtest.ExpectedErr, err) - }) - } -} diff --git a/internal/state/state.go b/internal/state/state.go deleted file mode 100644 index a93e46c..0000000 --- a/internal/state/state.go +++ /dev/null @@ -1,11 +0,0 @@ -package state - -import ( - "ebs-bootstrap/internal/config" -) - -type State interface { - Pull() (error) - Diff(c *config.Config) (error) - Push(c *config.Config) (error) -} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 27e7829..2fb9c55 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -1,29 +1,83 @@ package utils import ( + "fmt" "os/exec" "strings" - "fmt" ) +type Binary string + +const ( + Lsblk Binary = "lsblk" + MkfsExt4 Binary = "mkfs.ext4" + E2Label Binary = "e2label" + MkfsXfs Binary = "mkfs.xfs" + XfsAdmin Binary = "xfs_admin" + Mount Binary = "mount" + Umount Binary = "umount" +) + +type RunnerCache struct { + cache map[Binary]Runner +} + +func NewRunnerCache() *RunnerCache { + return &RunnerCache{ + cache: map[Binary]Runner{ + Lsblk: NewExecRunner(Lsblk), + MkfsExt4: NewExecRunner(MkfsExt4), + E2Label: NewExecRunner(E2Label), + MkfsXfs: NewExecRunner(MkfsXfs), + XfsAdmin: NewExecRunner(XfsAdmin), + Mount: NewExecRunner(Mount), + Umount: NewExecRunner(Umount), + }, + } +} + +func (rc *RunnerCache) GetRunner(binary Binary) Runner { + return rc.cache[binary] +} + type Runner interface { - Command(name string, arg ...string) (string, error) + Command(arg ...string) (string, error) + IsValid() bool } type ExecRunner struct { - Runner func(name string, arg ...string) *exec.Cmd + binary Binary + command func(name string, arg ...string) *exec.Cmd + lookPath func(file string) (string, error) + isValidated bool } -func NewExecRunner() *ExecRunner { - return &ExecRunner{exec.Command} +func NewExecRunner(binary Binary) *ExecRunner { + return &ExecRunner{ + binary: binary, + command: exec.Command, + lookPath: exec.LookPath, + isValidated: false, + } } -func (er *ExecRunner) Command(name string, arg ...string) (string, error) { - cmd := er.Runner(name, arg...) +func (er *ExecRunner) Command(arg ...string) (string, error) { + if !er.IsValid() { + return "", fmt.Errorf("🔴 %s is either not installed or accessible from $PATH", string(er.binary)) + } + cmd := er.command(string(er.binary), arg...) o, err := cmd.CombinedOutput() output := strings.TrimRight(string(o), "\n") if err != nil { - return "", fmt.Errorf("%s: %s", err, output) + return "", fmt.Errorf("🔴 %s: %s", err, output) } return output, err } + +func (er *ExecRunner) IsValid() bool { + if !er.isValidated { + _, err := er.lookPath(string(er.binary)) + er.isValidated = err == nil + } + return er.isValidated +} diff --git a/internal/utils/string.go b/internal/utils/string.go new file mode 100644 index 0000000..5f0f8af --- /dev/null +++ b/internal/utils/string.go @@ -0,0 +1,8 @@ +package utils + +func SafeString(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/internal/utils/testing.go b/internal/utils/testing.go deleted file mode 100644 index 9bbe052..0000000 --- a/internal/utils/testing.go +++ /dev/null @@ -1,37 +0,0 @@ -package utils - -import ( - "fmt" - "strings" - "os/user" - "testing" -) - -func CheckError(name string, t *testing.T, expected error, actual error) { - if actual != nil { - if expected == nil { - t.Errorf("%s [error] undetected: Actual=%v", name, actual) - return - } - if expected.Error() != actual.Error() { - t.Errorf("%s [error] mismatch: Expected=%v Actual=%v", name, expected, actual) - } - } -} - -func GetCurrentUserGroup() (*user.User, *user.Group, error) { - u, err := user.Current() - if err != nil { - return nil, nil, fmt.Errorf("🔴 Failed to get current user") - } - g, err := user.LookupGroupId(u.Gid) - if err != nil { - return nil, nil, fmt.Errorf("🔴 Failed to get current group") - } - /* user.Current() -> From experience, this function can return a username - in a capital case. This is not valid UNIX format for usernames so force - to lowercase */ - u.Name = strings.ToLower(u.Name) - g.Name = strings.ToLower(g.Name) - return u, g, nil -}