package ec2macosinit import ( "fmt" "strconv" "strings" "github.com/aws/ec2-macos-init/internal/paths" "github.com/google/go-cmp/cmp" ) // Module contains a few fields common to all Module types and containers for the configuration of any // potential module type. type Module struct { Type string Success bool Name string `toml:"Name"` PriorityGroup int `toml:"PriorityGroup"` FatalOnError bool `toml:"FatalOnError"` RunOnce bool `toml:"RunOnce"` RunPerBoot bool `toml:"RunPerBoot"` RunPerInstance bool `toml:"RunPerInstance"` CommandModule CommandModule `toml:"Command"` MOTDModule MOTDModule `toml:"MOTD"` SSHKeysModule SSHKeysModule `toml:"SSHKeys"` UserDataModule UserDataModule `toml:"UserData"` NetworkCheckModule NetworkCheckModule `toml:"NetworkCheck"` SystemConfigModule SystemConfigModule `toml:"SystemConfig"` UserManagementModule UserManagementModule `toml:"UserManagement"` } // ModuleContext contains fields that may need to be passed to the Do function for modules. type ModuleContext struct { Logger *Logger IMDS *IMDSConfig BaseDirectory string } // InstanceHistoryPath provides the history storage path for the current // instance. func (m ModuleContext) InstanceHistoryPath() string { if m.IMDS == nil || strings.TrimSpace(m.IMDS.InstanceID) == "" { // do *not* allow callers of this method to continue program run as // operation without this data may cause history to be lost and // subsequent ec2-macos-init runs to operate inappropriately. panic("no instance-id available") } return paths.InstanceHistory(m.BaseDirectory, m.IMDS.InstanceID) } // validateModule performs the following checks: // 1. Check that there is exactly one Run type set // 2. Check that Priority is set and is not less than 1 func (m *Module) validateModule() (err error) { // Check that there is exactly one Run type set var runs int8 if m.RunOnce { runs++ } if m.RunPerBoot { runs++ } if m.RunPerInstance { runs++ } if runs != 1 { return fmt.Errorf("ec2macosinit: incorrect number of run types\n") } // Check that Priority is set and not 0 or negative (must be 1 or greater) if m.PriorityGroup < 1 { return fmt.Errorf("ec2macosinit: module priority is unset or less than 1\n") } return nil } // identifyModule assigns a type to a module by comparing the empty struct for that module with the value provided. // This approach requires that a given module only have a single Type. func (m *Module) identifyModule() (err error) { if !cmp.Equal(m.CommandModule, CommandModule{}) { m.Type = "command" return nil } if !cmp.Equal(m.MOTDModule, MOTDModule{}) { m.Type = "motd" return nil } if !cmp.Equal(m.SSHKeysModule, SSHKeysModule{}) { m.Type = "sshkeys" return nil } if !cmp.Equal(m.UserDataModule, UserDataModule{}) { m.Type = "userdata" return nil } if !cmp.Equal(m.NetworkCheckModule, NetworkCheckModule{}) { m.Type = "networkcheck" return nil } if !cmp.Equal(m.SystemConfigModule, SystemConfigModule{}) { m.Type = "systemconfig" return nil } if !cmp.Equal(m.UserManagementModule, UserManagementModule{}) { m.Type = "usermanagement" return nil } return fmt.Errorf("ec2macosinit: unable to identify module type\n") } // generateHistoryKey takes a module and generates a key to be used in the instance history for that module. // History Key Format: key = m.PriorityLevel_RunType_m.Type_m.Name func (m *Module) generateHistoryKey() (key string) { // Generate key var runType string if m.RunOnce { runType = "RunOnce" } if m.RunPerInstance { runType = "RunPerInstance" } if m.RunPerBoot { runType = "RunPerBoot" } return strconv.Itoa(m.PriorityGroup) + "_" + runType + "_" + m.Type + "_" + m.Name } // ShouldRun determines if a module should be run, given a current instance ID and history. There are three cases: // 1. RunPerBoot - The module should run every boot, no matter what. The simplest case. // 2. RunPerInstance - The module should run once on every instance. Here we must look for the current instance ID // in the instance history and if found, compare the current module's key with all successfully run keys. If // not found, run the module. If found and unsuccessful, run the module. If found and successful, skip. // 3. RunOnce - The module should run once, ever. The process here is similar to RunPerInstance except the key must // be searched for in every instance history. If not found, run the module. If found and unsuccessful, run the // module. If found and successful, skip. func (m *Module) ShouldRun(instanceID string, history []History) (shouldRun bool) { // RunPerBoot runs every time if m.RunPerBoot { return true } // The rest will use the history key key := m.generateHistoryKey() // RunPerInstance only runs if the module's key doesn't exist in the current instance history and has // not run successfully. if m.RunPerInstance { // Check each instance in the instance history for _, instance := range history { if instanceID == instance.InstanceID { // If the current instance matches an ID in the history, check every module history for that instance for _, moduleHistory := range instance.ModuleHistories { if key == moduleHistory.Key && moduleHistory.Success { // If there is a matching key and it completed successfully, it doesn't need to be run return false } } // If there is an instance that matches and no keys match, run the module return true } } // If no instances match the instance history, run the module return true } // RunOnce only runs if the module's key doesn't exist in any instance history if m.RunOnce { for _, instance := range history { // Check every module history for that instance for _, moduleHistory := range instance.ModuleHistories { if key == moduleHistory.Key && moduleHistory.Success { // If there is a matching key and it completed successfully, it doesn't need to be run return false } } } // If no instances match the instance history, run the module return true } // Default here is false, though this position should never be reached. Preference is to not run actions which // may be potentially mutating but are misconfigured. return false }