blob: d09e8a7dbfa6ff8c243e3f4c5a93b871af07edce [file]
// Copyright 2016 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package main
import (
"fmt"
"sort"
"github.com/golang/protobuf/ptypes/duration"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/proto/google"
"github.com/luci/luci-go/deploytool/api/deploy"
"golang.org/x/net/context"
"gopkg.in/yaml.v2"
)
var errKubeResourceNotFound = errors.New("resource not found")
// kubeDeployment defines a [v1beta1.Deployment].
type kubeDeployment struct {
Kind string `yaml:"kind"`
APIVersion string `yaml:"apiVersion"`
Metadata *kubeObjectMeta `yaml:"metadata,omitempty"`
Spec *kubeDeploymentSpec `yaml:"spec,omitempty"`
Status *kubeDeploymentStatus `yaml:"status,omitempty"`
}
// kubeDeploymentSpec defines a [v1beta1.DeploymentSpec].
type kubeDeploymentSpec struct {
Replicas int `yaml:"replicas,omitempty"`
Selector *kubeLabelSelector `yaml:"selector,omitempty"`
Template *kubePodTemplateSpec `yaml:"template"`
MinReadySeconds int `yaml:"minReadySeconds,omitempty"`
RevisionHistoryLimit int `yaml:"revisionHistoryLimit,omitempty"`
Paused bool `yaml:"paused,omitempty"`
}
// kubeDeploymentSpec defines a [v1beta1.DeploymentStatus].
type kubeDeploymentStatus struct {
ObservedGeneration int `yaml:"observedGeneration"`
Replicas int `yaml:"replicas"`
UpdatedReplicas int `yaml:"updatedReplicas"`
AvailableRepicas int `yaml:"availableReplicas"`
UnavailableReplicas int `yaml:"unavailableReplicas"`
}
// kubePodTemplateSpec defines a [v1.PodTemplateSpec].
type kubePodTemplateSpec struct {
Metadata *kubeObjectMeta `yaml:"metadata,omitempty"`
Spec *kubePodSpec `yaml:"spec,omitempty"`
}
// kubePodSpec defines a [v1.PodSpec].
type kubePodSpec struct {
Containers []*kubeContainer `yaml:"containers"`
RestartPolicy string `yaml:"restartPolicy,omitempty"`
TerminationGracePeriodSeconds int `yaml:"terminationGracePeriodSeconds,omitempty"`
ActiveDeadlineSeconds int `yaml:"activeDeadlineSeconds,omitempty"`
}
// kubeContainer defines a [v1.Container].
type kubeContainer struct {
Name string `yaml:"name"`
Image string `yaml:"image,omitempty"`
Command []string `yaml:"command,omitempty"`
Args []string `yaml:"args,omitempty"`
WorkingDir string `yaml:"workingDir,omitempty"`
Ports []*kubeContainerPort `yaml:"ports,omitempty"`
Env []*kubeEnvVar `yaml:"env,omitempty"`
Resources *kubeResourceRequirements `yaml:"resources,omitempty"`
LivenessProbe *kubeProbe `yaml:"livenessProbe,omitempty"`
ReadinessProbe *kubeProbe `yaml:"readinessProbe,omitempty"`
Lifecycle *kubeLifecycle `yaml:"lifecycle,omitempty"`
}
// kubeContainerPort defines a [v1.ContainerPort].
type kubeContainerPort struct {
Name string `yaml:"name,omitempty"`
HostPort int `yaml:"hostPort,omitempty"`
ContainerPort int `yaml:"containerPort"`
Protocol string `yaml:"protocol,omitempty"`
HostIP string `yaml:"hostIP,omitempty"`
}
// kubeEnvVar defines a [v1.EnvVar].
type kubeEnvVar struct {
Name string `yaml:"name"`
Value string `yaml:"name,omitempty"`
ValueFrom *kubeEnvVarSource `yaml:"valueFrom,omitempty"`
}
// kubeEnvVarSource represents a [v1.EnvVarSource].
type kubeEnvVarSource struct {
FieldRef *kubeObjectFieldSelector
ConfigMapKeyRef *kubeConfigMapKeySelector `yaml:"configMapKeyRef,omitempty"`
SecretKeyRef *kubeSecretKeySelector `yaml:"secretKeyRef,omitempty"`
}
// kubeLabelSelector represents a [v1beta1.LabelSelector].
type kubeLabelSelector struct {
MatchLabels map[string]interface{} `yaml:"matchLabels,omitempty"`
MatchExpressions *kubeLabelSelectorRequirement `yaml:"matchExpressions,omitempty"`
}
// kubeLabelSelectorRequirement represets a [v1beta1.LabelSelectorRequirement].
type kubeLabelSelectorRequirement struct {
Key string `yaml:"key"`
Operator string `yaml:"operator"`
Values []string `yaml:"values,omitempty"`
}
// kubeObjectFieldSelector defines a [v1.ObjectFieldSelector].
type kubeObjectFieldSelector struct {
APIVersion string `yaml:"apiVersion,omitempty"`
FieldPath string `yaml:"fieldPath"`
}
// kubeConfigMapKeySelector defines a [v1.ConfigMapKeySelector].
type kubeConfigMapKeySelector struct {
Name string `yaml:"name,omitempty"`
Key string `yaml:"key"`
}
// kubeSecretKeySelector defines a [v1.SecretKeySelector].
type kubeSecretKeySelector struct {
Name string `yaml:"name,omitempty"`
Key string `yaml:"key"`
}
// kubeResourceRequirements defines a [v1.ResourceRequirements].
type kubeResourceRequirements struct {
Limits map[string]interface{} `yaml:"limits,omitempty"`
Requests map[string]interface{} `yaml:"requests,omitempty"`
}
// kubeHTTPGetAction defines a [v1.HTTPGetAction].
type kubeHTTPGetAction struct {
Path string `yaml:"path,omitempty"`
Port int `yaml:"port"`
Host string `yaml:"host,omitempty"`
Scheme string `yaml:"scheme,omitempty"`
HTTPHeaders []*kubeHTTPHeader `yaml:"httpHeaders,omitempty"`
}
// kubeHTTPHeader defines a [v1.HTTPHeader].
type kubeHTTPHeader struct {
Key string `yaml:"key"`
Value string `yaml:"value,omitempty"`
}
// kubeProbe defines a [v1.Probe]
type kubeProbe struct {
// Exec defines a [v1.ExecAction].
Exec *kubeExecAction
HTTPGet *kubeHTTPGetAction `yaml:"httpGet,omitempty"`
InitialDelaySeconds int `yaml:"initialDelaySeconds,omitempty"`
TimeoutSeconds int `yaml:"timeoutSeconds,omitempty"`
PeriodSeconds int `yaml:"periodSeconds,omitempty"`
SuccessThreshold int `yaml:"successThreshold,omitempty"`
FailureThreshold int `yaml:"failureThreshold,omitempty"`
}
// kubeExecAction defines a [v1.ExecAction].
type kubeExecAction struct {
Command []string `yaml:"command"`
}
// kubeLifecycle defines a [v1.Lifecycle].
type kubeLifecycle struct {
PostStart *kubeHandler `yaml:"postStart,omitempty"`
PreStop *kubeHandler `yaml:"preStop,omitempty"`
}
// kubeHandler defines a [v1.Handler].
type kubeHandler struct {
Exec *kubeExecAction `yaml:"exec,omitempty"`
HTTPGet *kubeHTTPGetAction `yaml:"httpGet,omitempty"`
}
// kubeObjectMeta defines a [v1.ObjectMeta].
type kubeObjectMeta struct {
Name string `yaml:"name,omitempty"`
GenerateName string `yaml:"generateName,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
SelfLink string `yaml:"selfLink,omitempty"`
UID string `yaml:"uid,omitempty"`
ResourceVersion string `yaml:"resourceVersion,omitempty"`
Generation string `yaml:"generation,omitempty"`
CreationTimestamp string `yaml:"creationTimestamp,omitempty"`
DeletionTimestamp string `yaml:"deletionTimestamp,omitempty"`
DeletionGracePeriodSeconds int `yaml:"deletionGracePeriodSeconds,omitempty"`
// Labels defines a series of [any] labels.
Labels map[string]interface{} `yaml:"labels,omitempty"`
// Annotations defines a series of [any] annotations.
Annotations map[string]interface{} `yaml:"annotations,omitempty"`
}
func (meta *kubeObjectMeta) addLabel(key string, value interface{}) {
if meta.Labels == nil {
meta.Labels = make(map[string]interface{})
}
meta.Labels[key] = value
}
func (meta *kubeObjectMeta) addAnnotation(key string, value interface{}) {
if meta.Annotations == nil {
meta.Annotations = make(map[string]interface{})
}
meta.Annotations[key] = value
}
func durationProtoToSec(p *duration.Duration) int {
return int(google.DurationFromProto(p).Seconds())
}
func kubeBuildDeploymentYAML(pb *layoutDeploymentGKEPodBinding, name string,
imageMap map[string]string) *kubeDeployment {
kp := pb.pod.KubePod
dep := kubeDeployment{
Kind: "Deployment",
APIVersion: "extensions/v1beta1",
Metadata: &kubeObjectMeta{
Name: name,
Labels: make(map[string]interface{}),
Annotations: make(map[string]interface{}),
},
Spec: &kubeDeploymentSpec{
Replicas: int(pb.Replicas),
Template: &kubePodTemplateSpec{
Metadata: &kubeObjectMeta{},
Spec: &kubePodSpec{
Containers: make([]*kubeContainer, len(kp.Container)),
RestartPolicy: kp.RestartPolicy.KubeString(),
TerminationGracePeriodSeconds: durationProtoToSec(kp.TerminationGracePeriod),
ActiveDeadlineSeconds: durationProtoToSec(kp.ActiveDeadline),
},
},
MinReadySeconds: durationProtoToSec(kp.MinReady),
},
}
tmpl := dep.Spec.Template
// Pod Template Metadata
for k, v := range kp.Labels {
tmpl.Metadata.Labels[k] = v
}
// Pod Template Containers
for i, kc := range kp.Container {
cont := kubeContainer{
Name: kc.Name,
Image: imageMap[kc.Name],
Command: kc.Command,
Args: kc.Args,
WorkingDir: kc.WorkingDir,
}
// Ports
if len(kc.Ports) > 0 {
cont.Ports = make([]*kubeContainerPort, len(kc.Ports))
for i, port := range kc.Ports {
cont.Ports[i] = &kubeContainerPort{
Name: port.Name,
ContainerPort: int(port.ContainerPort),
}
}
}
// Environment
if len(kc.Env) > 0 {
cont.Env = make([]*kubeEnvVar, 0, len(kc.Env))
for k, v := range kc.Env {
cont.Env = append(cont.Env, &kubeEnvVar{
Name: k,
Value: v,
})
}
sort.Sort(sortableEnvVarSlice(cont.Env))
}
// Resources
var res kubeResourceRequirements
for _, r := range []struct {
r *deploy.KubernetesPod_Container_Resources
p *map[string]interface{}
}{
{kc.Limits, &res.Limits},
{kc.Requested, &res.Requests},
} {
if r.r == nil {
continue
}
m := make(map[string]interface{}, 2)
if cpu := r.r.Cpu; cpu > 0 {
m["cpu"] = cpu
}
if mem := r.r.Memory; mem != nil {
m["memory"] = fmt.Sprintf("%d%s", mem.Amount, mem.Unit.KubeSuffix())
}
if len(m) > 0 {
// We have at least one resource value, so assign.
*r.p = m
cont.Resources = &res
}
}
// Probes
for _, p := range []struct {
v *deploy.KubernetesPod_Container_Probe
p **kubeProbe
}{
{kc.LivenessProbe, &cont.LivenessProbe},
{kc.ReadinessProbe, &cont.ReadinessProbe},
} {
if p.v == nil {
continue
}
probe := kubeProbe{
InitialDelaySeconds: durationProtoToSec(p.v.InitialDelay),
TimeoutSeconds: durationProtoToSec(p.v.Timeout),
PeriodSeconds: durationProtoToSec(p.v.Period),
SuccessThreshold: int(p.v.SuccessThreshold),
FailureThreshold: int(p.v.FailureThreshold),
}
if exec := p.v.Exec; len(exec) > 0 {
probe.Exec = &kubeExecAction{
Command: exec,
}
}
if hg := p.v.HttpGet; hg != nil {
probe.HTTPGet = kubeMakeHTTPGetAction(hg)
}
*p.p = &probe
}
// Handlers
var lc kubeLifecycle
for _, h := range []struct {
v *deploy.KubernetesPod_Container_Handler
p **kubeHandler
}{
{kc.PostStart, &lc.PostStart},
{kc.PreStop, &lc.PreStop},
} {
if h.v == nil {
continue
}
var handler kubeHandler
if exec := h.v.ExecCommand; len(exec) > 0 {
handler.Exec = &kubeExecAction{
Command: exec,
}
}
if hg := h.v.HttpGet; hg != nil {
handler.HTTPGet = kubeMakeHTTPGetAction(hg)
}
// We have an entry here, so assign.
*h.p = &handler
cont.Lifecycle = &lc
}
tmpl.Spec.Containers[i] = &cont
}
return &dep
}
func kubeMakeHTTPGetAction(hg *deploy.KubernetesPod_Container_HttpGet) *kubeHTTPGetAction {
act := kubeHTTPGetAction{
Path: hg.Path,
Port: int(hg.Port),
Host: hg.Host,
Scheme: hg.Scheme,
}
if len(hg.Headers) > 0 {
act.HTTPHeaders = make([]*kubeHTTPHeader, len(hg.Headers))
for i, hdr := range hg.Headers {
act.HTTPHeaders[i] = &kubeHTTPHeader{
Key: hdr.Name,
Value: hdr.Value,
}
}
}
return &act
}
type sortableEnvVarSlice []*kubeEnvVar
func (s sortableEnvVarSlice) Len() int { return len(s) }
func (s sortableEnvVarSlice) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s sortableEnvVarSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// kubeTool wraps the "kubectl" tool.
type kubeTool struct {
exe string
ctx string
}
func (t *kubeTool) hasContext(c context.Context) (bool, error) {
x := execute(t.exe, "config", "view",
"--output", fmt.Sprintf(`jsonpath='{.users[?(@.name == %q)].name}'`, t.ctx))
if err := x.check(c); err != nil {
return false, err
}
return (x.stdout.String() != "''"), nil
}
func (t *kubeTool) exec(commands ...string) *workExecutor {
args := make([]string, 0, 2+len(commands))
args = append(args, "--context", t.ctx)
args = append(args, commands...)
return execute(t.exe, args...)
}
func (t *kubeTool) getResource(c context.Context, resource string, obj interface{}) error {
x := t.exec("get", resource, "-o", "yaml")
switch rv, err := x.run(c); {
case err != nil:
return err
case rv != 0:
return errKubeResourceNotFound
default:
if err := yaml.Unmarshal(x.stdout.Bytes(), obj); err != nil {
return errors.Annotate(err).Reason("failed to unmarshal YAML %(type)T").D("type", obj).Err()
}
return nil
}
}