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  }