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

     1  package buildplan
     2  
     3  import (
     4  	"encoding/json"
     5  
     6  	"github.com/ActiveState/cli/internal/errs"
     7  	"github.com/ActiveState/cli/internal/logging"
     8  	"github.com/ActiveState/cli/internal/sliceutils"
     9  	"github.com/ActiveState/cli/pkg/buildplan/raw"
    10  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    11  	"github.com/go-openapi/strfmt"
    12  )
    13  
    14  type BuildPlan struct {
    15  	platforms    []strfmt.UUID
    16  	artifacts    Artifacts
    17  	requirements Requirements
    18  	ingredients  Ingredients
    19  	raw          *raw.Build
    20  }
    21  
    22  func Unmarshal(data []byte) (*BuildPlan, error) {
    23  	logging.Debug("Unmarshalling buildplan")
    24  
    25  	b := &BuildPlan{}
    26  
    27  	var rawBuild raw.Build
    28  	if err := json.Unmarshal(data, &rawBuild); err != nil {
    29  		return nil, errs.Wrap(err, "error unmarshalling build plan")
    30  	}
    31  
    32  	b.raw = &rawBuild
    33  
    34  	b.cleanup()
    35  
    36  	if err := b.hydrate(); err != nil {
    37  		return nil, errs.Wrap(err, "error hydrating build plan")
    38  	}
    39  
    40  	if len(b.artifacts) == 0 || len(b.ingredients) == 0 || len(b.platforms) == 0 {
    41  		return nil, errs.New("Buildplan unmarshalling failed as it got zero artifacts (%d), ingredients (%d) and or platforms (%d).",
    42  			len(b.artifacts), len(b.ingredients), len(b.platforms))
    43  	}
    44  
    45  	return b, nil
    46  }
    47  
    48  func (b *BuildPlan) Marshal() ([]byte, error) {
    49  	return json.Marshal(b.raw)
    50  }
    51  
    52  // cleanup empty targets
    53  // The type aliasing in the query populates the response with emtpy targets that we should remove
    54  func (b *BuildPlan) cleanup() {
    55  	logging.Debug("Cleaning up build plan")
    56  
    57  	b.raw.Steps = sliceutils.Filter(b.raw.Steps, func(s *raw.Step) bool {
    58  		return s.StepID != ""
    59  	})
    60  
    61  	b.raw.Sources = sliceutils.Filter(b.raw.Sources, func(s *raw.Source) bool {
    62  		return s.NodeID != ""
    63  	})
    64  
    65  	b.raw.Artifacts = sliceutils.Filter(b.raw.Artifacts, func(a *raw.Artifact) bool {
    66  		return a.NodeID != ""
    67  	})
    68  }
    69  
    70  func (b *BuildPlan) Platforms() []strfmt.UUID {
    71  	return b.platforms
    72  }
    73  
    74  func (b *BuildPlan) Artifacts(filters ...FilterArtifact) Artifacts {
    75  	return b.artifacts.Filter(filters...)
    76  }
    77  
    78  type filterIngredient func(i *Ingredient) bool
    79  
    80  func (b *BuildPlan) Ingredients(filters ...filterIngredient) Ingredients {
    81  	return b.ingredients.Filter(filters...)
    82  }
    83  
    84  func (b *BuildPlan) DiffArtifacts(oldBp *BuildPlan, requestedOnly bool) ArtifactChangeset {
    85  	// Basic outline of what needs to happen here:
    86  	//   - add ArtifactID to the `Added` field if artifactID only appears in the the `new` buildplan
    87  	//   - add ArtifactID to the `Removed` field if artifactID only appears in the the `old` buildplan
    88  	//   - add ArtifactID to the `Updated` field if `ResolvedRequirements.feature` appears in both buildplans, but the resolved version has changed.
    89  
    90  	var new ArtifactNameMap
    91  	var old ArtifactNameMap
    92  
    93  	if requestedOnly {
    94  		new = b.RequestedArtifacts().ToNameMap()
    95  		old = oldBp.RequestedArtifacts().ToNameMap()
    96  	} else {
    97  		new = b.Artifacts().ToNameMap()
    98  		old = oldBp.Artifacts().ToNameMap()
    99  	}
   100  
   101  	var updated []ArtifactUpdate
   102  	var added []*Artifact
   103  	for name, artf := range new {
   104  		if artfOld, notNew := old[name]; notNew {
   105  			// The artifact name exists in both the old and new recipe, maybe it was updated though
   106  			if artfOld.ArtifactID == artf.ArtifactID {
   107  				continue
   108  			}
   109  			updated = append(updated, ArtifactUpdate{
   110  				From: artfOld,
   111  				To:   artf,
   112  			})
   113  
   114  		} else {
   115  			// If it's not an update it is a new artifact
   116  			added = append(added, artf)
   117  		}
   118  	}
   119  
   120  	var removed []*Artifact
   121  	for name, artf := range old {
   122  		if _, noDiff := new[name]; noDiff {
   123  			continue
   124  		}
   125  		removed = append(removed, artf)
   126  	}
   127  
   128  	return ArtifactChangeset{
   129  		Added:   added,
   130  		Removed: removed,
   131  		Updated: updated,
   132  	}
   133  }
   134  
   135  func (b *BuildPlan) Engine() types.BuildEngine {
   136  	buildEngine := types.Alternative
   137  	for _, s := range b.raw.Sources {
   138  		if s.Namespace == "builder" && s.Name == "camel" {
   139  			buildEngine = types.Camel
   140  			break
   141  		}
   142  	}
   143  	return buildEngine
   144  }
   145  
   146  // RecipeID extracts the recipe ID from the BuildLogIDs.
   147  // We do this because if the build is in progress we will need to reciepe ID to
   148  // initialize the build log streamer.
   149  // This information will only be populated if the build is an alternate build.
   150  // This is specified in the build planner queries.
   151  func (b *BuildPlan) RecipeID() (strfmt.UUID, error) {
   152  	var result strfmt.UUID
   153  	for _, id := range b.raw.BuildLogIDs {
   154  		if result != "" && result.String() != id.ID {
   155  			return result, errs.New("Build plan contains multiple recipe IDs")
   156  		}
   157  		result = strfmt.UUID(id.ID)
   158  	}
   159  	return result, nil
   160  }
   161  
   162  func (b *BuildPlan) IsBuildReady() bool {
   163  	return b.raw.Status == raw.Completed
   164  }
   165  
   166  func (b *BuildPlan) IsBuildInProgress() bool {
   167  	return b.raw.Status == raw.Started || b.raw.Status == raw.Planned
   168  }
   169  
   170  // RequestedIngredients returns the resolved requirements of the buildplan as ingredients
   171  func (b *BuildPlan) RequestedIngredients() Ingredients {
   172  	ingredients := Ingredients{}
   173  	seen := make(map[strfmt.UUID]struct{})
   174  	for _, r := range b.requirements {
   175  		if _, ok := seen[r.Ingredient.IngredientID]; ok {
   176  			continue
   177  		}
   178  		seen[r.Ingredient.IngredientID] = struct{}{}
   179  		ingredients = append(ingredients, r.Ingredient)
   180  	}
   181  	return ingredients
   182  }
   183  
   184  // RequestedArtifacts returns the resolved requirements of the buildplan as artifacts
   185  func (b *BuildPlan) RequestedArtifacts() Artifacts {
   186  	result := []*Artifact{}
   187  	for _, i := range b.RequestedIngredients() {
   188  		for _, a := range i.Artifacts {
   189  			result = append(result, a)
   190  		}
   191  	}
   192  	return result
   193  }
   194  
   195  // Requirements returns what the project has defined as the top level requirements (ie. the "order").
   196  // This is usually the same as the "ingredients" but it can be different if the project has multiple requirements that
   197  // are satisfied by the same ingredient. eg. rake is satisfied by ruby.
   198  func (b *BuildPlan) Requirements() Requirements {
   199  	return b.requirements
   200  }