github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/buildplan/hydrate.go (about) 1 package buildplan 2 3 import ( 4 "strings" 5 6 "github.com/ActiveState/cli/internal/condition" 7 "github.com/ActiveState/cli/internal/errs" 8 "github.com/ActiveState/cli/internal/logging" 9 "github.com/ActiveState/cli/internal/rtutils/ptr" 10 "github.com/ActiveState/cli/internal/sliceutils" 11 "github.com/ActiveState/cli/pkg/buildplan/raw" 12 "github.com/go-openapi/strfmt" 13 ) 14 15 // hydrate will add additional information to the unmarshalled structures, based on the raw data that was unmarshalled. 16 // For example, rather than having to walk the buildplan to find associations between artifacts and ingredients, this 17 // will add this context straight on the relevant artifacts. 18 func (b *BuildPlan) hydrate() error { 19 logging.Debug("Hydrating build plan") 20 21 artifactLookup := make(map[strfmt.UUID]*Artifact) 22 ingredientLookup := make(map[strfmt.UUID]*Ingredient) 23 24 for _, t := range b.raw.Terminals { 25 platformID := ptr.To(strfmt.UUID("")) 26 27 if strings.HasPrefix(t.Tag, raw.PlatformTerminalPrefix) { 28 if err := platformID.UnmarshalText([]byte(strings.TrimPrefix(t.Tag, raw.PlatformTerminalPrefix))); err != nil { 29 return errs.Wrap(err, "error unmarshalling platform uuid") 30 } 31 b.platforms = append(b.platforms, *platformID) 32 } 33 34 if err := b.hydrateWithBuildClosure(t.NodeIDs, platformID, artifactLookup); err != nil { 35 return errs.Wrap(err, "hydrating with build closure failed") 36 } 37 if err := b.hydrateWithRuntimeClosure(t.NodeIDs, platformID, artifactLookup); err != nil { 38 return errs.Wrap(err, "hydrating with runtime closure failed") 39 } 40 41 // We have all the artifacts we're interested in now, but we still want to relate them to a source; ie. an ingredient. 42 // This will also hydrate our requirements, because they are based on the source ID. 43 for _, artifact := range b.artifacts { 44 if err := b.hydrateWithIngredients(artifact, platformID, ingredientLookup); err != nil { 45 return errs.Wrap(err, "hydrating with ingredients failed") 46 } 47 } 48 } 49 50 // Hydrate requirements 51 // Build map of requirement IDs so we can quickly look up the associated ingredient 52 sourceLookup := sliceutils.ToLookupMapByKey(b.raw.Sources, func(s *raw.Source) strfmt.UUID { return s.NodeID }) 53 for _, req := range b.raw.ResolvedRequirements { 54 source, ok := sourceLookup[req.Source] 55 if !ok { 56 return errs.New("missing source for source ID: %s", req.Source) 57 } 58 ingredient, ok := ingredientLookup[source.IngredientID] 59 if !ok { 60 return errs.New("missing ingredient for source ID: %s", req.Source) 61 } 62 b.requirements = append(b.requirements, &Requirement{ 63 Requirement: req.Requirement, 64 Ingredient: ingredient, 65 }) 66 } 67 68 if err := b.sanityCheck(); err != nil { 69 return errs.Wrap(err, "sanity check failed") 70 } 71 72 return nil 73 } 74 75 func (b *BuildPlan) hydrateWithBuildClosure(nodeIDs []strfmt.UUID, platformID *strfmt.UUID, artifactLookup map[strfmt.UUID]*Artifact) error { 76 err := b.raw.WalkViaSteps(nodeIDs, raw.TagDependency, func(node interface{}, parent *raw.Artifact) error { 77 switch v := node.(type) { 78 case *raw.Artifact: 79 // logging.Debug("Walking build closure artifact '%s (%s)'", v.DisplayName, v.NodeID) 80 artifact, ok := artifactLookup[v.NodeID] 81 if !ok { 82 artifact = createArtifact(v) 83 b.artifacts = append(b.artifacts, artifact) 84 artifactLookup[v.NodeID] = artifact 85 } 86 87 artifact.platforms = sliceutils.Unique(append(artifact.platforms, *platformID)) 88 artifact.isBuildtimeDependency = true 89 90 if parent != nil { 91 parentArtifact, ok := artifactLookup[parent.NodeID] 92 if !ok { 93 return errs.New("parent artifact does not exist in lookup table: %s", parent.NodeID) 94 } 95 parentArtifact.children = append(parentArtifact.children, ArtifactRelation{artifact, BuildtimeRelation}) 96 } 97 98 return nil 99 case *raw.Source: 100 return nil // We can encounter source nodes in the build steps because GeneratedBy can refer to a source rather than a step 101 default: 102 return errs.New("unexpected node type '%T': %#v", v, v) 103 } 104 return nil 105 }) 106 if err != nil { 107 return errs.Wrap(err, "error hydrating from build closure") 108 } 109 110 return nil 111 } 112 113 func (b *BuildPlan) hydrateWithRuntimeClosure(nodeIDs []strfmt.UUID, platformID *strfmt.UUID, artifactLookup map[strfmt.UUID]*Artifact) error { 114 err := b.raw.WalkViaRuntimeDeps(nodeIDs, func(node interface{}, parent *raw.Artifact) error { 115 switch v := node.(type) { 116 case *raw.Artifact: 117 // logging.Debug("Walking runtime closure artifact '%s (%s)'", v.DisplayName, v.NodeID) 118 artifact, ok := artifactLookup[v.NodeID] 119 if !ok { 120 artifact = createArtifact(v) 121 b.artifacts = append(b.artifacts, artifact) 122 artifactLookup[v.NodeID] = artifact 123 if parent != nil { 124 parentArtifact, ok := artifactLookup[parent.NodeID] 125 // for runtime closure it is possible that we don't have the parent artifact, because the parent 126 // might not be a state tool artifact (eg. an installer) and thus it is not part of the runtime closure. 127 if ok { 128 parentArtifact.children = append(parentArtifact.children, ArtifactRelation{artifact, RuntimeRelation}) 129 } 130 } 131 } 132 133 artifact.platforms = sliceutils.Unique(append(artifact.platforms, *platformID)) 134 artifact.isRuntimeDependency = true 135 136 return nil 137 default: 138 return errs.New("unexpected node type '%T': %#v", v, v) 139 } 140 return nil 141 }) 142 if err != nil { 143 return errs.Wrap(err, "error hydrating from runtime closure") 144 } 145 return nil 146 } 147 148 func (b *BuildPlan) hydrateWithIngredients(artifact *Artifact, platformID *strfmt.UUID, ingredientLookup map[strfmt.UUID]*Ingredient) error { 149 err := b.raw.WalkViaSteps([]strfmt.UUID{artifact.ArtifactID}, raw.TagSource, 150 func(node interface{}, parent *raw.Artifact) error { 151 switch v := node.(type) { 152 case *raw.Artifact: 153 return nil // We've already got our artifacts 154 case *raw.Source: 155 // logging.Debug("Walking source '%s (%s)'", v.Name, v.NodeID) 156 157 // Ingredients aren't explicitly represented in buildplans. Technically all sources are ingredients 158 // but this may not always be true in the future. For our purposes we will initialize our own ingredients 159 // based on the source information, but we do not want to make the assumption in our logic that all 160 // sources are ingredients. 161 ingredient, ok := ingredientLookup[v.IngredientID] 162 if !ok { 163 ingredient = &Ingredient{ 164 IngredientSource: &v.IngredientSource, 165 platforms: []strfmt.UUID{}, 166 Artifacts: []*Artifact{}, 167 } 168 b.ingredients = append(b.ingredients, ingredient) 169 ingredientLookup[v.IngredientID] = ingredient 170 } 171 172 // With multiple terminals it's possible we encounter the same combination multiple times. 173 // And an artifact usually only has one ingredient, so this is the cheapest lookup. 174 if !sliceutils.Contains(artifact.Ingredients, ingredient) { 175 artifact.Ingredients = append(artifact.Ingredients, ingredient) 176 ingredient.Artifacts = append(ingredient.Artifacts, artifact) 177 } 178 if platformID != nil { 179 ingredient.platforms = append(ingredient.platforms, *platformID) 180 } 181 182 if artifact.isBuildtimeDependency { 183 ingredient.IsBuildtimeDependency = true 184 } 185 if artifact.isRuntimeDependency { 186 ingredient.IsRuntimeDependency = true 187 } 188 189 return nil 190 default: 191 return errs.New("unexpected node type '%T': %#v", v, v) 192 } 193 194 return nil 195 }) 196 if err != nil { 197 return errs.Wrap(err, "error hydrating ingredients") 198 } 199 200 return nil 201 } 202 203 // sanityCheck will for convenience sake validate that we have no duplicates here while on a dev machine. 204 // If there are duplicates we're likely to see failures down the chain if live, though that's by no means guaranteed. 205 // Surfacing it here will make it easier to reason about the failure. 206 func (b *BuildPlan) sanityCheck() error { 207 // Ensure all artifacts have an associated ingredient 208 // If this fails either the API is bugged or the hydrate logic is bugged 209 for _, a := range b.Artifacts() { 210 if len(a.Ingredients) == 0 { 211 return errs.New("artifact '%s (%s)' does not have an ingredient", a.ArtifactID, a.DisplayName) 212 } 213 } 214 215 // The remainder of sanity checks aren't checking for error conditions so much as they are checking for smoking guns 216 // If these fail then it's likely the API has changed in a backward incompatible way, or we broke something. 217 // In any case it does not necessarily mean runtime sourcing is broken. 218 if !condition.BuiltOnDevMachine() && !condition.InActiveStateCI() { 219 return nil 220 } 221 222 seen := make(map[strfmt.UUID]struct{}) 223 for _, a := range b.artifacts { 224 if _, ok := seen[a.ArtifactID]; ok { 225 return errs.New("Artifact %s (%s) occurs multiple times", a.DisplayName, a.ArtifactID) 226 } 227 seen[a.ArtifactID] = struct{}{} 228 } 229 for _, i := range b.ingredients { 230 if _, ok := seen[i.IngredientID]; ok { 231 return errs.New("Ingredient %s (%s) occurs multiple times", i.Name, i.IngredientID) 232 } 233 seen[i.IngredientID] = struct{}{} 234 } 235 236 return nil 237 } 238 239 func createArtifact(rawArtifact *raw.Artifact) *Artifact { 240 return &Artifact{ 241 raw: rawArtifact, 242 ArtifactID: rawArtifact.NodeID, 243 DisplayName: rawArtifact.DisplayName, 244 MimeType: rawArtifact.MimeType, 245 URL: rawArtifact.URL, 246 LogURL: rawArtifact.LogURL, 247 Errors: rawArtifact.Errors, 248 Checksum: rawArtifact.Checksum, 249 Status: rawArtifact.Status, 250 children: []ArtifactRelation{}, 251 } 252 }