lib/ec2macosinit/module.go (129 lines of code) (raw):
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
}