github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/buildplan/raw/walk.go (about)

     1  package raw
     2  
     3  import (
     4  	"github.com/ActiveState/cli/internal/errs"
     5  	"github.com/ActiveState/cli/internal/logging"
     6  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
     7  	"github.com/go-openapi/strfmt"
     8  )
     9  
    10  type walkFunc func(node interface{}, parent *Artifact) error
    11  
    12  type WalkNodeContext struct {
    13  	Node           interface{}
    14  	ParentArtifact *Artifact
    15  	tag            StepInputTag
    16  	lookup         map[strfmt.UUID]interface{}
    17  }
    18  
    19  // WalkViaSteps walks the graph and reports on nodes it encounters
    20  // Note that the same node can be encountered multiple times if it is referenced in the graph multiple times.
    21  // In this case the context around the node may be different, even if the node itself isn't.
    22  func (b *Build) WalkViaSteps(nodeIDs []strfmt.UUID, inputTag StepInputTag, walk walkFunc) error {
    23  	lookup := b.LookupMap()
    24  
    25  	for _, nodeID := range nodeIDs {
    26  		node, ok := lookup[nodeID]
    27  		if !ok {
    28  			return errs.New("node ID '%s' does not exist in lookup table", nodeID)
    29  		}
    30  		if err := b.walkNodeViaSteps(node, nil, inputTag, walk); err != nil {
    31  			return errs.Wrap(err, "could not recurse over node IDs")
    32  		}
    33  	}
    34  
    35  	return nil
    36  }
    37  
    38  func (b *Build) walkNodeViaSteps(node interface{}, parent *Artifact, tag StepInputTag, walk walkFunc) error {
    39  	lookup := b.LookupMap()
    40  
    41  	if err := walk(node, parent); err != nil {
    42  		return errs.Wrap(err, "error walking over node")
    43  	}
    44  
    45  	_, isSource := node.(*Source)
    46  	if isSource {
    47  		return nil // Sources are at the end of the recursion.
    48  	}
    49  
    50  	ar, isArtifact := node.(*Artifact)
    51  	if !isArtifact {
    52  		return errs.New("node ID '%v' is not an artifact", node)
    53  	}
    54  
    55  	// Walk the build and source closure
    56  	generatedByNode, hasGeneratedByNode := lookup[ar.GeneratedBy]
    57  	if !hasGeneratedByNode {
    58  		return errs.New("generated by node ID '%s' does not exist in lookup table", ar.GeneratedBy)
    59  	}
    60  
    61  	// Sources can also be referenced by the generatedBy property, though this is considered an antipattern and
    62  	// at the time of writing is only used as a workaround for builders. In theory we should never hit this in the state
    63  	// tool, but it's technically possible to happen if someone requested a builder as part of their order.
    64  	_, isSource = generatedByNode.(*Source)
    65  	if isSource {
    66  		if err := b.walkNodeViaSteps(generatedByNode, ar, tag, walk); err != nil {
    67  			return errs.Wrap(err, "error walking source from generatedBy")
    68  		}
    69  		return nil // Sources are at the end of the recursion.
    70  	}
    71  
    72  	nodeIDs, err := b.inputNodeIDsFromStep(ar, tag)
    73  	if err != nil {
    74  		return errs.Wrap(err, "error walking over step inputs")
    75  	}
    76  
    77  	for _, id := range nodeIDs {
    78  		// Grab subnode that we want to iterate over next
    79  		subNode, ok := lookup[id]
    80  		if !ok {
    81  			return errs.New("node ID '%s' does not exist in lookup table", id)
    82  		}
    83  		if err := b.walkNodeViaSteps(subNode, ar, tag, walk); err != nil {
    84  			return errs.Wrap(err, "error iterating over %s", id)
    85  		}
    86  	}
    87  
    88  	return nil
    89  }
    90  
    91  // inputNodeIDsFromStep will look up all input node IDs for the given artifacts associated step (calculated from its generatedBy property)
    92  func (b *Build) inputNodeIDsFromStep(ar *Artifact, tag StepInputTag) ([]strfmt.UUID, error) {
    93  	lookup := b.LookupMap()
    94  
    95  	// Walk the build and source closure
    96  	generatedByNode, hasGeneratedByNode := lookup[ar.GeneratedBy]
    97  	if !hasGeneratedByNode {
    98  		return nil, errs.New("generated by node ID '%s' does not exist in lookup table", ar.GeneratedBy)
    99  	}
   100  
   101  	// If this refers to a source step then we have nothing to do
   102  	if _, isSource := generatedByNode.(*Source); isSource {
   103  		return []strfmt.UUID{}, nil
   104  	}
   105  
   106  	step, isStep := generatedByNode.(*Step)
   107  	if !isStep {
   108  		return nil, errs.New("node ID '%s' has unexpected type '%T'", ar.GeneratedBy, generatedByNode)
   109  	}
   110  
   111  	for _, input := range step.Inputs {
   112  		if input.Tag != string(tag) {
   113  			continue
   114  		}
   115  		return input.NodeIDs, nil
   116  	}
   117  
   118  	return []strfmt.UUID{}, nil
   119  }
   120  
   121  func (b *Build) WalkViaRuntimeDeps(nodeIDs []strfmt.UUID, walk walkFunc) error {
   122  	lookup := b.LookupMap()
   123  
   124  	for _, id := range nodeIDs {
   125  		node, ok := lookup[id]
   126  		if !ok {
   127  			return errs.New("node ID '%s' does not exist in lookup table", id)
   128  		}
   129  
   130  		if err := b.walkNodeViaRuntimeDeps(node, nil, walk); err != nil {
   131  			return errs.Wrap(err, "error walking over runtime dep %s", id)
   132  		}
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  func (b *Build) walkNodeViaRuntimeDeps(node interface{}, parent *Artifact, walk walkFunc) error {
   139  	lookup := b.LookupMap()
   140  
   141  	ar, ok := node.(*Artifact)
   142  	if !ok {
   143  		// Technically this should never happen, but because we allow evaluating any part of a buildscript we can
   144  		// encounter scenarios where we have top level sources. In this case we can simply skip them, because the
   145  		// remaining top level nodes still cover our use-cases.
   146  		logging.Debug("node '%#v' is not an artifact, skipping", node)
   147  		return nil
   148  	}
   149  
   150  	// Only state tool artifacts are considered to be a runtime dependency
   151  	if IsStateToolMimeType(ar.MimeType) {
   152  		if err := walk(ar, parent); err != nil {
   153  			return errs.Wrap(err, "error walking over runtime dep %+v", node)
   154  		}
   155  	}
   156  
   157  	// Certain artifacts such as docker images and installers do not set the runtimeDependencies, instead for those
   158  	// we need to look at the source input step.
   159  	if len(ar.RuntimeDependencies) == 0 && !IsStateToolMimeType(ar.MimeType) {
   160  		nodeIDs, err := b.inputNodeIDsFromStep(ar, TagSource)
   161  		if err != nil {
   162  			return errs.Wrap(err, "error walking over step inputs")
   163  		}
   164  		for _, id := range nodeIDs {
   165  			// Grab subnode that we want to iterate over next
   166  			subNode, ok := lookup[id]
   167  			if !ok {
   168  				return errs.New("step node ID '%s' does not exist in lookup table", id)
   169  			}
   170  			if err := b.walkNodeViaRuntimeDeps(subNode, ar, walk); err != nil {
   171  				return errs.Wrap(err, "error walking over runtime dep %s", id)
   172  			}
   173  		}
   174  	} else { // this else serves no purpose other than to make this code easier to grok; ie. this OR that is happening.
   175  		for _, id := range ar.RuntimeDependencies {
   176  			subNode, ok := lookup[id]
   177  			if !ok {
   178  				return errs.New("node ID '%s' does not exist in lookup table", id)
   179  			}
   180  			if err := b.walkNodeViaRuntimeDeps(subNode, ar, walk); err != nil {
   181  				return errs.Wrap(err, "error walking over runtime dep %s", id)
   182  			}
   183  		}
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func (b *Build) LookupMap() map[strfmt.UUID]interface{} {
   190  	if b.lookup != nil {
   191  		return b.lookup
   192  	}
   193  
   194  	b.lookup = make(map[strfmt.UUID]interface{})
   195  
   196  	for _, artifact := range b.Artifacts {
   197  		b.lookup[artifact.NodeID] = artifact
   198  	}
   199  	for _, step := range b.Steps {
   200  		b.lookup[step.StepID] = step
   201  	}
   202  	for _, source := range b.Sources {
   203  		b.lookup[source.NodeID] = source
   204  	}
   205  
   206  	return b.lookup
   207  }
   208  
   209  func IsStateToolMimeType(mimetype string) bool {
   210  	return mimetype == types.XArtifactMimeType ||
   211  		mimetype == types.XActiveStateArtifactMimeType ||
   212  		mimetype == types.XCamelInstallerMimeType
   213  }