asteris-llc/converge

View on GitHub
resource/status.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package resource

import (
    "errors"
    "fmt"
)

// StatusLevel will be used as a level in Status. It indicates if a resource
// needs to be changed, as well as fatal conditions.
type StatusLevel uint32

const (
    // StatusNoChange means no changes are necessary. This status signals that
    // execution of dependent resources can continue.
    StatusNoChange StatusLevel = iota

    // StatusWontChange indicates an acceptable delta that wont be corrected.
    // This status signals that execution of dependent resources can continue.
    StatusWontChange

    // StatusWillChange indicates an unacceptable delta that will be corrected.
    // This status signals that execution of dependent resources can continue.
    StatusWillChange

    // StatusMayChange indicates an unacceptable delta that may be corrected. This
    // is considered a warning state that indicates that the resource needs to
    // change but is likely dependent on the succesful execution of another
    // resource. This status signals that execution of dependent resources can
    // continue.
    StatusMayChange

    // StatusCantChange indicates an unacceptable delta that can't be corrected.
    // This is just like StatusFatal except the user will see that the resource
    // needs to change, but can't because of the condition specified in your
    // messaging. This status halts execution of dependent resources.
    StatusCantChange

    // StatusFatal indicates an error. This is just like StatusCantChange except
    // it does not imply that there are changes to be made. This status halts
    // execution of dependent resources.
    StatusFatal
)

// error messages
var (
    // ErrStatusCantChange is returned when an error is not set but the level is
    // StatusCantChange
    ErrStatusCantChange = errors.New("resource cannot change because of an error")

    // ErrStatusFatal is returned when an error is not set but the level is
    // StatusFatal
    ErrStatusFatal = errors.New("resource encountered an error")
)

func (l StatusLevel) String() string {
    switch l {
    case StatusNoChange:
        return "no change"

    case StatusWontChange:
        return "won't change"

    case StatusWillChange:
        return "will change"

    case StatusMayChange:
        return "may change"

    case StatusCantChange:
        return "can't change"

    case StatusFatal:
        return "fatal"
    }

    return "invalid status level"
}

type badDep struct {
    ID     string
    Status TaskStatus
}

// TaskStatus represents the results of Check called during planning or
// application.
type TaskStatus interface {
    Diffs() map[string]Diff
    StatusCode() StatusLevel
    Messages() []string
    HasChanges() bool
    Error() error
    Warning() string
    UpdateExportedFields(Task) error
    ExportedFields() FieldMap
}

// Status is the default TaskStatus implementation
type Status struct {
    // Differences contains the things that will change as a part of this
    // Status. This will be used almost exclusively in the Check phase of
    // operations on resources. Use `NewStatus` to get a Status with this
    // initialized properly.
    Differences map[string]Diff

    // Output is the human-consumable fields on this struct. Output will be
    // returned as the Status' messages
    Output []string

    // Level indicates the change level of the status. Level is a gradation (see
    // the Status* contsts above.)
    Level StatusLevel

    // Exported fields contains the fields that should be exported through lookup
    exportedFields FieldMap

    error       error
    warning     string
    failingDeps []badDep
}

// UpdateExportedFields sets the exported fields in the status
func (t *Status) UpdateExportedFields(input Task) error {
    fields, err := LookupMapFromInterface(input)
    if err != nil {
        return err
    }
    t.exportedFields = fields
    return nil
}

// ExportedFields returns the exported fields from the status
func (t *Status) ExportedFields() FieldMap {
    if t.exportedFields == nil {
        t.exportedFields = make(FieldMap)
    }
    return t.exportedFields
}

// NewStatus returns a Status with all fields initialized
func NewStatus() *Status {
    return &Status{
        Differences: map[string]Diff{},
    }
}

// SetWarning sets a warning on a status
func (t *Status) SetWarning(warning string) {
    if t == nil {
        *t = *NewStatus()
    }
    t.warning = warning
}

// SetError sets an error on a status
func (t *Status) SetError(err error) {
    if t == nil {
        *t = *NewStatus()
    }

    switch t.Level {
    case StatusWillChange, StatusCantChange:
        t.Level = StatusCantChange

    default:
        t.Level = StatusFatal
    }

    t.error = err
}

// Warning returns the warning message, if set.
func (t *Status) Warning() string {
    return t.warning
}

// Error returns an error, if set. If the level is StatusCantChange or
// StatusFatal and an error is not set, Error will generate an appropriate
// error message.
func (t *Status) Error() error {
    if t.error == nil {
        switch t.Level {
        case StatusCantChange:
            return ErrStatusCantChange

        case StatusFatal:
            return ErrStatusFatal
        }
    }

    return t.error
}

// Diffs returns the internal differences
func (t *Status) Diffs() map[string]Diff {
    return t.Differences
}

// StatusCode returns the current warning level
func (t *Status) StatusCode() StatusLevel {
    return t.Level
}

// Messages returns the current output slice
func (t *Status) Messages() []string {
    return t.Output
}

// HasChanges returns the WillChange value
func (t *Status) HasChanges() bool {
    if t.Level == StatusFatal {
        return false
    }

    if t.Level == StatusWillChange || t.Level == StatusCantChange || t.Level == StatusMayChange {
        return true
    }

    for _, diff := range t.Diffs() {
        if diff.Changes() {
            return true
        }
    }

    return false
}

// HealthCheck provides a default health check implementation for statuses
func (t *Status) HealthCheck() (status *HealthStatus, err error) {
    status = &HealthStatus{TaskStatus: t, FailingDeps: make(map[string]string)}
    if !t.HasChanges() && len(t.failingDeps) == 0 {
        return
    }

    // There are changes or failing dependencies so the health check is at least
    // at a warning status.
    status.UpgradeWarning(StatusWarning)

    for _, failingDep := range t.failingDeps {
        status.FailingDeps[failingDep.ID] = fmt.Sprintf("returned %d", failingDep.Status.StatusCode())
    }
    if t.StatusCode() >= 2 {
        status.UpgradeWarning(StatusError)
    }
    return
}

// FailingDep tracks a new failing dependency
func (t *Status) FailingDep(id string, stat TaskStatus) {
    t.failingDeps = append(t.failingDeps, badDep{ID: id, Status: stat})
}

// AddDifference adds a TextDiff to the Differences map
func (t *Status) AddDifference(name, original, current, defaultVal string) {
    t.Differences = AddTextDiff(t.Differences, name, original, current, defaultVal)
}

// AddMessage adds a human-readable message(s) to the output
func (t *Status) AddMessage(message ...string) {
    t.Output = append(t.Output, message...)
}

// RaiseLevel raises the status level to the given level
func (t *Status) RaiseLevel(level StatusLevel) {
    if level > t.Level {
        t.Level = level
    }
}

// RaiseLevelForDiffs raises the status level to StatusWillChange if there are
// differences in the Differences map
func (t *Status) RaiseLevelForDiffs() {
    if AnyChanges(t.Differences) {
        t.RaiseLevel(StatusWillChange)
    }
}

// Diff represents a difference
type Diff interface {
    Original() string
    Current() string
    Changes() bool
}

// TextDiff is the default Diff implementation
type TextDiff struct {
    Default string
    Values  [2]string
}

// Original returns the unmodified value of the diff
func (t TextDiff) Original() string {
    if t.Values[0] == "" {
        return t.Default
    }
    return t.Values[0]
}

// Current returns the modified value of the diff
func (t TextDiff) Current() string {
    if t.Values[1] == "" {
        return t.Default
    }
    return t.Values[1]
}

// Changes is true if the Original and Current values differ
func (t TextDiff) Changes() bool {
    return t.Values[0] != t.Values[1]
}

// AnyChanges takes a diff map and returns true if any of the diffs in the map
// have changes.
func AnyChanges(diffs map[string]Diff) bool {
    for _, diffIf := range diffs {
        diff, ok := diffIf.(Diff)
        if !ok {
            panic("invalid conversion")
        }
        if diff.Changes() {
            return true
        }
    }
    return false
}

// AddTextDiff inserts a new TextDiff into a map of names to Diffs
func AddTextDiff(m map[string]Diff, name, original, current, defaultVal string) map[string]Diff {
    if m == nil {
        m = make(map[string]Diff)
    }
    m[name] = TextDiff{Values: [2]string{original, current}, Default: defaultVal}
    return m
}