golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gomote/put.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "archive/tar" 9 "bytes" 10 "context" 11 "errors" 12 "flag" 13 "fmt" 14 "io" 15 "mime/multipart" 16 "net/http" 17 "net/url" 18 "os" 19 "path/filepath" 20 "regexp" 21 "strconv" 22 "strings" 23 24 "golang.org/x/build/internal/gomote/protos" 25 "golang.org/x/build/tarutil" 26 "golang.org/x/sync/errgroup" 27 ) 28 29 // putTar a .tar.gz 30 func putTar(args []string) error { 31 fs := flag.NewFlagSet("put", flag.ContinueOnError) 32 fs.Usage = func() { 33 fmt.Fprintln(os.Stderr, "puttar usage: gomote puttar [put-opts] [instance] <source>") 34 fmt.Fprintln(os.Stderr) 35 fmt.Fprintln(os.Stderr, "<source> may be one of:") 36 fmt.Fprintln(os.Stderr, "- A path to a local .tar.gz file.") 37 fmt.Fprintln(os.Stderr, "- A URL that points at a .tar.gz file.") 38 fmt.Fprintln(os.Stderr, "- The '-' character to indicate a .tar.gz file passed via stdin.") 39 fmt.Fprintln(os.Stderr, "- Git hash (min 7 characters) for the Go repository (extract a .tar.gz of the repository at that commit w/o history)") 40 fmt.Fprintln(os.Stderr) 41 fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.") 42 fs.PrintDefaults() 43 os.Exit(1) 44 } 45 var dir string 46 fs.StringVar(&dir, "dir", "", "relative directory from buildlet's work dir to extra tarball into") 47 48 fs.Parse(args) 49 50 // Parse arguments. 51 var putSet []string 52 var src string 53 switch fs.NArg() { 54 case 1: 55 // Must be just the source, so we need an active group. 56 if activeGroup == nil { 57 fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument") 58 fs.Usage() 59 } 60 for _, inst := range activeGroup.Instances { 61 putSet = append(putSet, inst) 62 } 63 src = fs.Arg(0) 64 case 2: 65 // Instance and source is specified. 66 putSet = []string{fs.Arg(0)} 67 src = fs.Arg(1) 68 case 0: 69 fmt.Fprintln(os.Stderr, "error: not enough arguments") 70 fs.Usage() 71 default: 72 fmt.Fprintln(os.Stderr, "error: too many arguments") 73 fs.Usage() 74 } 75 76 // Interpret source. 77 var putTarFn func(ctx context.Context, inst string) error 78 if src == "-" { 79 // We might have multiple readers, so slurp up STDIN 80 // and store it, then hand out bytes.Readers to everyone. 81 var buf bytes.Buffer 82 _, err := io.Copy(&buf, os.Stdin) 83 if err != nil { 84 return fmt.Errorf("reading stdin: %w", err) 85 } 86 sharedTarBuf := buf.Bytes() 87 putTarFn = func(ctx context.Context, inst string) error { 88 return doPutTar(ctx, inst, dir, bytes.NewReader(sharedTarBuf)) 89 } 90 } else { 91 u, err := url.Parse(src) 92 if err != nil { 93 // The URL parser should technically accept any of these, so the fact that 94 // we failed means its *very* malformed. 95 return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash") 96 } 97 if u.Scheme != "" || u.Host != "" { 98 // Probably a real URL. 99 putTarFn = func(ctx context.Context, inst string) error { 100 return doPutTarURL(ctx, inst, dir, u.String()) 101 } 102 } else { 103 // Probably a path. Check if it exists. 104 _, err := os.Stat(src) 105 if os.IsNotExist(err) { 106 // It must be a git hash. Check if this actually matches a git hash. 107 if len(src) < 7 || len(src) > 40 || regexp.MustCompile("[^a-f0-9]").MatchString(src) { 108 return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash") 109 } 110 putTarFn = func(ctx context.Context, inst string) error { 111 return doPutTarGoRev(ctx, inst, dir, src) 112 } 113 } else if err != nil { 114 return fmt.Errorf("failed to stat %q: %w", src, err) 115 } else { 116 // It's a path. 117 putTarFn = func(ctx context.Context, inst string) error { 118 f, err := os.Open(src) 119 if err != nil { 120 return fmt.Errorf("opening %q: %w", src, err) 121 } 122 defer f.Close() 123 return doPutTar(ctx, inst, dir, f) 124 } 125 } 126 } 127 } 128 eg, ctx := errgroup.WithContext(context.Background()) 129 for _, inst := range putSet { 130 inst := inst 131 eg.Go(func() error { 132 return putTarFn(ctx, inst) 133 }) 134 } 135 return eg.Wait() 136 } 137 138 func doPutTarURL(ctx context.Context, name, dir, tarURL string) error { 139 client := gomoteServerClient(ctx) 140 _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ 141 GomoteId: name, 142 Directory: dir, 143 Url: tarURL, 144 }) 145 if err != nil { 146 return fmt.Errorf("unable to write tar to instance: %w", err) 147 } 148 return nil 149 } 150 151 func doPutTarGoRev(ctx context.Context, name, dir, rev string) error { 152 tarURL := "https://go.googlesource.com/go/+archive/" + rev + ".tar.gz" 153 if err := doPutTarURL(ctx, name, dir, tarURL); err != nil { 154 return err 155 } 156 157 // Put a VERSION file there too, to avoid git usage. 158 version := strings.NewReader("devel " + rev) 159 var vtar tarutil.FileList 160 vtar.AddRegular(&tar.Header{ 161 Name: "VERSION", 162 Mode: 0644, 163 Size: int64(version.Len()), 164 }, int64(version.Len()), version) 165 tgz := vtar.TarGz() 166 defer tgz.Close() 167 168 client := gomoteServerClient(ctx) 169 resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{}) 170 if err != nil { 171 return fmt.Errorf("unable to request credentials for a file upload: %w", err) 172 } 173 if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil { 174 return fmt.Errorf("unable to upload version file to GCS: %w", err) 175 } 176 if _, err = client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ 177 GomoteId: name, 178 Directory: dir, 179 Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()), 180 }); err != nil { 181 return fmt.Errorf("unable to write tar to instance: %w", err) 182 } 183 return nil 184 } 185 186 func doPutTar(ctx context.Context, name, dir string, tgz io.Reader) error { 187 client := gomoteServerClient(ctx) 188 resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{}) 189 if err != nil { 190 return fmt.Errorf("unable to request credentials for a file upload: %w", err) 191 } 192 if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil { 193 return fmt.Errorf("unable to upload file to GCS: %w", err) 194 } 195 if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ 196 GomoteId: name, 197 Directory: dir, 198 Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()), 199 }); err != nil { 200 return fmt.Errorf("unable to write tar to instance: %w", err) 201 } 202 return nil 203 } 204 205 // putBootstrap places the bootstrap version of go in the workdir 206 func putBootstrap(args []string) error { 207 fs := flag.NewFlagSet("putbootstrap", flag.ContinueOnError) 208 fs.Usage = func() { 209 fmt.Fprintln(os.Stderr, "putbootstrap usage: gomote putbootstrap [instance]") 210 fmt.Fprintln(os.Stderr) 211 fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.") 212 fs.PrintDefaults() 213 os.Exit(1) 214 } 215 fs.Parse(args) 216 217 var putSet []string 218 switch fs.NArg() { 219 case 0: 220 if activeGroup == nil { 221 fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument") 222 fs.Usage() 223 } 224 for _, inst := range activeGroup.Instances { 225 putSet = append(putSet, inst) 226 } 227 case 1: 228 putSet = []string{fs.Arg(0)} 229 default: 230 fmt.Fprintln(os.Stderr, "error: too many arguments") 231 fs.Usage() 232 } 233 234 eg, ctx := errgroup.WithContext(context.Background()) 235 for _, inst := range putSet { 236 inst := inst 237 eg.Go(func() error { 238 // TODO(66635) remove once gomotes can no longer be created via the coordinator. 239 if luciDisabled() { 240 client := gomoteServerClient(ctx) 241 resp, err := client.AddBootstrap(ctx, &protos.AddBootstrapRequest{ 242 GomoteId: inst, 243 }) 244 if err != nil { 245 return fmt.Errorf("unable to add bootstrap version of Go to instance: %w", err) 246 } 247 if resp.GetBootstrapGoUrl() == "" { 248 fmt.Printf("No GoBootstrapURL defined for %q; ignoring. (may be baked into image)\n", inst) 249 } 250 } 251 return nil 252 }) 253 } 254 return eg.Wait() 255 } 256 257 // put single file 258 func put(args []string) error { 259 fs := flag.NewFlagSet("put", flag.ContinueOnError) 260 fs.Usage = func() { 261 fmt.Fprintln(os.Stderr, "put usage: gomote put [put-opts] [instance] <source or '-' for stdin> [destination]") 262 fmt.Fprintln(os.Stderr) 263 fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.") 264 fs.PrintDefaults() 265 os.Exit(1) 266 } 267 modeStr := fs.String("mode", "", "Unix file mode (octal); default to source file mode") 268 fs.Parse(args) 269 270 if fs.NArg() == 0 { 271 fs.Usage() 272 } 273 274 ctx := context.Background() 275 var putSet []string 276 var src, dst string 277 if err := doPing(ctx, fs.Arg(0)); instanceDoesNotExist(err) { 278 // When there's no active group, this is just an error. 279 if activeGroup == nil { 280 return fmt.Errorf("instance %q: %w", fs.Arg(0), err) 281 } 282 // When there is an active group, this just means that we're going 283 // to use the group instead and assume the rest is a command. 284 for _, inst := range activeGroup.Instances { 285 putSet = append(putSet, inst) 286 } 287 src = fs.Arg(0) 288 if fs.NArg() == 2 { 289 dst = fs.Arg(1) 290 } else if fs.NArg() != 1 { 291 fmt.Fprintln(os.Stderr, "error: too many arguments") 292 fs.Usage() 293 } 294 } else if err == nil { 295 putSet = append(putSet, fs.Arg(0)) 296 if fs.NArg() == 1 { 297 fmt.Fprintln(os.Stderr, "error: missing source") 298 fs.Usage() 299 } 300 src = fs.Arg(1) 301 if fs.NArg() == 3 { 302 dst = fs.Arg(2) 303 } else if fs.NArg() != 2 { 304 fmt.Fprintln(os.Stderr, "error: too many arguments") 305 fs.Usage() 306 } 307 } else { 308 return fmt.Errorf("checking instance %q: %w", fs.Arg(0), err) 309 } 310 if dst == "" { 311 if src == "-" { 312 return errors.New("must specify destination file name when source is standard input") 313 } 314 dst = filepath.Base(src) 315 } 316 317 var mode os.FileMode = 0666 318 if *modeStr != "" { 319 modeInt, err := strconv.ParseInt(*modeStr, 8, 64) 320 if err != nil { 321 return err 322 } 323 mode = os.FileMode(modeInt) 324 if !mode.IsRegular() { 325 return fmt.Errorf("bad mode: %v", mode) 326 } 327 } 328 329 var putFileFn func(context.Context, string) error 330 if src == "-" { 331 var buf bytes.Buffer 332 _, err := io.Copy(&buf, os.Stdin) 333 if err != nil { 334 return fmt.Errorf("reading from stdin: %w", err) 335 } 336 sharedFileBuf := buf.Bytes() 337 putFileFn = func(ctx context.Context, inst string) error { 338 return doPutFile(ctx, inst, bytes.NewReader(sharedFileBuf), dst, mode) 339 } 340 } else { 341 putFileFn = func(ctx context.Context, inst string) error { 342 f, err := os.Open(src) 343 if err != nil { 344 return err 345 } 346 defer f.Close() 347 348 if *modeStr == "" { 349 fi, err := f.Stat() 350 if err != nil { 351 return err 352 } 353 mode = fi.Mode() 354 } 355 return doPutFile(ctx, inst, f, dst, mode) 356 } 357 } 358 359 eg, ctx := errgroup.WithContext(ctx) 360 for _, inst := range putSet { 361 inst := inst 362 eg.Go(func() error { 363 return putFileFn(ctx, inst) 364 }) 365 } 366 return eg.Wait() 367 } 368 369 func doPutFile(ctx context.Context, inst string, r io.Reader, dst string, mode os.FileMode) error { 370 client := gomoteServerClient(ctx) 371 resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{}) 372 if err != nil { 373 return fmt.Errorf("unable to request credentials for a file upload: %w", err) 374 } 375 err = uploadToGCS(ctx, resp.GetFields(), r, dst, resp.GetUrl()) 376 if err != nil { 377 return fmt.Errorf("unable to upload file to GCS: %w", err) 378 } 379 _, err = client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{ 380 GomoteId: inst, 381 Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()), 382 Filename: dst, 383 Mode: uint32(mode), 384 }) 385 if err != nil { 386 return fmt.Errorf("unable to write the file from URL: %w", err) 387 } 388 return nil 389 } 390 391 func uploadToGCS(ctx context.Context, fields map[string]string, file io.Reader, filename, url string) error { 392 buf := new(bytes.Buffer) 393 mw := multipart.NewWriter(buf) 394 395 for k, v := range fields { 396 if err := mw.WriteField(k, v); err != nil { 397 return fmt.Errorf("unable to write field: %w", err) 398 } 399 } 400 _, err := mw.CreateFormFile("file", filename) 401 if err != nil { 402 return fmt.Errorf("unable to create form file: %w", err) 403 } 404 // Write our own boundary to avoid buffering entire file into the multipart Writer 405 bound := fmt.Sprintf("\r\n--%s--\r\n", mw.Boundary()) 406 req, err := http.NewRequestWithContext(ctx, "POST", url, io.NopCloser(io.MultiReader(buf, file, strings.NewReader(bound)))) 407 if err != nil { 408 return fmt.Errorf("unable to create request: %w", err) 409 } 410 req.Header.Set("Content-Type", mw.FormDataContentType()) 411 res, err := http.DefaultClient.Do(req) 412 if err != nil { 413 return fmt.Errorf("http request failed: %w", err) 414 } 415 if res.StatusCode != http.StatusNoContent { 416 return fmt.Errorf("http post failed: status code=%d", res.StatusCode) 417 } 418 return nil 419 }