go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/cmd/isolate/isolateimpl/batch_archive.go (about)

     1  // Copyright 2015 The LUCI Authors.
     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 isolateimpl
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"runtime/pprof"
    24  	"runtime/trace"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/maruel/subcommands"
    29  
    30  	"go.chromium.org/luci/auth"
    31  	"go.chromium.org/luci/client/casclient"
    32  	"go.chromium.org/luci/client/isolate"
    33  	"go.chromium.org/luci/common/errors"
    34  	"go.chromium.org/luci/common/system/signals"
    35  )
    36  
    37  // CmdBatchArchive returns an object for the `batcharchive` subcommand.
    38  func CmdBatchArchive(defaultAuthOpts auth.Options) *subcommands.Command {
    39  	return &subcommands.Command{
    40  		UsageLine: "batcharchive <options> file1 file2 ...",
    41  		ShortDesc: "archives multiple CAS trees at once.",
    42  		LongDesc: `Archives multiple CAS trees at once.
    43  
    44  Using single command instead of multiple sequential invocations allows to cut
    45  redundant work when CAS trees share common files (e.g. file hashes are
    46  checked only once, their presence on the server is checked only once, and
    47  so on).
    48  `,
    49  		CommandRun: func() subcommands.CommandRun {
    50  			c := batchArchiveRun{}
    51  			c.commonServerFlags.Init(defaultAuthOpts)
    52  			c.casFlags.Init(&c.Flags)
    53  			c.Flags.StringVar(&c.dumpJSON, "dump-json", "", "Write CAS root digest of archived trees to this file as JSON")
    54  			return &c
    55  		},
    56  	}
    57  }
    58  
    59  type batchArchiveRun struct {
    60  	commonServerFlags
    61  	casFlags casclient.Flags
    62  	dumpJSON string
    63  }
    64  
    65  func (c *batchArchiveRun) Parse(a subcommands.Application, args []string) error {
    66  	if err := c.commonServerFlags.Parse(); err != nil {
    67  		return err
    68  	}
    69  	if err := c.casFlags.Parse(); err != nil {
    70  		return err
    71  	}
    72  	if len(args) == 0 {
    73  		return errors.Reason("at least one isolate file required").Err()
    74  	}
    75  	return nil
    76  }
    77  
    78  func parseArchiveCMD(args []string, cwd string) (*isolate.ArchiveOptions, error) {
    79  	// Python isolate allows form "--XXXX-variable key value".
    80  	// Golang flag pkg doesn't consider value to be part of --XXXX-variable flag.
    81  	// Therefore, we convert all such "--XXXX-variable key value" to
    82  	// "--XXXX-variable key --XXXX-variable value" form.
    83  	// Note, that key doesn't have "=" in it in either case, but value might.
    84  	// TODO(tandrii): eventually, we want to retire this hack.
    85  	args = convertPyToGoArchiveCMDArgs(args)
    86  	base := subcommands.CommandRunBase{}
    87  	i := isolateFlags{}
    88  	i.Init(&base.Flags)
    89  	if err := base.GetFlags().Parse(args); err != nil {
    90  		return nil, err
    91  	}
    92  	if err := i.Parse(cwd); err != nil {
    93  		return nil, err
    94  	}
    95  	if base.GetFlags().NArg() > 0 {
    96  		return nil, errors.Reason("no positional arguments expected").Err()
    97  	}
    98  	i.PostProcess(cwd)
    99  	return &i.ArchiveOptions, nil
   100  }
   101  
   102  // convertPyToGoArchiveCMDArgs converts kv-args from old python isolate into go variants.
   103  // Essentially converts "--X key value" into "--X key=value".
   104  func convertPyToGoArchiveCMDArgs(args []string) []string {
   105  	kvars := map[string]bool{
   106  		"--path-variable":   true,
   107  		"--config-variable": true,
   108  	}
   109  	var newArgs []string
   110  	for i := 0; i < len(args); {
   111  		newArgs = append(newArgs, args[i])
   112  		kvar := args[i]
   113  		i++
   114  		if !kvars[kvar] {
   115  			continue
   116  		}
   117  		if i >= len(args) {
   118  			// Ignore unexpected behaviour, it'll be caught by flags.Parse() .
   119  			break
   120  		}
   121  		appendArg := args[i]
   122  		i++
   123  		if !strings.Contains(appendArg, "=") && i < len(args) {
   124  			// appendArg is key, and args[i] is value .
   125  			appendArg = fmt.Sprintf("%s=%s", appendArg, args[i])
   126  			i++
   127  		}
   128  		newArgs = append(newArgs, appendArg)
   129  	}
   130  	return newArgs
   131  }
   132  
   133  func (c *batchArchiveRun) main(a subcommands.Application, args []string) error {
   134  	start := time.Now()
   135  	ctx, cancel := context.WithCancel(c.defaultFlags.MakeLoggingContext(os.Stderr))
   136  	defer cancel()
   137  	defer signals.HandleInterrupt(func() {
   138  		pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   139  		cancel()
   140  	})()
   141  
   142  	ctx, task := trace.NewTask(ctx, "batcharchive")
   143  	defer task.End()
   144  
   145  	opts, err := toArchiveOptions(args)
   146  	if err != nil {
   147  		return errors.Annotate(err, "failed to process input JSONs").Err()
   148  	}
   149  
   150  	al := &archiveLogger{
   151  		start: start,
   152  		quiet: c.defaultFlags.Quiet,
   153  	}
   154  
   155  	ctx, err = casclient.ContextWithMetadata(ctx, "isolate")
   156  	if err != nil {
   157  		return err
   158  	}
   159  	_, err = c.uploadToCAS(ctx, c.dumpJSON, c.commonServerFlags.parsedAuthOpts, &c.casFlags, al, opts...)
   160  	return err
   161  }
   162  
   163  func toArchiveOptions(genJSONPaths []string) ([]*isolate.ArchiveOptions, error) {
   164  	opts := make([]*isolate.ArchiveOptions, len(genJSONPaths))
   165  	for i, genJSONPath := range genJSONPaths {
   166  		o, err := processGenJSON(genJSONPath)
   167  		if err != nil {
   168  			return nil, errors.Annotate(err, "%q", genJSONPath).Err()
   169  		}
   170  		opts[i] = o
   171  	}
   172  	return opts, nil
   173  }
   174  
   175  // processGenJSON validates a genJSON file and returns the contents.
   176  func processGenJSON(genJSONPath string) (*isolate.ArchiveOptions, error) {
   177  	f, err := os.Open(genJSONPath)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	defer f.Close()
   182  	return processGenJSONData(f)
   183  }
   184  
   185  // processGenJSONData implements processGenJSON, but operates on an io.Reader.
   186  func processGenJSONData(r io.Reader) (*isolate.ArchiveOptions, error) {
   187  	var data struct {
   188  		Args    []string
   189  		Dir     string
   190  		Version int
   191  	}
   192  	if err := json.NewDecoder(r).Decode(&data); err != nil {
   193  		return nil, errors.Annotate(err, "failed to decode").Err()
   194  	}
   195  
   196  	if data.Version != isolate.IsolatedGenJSONVersion {
   197  		return nil, errors.Reason("unsupported version %d", data.Version).Err()
   198  	}
   199  
   200  	if fileInfo, err := os.Stat(data.Dir); err != nil || !fileInfo.IsDir() {
   201  		return nil, errors.Reason("invalid dir %q", data.Dir).Err()
   202  	}
   203  
   204  	opts, err := parseArchiveCMD(data.Args, data.Dir)
   205  	if err != nil {
   206  		return nil, errors.Annotate(err, "invalid archive command").Err()
   207  	}
   208  	return opts, nil
   209  }
   210  
   211  func (c *batchArchiveRun) Run(a subcommands.Application, args []string, _ subcommands.Env) int {
   212  	if err := c.Parse(a, args); err != nil {
   213  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   214  		return 1
   215  	}
   216  	defer c.profiler.Stop()
   217  	if err := c.main(a, args); err != nil {
   218  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), strings.Join(errors.RenderStack(err), "\n"))
   219  		return 1
   220  	}
   221  	return 0
   222  }