diff --git a/cli/app/app.go b/cli/app/app.go index 1ab9132d..1adb05e7 100644 --- a/cli/app/app.go +++ b/cli/app/app.go @@ -27,12 +27,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/urfave/cli" - "github.com/docker/docker/api/client/bundlefile" - "github.com/docker/libcompose/config" - "github.com/docker/libcompose/docker" - "github.com/docker/libcompose/lookup" - "github.com/docker/libcompose/project" - "encoding/json" "io/ioutil" @@ -45,6 +39,7 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" client "k8s.io/kubernetes/pkg/client/unversioned" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/intstr" @@ -52,8 +47,12 @@ import ( // install kubernetes api _ "github.com/openshift/origin/pkg/deploy/api/install" - "github.com/fatih/structs" "github.com/ghodss/yaml" + "github.com/skippbox/kompose/pkg/kobject" + "github.com/skippbox/kompose/pkg/transformer" + "github.com/docker/libcompose/lookup" + "github.com/docker/libcompose/config" + "github.com/docker/libcompose/project" ) const ( @@ -61,49 +60,7 @@ const ( DefaultComposeFile = "docker-compose.yml" ) -var unsupportedKey = map[string]int{ - "Build": 0, - "CapAdd": 0, - "CapDrop": 0, - "CPUSet": 0, - "CPUShares": 0, - "CPUQuota": 0, - "CgroupParent": 0, - "Devices": 0, - "DependsOn": 0, - "DNS": 0, - "DNSSearch": 0, - "DomainName": 0, - "Entrypoint": 0, - "EnvFile": 0, - "Expose": 0, - "Extends": 0, - "ExternalLinks": 0, - "ExtraHosts": 0, - "Hostname": 0, - "Ipc": 0, - "Logging": 0, - "MacAddress": 0, - "MemLimit": 0, - "MemSwapLimit": 0, - "NetworkMode": 0, - "Networks": 0, - "Pid": 0, - "SecurityOpt": 0, - "ShmSize": 0, - "StopSignal": 0, - "VolumeDriver": 0, - "VolumesFrom": 0, - "Uts": 0, - "ReadOnly": 0, - "StdinOpen": 0, - "Tty": 0, - "User": 0, - "Ulimits": 0, - "Dockerfile": 0, - "Net": 0, - "Args": 0, -} +var inputFormat = "compose" var composeOptions = map[string]string{ "Build": "build", @@ -315,7 +272,7 @@ func createOutFile(out string) *os.File { } // Init RC object -func initRC(name string, service ServiceConfig, replicas int) *api.ReplicationController { +func initRC(name string, service kobject.ServiceConfig, replicas int) *api.ReplicationController { rc := &api.ReplicationController{ TypeMeta: unversioned.TypeMeta{ Kind: "ReplicationController", @@ -347,7 +304,7 @@ func initRC(name string, service ServiceConfig, replicas int) *api.ReplicationCo } // Init SC object -func initSC(name string, service ServiceConfig) *api.Service { +func initSC(name string, service kobject.ServiceConfig) *api.Service { sc := &api.Service{ TypeMeta: unversioned.TypeMeta{ Kind: "Service", @@ -365,7 +322,7 @@ func initSC(name string, service ServiceConfig) *api.Service { } // Init DC object -func initDC(name string, service ServiceConfig, replicas int) *extensions.Deployment { +func initDC(name string, service kobject.ServiceConfig, replicas int) *extensions.Deployment { dc := &extensions.Deployment{ TypeMeta: unversioned.TypeMeta{ Kind: "Deployment", @@ -400,7 +357,7 @@ func initDC(name string, service ServiceConfig, replicas int) *extensions.Deploy } // Init DS object -func initDS(name string, service ServiceConfig) *extensions.DaemonSet { +func initDS(name string, service kobject.ServiceConfig) *extensions.DaemonSet { ds := &extensions.DaemonSet{ TypeMeta: unversioned.TypeMeta{ Kind: "DaemonSet", @@ -429,7 +386,7 @@ func initDS(name string, service ServiceConfig) *extensions.DaemonSet { } // Init RS object -func initRS(name string, service ServiceConfig, replicas int) *extensions.ReplicaSet { +func initRS(name string, service kobject.ServiceConfig, replicas int) *extensions.ReplicaSet { rs := &extensions.ReplicaSet{ TypeMeta: unversioned.TypeMeta{ Kind: "ReplicaSet", @@ -460,7 +417,7 @@ func initRS(name string, service ServiceConfig, replicas int) *extensions.Replic } // initDeploymentConfig initialize OpenShifts DeploymentConfig object -func initDeploymentConfig(name string, service ServiceConfig, replicas int) *deployapi.DeploymentConfig { +func initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int) *deployapi.DeploymentConfig { dc := &deployapi.DeploymentConfig{ TypeMeta: unversioned.TypeMeta{ Kind: "DeploymentConfig", @@ -493,7 +450,7 @@ func initDeploymentConfig(name string, service ServiceConfig, replicas int) *dep } // Configure the environment variables. -func configEnvs(name string, service ServiceConfig) []api.EnvVar { +func configEnvs(name string, service kobject.ServiceConfig) []api.EnvVar { envs := []api.EnvVar{} for _, v := range service.Environment { envs = append(envs, api.EnvVar{ @@ -506,7 +463,7 @@ func configEnvs(name string, service ServiceConfig) []api.EnvVar { } // Configure the container volumes. -func configVolumes(service ServiceConfig) ([]api.VolumeMount, []api.Volume) { +func configVolumes(service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume) { volumesMount := []api.VolumeMount{} volumes := []api.Volume{} volumeSource := api.VolumeSource{} @@ -574,16 +531,16 @@ func isPath(substring string) bool { } // Configure the container ports. -func configPorts(name string, service ServiceConfig) []api.ContainerPort { +func configPorts(name string, service kobject.ServiceConfig) []api.ContainerPort { ports := []api.ContainerPort{} for _, port := range service.Port { var p api.Protocol switch port.Protocol { default: p = api.ProtocolTCP - case ProtocolTCP: + case kobject.ProtocolTCP: p = api.ProtocolTCP - case ProtocolUDP: + case kobject.ProtocolUDP: p = api.ProtocolUDP } ports = append(ports, api.ContainerPort{ @@ -596,7 +553,7 @@ func configPorts(name string, service ServiceConfig) []api.ContainerPort { } // Configure the container service ports. -func configServicePorts(name string, service ServiceConfig) []api.ServicePort { +func configServicePorts(name string, service kobject.ServiceConfig) []api.ServicePort { servicePorts := []api.ServicePort{} for _, port := range service.Port { if port.HostPort == 0 { @@ -606,9 +563,9 @@ func configServicePorts(name string, service ServiceConfig) []api.ServicePort { switch port.Protocol { default: p = api.ProtocolTCP - case ProtocolTCP: + case kobject.ProtocolTCP: p = api.ProtocolTCP - case ProtocolUDP: + case kobject.ProtocolUDP: p = api.ProtocolUDP } var targetPort intstr.IntOrString @@ -625,7 +582,7 @@ func configServicePorts(name string, service ServiceConfig) []api.ServicePort { } // Transform data to json/yaml -func transformer(obj runtime.Object, generateYaml bool) ([]byte, error) { +func transformData(obj runtime.Object, generateYaml bool) ([]byte, error) { // Convert to versioned object objectVersion := obj.GetObjectKind().GroupVersionKind() version := unversioned.GroupVersion{Group: objectVersion.Group, Version: objectVersion.Version} @@ -646,284 +603,6 @@ func transformer(obj runtime.Object, generateYaml bool) ([]byte, error) { return data, nil } -// load Environment Variable from bundles file -func loadEnvVars(service bundlefile.Service) ([]EnvVar, string) { - envs := []EnvVar{} - for _, env := range service.Env { - character := "=" - if strings.Contains(env, character) { - value := env[strings.Index(env, character)+1:] - name := env[0:strings.Index(env, character)] - name = strings.TrimSpace(name) - value = strings.TrimSpace(value) - envs = append(envs, EnvVar{ - Name: name, - Value: value, - }) - } else { - character = ":" - if strings.Contains(env, character) { - charQuote := "'" - value := env[strings.Index(env, character)+1:] - name := env[0:strings.Index(env, character)] - name = strings.TrimSpace(name) - value = strings.TrimSpace(value) - if strings.Contains(value, charQuote) { - value = strings.Trim(value, "'") - } - envs = append(envs, EnvVar{ - Name: name, - Value: value, - }) - } else { - return envs, "Invalid container env " + env - } - } - } - return envs, "" -} - -// load Environment Variable from compose file -func loadEnvVarsFromCompose(e map[string]string) []EnvVar { - envs := []EnvVar{} - for k, v := range e { - envs = append(envs, EnvVar{ - Name: k, - Value: v, - }) - } - return envs -} - -// load Ports from bundles file -func loadPorts(service bundlefile.Service) ([]Ports, string) { - ports := []Ports{} - for _, port := range service.Ports { - var p Protocol - switch port.Protocol { - default: - p = ProtocolTCP - case "TCP": - p = ProtocolTCP - case "UDP": - p = ProtocolUDP - } - ports = append(ports, Ports{ - HostPort: int32(port.Port), - ContainerPort: int32(port.Port), - Protocol: p, - }) - } - return ports, "" -} - -// Load Ports from compose file -func loadPortsFromCompose(composePorts []string) ([]Ports, string) { - ports := []Ports{} - character := ":" - for _, port := range composePorts { - p := ProtocolTCP - if strings.Contains(port, character) { - hostPort := port[0:strings.Index(port, character)] - hostPort = strings.TrimSpace(hostPort) - hostPortInt, err := strconv.Atoi(hostPort) - if err != nil { - return nil, "Invalid host port of " + port - } - containerPort := port[strings.Index(port, character)+1:] - containerPort = strings.TrimSpace(containerPort) - containerPortInt, err := strconv.Atoi(containerPort) - if err != nil { - return nil, "Invalid container port of " + port - } - ports = append(ports, Ports{ - HostPort: int32(hostPortInt), - ContainerPort: int32(containerPortInt), - Protocol: p, - }) - } else { - containerPortInt, err := strconv.Atoi(port) - if err != nil { - return nil, "Invalid container port of " + port - } - ports = append(ports, Ports{ - ContainerPort: int32(containerPortInt), - Protocol: p, - }) - } - - } - return ports, "" -} - -// load Image from bundles file -func loadImage(service bundlefile.Service) (string, string) { - character := "@" - if strings.Contains(service.Image, character) { - return service.Image[0:strings.Index(service.Image, character)], "" - } - return "", "Invalid image format" -} - -// Load DAB file into KomposeObject -func loadBundlesFile(file string) KomposeObject { - komposeObject := KomposeObject{ - ServiceConfigs: make(map[string]ServiceConfig), - } - buf, err := ioutil.ReadFile(file) - if err != nil { - logrus.Fatalf("Failed to read bundles file: %v", err) - } - reader := strings.NewReader(string(buf)) - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - logrus.Fatalf("Failed to parse bundles file: %v", err) - } - - for name, service := range bundle.Services { - checkUnsupportedKey(service) - serviceConfig := ServiceConfig{} - serviceConfig.Command = service.Command - serviceConfig.Args = service.Args - // convert bundle labels to annotations - serviceConfig.Annotations = service.Labels - - image, err := loadImage(service) - if err != "" { - logrus.Fatalf("Failed to load image from bundles file: %v", err) - } - serviceConfig.Image = image - - envs, err := loadEnvVars(service) - if err != "" { - logrus.Fatalf("Failed to load envvar from bundles file: %v", err) - } - serviceConfig.Environment = envs - - ports, err := loadPorts(service) - if err != "" { - logrus.Fatalf("Failed to load ports from bundles file: %v", err) - } - serviceConfig.Port = ports - - if service.WorkingDir != nil { - serviceConfig.WorkingDir = *service.WorkingDir - } - - komposeObject.ServiceConfigs[name] = serviceConfig - } - return komposeObject -} - -// Load compose file into KomposeObject -func loadComposeFile(file string) KomposeObject { - komposeObject := KomposeObject{ - ServiceConfigs: make(map[string]ServiceConfig), - } - context := &docker.Context{} - if file == "" { - file = "docker-compose.yml" - } - context.ComposeFiles = []string{file} - - if context.ResourceLookup == nil { - context.ResourceLookup = &lookup.FileResourceLookup{} - } - - if context.EnvironmentLookup == nil { - cwd, err := os.Getwd() - if err != nil { - return KomposeObject{} - } - context.EnvironmentLookup = &lookup.ComposableEnvLookup{ - Lookups: []config.EnvironmentLookup{ - &lookup.EnvfileLookup{ - Path: filepath.Join(cwd, ".env"), - }, - &lookup.OsEnvLookup{}, - }, - } - } - - // load compose file into composeObject - composeObject := project.NewProject(&context.Context, nil, nil) - err := composeObject.Parse() - if err != nil { - logrus.Fatalf("Failed to load compose file: %v", err) - } - - // transform composeObject into komposeObject - composeServiceNames := composeObject.ServiceConfigs.Keys() - - // volume config and network config are not supported - if len(composeObject.NetworkConfigs) > 0 { - logrus.Warningf("Unsupported network configuration of compose v2 - ignoring") - } - if len(composeObject.VolumeConfigs) > 0 { - logrus.Warningf("Unsupported volume configuration of compose v2 - ignoring") - } - - networksWarningFound := false - for _, name := range composeServiceNames { - if composeServiceConfig, ok := composeObject.ServiceConfigs.Get(name); ok { - //FIXME: networks always contains one default element, even it isn't declared in compose v2. - if len(composeServiceConfig.Networks.Networks) > 0 && - composeServiceConfig.Networks.Networks[0].Name != "default" && - !networksWarningFound { - logrus.Warningf("Unsupported key networks - ignoring") - networksWarningFound = true - } - checkUnsupportedKey(composeServiceConfig) - serviceConfig := ServiceConfig{} - serviceConfig.Image = composeServiceConfig.Image - serviceConfig.ContainerName = composeServiceConfig.ContainerName - - // load environment variables - envs := loadEnvVarsFromCompose(composeServiceConfig.Environment.ToMap()) - serviceConfig.Environment = envs - - // load ports - ports, err := loadPortsFromCompose(composeServiceConfig.Ports) - if err != "" { - logrus.Fatalf("Failed to load ports from compose file: %v", err) - } - serviceConfig.Port = ports - - serviceConfig.WorkingDir = composeServiceConfig.WorkingDir - serviceConfig.Volumes = composeServiceConfig.Volumes - - // convert compose labels to annotations - serviceConfig.Annotations = map[string]string(composeServiceConfig.Labels) - - serviceConfig.CPUSet = composeServiceConfig.CPUSet - serviceConfig.CPUShares = composeServiceConfig.CPUShares - serviceConfig.CPUQuota = composeServiceConfig.CPUQuota - serviceConfig.CapAdd = composeServiceConfig.CapAdd - serviceConfig.CapDrop = composeServiceConfig.CapDrop - serviceConfig.Expose = composeServiceConfig.Expose - serviceConfig.Privileged = composeServiceConfig.Privileged - serviceConfig.Restart = composeServiceConfig.Restart - serviceConfig.User = composeServiceConfig.User - - komposeObject.ServiceConfigs[name] = serviceConfig - } - } - return komposeObject -} - -type convertOptions struct { - toStdout bool - createD bool - createRC bool - createDS bool - createDeploymentConfig bool - createChart bool - generateYaml bool - replicas int - inputFile string - outFile string -} - // Convert komposeObject to K8S controllers func komposeConvert(komposeObject KomposeObject, opt convertOptions) []runtime.Object { var svcnames []string @@ -1119,28 +798,28 @@ func ConvertToVersion(objs []runtime.Object) ([]runtime.Object, error) { return ret, nil } -func validateFlags(opt convertOptions, singleOutput bool, dabFile, inputFile string) { - if len(opt.outFile) != 0 && opt.toStdout { +func validateFlags(opt kobject.ConvertOptions, singleOutput bool, dabFile, inputFile string) { + if len(opt.OutFile) != 0 && opt.ToStdout { logrus.Fatalf("Error: --out and --stdout can't be set at the same time") } - if opt.createChart && opt.toStdout { + if opt.CreateChart && opt.ToStdout { logrus.Fatalf("Error: chart cannot be generated when --stdout is specified") } - if opt.replicas < 0 { + if opt.Replicas < 0 { logrus.Fatalf("Error: --replicas cannot be negative") } if singleOutput { count := 0 - if opt.createD { + if opt.CreateD { count++ } - if opt.createDS { + if opt.CreateDS { count++ } - if opt.createRC { + if opt.CreateRC { count++ } - if opt.createDeploymentConfig { + if opt.CreateDeploymentConfig { count++ } if count > 1 { @@ -1172,51 +851,42 @@ func Convert(c *cli.Context) { createD = true } + komposeObject := kobject.KomposeObject{ + ServiceConfigs: make(map[string]kobject.ServiceConfig), + } + file := inputFile if len(dabFile) > 0 { + inputFormat = "bundle" file = dabFile } - opt := convertOptions{ - toStdout: toStdout, - createD: createD, - createRC: createRC, - createDS: createDS, - createDeploymentConfig: createDeploymentConfig, - createChart: createChart, - generateYaml: generateYaml, - replicas: replicas, - inputFile: file, - outFile: outFile, + opt := kobject.ConvertOptions{ + ToStdout: toStdout, + CreateD: createD, + CreateRC: createRC, + CreateDS: createDS, + CreateDeploymentConfig: createDeploymentConfig, + CreateChart: createChart, + GenerateYaml: generateYaml, + Replicas: replicas, + InputFile: file, + OutFile: outFile, } + f := createOutFile(opt.OutFile) + defer f.Close() + validateFlags(opt, singleOutput, dabFile, inputFile) - komposeObject := KomposeObject{} - - if len(dabFile) > 0 { - komposeObject = loadBundlesFile(dabFile) - } else { - komposeObject = loadComposeFile(inputFile) - } + komposeObject.Loader(file, inputFormat) + //komposeObject.Transformer(opt) // Convert komposeObject to K8S controllers - objects := komposeConvert(komposeObject, opt) + mServices, mDeployments, mDaemonSets, mReplicationControllers, mDeploymentConfigs, svcnames := transformer.Transform(komposeObject, opt) - // print output to places as needed - PrintList(objects, opt) -} - -func checkUnsupportedKey(service interface{}) { - s := structs.New(service) - for _, f := range s.Fields() { - if f.IsExported() && !f.IsZero() && f.Name() != "Networks" { - if count, ok := unsupportedKey[f.Name()]; ok && count == 0 { - logrus.Warningf("Unsupported key %s - ignoring", composeOptions[f.Name()]) - unsupportedKey[f.Name()]++ - } - } - } + // Print output + transformer.PrintControllers(mServices, mDeployments, mDaemonSets, mReplicationControllers, mDeploymentConfigs, svcnames, opt, f) } // Either print to stdout or to file/s diff --git a/cli/app/types.go b/cli/app/types.go deleted file mode 100644 index b5fe4176..00000000 --- a/cli/app/types.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2016 Skippbox, Ltd All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package app - -// KomposeObject holds the generic struct of Kompose transformation -type KomposeObject struct { - ServiceConfigs map[string]ServiceConfig -} - -// ServiceConfig holds the basic struct of a container -type ServiceConfig struct { - ContainerName string - Image string - Environment []EnvVar - Port []Ports - Command []string - WorkingDir string - Args []string - Volumes []string - Network []string - Labels map[string]string - Annotations map[string]string - CPUSet string - CPUShares int64 - CPUQuota int64 - CapAdd []string - CapDrop []string - Entrypoint []string - Expose []string - Privileged bool - Restart string - User string -} - -// EnvVar holds the environment variable struct of a container -type EnvVar struct { - Name string - Value string -} - -// Ports holds the ports struct of a container -type Ports struct { - HostPort int32 - ContainerPort int32 - Protocol Protocol -} - -// Protocol defines network protocols supported for things like container ports. -type Protocol string - -const ( - // ProtocolTCP is the TCP protocol. - ProtocolTCP Protocol = "TCP" - // ProtocolUDP is the UDP protocol. - ProtocolUDP Protocol = "UDP" -) diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go new file mode 100644 index 00000000..8121fedd --- /dev/null +++ b/pkg/kobject/kobject.go @@ -0,0 +1,167 @@ +/* +Copyright 2016 Skippbox, Ltd All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kobject + +import ( + "github.com/Sirupsen/logrus" + + "fmt" + "github.com/skippbox/kompose/pkg/loader" + "github.com/fatih/structs" + "github.com/skippbox/kompose/pkg/transformer" +) + +var unsupportedKey = map[string]int{ + "Build": 0, + "CapAdd": 0, + "CapDrop": 0, + "CPUSet": 0, + "CPUShares": 0, + "CPUQuota": 0, + "CgroupParent": 0, + "Devices": 0, + "DependsOn": 0, + "DNS": 0, + "DNSSearch": 0, + "DomainName": 0, + "Entrypoint": 0, + "EnvFile": 0, + "Expose": 0, + "Extends": 0, + "ExternalLinks": 0, + "ExtraHosts": 0, + "Hostname": 0, + "Ipc": 0, + "Logging": 0, + "MacAddress": 0, + "MemLimit": 0, + "MemSwapLimit": 0, + "NetworkMode": 0, + "Networks": 0, + "Pid": 0, + "SecurityOpt": 0, + "ShmSize": 0, + "StopSignal": 0, + "VolumeDriver": 0, + "VolumesFrom": 0, + "Uts": 0, + "ReadOnly": 0, + "StdinOpen": 0, + "Tty": 0, + "User": 0, + "Ulimits": 0, + "Dockerfile": 0, + "Net": 0, + "Args": 0, +} + +// KomposeObject holds the generic struct of Kompose transformation +type KomposeObject struct { + ServiceConfigs map[string]ServiceConfig +} + +type ConvertOptions struct { + ToStdout bool + CreateD bool + CreateRC bool + CreateDS bool + CreateDeploymentConfig bool + CreateChart bool + GenerateYaml bool + Replicas int + InputFile string + OutFile string +} + +// ServiceConfig holds the basic struct of a container +type ServiceConfig struct { + ContainerName string + Image string + Environment []EnvVar + Port []Ports + Command []string + WorkingDir string + Args []string + Volumes []string + Network []string + Labels map[string]string + CPUSet string + CPUShares int64 + CPUQuota int64 + CapAdd []string + CapDrop []string + Entrypoint []string + Expose []string + Privileged bool + Restart string + User string +} + +// EnvVar holds the environment variable struct of a container +type EnvVar struct { + Name string + Value string +} + +// Ports holds the ports struct of a container +type Ports struct { + HostPort int32 + ContainerPort int32 + Protocol Protocol +} + +// Protocol defines network protocols supported for things like container ports. +type Protocol string + +const ( + // ProtocolTCP is the TCP protocol. + ProtocolTCP Protocol = "TCP" + // ProtocolUDP is the UDP protocol. + ProtocolUDP Protocol = "UDP" +) + +// loader takes input and converts to KomposeObject +func (k *KomposeObject) Loader(file string, inp string) { + switch inp { + case "bundle": + //k.loadBundleFile(file) + loader.LoadBundle(k, file) + case "compose": + //k.loadComposeFile(file) + loader.LoadCompose(k, file) + default: + logrus.Fatalf("Input file format is not supported") + + } +} + +// transformer takes KomposeObject and converts to K8S / OpenShift primitives +func (k *KomposeObject) Transformer(opt ConvertOptions) { + transformer.Transform(k, opt) +} + +func CheckUnsupportedKey(service interface{}) { + s := structs.New(service) + for _, f := range s.Fields() { + if f.IsExported() && !f.IsZero() { + if count, ok := unsupportedKey[f.Name()]; ok && count == 0 { + fmt.Println("WARNING: Unsupported key " + f.Name() + " - ignoring") + unsupportedKey[f.Name()]++ + } + } + } +} diff --git a/pkg/loader/bundle.go b/pkg/loader/bundle.go index 529648a2..666cc3ba 100644 --- a/pkg/loader/bundle.go +++ b/pkg/loader/bundle.go @@ -1 +1,140 @@ +/* +Copyright 2016 Skippbox, Ltd All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package loader + +import ( + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/client/bundlefile" + "io/ioutil" + "strings" + "github.com/skippbox/kompose/pkg/kobject" +) + +// load Image from bundles file +func loadImage(service bundlefile.Service) (string, string) { + character := "@" + if strings.Contains(service.Image, character) { + return service.Image[0:strings.Index(service.Image, character)], "" + } + return "", "Invalid image format" +} + +// load Environment Variable from bundles file +func loadEnvVarsfromBundle(service bundlefile.Service) ([]kobject.EnvVar, string) { + envs := []kobject.EnvVar{} + for _, env := range service.Env { + character := "=" + if strings.Contains(env, character) { + value := env[strings.Index(env, character)+1:] + name := env[0:strings.Index(env, character)] + name = strings.TrimSpace(name) + value = strings.TrimSpace(value) + envs = append(envs, kobject.EnvVar{ + Name: name, + Value: value, + }) + } else { + character = ":" + if strings.Contains(env, character) { + charQuote := "'" + value := env[strings.Index(env, character)+1:] + name := env[0:strings.Index(env, character)] + name = strings.TrimSpace(name) + value = strings.TrimSpace(value) + if strings.Contains(value, charQuote) { + value = strings.Trim(value, "'") + } + envs = append(envs, kobject.EnvVar{ + Name: name, + Value: value, + }) + } else { + return envs, "Invalid container env " + env + } + } + } + return envs, "" +} + +// load Ports from bundles file +func loadPortsfromBundle(service bundlefile.Service) ([]kobject.Ports, string) { + ports := []kobject.Ports{} + for _, port := range service.Ports { + var p kobject.Protocol + switch port.Protocol { + default: + p = kobject.ProtocolTCP + case "TCP": + p = kobject.ProtocolTCP + case "UDP": + p = kobject.ProtocolUDP + } + ports = append(ports, kobject.Ports{ + HostPort: int32(port.Port), + ContainerPort: int32(port.Port), + Protocol: p, + }) + } + return ports, "" +} + +// load Bundlefile into KomposeObject +func LoadBundle(komposeObject *kobject.KomposeObject, file string) { + buf, err := ioutil.ReadFile(file) + if err != nil { + logrus.Fatalf("Failed to read bundles file: ", err) + } + reader := strings.NewReader(string(buf)) + bundle, err := bundlefile.LoadFile(reader) + if err != nil { + logrus.Fatalf("Failed to parse bundles file: ", err) + } + + for name, service := range bundle.Services { + kobject.CheckUnsupportedKey(service) + + serviceConfig := kobject.ServiceConfig{} + serviceConfig.Command = service.Command + serviceConfig.Args = service.Args + // convert bundle labels to annotations + serviceConfig.Annotations = service.Labels + + image, err := loadImage(service) + if err != "" { + logrus.Fatalf("Failed to load image from bundles file: " + err) + } + serviceConfig.Image = image + + envs, err := loadEnvVarsfromBundle(service) + if err != "" { + logrus.Fatalf("Failed to load envvar from bundles file: " + err) + } + serviceConfig.Environment = envs + + ports, err := loadPortsfromBundle(service) + if err != "" { + logrus.Fatalf("Failed to load ports from bundles file: " + err) + } + serviceConfig.Port = ports + + if service.WorkingDir != nil { + serviceConfig.WorkingDir = *service.WorkingDir + } + + komposeObject.ServiceConfigs[name] = serviceConfig + } +} diff --git a/pkg/loader/compose.go b/pkg/loader/compose.go index 529648a2..aee17070 100644 --- a/pkg/loader/compose.go +++ b/pkg/loader/compose.go @@ -1 +1,170 @@ +/* +Copyright 2016 Skippbox, Ltd All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package loader + +import ( + "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" + "github.com/skippbox/kompose/pkg/kobject" + "strconv" + "strings" +) + +// load Environment Variable from compose file +func loadEnvVarsfromCompose(e map[string]string) []kobject.EnvVar { + envs := []kobject.EnvVar{} + for k, v := range e { + envs = append(envs, kobject.EnvVar{ + Name: k, + Value: v, + }) + } + return envs +} + +// Load Ports from compose file +func loadPortsFromCompose(composePorts []string) ([]kobject.Ports, string) { + ports := []kobject.Ports{} + character := ":" + for _, port := range composePorts { + p := kobject.ProtocolTCP + if strings.Contains(port, character) { + hostPort := port[0:strings.Index(port, character)] + hostPort = strings.TrimSpace(hostPort) + hostPortInt, err := strconv.Atoi(hostPort) + if err != nil { + return nil, "Invalid host port of " + port + } + containerPort := port[strings.Index(port, character)+1:] + containerPort = strings.TrimSpace(containerPort) + containerPortInt, err := strconv.Atoi(containerPort) + if err != nil { + return nil, "Invalid container port of " + port + } + ports = append(ports, kobject.Ports{ + HostPort: int32(hostPortInt), + ContainerPort: int32(containerPortInt), + Protocol: p, + }) + } else { + containerPortInt, err := strconv.Atoi(port) + if err != nil { + return nil, "Invalid container port of " + port + } + ports = append(ports, kobject.Ports{ + ContainerPort: int32(containerPortInt), + Protocol: p, + }) + } + + } + return ports, "" +} + +// load Docker Compose file into KomposeObject +func LoadCompose(komposeObject *kobject.KomposeObject, file string) { + context := &docker.Context{} + if file == "" { + file = "docker-compose.yml" + } + context.ComposeFiles = []string{file} + + if context.ResourceLookup == nil { + context.ResourceLookup = &lookup.FileResourceLookup{} + } + + if context.EnvironmentLookup == nil { + cwd, err := os.Getwd() + if err != nil { + return KomposeObject{} + } + context.EnvironmentLookup = &lookup.ComposableEnvLookup{ + Lookups: []config.EnvironmentLookup{ + &lookup.EnvfileLookup{ + Path: filepath.Join(cwd, ".env"), + }, + &lookup.OsEnvLookup{}, + }, + } + } + + // load compose file into composeObject + composeObject := project.NewProject(&context.Context, nil, nil) + err := composeObject.Parse() + if err != nil { + logrus.Fatalf("Failed to load compose file", err) + } + + // transform composeObject into komposeObject + composeServiceNames := composeObject.ServiceConfigs.Keys() + + // volume config and network config are not supported + if len(composeObject.NetworkConfigs) > 0 { + logrus.Warningf("Unsupported network configuration of compose v2 - ignoring") + } + if len(composeObject.VolumeConfigs) > 0 { + logrus.Warningf("Unsupported volume configuration of compose v2 - ignoring") + } + + networksWarningFound := false + + for _, name := range composeServiceNames { + if composeServiceConfig, ok := composeObject.ServiceConfigs.Get(name); ok { + //FIXME: networks always contains one default element, even it isn't declared in compose v2. + if len(composeServiceConfig.Networks.Networks) > 0 && + composeServiceConfig.Networks.Networks[0].Name != "default" && + !networksWarningFound { + logrus.Warningf("Unsupported key networks - ignoring") + networksWarningFound = true + } + kobject.CheckUnsupportedKey(composeServiceConfig) + serviceConfig := kobject.ServiceConfig{} + serviceConfig.Image = composeServiceConfig.Image + serviceConfig.ContainerName = composeServiceConfig.ContainerName + + // load environment variables + envs := loadEnvVarsfromCompose(composeServiceConfig.Environment.ToMap()) + serviceConfig.Environment = envs + + // load ports + ports, err := loadPortsFromCompose(composeServiceConfig.Ports) + if err != "" { + logrus.Fatalf("Failed to load ports from compose file: " + err) + } + serviceConfig.Port = ports + + serviceConfig.WorkingDir = composeServiceConfig.WorkingDir + serviceConfig.Volumes = composeServiceConfig.Volumes + + // convert compose labels to annotations + serviceConfig.Annotations = map[string]string(composeServiceConfig.Labels) + + serviceConfig.CPUSet = composeServiceConfig.CPUSet + serviceConfig.CPUShares = composeServiceConfig.CPUShares + serviceConfig.CPUQuota = composeServiceConfig.CPUQuota + serviceConfig.CapAdd = composeServiceConfig.CapAdd + serviceConfig.CapDrop = composeServiceConfig.CapDrop + serviceConfig.Expose = composeServiceConfig.Expose + serviceConfig.Privileged = composeServiceConfig.Privileged + serviceConfig.Restart = composeServiceConfig.Restart + serviceConfig.User = composeServiceConfig.User + + komposeObject.ServiceConfigs[name] = serviceConfig + } + } +} diff --git a/pkg/transformer/kubernetes.go b/pkg/transformer/kubernetes.go index 38ef5e16..884a001f 100644 --- a/pkg/transformer/kubernetes.go +++ b/pkg/transformer/kubernetes.go @@ -1 +1,189 @@ package transformer + +import ( + "github.com/skippbox/kompose/pkg/kobject" + "fmt" + "os" +) + +func Transform(komposeObject *kobject.KomposeObject, opt kobject.ConvertOptions) (map[string][]byte, map[string][]byte, map[string][]byte, map[string][]byte, map[string][]byte, []string) { + mServices := make(map[string][]byte) + mReplicationControllers := make(map[string][]byte) + mDeployments := make(map[string][]byte) + mDaemonSets := make(map[string][]byte) + mReplicaSets := make(map[string][]byte) + // OpenShift DeploymentConfigs + mDeploymentConfigs := make(map[string][]byte) + + f := createOutFile(opt.outFile) + defer f.Close() + + var svcnames []string + + for name, service := range komposeObject.ServiceConfigs { + svcnames = append(svcnames, name) + + rc := initRC(name, service, opt.replicas) + sc := initSC(name, service) + dc := initDC(name, service, opt.replicas) + ds := initDS(name, service) + osDC := initDeploymentConfig(name, service, opt.replicas) // OpenShift DeploymentConfigs + + // Configure the environment variables. + envs := configEnvs(name, service) + + // Configure the container command. + var cmds []string + for _, cmd := range service.Command { + cmds = append(cmds, cmd) + } + // Configure the container volumes. + volumesMount, volumes := configVolumes(service) + + // Configure the container ports. + ports := configPorts(name, service) + + // Configure the service ports. + servicePorts := configServicePorts(name, service) + sc.Spec.Ports = servicePorts + + // Configure label + labels := map[string]string{"service": name} + sc.ObjectMeta.Labels = labels + // Configure annotations + annotations := map[string]string{} + for key, value := range service.Annotations { + annotations[key] = value + } + sc.ObjectMeta.Annotations = annotations + + // fillTemplate fills the pod template with the value calculated from config + fillTemplate := func(template *api.PodTemplateSpec) { + template.Spec.Containers[0].Env = envs + template.Spec.Containers[0].Command = cmds + template.Spec.Containers[0].WorkingDir = service.WorkingDir + template.Spec.Containers[0].VolumeMounts = volumesMount + template.Spec.Volumes = volumes + // Configure the container privileged mode + if service.Privileged == true { + template.Spec.Containers[0].SecurityContext = &api.SecurityContext{ + Privileged: &service.Privileged, + } + } + template.Spec.Containers[0].Ports = ports + template.ObjectMeta.Labels = labels + // Configure the container restart policy. + switch service.Restart { + case "", "always": + template.Spec.RestartPolicy = api.RestartPolicyAlways + case "no": + template.Spec.RestartPolicy = api.RestartPolicyNever + case "on-failure": + template.Spec.RestartPolicy = api.RestartPolicyOnFailure + default: + logrus.Fatalf("Unknown restart policy %s for service %s", service.Restart, name) + } + } + + // fillObjectMeta fills the metadata with the value calculated from config + fillObjectMeta := func(meta *api.ObjectMeta) { + meta.Labels = labels + meta.Annotations = annotations + } + + // Update each supported controllers + updateController(rc, fillTemplate, fillObjectMeta) + updateController(dc, fillTemplate, fillObjectMeta) + updateController(ds, fillTemplate, fillObjectMeta) + // OpenShift DeploymentConfigs + updateController(osDC, fillTemplate, fillObjectMeta) + + // convert datarc to json / yaml + datarc, err := transformer(rc, opt.generateYaml) + if err != nil { + logrus.Fatalf(err.Error()) + } + + // convert datadc to json / yaml + datadc, err := transformer(dc, opt.generateYaml) + if err != nil { + logrus.Fatalf(err.Error()) + } + + // convert datads to json / yaml + datads, err := transformer(ds, opt.generateYaml) + if err != nil { + logrus.Fatalf(err.Error()) + } + + var datasvc []byte + // If ports not provided in configuration we will not make service + if len(ports) == 0 { + logrus.Warningf("[%s] Service cannot be created because of missing port.", name) + } else if len(ports) != 0 { + // convert datasvc to json / yaml + datasvc, err = transformer(sc, opt.generateYaml) + if err != nil { + logrus.Fatalf(err.Error()) + } + } + + // convert OpenShift DeploymentConfig to json / yaml + dataDeploymentConfig, err := transformer(osDC, opt.generateYaml) + if err != nil { + logrus.Fatalf(err.Error()) + } + + mServices[name] = datasvc + mReplicationControllers[name] = datarc + mDeployments[name] = datadc + mDaemonSets[name] = datads + mDeploymentConfigs[name] = dataDeploymentConfig + } + + return mServices, mDeployments, mDaemonSets, mReplicationControllers, mDeploymentConfigs, svcnames +} + +func PrintControllers(mServices, mDeployments, mDaemonSets, mReplicationControllers, mDeploymentConfigs map[string][]byte, svcnames []string, opt convertOptions, f *os.File) { + for k, v := range mServices { + if v != nil { + print(k, "svc", v, opt.toStdout, opt.generateYaml, f) + } + } + + // If --out or --stdout is set, the validation should already prevent multiple controllers being generated + if opt.createD { + for k, v := range mDeployments { + print(k, "deployment", v, opt.toStdout, opt.generateYaml, f) + } + } + + if opt.createDS { + for k, v := range mDaemonSets { + print(k, "daemonset", v, opt.toStdout, opt.generateYaml, f) + } + } + + if opt.createRC { + for k, v := range mReplicationControllers { + print(k, "rc", v, opt.toStdout, opt.generateYaml, f) + } + } + + if f != nil { + fmt.Fprintf(os.Stdout, "file %q created\n", opt.outFile) + } + + if opt.createChart { + err := generateHelm(opt.inputFile, svcnames, opt.generateYaml, opt.createD, opt.createDS, opt.createRC, opt.outFile) + if err != nil { + logrus.Fatalf("Failed to create Chart data: %v", err) + } + } + + if opt.createDeploymentConfig { + for k, v := range mDeploymentConfigs { + print(k, "deploymentconfig", v, opt.toStdout, opt.generateYaml, f) + } + } +} diff --git a/version/version.go b/version/version.go index c22807ea..4dcae1d7 100644 --- a/version/version.go +++ b/version/version.go @@ -17,9 +17,9 @@ limitations under the License. package version var ( -// VERSION should be updated by hand at each release + // VERSION should be updated by hand at each release VERSION = "0.0.1-beta" -// GITCOMMIT will be overwritten automatically by the build system + // GITCOMMIT will be overwritten automatically by the build system GITCOMMIT = "HEAD" )