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

     1  package show
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/go-openapi/strfmt"
     9  
    10  	"github.com/ActiveState/cli/internal/constraints"
    11  	"github.com/ActiveState/cli/internal/errs"
    12  	"github.com/ActiveState/cli/internal/fileutils"
    13  	"github.com/ActiveState/cli/internal/locale"
    14  	"github.com/ActiveState/cli/internal/logging"
    15  	"github.com/ActiveState/cli/internal/output"
    16  	"github.com/ActiveState/cli/internal/primer"
    17  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    18  	"github.com/ActiveState/cli/internal/secrets"
    19  	"github.com/ActiveState/cli/pkg/localcommit"
    20  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_models"
    21  	secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets"
    22  	"github.com/ActiveState/cli/pkg/platform/authentication"
    23  	"github.com/ActiveState/cli/pkg/platform/model"
    24  	"github.com/ActiveState/cli/pkg/platform/runtime/setup"
    25  	"github.com/ActiveState/cli/pkg/platform/runtime/target"
    26  	"github.com/ActiveState/cli/pkg/project"
    27  	"github.com/ActiveState/cli/pkg/projectfile"
    28  )
    29  
    30  // Params describes the data required for the show run func.
    31  type Params struct {
    32  	Remote string
    33  }
    34  
    35  // Show manages the show run execution context.
    36  type Show struct {
    37  	project     *project.Project
    38  	out         output.Outputer
    39  	conditional *constraints.Conditional
    40  	auth        *authentication.Auth
    41  }
    42  
    43  type primeable interface {
    44  	primer.Projecter
    45  	primer.Outputer
    46  	primer.Conditioner
    47  	primer.Auther
    48  }
    49  
    50  type RuntimeDetails struct {
    51  	Name         string `json:"name" locale:"state_show_details_name,Name"`
    52  	Organization string `json:"organization" locale:"state_show_details_organization,Organization"`
    53  	NameSpace    string `json:"namespace" locale:"state_show_details_namespace,Namespace"`
    54  	Location     string `json:"location" locale:"state_show_details_location,Location"`
    55  	Executables  string `json:"executables" locale:"state_show_details_executables,Executables"`
    56  	Visibility   string `json:"visibility" locale:"state_show_details_visibility,Visibility"`
    57  	LastCommit   string `json:"last_commit" locale:"state_show_details_latest_commit,Latest Commit"`
    58  }
    59  
    60  type showOutput struct {
    61  	output output.Outputer
    62  	data   outputData
    63  }
    64  
    65  type outputData struct {
    66  	ProjectURL string `json:"project_url" locale:"project_url,Project URL"`
    67  	RuntimeDetails
    68  	Platforms []platformRow     `json:"platforms"`
    69  	Languages []languageRow     `json:"languages"`
    70  	Secrets   *secretOutput     `json:"secrets" locale:"secrets,Secrets"`
    71  	Events    []string          `json:"events,omitempty"`
    72  	Scripts   map[string]string `json:"scripts,omitempty"`
    73  }
    74  
    75  func formatScripts(scripts map[string]string) string {
    76  	var res []string
    77  
    78  	for k, v := range scripts {
    79  		res = append(res, fmt.Sprintf("• %s", k))
    80  		if v != "" {
    81  			res = append(res, fmt.Sprintf("  └─  %s", v))
    82  		}
    83  	}
    84  	return strings.Join(res, "\n")
    85  }
    86  
    87  func formatSlice(slice []string) string {
    88  	var res []string
    89  
    90  	for _, v := range slice {
    91  		res = append(res, fmt.Sprintf("• %s", v))
    92  	}
    93  	return strings.Join(res, "\n")
    94  }
    95  
    96  func (o *showOutput) MarshalOutput(format output.Format) interface{} {
    97  	o.output.Print(locale.Tl("show_details_intro", "Here are the details of your runtime environment.\n"))
    98  	o.output.Print(
    99  		struct {
   100  			*RuntimeDetails `opts:"verticalTable"`
   101  		}{&o.data.RuntimeDetails},
   102  	)
   103  	o.output.Print(output.Title(locale.Tl("state_show_events_header", "Events")))
   104  	o.output.Print(formatSlice(o.data.Events))
   105  	o.output.Print(output.Title(locale.Tl("state_show_scripts_header", "Scripts")))
   106  	o.output.Print(formatScripts(o.data.Scripts))
   107  	o.output.Print(output.Title(locale.Tl("state_show_platforms_header", "Platforms")))
   108  	o.output.Print(o.data.Platforms)
   109  	o.output.Print(output.Title(locale.Tl("state_show_languages_header", "Languages")))
   110  	o.output.Print(o.data.Languages)
   111  
   112  	return output.Suppress
   113  }
   114  
   115  func (o *showOutput) MarshalStructured(format output.Format) interface{} {
   116  	return o.data
   117  }
   118  
   119  type secretOutput struct {
   120  	User    []string `locale:"user,User"`
   121  	Project []string `locale:"project,Project"`
   122  }
   123  
   124  // New returns a pointer to an instance of Show.
   125  func New(prime primeable) *Show {
   126  	return &Show{
   127  		prime.Project(),
   128  		prime.Output(),
   129  		prime.Conditional(),
   130  		prime.Auth(),
   131  	}
   132  }
   133  
   134  // Run is the primary show logic.
   135  func (s *Show) Run(params Params) error {
   136  	logging.Debug("Execute show")
   137  
   138  	var (
   139  		owner       string
   140  		projectName string
   141  		projectURL  string
   142  		commitID    strfmt.UUID
   143  		branchName  string
   144  		events      []string
   145  		scripts     map[string]string
   146  		err         error
   147  	)
   148  
   149  	var projectDir string
   150  	var projectTarget string
   151  	if params.Remote != "" {
   152  		namespaced, err := project.ParseNamespace(params.Remote)
   153  		if err != nil {
   154  			return locale.WrapError(err, "err_show_parse_namespace", "Invalid remote argument, must be of the form <org/project>")
   155  		}
   156  
   157  		owner = namespaced.Owner
   158  		projectName = namespaced.Project
   159  
   160  		branch, err := model.DefaultBranchForProjectName(owner, projectName)
   161  		if err != nil {
   162  			return locale.WrapError(err, "err_show_get_default_branch", "Could not get project information from the platform")
   163  		}
   164  		if branch.CommitID == nil {
   165  			return locale.NewError("err_show_commitID", "Remote project details are incorrect. Default branch is missing commitID")
   166  		}
   167  		branchName = branch.Label
   168  		commitID = *branch.CommitID
   169  	} else {
   170  		if s.project == nil {
   171  			return rationalize.ErrNoProject
   172  		}
   173  
   174  		if s.project.IsHeadless() {
   175  			return locale.NewInputError("err_show_not_supported_headless", "This is not supported while in a headless state. Please visit {{.V0}} to create your project.", s.project.URL())
   176  		}
   177  
   178  		owner = s.project.Owner()
   179  		projectName = s.project.Name()
   180  		projectURL = s.project.URL()
   181  		branchName = s.project.BranchName()
   182  
   183  		events, err = eventsData(s.project.Source(), s.conditional)
   184  		if err != nil {
   185  			return locale.WrapError(err, "err_show_events", "Could not parse events")
   186  		}
   187  
   188  		scripts, err = scriptsData(s.project.Source(), s.conditional)
   189  		if err != nil {
   190  			return locale.WrapError(err, "err_show_scripts", "Could not parse scripts")
   191  		}
   192  
   193  		commitID, err = localcommit.Get(s.project.Dir())
   194  		if err != nil {
   195  			return errs.Wrap(err, "Unable to get local commit")
   196  		}
   197  
   198  		projectDir = filepath.Dir(s.project.Path())
   199  		if fileutils.IsSymlink(projectDir) {
   200  			projectDir, err = fileutils.ResolveUniquePath(projectDir)
   201  			if err != nil {
   202  				return locale.WrapError(err, "err_show_projectdir", "Could not resolve project directory symlink")
   203  			}
   204  		}
   205  
   206  		projectTarget = target.NewProjectTarget(s.project, nil, "").Dir()
   207  	}
   208  
   209  	remoteProject, err := model.LegacyFetchProjectByName(owner, projectName)
   210  	if err != nil && errs.Matches(err, &model.ErrProjectNotFound{}) {
   211  		return locale.WrapError(err, "err_show_project_not_found", "Please run '[ACTIONABLE]state push[/RESET]' to synchronize this project with the ActiveState Platform.")
   212  	} else if err != nil {
   213  		return locale.WrapError(err, "err_show_get_project", "Could not get remote project details")
   214  	}
   215  
   216  	if projectURL == "" {
   217  		projectURL = model.ProjectURL(owner, projectName, commitID.String())
   218  	}
   219  
   220  	platforms, err := platformsData(owner, projectName, commitID, s.auth)
   221  	if err != nil {
   222  		return locale.WrapError(err, "err_show_platforms", "Could not retrieve platform information")
   223  	}
   224  
   225  	languages, err := languagesData(commitID, s.auth)
   226  	if err != nil {
   227  		return locale.WrapError(err, "err_show_langauges", "Could not retrieve language information")
   228  	}
   229  
   230  	commit, err := commitsData(owner, projectName, branchName, commitID, s.project, s.auth)
   231  	if err != nil {
   232  		return locale.WrapError(err, "err_show_commit", "Could not get commit information")
   233  	}
   234  
   235  	secrets, err := secretsData(owner, projectName, s.auth)
   236  	if err != nil {
   237  		return locale.WrapError(err, "err_show_secrets", "Could not get secret information")
   238  	}
   239  
   240  	rd := RuntimeDetails{
   241  		NameSpace:    fmt.Sprintf("%s/%s", owner, projectName),
   242  		Name:         projectName,
   243  		Organization: owner,
   244  		Visibility:   visibilityData(owner, projectName, remoteProject),
   245  		LastCommit:   commit,
   246  	}
   247  
   248  	if projectDir != "" {
   249  		rd.Location = projectDir
   250  	}
   251  
   252  	if projectTarget != "" {
   253  		rd.Executables = setup.ExecDir(projectTarget)
   254  	}
   255  
   256  	outputData := outputData{
   257  		ProjectURL:     projectURL,
   258  		RuntimeDetails: rd,
   259  		Languages:      languages,
   260  		Platforms:      platforms,
   261  		Secrets:        secrets,
   262  		Events:         events,
   263  		Scripts:        scripts,
   264  	}
   265  
   266  	s.out.Print(&showOutput{s.out, outputData})
   267  
   268  	return nil
   269  }
   270  
   271  type platformRow struct {
   272  	Name     string `json:"name" locale:"state_show_platform_name,Name"`
   273  	Version  string `json:"version" locale:"state_show_platform_version,Version"`
   274  	BitWidth string `json:"bit_width" locale:"state_show_platform_bitwidth,Bit Width"`
   275  }
   276  
   277  type languageRow struct {
   278  	Name    string `json:"name" locale:"state_show_language_name,Name"`
   279  	Version string `json:"version" locale:"state_show_language_version,Version"`
   280  }
   281  
   282  func eventsData(project *projectfile.Project, conditional *constraints.Conditional) ([]string, error) {
   283  	if len(project.Events) == 0 {
   284  		return nil, nil
   285  	}
   286  
   287  	constrained, err := constraints.FilterUnconstrained(conditional, project.Events.AsConstrainedEntities())
   288  	if err != nil {
   289  		return nil, locale.WrapError(err, "err_event_condition", "Event has invalid conditional")
   290  	}
   291  
   292  	es := projectfile.MakeEventsFromConstrainedEntities(constrained)
   293  
   294  	var data []string
   295  	for _, event := range es {
   296  		data = append(data, event.Name)
   297  	}
   298  
   299  	return data, nil
   300  }
   301  
   302  func scriptsData(project *projectfile.Project, conditional *constraints.Conditional) (map[string]string, error) {
   303  	if len(project.Scripts) == 0 {
   304  		return nil, nil
   305  	}
   306  
   307  	constrained, err := constraints.FilterUnconstrained(conditional, project.Scripts.AsConstrainedEntities())
   308  	if err != nil {
   309  		return nil, locale.WrapError(err, "err_script_condition", "Script has invalid conditional")
   310  	}
   311  
   312  	scripts := projectfile.MakeScriptsFromConstrainedEntities(constrained)
   313  
   314  	data := make(map[string]string)
   315  	for _, script := range scripts {
   316  		data[script.Name] = script.Description
   317  	}
   318  
   319  	return data, nil
   320  }
   321  
   322  func platformsData(owner, project string, branchID strfmt.UUID, auth *authentication.Auth) ([]platformRow, error) {
   323  	remotePlatforms, err := model.FetchPlatformsForCommit(branchID, auth)
   324  	if err != nil {
   325  		return nil, locale.WrapError(err, "err_show_get_platforms", "Could not get platform details for commit: {{.V0}}", branchID.String())
   326  	}
   327  
   328  	platforms := make([]platformRow, 0, len(remotePlatforms))
   329  	for _, plat := range remotePlatforms {
   330  		if plat.DisplayName != nil {
   331  			p := platformRow{Name: *plat.OperatingSystem.Name, Version: *plat.OperatingSystemVersion.Version, BitWidth: *plat.CPUArchitecture.BitWidth}
   332  			platforms = append(platforms, p)
   333  		}
   334  	}
   335  
   336  	return platforms, nil
   337  }
   338  
   339  func languagesData(commitID strfmt.UUID, auth *authentication.Auth) ([]languageRow, error) {
   340  	platformLanguages, err := model.FetchLanguagesForCommit(commitID, auth)
   341  	if err != nil {
   342  		return nil, locale.WrapError(err, "err_show_get_languages", "Could not get languages for project")
   343  	}
   344  
   345  	languages := make([]languageRow, 0, len(platformLanguages))
   346  	for _, pl := range platformLanguages {
   347  		l := languageRow{Name: pl.Name, Version: pl.Version}
   348  		languages = append(languages, l)
   349  	}
   350  
   351  	return languages, nil
   352  }
   353  
   354  func visibilityData(owner, project string, remoteProject *mono_models.Project) string {
   355  	if remoteProject.Private {
   356  		return locale.T("private")
   357  	}
   358  	return locale.T("public")
   359  }
   360  
   361  func commitsData(owner, project, branchName string, commitID strfmt.UUID, localProject *project.Project, auth *authentication.Auth) (string, error) {
   362  	latestCommit, err := model.BranchCommitID(owner, project, branchName)
   363  	if err != nil {
   364  		return "", locale.WrapError(err, "err_show_get_latest_commit", "Could not get latest commit ID")
   365  	}
   366  
   367  	if !auth.Authenticated() {
   368  		return latestCommit.String(), nil
   369  	}
   370  
   371  	belongs, err := model.CommitBelongsToBranch(owner, project, branchName, commitID, auth)
   372  	if err != nil {
   373  		return "", locale.WrapError(err, "err_show_get_commit_belongs", "Could not determine if commit belongs to branch")
   374  	}
   375  
   376  	if localProject != nil && localProject.Owner() == owner && localProject.Name() == project && belongs {
   377  		var latestCommitID strfmt.UUID
   378  		if latestCommit != nil {
   379  			latestCommitID = *latestCommit
   380  		}
   381  		behind, err := model.CommitsBehind(latestCommitID, commitID, auth)
   382  		if err != nil {
   383  			return "", locale.WrapError(err, "err_show_commits_behind", "Could not determine number of commits behind latest")
   384  		}
   385  		localCommitID, err := localcommit.Get(localProject.Dir())
   386  		if err != nil {
   387  			return "", errs.Wrap(err, "Unable to get local commit")
   388  		}
   389  		if behind > 0 {
   390  			return fmt.Sprintf("%s (%d %s)", localCommitID.String(), behind, locale.Tl("show_commits_behind_latest", "behind latest")), nil
   391  		} else if behind < 0 {
   392  			return fmt.Sprintf("%s (%d %s)", localCommitID.String(), -behind, locale.Tl("show_commits_ahead_of_latest", "ahead of latest")), nil
   393  		}
   394  		return localCommitID.String(), nil
   395  	}
   396  
   397  	return latestCommit.String(), nil
   398  }
   399  
   400  func secretsData(owner, project string, auth *authentication.Auth) (*secretOutput, error) {
   401  	if !auth.Authenticated() {
   402  		return nil, nil
   403  	}
   404  
   405  	client := secretsapi.Get(auth)
   406  	sec, err := secrets.DefsByProject(client, owner, project)
   407  	if err != nil {
   408  		logging.Debug("Could not get secret definitions, got failure: %s", err)
   409  		return nil, locale.WrapError(err, "err_show_get_secrets", "Could not get secret definitions, you may not be authorized to view secrets on this project")
   410  	}
   411  
   412  	var userSecrets []string
   413  	var projectSecrets []string
   414  	for _, s := range sec {
   415  		data := *s.Name
   416  		if s.Description != "" {
   417  			data = fmt.Sprintf("%s: %s", *s.Name, s.Description)
   418  		}
   419  		if strings.ToLower(*s.Scope) == "project" {
   420  			projectSecrets = append(projectSecrets, data)
   421  			continue
   422  		}
   423  		userSecrets = append(userSecrets, data)
   424  	}
   425  
   426  	if len(userSecrets) == 0 && len(projectSecrets) == 0 {
   427  		return nil, nil
   428  	}
   429  
   430  	secrets := secretOutput{}
   431  	if len(userSecrets) > 0 {
   432  		secrets.User = userSecrets
   433  	}
   434  	if len(projectSecrets) > 0 {
   435  		secrets.Project = projectSecrets
   436  	}
   437  
   438  	return &secrets, nil
   439  }