github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/artifacts/artifacts.go (about)

     1  package artifacts
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"path/filepath"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/ActiveState/cli/internal/analytics"
    11  	"github.com/ActiveState/cli/internal/config"
    12  	"github.com/ActiveState/cli/internal/constants"
    13  	"github.com/ActiveState/cli/internal/errs"
    14  	"github.com/ActiveState/cli/internal/locale"
    15  	"github.com/ActiveState/cli/internal/output"
    16  	"github.com/ActiveState/cli/internal/primer"
    17  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    18  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    19  	"github.com/ActiveState/cli/pkg/buildplan"
    20  	"github.com/ActiveState/cli/pkg/localcommit"
    21  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/request"
    22  	bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response"
    23  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    24  	"github.com/ActiveState/cli/pkg/platform/authentication"
    25  	"github.com/ActiveState/cli/pkg/platform/model"
    26  	bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    27  	"github.com/ActiveState/cli/pkg/project"
    28  	"github.com/go-openapi/strfmt"
    29  	"github.com/google/uuid"
    30  )
    31  
    32  type primeable interface {
    33  	primer.Outputer
    34  	primer.Auther
    35  	primer.Projecter
    36  	primer.SvcModeler
    37  	primer.Configurer
    38  	primer.Analyticer
    39  }
    40  
    41  type Params struct {
    42  	All       bool
    43  	Namespace *project.Namespaced
    44  	CommitID  string
    45  	Target    string
    46  	Full      bool
    47  }
    48  
    49  type Configurable interface {
    50  	GetString(key string) string
    51  	GetBool(key string) bool
    52  }
    53  
    54  type Artifacts struct {
    55  	out       output.Outputer
    56  	project   *project.Project
    57  	analytics analytics.Dispatcher
    58  	svcModel  *model.SvcModel
    59  	auth      *authentication.Auth
    60  	config    *config.Instance
    61  }
    62  
    63  type StructuredOutput struct {
    64  	BuildComplete      bool                  `json:"build_completed"`
    65  	HasFailedArtifacts bool                  `json:"has_failed_artifacts"`
    66  	Platforms          []*structuredPlatform `json:"platforms"`
    67  }
    68  
    69  func (o *StructuredOutput) MarshalStructured(output.Format) interface{} {
    70  	return o
    71  }
    72  
    73  type structuredPlatform struct {
    74  	ID        string                `json:"id"`
    75  	Name      string                `json:"name"`
    76  	Artifacts []*structuredArtifact `json:"artifacts"`
    77  	Packages  []*structuredArtifact `json:"packages"`
    78  }
    79  
    80  type structuredArtifact struct {
    81  	ID   string `json:"id"`
    82  	Name string `json:"name"`
    83  	URL  string `json:"url"`
    84  }
    85  
    86  func New(p primeable) *Artifacts {
    87  	return &Artifacts{
    88  		out:       p.Output(),
    89  		project:   p.Project(),
    90  		auth:      p.Auth(),
    91  		svcModel:  p.SvcModel(),
    92  		config:    p.Config(),
    93  		analytics: p.Analytics(),
    94  	}
    95  }
    96  
    97  type errInvalidCommitId struct {
    98  	error
    99  	id string
   100  }
   101  
   102  func rationalizeArtifactsError(rerr *error, auth *authentication.Auth) {
   103  	if rerr == nil {
   104  		return
   105  	}
   106  
   107  	var planningError *bpResp.BuildPlannerError
   108  	switch {
   109  	case errors.As(*rerr, &planningError):
   110  		// Forward API error to user.
   111  		*rerr = errs.WrapUserFacing(*rerr, planningError.Error())
   112  
   113  	default:
   114  		rationalizeCommonError(rerr, auth)
   115  	}
   116  }
   117  
   118  func (b *Artifacts) Run(params *Params) (rerr error) {
   119  	defer rationalizeArtifactsError(&rerr, b.auth)
   120  
   121  	if b.project != nil && !params.Namespace.IsValid() {
   122  		b.out.Notice(locale.Tr("operating_message", b.project.NamespaceString(), b.project.Dir()))
   123  	}
   124  
   125  	bp, err := getBuildPlan(
   126  		b.project, params.Namespace, params.CommitID, params.Target, b.auth, b.out)
   127  	if err != nil {
   128  		return errs.Wrap(err, "Could not get buildplan")
   129  	}
   130  
   131  	platformMap, err := model.FetchPlatformsMap()
   132  	if err != nil {
   133  		return errs.Wrap(err, "Could not get platforms")
   134  	}
   135  
   136  	hasFailedArtifacts := len(bp.Artifacts()) != len(bp.Artifacts(buildplan.FilterSuccessfulArtifacts()))
   137  
   138  	out := &StructuredOutput{HasFailedArtifacts: hasFailedArtifacts, BuildComplete: bp.IsBuildReady()}
   139  	for _, platformUUID := range bp.Platforms() {
   140  		platform, ok := platformMap[platformUUID]
   141  		if !ok {
   142  			return errs.New("Platform does not exist on inventory API: %s", platformUUID)
   143  		}
   144  		p := &structuredPlatform{
   145  			ID:        string(platformUUID),
   146  			Name:      *platform.DisplayName,
   147  			Artifacts: []*structuredArtifact{},
   148  		}
   149  		for _, artifact := range bp.Artifacts(buildplan.FilterPlatformArtifacts(platformUUID)) {
   150  			if artifact.MimeType == types.XActiveStateBuilderMimeType {
   151  				continue
   152  			}
   153  			name := artifact.Name()
   154  
   155  			// Detect and drop artifact names which start with a uuid, as this isn't user friendly
   156  			nameBits := strings.Split(name, " ")
   157  			if len(nameBits) > 1 {
   158  				if _, err := uuid.Parse(nameBits[0]); err == nil {
   159  					name = fmt.Sprintf("%s (%s)", strings.Join(nameBits[1:], " "), filepath.Base(artifact.URL))
   160  				}
   161  			}
   162  
   163  			version := artifact.Version()
   164  			if version != "" {
   165  				name = fmt.Sprintf("%s@%s", name, version)
   166  			}
   167  
   168  			build := &structuredArtifact{
   169  				ID:   string(artifact.ArtifactID),
   170  				Name: name,
   171  				URL:  artifact.URL,
   172  			}
   173  			if bpModel.IsStateToolArtifact(artifact.MimeType) {
   174  				if !params.All {
   175  					continue
   176  				}
   177  				p.Packages = append(p.Packages, build)
   178  			} else {
   179  				p.Artifacts = append(p.Artifacts, build)
   180  			}
   181  		}
   182  		sort.Slice(p.Artifacts, func(i, j int) bool {
   183  			return strings.ToLower(p.Artifacts[i].Name) < strings.ToLower(p.Artifacts[j].Name)
   184  		})
   185  		sort.Slice(p.Packages, func(i, j int) bool {
   186  			return strings.ToLower(p.Packages[i].Name) < strings.ToLower(p.Packages[j].Name)
   187  		})
   188  		out.Platforms = append(out.Platforms, p)
   189  	}
   190  
   191  	sort.Slice(out.Platforms, func(i, j int) bool {
   192  		return strings.ToLower(out.Platforms[i].Name) < strings.ToLower(out.Platforms[j].Name)
   193  	})
   194  
   195  	if b.out.Type().IsStructured() {
   196  		b.out.Print(out)
   197  		return nil
   198  	}
   199  
   200  	return b.outputPlain(out, params.Full)
   201  }
   202  
   203  func (b *Artifacts) outputPlain(out *StructuredOutput, fullID bool) error {
   204  	if out.HasFailedArtifacts {
   205  		b.out.Error(locale.T("warn_has_failed_artifacts"))
   206  	}
   207  
   208  	for _, platform := range out.Platforms {
   209  		b.out.Print(fmt.Sprintf("• [NOTICE]%s[/RESET]", platform.Name))
   210  		for _, artifact := range platform.Artifacts {
   211  			if artifact.URL == "" {
   212  				b.out.Print(fmt.Sprintf("  • %s ([WARNING]%s ...[/RESET])", artifact.Name, locale.T("artifact_status_building")))
   213  				continue
   214  			}
   215  			id := strings.ToUpper(artifact.ID)
   216  			if !fullID {
   217  				id = id[0:8]
   218  			}
   219  			b.out.Print(fmt.Sprintf("  • %s (ID: [ACTIONABLE]%s[/RESET])", artifact.Name, id))
   220  		}
   221  
   222  		if len(platform.Packages) > 0 {
   223  			b.out.Print(fmt.Sprintf("  • %s", locale.Tl("artifacts_packages", "[NOTICE]Packages[/RESET]")))
   224  		}
   225  		for _, artifact := range platform.Packages {
   226  			if artifact.URL == "" {
   227  				b.out.Print(fmt.Sprintf("    • %s ([WARNING]%s ...[/RESET])", artifact.Name, locale.T("artifact_status_building")))
   228  				continue
   229  			}
   230  			id := strings.ToUpper(artifact.ID)
   231  			if !fullID {
   232  				id = id[0:8]
   233  			}
   234  			b.out.Print(fmt.Sprintf("    • %s (ID: [ACTIONABLE]%s[/RESET])", artifact.Name, id))
   235  		}
   236  
   237  		if len(platform.Artifacts) == 0 && len(platform.Packages) == 0 {
   238  			b.out.Print(fmt.Sprintf("  • %s", locale.Tl("no_artifacts", "No artifacts")))
   239  		}
   240  	}
   241  
   242  	if !out.BuildComplete {
   243  		b.out.Notice("") // blank line
   244  		b.out.Notice(locale.T("warn_build_not_complete"))
   245  	}
   246  
   247  	b.out.Print("\nTo download artifacts run '[ACTIONABLE]state artifacts dl <ID>[/RESET]'.")
   248  	return nil
   249  }
   250  
   251  // getBuildPlan returns a project's terminal artifact map, depending on the given
   252  // arguments. By default, the map for the current project is returned, but a map for a given
   253  // commitID for the current project can be returned, as can the map for a remote project
   254  // (and optional commitID).
   255  func getBuildPlan(
   256  	pj *project.Project,
   257  	namespace *project.Namespaced,
   258  	commitID string,
   259  	target string,
   260  	auth *authentication.Auth,
   261  	out output.Outputer) (bp *buildplan.BuildPlan, rerr error) {
   262  	if pj == nil && !namespace.IsValid() {
   263  		return nil, rationalize.ErrNoProject
   264  	}
   265  
   266  	commitUUID := strfmt.UUID(commitID)
   267  	if commitUUID != "" && !strfmt.IsUUID(commitUUID.String()) {
   268  		return nil, &errInvalidCommitId{errs.New("Invalid commit ID"), commitUUID.String()}
   269  	}
   270  
   271  	namespaceProvided := namespace.IsValid()
   272  	commitIdProvided := commitUUID != ""
   273  
   274  	// Show a spinner when fetching a terminal artifact map.
   275  	// Sourcing the local runtime for an artifact map has its own spinner.
   276  	pb := output.StartSpinner(out, locale.T("progress_solve"), constants.TerminalAnimationInterval)
   277  	defer func() {
   278  		message := locale.T("progress_success")
   279  		if rerr != nil {
   280  			message = locale.T("progress_fail")
   281  		}
   282  		pb.Stop(message + "\n") // extra empty line
   283  	}()
   284  
   285  	targetPtr := ptr.To(request.TargetAll)
   286  	if target != "" {
   287  		targetPtr = &target
   288  	}
   289  
   290  	var err error
   291  	var commit *bpModel.Commit
   292  	switch {
   293  	// Return the artifact map from this runtime.
   294  	case !namespaceProvided && !commitIdProvided:
   295  		localCommitID, err := localcommit.Get(pj.Path())
   296  		if err != nil {
   297  			return nil, errs.Wrap(err, "Could not get local commit")
   298  		}
   299  
   300  		bp := bpModel.NewBuildPlannerModel(auth)
   301  		commit, err = bp.FetchCommit(localCommitID, pj.Owner(), pj.Name(), targetPtr)
   302  		if err != nil {
   303  			return nil, errs.Wrap(err, "Failed to fetch commit")
   304  		}
   305  
   306  	// Return artifact map from the given commitID for the current project.
   307  	case !namespaceProvided && commitIdProvided:
   308  		bp := bpModel.NewBuildPlannerModel(auth)
   309  		commit, err = bp.FetchCommit(commitUUID, pj.Owner(), pj.Name(), targetPtr)
   310  		if err != nil {
   311  			return nil, errs.Wrap(err, "Failed to fetch commit")
   312  		}
   313  
   314  	// Return the artifact map for the latest commitID of the given project.
   315  	case namespaceProvided && !commitIdProvided:
   316  		pj, err := model.FetchProjectByName(namespace.Owner, namespace.Project, auth)
   317  		if err != nil {
   318  			return nil, locale.WrapExternalError(err, "err_fetch_project", "", namespace.String())
   319  		}
   320  
   321  		branch, err := model.DefaultBranchForProject(pj)
   322  		if err != nil {
   323  			return nil, errs.Wrap(err, "Could not grab branch for project")
   324  		}
   325  
   326  		branchCommitUUID, err := model.BranchCommitID(namespace.Owner, namespace.Project, branch.Label)
   327  		if err != nil {
   328  			return nil, errs.Wrap(err, "Could not get commit ID for project")
   329  		}
   330  		commitUUID = *branchCommitUUID
   331  
   332  		bp := bpModel.NewBuildPlannerModel(auth)
   333  		commit, err = bp.FetchCommit(commitUUID, namespace.Owner, namespace.Project, targetPtr)
   334  		if err != nil {
   335  			return nil, errs.Wrap(err, "Failed to fetch commit")
   336  		}
   337  
   338  	// Return the artifact map for the given commitID of the given project.
   339  	case namespaceProvided && commitIdProvided:
   340  		bp := bpModel.NewBuildPlannerModel(auth)
   341  		commit, err = bp.FetchCommit(commitUUID, namespace.Owner, namespace.Project, targetPtr)
   342  		if err != nil {
   343  			return nil, errs.Wrap(err, "Failed to fetch commit")
   344  		}
   345  
   346  	default:
   347  		return nil, errs.New("Unhandled case")
   348  	}
   349  
   350  	return commit.BuildPlan(), nil
   351  }