github.com/coreos/mantle@v0.13.0/cmd/plume/prerelease.go (about)

     1  // Copyright 2016 CoreOS, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"html/template"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"sort"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/Azure/azure-sdk-for-go/management/storageservice"
    33  	"github.com/Microsoft/azure-vhd-utils/vhdcore/validator"
    34  	"github.com/spf13/cobra"
    35  	"golang.org/x/net/context"
    36  	gs "google.golang.org/api/storage/v1"
    37  
    38  	"github.com/coreos/mantle/platform/api/aws"
    39  	"github.com/coreos/mantle/platform/api/azure"
    40  	"github.com/coreos/mantle/sdk"
    41  	"github.com/coreos/mantle/storage"
    42  	"github.com/coreos/mantle/util"
    43  )
    44  
    45  var (
    46  	cmdPreRelease = &cobra.Command{
    47  		Use:   "pre-release [options]",
    48  		Short: "Run pre-release steps for CoreOS",
    49  		Long:  "Runs pre-release steps for CoreOS, such as image uploading and OS image creation, and replication across regions.",
    50  		RunE:  runPreRelease,
    51  	}
    52  
    53  	platforms = map[string]platform{
    54  		"aws": platform{
    55  			displayName: "AWS",
    56  			handler:     awsPreRelease,
    57  		},
    58  		"azure": platform{
    59  			displayName: "Azure",
    60  			handler:     azurePreRelease,
    61  		},
    62  	}
    63  	platformList []string
    64  
    65  	selectedPlatforms  []string
    66  	selectedDistro     string
    67  	azureProfile       string
    68  	awsCredentialsFile string
    69  	verifyKeyFile      string
    70  	imageInfoFile      string
    71  )
    72  
    73  type imageMetadataAbstract struct {
    74  	Env       string
    75  	Version   string
    76  	Timestamp string
    77  	Respin    string
    78  	ImageType string
    79  	Arch      string
    80  }
    81  
    82  type platform struct {
    83  	displayName string
    84  	handler     func(context.Context, *http.Client, *storage.Bucket, *channelSpec, *imageInfo) error
    85  }
    86  
    87  type imageInfo struct {
    88  	AWS   *amiList        `json:"aws,omitempty"`
    89  	Azure *azureImageInfo `json:"azure,omitempty"`
    90  }
    91  
    92  func init() {
    93  	for k, _ := range platforms {
    94  		platformList = append(platformList, k)
    95  	}
    96  	sort.Sort(sort.StringSlice(platformList))
    97  
    98  	cmdPreRelease.Flags().StringSliceVar(&selectedPlatforms, "platform", platformList, "platform to pre-release")
    99  	cmdPreRelease.Flags().StringVar(&selectedDistro, "system", "cl", "system to pre-release")
   100  	cmdPreRelease.Flags().StringVar(&azureProfile, "azure-profile", "", "Azure Profile json file")
   101  	cmdPreRelease.Flags().StringVar(&awsCredentialsFile, "aws-credentials", "", "AWS credentials file")
   102  	cmdPreRelease.Flags().StringVar(&verifyKeyFile,
   103  		"verify-key", "", "path to ASCII-armored PGP public key to be used in verifying download signatures.  Defaults to CoreOS Buildbot (0412 7D0B FABE C887 1FFB  2CCE 50E0 8855 93D2 DCB4)")
   104  	cmdPreRelease.Flags().StringVar(&imageInfoFile, "write-image-list", "", "optional output file describing uploaded images")
   105  
   106  	AddSpecFlags(cmdPreRelease.Flags())
   107  	AddFedoraSpecFlags(cmdPreRelease.Flags())
   108  	root.AddCommand(cmdPreRelease)
   109  }
   110  
   111  func runPreRelease(cmd *cobra.Command, args []string) error {
   112  	if len(args) > 0 {
   113  		return errors.New("no args accepted")
   114  	}
   115  
   116  	for _, platformName := range selectedPlatforms {
   117  		if _, ok := platforms[platformName]; !ok {
   118  			return fmt.Errorf("Unknown platform %q", platformName)
   119  		}
   120  	}
   121  
   122  	switch selectedDistro {
   123  	case "cl":
   124  		if err := runCLPreRelease(cmd); err != nil {
   125  			return err
   126  		}
   127  	case "fedora":
   128  		if err := runFedoraPreRelease(cmd); err != nil {
   129  			return err
   130  		}
   131  	default:
   132  		return fmt.Errorf("Unknown distro %q", selectedDistro)
   133  	}
   134  	plog.Printf("Pre-release complete, run `plume release` to finish.")
   135  
   136  	return nil
   137  }
   138  
   139  func runFedoraPreRelease(cmd *cobra.Command) error {
   140  	spec, err := ChannelFedoraSpec()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	ctx := context.Background()
   145  	client := http.Client{}
   146  
   147  	var imageInfo imageInfo
   148  
   149  	for _, platformName := range selectedPlatforms {
   150  		platform := platforms[platformName]
   151  		plog.Printf("Running %v pre-release...", platform.displayName)
   152  		if err := platform.handler(ctx, &client, nil, &spec, &imageInfo); err != nil {
   153  			return err
   154  		}
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  func runCLPreRelease(cmd *cobra.Command) error {
   161  	spec := ChannelSpec()
   162  	ctx := context.Background()
   163  	client, err := getGoogleClient()
   164  	if err != nil {
   165  		plog.Fatal(err)
   166  	}
   167  
   168  	src, err := storage.NewBucket(client, spec.SourceURL())
   169  	if err != nil {
   170  		plog.Fatal(err)
   171  	}
   172  
   173  	if err := src.Fetch(ctx); err != nil {
   174  		plog.Fatal(err)
   175  	}
   176  
   177  	// Sanity check!
   178  	if vertxt := src.Object(src.Prefix() + "version.txt"); vertxt == nil {
   179  		verurl := src.URL().String() + "version.txt"
   180  		plog.Fatalf("File not found: %s", verurl)
   181  	}
   182  
   183  	var imageInfo imageInfo
   184  	for _, platformName := range selectedPlatforms {
   185  		platform := platforms[platformName]
   186  		plog.Printf("Running %v pre-release...", platform.displayName)
   187  		if err := platform.handler(ctx, client, src, &spec, &imageInfo); err != nil {
   188  			plog.Fatal(err)
   189  		}
   190  	}
   191  
   192  	if imageInfoFile != "" {
   193  		f, err := os.OpenFile(imageInfoFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
   194  		if err != nil {
   195  			plog.Fatal(err)
   196  		}
   197  		defer f.Close()
   198  
   199  		encoder := json.NewEncoder(f)
   200  		encoder.SetIndent("", "  ")
   201  		if err := encoder.Encode(imageInfo); err != nil {
   202  			plog.Fatalf("couldn't encode image list: %v", err)
   203  		}
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  // getImageFile downloads a bzipped CoreOS image, verifies its signature,
   210  // decompresses it, and returns the decompressed path.
   211  func getImageFile(client *http.Client, spec *channelSpec, src *storage.Bucket, fileName string) (string, error) {
   212  	switch selectedDistro {
   213  	case "cl":
   214  		return getCLImageFile(client, src, fileName)
   215  	case "fedora":
   216  		return getFedoraImageFile(client, spec, src, fileName)
   217  	default:
   218  		return "", fmt.Errorf("Invalid system: %v", selectedDistro)
   219  	}
   220  }
   221  
   222  func getImageTypeURI() string {
   223  	if specImageType == "Cloud-Base" {
   224  		return "Cloud"
   225  	}
   226  	return specImageType
   227  }
   228  
   229  func getCLImageFile(client *http.Client, src *storage.Bucket, fileName string) (string, error) {
   230  	cacheDir := filepath.Join(sdk.RepoCache(), "images", specChannel, specBoard, specVersion)
   231  	bzipPath := filepath.Join(cacheDir, fileName)
   232  	imagePath := strings.TrimSuffix(bzipPath, filepath.Ext(bzipPath))
   233  
   234  	if _, err := os.Stat(imagePath); err == nil {
   235  		plog.Printf("Reusing existing image %q", imagePath)
   236  		return imagePath, nil
   237  	}
   238  
   239  	bzipUri, err := url.Parse(fileName)
   240  	if err != nil {
   241  		return "", err
   242  	}
   243  
   244  	bzipUri = src.URL().ResolveReference(bzipUri)
   245  
   246  	plog.Printf("Downloading image %q to %q", bzipUri, bzipPath)
   247  
   248  	if err := sdk.UpdateSignedFile(bzipPath, bzipUri.String(), client, verifyKeyFile); err != nil {
   249  		return "", err
   250  	}
   251  
   252  	// decompress it
   253  	plog.Printf("Decompressing %q...", bzipPath)
   254  	if err := util.Bunzip2File(imagePath, bzipPath); err != nil {
   255  		return "", err
   256  	}
   257  	return imagePath, nil
   258  }
   259  
   260  func getFedoraImageFile(client *http.Client, spec *channelSpec, src *storage.Bucket, fileName string) (string, error) {
   261  	cacheDir := filepath.Join(sdk.RepoCache(), "images", specChannel, specVersion)
   262  	rawxzPath := filepath.Join(cacheDir, fileName)
   263  	imagePath := strings.TrimSuffix(rawxzPath, ".xz")
   264  
   265  	if _, err := os.Stat(imagePath); err == nil {
   266  		plog.Printf("Reusing existing image %q", imagePath)
   267  		return imagePath, nil
   268  	}
   269  
   270  	rawxzURI, err := url.Parse(fmt.Sprintf("%v/%v/compose/%v/%v/images/%v", spec.BaseURL, specComposeID, getImageTypeURI(), specBoard, fileName))
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  
   275  	plog.Printf("Downloading image %q to %q", rawxzURI, rawxzPath)
   276  
   277  	if err := sdk.UpdateFile(rawxzPath, rawxzURI.String(), client); err != nil {
   278  		return "", err
   279  	}
   280  
   281  	// decompress it
   282  	plog.Printf("Decompressing %q...", rawxzPath)
   283  	if err := util.XZ2File(imagePath, rawxzPath); err != nil {
   284  		return "", err
   285  	}
   286  	return imagePath, nil
   287  }
   288  
   289  func uploadAzureBlob(spec *channelSpec, api *azure.API, storageKey storageservice.GetStorageServiceKeysResponse, vhdfile, container, blobName string) error {
   290  	blobExists, err := api.BlobExists(spec.Azure.StorageAccount, storageKey.PrimaryKey, container, blobName)
   291  	if err != nil {
   292  		return fmt.Errorf("failed to check if file %q in account %q container %q exists: %v", vhdfile, spec.Azure.StorageAccount, container, err)
   293  	}
   294  
   295  	if blobExists {
   296  		return nil
   297  	}
   298  
   299  	if err := api.UploadBlob(spec.Azure.StorageAccount, storageKey.PrimaryKey, vhdfile, container, blobName, false); err != nil {
   300  		if _, ok := err.(azure.BlobExistsError); !ok {
   301  			return fmt.Errorf("uploading file %q to account %q container %q failed: %v", vhdfile, spec.Azure.StorageAccount, container, err)
   302  		}
   303  	}
   304  	return nil
   305  }
   306  
   307  func createAzureImage(spec *channelSpec, api *azure.API, blobName, imageName string) error {
   308  	imageexists, err := api.OSImageExists(imageName)
   309  	if err != nil {
   310  		return fmt.Errorf("failed to check if image %q exists: %T %v", imageName, err, err)
   311  	}
   312  
   313  	if imageexists {
   314  		plog.Printf("OS Image %q exists, using it", imageName)
   315  		return nil
   316  	}
   317  
   318  	plog.Printf("Creating OS image with name %q", imageName)
   319  
   320  	bloburl := api.UrlOfBlob(spec.Azure.StorageAccount, spec.Azure.Container, blobName).String()
   321  
   322  	// a la https://github.com/coreos/scripts/blob/998c7e093922298637e7c7e82e25cee7d336144d/oem/azure/set-image-metadata.sh
   323  	md := &azure.OSImage{
   324  		Label:             spec.Azure.Label,
   325  		Name:              imageName,
   326  		OS:                "Linux",
   327  		Description:       spec.Azure.Description,
   328  		MediaLink:         bloburl,
   329  		ImageFamily:       spec.Azure.Label,
   330  		PublishedDate:     time.Now().UTC().Format("2006-01-02"),
   331  		RecommendedVMSize: spec.Azure.RecommendedVMSize,
   332  		IconURI:           spec.Azure.IconURI,
   333  		SmallIconURI:      spec.Azure.SmallIconURI,
   334  	}
   335  
   336  	return api.AddOSImage(md)
   337  }
   338  
   339  func replicateAzureImage(spec *channelSpec, api *azure.API, imageName string) error {
   340  	plog.Printf("Fetching Azure Locations...")
   341  	locations, err := api.Locations()
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	plog.Printf("Replicating image to locations: %s", strings.Join(locations, ", "))
   347  
   348  	channelTitle := strings.Title(specChannel)
   349  
   350  	if err := api.ReplicateImage(imageName, spec.Azure.Offer, channelTitle, specVersion, locations...); err != nil {
   351  		return fmt.Errorf("image replication failed: %v", err)
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  type azureImageInfo struct {
   358  	ImageName string `json:"image"`
   359  }
   360  
   361  // azurePreRelease runs everything necessary to prepare a CoreOS release for Azure.
   362  //
   363  // This includes uploading the vhd image to Azure storage, creating an OS image from it,
   364  // and replicating that OS image.
   365  func azurePreRelease(ctx context.Context, client *http.Client, src *storage.Bucket, spec *channelSpec, imageInfo *imageInfo) error {
   366  	if spec.Azure.StorageAccount == "" {
   367  		plog.Notice("Azure image creation disabled.")
   368  		return nil
   369  	}
   370  
   371  	// download azure vhd image and unzip it
   372  	vhdfile, err := getImageFile(client, spec, src, spec.Azure.Image)
   373  	if err != nil {
   374  		return err
   375  	}
   376  
   377  	// sanity check - validate VHD file
   378  	plog.Printf("Validating VHD file %q", vhdfile)
   379  	if err := validator.ValidateVhd(vhdfile); err != nil {
   380  		return err
   381  	}
   382  	if err := validator.ValidateVhdSize(vhdfile); err != nil {
   383  		return err
   384  	}
   385  
   386  	blobName := fmt.Sprintf("container-linux-%s-%s.vhd", specVersion, specChannel)
   387  	// channel name should be caps for azure image
   388  	imageName := fmt.Sprintf("%s-%s-%s", spec.Azure.Offer, strings.Title(specChannel), specVersion)
   389  
   390  	for _, environment := range spec.Azure.Environments {
   391  		// construct azure api client
   392  		api, err := azure.New(&azure.Options{
   393  			AzureProfile:      azureProfile,
   394  			AzureSubscription: environment.SubscriptionName,
   395  		})
   396  		if err != nil {
   397  			return fmt.Errorf("failed to create Azure API: %v", err)
   398  		}
   399  
   400  		plog.Printf("Fetching Azure storage credentials")
   401  
   402  		storageKey, err := api.GetStorageServiceKeys(spec.Azure.StorageAccount)
   403  		if err != nil {
   404  			return err
   405  		}
   406  
   407  		// upload blob, do not overwrite
   408  		plog.Printf("Uploading %q to Azure Storage...", vhdfile)
   409  
   410  		containers := append([]string{spec.Azure.Container}, environment.AdditionalContainers...)
   411  		for _, container := range containers {
   412  			err := uploadAzureBlob(spec, api, storageKey, vhdfile, container, blobName)
   413  			if err != nil {
   414  				return err
   415  			}
   416  		}
   417  
   418  		// create image
   419  		if err := createAzureImage(spec, api, blobName, imageName); err != nil {
   420  			// if it is a conflict, it already exists!
   421  			if !azure.IsConflictError(err) {
   422  				return err
   423  			}
   424  
   425  			plog.Printf("Azure image %q already exists", imageName)
   426  		}
   427  
   428  		// replicate it
   429  		if err := replicateAzureImage(spec, api, imageName); err != nil {
   430  			return err
   431  		}
   432  	}
   433  
   434  	imageInfo.Azure = &azureImageInfo{
   435  		ImageName: imageName,
   436  	}
   437  	return nil
   438  }
   439  
   440  func getSpecAWSImageMetadata(spec *channelSpec) (map[string]string, error) {
   441  	imageFileName := spec.AWS.Image
   442  	imageMetadata := imageMetadataAbstract{
   443  		Env:       specEnv,
   444  		Version:   specVersion,
   445  		Timestamp: specTimestamp,
   446  		Respin:    specRespin,
   447  		ImageType: specImageType,
   448  		Arch:      specBoard,
   449  	}
   450  	t := template.Must(template.New("filename").Parse(imageFileName))
   451  	buffer := &bytes.Buffer{}
   452  	if err := t.Execute(buffer, imageMetadata); err != nil {
   453  		return nil, err
   454  	}
   455  	imageFileName = buffer.String()
   456  
   457  	var imageName string
   458  	switch selectedDistro {
   459  	case "cl":
   460  		imageName = fmt.Sprintf("%v-%v-%v", spec.AWS.BaseName, specChannel, specVersion)
   461  		imageName = regexp.MustCompile(`[^A-Za-z0-9()\\./_-]`).ReplaceAllLiteralString(imageName, "_")
   462  	case "fedora":
   463  		imageName = strings.TrimSuffix(imageFileName, ".raw.xz")
   464  	}
   465  
   466  	imageDescription := fmt.Sprintf("%v %v %v", spec.AWS.BaseDescription, specChannel, specVersion)
   467  
   468  	awsImageMetaData := map[string]string{
   469  		"imageFileName":    imageFileName,
   470  		"imageName":        imageName,
   471  		"imageDescription": imageDescription,
   472  	}
   473  
   474  	return awsImageMetaData, nil
   475  }
   476  
   477  func awsUploadToPartition(spec *channelSpec, part *awsPartitionSpec, imageName, imageDescription, imagePath string) (map[string]string, map[string]string, error) {
   478  	plog.Printf("Connecting to %v...", part.Name)
   479  	api, err := aws.New(&aws.Options{
   480  		CredentialsFile: awsCredentialsFile,
   481  		Profile:         part.Profile,
   482  		Region:          part.BucketRegion,
   483  	})
   484  	if err != nil {
   485  		return nil, nil, fmt.Errorf("creating client for %v: %v", part.Name, err)
   486  	}
   487  
   488  	f, err := os.Open(imagePath)
   489  	if err != nil {
   490  		return nil, nil, fmt.Errorf("Could not open image file %v: %v", imagePath, err)
   491  	}
   492  	defer f.Close()
   493  
   494  	awsImageMetadata, err := getSpecAWSImageMetadata(spec)
   495  	if err != nil {
   496  		return nil, nil, fmt.Errorf("Could not generate the image metadata: %v", err)
   497  	}
   498  
   499  	imageFileName := awsImageMetadata["imageFileName"]
   500  	imageName = awsImageMetadata["imageName"]
   501  	imageDescription = awsImageMetadata["imageDescription"]
   502  
   503  	var s3ObjectPath string
   504  	switch selectedDistro {
   505  	case "cl":
   506  		s3ObjectPath = fmt.Sprintf("%s/%s/%s", specBoard, specVersion, strings.TrimSuffix(imageFileName, filepath.Ext(imageFileName)))
   507  	case "fedora":
   508  		s3ObjectPath = fmt.Sprintf("%s/%s/%s", specBoard, specVersion, strings.TrimSuffix(imageFileName, filepath.Ext(imageFileName)))
   509  	}
   510  	s3ObjectURL := fmt.Sprintf("s3://%s/%s", part.Bucket, s3ObjectPath)
   511  
   512  	snapshot, err := api.FindSnapshot(imageName)
   513  	if err != nil {
   514  		return nil, nil, fmt.Errorf("unable to check for snapshot: %v", err)
   515  	}
   516  
   517  	if snapshot == nil {
   518  		plog.Printf("Creating S3 object %v...", s3ObjectURL)
   519  		err = api.UploadObject(f, part.Bucket, s3ObjectPath, false, "", "")
   520  		if err != nil {
   521  			return nil, nil, fmt.Errorf("Error uploading: %v", err)
   522  		}
   523  
   524  		plog.Printf("Creating EBS snapshot...")
   525  
   526  		var format aws.EC2ImageFormat
   527  		switch selectedDistro {
   528  		case "cl":
   529  			format = aws.EC2ImageFormatVmdk
   530  		case "fedora":
   531  			format = aws.EC2ImageFormatRaw
   532  		}
   533  
   534  		snapshot, err = api.CreateSnapshot(imageName, s3ObjectURL, format)
   535  		if err != nil {
   536  			return nil, nil, fmt.Errorf("unable to create snapshot: %v", err)
   537  		}
   538  	}
   539  
   540  	// delete unconditionally to avoid leaks after a restart
   541  	plog.Printf("Deleting S3 object %v...", s3ObjectURL)
   542  	err = api.DeleteObject(part.Bucket, s3ObjectPath)
   543  	if err != nil {
   544  		return nil, nil, fmt.Errorf("Error deleting S3 object: %v", err)
   545  	}
   546  
   547  	plog.Printf("Creating AMIs from %v...", snapshot.SnapshotID)
   548  
   549  	hvmImageID, err := api.CreateHVMImage(snapshot.SnapshotID, aws.ContainerLinuxDiskSizeGiB, imageName+"-hvm", imageDescription+" (HVM)")
   550  	if err != nil {
   551  		return nil, nil, fmt.Errorf("unable to create HVM image: %v", err)
   552  	}
   553  	resources := []string{snapshot.SnapshotID, hvmImageID}
   554  
   555  	var pvImageID string
   556  	if selectedDistro == "cl" {
   557  		pvImageID, err = api.CreatePVImage(snapshot.SnapshotID, aws.ContainerLinuxDiskSizeGiB, imageName, imageDescription+" (PV)")
   558  		if err != nil {
   559  			return nil, nil, fmt.Errorf("unable to create PV image: %v", err)
   560  		}
   561  		resources = append(resources, pvImageID)
   562  	}
   563  
   564  	switch selectedDistro {
   565  	case "cl":
   566  		err = api.CreateTags(resources, map[string]string{
   567  			"Channel": specChannel,
   568  			"Version": specVersion,
   569  		})
   570  		if err != nil {
   571  			return nil, nil, fmt.Errorf("couldn't tag images: %v", err)
   572  		}
   573  	case "fedora":
   574  		err = api.CreateTags(resources, map[string]string{
   575  			"Channel":   specChannel,
   576  			"Version":   specVersion,
   577  			"ComposeID": specComposeID,
   578  		})
   579  		if err != nil {
   580  			return nil, nil, fmt.Errorf("couldn't tag images: %v", err)
   581  		}
   582  	}
   583  
   584  	postprocess := func(imageID string, pv bool) (map[string]string, error) {
   585  		if len(part.LaunchPermissions) > 0 {
   586  			if err := api.GrantLaunchPermission(imageID, part.LaunchPermissions); err != nil {
   587  				return nil, err
   588  			}
   589  		}
   590  
   591  		destRegions := make([]string, 0, len(part.Regions))
   592  		foundBucketRegion := false
   593  		for _, region := range part.Regions {
   594  			if region != part.BucketRegion {
   595  				if pv && !aws.RegionSupportsPV(region) {
   596  					plog.Debugf("%v doesn't support PV AMIs; skipping", region)
   597  				} else {
   598  					destRegions = append(destRegions, region)
   599  				}
   600  			} else {
   601  				foundBucketRegion = true
   602  			}
   603  		}
   604  		if !foundBucketRegion {
   605  			// We don't handle this case and shouldn't ever
   606  			// encounter it
   607  			return nil, fmt.Errorf("BucketRegion %v is not listed in Regions", part.BucketRegion)
   608  		}
   609  
   610  		amis := map[string]string{}
   611  		if len(destRegions) > 0 {
   612  			plog.Printf("Replicating AMI %v...", imageID)
   613  			amis, err = api.CopyImage(imageID, destRegions)
   614  			if err != nil {
   615  				return nil, fmt.Errorf("couldn't copy image: %v", err)
   616  			}
   617  		}
   618  		amis[part.BucketRegion] = imageID
   619  
   620  		return amis, nil
   621  	}
   622  
   623  	hvmAmis, err := postprocess(hvmImageID, false)
   624  	if err != nil {
   625  		return nil, nil, fmt.Errorf("processing HVM images: %v", err)
   626  	}
   627  
   628  	var pvAmis map[string]string
   629  	if selectedDistro == "cl" {
   630  		pvAmis, err = postprocess(pvImageID, true)
   631  		if err != nil {
   632  			return nil, nil, fmt.Errorf("processing PV images: %v", err)
   633  		}
   634  	}
   635  
   636  	return hvmAmis, pvAmis, nil
   637  }
   638  
   639  type amiListEntry struct {
   640  	Region string `json:"name"`
   641  	PvAmi  string `json:"pv,omitempty"`
   642  	HvmAmi string `json:"hvm"`
   643  }
   644  
   645  type amiList struct {
   646  	Entries []amiListEntry `json:"amis"`
   647  }
   648  
   649  func awsUploadAmiLists(ctx context.Context, bucket *storage.Bucket, spec *channelSpec, amis *amiList) error {
   650  	upload := func(name string, data string) error {
   651  		var contentType string
   652  		if strings.HasSuffix(name, ".txt") {
   653  			contentType = "text/plain"
   654  		} else if strings.HasSuffix(name, ".json") {
   655  			contentType = "application/json"
   656  		} else {
   657  			return fmt.Errorf("unknown file extension in %v", name)
   658  		}
   659  
   660  		obj := gs.Object{
   661  			Name:        bucket.Prefix() + spec.AWS.Prefix + name,
   662  			ContentType: contentType,
   663  		}
   664  		media := bytes.NewReader([]byte(data))
   665  		if err := bucket.Upload(ctx, &obj, media); err != nil {
   666  			return fmt.Errorf("couldn't upload %v: %v", name, err)
   667  		}
   668  		return nil
   669  	}
   670  
   671  	// emit keys in stable order
   672  	sort.Slice(amis.Entries, func(i, j int) bool {
   673  		return amis.Entries[i].Region < amis.Entries[j].Region
   674  	})
   675  
   676  	// format JSON AMI list
   677  	var jsonBuf bytes.Buffer
   678  	encoder := json.NewEncoder(&jsonBuf)
   679  	encoder.SetIndent("", "  ")
   680  	if err := encoder.Encode(amis); err != nil {
   681  		return fmt.Errorf("couldn't encode JSON: %v", err)
   682  	}
   683  	jsonAll := jsonBuf.String()
   684  
   685  	// format text AMI lists and upload AMI IDs for individual regions
   686  	var hvmRecords, pvRecords []string
   687  	for _, entry := range amis.Entries {
   688  		hvmRecords = append(hvmRecords,
   689  			fmt.Sprintf("%v=%v", entry.Region, entry.HvmAmi))
   690  		if entry.PvAmi != "" {
   691  			pvRecords = append(pvRecords,
   692  				fmt.Sprintf("%v=%v", entry.Region, entry.PvAmi))
   693  		}
   694  
   695  		if err := upload(fmt.Sprintf("hvm_%v.txt", entry.Region),
   696  			entry.HvmAmi+"\n"); err != nil {
   697  			return err
   698  		}
   699  		if entry.PvAmi != "" {
   700  			if err := upload(fmt.Sprintf("pv_%v.txt", entry.Region),
   701  				entry.PvAmi+"\n"); err != nil {
   702  				return err
   703  			}
   704  			// compatibility
   705  			if err := upload(fmt.Sprintf("%v.txt", entry.Region),
   706  				entry.PvAmi+"\n"); err != nil {
   707  				return err
   708  			}
   709  		}
   710  	}
   711  	hvmAll := strings.Join(hvmRecords, "|") + "\n"
   712  	pvAll := strings.Join(pvRecords, "|") + "\n"
   713  
   714  	// upload AMI lists
   715  	if err := upload("all.json", jsonAll); err != nil {
   716  		return err
   717  	}
   718  	if err := upload("hvm.txt", hvmAll); err != nil {
   719  		return err
   720  	}
   721  	if err := upload("pv.txt", pvAll); err != nil {
   722  		return err
   723  	}
   724  	// compatibility
   725  	if err := upload("all.txt", pvAll); err != nil {
   726  		return err
   727  	}
   728  
   729  	return nil
   730  }
   731  
   732  // awsPreRelease runs everything necessary to prepare a CoreOS release for AWS.
   733  //
   734  // This includes uploading the ami_vmdk image to an S3 bucket in each EC2
   735  // partition, creating HVM and PV AMIs, and replicating the AMIs to each
   736  // region.
   737  func awsPreRelease(ctx context.Context, client *http.Client, src *storage.Bucket, spec *channelSpec, imageInfo *imageInfo) error {
   738  	if spec.AWS.Image == "" {
   739  		plog.Notice("AWS image creation disabled.")
   740  		return nil
   741  	}
   742  
   743  	awsImageMetadata, err := getSpecAWSImageMetadata(spec)
   744  	if err != nil {
   745  		return fmt.Errorf("Could not generate the image filname: %v", err)
   746  	}
   747  
   748  	imageFileName := awsImageMetadata["imageFileName"]
   749  	imageName := awsImageMetadata["imageName"]
   750  	imageDescription := awsImageMetadata["imageDescription"]
   751  
   752  	imagePath, err := getImageFile(client, spec, src, imageFileName)
   753  	if err != nil {
   754  		return err
   755  	}
   756  
   757  	var amis amiList
   758  	for i := range spec.AWS.Partitions {
   759  		hvmAmis, pvAmis, err := awsUploadToPartition(spec, &spec.AWS.Partitions[i], imageName, imageDescription, imagePath)
   760  		if err != nil {
   761  			return err
   762  		}
   763  
   764  		for region := range hvmAmis {
   765  			amis.Entries = append(amis.Entries, amiListEntry{
   766  				Region: region,
   767  				PvAmi:  pvAmis[region],
   768  				HvmAmi: hvmAmis[region],
   769  			})
   770  		}
   771  	}
   772  
   773  	if selectedDistro == "cl" {
   774  		if err := awsUploadAmiLists(ctx, src, spec, &amis); err != nil {
   775  			return fmt.Errorf("uploading AMI IDs: %v", err)
   776  		}
   777  	}
   778  
   779  	imageInfo.AWS = &amis
   780  	return nil
   781  }