github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/release/update/s3.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package update 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "log" 11 "os" 12 "path/filepath" 13 "sort" 14 "strings" 15 "text/tabwriter" 16 "time" 17 18 "github.com/alecthomas/template" 19 "github.com/blang/semver" 20 "github.com/keybase/client/go/release/version" 21 22 "github.com/aws/aws-sdk-go/aws" 23 "github.com/aws/aws-sdk-go/aws/session" 24 "github.com/aws/aws-sdk-go/service/s3" 25 ) 26 27 const defaultCacheControl = "max-age=60" 28 29 const defaultChannel = "v2" 30 31 // Section defines a set of releases 32 type Section struct { 33 Header string 34 Releases []Release 35 } 36 37 // Release defines a release bundle 38 type Release struct { 39 Name string 40 Key string 41 URL string 42 Version string 43 DateString string 44 Date time.Time 45 Commit string 46 } 47 48 // ByRelease defines how to sort releases 49 type ByRelease []Release 50 51 func (s ByRelease) Len() int { 52 return len(s) 53 } 54 55 func (s ByRelease) Swap(i, j int) { 56 s[i], s[j] = s[j], s[i] 57 } 58 59 func (s ByRelease) Less(i, j int) bool { 60 // Reverse date order 61 return s[j].Date.Before(s[i].Date) 62 } 63 64 // Client is an S3 client 65 type Client struct { 66 svc *s3.S3 67 } 68 69 // NewClient constructs a Client 70 func NewClient() (*Client, error) { 71 sess, err := session.NewSession(&aws.Config{Region: aws.String("us-east-1")}) 72 if err != nil { 73 return nil, err 74 } 75 svc := s3.New(sess) 76 return &Client{svc: svc}, nil 77 } 78 79 func convertEastern(t time.Time) time.Time { 80 locationNewYork, err := time.LoadLocation("America/New_York") 81 if err != nil { 82 log.Printf("Couldn't load location: %s", err) 83 } 84 return t.In(locationNewYork) 85 } 86 87 func loadReleases(objects []*s3.Object, bucketName string, prefix string, suffix string, truncate int) []Release { 88 var releases []Release 89 for _, obj := range objects { 90 if strings.HasSuffix(*obj.Key, suffix) { 91 urlString, name := urlStringForKey(*obj.Key, bucketName, prefix) 92 if name == "index.html" { 93 continue 94 } 95 version, _, date, commit, err := version.Parse(name) 96 if err != nil { 97 log.Printf("Couldn't get version from name: %s\n", name) 98 } 99 date = convertEastern(date) 100 releases = append(releases, 101 Release{ 102 Name: name, 103 Key: *obj.Key, 104 URL: urlString, 105 Version: version, 106 Date: date, 107 DateString: date.Format("Mon Jan _2 15:04:05 MST 2006"), 108 Commit: commit, 109 }) 110 } 111 } 112 // TODO: Should also sanity check that version sort is same as time sort 113 // otherwise something got messed up 114 sort.Sort(ByRelease(releases)) 115 if truncate > 0 && len(releases) > truncate { 116 releases = releases[0:truncate] 117 } 118 return releases 119 } 120 121 // WriteHTML creates an html file for releases 122 func WriteHTML(bucketName string, prefixes string, suffix string, outPath string, uploadDest string) error { 123 var sections []Section 124 for _, prefix := range strings.Split(prefixes, ",") { 125 126 objs, listErr := listAllObjects(bucketName, prefix) 127 if listErr != nil { 128 return listErr 129 } 130 131 releases := loadReleases(objs, bucketName, prefix, suffix, 50) 132 if len(releases) > 0 { 133 log.Printf("Found %d release(s) at %s\n", len(releases), prefix) 134 // for _, release := range releases { 135 // log.Printf(" %s %s %s\n", release.Name, release.Version, release.DateString) 136 // } 137 } 138 sections = append(sections, Section{ 139 Header: prefix, 140 Releases: releases, 141 }) 142 } 143 144 var buf bytes.Buffer 145 err := WriteHTMLForLinks(bucketName, sections, &buf) 146 if err != nil { 147 return err 148 } 149 if outPath != "" { 150 err = makeParentDirs(outPath) 151 if err != nil { 152 return err 153 } 154 err = os.WriteFile(outPath, buf.Bytes(), 0644) 155 if err != nil { 156 return err 157 } 158 } 159 160 if uploadDest != "" { 161 client, err := NewClient() 162 if err != nil { 163 return err 164 } 165 166 log.Printf("Uploading to %s", uploadDest) 167 _, err = client.svc.PutObject(&s3.PutObjectInput{ 168 Bucket: aws.String(bucketName), 169 Key: aws.String(uploadDest), 170 CacheControl: aws.String(defaultCacheControl), 171 ACL: aws.String("public-read"), 172 Body: bytes.NewReader(buf.Bytes()), 173 ContentLength: aws.Int64(int64(buf.Len())), 174 ContentType: aws.String("text/html"), 175 }) 176 if err != nil { 177 return err 178 } 179 } 180 181 return nil 182 } 183 184 var htmlTemplate = ` 185 <!doctype html> 186 <html lang="en"> 187 <head> 188 <title>{{ .Title }}</title> 189 <style> 190 body { font-family: monospace; } 191 </style> 192 </head> 193 <body> 194 {{ range $index, $sec := .Sections }} 195 <h3>{{ $sec.Header }}</h3> 196 <ul> 197 {{ range $index2, $rel := $sec.Releases }} 198 <li><a href="{{ $rel.URL }}">{{ $rel.Name }}</a> <strong>{{ $rel.Version }}</strong> <em>{{ $rel.Date }}</em> <a href="https://github.com/keybase/client/commit/{{ $rel.Commit }}"">{{ $rel.Commit }}</a></li> 199 {{ end }} 200 </ul> 201 {{ end }} 202 </body> 203 </html> 204 ` 205 206 // WriteHTMLForLinks writes a summary document for a set of releases 207 func WriteHTMLForLinks(title string, sections []Section, writer io.Writer) error { 208 vars := map[string]interface{}{ 209 "Title": title, 210 "Sections": sections, 211 } 212 213 t, err := template.New("t").Parse(htmlTemplate) 214 if err != nil { 215 return err 216 } 217 218 return t.Execute(writer, vars) 219 } 220 221 // Platform defines where platform specific files are (in darwin, linux, windows) 222 type Platform struct { 223 Name string 224 Prefix string 225 PrefixSupport string 226 Suffix string 227 LatestName string 228 } 229 230 // CopyLatest copies latest release to a fixed path 231 func CopyLatest(bucketName string, platform string, dryRun bool) error { 232 client, err := NewClient() 233 if err != nil { 234 return err 235 } 236 return client.CopyLatest(bucketName, platform, dryRun) 237 } 238 239 const ( 240 // PlatformTypeDarwin is platform type for OS X 241 PlatformTypeDarwin = "darwin" 242 PlatformTypeDarwinArm64 = "darwin-arm64" 243 // PlatformTypeLinux is platform type for Linux 244 PlatformTypeLinux = "linux" 245 // PlatformTypeWindows is platform type for windows 246 PlatformTypeWindows = "windows" 247 ) 248 249 var platformDarwin = Platform{Name: PlatformTypeDarwin, Prefix: "darwin/", PrefixSupport: "darwin-support/", LatestName: "Keybase.dmg"} 250 var platformDarwinArm64 = Platform{Name: PlatformTypeDarwinArm64, Prefix: "darwin-arm64/", PrefixSupport: "darwin-arm64-support/", LatestName: "Keybase-arm64.dmg"} 251 var platformLinuxDeb = Platform{Name: "deb", Prefix: "linux_binaries/deb/", Suffix: "_amd64.deb", LatestName: "keybase_amd64.deb"} 252 var platformLinuxRPM = Platform{Name: "rpm", Prefix: "linux_binaries/rpm/", Suffix: ".x86_64.rpm", LatestName: "keybase_amd64.rpm"} 253 var platformWindows = Platform{Name: PlatformTypeWindows, Prefix: "windows/", PrefixSupport: "windows-support/", LatestName: "keybase_setup_amd64.msi"} 254 255 var platformsAll = []Platform{ 256 platformDarwin, 257 platformDarwinArm64, 258 platformLinuxDeb, 259 platformLinuxRPM, 260 platformWindows, 261 } 262 263 // Platforms returns platforms for a name (linux may have multiple platforms) or all platforms is "" is specified 264 func Platforms(name string) ([]Platform, error) { 265 switch name { 266 case PlatformTypeDarwin: 267 return []Platform{platformDarwin}, nil 268 case PlatformTypeDarwinArm64: 269 return []Platform{platformDarwinArm64}, nil 270 case PlatformTypeLinux: 271 return []Platform{platformLinuxDeb, platformLinuxRPM}, nil 272 case PlatformTypeWindows: 273 return []Platform{platformWindows}, nil 274 case "": 275 return platformsAll, nil 276 default: 277 return nil, fmt.Errorf("Invalid platform %s", name) 278 } 279 } 280 281 func listAllObjects(bucketName string, prefix string) ([]*s3.Object, error) { 282 client, err := NewClient() 283 if err != nil { 284 return nil, err 285 } 286 287 marker := "" 288 objs := make([]*s3.Object, 0, 1000) 289 for { 290 resp, err := client.svc.ListObjects(&s3.ListObjectsInput{ 291 Bucket: aws.String(bucketName), 292 Delimiter: aws.String("/"), 293 Prefix: aws.String(prefix), 294 Marker: aws.String(marker), 295 }) 296 if err != nil { 297 return nil, err 298 } 299 if resp == nil { 300 break 301 } 302 303 out := *resp 304 nextMarker := "" 305 truncated := false 306 if out.NextMarker != nil { 307 nextMarker = *out.NextMarker 308 } 309 if out.IsTruncated != nil { 310 truncated = *out.IsTruncated 311 } 312 313 objs = append(objs, out.Contents...) 314 if !truncated { 315 break 316 } 317 318 log.Printf("Response is truncated, next marker is %s\n", nextMarker) 319 marker = nextMarker 320 } 321 322 return objs, nil 323 } 324 325 // FindRelease searches for a release matching a predicate 326 func (p *Platform) FindRelease(bucketName string, f func(r Release) bool) (*Release, error) { 327 contents, err := listAllObjects(bucketName, p.Prefix) 328 if err != nil { 329 return nil, err 330 } 331 332 releases := loadReleases(contents, bucketName, p.Prefix, p.Suffix, 0) 333 for _, release := range releases { 334 if !strings.HasSuffix(release.Key, p.Suffix) { 335 continue 336 } 337 if f(release) { 338 return &release, nil 339 } 340 } 341 return nil, nil 342 } 343 344 // Files returns all files associated with this platforms release 345 func (p Platform) Files(releaseName string) ([]string, error) { 346 switch p.Name { 347 case PlatformTypeDarwin: 348 return []string{ 349 fmt.Sprintf("darwin/Keybase-%s.dmg", releaseName), 350 fmt.Sprintf("darwin-updates/Keybase-%s.zip", releaseName), 351 fmt.Sprintf("darwin-support/update-darwin-prod-%s.json", releaseName), 352 }, nil 353 case PlatformTypeDarwinArm64: 354 return []string{ 355 fmt.Sprintf("darwin-arm64/Keybase-%s.dmg", releaseName), 356 fmt.Sprintf("darwin-arm64-updates/Keybase-%s.zip", releaseName), 357 fmt.Sprintf("darwin-arm64-support/update-darwin-prod-%s.json", releaseName), 358 }, nil 359 default: 360 return nil, fmt.Errorf("Unsupported for this platform: %s", p.Name) 361 } 362 } 363 364 // WriteHTML will generate index.html for the platform 365 func (p Platform) WriteHTML(bucketName string) error { 366 return WriteHTML(bucketName, p.Prefix, "", "", p.Prefix+"/index.html") 367 } 368 369 // CopyLatest copies latest release to a fixed path for the Client 370 func (c *Client) CopyLatest(bucketName string, platform string, dryRun bool) error { 371 platforms, err := Platforms(platform) 372 if err != nil { 373 return err 374 } 375 for _, platform := range platforms { 376 var url string 377 // Use update json to look for current DMG (for darwin) 378 // TODO: Fix for linux 379 switch platform.Name { 380 case PlatformTypeDarwin, PlatformTypeDarwinArm64, PlatformTypeWindows: 381 url, err = c.copyFromUpdate(platform, bucketName) 382 default: 383 _, url, err = c.copyFromReleases(platform, bucketName) 384 } 385 if err != nil { 386 return err 387 } 388 if url == "" { 389 continue 390 } 391 392 if dryRun { 393 log.Printf("DRYRUN: Would copy latest %s to %s\n", url, platform.LatestName) 394 return nil 395 } 396 397 _, err := c.svc.CopyObject(&s3.CopyObjectInput{ 398 Bucket: aws.String(bucketName), 399 CopySource: aws.String(url), 400 Key: aws.String(platform.LatestName), 401 CacheControl: aws.String(defaultCacheControl), 402 ACL: aws.String("public-read"), 403 }) 404 if err != nil { 405 return err 406 } 407 } 408 return nil 409 } 410 411 func (c *Client) copyFromUpdate(platform Platform, bucketName string) (url string, err error) { 412 currentUpdate, path, err := c.CurrentUpdate(bucketName, defaultChannel, platform.Name, "prod") 413 if err != nil { 414 err = fmt.Errorf("Error getting current public update: %s", err) 415 return 416 } 417 if currentUpdate == nil { 418 err = fmt.Errorf("No latest for %s at %s", platform.Name, path) 419 return 420 } 421 switch platform.Name { 422 case PlatformTypeDarwin, PlatformTypeDarwinArm64: 423 url = urlString(bucketName, platform.Prefix, fmt.Sprintf("Keybase-%s.dmg", currentUpdate.Version)) 424 case PlatformTypeWindows: 425 url = urlString(bucketName, platform.Prefix, fmt.Sprintf("Keybase_%s.amd64.msi", currentUpdate.Version)) 426 default: 427 err = fmt.Errorf("Unsupported platform for copyFromUpdate") 428 } 429 return 430 } 431 432 func (c *Client) copyFromReleases(platform Platform, bucketName string) (release *Release, url string, err error) { 433 release, err = platform.FindRelease(bucketName, func(r Release) bool { return true }) 434 if err != nil || release == nil { 435 return 436 } 437 url, _ = urlStringForKey(release.Key, bucketName, platform.Prefix) 438 return 439 } 440 441 // CurrentUpdate returns current update for a platform 442 func (c *Client) CurrentUpdate(bucketName string, channel string, platformName string, env string) (currentUpdate *Update, path string, err error) { 443 path = updateJSONName(channel, platformName, env) 444 log.Printf("Fetching current update at %s", path) 445 resp, err := c.svc.GetObject(&s3.GetObjectInput{ 446 Bucket: aws.String(bucketName), 447 Key: aws.String(path), 448 }) 449 if err != nil { 450 return 451 } 452 defer func() { _ = resp.Body.Close() }() 453 currentUpdate, err = DecodeJSON(resp.Body) 454 return 455 } 456 457 func promoteRelease(bucketName string, delay time.Duration, hourEastern int, toChannel string, platform Platform, env string, allowDowngrade bool, release string) (*Release, error) { 458 client, err := NewClient() 459 if err != nil { 460 return nil, err 461 } 462 return client.PromoteRelease(bucketName, delay, hourEastern, toChannel, platform, env, allowDowngrade, release) 463 } 464 465 func updateJSONName(channel string, platformName string, env string) string { 466 if channel == "" { 467 return fmt.Sprintf("update-%s-%s.json", platformName, env) 468 } 469 return fmt.Sprintf("update-%s-%s-%s.json", platformName, env, channel) 470 } 471 472 // PromoteARelease promotes a specific release to Prod. 473 func PromoteARelease(releaseName string, bucketName string, platform string, dryRun bool) (release *Release, err error) { 474 switch platform { 475 case PlatformTypeDarwin, PlatformTypeDarwinArm64, PlatformTypeWindows: 476 // pass 477 default: 478 return nil, fmt.Errorf("Promoting releases is only supported for darwin or windows") 479 480 } 481 482 client, err := NewClient() 483 if err != nil { 484 return nil, err 485 } 486 487 platformRes, err := Platforms(platform) 488 if err != nil { 489 return nil, err 490 } 491 if len(platformRes) != 1 { 492 return nil, fmt.Errorf("Promoting on multiple platforms is not supported") 493 } 494 495 platformType := platformRes[0] 496 release, err = client.promoteAReleaseToProd(releaseName, bucketName, platformType, "prod", defaultChannel, dryRun) 497 if err != nil { 498 return nil, err 499 } 500 if dryRun { 501 return release, nil 502 } 503 log.Printf("Promoted %s release: %s\n", platform, releaseName) 504 return release, nil 505 } 506 507 func (c *Client) promoteAReleaseToProd(releaseName string, bucketName string, platform Platform, env string, toChannel string, dryRun bool) (release *Release, err error) { 508 var filePath string 509 switch platform.Name { 510 case PlatformTypeDarwin, PlatformTypeDarwinArm64: 511 filePath = fmt.Sprintf("Keybase-%s.dmg", releaseName) 512 case PlatformTypeWindows: 513 filePath = fmt.Sprintf("Keybase_%s.amd64.msi", releaseName) 514 default: 515 return nil, fmt.Errorf("Unsupported for this platform: %s", platform.Name) 516 } 517 518 release, err = platform.FindRelease(bucketName, func(r Release) bool { 519 return r.Name == filePath 520 }) 521 if err != nil { 522 return nil, err 523 } 524 if release == nil { 525 return nil, fmt.Errorf("No matching release found") 526 } 527 log.Printf("Found %s release %s (%s), %s", platform.Name, release.Name, time.Since(release.Date), release.Version) 528 jsonName := updateJSONName(toChannel, platform.Name, env) 529 jsonURL := urlString(bucketName, platform.PrefixSupport, fmt.Sprintf("update-%s-%s-%s.json", platform.Name, env, release.Version)) 530 531 if dryRun { 532 log.Printf("DRYRUN: Would PutCopy %s to %s\n", jsonURL, jsonName) 533 return release, nil 534 } 535 log.Printf("PutCopying %s to %s\n", jsonURL, jsonName) 536 _, err = c.svc.CopyObject(&s3.CopyObjectInput{ 537 Bucket: aws.String(bucketName), 538 CopySource: aws.String(jsonURL), 539 Key: aws.String(jsonName), 540 CacheControl: aws.String(defaultCacheControl), 541 ACL: aws.String("public-read"), 542 }) 543 return release, err 544 } 545 546 // PromoteRelease promotes a release to a channel 547 func (c *Client) PromoteRelease(bucketName string, delay time.Duration, beforeHourEastern int, toChannel string, platform Platform, env string, allowDowngrade bool, releaseName string) (*Release, error) { 548 log.Printf("Finding release to promote to %q (%s delay) in env %s", toChannel, delay, env) 549 var release *Release 550 var err error 551 552 if releaseName != "" { 553 releaseName = fmt.Sprintf("Keybase-%s.dmg", releaseName) 554 release, err = platform.FindRelease(bucketName, func(r Release) bool { 555 return r.Name == releaseName 556 }) 557 } else { 558 release, err = platform.FindRelease(bucketName, func(r Release) bool { 559 log.Printf("Checking release date %s", r.Date) 560 if delay != 0 && time.Since(r.Date) < delay { 561 return false 562 } 563 hour, _, _ := r.Date.Clock() 564 if beforeHourEastern != 0 && hour >= beforeHourEastern { 565 return false 566 } 567 return true 568 }) 569 } 570 571 if err != nil { 572 return nil, err 573 } 574 575 if release == nil { 576 log.Printf("No matching release found") 577 return nil, nil 578 } 579 log.Printf("Found release %s (%s), %s", release.Name, time.Since(release.Date), release.Version) 580 581 currentUpdate, _, err := c.CurrentUpdate(bucketName, toChannel, platform.Name, env) 582 if err != nil { 583 log.Printf("Error looking for current update: %s (%s)", err, platform.Name) 584 } 585 if currentUpdate != nil { 586 log.Printf("Found current update: %s", currentUpdate.Version) 587 var currentVer semver.Version 588 currentVer, err = semver.Make(currentUpdate.Version) 589 if err != nil { 590 return nil, err 591 } 592 var releaseVer semver.Version 593 releaseVer, err = semver.Make(release.Version) 594 if err != nil { 595 return nil, err 596 } 597 598 if releaseVer.Equals(currentVer) { 599 log.Printf("Release unchanged") 600 return nil, nil 601 } else if releaseVer.LT(currentVer) { 602 if !allowDowngrade { 603 log.Printf("Release older than current update") 604 return nil, nil 605 } 606 log.Printf("Allowing downgrade") 607 } 608 } 609 610 jsonURL := urlString(bucketName, platform.PrefixSupport, fmt.Sprintf("update-%s-%s-%s.json", platform.Name, env, release.Version)) 611 jsonName := updateJSONName(toChannel, platform.Name, env) 612 log.Printf("PutCopying %s to %s\n", jsonURL, jsonName) 613 _, err = c.svc.CopyObject(&s3.CopyObjectInput{ 614 Bucket: aws.String(bucketName), 615 CopySource: aws.String(jsonURL), 616 Key: aws.String(jsonName), 617 CacheControl: aws.String(defaultCacheControl), 618 ACL: aws.String("public-read"), 619 }) 620 621 if err != nil { 622 return nil, err 623 } 624 return release, nil 625 } 626 627 func copyUpdateJSON(bucketName string, fromChannel string, toChannel string, platformName string, env string) error { 628 client, err := NewClient() 629 if err != nil { 630 return err 631 } 632 jsonNameDest := updateJSONName(toChannel, platformName, env) 633 jsonURLSource := urlString(bucketName, "", updateJSONName(fromChannel, platformName, env)) 634 635 log.Printf("PutCopying %s to %s\n", jsonURLSource, jsonNameDest) 636 _, err = client.svc.CopyObject(&s3.CopyObjectInput{ 637 Bucket: aws.String(bucketName), 638 CopySource: aws.String(jsonURLSource), 639 Key: aws.String(jsonNameDest), 640 CacheControl: aws.String(defaultCacheControl), 641 ACL: aws.String("public-read"), 642 }) 643 return err 644 } 645 646 func (c *Client) report(tw io.Writer, bucketName string, channel string, platformName string) { 647 update, jsonPath, err := c.CurrentUpdate(bucketName, channel, platformName, "prod") 648 fmt.Fprintf(tw, "%s\t%s\t", platformName, channel) 649 if err != nil { 650 fmt.Fprintln(tw, "Error") 651 } else if update != nil { 652 published := "" 653 if update.PublishedAt != nil { 654 published = convertEastern(FromTime(*update.PublishedAt)).Format(time.UnixDate) 655 } 656 fmt.Fprintf(tw, "%s\t%s\t%s\n", update.Version, published, jsonPath) 657 } else { 658 fmt.Fprintln(tw, "None") 659 } 660 } 661 662 // Report returns a summary of releases 663 func Report(bucketName string, writer io.Writer) error { 664 client, err := NewClient() 665 if err != nil { 666 return err 667 } 668 669 tw := tabwriter.NewWriter(writer, 5, 0, 3, ' ', 0) 670 fmt.Fprintln(tw, "Platform\tChannel\tVersion\tCreated\tSource") 671 client.report(tw, bucketName, "test-v2", PlatformTypeDarwin) 672 client.report(tw, bucketName, "v2", PlatformTypeDarwin) 673 client.report(tw, bucketName, "test-v2", PlatformTypeDarwinArm64) 674 client.report(tw, bucketName, "v2", PlatformTypeDarwinArm64) 675 client.report(tw, bucketName, "test", PlatformTypeLinux) 676 client.report(tw, bucketName, "", PlatformTypeLinux) 677 return tw.Flush() 678 } 679 680 // promoteTestReleaseForDarwin creates a test release for darwin 681 func promoteTestReleaseForDarwin(bucketName string, release string) (*Release, error) { 682 return promoteRelease(bucketName, time.Duration(0), 0, "test-v2", platformDarwin, "prod", true, release) 683 } 684 685 func promoteTestReleaseForDarwinArm64(bucketName string, release string) (*Release, error) { 686 return promoteRelease(bucketName, time.Duration(0), 0, "test-v2", platformDarwinArm64, "prod", true, release) 687 } 688 689 // promoteTestReleaseForLinux creates a test release for linux 690 func promoteTestReleaseForLinux(bucketName string) error { 691 // This just copies public to test since we don't do promotion on this platform yet 692 return copyUpdateJSON(bucketName, "", "test", PlatformTypeLinux, "prod") 693 } 694 695 // promoteTestReleaseForWindows creates a test release for windows 696 func promoteTestReleaseForWindows(bucketName string) error { 697 // This just copies public to test since we don't do promotion on this platform yet 698 return copyUpdateJSON(bucketName, "", "test", PlatformTypeWindows, "prod") 699 } 700 701 // PromoteTestReleases creates test releases for a platform 702 func PromoteTestReleases(bucketName string, platformName string, release string) error { 703 switch platformName { 704 case PlatformTypeDarwin: 705 _, err := promoteTestReleaseForDarwin(bucketName, release) 706 return err 707 case PlatformTypeDarwinArm64: 708 _, err := promoteTestReleaseForDarwinArm64(bucketName, release) 709 return err 710 case PlatformTypeLinux: 711 return promoteTestReleaseForLinux(bucketName) 712 case PlatformTypeWindows: 713 return promoteTestReleaseForWindows(bucketName) 714 default: 715 return fmt.Errorf("Invalid platform %s", platformName) 716 } 717 } 718 719 // PromoteReleases creates releases for a platform 720 func PromoteReleases(bucketName string, platformType string) (release *Release, err error) { 721 var platform Platform 722 switch platformType { 723 case PlatformTypeDarwin: 724 platform = platformDarwin 725 case PlatformTypeDarwinArm64: 726 platform = platformDarwinArm64 727 default: 728 log.Printf("Promoting releases is unsupported for %s", platformType) 729 return 730 } 731 release, err = promoteRelease(bucketName, time.Hour*27, 10, defaultChannel, platform, "prod", false, "") 732 if err != nil { 733 return nil, err 734 } 735 if release != nil { 736 log.Printf("Promoted (darwin) release: %s\n", release.Name) 737 } 738 return release, nil 739 } 740 741 // ReleaseBroken marks a release as broken. The releaseName is the version, 742 // for example, 1.2.3+400-deadbeef. 743 func ReleaseBroken(releaseName string, bucketName string, platformName string) ([]string, error) { 744 client, err := NewClient() 745 if err != nil { 746 return nil, err 747 } 748 platforms, err := Platforms(platformName) 749 if err != nil { 750 return nil, err 751 } 752 removed := []string{} 753 for _, platform := range platforms { 754 files, err := platform.Files(releaseName) 755 if err != nil { 756 return nil, err 757 } 758 for _, path := range files { 759 sourceURL := urlString(bucketName, "", path) 760 brokenPath := fmt.Sprintf("broken/%s", path) 761 log.Printf("Copying %s to %s", sourceURL, brokenPath) 762 763 _, err := client.svc.CopyObject(&s3.CopyObjectInput{ 764 Bucket: aws.String(bucketName), 765 CopySource: aws.String(sourceURL), 766 Key: aws.String(brokenPath), 767 CacheControl: aws.String(defaultCacheControl), 768 ACL: aws.String("public-read"), 769 }) 770 if err != nil { 771 log.Printf("There was an error trying to (put) copy %s: %s", sourceURL, err) 772 continue 773 } 774 775 log.Printf("Deleting: %s", path) 776 _, err = client.svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucketName), Key: aws.String(path)}) 777 if err != nil { 778 return removed, err 779 } 780 removed = append(removed, path) 781 } 782 783 // Update html for platform 784 if err := platform.WriteHTML(bucketName); err != nil { 785 log.Printf("Error updating html: %s", err) 786 } 787 788 // Fix test releases if needed 789 if err := PromoteTestReleases(bucketName, platform.Name, ""); err != nil { 790 log.Printf("Error fixing test releases: %s", err) 791 } 792 } 793 log.Printf("Deleted %d files for %s", len(removed), releaseName) 794 if len(removed) == 0 { 795 return removed, fmt.Errorf("No files to remove for %s", releaseName) 796 } 797 798 return removed, nil 799 } 800 801 // SaveLog saves log to S3 bucket (last maxNumBytes) and returns the URL. 802 // The log is publicly readable on S3 but the url is not discoverable. 803 func SaveLog(bucketName string, localPath string, maxNumBytes int64) (string, error) { 804 client, err := NewClient() 805 if err != nil { 806 return "", err 807 } 808 809 file, err := os.Open(localPath) 810 if err != nil { 811 return "", fmt.Errorf("Error opening: %s", err) 812 } 813 defer func() { _ = file.Close() }() 814 815 stat, err := os.Stat(localPath) 816 if err != nil { 817 return "", fmt.Errorf("Error in stat: %s", err) 818 } 819 if maxNumBytes > stat.Size() { 820 maxNumBytes = stat.Size() 821 } 822 823 data := make([]byte, maxNumBytes) 824 start := stat.Size() - maxNumBytes 825 _, err = file.ReadAt(data, start) 826 if err != nil { 827 return "", fmt.Errorf("Error reading: %s", err) 828 } 829 830 filename := filepath.Base(localPath) 831 logID, err := RandomID() 832 if err != nil { 833 return "", err 834 } 835 uploadDest := filepath.ToSlash(filepath.Join("logs", fmt.Sprintf("%s-%s%s", filename, logID, ".txt"))) 836 837 _, err = client.svc.PutObject(&s3.PutObjectInput{ 838 Bucket: aws.String(bucketName), 839 Key: aws.String(uploadDest), 840 CacheControl: aws.String(defaultCacheControl), 841 ACL: aws.String("public-read"), 842 Body: bytes.NewReader(data), 843 ContentLength: aws.Int64(int64(len(data))), 844 ContentType: aws.String("text/plain"), 845 }) 846 if err != nil { 847 return "", err 848 } 849 850 url := urlStringNoEscape(bucketName, uploadDest) 851 return url, nil 852 }