helpers/foundation-deployer/steps/steps.go (195 lines of code) (raw):
// Copyright 2023 Google 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 steps
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
)
const (
completedStatus = "COMPLETED"
destroyedStatus = "DESTROYED"
failedStatus = "FAILED"
pendingStatus = "PENDING"
)
type Step struct {
Name string `json:"name"`
Status string `json:"status"`
Error string `json:"error"`
}
type Steps struct {
File string `json:"file"`
Steps map[string]Step `json:"steps"`
}
// String creates a string representation of the step
func (s Step) String() string {
if s.Error == "" {
return fmt.Sprintf("%s %s", s.Name, s.Status)
}
return fmt.Sprintf("%s %s error:%s", s.Name, s.Status, s.Error)
}
// DeleteStepsFile deletes the whole steps file
func DeleteStepsFile(file string) error {
_, err := os.Stat(file)
if err != nil && !os.IsNotExist(err) {
return err
}
if !os.IsNotExist(err) {
return os.Remove(file)
}
return nil
}
// LoadSteps loads a previous execution steps from the given file.
func LoadSteps(file string) (Steps, error) {
var s Steps
_, err := os.Stat(file)
if err != nil && !os.IsNotExist(err) {
return s, err
}
if os.IsNotExist(err) {
fmt.Printf("# creating new steps file '%s'.\n", file)
s = Steps{
File: file,
}
} else {
f, err := os.ReadFile(file)
if err != nil {
return s, err
}
err = json.Unmarshal(f, &s)
if err != nil {
return s, err
}
s.File = file
}
if s.Steps == nil {
s.Steps = map[string]Step{}
}
return s, nil
}
// SaveSteps saves the current execution state of the steps in the file that was loaded.
func (s Steps) SaveSteps() error {
f, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.File, f, 0644)
}
// CompleteStep marks a given step as completed.
func (s Steps) CompleteStep(name string) error {
s.Steps[name] = Step{
Name: name,
Status: completedStatus,
}
err := s.SaveSteps()
if err != nil {
return err
}
fmt.Printf("# completing step '%s' execution\n", name)
return nil
}
// IsStepComplete checks if the given step is completed.
func (s Steps) IsStepComplete(name string) bool {
v, ok := s.Steps[name]
if ok {
return v.Status == completedStatus
}
return false
}
// StepExists checks if the given step exists
func (s Steps) StepExists(name string) bool {
_, ok := s.Steps[name]
return ok
}
// FailStep marks a given step as failed and saves the error message.
func (s Steps) FailStep(name string, err string) error {
s.Steps[name] = Step{
Name: name,
Status: failedStatus,
Error: err,
}
e := s.SaveSteps()
if e != nil {
return e
}
return nil
}
func isNested(name string) bool {
return strings.Contains(name, ".")
}
func parent(name string) string {
return strings.Split(name, ".")[0]
}
// ResetStep resets the execution status of a given step and its parent.
func (s Steps) ResetStep(name string) error {
s.Steps[name] = Step{
Name: name,
Status: pendingStatus,
}
err := s.SaveSteps()
if err != nil {
return err
}
fmt.Printf("# resetting step '%s' execution\n", name)
if isNested(name) {
return s.ResetStep(parent(name))
}
return nil
}
// GetStepError gets the error message save in an step.
func (s Steps) GetStepError(name string) string {
v, ok := s.Steps[name]
if ok {
return v.Error
}
return ""
}
// ListSteps lists the executed steps.
func (s Steps) ListSteps() []string {
l := []string{}
for _, v := range s.Steps {
l = append(l, v.String())
}
sort.Strings(l)
return l
}
// RunStep executes a step and marks it as completed or failed.
// Completed steps are not executed again.
func (s Steps) RunStep(step string, f func() error) error {
if s.IsStepComplete(step) {
fmt.Printf("# skipping step '%s' execution\n", step)
return nil
}
fmt.Printf("# starting step '%s' execution\n", step)
err := f()
if err != nil {
e := s.FailStep(step, err.Error())
if e != nil {
return fmt.Errorf("error on FailStep %v, original error %w", e, err)
}
return err
}
return s.CompleteStep(step)
}
// IsStepDestroyed checks is the step was destroyed
func (s Steps) IsStepDestroyed(name string) bool {
v, ok := s.Steps[name]
if ok {
return v.Status == destroyedStatus
}
return false
}
// DestroyStep destroys the given step
func (s Steps) DestroyStep(name string) error {
s.Steps[name] = Step{
Name: name,
Status: destroyedStatus,
}
err := s.SaveSteps()
if err != nil {
return err
}
fmt.Printf("# destroying step '%s'\n", name)
return nil
}
// RunDestroyStep destroys a step and marks it as destroyed or failed.
func (s Steps) RunDestroyStep(step string, f func() error) error {
if s.IsStepDestroyed(step) || !s.StepExists(step) {
fmt.Printf("# skipping step '%s' destruction\n", step)
return nil
}
fmt.Printf("# starting step '%s' destruction\n", step)
err := f()
if err != nil {
e := s.FailStep(step, err.Error())
if e != nil {
return fmt.Errorf("error on FailStep %v, original error %w", e, err)
}
return err
}
return s.DestroyStep(step)
}