blob: 5ad7ec07b7d69f39f35708e8f779eb1d0d513617 [file] [log] [blame]
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package cmd contains cos-customizer subcommand implementations.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"cos.googlesource.com/cos/tools.git/src/pkg/config"
"cos.googlesource.com/cos/tools.git/src/pkg/fs"
"cos.googlesource.com/cos/tools.git/src/pkg/gce"
"cos.googlesource.com/cos/tools.git/src/pkg/provisioner"
"cos.googlesource.com/cos/tools.git/src/pkg/utils"
"cloud.google.com/go/storage"
"github.com/google/subcommands"
compute "google.golang.org/api/compute/v1"
)
// ServiceClients gets the GCE and GCS clients to use.
type ServiceClients func(ctx context.Context, anonymousCreds bool) (*compute.Service, *storage.Client, error)
// StartImageBuild implements subcommands.Command for the 'start-image-build' command.
// This command initializes a new image customization process.
type StartImageBuild struct {
buildContext string
gcsBucket string
gcsWorkdir string
imageProject string
imageName string
milestone int
imageFamily string
}
// Name implements subcommands.Command.Name.
func (*StartImageBuild) Name() string {
return "start-image-build"
}
// Synopsis implements subcommands.Command.Synopsis.
func (*StartImageBuild) Synopsis() string {
return "Start a COS image build."
}
// Usage implements subcommands.Command.Usage.
func (*StartImageBuild) Usage() string {
return `start-image-build [flags]
`
}
// SetFlags implements subcommands.Command.SetFlags.
func (s *StartImageBuild) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.buildContext, "build-context", ".", "Path to the build context")
f.StringVar(&s.gcsBucket, "gcs-bucket", "", "GCS bucket to use for scratch space")
f.StringVar(&s.gcsWorkdir, "gcs-workdir", "", "GCS directory to use for scratch space")
f.StringVar(&s.imageProject, "image-project", "", "Source image project")
f.StringVar(&s.imageName, "image-name", "", "Source image name. Mutually exclusive with 'image-milestone' and "+
"'image-family'.")
f.IntVar(&s.milestone, "image-milestone", 0, "Source image milestone. Mutually exclusive with 'image-name' "+
"and 'image-family'. Can only be used if 'image-project' is cos-cloud.")
f.StringVar(&s.imageFamily, "image-family", "", "Source image family. Mutually exclusive with 'image-name' "+
"and 'image-milestone'.")
}
func (s *StartImageBuild) validate() error {
numSet := 0
for _, val := range []bool{s.imageName != "", s.milestone != 0, s.imageFamily != ""} {
if val {
numSet++
}
}
switch {
case numSet != 1:
return fmt.Errorf("exactly one of image-name, image-milestone, image-family must be set")
case s.milestone != 0 && s.imageProject != "cos-cloud":
return fmt.Errorf("image-milestone can only be used if image-project is set to cos-cloud. "+
"image-milestone: %d image-project: %s", s.milestone, s.imageProject)
case s.gcsBucket == "":
return fmt.Errorf("gcs-bucket must be set")
case s.gcsWorkdir == "":
return fmt.Errorf("gcs-workdir must be set")
case s.imageProject == "":
return fmt.Errorf("image-project must be set")
default:
return nil
}
}
func (s *StartImageBuild) resolveImageName(ctx context.Context, svc *compute.Service) error {
switch {
case s.milestone != 0:
var err error
s.imageName, err = gce.ResolveMilestone(ctx, svc, s.milestone)
if err != nil {
if err == gce.ErrImageNotFound {
return fmt.Errorf("no image found on milestone %d", s.milestone)
}
return err
}
log.Printf("Using image %s from milestone %d\n", s.imageName, s.milestone)
case s.imageFamily != "":
image, err := svc.Images.GetFromFamily(s.imageProject, s.imageFamily).Do()
if err != nil {
return err
}
s.imageName = image.Name
log.Printf("Using image %s from family %s\n", s.imageName, s.imageFamily)
default:
exists, err := gce.ImageExists(svc, s.imageProject, s.imageName)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("could not find source image %s in project %s", s.imageName, s.imageProject)
}
}
return nil
}
func saveImage(imageName, imageProject, dst string) error {
image := config.NewImage(imageName, imageProject)
if err := os.MkdirAll(filepath.Dir(dst), 0774); err != nil {
return err
}
outFile, err := os.Create(dst)
if err != nil {
return err
}
defer outFile.Close()
return config.Save(outFile, image)
}
func saveBuildConfig(gcsBucket, gcsWorkdir, dst string) error {
buildConfig := &config.Build{GCSBucket: gcsBucket, GCSDir: gcsWorkdir}
if err := os.MkdirAll(filepath.Dir(dst), 0774); err != nil {
return err
}
outFile, err := os.Create(dst)
if err != nil {
return err
}
defer outFile.Close()
return config.SaveConfigToFile(outFile, buildConfig)
}
func saveProvConfig(dst string) (err error) {
provConfig := &provisioner.Config{}
if err := os.MkdirAll(filepath.Dir(dst), 0774); err != nil {
return err
}
outFile, err := os.Create(dst)
if err != nil {
return err
}
defer utils.CheckClose(outFile, "error closing provisioner config", &err)
return config.SaveConfigToFile(outFile, provConfig)
}
// Execute implements subcommands.Command.Execute. It initializes persistent state for a new
// image customization process.
func (s *StartImageBuild) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
if f.NArg() != 0 {
f.Usage()
return subcommands.ExitUsageError
}
files := args[0].(*fs.Files)
svc, _, err := args[1].(ServiceClients)(ctx, false)
if err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := s.validate(); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := s.resolveImageName(ctx, svc); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := fs.CreateBuildContextArchive(s.buildContext, files.UserBuildContextArchive); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := saveImage(s.imageName, s.imageProject, files.SourceImageConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := saveBuildConfig(s.gcsBucket, s.gcsWorkdir, files.BuildConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := saveProvConfig(files.ProvConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}