github.com/docker/compose-on-kubernetes@v0.5.0/internal/convert/pod.go (about)

     1  package convert
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/docker/compose-on-kubernetes/api/compose/latest"
    10  	"github.com/docker/docker/api/types/swarm"
    11  	"github.com/pkg/errors"
    12  	apiv1 "k8s.io/api/core/v1"
    13  	"k8s.io/apimachinery/pkg/api/resource"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  )
    16  
    17  func toPodTemplate(serviceConfig latest.ServiceConfig, labels map[string]string, configuration *latest.StackSpec, original apiv1.PodTemplateSpec) (apiv1.PodTemplateSpec, error) {
    18  	tpl := *original.DeepCopy()
    19  	nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy.Placement.Constraints)
    20  	if err != nil {
    21  		return apiv1.PodTemplateSpec{}, err
    22  	}
    23  	hostAliases, err := toHostAliases(serviceConfig.ExtraHosts)
    24  	if err != nil {
    25  		return apiv1.PodTemplateSpec{}, err
    26  	}
    27  	env, err := toEnv(serviceConfig.Environment)
    28  	if err != nil {
    29  		return apiv1.PodTemplateSpec{}, err
    30  	}
    31  	restartPolicy, err := toRestartPolicy(serviceConfig, tpl.Spec.RestartPolicy)
    32  	if err != nil {
    33  		return apiv1.PodTemplateSpec{}, err
    34  	}
    35  	limits, err := toResource(serviceConfig.Deploy.Resources.Limits)
    36  	if err != nil {
    37  		return apiv1.PodTemplateSpec{}, err
    38  	}
    39  	requests, err := toResource(serviceConfig.Deploy.Resources.Reservations)
    40  	if err != nil {
    41  		return apiv1.PodTemplateSpec{}, err
    42  	}
    43  	volumes, err := toVolumes(serviceConfig, configuration)
    44  	if err != nil {
    45  		return apiv1.PodTemplateSpec{}, err
    46  	}
    47  	volumeMounts, err := toVolumeMounts(serviceConfig, configuration)
    48  	if err != nil {
    49  		return apiv1.PodTemplateSpec{}, err
    50  	}
    51  	pullPolicy, err := toImagePullPolicy(serviceConfig.Image, serviceConfig.PullPolicy)
    52  	if err != nil {
    53  		return apiv1.PodTemplateSpec{}, err
    54  	}
    55  	tpl.ObjectMeta = metav1.ObjectMeta{
    56  		Labels:      labels,
    57  		Annotations: serviceConfig.Labels,
    58  	}
    59  	tpl.Spec.RestartPolicy = restartPolicy
    60  	tpl.Spec.Volumes = volumes
    61  	tpl.Spec.HostPID = toHostPID(serviceConfig.Pid)
    62  	tpl.Spec.HostIPC = toHostIPC(serviceConfig.Ipc)
    63  	tpl.Spec.Hostname = serviceConfig.Hostname
    64  	tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod, tpl.Spec.TerminationGracePeriodSeconds)
    65  	tpl.Spec.HostAliases = hostAliases
    66  	tpl.Spec.Affinity = nodeAffinity
    67  	// we dont want to remove all containers and recreate them because:
    68  	// an admission plugin can add sidecar containers
    69  	// we for sure want to keep the main container to be additive
    70  	if len(tpl.Spec.Containers) == 0 {
    71  		tpl.Spec.Containers = []apiv1.Container{{}}
    72  	}
    73  
    74  	containerIX := 0
    75  	for ix, c := range tpl.Spec.Containers {
    76  		if c.Name == serviceConfig.Name {
    77  			containerIX = ix
    78  			break
    79  		}
    80  	}
    81  	tpl.Spec.Containers[containerIX].Name = serviceConfig.Name
    82  	tpl.Spec.Containers[containerIX].Image = serviceConfig.Image
    83  	tpl.Spec.Containers[containerIX].ImagePullPolicy = pullPolicy
    84  	tpl.Spec.Containers[containerIX].Command = serviceConfig.Entrypoint
    85  	tpl.Spec.Containers[containerIX].Args = serviceConfig.Command
    86  	tpl.Spec.Containers[containerIX].WorkingDir = serviceConfig.WorkingDir
    87  	tpl.Spec.Containers[containerIX].TTY = serviceConfig.Tty
    88  	tpl.Spec.Containers[containerIX].Stdin = serviceConfig.StdinOpen
    89  	tpl.Spec.Containers[containerIX].Ports = toPorts(serviceConfig.Ports)
    90  	tpl.Spec.Containers[containerIX].LivenessProbe = toLivenessProbe(serviceConfig.HealthCheck)
    91  	tpl.Spec.Containers[containerIX].Env = env
    92  	tpl.Spec.Containers[containerIX].VolumeMounts = volumeMounts
    93  	tpl.Spec.Containers[containerIX].SecurityContext = toSecurityContext(serviceConfig)
    94  	tpl.Spec.Containers[containerIX].Resources = apiv1.ResourceRequirements{
    95  		Limits:   limits,
    96  		Requests: requests,
    97  	}
    98  
    99  	if serviceConfig.PullSecret != "" {
   100  		pullSecrets := map[string]struct{}{}
   101  		for _, ps := range tpl.Spec.ImagePullSecrets {
   102  			pullSecrets[ps.Name] = struct{}{}
   103  		}
   104  		if _, ok := pullSecrets[serviceConfig.PullSecret]; !ok {
   105  			tpl.Spec.ImagePullSecrets = append(tpl.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: serviceConfig.PullSecret})
   106  		}
   107  	}
   108  	return tpl, nil
   109  }
   110  
   111  func toImagePullPolicy(image string, specifiedPolicy string) (apiv1.PullPolicy, error) {
   112  	if specifiedPolicy == "" {
   113  		if strings.HasSuffix(image, ":latest") {
   114  			return apiv1.PullAlways, nil
   115  		}
   116  		return apiv1.PullIfNotPresent, nil
   117  	}
   118  	switch apiv1.PullPolicy(specifiedPolicy) {
   119  	case apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever:
   120  		return apiv1.PullPolicy(specifiedPolicy), nil
   121  	default:
   122  		return "", errors.Errorf("invalid pull policy %q, must be %q, %q or %q", specifiedPolicy, apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever)
   123  	}
   124  }
   125  
   126  func toHostAliases(extraHosts []string) ([]apiv1.HostAlias, error) {
   127  	if extraHosts == nil {
   128  		return nil, nil
   129  	}
   130  
   131  	byHostnames := map[string]string{}
   132  	for _, host := range extraHosts {
   133  		split := strings.SplitN(host, ":", 2)
   134  		if len(split) != 2 {
   135  			return nil, errors.Errorf("malformed host %s", host)
   136  		}
   137  		byHostnames[split[0]] = split[1]
   138  	}
   139  
   140  	byIPs := map[string][]string{}
   141  	for k, v := range byHostnames {
   142  		byIPs[v] = append(byIPs[v], k)
   143  	}
   144  
   145  	aliases := make([]apiv1.HostAlias, len(byIPs))
   146  	i := 0
   147  	for key, hosts := range byIPs {
   148  		sort.Strings(hosts)
   149  		aliases[i] = apiv1.HostAlias{
   150  			IP:        key,
   151  			Hostnames: hosts,
   152  		}
   153  		i++
   154  	}
   155  	sort.Slice(aliases, func(i, j int) bool { return aliases[i].IP < aliases[j].IP })
   156  	return aliases, nil
   157  }
   158  
   159  func toHostPID(pid string) bool {
   160  	return "host" == pid
   161  }
   162  
   163  func toHostIPC(ipc string) bool {
   164  	return "host" == ipc
   165  }
   166  
   167  func toTerminationGracePeriodSeconds(duration *time.Duration, original *int64) *int64 {
   168  	if duration == nil {
   169  		return original
   170  	}
   171  	gracePeriod := int64(duration.Seconds())
   172  	return &gracePeriod
   173  }
   174  
   175  func toLivenessProbe(hc *latest.HealthCheckConfig) *apiv1.Probe {
   176  	if hc == nil || len(hc.Test) < 1 || hc.Test[0] == "NONE" {
   177  		return nil
   178  	}
   179  
   180  	command := hc.Test[1:]
   181  	if hc.Test[0] == "CMD-SHELL" {
   182  		command = append([]string{"sh", "-c"}, command...)
   183  	}
   184  
   185  	return &apiv1.Probe{
   186  		TimeoutSeconds:   toSecondsOrDefault(hc.Timeout, 1),
   187  		PeriodSeconds:    toSecondsOrDefault(hc.Interval, 1),
   188  		FailureThreshold: int32(defaultUint64(hc.Retries, 3)),
   189  		Handler: apiv1.Handler{
   190  			Exec: &apiv1.ExecAction{
   191  				Command: command,
   192  			},
   193  		},
   194  	}
   195  }
   196  
   197  func toEnv(env map[string]*string) ([]apiv1.EnvVar, error) {
   198  	var envVars []apiv1.EnvVar
   199  
   200  	for k, v := range env {
   201  		if v == nil {
   202  			return nil, errors.Errorf("%s has no value, unsetting an environment variable is not supported", k)
   203  		}
   204  		envVars = append(envVars, toEnvVar(k, *v))
   205  	}
   206  	sort.Slice(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name })
   207  	return envVars, nil
   208  }
   209  
   210  func toEnvVar(key, value string) apiv1.EnvVar {
   211  	return apiv1.EnvVar{
   212  		Name:  key,
   213  		Value: value,
   214  	}
   215  }
   216  
   217  func toPorts(list []latest.ServicePortConfig) []apiv1.ContainerPort {
   218  	var ports []apiv1.ContainerPort
   219  
   220  	for _, v := range list {
   221  		ports = append(ports, apiv1.ContainerPort{
   222  			ContainerPort: int32(v.Target),
   223  			Protocol:      toProtocol(v.Protocol),
   224  		})
   225  	}
   226  
   227  	return ports
   228  }
   229  
   230  func toProtocol(value string) apiv1.Protocol {
   231  	if value == "udp" {
   232  		return apiv1.ProtocolUDP
   233  	}
   234  	return apiv1.ProtocolTCP
   235  }
   236  
   237  func toRestartPolicy(s latest.ServiceConfig, original apiv1.RestartPolicy) (apiv1.RestartPolicy, error) {
   238  	policy := s.Deploy.RestartPolicy
   239  	if policy == nil {
   240  		return original, nil
   241  	}
   242  
   243  	switch policy.Condition {
   244  	case string(swarm.RestartPolicyConditionAny):
   245  		return apiv1.RestartPolicyAlways, nil
   246  	case string(swarm.RestartPolicyConditionNone):
   247  		return apiv1.RestartPolicyNever, nil
   248  	case string(swarm.RestartPolicyConditionOnFailure):
   249  		return apiv1.RestartPolicyOnFailure, nil
   250  	default:
   251  		return "", errors.Errorf("unsupported restart policy %s", policy.Condition)
   252  	}
   253  }
   254  
   255  func toResource(res *latest.Resource) (apiv1.ResourceList, error) {
   256  	if res == nil {
   257  		return nil, nil
   258  	}
   259  	list := make(apiv1.ResourceList)
   260  	if res.NanoCPUs != "" {
   261  		cpus, err := resource.ParseQuantity(res.NanoCPUs)
   262  		if err != nil {
   263  			return nil, err
   264  		}
   265  		list[apiv1.ResourceCPU] = cpus
   266  	}
   267  	if res.MemoryBytes != 0 {
   268  		memory, err := resource.ParseQuantity(fmt.Sprintf("%v", res.MemoryBytes))
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  		list[apiv1.ResourceMemory] = memory
   273  	}
   274  	return list, nil
   275  }
   276  
   277  func toSecurityContext(s latest.ServiceConfig) *apiv1.SecurityContext {
   278  	isPrivileged := toBoolPointer(s.Privileged)
   279  	isReadOnly := toBoolPointer(s.ReadOnly)
   280  
   281  	var capabilities *apiv1.Capabilities
   282  	if s.CapAdd != nil || s.CapDrop != nil {
   283  		capabilities = &apiv1.Capabilities{
   284  			Add:  toCapabilities(s.CapAdd),
   285  			Drop: toCapabilities(s.CapDrop),
   286  		}
   287  	}
   288  
   289  	if isPrivileged == nil && isReadOnly == nil && capabilities == nil && s.User == nil {
   290  		return nil
   291  	}
   292  
   293  	return &apiv1.SecurityContext{
   294  		RunAsUser:              s.User,
   295  		Privileged:             isPrivileged,
   296  		ReadOnlyRootFilesystem: isReadOnly,
   297  		Capabilities:           capabilities,
   298  	}
   299  }
   300  
   301  func toBoolPointer(value bool) *bool {
   302  	if value {
   303  		return &value
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  func defaultUint64(v *uint64, defaultValue uint64) uint64 { //nolint: unparam
   310  	if v == nil {
   311  		return defaultValue
   312  	}
   313  
   314  	return *v
   315  }
   316  
   317  func toCapabilities(list []string) (capabilities []apiv1.Capability) {
   318  	for _, c := range list {
   319  		capabilities = append(capabilities, apiv1.Capability(c))
   320  	}
   321  	return
   322  }
   323  
   324  //nolint: unparam
   325  func forceRestartPolicy(podTemplate apiv1.PodTemplateSpec, forcedRestartPolicy apiv1.RestartPolicy) apiv1.PodTemplateSpec {
   326  	if podTemplate.Spec.RestartPolicy != "" {
   327  		podTemplate.Spec.RestartPolicy = forcedRestartPolicy
   328  	}
   329  
   330  	return podTemplate
   331  }