github.com/kubeshop/testkube@v1.17.23/cmd/tcl/testworkflow-init/data/state.go (about)

     1  // Copyright 2024 Testkube.
     2  //
     3  // Licensed as a Testkube Pro file under the Testkube Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //	https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
     8  
     9  package data
    10  
    11  import (
    12  	"bytes"
    13  	"encoding/gob"
    14  	"encoding/json"
    15  	"fmt"
    16  	"os"
    17  	"path/filepath"
    18  	"sync"
    19  
    20  	"github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
    21  )
    22  
    23  const (
    24  	defaultInternalPath       = "/.tktw"
    25  	defaultTerminationLogPath = "/dev/termination-log"
    26  )
    27  
    28  type state struct {
    29  	Status TestWorkflowStatus   `json:"status"`
    30  	Steps  map[string]*StepInfo `json:"steps"`
    31  	Output map[string]string    `json:"output"`
    32  }
    33  
    34  var State = &state{
    35  	Steps:  map[string]*StepInfo{},
    36  	Output: map[string]string{},
    37  }
    38  
    39  func (s *state) GetStep(ref string) *StepInfo {
    40  	_, ok := State.Steps[ref]
    41  	if !ok {
    42  		State.Steps[ref] = &StepInfo{Ref: ref}
    43  	}
    44  	return State.Steps[ref]
    45  }
    46  
    47  func (s *state) GetOutput(name string) (expressionstcl.Expression, bool, error) {
    48  	v, ok := s.Output[name]
    49  	if !ok {
    50  		return expressionstcl.None, false, nil
    51  	}
    52  	expr, err := expressionstcl.Compile(v)
    53  	return expr, true, err
    54  }
    55  
    56  func (s *state) SetOutput(ref, name string, value interface{}) {
    57  	if s.Output == nil {
    58  		s.Output = make(map[string]string)
    59  	}
    60  	v, err := json.Marshal(value)
    61  	if err == nil {
    62  		s.Output[name] = string(v)
    63  	} else {
    64  		fmt.Printf("Warning: couldn't save '%s' (%s) output: %s\n", name, ref, err.Error())
    65  	}
    66  }
    67  
    68  func (s *state) GetSelfStatus() string {
    69  	if Step.Executed {
    70  		return string(Step.Status)
    71  	}
    72  	v := s.GetStep(Step.Ref)
    73  	if v.Status != StepStatusPassed {
    74  		return string(v.Status)
    75  	}
    76  	return string(Step.Status)
    77  }
    78  
    79  func (s *state) GetStatus() string {
    80  	if Step.Executed {
    81  		return string(Step.Status)
    82  	}
    83  	if Step.InitStatus == "" {
    84  		return string(s.Status)
    85  	}
    86  	v, err := RefStatusExpression(Step.InitStatus)
    87  	if err != nil {
    88  		return string(s.Status)
    89  	}
    90  	str, _ := v.Static().StringValue()
    91  	if str == "" {
    92  		return string(s.Status)
    93  	}
    94  	return str
    95  }
    96  
    97  func readState(filePath string) {
    98  	b, err := os.ReadFile(filePath)
    99  	if err != nil {
   100  		if !os.IsNotExist(err) {
   101  			panic(err)
   102  		}
   103  		return
   104  	}
   105  	if len(b) == 0 {
   106  		return
   107  	}
   108  	err = gob.NewDecoder(bytes.NewBuffer(b)).Decode(&State)
   109  	if err != nil {
   110  		panic(err)
   111  	}
   112  }
   113  
   114  func persistState(filePath string) {
   115  	b := bytes.Buffer{}
   116  	err := gob.NewEncoder(&b).Encode(State)
   117  	if err != nil {
   118  		panic(err)
   119  	}
   120  
   121  	err = os.WriteFile(filePath, b.Bytes(), 0777)
   122  	if err != nil {
   123  		panic(err)
   124  	}
   125  }
   126  
   127  func recomputeStatuses() {
   128  	// Read current status
   129  	status := StepStatus(State.GetSelfStatus())
   130  
   131  	// Update own status
   132  	State.GetStep(Step.Ref).SetStatus(status)
   133  
   134  	// Update expected failure statuses
   135  	Iterate(Config.Resulting, func(r Rule) bool {
   136  		v, err := RefSuccessExpression(r.Expr)
   137  		if err != nil {
   138  			return false
   139  		}
   140  		vv, _ := v.Static().BoolValue()
   141  		if !vv {
   142  			for _, ref := range r.Refs {
   143  				if ref == "" {
   144  					State.Status = TestWorkflowStatusFailed
   145  				} else {
   146  					State.GetStep(ref).SetStatus(StepStatusFailed)
   147  				}
   148  			}
   149  		}
   150  		return true
   151  	})
   152  }
   153  
   154  func persistStatus(filePath string) {
   155  	// Persist container termination result
   156  	res := fmt.Sprintf(`%s,%d`, State.GetStep(Step.Ref).Status, Step.ExitCode)
   157  	err := os.WriteFile(filePath, []byte(res), 0755)
   158  	if err != nil {
   159  		panic(err)
   160  	}
   161  }
   162  
   163  var loadStateMu sync.Mutex
   164  var loadedState bool
   165  
   166  func LoadState() {
   167  	defer loadStateMu.Unlock()
   168  	loadStateMu.Lock()
   169  	if !loadedState {
   170  		readState(filepath.Join(defaultInternalPath, "state"))
   171  		loadedState = true
   172  	}
   173  }
   174  
   175  func Finish() {
   176  	// Persist step information and shared data
   177  	recomputeStatuses()
   178  	persistStatus(defaultTerminationLogPath)
   179  	persistState(filepath.Join(defaultInternalPath, "state"))
   180  
   181  	// Kill the sub-process
   182  	if Step.Cmd != nil && Step.Cmd.Process != nil {
   183  		_ = Step.Cmd.Process.Kill()
   184  	}
   185  
   186  	// Emit end hint to allow exporting the timestamp
   187  	PrintHint(Step.Ref, "end")
   188  
   189  	// The init process needs to finish with zero exit code,
   190  	// to continue with the next container.
   191  	os.Exit(0)
   192  }