lib/ec2macosinit/instancehistory.go (123 lines of code) (raw):
package ec2macosinit
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// This is unused for now but will allow us to modify the version of this history in the future.
const historyVersion = 1
// History contains an instance ID, run time and a slice of individual module histories.
type History struct {
InstanceID string `json:"instanceID"`
RunTime time.Time `json:"runTime"`
ModuleHistories []ModuleHistory `json:"moduleHistory"`
Version int `json:"version"`
}
// ModuleHistory contains a key of the configuration struct for future comparison and whether that run was successful.
type ModuleHistory struct {
Key string `json:"key"`
Success bool `json:"success"`
}
// HistoryError wraps a normal error and gives the caller insight into the type of error.
// The caller can check the type of error and handle different types of error differently.
// Currently HistoryError only handles errors for invalid JSON but the struct is flexible
// and can be adjusted to handle several different errors differently.
type HistoryError struct {
err error
}
func (h HistoryError) Unwrap() error {
return h.err
}
func (h HistoryError) Error() string {
return h.err.Error()
}
// GetInstanceHistory takes a path to instance history directory and a file name for history files and searches for
// any files that match. Then, for each file, it calls readHistoryFile() to read the file and add it to the
// InstanceHistory struct.
func (c *InitConfig) GetInstanceHistory() (err error) {
// Read instance history directory
dirs, err := os.ReadDir(c.HistoryPath)
if err != nil {
return fmt.Errorf("ec2macosinit: unable to read instance history directory: %w", err)
}
// For each directory, check for a history file and call readHistoryFile()
for _, dir := range dirs {
if dir.IsDir() {
historyFile := filepath.Join(c.HistoryPath, dir.Name(), c.HistoryFilename)
if info, err := os.Stat(historyFile); err == nil {
// Check to make sure info is a file and not a directory.
if !info.Mode().IsRegular() {
continue
}
// If there is an error getting the history file or if the history file is empty do not append to Instance History
if info.Size() == 0 {
c.Log.Warnf("The history file exists at %s but is empty. Skipping this file...", historyFile)
continue
}
history, err := readHistoryFile(historyFile)
if err != nil {
return fmt.Errorf("ec2macosinit: error while reading history file at %s: %w", historyFile, err)
}
// Append the returned History struct to the InstanceHistory slice
c.InstanceHistory = append(c.InstanceHistory, history)
}
}
}
return nil
}
// readHistoryFile takes an instance history file and returns a History struct containing the same information.
func readHistoryFile(file string) (history History, err error) {
// Read file
historyBytes, err := os.ReadFile(file)
if err != nil {
return History{}, fmt.Errorf("ec2macosinit: error reading config file located at %s: %w", file, err)
}
// Unmarshal to struct
err = json.Unmarshal(historyBytes, &history)
if err != nil {
return History{}, HistoryError{err: err}
}
return history, nil
}
// WriteHistoryFile takes ModulesByPriority and writes it to a given history path and filename as JSON.
func (c *InitConfig) WriteHistoryFile() (err error) {
history := History{
InstanceID: c.IMDS.InstanceID,
RunTime: time.Now(),
Version: historyVersion,
}
// Copy relevant fields from InitConfig to History struct
for _, p := range c.ModulesByPriority {
for _, m := range p {
history.ModuleHistories = append(
history.ModuleHistories,
ModuleHistory{
Key: m.generateHistoryKey(),
Success: m.Success,
},
)
}
}
// Marshal to JSON
historyBytes, err := json.Marshal(history)
if err != nil {
return fmt.Errorf("ec2macosinit: unable to write history file: %w", err)
}
// Ensure the path exists and create it if it doesn't
err = c.CreateDirectories()
if err != nil {
return fmt.Errorf("ec2macosinit: unable to write history file: :%w", err)
}
// Write history JSON file
path := filepath.Join(c.HistoryPath, c.IMDS.InstanceID, c.HistoryFilename)
err = safeWrite(path, historyBytes)
if err != nil {
return fmt.Errorf("ec2macosinit: unable to write history file: %w", err)
}
return nil
}
// safeWrite writes data to the desired file path or not at all. This function
// protects against partially written or unflushed data intended for the file.
func safeWrite(path string, data []byte) error {
f, err := os.CreateTemp(filepath.Dir(path), fmt.Sprintf(".%s.*", filepath.Base(path)))
if err != nil {
return err
}
defer os.Remove(f.Name())
defer f.Close()
_, err = f.Write(data)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
return os.Rename(f.Name(), path)
}
// CreateDirectories creates the instance directory, if it doesn't exist and a directory for the running instance.
func (c *InitConfig) CreateDirectories() (err error) {
if _, err := os.Stat(filepath.Join(c.HistoryPath, c.IMDS.InstanceID)); os.IsNotExist(err) {
err := os.MkdirAll(filepath.Join(c.HistoryPath, c.IMDS.InstanceID), 0755)
if err != nil {
return fmt.Errorf("ec2macosinit: unable to create directory: %w", err)
}
}
return nil
}