forked from LaconicNetwork/kompose
support multiple containers in a pod (#1394)
This commit is contained in:
@@ -77,6 +77,8 @@ type ConvertOptions struct {
|
||||
YAMLIndent int
|
||||
|
||||
WithKomposeAnnotation bool
|
||||
|
||||
MultipleContainerMode bool
|
||||
}
|
||||
|
||||
// IsPodController indicate if the user want to use a controller
|
||||
@@ -84,8 +86,11 @@ func (opt *ConvertOptions) IsPodController() bool {
|
||||
return opt.IsDeploymentFlag || opt.IsDaemonSetFlag || opt.IsReplicationControllerFlag || opt.Controller != ""
|
||||
}
|
||||
|
||||
type ServiceConfigGroup []ServiceConfig
|
||||
|
||||
// ServiceConfig holds the basic struct of a container
|
||||
type ServiceConfig struct {
|
||||
Name string
|
||||
ContainerName string
|
||||
Image string `compose:"image"`
|
||||
Environment []EnvVar `compose:"environment"`
|
||||
|
||||
@@ -32,6 +32,8 @@ import (
|
||||
const (
|
||||
// LabelServiceType defines the type of service to be created
|
||||
LabelServiceType = "kompose.service.type"
|
||||
// LabelServiceGroup defines the group of services in a single pod
|
||||
LabelServiceGroup = "kompose.service.group"
|
||||
// LabelNodePortPort defines the port value for NodePort service
|
||||
LabelNodePortPort = "kompose.service.nodeport.port"
|
||||
// LabelServiceExpose defines if the service needs to be made accessible from outside the cluster or not
|
||||
|
||||
@@ -178,6 +178,7 @@ func libComposeToKomposeMapping(composeObject *project.Project) (kobject.Kompose
|
||||
// all relevant information as well as avoid the unsupported keys as well.
|
||||
for name, composeServiceConfig := range composeObject.ServiceConfigs.All() {
|
||||
serviceConfig := kobject.ServiceConfig{}
|
||||
serviceConfig.Name = name
|
||||
serviceConfig.Image = composeServiceConfig.Image
|
||||
serviceConfig.Build = composeServiceConfig.Build.Context
|
||||
newName := normalizeContainerNames(composeServiceConfig.ContainerName)
|
||||
|
||||
@@ -364,6 +364,7 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose
|
||||
// No need to modify before importation
|
||||
name := composeServiceConfig.Name
|
||||
serviceConfig := kobject.ServiceConfig{}
|
||||
serviceConfig.Name = name
|
||||
serviceConfig.Image = composeServiceConfig.Image
|
||||
serviceConfig.WorkingDir = composeServiceConfig.WorkingDir
|
||||
serviceConfig.Annotations = map[string]string(composeServiceConfig.Labels)
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kubernetes/kompose/pkg/kobject"
|
||||
"github.com/kubernetes/kompose/pkg/loader/compose"
|
||||
"github.com/kubernetes/kompose/pkg/transformer"
|
||||
deployapi "github.com/openshift/api/apps/v1"
|
||||
"github.com/pkg/errors"
|
||||
@@ -440,11 +441,44 @@ func (k *Kubernetes) CreateHeadlessService(name string, service kobject.ServiceC
|
||||
|
||||
return svc
|
||||
}
|
||||
func (k *Kubernetes) UpdateKubernetesObjectsMultipleContainers(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object, podSpec PodSpec) error {
|
||||
// Configure annotations
|
||||
annotations := transformer.ConfigAnnotations(service)
|
||||
|
||||
// fillTemplate fills the pod template with the value calculated from config
|
||||
fillTemplate := func(template *api.PodTemplateSpec) error {
|
||||
template.ObjectMeta.Labels = transformer.ConfigLabelsWithNetwork(name, service.Network)
|
||||
template.Spec = podSpec.Get()
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillObjectMeta fills the metadata with the value calculated from config
|
||||
fillObjectMeta := func(meta *metav1.ObjectMeta) {
|
||||
meta.Annotations = annotations
|
||||
}
|
||||
|
||||
// update supported controller
|
||||
for _, obj := range *objects {
|
||||
err := k.UpdateController(obj, fillTemplate, fillObjectMeta)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "k.UpdateController failed")
|
||||
}
|
||||
if len(service.Volumes) > 0 {
|
||||
switch objType := obj.(type) {
|
||||
case *appsv1.Deployment:
|
||||
objType.Spec.Strategy.Type = appsv1.RecreateDeploymentStrategyType
|
||||
case *deployapi.DeploymentConfig:
|
||||
objType.Spec.Strategy.Type = deployapi.DeploymentStrategyTypeRecreate
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateKubernetesObjects loads configurations to k8s objects
|
||||
func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) error {
|
||||
// Configure the environment variables.
|
||||
envs, err := k.ConfigEnvs(name, service, opt)
|
||||
envs, err := ConfigEnvs(name, service, opt)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to load env variables")
|
||||
}
|
||||
@@ -479,10 +513,10 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
|
||||
}
|
||||
|
||||
// Configure the container ports.
|
||||
ports := k.ConfigPorts(name, service)
|
||||
ports := ConfigPorts(name, service)
|
||||
|
||||
// Configure capabilities
|
||||
capabilities := k.ConfigCapabilities(service)
|
||||
capabilities := ConfigCapabilities(service)
|
||||
|
||||
// Configure annotations
|
||||
annotations := transformer.ConfigAnnotations(service)
|
||||
@@ -662,6 +696,20 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
|
||||
return nil
|
||||
}
|
||||
|
||||
// KomposeObjectToServiceConfigGroupMapping returns the service config group by name
|
||||
func KomposeObjectToServiceConfigGroupMapping(komposeObject kobject.KomposeObject) map[string]kobject.ServiceConfigGroup {
|
||||
serviceConfigGroup := make(map[string]kobject.ServiceConfigGroup)
|
||||
for name, service := range komposeObject.ServiceConfigs {
|
||||
if groupID, ok := service.Labels[compose.LabelServiceGroup]; ok {
|
||||
service.Name = name
|
||||
serviceConfigGroup[groupID] = append(serviceConfigGroup[groupID], service)
|
||||
} else {
|
||||
serviceConfigGroup[name] = append(serviceConfigGroup[name], service)
|
||||
}
|
||||
}
|
||||
return serviceConfigGroup
|
||||
}
|
||||
|
||||
// TranslatePodResource config pod resources
|
||||
func TranslatePodResource(service *kobject.ServiceConfig, template *api.PodTemplateSpec) {
|
||||
// Configure the resource limits
|
||||
|
||||
@@ -536,7 +536,7 @@ func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorVa
|
||||
}
|
||||
|
||||
// ConfigPorts configures the container ports.
|
||||
func (k *Kubernetes) ConfigPorts(name string, service kobject.ServiceConfig) []api.ContainerPort {
|
||||
func ConfigPorts(name string, service kobject.ServiceConfig) []api.ContainerPort {
|
||||
ports := []api.ContainerPort{}
|
||||
exist := map[string]bool{}
|
||||
for _, port := range service.Port {
|
||||
@@ -641,7 +641,7 @@ func (k *Kubernetes) ConfigServicePorts(name string, service kobject.ServiceConf
|
||||
}
|
||||
|
||||
//ConfigCapabilities configure POSIX capabilities that can be added or removed to a container
|
||||
func (k *Kubernetes) ConfigCapabilities(service kobject.ServiceConfig) *api.Capabilities {
|
||||
func ConfigCapabilities(service kobject.ServiceConfig) *api.Capabilities {
|
||||
capsAdd := []api.Capability{}
|
||||
capsDrop := []api.Capability{}
|
||||
for _, capAdd := range service.CapAdd {
|
||||
@@ -947,7 +947,7 @@ func (k *Kubernetes) ConfigPVCVolumeSource(name string, readonly bool) *api.Volu
|
||||
}
|
||||
|
||||
// ConfigEnvs configures the environment variables.
|
||||
func (k *Kubernetes) ConfigEnvs(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]api.EnvVar, error) {
|
||||
func ConfigEnvs(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]api.EnvVar, error) {
|
||||
envs := transformer.EnvSort{}
|
||||
|
||||
keysFromEnvFile := make(map[string]bool)
|
||||
@@ -1132,98 +1132,249 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
|
||||
}
|
||||
}
|
||||
|
||||
sortedKeys := SortedKeys(komposeObject)
|
||||
for _, name := range sortedKeys {
|
||||
service := komposeObject.ServiceConfigs[name]
|
||||
var objects []runtime.Object
|
||||
if opt.MultipleContainerMode {
|
||||
komposeObjectToServiceConfigGroupMapping := KomposeObjectToServiceConfigGroupMapping(komposeObject)
|
||||
for name, group := range komposeObjectToServiceConfigGroupMapping {
|
||||
service := komposeObject.ServiceConfigs[name]
|
||||
var objects []runtime.Object
|
||||
|
||||
service.WithKomposeAnnotation = opt.WithKomposeAnnotation
|
||||
service.WithKomposeAnnotation = opt.WithKomposeAnnotation
|
||||
|
||||
// Must build the images before conversion (got to add service.Image in case 'image' key isn't provided
|
||||
// Check that --build is set to true
|
||||
// Check to see if there is an InputFile (required!) before we build the container
|
||||
// Check that there's actually a Build key
|
||||
// Lastly, we must have an Image name to continue
|
||||
if opt.Build == "local" && opt.InputFiles != nil && service.Build != "" {
|
||||
// If there's no "image" key, use the name of the container that's built
|
||||
if service.Image == "" {
|
||||
service.Image = name
|
||||
}
|
||||
// Must build the images before conversion (got to add service.Image in case 'image' key isn't provided
|
||||
// Check that --build is set to true
|
||||
// Check to see if there is an InputFile (required!) before we build the container
|
||||
// Check that there's actually a Build key
|
||||
// Lastly, we must have an Image name to continue
|
||||
if opt.Build == "local" && opt.InputFiles != nil && service.Build != "" {
|
||||
// If there's no "image" key, use the name of the container that's built
|
||||
if service.Image == "" {
|
||||
service.Image = name
|
||||
}
|
||||
|
||||
if service.Image == "" {
|
||||
return nil, fmt.Errorf("image key required within build parameters in order to build and push service '%s'", name)
|
||||
}
|
||||
if service.Image == "" {
|
||||
return nil, fmt.Errorf("image key required within build parameters in order to build and push service '%s'", name)
|
||||
}
|
||||
|
||||
log.Infof("Build key detected. Attempting to build image '%s'", service.Image)
|
||||
log.Infof("Build key detected. Attempting to build image '%s'", service.Image)
|
||||
|
||||
// Build the image!
|
||||
err := transformer.BuildDockerImage(service, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to build Docker image for service %v", name)
|
||||
}
|
||||
|
||||
// Push the built image to the repo!
|
||||
if opt.PushImage {
|
||||
log.Infof("Push image enabled. Attempting to push image '%s'", service.Image)
|
||||
err = transformer.PushDockerImage(service, name)
|
||||
// Build the image!
|
||||
err := transformer.BuildDockerImage(service, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name)
|
||||
return nil, errors.Wrapf(err, "Unable to build Docker image for service %v", name)
|
||||
}
|
||||
|
||||
// Push the built image to the repo!
|
||||
if opt.PushImage {
|
||||
log.Infof("Push image enabled. Attempting to push image '%s'", service.Image)
|
||||
err = transformer.PushDockerImage(service, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate pod only and nothing more
|
||||
if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() {
|
||||
log.Infof("Create kubernetes pod instead of pod controller due to restart policy: %s", service.Restart)
|
||||
pod := k.InitPod(name, service)
|
||||
objects = append(objects, pod)
|
||||
} else {
|
||||
objects = k.CreateKubernetesObjects(name, service, opt)
|
||||
}
|
||||
podSpec := PodSpec{}
|
||||
|
||||
if k.PortsExist(service) {
|
||||
if service.ServiceType == "LoadBalancer" {
|
||||
svcs := k.CreateLBService(name, service, objects)
|
||||
for _, svc := range svcs {
|
||||
// added a container
|
||||
for _, service := range group {
|
||||
podSpec.Append(AddContainer(service, opt))
|
||||
|
||||
// Generate pod only and nothing more
|
||||
if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() {
|
||||
log.Infof("Create kubernetes pod instead of pod controller due to restart policy: %s", service.Restart)
|
||||
pod := k.InitPod(name, service)
|
||||
objects = append(objects, pod)
|
||||
} else {
|
||||
objects = k.CreateKubernetesObjects(name, service, opt)
|
||||
}
|
||||
|
||||
if k.PortsExist(service) {
|
||||
if service.ServiceType == "LoadBalancer" {
|
||||
svcs := k.CreateLBService(name, service, objects)
|
||||
for _, svc := range svcs {
|
||||
objects = append(objects, svc)
|
||||
}
|
||||
if len(svcs) > 1 {
|
||||
log.Warningf("Create multiple service to avoid using mixed protocol in the same service when it's loadbalander type")
|
||||
}
|
||||
} else {
|
||||
svc := k.CreateService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
if service.ExposeService != "" {
|
||||
objects = append(objects, k.initIngress(name, service, svc.Spec.Ports[0].Port))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if service.ServiceType == "Headless" {
|
||||
svc := k.CreateHeadlessService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
} else {
|
||||
log.Warnf("Service %q won't be created because 'ports' is not specified", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the container volumes.
|
||||
volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "k.ConfigVolumes failed")
|
||||
}
|
||||
podSpec.Append(
|
||||
SetVolumeMounts(volumesMount),
|
||||
SetVolumes(volumes),
|
||||
)
|
||||
|
||||
// Configure Tmpfs
|
||||
if len(service.TmpFs) > 0 {
|
||||
TmpVolumesMount, TmpVolumes := k.ConfigTmpfs(name, service)
|
||||
|
||||
volumes = append(volumes, TmpVolumes...)
|
||||
|
||||
volumesMount = append(volumesMount, TmpVolumesMount...)
|
||||
}
|
||||
|
||||
if pvc != nil {
|
||||
// Looping on the slice pvc instead of `*objects = append(*objects, pvc...)`
|
||||
// because the type of objects and pvc is different, but when doing append
|
||||
// one element at a time it gets converted to runtime.Object for objects slice
|
||||
for _, p := range pvc {
|
||||
objects = append(objects, p)
|
||||
}
|
||||
}
|
||||
|
||||
if cms != nil {
|
||||
for _, c := range cms {
|
||||
objects = append(objects, c)
|
||||
}
|
||||
}
|
||||
|
||||
podSpec.Append(
|
||||
SetPorts(name, service),
|
||||
ImagePullPolicy(name, service),
|
||||
RestartPolicy(name, service),
|
||||
SecurityContext(name, service),
|
||||
LivenessProbe(service),
|
||||
ReadinessProbe(service),
|
||||
HostName(service),
|
||||
DomainName(service),
|
||||
ResourcesLimits(service),
|
||||
ResourcesRequests(service),
|
||||
TerminationGracePeriodSeconds(name, service),
|
||||
)
|
||||
|
||||
err = k.UpdateKubernetesObjectsMultipleContainers(name, service, opt, &objects, podSpec)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error transforming Kubernetes objects")
|
||||
}
|
||||
}
|
||||
|
||||
if len(service.Network) > 0 {
|
||||
for _, net := range service.Network {
|
||||
log.Infof("Network %s is detected at Source, shall be converted to equivalent NetworkPolicy at Destination", net)
|
||||
np, err := k.CreateNetworkPolicy(name, net)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to create Network Policy for network %v for service %v", net, name)
|
||||
}
|
||||
objects = append(objects, np)
|
||||
}
|
||||
}
|
||||
|
||||
allobjects = append(allobjects, objects...)
|
||||
}
|
||||
} else {
|
||||
sortedKeys := SortedKeys(komposeObject)
|
||||
for _, name := range sortedKeys {
|
||||
service := komposeObject.ServiceConfigs[name]
|
||||
var objects []runtime.Object
|
||||
|
||||
service.WithKomposeAnnotation = opt.WithKomposeAnnotation
|
||||
|
||||
// Must build the images before conversion (got to add service.Image in case 'image' key isn't provided
|
||||
// Check that --build is set to true
|
||||
// Check to see if there is an InputFile (required!) before we build the container
|
||||
// Check that there's actually a Build key
|
||||
// Lastly, we must have an Image name to continue
|
||||
if opt.Build == "local" && opt.InputFiles != nil && service.Build != "" {
|
||||
// If there's no "image" key, use the name of the container that's built
|
||||
if service.Image == "" {
|
||||
service.Image = name
|
||||
}
|
||||
|
||||
if service.Image == "" {
|
||||
return nil, fmt.Errorf("image key required within build parameters in order to build and push service '%s'", name)
|
||||
}
|
||||
|
||||
log.Infof("Build key detected. Attempting to build image '%s'", service.Image)
|
||||
|
||||
// Build the image!
|
||||
err := transformer.BuildDockerImage(service, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to build Docker image for service %v", name)
|
||||
}
|
||||
|
||||
// Push the built image to the repo!
|
||||
if opt.PushImage {
|
||||
log.Infof("Push image enabled. Attempting to push image '%s'", service.Image)
|
||||
err = transformer.PushDockerImage(service, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate pod only and nothing more
|
||||
if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() {
|
||||
log.Infof("Create kubernetes pod instead of pod controller due to restart policy: %s", service.Restart)
|
||||
pod := k.InitPod(name, service)
|
||||
objects = append(objects, pod)
|
||||
} else {
|
||||
objects = k.CreateKubernetesObjects(name, service, opt)
|
||||
}
|
||||
|
||||
if k.PortsExist(service) {
|
||||
if service.ServiceType == "LoadBalancer" {
|
||||
svcs := k.CreateLBService(name, service, objects)
|
||||
for _, svc := range svcs {
|
||||
objects = append(objects, svc)
|
||||
}
|
||||
if len(svcs) > 1 {
|
||||
log.Warningf("Create multiple service to avoid using mixed protocol in the same service when it's loadbalander type")
|
||||
}
|
||||
} else {
|
||||
svc := k.CreateService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
}
|
||||
if len(svcs) > 1 {
|
||||
log.Warningf("Create multiple service to avoid using mixed protocol in the same service when it's loadbalander type")
|
||||
if service.ExposeService != "" {
|
||||
objects = append(objects, k.initIngress(name, service, svc.Spec.Ports[0].Port))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
svc := k.CreateService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
if service.ExposeService != "" {
|
||||
objects = append(objects, k.initIngress(name, service, svc.Spec.Ports[0].Port))
|
||||
if service.ServiceType == "Headless" {
|
||||
svc := k.CreateHeadlessService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
} else {
|
||||
log.Warnf("Service %q won't be created because 'ports' is not specified", name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if service.ServiceType == "Headless" {
|
||||
svc := k.CreateHeadlessService(name, service, objects)
|
||||
objects = append(objects, svc)
|
||||
} else {
|
||||
log.Warnf("Service %q won't be created because 'ports' is not specified", name)
|
||||
|
||||
err := k.UpdateKubernetesObjects(name, service, opt, &objects)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error transforming Kubernetes objects")
|
||||
}
|
||||
}
|
||||
|
||||
err := k.UpdateKubernetesObjects(name, service, opt, &objects)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error transforming Kubernetes objects")
|
||||
}
|
||||
if len(service.Network) > 0 {
|
||||
for _, net := range service.Network {
|
||||
log.Infof("Network %s is detected at Source, shall be converted to equivalent NetworkPolicy at Destination", net)
|
||||
np, err := k.CreateNetworkPolicy(name, net)
|
||||
|
||||
if len(service.Network) > 0 {
|
||||
for _, net := range service.Network {
|
||||
log.Infof("Network %s is detected at Source, shall be converted to equivalent NetworkPolicy at Destination", net)
|
||||
np, err := k.CreateNetworkPolicy(name, net)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to create Network Policy for network %v for service %v", net, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to create Network Policy for network %v for service %v", net, name)
|
||||
}
|
||||
objects = append(objects, np)
|
||||
}
|
||||
objects = append(objects, np)
|
||||
}
|
||||
}
|
||||
|
||||
allobjects = append(allobjects, objects...)
|
||||
allobjects = append(allobjects, objects...)
|
||||
}
|
||||
}
|
||||
|
||||
// sort all object so Services are first
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes/kompose/pkg/kobject"
|
||||
"github.com/kubernetes/kompose/pkg/loader/compose"
|
||||
"github.com/kubernetes/kompose/pkg/transformer"
|
||||
deployapi "github.com/openshift/api/apps/v1"
|
||||
"github.com/pkg/errors"
|
||||
@@ -535,12 +536,106 @@ func TestConfigCapabilities(t *testing.T) {
|
||||
"ConfigCapsNoAddDrop": {kobject.ServiceConfig{CapAdd: nil, CapDrop: nil}, api.Capabilities{Add: []api.Capability{}, Drop: []api.Capability{}}},
|
||||
}
|
||||
|
||||
k := Kubernetes{}
|
||||
for name, test := range testCases {
|
||||
t.Log("Test case:", name)
|
||||
result := k.ConfigCapabilities(test.service)
|
||||
result := ConfigCapabilities(test.service)
|
||||
if !reflect.DeepEqual(result.Add, test.result.Add) || !reflect.DeepEqual(result.Drop, test.result.Drop) {
|
||||
t.Errorf("Not expected result for ConfigCapabilities")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleContainersInPod(t *testing.T) {
|
||||
groupName := "pod_group"
|
||||
containerName := ""
|
||||
|
||||
createConfig := func(name string, containerName *string) kobject.ServiceConfig {
|
||||
config := newServiceConfig()
|
||||
config.Labels = map[string]string{compose.LabelServiceGroup: groupName}
|
||||
config.Name = name
|
||||
if containerName != nil {
|
||||
config.ContainerName = *containerName
|
||||
}
|
||||
config.Volumes = []kobject.Volumes{
|
||||
{
|
||||
VolumeName: "mountVolume",
|
||||
MountPath: "/data",
|
||||
},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
komposeObject kobject.KomposeObject
|
||||
opt kobject.ConvertOptions
|
||||
expectedNumObjs int
|
||||
expectedNames []string
|
||||
}{
|
||||
"Converted multiple containers": {
|
||||
kobject.KomposeObject{
|
||||
ServiceConfigs: map[string]kobject.ServiceConfig{
|
||||
"app1": createConfig("app1", &containerName),
|
||||
"app2": createConfig("app2", &containerName),
|
||||
},
|
||||
}, kobject.ConvertOptions{MultipleContainerMode: true}, 2, []string{"app1", "app2"}},
|
||||
"Converted multiple containers to Deployments (D)": {
|
||||
kobject.KomposeObject{
|
||||
ServiceConfigs: map[string]kobject.ServiceConfig{
|
||||
"app1": createConfig("app1", &containerName),
|
||||
"app2": createConfig("app2", &containerName),
|
||||
},
|
||||
}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 3, []string{"app1", "app2"}},
|
||||
"Converted multiple containers (ContainerName are nil) to Deployments (D)": {
|
||||
kobject.KomposeObject{
|
||||
ServiceConfigs: map[string]kobject.ServiceConfig{
|
||||
"app1": createConfig("app1", nil),
|
||||
"app2": createConfig("app2", nil),
|
||||
},
|
||||
}, kobject.ConvertOptions{MultipleContainerMode: true, CreateD: true}, 3, []string{"name", "name"}},
|
||||
// TODO: add more tests
|
||||
}
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Log("Test case:", name)
|
||||
k := Kubernetes{}
|
||||
// Run Transform
|
||||
objs, err := k.Transform(test.komposeObject, test.opt)
|
||||
if err != nil {
|
||||
t.Error(errors.Wrap(err, "k.Transform failed"))
|
||||
}
|
||||
if len(objs) != test.expectedNumObjs {
|
||||
t.Errorf("Expected %d objects returned, got %d", test.expectedNumObjs, len(objs))
|
||||
}
|
||||
|
||||
// Check results
|
||||
for _, obj := range objs {
|
||||
if svc, ok := obj.(*api.Service); ok {
|
||||
if svc.Name != groupName {
|
||||
t.Errorf("Expected %v returned, got %v", groupName, svc.Name)
|
||||
}
|
||||
}
|
||||
if deployment, ok := obj.(*appsv1.Deployment); ok {
|
||||
if deployment.Name != groupName {
|
||||
t.Errorf("Expected %v returned, got %v", groupName, deployment.Name)
|
||||
}
|
||||
if len(deployment.Spec.Template.Spec.Containers) != 2 {
|
||||
t.Errorf("Expected %d returned, got %d", 2, len(deployment.Spec.Template.Spec.Containers))
|
||||
}
|
||||
nameSet := make(map[string]api.Container)
|
||||
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||
nameSet[container.Name] = container
|
||||
}
|
||||
if container, ok := nameSet[test.expectedNames[0]]; !ok {
|
||||
t.Errorf("Expected %v returned, got %v", test.expectedNames[0], container.Name)
|
||||
} else if len(container.VolumeMounts) != 1 {
|
||||
t.Errorf("Expected %v returned, got %v", 1, len(container.VolumeMounts))
|
||||
}
|
||||
if container, ok := nameSet[test.expectedNames[1]]; !ok {
|
||||
t.Errorf("Expected %v returned, got %v", test.expectedNames[1], container.Name)
|
||||
} else if len(container.VolumeMounts) != 1 {
|
||||
t.Errorf("Expected %v returned, got %v", 1, len(container.VolumeMounts))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
mapset "github.com/deckarep/golang-set"
|
||||
"github.com/kubernetes/kompose/pkg/kobject"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type PodSpec struct {
|
||||
api.PodSpec
|
||||
}
|
||||
|
||||
type PodSpecOption func(*PodSpec)
|
||||
|
||||
func AddContainer(service kobject.ServiceConfig, opt kobject.ConvertOptions) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
name := service.Name
|
||||
image := service.Image
|
||||
|
||||
if image == "" {
|
||||
image = name
|
||||
}
|
||||
|
||||
// do not override in openshift case?
|
||||
if len(service.ContainerName) > 0 {
|
||||
name = FormatContainerName(service.ContainerName)
|
||||
}
|
||||
|
||||
envs, err := ConfigEnvs(name, service, opt)
|
||||
if err != nil {
|
||||
panic("Unable to load env variables")
|
||||
}
|
||||
|
||||
podSpec.Containers = append(podSpec.Containers, api.Container{
|
||||
Name: name,
|
||||
Image: image,
|
||||
Env: envs,
|
||||
Command: service.Command,
|
||||
Args: service.Args,
|
||||
WorkingDir: service.WorkingDir,
|
||||
Stdin: service.Stdin,
|
||||
TTY: service.Tty,
|
||||
})
|
||||
podSpec.NodeSelector = service.Placement
|
||||
}
|
||||
}
|
||||
|
||||
func ImagePullSecrets(pullSecret string) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
podSpec.ImagePullSecrets = append(podSpec.ImagePullSecrets,
|
||||
api.LocalObjectReference{
|
||||
Name: pullSecret,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TerminationGracePeriodSeconds(name string, service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
var err error
|
||||
if service.StopGracePeriod != "" {
|
||||
podSpec.TerminationGracePeriodSeconds, err = DurationStrToSecondsInt(service.StopGracePeriod)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to parse duration \"%v\" for service \"%v\"", service.StopGracePeriod, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the resource limits
|
||||
func ResourcesLimits(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if service.MemLimit != 0 || service.CPULimit != 0 {
|
||||
resourceLimit := api.ResourceList{}
|
||||
|
||||
if service.MemLimit != 0 {
|
||||
resourceLimit[api.ResourceMemory] = *resource.NewQuantity(int64(service.MemLimit), "RandomStringForFormat")
|
||||
}
|
||||
|
||||
if service.CPULimit != 0 {
|
||||
resourceLimit[api.ResourceCPU] = *resource.NewMilliQuantity(service.CPULimit, resource.DecimalSI)
|
||||
}
|
||||
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].Resources.Limits = resourceLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the resource requests
|
||||
func ResourcesRequests(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if service.MemReservation != 0 || service.CPUReservation != 0 {
|
||||
resourceRequests := api.ResourceList{}
|
||||
|
||||
if service.MemReservation != 0 {
|
||||
resourceRequests[api.ResourceMemory] = *resource.NewQuantity(int64(service.MemReservation), "RandomStringForFormat")
|
||||
}
|
||||
|
||||
if service.CPUReservation != 0 {
|
||||
resourceRequests[api.ResourceCPU] = *resource.NewMilliQuantity(service.CPUReservation, resource.DecimalSI)
|
||||
}
|
||||
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].Resources.Requests = resourceRequests
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure SecurityContext
|
||||
func SecurityContext(name string, service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
// Configure resource reservations
|
||||
podSecurityContext := &api.PodSecurityContext{}
|
||||
|
||||
//set pid namespace mode
|
||||
if service.Pid != "" {
|
||||
if service.Pid == "host" {
|
||||
// podSecurityContext.HostPID = true
|
||||
} else {
|
||||
log.Warningf("Ignoring PID key for service \"%v\". Invalid value \"%v\".", name, service.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
//set supplementalGroups
|
||||
if service.GroupAdd != nil {
|
||||
podSecurityContext.SupplementalGroups = service.GroupAdd
|
||||
}
|
||||
|
||||
// Setup security context
|
||||
securityContext := &api.SecurityContext{}
|
||||
if service.Privileged {
|
||||
securityContext.Privileged = &service.Privileged
|
||||
}
|
||||
if service.User != "" {
|
||||
uid, err := strconv.ParseInt(service.User, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Ignoring user directive. User to be specified as a UID (numeric).")
|
||||
} else {
|
||||
securityContext.RunAsUser = &uid
|
||||
}
|
||||
}
|
||||
|
||||
// Configure capabilities
|
||||
capabilities := ConfigCapabilities(service)
|
||||
|
||||
//set capabilities if it is not empty
|
||||
if len(capabilities.Add) > 0 || len(capabilities.Drop) > 0 {
|
||||
securityContext.Capabilities = capabilities
|
||||
}
|
||||
|
||||
// update template only if securityContext is not empty
|
||||
if *securityContext != (api.SecurityContext{}) {
|
||||
podSpec.Containers[0].SecurityContext = securityContext
|
||||
}
|
||||
if !reflect.DeepEqual(*podSecurityContext, api.PodSecurityContext{}) {
|
||||
podSpec.SecurityContext = podSecurityContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetVolumeNames(volumes []api.Volume) mapset.Set {
|
||||
set := mapset.NewSet()
|
||||
for _, volume := range volumes {
|
||||
set.Add(volume.Name)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func SetVolumes(volumes []api.Volume) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
volumesSet := SetVolumeNames(volumes)
|
||||
containerVolumesSet := SetVolumeNames(podSpec.Volumes)
|
||||
for diffVolumeName := range volumesSet.Difference(containerVolumesSet).Iter() {
|
||||
for _, volume := range volumes {
|
||||
if volume.Name == diffVolumeName {
|
||||
podSpec.Volumes = append(podSpec.Volumes, volume)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetVolumeMountPaths(volumesMount []api.VolumeMount) mapset.Set {
|
||||
set := mapset.NewSet()
|
||||
for _, volumeMount := range volumesMount {
|
||||
set.Add(volumeMount.MountPath)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func SetVolumeMounts(volumesMount []api.VolumeMount) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
volumesMountSet := SetVolumeMountPaths(volumesMount)
|
||||
for i := range podSpec.Containers {
|
||||
containerVolumeMountsSet := SetVolumeMountPaths(podSpec.Containers[i].VolumeMounts)
|
||||
for diffVolumeMountPath := range volumesMountSet.Difference(containerVolumeMountsSet).Iter() {
|
||||
for _, volumeMount := range volumesMount {
|
||||
if volumeMount.MountPath == diffVolumeMountPath {
|
||||
podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, volumeMount)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure ports
|
||||
func SetPorts(name string, service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
// Configure the container ports.
|
||||
ports := ConfigPorts(name, service)
|
||||
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].Ports = ports
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the image pull policy
|
||||
func ImagePullPolicy(name string, service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if policy, err := GetImagePullPolicy(name, service.ImagePullPolicy); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].ImagePullPolicy = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the container restart policy.
|
||||
func RestartPolicy(name string, service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if restart, err := GetRestartPolicy(name, service.Restart); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
podSpec.RestartPolicy = restart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HostName(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
// Configure hostname/domain_name settings
|
||||
if service.HostName != "" {
|
||||
podSpec.Hostname = service.HostName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DomainName(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if service.DomainName != "" {
|
||||
podSpec.Subdomain = service.DomainName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LivenessProbe(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
// Configure the HealthCheck
|
||||
// We check to see if it's blank
|
||||
if !reflect.DeepEqual(service.HealthChecks.Liveness, kobject.HealthCheck{}) {
|
||||
probe := api.Probe{}
|
||||
|
||||
if len(service.HealthChecks.Liveness.Test) > 0 {
|
||||
probe.Handler = api.Handler{
|
||||
Exec: &api.ExecAction{
|
||||
Command: service.HealthChecks.Liveness.Test,
|
||||
},
|
||||
}
|
||||
} else if !reflect.ValueOf(service.HealthChecks.Liveness.HTTPPath).IsZero() &&
|
||||
!reflect.ValueOf(service.HealthChecks.Liveness.HTTPPort).IsZero() {
|
||||
probe.Handler = api.Handler{
|
||||
HTTPGet: &api.HTTPGetAction{
|
||||
Path: service.HealthChecks.Liveness.HTTPPath,
|
||||
Port: intstr.FromInt(int(service.HealthChecks.Liveness.HTTPPort)),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
panic(errors.New("Health check must contain a command"))
|
||||
}
|
||||
|
||||
probe.TimeoutSeconds = service.HealthChecks.Liveness.Timeout
|
||||
probe.PeriodSeconds = service.HealthChecks.Liveness.Interval
|
||||
probe.FailureThreshold = service.HealthChecks.Liveness.Retries
|
||||
|
||||
// See issue: https://github.com/docker/cli/issues/116
|
||||
// StartPeriod has been added to docker/cli however, it is not yet added
|
||||
// to compose. Once the feature has been implemented, this will automatically work
|
||||
probe.InitialDelaySeconds = service.HealthChecks.Liveness.StartPeriod
|
||||
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].LivenessProbe = &probe
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ReadinessProbe(service kobject.ServiceConfig) PodSpecOption {
|
||||
return func(podSpec *PodSpec) {
|
||||
if !reflect.DeepEqual(service.HealthChecks.Readiness, kobject.HealthCheck{}) {
|
||||
probeHealthCheckReadiness := api.Probe{}
|
||||
if len(service.HealthChecks.Readiness.Test) > 0 {
|
||||
probeHealthCheckReadiness.Handler = api.Handler{
|
||||
Exec: &api.ExecAction{
|
||||
Command: service.HealthChecks.Readiness.Test,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
panic(errors.New("Health check must contain a command"))
|
||||
}
|
||||
|
||||
probeHealthCheckReadiness.TimeoutSeconds = service.HealthChecks.Readiness.Timeout
|
||||
probeHealthCheckReadiness.PeriodSeconds = service.HealthChecks.Readiness.Interval
|
||||
probeHealthCheckReadiness.FailureThreshold = service.HealthChecks.Readiness.Retries
|
||||
|
||||
// See issue: https://github.com/docker/cli/issues/116
|
||||
// StartPeriod has been added to docker/cli however, it is not yet added
|
||||
// to compose. Once the feature has been implemented, this will automatically work
|
||||
probeHealthCheckReadiness.InitialDelaySeconds = service.HealthChecks.Readiness.StartPeriod
|
||||
|
||||
for i := range podSpec.Containers {
|
||||
podSpec.Containers[i].ReadinessProbe = &probeHealthCheckReadiness
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (podSpec *PodSpec) Append(ops ...PodSpecOption) *PodSpec {
|
||||
for _, option := range ops {
|
||||
option(podSpec)
|
||||
}
|
||||
return podSpec
|
||||
}
|
||||
|
||||
func (podSpec *PodSpec) Get() api.PodSpec {
|
||||
return podSpec.PodSpec
|
||||
}
|
||||
Reference in New Issue
Block a user