github.com/coreos/mantle@v0.13.0/cmd/ore/aws/upload.go (about)

     1  // Copyright 2017 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 aws
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"net/url"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/coreos/mantle/platform/api/aws"
    26  	"github.com/coreos/mantle/sdk"
    27  	"github.com/coreos/mantle/util"
    28  	"github.com/spf13/cobra"
    29  )
    30  
    31  var (
    32  	cmdUpload = &cobra.Command{
    33  		Use:   "upload",
    34  		Short: "Create AWS images",
    35  		Long: `Upload CoreOS image to S3 and create relevant AMIs (hvm and pv).
    36  
    37  Supported source formats are VMDK (as created with ./image_to_vm --format=ami_vmdk) and RAW.
    38  
    39  After a successful run, the final line of output will be a line of JSON describing the relevant resources.
    40  `,
    41  		Example: `  ore aws upload --region=us-east-1 \
    42  	  --ami-name="CoreOS-stable-1234.5.6" \
    43  	  --ami-description="CoreOS stable 1234.5.6" \
    44  	  --file="/home/.../coreos_production_ami_vmdk_image.vmdk" \
    45  	  --tags="machine=production"`,
    46  		RunE: runUpload,
    47  	}
    48  
    49  	uploadSourceObject    string
    50  	uploadBucket          string
    51  	uploadImageName       string
    52  	uploadBoard           string
    53  	uploadFile            string
    54  	uploadDiskSizeGiB     uint
    55  	uploadDiskSizeInspect bool
    56  	uploadDeleteObject    bool
    57  	uploadForce           bool
    58  	uploadSourceSnapshot  string
    59  	uploadObjectFormat    aws.EC2ImageFormat
    60  	uploadAMIName         string
    61  	uploadAMIDescription  string
    62  	uploadGrantUsers      []string
    63  	uploadCreatePV        bool
    64  	uploadTags            []string
    65  )
    66  
    67  func init() {
    68  	AWS.AddCommand(cmdUpload)
    69  	cmdUpload.Flags().StringVar(&uploadSourceObject, "source-object", "", "'s3://' URI pointing to image data (default: same as upload)")
    70  	cmdUpload.Flags().StringVar(&uploadBucket, "bucket", "", "s3://bucket/prefix/ (defaults to a regional bucket and prefix defaults to $USER/board/name)")
    71  	cmdUpload.Flags().StringVar(&uploadImageName, "name", "", "name of uploaded image (default COREOS_VERSION)")
    72  	cmdUpload.Flags().StringVar(&uploadBoard, "board", "amd64-usr", "board used for naming with default prefix only")
    73  	cmdUpload.Flags().StringVar(&uploadFile, "file",
    74  		defaultUploadFile(),
    75  		"path to CoreOS image (build with: ./image_to_vm.sh --format=ami_vmdk ...)")
    76  	cmdUpload.Flags().UintVarP(&uploadDiskSizeGiB, "disk-size-gib", "", aws.ContainerLinuxDiskSizeGiB, "AMI disk size in GiB")
    77  	cmdUpload.Flags().BoolVar(&uploadDiskSizeInspect, "disk-size-inspect", false, "set AMI disk size to size of local file")
    78  	cmdUpload.Flags().BoolVar(&uploadDeleteObject, "delete-object", true, "delete uploaded S3 object after snapshot is created")
    79  	cmdUpload.Flags().BoolVar(&uploadForce, "force", false, "overwrite existing S3 object rather than reusing")
    80  	cmdUpload.Flags().StringVar(&uploadSourceSnapshot, "source-snapshot", "", "the snapshot ID to base this AMI on (default: create new snapshot)")
    81  	cmdUpload.Flags().Var(&uploadObjectFormat, "object-format", fmt.Sprintf("object format: %s or %s (default: %s)", aws.EC2ImageFormatVmdk, aws.EC2ImageFormatRaw, aws.EC2ImageFormatVmdk))
    82  	cmdUpload.Flags().StringVar(&uploadAMIName, "ami-name", "", "name of the AMI to create (default: Container-Linux-$USER-$VERSION)")
    83  	cmdUpload.Flags().StringVar(&uploadAMIDescription, "ami-description", "", "description of the AMI to create (default: empty)")
    84  	cmdUpload.Flags().StringSliceVar(&uploadGrantUsers, "grant-user", []string{}, "grant launch permission to this AWS user ID")
    85  	cmdUpload.Flags().BoolVar(&uploadCreatePV, "create-pv", false, "create a PV AMI in addition to the HVM AMI")
    86  	cmdUpload.Flags().StringSliceVar(&uploadTags, "tags", []string{}, "list of key=value tags to attach to the AMI")
    87  }
    88  
    89  func defaultBucketNameForRegion(region string) string {
    90  	return fmt.Sprintf("coreos-dev-ami-import-%s", region)
    91  }
    92  
    93  func defaultUploadFile() string {
    94  	build := sdk.BuildRoot()
    95  	return build + "/images/amd64-usr/latest/coreos_production_ami_vmdk_image.vmdk"
    96  }
    97  
    98  // defaultBucketURL determines the location the tool should upload to.
    99  // The 'urlPrefix' parameter, if it contains a path, will override all other
   100  // arguments
   101  func defaultBucketURL(urlPrefix, imageName, board, file, region string) (*url.URL, error) {
   102  	if urlPrefix == "" {
   103  		urlPrefix = fmt.Sprintf("s3://%s", defaultBucketNameForRegion(region))
   104  	}
   105  
   106  	s3URL, err := url.Parse(urlPrefix)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	if s3URL.Scheme != "s3" {
   111  		return nil, fmt.Errorf("invalid s3 scheme; must be 's3://', not '%s://'", s3URL.Scheme)
   112  	}
   113  	if s3URL.Host == "" {
   114  		return nil, fmt.Errorf("URL missing bucket name %v\n", urlPrefix)
   115  	}
   116  
   117  	// if prefix not specified, default to /$USER/$BOARD/$VERSION
   118  	if s3URL.Path == "" {
   119  		s3URL.Path = fmt.Sprintf("/%s/%s/%s", os.Getenv("USER"), board, imageName)
   120  	}
   121  
   122  	if s3URL.Path[len(s3URL.Path)-1] != '/' {
   123  		s3URL.Path += "/"
   124  	}
   125  	s3URL.Path += filepath.Base(file)
   126  
   127  	return s3URL, nil
   128  }
   129  
   130  func runUpload(cmd *cobra.Command, args []string) error {
   131  	if len(args) != 0 {
   132  		fmt.Fprintf(os.Stderr, "Unrecognized args in aws upload cmd: %v\n", args)
   133  		os.Exit(2)
   134  	}
   135  	if uploadSourceObject != "" && uploadSourceSnapshot != "" {
   136  		fmt.Fprintf(os.Stderr, "At most one of --source-object and --source-snapshot may be specified.\n")
   137  		os.Exit(2)
   138  	}
   139  	if uploadDiskSizeInspect && (uploadSourceObject != "" || uploadSourceSnapshot != "") {
   140  		fmt.Fprintf(os.Stderr, "--disk-size-inspect cannot be used with --source-object or --source-snapshot.\n")
   141  		os.Exit(2)
   142  	}
   143  
   144  	// if an image name is unspecified try to use version.txt
   145  	imageName := uploadImageName
   146  	if imageName == "" {
   147  		ver, err := sdk.VersionsFromDir(filepath.Dir(uploadFile))
   148  		if err != nil {
   149  			fmt.Fprintf(os.Stderr, "Unable to get version from image directory, provide a -name flag or include a version.txt in the image directory: %v\n", err)
   150  			os.Exit(1)
   151  		}
   152  		imageName = ver.Version
   153  	}
   154  
   155  	if uploadDiskSizeInspect {
   156  		imageInfo, err := util.GetImageInfo(uploadFile)
   157  		if err != nil {
   158  			fmt.Fprintf(os.Stderr, "Unable to query size of disk: %v\n", err)
   159  			os.Exit(1)
   160  		}
   161  		plog.Debugf("Image size: %v\n", imageInfo.VirtualSize)
   162  		const GiB = 1024 * 1024 * 1024
   163  		uploadDiskSizeGiB = uint(imageInfo.VirtualSize / GiB)
   164  		// Round up if there's leftover
   165  		if imageInfo.VirtualSize%GiB > 0 {
   166  			uploadDiskSizeGiB += 1
   167  		}
   168  	}
   169  
   170  	amiName := uploadAMIName
   171  	if amiName == "" {
   172  		ver, err := sdk.VersionsFromDir(filepath.Dir(uploadFile))
   173  		if err != nil {
   174  			fmt.Fprintf(os.Stderr, "could not guess image name: %v\n", err)
   175  			os.Exit(1)
   176  		}
   177  		awsVersion := strings.Replace(ver.Version, "+", "-", -1) // '+' is invalid in an AMI name
   178  		amiName = fmt.Sprintf("Container-Linux-dev-%s-%s", os.Getenv("USER"), awsVersion)
   179  	}
   180  
   181  	var s3URL *url.URL
   182  	var err error
   183  	if uploadSourceObject != "" {
   184  		s3URL, err = url.Parse(uploadSourceObject)
   185  		if err != nil {
   186  			fmt.Fprintf(os.Stderr, "%v\n", err)
   187  			os.Exit(1)
   188  		}
   189  	} else {
   190  		s3URL, err = defaultBucketURL(uploadBucket, imageName, uploadBoard, uploadFile, region)
   191  		if err != nil {
   192  			fmt.Fprintf(os.Stderr, "%v\n", err)
   193  			os.Exit(1)
   194  		}
   195  	}
   196  	plog.Debugf("S3 object: %v\n", s3URL)
   197  	s3BucketName := s3URL.Host
   198  	s3ObjectPath := strings.TrimPrefix(s3URL.Path, "/")
   199  
   200  	// if no snapshot was specified, check for an existing one or a
   201  	// snapshot task in progress
   202  	sourceSnapshot := uploadSourceSnapshot
   203  	if sourceSnapshot == "" {
   204  		snapshot, err := API.FindSnapshot(imageName)
   205  		if err != nil {
   206  			fmt.Fprintf(os.Stderr, "failed finding snapshot: %v\n", err)
   207  			os.Exit(1)
   208  		}
   209  		if snapshot != nil {
   210  			sourceSnapshot = snapshot.SnapshotID
   211  		}
   212  	}
   213  
   214  	// if there's no existing snapshot and no provided S3 object to
   215  	// make one from, upload to S3
   216  	if uploadSourceObject == "" && sourceSnapshot == "" {
   217  		f, err := os.Open(uploadFile)
   218  		if err != nil {
   219  			fmt.Fprintf(os.Stderr, "Could not open image file %v: %v\n", uploadFile, err)
   220  			os.Exit(1)
   221  		}
   222  		defer f.Close()
   223  
   224  		err = API.UploadObject(f, s3BucketName, s3ObjectPath, uploadForce, "", "")
   225  		if err != nil {
   226  			fmt.Fprintf(os.Stderr, "Error uploading: %v\n", err)
   227  			os.Exit(1)
   228  		}
   229  	}
   230  
   231  	// if we don't already have a snapshot, make one
   232  	if sourceSnapshot == "" {
   233  		snapshot, err := API.CreateSnapshot(imageName, s3URL.String(), uploadObjectFormat)
   234  		if err != nil {
   235  			fmt.Fprintf(os.Stderr, "unable to create snapshot: %v\n", err)
   236  			os.Exit(1)
   237  		}
   238  		sourceSnapshot = snapshot.SnapshotID
   239  	}
   240  
   241  	// if delete is enabled and we created the snapshot from an S3
   242  	// object that we also created (perhaps in a previous run), delete
   243  	// the S3 object
   244  	if uploadSourceObject == "" && uploadSourceSnapshot == "" && uploadDeleteObject {
   245  		if err := API.DeleteObject(s3BucketName, s3ObjectPath); err != nil {
   246  			fmt.Fprintf(os.Stderr, "unable to delete object: %v\n", err)
   247  			os.Exit(1)
   248  		}
   249  	}
   250  
   251  	// create AMIs and grant permissions
   252  	hvmID, err := API.CreateHVMImage(sourceSnapshot, uploadDiskSizeGiB, amiName+"-hvm", uploadAMIDescription)
   253  	if err != nil {
   254  		fmt.Fprintf(os.Stderr, "unable to create HVM image: %v\n", err)
   255  		os.Exit(1)
   256  	}
   257  
   258  	if len(uploadGrantUsers) > 0 {
   259  		err = API.GrantLaunchPermission(hvmID, uploadGrantUsers)
   260  		if err != nil {
   261  			fmt.Fprintf(os.Stderr, "unable to grant launch permission: %v\n", err)
   262  			os.Exit(1)
   263  		}
   264  	}
   265  
   266  	tagMap := make(map[string]string)
   267  	for _, tag := range uploadTags {
   268  		splitTag := strings.SplitN(tag, "=", 2)
   269  		if len(splitTag) != 2 {
   270  			fmt.Fprintf(os.Stderr, "invalid tag format; should be key=value, not %v\n", tag)
   271  			os.Exit(1)
   272  		}
   273  		key, value := splitTag[0], splitTag[1]
   274  		tagMap[key] = value
   275  	}
   276  
   277  	if err := API.CreateTags([]string{hvmID, sourceSnapshot}, tagMap); err != nil {
   278  		fmt.Fprintf(os.Stderr, "unable to add tags: %v\n", err)
   279  		os.Exit(1)
   280  	}
   281  
   282  	var pvID string
   283  	if uploadCreatePV {
   284  		pvImageID, err := API.CreatePVImage(sourceSnapshot, uploadDiskSizeGiB, amiName, uploadAMIDescription)
   285  		if err != nil {
   286  			fmt.Fprintf(os.Stderr, "unable to create PV image: %v\n", err)
   287  			os.Exit(1)
   288  		}
   289  		pvID = pvImageID
   290  
   291  		if len(uploadGrantUsers) > 0 {
   292  			err = API.GrantLaunchPermission(pvID, uploadGrantUsers)
   293  			if err != nil {
   294  				fmt.Fprintf(os.Stderr, "unable to grant launch permission: %v\n", err)
   295  				os.Exit(1)
   296  			}
   297  		}
   298  
   299  		if err := API.CreateTags([]string{pvID}, tagMap); err != nil {
   300  			fmt.Fprintf(os.Stderr, "unable to add tags: %v\n", err)
   301  			os.Exit(1)
   302  		}
   303  	}
   304  
   305  	err = json.NewEncoder(os.Stdout).Encode(&struct {
   306  		HVM        string
   307  		PV         string `json:",omitempty"`
   308  		SnapshotID string
   309  		S3Object   string
   310  	}{
   311  		HVM:        hvmID,
   312  		PV:         pvID,
   313  		SnapshotID: sourceSnapshot,
   314  		S3Object:   s3URL.String(),
   315  	})
   316  	if err != nil {
   317  		fmt.Fprintf(os.Stderr, "Couldn't encode result: %v\n", err)
   318  		os.Exit(1)
   319  	}
   320  	return nil
   321  }