blob: 8f9d1efff4a4d1f1927f52329d868e4ec31ee082 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package buildlet
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"strings"
"time"
"golang.org/x/build/buildenv"
"golang.org/x/build/dashboard"
"golang.org/x/build/kubernetes"
"golang.org/x/build/kubernetes/api"
"golang.org/x/net/context/ctxhttp"
)
var (
// TODO(evanbrown): resource requirements should be
// defined per-builder in dashboard/builders.go
BuildletCPU = api.MustParse("2") // 2 Cores
BuildletCPULimit = api.MustParse("8") // 8 Cores
BuildletMemory = api.MustParse("4000000Ki") // 4,000,000Ki RAM
)
// PodOpts control how new pods are started.
type PodOpts struct {
// ProjectID is the GCE project ID. Required.
ProjectID string
// ImageRegistry specifies the Docker registry Kubernetes
// will use to create the pod. Required.
ImageRegistry string
// TLS optionally specifies the TLS keypair to use.
// If zero, http without auth is used.
TLS KeyPair
// Description optionally describes the pod.
Description string
// Labels optionally specify key=value strings that Kubernetes
// can use to filter and group pods.
Labels map[string]string
// DeleteIn optionally specifies a duration at which
// to delete the pod.
DeleteIn time.Duration
// OnPodCreating optionally specifies a hook to run synchronously
// after the pod create request has been made, but before the create
// has succeeded.
OnPodCreating func()
// OnPodCreated optionally specifies a hook to run synchronously
// after the pod create request succeeds.
OnPodCreated func()
// OnGotPodInfo optionally specifies a hook to run synchronously
// after the pod Get call.
OnGotPodInfo func()
}
// StartPod creates a new pod on a Kubernetes cluster and returns a buildlet client
// configured to speak to it.
func StartPod(ctx context.Context, kubeClient *kubernetes.Client, podName, hostType string, opts PodOpts) (*Client, error) {
conf, ok := dashboard.Hosts[hostType]
if !ok || conf.ContainerImage == "" {
return nil, fmt.Errorf("invalid builder type %q", hostType)
}
pod := &api.Pod{
TypeMeta: api.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: api.ObjectMeta{
Name: podName,
Labels: map[string]string{
"name": podName,
"type": hostType,
"role": "buildlet",
},
Annotations: map[string]string{},
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyNever,
Containers: []api.Container{
{
Name: "buildlet",
Image: imageID(opts.ImageRegistry, conf.ContainerImage),
ImagePullPolicy: api.PullAlways,
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceCPU: BuildletCPU,
api.ResourceMemory: BuildletMemory,
},
Limits: api.ResourceList{
api.ResourceCPU: BuildletCPULimit,
api.ResourceMemory: BuildletMemory,
},
},
Command: []string{"/usr/local/bin/stage0"},
Ports: []api.ContainerPort{
{
ContainerPort: 80,
},
},
Env: []api.EnvVar{},
},
},
},
}
addEnv := func(name, value string) {
for i, _ := range pod.Spec.Containers {
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, api.EnvVar{
Name: name,
Value: value,
})
}
}
// The buildlet-binary-url is the URL of the buildlet binary
// which the pods are configured to download at boot and run.
// This lets us/ update the buildlet more easily than
// rebuilding the whole pod image.
addEnv("META_BUILDLET_BINARY_URL", conf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
addEnv("META_BUILDLET_HOST_TYPE", hostType)
if !opts.TLS.IsZero() {
addEnv("META_TLS_CERT", opts.TLS.CertPEM)
addEnv("META_TLS_KEY", opts.TLS.KeyPEM)
addEnv("META_PASSWORD", opts.TLS.Password())
}
if opts.DeleteIn != 0 {
// In case the pod gets away from us (generally: if the
// coordinator dies while a build is running), then we
// set this annotation of when it should be killed so
// we can kill it later when the coordinator is
// restarted. The cleanUpOldPods goroutine loop handles
// that killing.
pod.ObjectMeta.Annotations["delete-at"] = fmt.Sprint(time.Now().Add(opts.DeleteIn).Unix())
}
condRun(opts.OnPodCreating)
podStatus, err := kubeClient.RunLongLivedPod(ctx, pod)
if err != nil {
return nil, err
}
// The new pod must be in Running phase. Possible phases are described at
// http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#pod-phase
if podStatus.Phase != api.PodRunning {
return nil, fmt.Errorf("pod is in invalid state %q: %v", podStatus.Phase, podStatus.Message)
}
condRun(opts.OnPodCreated)
// Wait for the pod to boot and its buildlet to come up.
var buildletURL string
var ipPort string
if !opts.TLS.IsZero() {
buildletURL = "https://" + podStatus.PodIP
ipPort = podStatus.PodIP + ":443"
} else {
buildletURL = "http://" + podStatus.PodIP
ipPort = podStatus.PodIP + ":80"
}
condRun(opts.OnGotPodInfo)
impatientClient := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
Dial: defaultDialer(),
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
c := make(chan error, 1)
go func() {
defer close(c)
try := 0
for {
try++
// Make sure pod is still running
podStatus, err := kubeClient.PodStatus(ctx, pod.Name)
if err != nil {
c <- fmt.Errorf("polling the buildlet pod for its status failed: %v", err)
return
}
if podStatus.Phase != api.PodRunning {
podLog, err := kubeClient.PodLog(ctx, pod.Name)
if err != nil {
log.Printf("failed to retrieve log for pod %q: %v", pod.Name, err)
c <- fmt.Errorf("buildlet pod left the Running phase and entered phase %q", podStatus.Phase)
return
}
log.Printf("log from pod %q: %v", pod.Name, podLog)
c <- fmt.Errorf("buildlet pod left the Running phase and entered phase %q", podStatus.Phase)
return
}
res, err := ctxhttp.Get(ctx, impatientClient, buildletURL)
if err != nil {
time.Sleep(1 * time.Second)
continue
}
res.Body.Close()
if res.StatusCode != 200 {
c <- fmt.Errorf("buildlet returned HTTP status code %d on try number %d", res.StatusCode, try)
}
return
}
}()
// Wait for the buildlet to respond to an HTTP request. If the timeout happens first, or
// if the buildlet pod leaves the running state, return an error.
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case err = <-c:
if err != nil {
return nil, err
}
return NewClient(ipPort, opts.TLS), nil
}
}
}
func imageID(registry, image string) string {
// Sanitize the registry and image names
registry = strings.TrimRight(registry, "/")
image = strings.TrimLeft(image, "/")
return registry + "/" + image
}