| // Copyright 2021 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 provisioner |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| |
| "cloud.google.com/go/storage" |
| "cos.googlesource.com/cos/tools.git/src/pkg/utils" |
| ) |
| |
| var ( |
| errStateAlreadyExists = errors.New("state already exists") |
| ) |
| |
| type stateData struct { |
| Config Config |
| CurrentStep int |
| DiskResizeComplete bool |
| } |
| |
| type state struct { |
| dir string |
| data stateData |
| } |
| |
| func (s *state) dataPath() string { |
| return filepath.Join(s.dir, "state.json") |
| } |
| |
| func (s *state) read() error { |
| data, err := ioutil.ReadFile(s.dataPath()) |
| if err != nil { |
| return fmt.Errorf("error reading %q: %v", s.dataPath(), err) |
| } |
| if err := json.Unmarshal(data, &s.data); err != nil { |
| return fmt.Errorf("error parsing JSON file %q: %v", s.dataPath(), err) |
| } |
| return nil |
| } |
| |
| func (s *state) write() error { |
| data, err := json.Marshal(&s.data) |
| if err != nil { |
| return fmt.Errorf("error marshalling JSON: %v", err) |
| } |
| if err := ioutil.WriteFile(s.dataPath(), data, 0660); err != nil { |
| return fmt.Errorf("error writing %q: %v", s.dataPath(), err) |
| } |
| return nil |
| } |
| |
| func downloadGCSObject(ctx context.Context, gcsClient *storage.Client, bucket, object, localPath string) error { |
| address := fmt.Sprintf("gs://%s/%s", bucket, object) |
| gcsObj, err := gcsClient.Bucket(bucket).Object(object).NewReader(ctx) |
| if err != nil { |
| return fmt.Errorf("error reading %q: %v", address, err) |
| } |
| defer utils.CheckClose(gcsObj, fmt.Sprintf("error closing GCS reader %q", address), &err) |
| localFile, err := os.Create(localPath) |
| if err != nil { |
| return err |
| } |
| defer utils.CheckClose(localFile, "", &err) |
| if _, err := io.Copy(localFile, gcsObj); err != nil { |
| return fmt.Errorf("error copying %q to %q: %v", address, localFile.Name(), err) |
| } |
| return nil |
| } |
| |
| func (s *state) unpackBuildContexts(ctx context.Context, deps Deps) (err error) { |
| for name, address := range s.data.Config.BuildContexts { |
| log.Printf("Unpacking build context %q from %q", name, address) |
| if address[:len("gs://")] != "gs://" { |
| return fmt.Errorf("cannot use address %q, only gs:// addresses are supported", address) |
| } |
| splitAddr := strings.SplitN(address[len("gs://"):], "/", 2) |
| if len(splitAddr) != 2 || splitAddr[0] == "" || splitAddr[1] == "" { |
| return fmt.Errorf("address %q is malformed", address) |
| } |
| bucket, object := splitAddr[0], splitAddr[1] |
| tarPath := filepath.Join(s.dir, name+".tar") |
| if err := downloadGCSObject(ctx, deps.GCSClient, bucket, object, tarPath); err != nil { |
| return fmt.Errorf("error downloading %q to %q: %v", address, tarPath, err) |
| } |
| tarDir := filepath.Join(s.dir, name) |
| if err := os.Mkdir(tarDir, 0770); err != nil { |
| return err |
| } |
| args := []string{"xf", tarPath, "-C", tarDir} |
| cmd := exec.Command(deps.TarCmd, args...) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return fmt.Errorf(`error in cmd "%s %v", see stderr for details: %v`, deps.TarCmd, args, err) |
| } |
| if err := os.Remove(tarPath); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func initState(ctx context.Context, deps Deps, dir string, c Config) (*state, error) { |
| s := &state{dir: dir, data: stateData{Config: c, CurrentStep: 0}} |
| if _, err := os.Stat(s.dataPath()); err == nil { |
| return nil, errStateAlreadyExists |
| } |
| if err := os.MkdirAll(dir, 0770); err != nil { |
| return nil, fmt.Errorf("error creating directory %q: %v", dir, err) |
| } |
| if err := s.write(); err != nil { |
| return nil, err |
| } |
| if err := s.unpackBuildContexts(ctx, deps); err != nil { |
| return nil, fmt.Errorf("error unpacking build contexts: %v", err) |
| } |
| return s, nil |
| } |
| |
| func loadState(dir string) (*state, error) { |
| s := &state{dir: dir} |
| if err := s.read(); err != nil { |
| return nil, err |
| } |
| return s, nil |
| } |