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 }