| // 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 | |
| } |