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 }