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 }