compose/convert/pod.go

347 lines
9.4 KiB
Go
Raw Normal View History

package convert
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/compose-spec/compose-go/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/helm-prototype/pkg/compose"
"github.com/pkg/errors"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *compose.Project) (apiv1.PodTemplateSpec, error) {
tpl := apiv1.PodTemplateSpec{}
hostAliases, err := toHostAliases(serviceConfig.ExtraHosts)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
env, err := toEnv(serviceConfig.Environment)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
restartPolicy, err := toRestartPolicy(serviceConfig)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
var limits apiv1.ResourceList
if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Limits != nil {
limits, err = toResource(serviceConfig.Deploy.Resources.Limits)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
}
var requests apiv1.ResourceList
if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Reservations != nil {
requests, err = toResource(serviceConfig.Deploy.Resources.Reservations)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
}
volumes, err := toVolumes(serviceConfig, model)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
volumeMounts, err := toVolumeMounts(serviceConfig, model)
if err != nil {
return apiv1.PodTemplateSpec{}, err
}
/* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy)
if err != nil {
return apiv1.PodTemplateSpec{}, err
} */
tpl.ObjectMeta = metav1.ObjectMeta{
Labels: labels,
Annotations: serviceConfig.Labels,
}
tpl.Spec.RestartPolicy = restartPolicy
tpl.Spec.Volumes = volumes
tpl.Spec.HostPID = toHostPID(serviceConfig.Pid)
tpl.Spec.HostIPC = toHostIPC(serviceConfig.Ipc)
tpl.Spec.Hostname = serviceConfig.Hostname
tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod)
tpl.Spec.HostAliases = hostAliases
// FIXME tpl.Spec.Affinity = nodeAffinity
// we dont want to remove all containers and recreate them because:
// an admission plugin can add sidecar containers
// we for sure want to keep the main container to be additive
if len(tpl.Spec.Containers) == 0 {
tpl.Spec.Containers = []apiv1.Container{{}}
}
containerIX := 0
for ix, c := range tpl.Spec.Containers {
if c.Name == serviceConfig.Name {
containerIX = ix
break
}
}
tpl.Spec.Containers[containerIX].Name = serviceConfig.Name
tpl.Spec.Containers[containerIX].Image = serviceConfig.Image
// FIXME tpl.Spec.Containers[containerIX].ImagePullPolicy = pullPolicy
tpl.Spec.Containers[containerIX].Command = serviceConfig.Entrypoint
tpl.Spec.Containers[containerIX].Args = serviceConfig.Command
tpl.Spec.Containers[containerIX].WorkingDir = serviceConfig.WorkingDir
tpl.Spec.Containers[containerIX].TTY = serviceConfig.Tty
tpl.Spec.Containers[containerIX].Stdin = serviceConfig.StdinOpen
tpl.Spec.Containers[containerIX].Ports = toPorts(serviceConfig.Ports)
tpl.Spec.Containers[containerIX].LivenessProbe = toLivenessProbe(serviceConfig.HealthCheck)
tpl.Spec.Containers[containerIX].Env = env
tpl.Spec.Containers[containerIX].VolumeMounts = volumeMounts
tpl.Spec.Containers[containerIX].SecurityContext = toSecurityContext(serviceConfig)
tpl.Spec.Containers[containerIX].Resources = apiv1.ResourceRequirements{
Limits: limits,
Requests: requests,
}
/* FIXME
if serviceConfig.PullSecret != "" {
pullSecrets := map[string]struct{}{}
for _, ps := range tpl.Spec.ImagePullSecrets {
pullSecrets[ps.Name] = struct{}{}
}
if _, ok := pullSecrets[serviceConfig.PullSecret]; !ok {
tpl.Spec.ImagePullSecrets = append(tpl.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: serviceConfig.PullSecret})
}
}
*/
return tpl, nil
}
func toImagePullPolicy(image string, specifiedPolicy string) (apiv1.PullPolicy, error) {
if specifiedPolicy == "" {
if strings.HasSuffix(image, ":latest") {
return apiv1.PullAlways, nil
}
return apiv1.PullIfNotPresent, nil
}
switch apiv1.PullPolicy(specifiedPolicy) {
case apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever:
return apiv1.PullPolicy(specifiedPolicy), nil
default:
return "", errors.Errorf("invalid pull policy %q, must be %q, %q or %q", specifiedPolicy, apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever)
}
}
func toHostAliases(extraHosts []string) ([]apiv1.HostAlias, error) {
if extraHosts == nil {
return nil, nil
}
byHostnames := map[string]string{}
for _, host := range extraHosts {
split := strings.SplitN(host, ":", 2)
if len(split) != 2 {
return nil, errors.Errorf("malformed host %s", host)
}
byHostnames[split[0]] = split[1]
}
byIPs := map[string][]string{}
for k, v := range byHostnames {
byIPs[v] = append(byIPs[v], k)
}
aliases := make([]apiv1.HostAlias, len(byIPs))
i := 0
for key, hosts := range byIPs {
sort.Strings(hosts)
aliases[i] = apiv1.HostAlias{
IP: key,
Hostnames: hosts,
}
i++
}
sort.Slice(aliases, func(i, j int) bool { return aliases[i].IP < aliases[j].IP })
return aliases, nil
}
func toHostPID(pid string) bool {
return "host" == pid
}
func toHostIPC(ipc string) bool {
return "host" == ipc
}
func toTerminationGracePeriodSeconds(duration *types.Duration) *int64 {
if duration == nil {
return nil
}
gracePeriod := int64(time.Duration(*duration).Seconds())
return &gracePeriod
}
func toLivenessProbe(hc *types.HealthCheckConfig) *apiv1.Probe {
if hc == nil || len(hc.Test) < 1 || hc.Test[0] == "NONE" {
return nil
}
command := hc.Test[1:]
if hc.Test[0] == "CMD-SHELL" {
command = append([]string{"sh", "-c"}, command...)
}
return &apiv1.Probe{
TimeoutSeconds: toSecondsOrDefault(hc.Timeout, 1),
PeriodSeconds: toSecondsOrDefault(hc.Interval, 1),
FailureThreshold: int32(defaultUint64(hc.Retries, 3)),
Handler: apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: command,
},
},
}
}
func toEnv(env map[string]*string) ([]apiv1.EnvVar, error) {
var envVars []apiv1.EnvVar
for k, v := range env {
if v == nil {
return nil, errors.Errorf("%s has no value, unsetting an environment variable is not supported", k)
}
envVars = append(envVars, toEnvVar(k, *v))
}
sort.Slice(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name })
return envVars, nil
}
func toEnvVar(key, value string) apiv1.EnvVar {
return apiv1.EnvVar{
Name: key,
Value: value,
}
}
func toPorts(list []types.ServicePortConfig) []apiv1.ContainerPort {
var ports []apiv1.ContainerPort
for _, v := range list {
ports = append(ports, apiv1.ContainerPort{
ContainerPort: int32(v.Target),
Protocol: toProtocol(v.Protocol),
})
}
return ports
}
func toProtocol(value string) apiv1.Protocol {
if value == "udp" {
return apiv1.ProtocolUDP
}
return apiv1.ProtocolTCP
}
func toRestartPolicy(s types.ServiceConfig) (apiv1.RestartPolicy, error) {
if s.Deploy == nil || s.Deploy.RestartPolicy == nil {
return apiv1.RestartPolicyAlways, nil
}
policy := s.Deploy.RestartPolicy
switch policy.Condition {
case string(swarm.RestartPolicyConditionAny):
return apiv1.RestartPolicyAlways, nil
case string(swarm.RestartPolicyConditionNone):
return apiv1.RestartPolicyNever, nil
case string(swarm.RestartPolicyConditionOnFailure):
return apiv1.RestartPolicyOnFailure, nil
default:
return "", errors.Errorf("unsupported restart policy %s", policy.Condition)
}
}
func toResource(res *types.Resource) (apiv1.ResourceList, error) {
list := make(apiv1.ResourceList)
if res.NanoCPUs != "" {
cpus, err := resource.ParseQuantity(res.NanoCPUs)
if err != nil {
return nil, err
}
list[apiv1.ResourceCPU] = cpus
}
if res.MemoryBytes != 0 {
memory, err := resource.ParseQuantity(fmt.Sprintf("%v", res.MemoryBytes))
if err != nil {
return nil, err
}
list[apiv1.ResourceMemory] = memory
}
return list, nil
}
func toSecurityContext(s types.ServiceConfig) *apiv1.SecurityContext {
isPrivileged := toBoolPointer(s.Privileged)
isReadOnly := toBoolPointer(s.ReadOnly)
var capabilities *apiv1.Capabilities
if s.CapAdd != nil || s.CapDrop != nil {
capabilities = &apiv1.Capabilities{
Add: toCapabilities(s.CapAdd),
Drop: toCapabilities(s.CapDrop),
}
}
var userID *int64
if s.User != "" {
numerical, err := strconv.Atoi(s.User)
if err == nil {
unixUserID := int64(numerical)
userID = &unixUserID
}
}
if isPrivileged == nil && isReadOnly == nil && capabilities == nil && userID == nil {
return nil
}
return &apiv1.SecurityContext{
RunAsUser: userID,
Privileged: isPrivileged,
ReadOnlyRootFilesystem: isReadOnly,
Capabilities: capabilities,
}
}
func toBoolPointer(value bool) *bool {
if value {
return &value
}
return nil
}
func defaultUint64(v *uint64, defaultValue uint64) uint64 { //nolint: unparam
if v == nil {
return defaultValue
}
return *v
}
func toCapabilities(list []string) (capabilities []apiv1.Capability) {
for _, c := range list {
capabilities = append(capabilities, apiv1.Capability(c))
}
return
}
//nolint: unparam
func forceRestartPolicy(podTemplate apiv1.PodTemplateSpec, forcedRestartPolicy apiv1.RestartPolicy) apiv1.PodTemplateSpec {
if podTemplate.Spec.RestartPolicy != "" {
podTemplate.Spec.RestartPolicy = forcedRestartPolicy
}
return podTemplate
}