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 }