golang.org/x/build@v0.0.0-20240506185731-218518f32b70/buildlet/buildlet.go (about)

     1  // Copyright 2020 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 buildlet
     6  
     7  import (
     8  	"context"
     9  	"crypto/tls"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"net/http"
    14  	"time"
    15  
    16  	"golang.org/x/build/internal/cloud"
    17  	"google.golang.org/api/compute/v1"
    18  )
    19  
    20  // VMOpts control how new VMs are started.
    21  type VMOpts struct {
    22  	// Zone is the GCE zone to create the VM in.
    23  	// Optional; defaults to provided build environment's zone.
    24  	Zone string
    25  
    26  	// ProjectID is the GCE project ID (e.g. "foo-bar-123", not
    27  	// the numeric ID).
    28  	// Optional; defaults to provided build environment's project ID ("name").
    29  	ProjectID string
    30  
    31  	// TLS optionally specifies the TLS keypair to use.
    32  	// If zero, http without auth is used.
    33  	TLS KeyPair
    34  
    35  	// Optional description of the VM.
    36  	Description string
    37  
    38  	// Optional metadata to put on the instance.
    39  	Meta map[string]string
    40  
    41  	// DeleteIn optionally specifies a duration at which
    42  	// to delete the VM.
    43  	// If zero, a short default is used (not enough for longtest builders).
    44  	// Negative means no deletion timeout.
    45  	DeleteIn time.Duration
    46  
    47  	// OnInstanceRequested optionally specifies a hook to run synchronously
    48  	// after the computeService.Instances.Insert call, but before
    49  	// waiting for its operation to proceed.
    50  	OnInstanceRequested func()
    51  
    52  	// OnInstanceCreated optionally specifies a hook to run synchronously
    53  	// after the instance operation succeeds.
    54  	OnInstanceCreated func()
    55  
    56  	// OnInstanceCreated optionally specifies a hook to run synchronously
    57  	// after the computeService.Instances.Get call.
    58  	// Only valid for GCE resources.
    59  	OnGotInstanceInfo func(*compute.Instance)
    60  
    61  	// OnInstanceCreated optionally specifies a hook to run synchronously
    62  	// after the EC2 instance information is retrieved.
    63  	// Only valid for EC2 resources.
    64  	OnGotEC2InstanceInfo func(*cloud.Instance)
    65  
    66  	// OnBeginBuildletProbe optionally specifies a hook to run synchronously
    67  	// before StartNewVM tries to hit buildletURL to see if it's up yet.
    68  	OnBeginBuildletProbe func(buildletURL string)
    69  
    70  	// OnEndBuildletProbe optionally specifies a hook to run synchronously
    71  	// after StartNewVM tries to hit the buildlet's URL to see if it's up.
    72  	// The hook parameters are the return values from http.Get.
    73  	OnEndBuildletProbe func(*http.Response, error)
    74  
    75  	// SkipEndpointVerification does not verify that the builder is listening
    76  	// on port 80 or 443 before creating a buildlet client.
    77  	SkipEndpointVerification bool
    78  
    79  	// UseIAPTunnel uses an IAP tunnel to connect to buildlets on GCP.
    80  	UseIAPTunnel bool
    81  
    82  	// DiskSizeGB specifies the size of the boot disk in base-2 GB. The default
    83  	// disk size is used if unset.
    84  	// Only valid for GCE resources.
    85  	DiskSizeGB int64
    86  }
    87  
    88  // buildletClient returns a buildlet client configured to speak to a VM via the buildlet
    89  // URL. The communication will use TLS if one is provided in the vmopts. This will wait until
    90  // it can connect with the endpoint before returning. The buildletURL is in the form of:
    91  // "https://<ip>". The ipPort field is in the form of "<ip>:<port>". The function
    92  // will attempt to connect to the buildlet for the lesser of: the default timeout period
    93  // (10 minutes) or the timeout set in the passed in context.
    94  func buildletClient(ctx context.Context, buildletURL, ipPort string, opts *VMOpts) (Client, error) {
    95  	ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
    96  	defer cancel()
    97  	try := 0
    98  	for !opts.SkipEndpointVerification {
    99  		try++
   100  		if ctx.Err() != nil {
   101  			return nil, fmt.Errorf("unable to probe buildet at %s after %d attempts", buildletURL, try)
   102  		}
   103  		err := probeBuildlet(ctx, buildletURL, opts)
   104  		if err == nil {
   105  			break
   106  		}
   107  		log.Printf("probing buildlet at %s with attempt %d failed: %s", buildletURL, try, err)
   108  		time.Sleep(3 * time.Second)
   109  	}
   110  	return NewClient(ipPort, opts.TLS), nil
   111  }
   112  
   113  // probeBuildlet attempts to the connect to a buildlet at the provided URL. An error
   114  // is returned if it unable to connect to the buildlet. Each request is limited by either
   115  // a five second limit or the timeout set in the context.
   116  func probeBuildlet(ctx context.Context, buildletURL string, opts *VMOpts) error {
   117  	cl := &http.Client{
   118  		Transport: &http.Transport{
   119  			Dial:              defaultDialer(),
   120  			DisableKeepAlives: true,
   121  			TLSClientConfig: &tls.Config{
   122  				InsecureSkipVerify: true,
   123  			},
   124  		},
   125  	}
   126  	if fn := opts.OnBeginBuildletProbe; fn != nil {
   127  		fn(buildletURL)
   128  	}
   129  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   130  	defer cancel()
   131  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, buildletURL, nil)
   132  	if err != nil {
   133  		return fmt.Errorf("error creating buildlet probe request: %w", err)
   134  	}
   135  	res, err := cl.Do(req)
   136  	if fn := opts.OnEndBuildletProbe; fn != nil {
   137  		fn(res, err)
   138  	}
   139  	if err != nil {
   140  		return fmt.Errorf("error probe buildlet %s: %w", buildletURL, err)
   141  	}
   142  	io.ReadAll(res.Body)
   143  	res.Body.Close()
   144  	if res.StatusCode != http.StatusOK {
   145  		return fmt.Errorf("buildlet returned HTTP status code %d for %s", res.StatusCode, buildletURL)
   146  	}
   147  	return nil
   148  }