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

     1  // Copyright 2015 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 gcloud
    16  
    17  import (
    18  	"fmt"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/spf13/cobra"
    25  	"google.golang.org/api/googleapi"
    26  	"google.golang.org/api/storage/v1"
    27  
    28  	"github.com/coreos/mantle/platform/api/gcloud"
    29  	"github.com/coreos/mantle/sdk"
    30  )
    31  
    32  var (
    33  	cmdUpload = &cobra.Command{
    34  		Use:   "upload",
    35  		Short: "Upload os image",
    36  		Long:  "Upload os image to Google Storage bucket and create image in GCE. Intended for use in SDK.",
    37  		Run:   runUpload,
    38  	}
    39  
    40  	uploadBucket    string
    41  	uploadImageName string
    42  	uploadBoard     string
    43  	uploadFile      string
    44  	uploadForce     bool
    45  )
    46  
    47  func init() {
    48  	build := sdk.BuildRoot()
    49  	cmdUpload.Flags().StringVar(&uploadBucket, "bucket", "gs://users.developer.core-os.net", "gs://bucket/prefix/ prefix defaults to $USER")
    50  	cmdUpload.Flags().StringVar(&uploadImageName, "name", "", "name for uploaded image, defaults to COREOS_VERSION")
    51  	cmdUpload.Flags().StringVar(&uploadBoard, "board", "amd64-usr", "board used for naming with default prefix only")
    52  	cmdUpload.Flags().StringVar(&uploadFile, "file",
    53  		build+"/images/amd64-usr/latest/coreos_production_gce.tar.gz",
    54  		"path_to_coreos_image (build with: ./image_to_vm.sh --format=gce ...)")
    55  	cmdUpload.Flags().BoolVar(&uploadForce, "force", false, "overwrite existing GS and GCE images without prompt")
    56  	GCloud.AddCommand(cmdUpload)
    57  }
    58  
    59  func runUpload(cmd *cobra.Command, args []string) {
    60  	if len(args) != 0 {
    61  		fmt.Fprintf(os.Stderr, "Unrecognized args in plume upload cmd: %v\n", args)
    62  		os.Exit(2)
    63  	}
    64  
    65  	// if an image name is unspecified try to use version.txt
    66  	if uploadImageName == "" {
    67  		ver, err := sdk.VersionsFromDir(filepath.Dir(uploadFile))
    68  		if err != nil {
    69  			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)
    70  			os.Exit(1)
    71  		}
    72  		uploadImageName = ver.Version
    73  	}
    74  
    75  	gsURL, err := url.Parse(uploadBucket)
    76  	if err != nil {
    77  		fmt.Fprintf(os.Stderr, "%v\n", err)
    78  		os.Exit(1)
    79  	}
    80  	if gsURL.Scheme != "gs" {
    81  		fmt.Fprintf(os.Stderr, "URL missing gs:// scheme prefix: %v\n", uploadBucket)
    82  		os.Exit(1)
    83  	}
    84  	if gsURL.Host == "" {
    85  		fmt.Fprintf(os.Stderr, "URL missing bucket name %v\n", uploadBucket)
    86  		os.Exit(1)
    87  	}
    88  	// if prefix not specified default name to gs://bucket/$USER/$BOARD/$VERSION
    89  	if gsURL.Path == "" {
    90  		if user := os.Getenv("USER"); user != "" {
    91  			gsURL.Path = "/" + os.Getenv("USER")
    92  			gsURL.Path += "/" + uploadBoard
    93  		}
    94  	}
    95  
    96  	uploadBucket = gsURL.Host
    97  	uploadImageName = strings.TrimPrefix(gsURL.Path+"/"+uploadImageName, "/")
    98  	// create equivalent image names for GS and GCE
    99  	imageNameGCE := gceSanitize(uploadImageName)
   100  	imageNameGS := uploadImageName + ".tar.gz"
   101  
   102  	storageAPI, err := storage.New(api.Client())
   103  	if err != nil {
   104  		fmt.Fprintf(os.Stderr, "Storage client failed: %v\n", err)
   105  		os.Exit(1)
   106  	}
   107  
   108  	// check if this file is already uploaded and give option to skip
   109  	alreadyExists, err := fileQuery(storageAPI, uploadBucket, imageNameGS)
   110  	if err != nil {
   111  		fmt.Fprintf(os.Stderr, "Uploading image failed: %v\n", err)
   112  		os.Exit(1)
   113  	}
   114  
   115  	if alreadyExists && !uploadForce {
   116  		var ans string
   117  		fmt.Printf("File %v already exists on Google Storage. Overwrite? (y/n):", imageNameGS)
   118  		if _, err = fmt.Scan(&ans); err != nil {
   119  			fmt.Fprintf(os.Stderr, "Scanning overwrite input: %v", err)
   120  			os.Exit(1)
   121  		}
   122  		switch ans {
   123  		case "y", "Y", "yes":
   124  			fmt.Println("Overriding existing file...")
   125  			err = writeFile(storageAPI, uploadBucket, uploadFile, imageNameGS)
   126  		default:
   127  			fmt.Println("Skipped file upload")
   128  		}
   129  	} else {
   130  		err = writeFile(storageAPI, uploadBucket, uploadFile, imageNameGS)
   131  	}
   132  	if err != nil {
   133  		fmt.Fprintf(os.Stderr, "Uploading image failed: %v\n", err)
   134  		os.Exit(1)
   135  	}
   136  
   137  	fmt.Printf("Creating image in GCE: %v...\n", imageNameGCE)
   138  
   139  	// create image on gce
   140  	storageSrc := fmt.Sprintf("https://storage.googleapis.com/%v/%v", uploadBucket, imageNameGS)
   141  	_, pending, err := api.CreateImage(&gcloud.ImageSpec{
   142  		Name:        imageNameGCE,
   143  		SourceImage: storageSrc,
   144  	}, uploadForce)
   145  	if err == nil {
   146  		err = pending.Wait()
   147  	}
   148  
   149  	// if image already exists ask to delete and try again
   150  	if err != nil && strings.HasSuffix(err.Error(), "alreadyExists") {
   151  		var ans string
   152  		fmt.Printf("Image %v already exists on GCE. Overwrite? (y/n):", imageNameGCE)
   153  		if _, err = fmt.Scan(&ans); err != nil {
   154  			fmt.Fprintf(os.Stderr, "Scanning overwrite input: %v", err)
   155  			os.Exit(1)
   156  		}
   157  		switch ans {
   158  		case "y", "Y", "yes":
   159  			fmt.Println("Overriding existing image...")
   160  			_, pending, err = api.CreateImage(&gcloud.ImageSpec{
   161  				Name:        imageNameGCE,
   162  				SourceImage: storageSrc,
   163  			}, true)
   164  			if err == nil {
   165  				err = pending.Wait()
   166  			}
   167  			if err != nil {
   168  				fmt.Fprintf(os.Stderr, "Creating GCE image failed: %v\n", err)
   169  				os.Exit(1)
   170  			}
   171  			fmt.Printf("Image %v sucessfully created in GCE\n", imageNameGCE)
   172  		default:
   173  			fmt.Println("Skipped GCE image creation")
   174  		}
   175  	}
   176  
   177  	if err != nil {
   178  		fmt.Fprintf(os.Stderr, "Creating GCE image failed: %v\n", err)
   179  		os.Exit(1)
   180  	}
   181  }
   182  
   183  // Converts an image name from Google Storage to an equivalent GCE image
   184  // name. NOTE: Not a fully generlized sanitizer for GCE. Designed for
   185  // the default version.txt name (ex: 633.1.0+2015-03-31-1538). See:
   186  // https://godoc.org/google.golang.org/api/compute/v1#Image
   187  func gceSanitize(name string) string {
   188  	if name == "" {
   189  		return name
   190  	}
   191  
   192  	// remove incompatible chars from version.txt
   193  	name = strings.Replace(name, ".", "-", -1)
   194  	name = strings.Replace(name, "+", "-", -1)
   195  
   196  	// remove forward slashes likely from prefix
   197  	name = strings.Replace(name, "/", "-", -1)
   198  
   199  	// ensure name starts with [a-z]
   200  	char := name[0]
   201  	if char >= 'a' && char <= 'z' {
   202  		return name
   203  	}
   204  	if char >= 'A' && char <= 'Z' {
   205  		return strings.ToLower(name[:1]) + name[1:]
   206  	}
   207  	return "v" + name
   208  }
   209  
   210  // Write file to Google Storage
   211  func writeFile(api *storage.Service, bucket, filename, destname string) error {
   212  	fmt.Printf("Writing %v to gs://%v ...\n", filename, bucket)
   213  	fmt.Printf("(Sometimes this takes a few minutes)\n")
   214  
   215  	file, err := os.Open(filename)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	defer file.Close()
   220  
   221  	req := api.Objects.Insert(bucket, &storage.Object{
   222  		Name:        destname,
   223  		ContentType: "application/x-gzip",
   224  	})
   225  	req.PredefinedAcl("authenticatedRead")
   226  	req.Media(file)
   227  
   228  	if _, err := req.Do(); err != nil {
   229  		return err
   230  	}
   231  
   232  	fmt.Printf("Upload successful!\n")
   233  	return nil
   234  }
   235  
   236  // Test if file exists in Google Storage
   237  func fileQuery(api *storage.Service, bucket, name string) (bool, error) {
   238  	req := api.Objects.Get(bucket, name)
   239  	if _, err := req.Do(); err != nil {
   240  		if e, ok := err.(*googleapi.Error); ok && e.Code == 404 {
   241  			return false, nil
   242  		}
   243  		return false, err
   244  	}
   245  	return true, nil
   246  }