pkg/status/status.go (130 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package status import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) const ( // extension needn't write status files for other operations EnableStatus = "Enable" UpdateStatus = "Update" DisableStatus = "Disable" ) const ( ErrorClarificationSubStatusName = "ErrorClarification" ) type CmdFunc func() (message string, err error) type Cmd struct { f CmdFunc // associated function name string // human readable string shouldReportStatus bool // determines if running this should log to a .status file failExitCode int // exitCode to use when commands fail } // StatusReport contains one or more status items and is the parent object type StatusReport []StatusItem // StatusItem is used to serialize an individual part of the status read by the server type StatusItem struct { Version int `json:"version"` TimestampUTC string `json:"timestampUTC"` Status Status `json:"status"` } type ErrorClarification struct { Code int Message string } type StatusType string const ( // StatusTransitioning indicates the operation has begun but not yet completed StatusTransitioning StatusType = "transitioning" // StatusError indicates the operation failed StatusError StatusType = "error" // StatusSuccess indicates the operation succeeded StatusSuccess StatusType = "success" ) // Status is used for serializing status in a manner the server understands type Status struct { Operation string `json:"operation"` Status StatusType `json:"status"` FormattedMessage FormattedMessage `json:"formattedMessage"` Substatuses []Substatus `json:"substatus"` } type Substatus struct { Name string `json:"name"` Status string `json:"status"` Code int `json:"code"` } // FormattedMessage is a struct used for serializing status type FormattedMessage struct { Lang string `json:"lang"` Message string `json:"message"` } // New creates a new Status instance func New(t StatusType, operation string, message string) StatusReport { return []StatusItem{ { Version: 1, // this is the protocol version do not change unless you are sure TimestampUTC: time.Now().UTC().Format(time.RFC3339), Status: Status{ Operation: operation, Status: t, FormattedMessage: FormattedMessage{ Lang: "en", Message: message}, }, }, } } func NewError(operation string, ec ErrorClarification) StatusReport { return []StatusItem{ { Version: 1, // this is the protocol version do not change unless you are sure TimestampUTC: time.Now().UTC().Format(time.RFC3339), Status: Status{ Operation: operation, Status: StatusError, FormattedMessage: FormattedMessage{ Lang: "en", Message: ec.Message}, Substatuses: []Substatus{ { Name: ErrorClarificationSubStatusName, Status: string(StatusError), Code: ec.Code, }, }, }, }, } } func (r StatusReport) marshal() ([]byte, error) { return json.MarshalIndent(r, "", "\t") } // Save persists the status message to the specified status folder using the // sequence number. The operation consists of writing to a temporary file in the // same folder and moving it to the final destination for atomicity. func (r StatusReport) Save(statusFolder string, seqNo uint) error { fn := fmt.Sprintf("%d.status", seqNo) path := filepath.Join(statusFolder, fn) tmpFile, err := os.CreateTemp(statusFolder, fn) if err != nil { return fmt.Errorf("status: failed to create temporary file: %v", err) } tmpFile.Close() b, err := r.marshal() if err != nil { return fmt.Errorf("status: failed to marshal into json: %v", err) } if err := os.WriteFile(tmpFile.Name(), b, 0644); err != nil { return fmt.Errorf("status: failed to path=%s error=%v", tmpFile.Name(), err) } if err := os.Rename(tmpFile.Name(), path); err != nil { return fmt.Errorf("status: failed to move to path=%s error=%v", path, err) } return nil } type StatusMessageFormatter func(operationName string, t StatusType, msg string) string // StatusMsg creates the reported status message based on the provided operation // type and the given message string. // // A message will be generated for empty string. For error status, pass the // error message. func StatusMsg(operationName string, t StatusType, msg string) string { s := operationName switch t { case StatusSuccess: s += " succeeded" case StatusTransitioning: s += " in progress" case StatusError: s += " failed" } if msg != "" { // append the original s += ": " + msg } return s }