blob: 21a9096eb5429651155af1cb6a6d7759bff88e23 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// 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 annotation
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/timestamp"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/proto/google"
"go.chromium.org/luci/common/proto/srcman"
"go.chromium.org/luci/logdog/common/types"
annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto"
)
// UpdateType is information sent to the Updated callback to indicate the nature
// of the update.
type UpdateType int
const (
// UpdateIterative indicates that a non-structural update occurred.
UpdateIterative UpdateType = iota
// UpdateStructural indicates that a structural update has occurred. A
// structural update is one that affects the existence of or relationship of
// the Steps in the annotation.
UpdateStructural
)
// Callbacks is the set of callbacks that a State may invoke as it processes
// annotations.
type Callbacks interface {
// StepClosed is called when a Step has closed. An Updated callback will still
// be invoked.
StepClosed(*Step)
// Updated is called when a Step's state has been updated.
Updated(*Step, UpdateType)
// StepLogLine is called when a Step emits a log line.
StepLogLine(s *Step, stream types.StreamName, label, line string)
// StepLogEnd is called when a Step finishes emitting logs.
StepLogEnd(*Step, types.StreamName)
}
// State is the aggregate annotation state for a given annotation
// stream. It receives updates in the form of annotations added via Append,
// and can be serialized to an annotation state protobuf.
type State struct {
// LogNameBase is the base log stream name that is prepeneded to generated
// log streams.
LogNameBase types.StreamName
// Callbacks implements annotation callbacks. It may not be nil.
Callbacks Callbacks
// Execution is the supplied Execution. If nil, no execution details will be
// added to the generated annotation protos.
Execution *Execution
// Offline specifies whether parsing happens not at the same time as
// emitting. If true and CURRENT_TIMESTAMP annotations are not provided
// then step start/end times are left empty.
Offline bool
// Clock is the clock implementation to use for time information.
// Defaults to system time.
Clock clock.Clock
// stepMap is a map of step name to Step instance.
//
// If stepMap is nil, the State is considered uninitialized.
stepMap map[string]*Step
latestStep *Step
rootStep Step
// stepCursor is the current cursor step name. This will always point to a
// valid Step, falling back to rootStep.
stepCursor *Step
// startedProcessing is true iff processed at least one annotation.
startedProcessing bool
// stepLookup is a mapping of *annopb.Step entries to their respective *Step
// entries.
stepLookup map[*annopb.Step]*Step
// currentTimestamp is time for the next annotation expected in Append.
currentTimestamp *timestamp.Timestamp
closed bool
haltOnFailure bool
}
// initialize sets of the State's initial state. It will execute exactly once,
// and must be called by any State methods that access internal variables.
func (s *State) initialize() {
if s.stepMap != nil {
return
}
s.stepMap = map[string]*Step{}
s.stepLookup = map[*annopb.Step]*Step{}
name := "steps"
if s.Execution != nil {
name = s.Execution.Name
}
s.rootStep.initializeStep(s, nil, name, false)
s.rootStep.LogNameBase = s.LogNameBase
s.SetCurrentStep(nil)
// Add our Command parameters, if applicable.
if exec := s.Execution; exec != nil {
s.rootStep.Command = &annopb.Step_Command{
CommandLine: exec.Command,
Cwd: exec.Dir,
Environ: exec.Env,
}
}
var annotatedNow *timestamp.Timestamp
if !s.Offline {
annotatedNow = s.now()
}
s.rootStep.Start(annotatedNow)
}
// Append adds an annotation to the state. If the state was updated, Append will
// return true.
//
// The appended annotation should only contain the annotation text body, not any
// annotation indicators (e.g., "@@@") that surround it.
//
// If the annotation is invalid or could not be added to the state, an error
// will be returned.
//
// Steps and descriptions can be found at:
// https://chromium.googlesource.com/chromium/tools/build/+/master/scripts/
// master/chromium_step.py
func (s *State) Append(annotation string) error {
s.initialize()
firstAnnotation := !s.startedProcessing
s.startedProcessing = true
command, params := annotation, ""
splitIdx := strings.IndexAny(command, "@ ")
if splitIdx > 0 {
command, params = command[:splitIdx], command[splitIdx+1:]
}
if s.closed {
return nil
}
var (
updated *Step
updateType UpdateType
)
updatedIf := func(s *Step, u UpdateType, b bool) {
if b {
updated, updateType = s, u
}
}
annotatedNow := s.currentTimestamp
s.currentTimestamp = nil
if annotatedNow == nil && !s.Offline {
annotatedNow = s.now()
}
switch command {
// @@@CURRENT_TIMESTAMP@unix_timestamp@@@
case "CURRENT_TIMESTAMP":
// This annotation is printed at the beginning and end of the
// stream, as well as before each STEP_STARTED and STEP_CLOSED
// annotations. It effectively specifies step start/end times,
// including root step.
timestamp, err := strconv.ParseFloat(params, 64)
if err != nil {
return fmt.Errorf("CURRENT_TIMESTAMP parameter %q is not a number: %s", params, err)
}
s.currentTimestamp = google.NewTimestamp(time.Unix(
int64(timestamp),
int64(timestamp*1000000000)%1000000000))
if firstAnnotation {
s.rootStep.Started = s.currentTimestamp
}
// @@@BUILD_STEP <stepname>@@@
case "BUILD_STEP":
// Close the last section.
step := s.CurrentStep()
if step != nil && step != s.RootStep() {
if step.Name() == params {
// Same step; ignore the command.
break
}
if step.legacy {
step.Close(annotatedNow)
}
}
step = s.rootStep.AddStep(params, true)
step.Start(annotatedNow)
s.SetCurrentStep(step)
updatedIf(step, UpdateStructural, true)
// @@@SEED_STEP <stepname>@@@
case "SEED_STEP":
step := s.LookupStep(params)
if step == nil {
step = s.rootStep.AddStep(params, false)
updatedIf(step, UpdateIterative, true)
}
// @@@STEP_CURSOR <stepname>@@@
case "STEP_CURSOR":
step, err := s.LookupStepErr(params)
if err != nil {
return fmt.Errorf("STEP_CURSOR could not lookup step: %s", err)
}
s.SetCurrentStep(step)
// @@@STEP_LINK@<label>@<url>@@@
case "link":
fallthrough
case "STEP_LINK":
step := s.CurrentStep()
parts := strings.SplitN(params, "@", 2)
if len(parts) != 2 {
return fmt.Errorf("STEP_LINK link [%s] missing URL", parts[0])
}
// If if link is an alias, parse it as one.
alias := strings.SplitN(parts[0], "-->", 2)
if len(alias) == 2 && len(alias[0]) > 0 && len(alias[1]) > 0 {
// parrts[0] is an alias of the form: "text-->base"
step.AddURLLink(alias[1], alias[0], parts[1])
} else {
step.AddURLLink(parts[0], "", parts[1])
}
updatedIf(step, UpdateIterative, true)
// @@@STEP_STARTED@@@
case "STEP_STARTED":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.Start(annotatedNow))
// @@@STEP_WARNINGS@@@
case "BUILD_WARNINGS":
fallthrough
case "STEP_WARNINGS":
// No warnings because they don't generally help. Builds that want to add
// information can do so with notes. A "WARNING" state is traditionally a
// success state with a call to attention, and that call can be done through
// other means.
break
// @@@STEP_FAILURE@@@
case "BUILD_FAILED":
fallthrough
case "STEP_FAILURE":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.SetStatus(annopb.Status_FAILURE, nil))
if s.haltOnFailure {
updatedIf(step, UpdateIterative, s.finishWithStatus(annopb.Status_FAILURE, nil))
}
// @@@STEP_EXCEPTION@@@
case "BUILD_EXCEPTION":
fallthrough
case "STEP_EXCEPTION":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.SetStatus(annopb.Status_FAILURE, &annopb.FailureDetails{
Type: annopb.FailureDetails_EXCEPTION,
}))
// @@@STEP_CLOSED@@@
case "STEP_CLOSED":
step := s.CurrentStep()
updatedIf(step, UpdateStructural, step.Close(annotatedNow))
// @@@STEP_LOG_LINE@<label>@<line>@@@
case "STEP_LOG_LINE":
step := s.CurrentStep()
parts := strings.SplitN(params, "@", 2)
label, line := parts[0], ""
if len(parts) == 2 {
line = parts[1]
}
updatedIf(step, UpdateIterative, step.LogLine(label, line))
// @@@STEP_LOG_END@<label>@@@
case "STEP_LOG_END":
s.CurrentStep().LogEnd(params)
// @@@STEP_LOG_END_PERF@<label>@@@
case "STEP_LOG_END_PERF":
// Ignore for now. Ideally would emit a link to the perf dashboard.
break
// @@@STEP_CLEAR@@@
case "STEP_CLEAR":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.ClearText())
// @@@STEP_SUMMARY_CLEAR@@@
case "STEP_SUMMARY_CLEAR":
step := s.CurrentStep()
step.ClearSummary()
updatedIf(step, UpdateIterative, true)
// @@@STEP_TEXT@<msg>@@@
case "STEP_TEXT":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.AddText(params))
// @@@SEED_STEP_TEXT@step@<msg>@@@
case "SEED_STEP_TEXT":
parts := strings.SplitN(params, "@", 2)
if len(parts) < 2 {
return nil
}
step, err := s.LookupStepErr(parts[0])
if err != nil {
return err
}
updatedIf(step, UpdateIterative, step.AddText(parts[1]))
// @@@STEP_SUMMARY_TEXT@<msg>@@@
case "STEP_SUMMARY_TEXT":
step := s.CurrentStep()
updatedIf(step, UpdateIterative, step.SetSummary(params))
// @@@STEP_NEST_LEVEL@<level>@@@
case "STEP_NEST_LEVEL":
level, err := strconv.Atoi(params)
if err != nil {
return fmt.Errorf("could not parse nest level from %q: %v", params, err)
}
if level < 0 {
return fmt.Errorf("level must be >= 0, not %d", level)
}
step := s.CurrentStep()
updatedIf(step, UpdateStructural, step.SetNestLevel(level))
break
// @@@HALT_ON_FAILURE@@@
case "HALT_ON_FAILURE":
s.haltOnFailure = true
// @@@HONOR_ZERO_RETURN_CODE@@@
case "HONOR_ZERO_RETURN_CODE":
// We don't capture the step return code, so not much we can do here.
break
// @@@SET_BUILD_PROPERTY@<name>@<json>@@@
case "SET_BUILD_PROPERTY":
step := s.CurrentStep()
parts := strings.SplitN(params, "@", 2)
if len(parts) == 1 {
parts = append(parts, "")
}
updatedIf(step, UpdateIterative, step.SetProperty(parts[0], parts[1]))
// @@@STEP_TRIGGER@<spec>@@@
case "STEP_TRIGGER":
// Annotee will stop short of sending an actual request to BuildBucket.
break
// This is ONLY supported by annotee, not by buildbot.
// @@@SOURCE_MANIFEST@<name>@<sha256>@<url>@@@
case "SOURCE_MANIFEST":
parts := strings.SplitN(params, "@", 3)
if len(parts) != 3 {
return fmt.Errorf("SOURCE_MANIFEST expected 3 params, got %q", params)
}
step := s.RootStep()
if step.SourceManifests == nil {
step.SourceManifests = map[string]*srcman.ManifestLink{}
}
name, hashHex, url := parts[0], parts[1], parts[2]
hash, err := hex.DecodeString(hashHex)
if err != nil {
return fmt.Errorf("SOURCE_MANIFEST has bad hash: %s", err)
}
if _, ok := step.SourceManifests[name]; ok {
return fmt.Errorf("repeated SOURCE_MANIFEST name %q", name)
}
step.SourceManifests[name] = &srcman.ManifestLink{
Sha256: hash,
Url: url,
}
updated = step
}
if updated != nil {
s.Callbacks.Updated(updated, updateType)
}
return nil
}
// Finish closes the top-level annotation state and any outstanding steps.
func (s *State) Finish() {
s.initialize()
s.finishAndDeriveStatus()
}
func (s *State) finishAndDeriveStatus() bool {
return s.finishWithStatusImpl(nil, nil)
}
func (s *State) finishWithStatus(st annopb.Status, fd *annopb.FailureDetails) bool {
return s.finishWithStatusImpl(&st, fd)
}
func (s *State) finishWithStatusImpl(status *annopb.Status, fd *annopb.FailureDetails) bool {
if s.closed {
return false
}
// if s.currentTimestamp is not nil, the last annotation was
// CURRENT_TIMESTAMP and s.currentTimestamp contains its value.
buildEndTime := s.currentTimestamp
s.currentTimestamp = nil
if buildEndTime == nil && !s.Offline {
buildEndTime = s.now()
}
// Traverse through every step *except* our root step.
unfinished := false
for step := s.rootStep.nextStep; step != nil; step = step.nextStep {
if u := step.closeWithStatus(buildEndTime, nil); u {
unfinished = true
}
}
// If some steps were unfinished, show a root exception.
if unfinished && status == nil {
exception := annopb.Status_FAILURE
status = &exception
if fd == nil {
fd = &annopb.FailureDetails{
Type: annopb.FailureDetails_EXCEPTION,
}
}
}
s.rootStep.FailureDetails = fd
s.rootStep.closeWithStatus(buildEndTime, status)
// Probe the status from our steps, if one is not supplied.
s.closed = true
return true
}
// LookupStep returns the step with the supplied name, or nil if no such step
// exists.
//
// If multiple steps share a name, this will return the latest registered step
// with that name.
func (s *State) LookupStep(name string) *Step { return s.stepMap[name] }
// LookupStepErr returns the step with the supplied name, or an error if no
// such step exists.
//
// If multiple steps share a name, this will return the latest registered step
// with that name.
func (s *State) LookupStepErr(name string) (*Step, error) {
if as := s.LookupStep(name); as != nil {
return as, nil
}
return nil, fmt.Errorf("no step named %q", name)
}
// ResolveStep returns the annotation package *Step corresponding to the
// supplied *annopb.Step. This is a reverse lookup operation.
//
// If the supplied *annopb.Step is not registered with this annotation State,
// this function will return nil.
func (s *State) ResolveStep(ms *annopb.Step) *Step { return s.stepLookup[ms] }
// RootStep returns the root step.
func (s *State) RootStep() *Step {
s.initialize()
return &s.rootStep
}
// CurrentStep returns the step referenced by the step cursor.
func (s *State) CurrentStep() *Step {
s.initialize()
return s.stepCursor
}
// SetCurrentStep sets the current step. If the supplied step is nil, the root
// step will be used.
//
// The supplied step must already be registered with the State.
func (s *State) SetCurrentStep(v *Step) {
if v == nil {
v = &s.rootStep
}
if v.s != s {
panic("step is not bound to state")
}
s.stepCursor = v
}
func (s *State) registerStep(as *Step) {
s.stepMap[as.Name()] = as
s.stepLookup[&as.Step] = as
if latest := s.latestStep; latest != nil {
latest.nextStep = as
as.prevStep = latest
}
s.latestStep = as
}
func (s *State) unregisterStep(as *Step) {
name := as.Name()
if cas := s.stepMap[name]; cas == as {
delete(s.stepMap, name)
}
if s.stepCursor == as {
s.stepCursor = as.closestOpenStep()
}
}
// now returns current time of s.Clock. Defaults to system clock.
func (s *State) now() *timestamp.Timestamp {
c := s.Clock
if c == nil {
c = clock.GetSystemClock()
}
return google.NewTimestamp(c.Now())
}
// Step represents a single step.
type Step struct {
annopb.Step
s *State
// parent is the step that spawned this step.
parent *Step
// prevStep is the step that was created immediately before this step. It is
// nil if this is the root step.
//
// Both prevStep and nextStep are creation-ordered, and don't change even if
// a Step is reparented.
prevStep *Step
// nextStep is the step that was created immediately after this step. It is
// nil if this is the latest step.
//
// Both prevStep and nextStep are creation-ordered, and don't change even if
// a Step is reparented.
nextStep *Step
level int
// legacy is explicit support for the legacy "BUILD_STEP" annotation. Any Step
// that is created via BUILD_STEP is considered a legacy step. Only legacy
// steps get automatically closed when a new "BUILD_STEP" annotation is
// encountered.
legacy bool
// logPathIndex is a map of the number of log paths with the given base name.
// Each time a log path is generated, it will register with this map and
// increase the count.
logPathIndex map[types.StreamName]int
// logLines is a map of log line labels to full log stream names.
logLines map[string]types.StreamName
// logLineCount is a map of log line label to the number of times that log
// line has appeared. This is to prevent the case where multiple log lines
// with the same label may be emitted, which would cause duplicate log stream
// names.
logLineCount map[string]int
// linkMap is a map of link label to link struct. BuildBot only retains the
// latest link for a given label, so we use this to enforce that.
linkMap map[string]*annopb.AnnotationLink
// logNameBase is the LogDog stream name root for this step.
LogNameBase types.StreamName
// hasSummary, if true, means that this Step has summary text. The summary
// text is stored as the first line in its Step.Text slice.
hasSummary bool
// closed is true if the element is closed.
closed bool
}
func (as *Step) String() string { return string(as.LogNameBase) }
func (as *Step) initializeStep(s *State, parent *Step, name string, legacy bool) *Step {
t := annopb.Status_RUNNING
as.Step = annopb.Step{
Name: name,
Status: t,
}
as.s = s
as.legacy = legacy
as.logLines = map[string]types.StreamName{}
as.logLineCount = map[string]int{}
as.logPathIndex = map[types.StreamName]int{}
// Add this Step to our parent's Substep list.
if parent != nil {
parent.appendSubstep(as)
}
s.registerStep(as)
return as
}
func (as *Step) appendSubstep(s *Step) {
if s.parent == as {
// Already parented to as, so do nothing.
return
}
s.detachFromParent()
s.parent = as
as.Substep = append(as.Substep, &annopb.Step_Substep{
Substep: &annopb.Step_Substep_Step{
Step: &s.Step,
},
})
s.regenerateLogPath()
}
func (as *Step) detachFromParent() {
parent := as.parent
if parent == nil {
return
}
// Remove any instances of "as" from its current parent's Substeps.
ssPtr := 0
for _, ss := range parent.Substep {
if ss.GetStep() != &as.Step {
parent.Substep[ssPtr] = ss
ssPtr++
}
}
parent.Substep = parent.Substep[:ssPtr]
as.parent = nil
}
// Name returns the step's component name.
func (as *Step) Name() string {
return as.Step.Name
}
// Proto returns the annotation Step protobuf associated with this Step.
func (as *Step) Proto() *annopb.Step {
return &as.Step
}
// BaseStream returns the supplied name prepended with this Step's base
// log name.
//
// For example, if the base name is "foo/bar", BaseStream("baz") will return
// "foo/bar/baz".
func (as *Step) BaseStream(name types.StreamName) types.StreamName {
if as.LogNameBase == "" {
return name
}
return as.LogNameBase.Concat(name)
}
// AddStep generates a new substep.
func (as *Step) AddStep(name string, legacy bool) *Step {
return (&Step{}).initializeStep(as.s, as, name, legacy)
}
func (as *Step) regenerateLogPath() {
if as.parent == nil {
panic("log path regeneration cannot be called on root step")
}
// Recipe engine nests steps by prepending their parents' name, e.g.
// if "foo" has a nested child, it will be named "foo.bar". This is redundant
// for our stream names, so strip that off.
//
// We throw the length conditional in just in case the child step happens to
// have the exact same name as the parent. This shouldn't happen naturally,
// but let's be robust.
name := as.Name()
if parentPrefix := (as.parent.Name() + "."); len(parentPrefix) < len(name) {
name = strings.TrimPrefix(name, parentPrefix)
}
logPath, err := types.MakeStreamName("s_", "steps", name)
if err != nil {
panic(fmt.Errorf("failed to generate step name for [%s]: %s", as.Name(), err))
}
index := as.parent.logPathIndex[logPath]
as.parent.logPathIndex[logPath] = (index + 1)
// Append the index to the stream name.
logPath = logPath.Concat(types.StreamName(strconv.Itoa(index)))
if err := logPath.Validate(); err != nil {
panic(fmt.Errorf("generated invalid log stream path %q: %v", logPath, err))
}
as.LogNameBase = as.parent.BaseStream(logPath)
}
// Start marks the Step as started.
func (as *Step) Start(startTime *timestamp.Timestamp) bool {
if as.Started != nil {
return false
}
as.Started = startTime
return true
}
// Close closes this step and any outstanding resources that it owns.
// If it is already closed, does not have side effects and returns false.
func (as *Step) Close(closeTime *timestamp.Timestamp) bool {
return as.closeWithStatus(closeTime, nil)
}
func (as *Step) closeWithStatus(closeTime *timestamp.Timestamp, sp *annopb.Status) bool {
if as.closed {
return false
}
// Close our outstanding substeps, and get their highest status value.
stepStatus := annopb.Status_SUCCESS
if sp == nil {
for _, ss := range as.Substep {
sub := as.s.ResolveStep(ss.GetStep())
if sub == nil {
continue
}
sub.Close(closeTime)
if sub.Status > stepStatus {
stepStatus = sub.Status
}
}
} else {
// If a status is provided, use it.
stepStatus = *sp
}
// Close any outstanding log streams.
for l := range as.logLines {
as.LogEnd(l)
}
if as.Status == annopb.Status_RUNNING {
as.Status = stepStatus
}
as.Ended = closeTime
if as.Started == nil {
as.Started = as.Ended
}
as.closed = true
as.s.unregisterStep(as)
as.s.Callbacks.Updated(as, UpdateStructural)
as.s.Callbacks.StepClosed(as)
return true
}
func (as *Step) closestOpenStep() *Step {
for ps := as.prevStep; ps != nil; ps = ps.prevStep {
if !ps.closed {
return ps
}
}
return &as.s.rootStep
}
// LogLine emits a log line for a specified log label.
func (as *Step) LogLine(label, line string) bool {
updated := false
name, ok := as.logLines[label]
if !ok {
// No entry for this log line. Create a new one and register it.
//
// This will appear as:
// [BASE]/logs/[label]/[ord]
subName, err := types.MakeStreamName("s_", "logs", label, strconv.Itoa(as.logLineCount[label]))
if err != nil {
panic(fmt.Errorf("failed to generate log stream name for [%s]: %s", label, err))
}
name = as.BaseStream(subName)
as.AddLogdogStreamLink("", label, "", name)
as.logLines[label] = name
as.logLineCount[label]++
updated = true
}
as.s.Callbacks.StepLogLine(as, name, label, line)
return updated
}
// LogEnd ends the log for the specified label.
func (as *Step) LogEnd(label string) {
name, ok := as.logLines[label]
if !ok {
return
}
delete(as.logLines, label)
as.s.Callbacks.StepLogEnd(as, name)
}
// AddText adds a line of step component text.
func (as *Step) AddText(text string) bool {
as.Text = append(as.Text, text)
return true
}
// ClearText clears step component text.
func (as *Step) ClearText() bool {
if len(as.Text) == 0 {
return false
}
as.Text = nil
return true
}
// SetSummary sets the Step's summary text.
//
// The summary is implemented as the first line of step component text. If no
// summary is currently defined, one will be inserted; otherwise, the current
// summary will be replaced.
func (as *Step) SetSummary(value string) bool {
if as.hasSummary {
if as.Text[0] == value {
return false
}
as.Text[0] = value
} else {
as.Text = append(as.Text, "")
copy(as.Text[1:], as.Text)
as.Text[0] = value
as.hasSummary = true
}
return true
}
// ClearSummary clears the step's summary text.
func (as *Step) ClearSummary() {
if as.hasSummary {
as.Text = as.Text[:copy(as.Text, as.Text[1:])]
as.hasSummary = false
}
}
// SetNestLevel sets the nest level of this Step, and identifies its nesting
// parent.
//
// If no parent could be found at level "l-1", the root step will become the
// parent.
func (as *Step) SetNestLevel(l int) bool {
if as.level == l {
return false
}
as.level = l
// Attach this step to the correct parent step based on nest level. Ascend
// up the previously-declared steps.
var nestParent *Step
for prev := as.prevStep; prev != nil; prev = prev.prevStep {
if prev.level < l {
nestParent = prev
break
}
}
if nestParent == nil || nestParent == as.parent {
return true
}
nestParent.appendSubstep(as)
return true
}
// AddLogdogStreamLink adds a LogDog stream link to this Step's links list.
func (as *Step) AddLogdogStreamLink(server, label string, prefix, name types.StreamName) {
link := as.getOrCreateLinkForLabel(label)
link.Value = &annopb.AnnotationLink_LogdogStream{&annopb.LogdogStream{
Name: string(name),
Server: server,
Prefix: string(prefix),
}}
}
// AddURLLink adds a URL link to this Step's links list.
func (as *Step) AddURLLink(label, alias, url string) {
link := as.getOrCreateLinkForLabel(label)
link.AliasLabel = alias
link.Value = &annopb.AnnotationLink_Url{url}
}
func (as *Step) getOrCreateLinkForLabel(label string) *annopb.AnnotationLink {
if cur := as.linkMap[label]; cur != nil {
return cur
}
// New label, so create a new link.
link := &annopb.AnnotationLink{
Label: label,
}
if as.linkMap == nil {
as.linkMap = make(map[string]*annopb.AnnotationLink)
}
as.OtherLinks = append(as.OtherLinks, link)
as.linkMap[label] = link
return link
}
// SetStatus sets this step's component status.
//
// If the status doesn't change, the supplied failure details will be ignored.
func (as *Step) SetStatus(s annopb.Status, fd *annopb.FailureDetails) bool {
if as.closed || as.Status == s {
return false
}
as.Status = s
as.FailureDetails = fd
return true
}
// SetProperty sets a key/value property for this Step.
func (as *Step) SetProperty(name, value string) bool {
for _, p := range as.Property {
if p.Name == name {
if p.Value == value {
return false
}
p.Value = value
return true
}
}
as.Property = append(as.Property, &annopb.Step_Property{
Name: name,
Value: value,
})
return true
}
// SetSTDOUTStream sets the LogDog STDOUT stream value, returning true if the
// Step was updated.
func (as *Step) SetSTDOUTStream(st *annopb.LogdogStream) (updated bool) {
as.StdoutStream, updated = as.maybeSetLogDogStream(as.StdoutStream, st)
return
}
// SetSTDERRStream sets the LogDog STDERR stream value, returning true if the
// Step was updated.
func (as *Step) SetSTDERRStream(st *annopb.LogdogStream) (updated bool) {
as.StderrStream, updated = as.maybeSetLogDogStream(as.StderrStream, st)
return
}
func (as *Step) maybeSetLogDogStream(target *annopb.LogdogStream, st *annopb.LogdogStream) (*annopb.LogdogStream, bool) {
if (target == nil && st == nil) || (target != nil && st != nil && proto.Equal(target, st)) {
return target, false
}
return st, true
}