/* 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 transformer import ( "encoding/json" "fmt" "github.com/Sirupsen/logrus" "github.com/ghodss/yaml" "github.com/skippbox/kompose/pkg/kobject" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/util/intstr" "math/rand" "os" "strconv" "strings" deployapi "github.com/openshift/origin/pkg/deploy/api" "io/ioutil" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/runtime" ) const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789" // RandStringBytes generates randomly n-character string func RandStringBytes(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } // Create the file to write to if --out is specified func createOutFile(out string) *os.File { var f *os.File var err error if len(out) != 0 { f, err = os.Create(out) if err != nil { logrus.Fatalf("error opening file: %v", err) } } return f } // Init RC object func initRC(name string, service kobject.ServiceConfig, replicas int) *api.ReplicationController { rc := &api.ReplicationController{ TypeMeta: unversioned.TypeMeta{ Kind: "ReplicationController", APIVersion: "v1", }, ObjectMeta: api.ObjectMeta{ Name: name, //Labels: map[string]string{"service": name}, }, Spec: api.ReplicationControllerSpec{ Selector: map[string]string{"service": name}, Replicas: int32(replicas), Template: &api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ //Labels: map[string]string{"service": name}, }, Spec: api.PodSpec{ Containers: []api.Container{ { Name: name, Image: service.Image, }, }, }, }, }, } return rc } // Init SC object func initSC(name string, service kobject.ServiceConfig) *api.Service { sc := &api.Service{ TypeMeta: unversioned.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: api.ObjectMeta{ Name: name, //Labels: map[string]string{"service": name}, }, Spec: api.ServiceSpec{ Selector: map[string]string{"service": name}, }, } return sc } // Init DC object func initDC(name string, service kobject.ServiceConfig, replicas int) *extensions.Deployment { dc := &extensions.Deployment{ TypeMeta: unversioned.TypeMeta{ Kind: "Deployment", APIVersion: "extensions/v1beta1", }, ObjectMeta: api.ObjectMeta{ Name: name, Labels: map[string]string{"service": name}, }, Spec: extensions.DeploymentSpec{ Replicas: int32(replicas), Selector: &unversioned.LabelSelector{ MatchLabels: map[string]string{"service": name}, }, //UniqueLabelKey: p.Name, Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"service": name}, }, Spec: api.PodSpec{ Containers: []api.Container{ { Name: name, Image: service.Image, }, }, }, }, }, } return dc } // Init DS object func initDS(name string, service kobject.ServiceConfig) *extensions.DaemonSet { ds := &extensions.DaemonSet{ TypeMeta: unversioned.TypeMeta{ Kind: "DaemonSet", APIVersion: "extensions/v1beta1", }, ObjectMeta: api.ObjectMeta{ Name: name, }, Spec: extensions.DaemonSetSpec{ Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Name: name, }, Spec: api.PodSpec{ Containers: []api.Container{ { Name: name, Image: service.Image, }, }, }, }, }, } return ds } // initDeploymentConfig initialize OpenShifts DeploymentConfig object func initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int) *deployapi.DeploymentConfig { dc := &deployapi.DeploymentConfig{ TypeMeta: unversioned.TypeMeta{ Kind: "DeploymentConfig", APIVersion: "v1", }, ObjectMeta: api.ObjectMeta{ Name: name, Labels: map[string]string{"service": name}, }, Spec: deployapi.DeploymentConfigSpec{ Replicas: int32(replicas), Selector: map[string]string{"service": name}, //UniqueLabelKey: p.Name, Template: &api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"service": name}, }, Spec: api.PodSpec{ Containers: []api.Container{ { Name: name, Image: service.Image, }, }, }, }, }, } return dc } // Configure the environment variables. func configEnvs(name string, service kobject.ServiceConfig) []api.EnvVar { envs := []api.EnvVar{} for _, v := range service.Environment { envs = append(envs, api.EnvVar{ Name: v.Name, Value: v.Value, }) } return envs } // Configure the container volumes. func configVolumes(service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume) { volumesMount := []api.VolumeMount{} volumes := []api.Volume{} volumeSource := api.VolumeSource{} for _, volume := range service.Volumes { name, host, container, mode, err := parseVolume(volume) if err != nil { logrus.Warningf("Failed to configure container volume: %v", err) continue } // if volume name isn't specified, set it to a random string of 20 chars if len(name) == 0 { name = RandStringBytes(20) } // check if ro/rw mode is defined, default rw readonly := len(mode) > 0 && mode == "ro" volumesMount = append(volumesMount, api.VolumeMount{Name: name, ReadOnly: readonly, MountPath: container}) if len(host) > 0 { volumeSource = api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: host}} } else { volumeSource = api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}} } volumes = append(volumes, api.Volume{Name: name, VolumeSource: volumeSource}) } return volumesMount, volumes } // parseVolume parse a given volume, which might be [name:][host:]container[:access_mode] func parseVolume(volume string) (name, host, container, mode string, err error) { separator := ":" volumeStrings := strings.Split(volume, separator) if len(volumeStrings) == 0 { return } // Set name if existed if !isPath(volumeStrings[0]) { name = volumeStrings[0] volumeStrings = volumeStrings[1:] } if len(volumeStrings) == 0 { err = fmt.Errorf("invalid volume format: %s", volume) return } if volumeStrings[len(volumeStrings)-1] == "rw" || volumeStrings[len(volumeStrings)-1] == "ro" { mode = volumeStrings[len(volumeStrings)-1] volumeStrings = volumeStrings[:len(volumeStrings)-1] } container = volumeStrings[len(volumeStrings)-1] volumeStrings = volumeStrings[:len(volumeStrings)-1] if len(volumeStrings) == 1 { host = volumeStrings[0] } if !isPath(container) || (len(host) > 0 && !isPath(host)) || len(volumeStrings) > 1 { err = fmt.Errorf("invalid volume format: %s", volume) return } return } func isPath(substring string) bool { return strings.Contains(substring, "/") } // Configure the container ports. 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 kobject.ProtocolTCP: p = api.ProtocolTCP case kobject.ProtocolUDP: p = api.ProtocolUDP } ports = append(ports, api.ContainerPort{ ContainerPort: port.ContainerPort, Protocol: p, }) } return ports } // Configure the container service ports. func configServicePorts(name string, service kobject.ServiceConfig) []api.ServicePort { servicePorts := []api.ServicePort{} for _, port := range service.Port { if port.HostPort == 0 { port.HostPort = port.ContainerPort } var p api.Protocol switch port.Protocol { default: p = api.ProtocolTCP case kobject.ProtocolTCP: p = api.ProtocolTCP case kobject.ProtocolUDP: p = api.ProtocolUDP } var targetPort intstr.IntOrString targetPort.IntVal = port.ContainerPort targetPort.StrVal = strconv.Itoa(int(port.ContainerPort)) servicePorts = append(servicePorts, api.ServicePort{ Name: strconv.Itoa(int(port.HostPort)), Protocol: p, Port: port.HostPort, TargetPort: targetPort, }) } return servicePorts } // Transform data to json/yaml func transformer(obj runtime.Object, GenerateYaml bool) ([]byte, error) { // Convert to versioned object objectVersion := obj.GetObjectKind().GroupVersionKind() version := unversioned.GroupVersion{Group: objectVersion.Group, Version: objectVersion.Version} versionedObj, err := api.Scheme.ConvertToVersion(obj, version) if err != nil { return nil, err } // convert data to json / yaml data, err := json.MarshalIndent(versionedObj, "", " ") if GenerateYaml == true { data, err = yaml.Marshal(versionedObj) } if err != nil { return nil, err } logrus.Debugf("%s\n", data) return data, nil } 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) // 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 kobject.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) } } } // updateController updates the given object with the given pod template update function and ObjectMeta update function func updateController(obj runtime.Object, updateTemplate func(*api.PodTemplateSpec), updateMeta func(meta *api.ObjectMeta)) { switch t := obj.(type) { case *api.ReplicationController: if t.Spec.Template == nil { t.Spec.Template = &api.PodTemplateSpec{} } updateTemplate(t.Spec.Template) updateMeta(&t.ObjectMeta) case *extensions.Deployment: updateTemplate(&t.Spec.Template) updateMeta(&t.ObjectMeta) case *extensions.ReplicaSet: updateTemplate(&t.Spec.Template) updateMeta(&t.ObjectMeta) case *extensions.DaemonSet: updateTemplate(&t.Spec.Template) updateMeta(&t.ObjectMeta) case *deployapi.DeploymentConfig: updateTemplate(t.Spec.Template) updateMeta(&t.ObjectMeta) } } func print(name, trailing string, data []byte, toStdout, generateYaml bool, f *os.File) { file := fmt.Sprintf("%s-%s.json", name, trailing) if generateYaml { file = fmt.Sprintf("%s-%s.yaml", name, trailing) } separator := "" if generateYaml { separator = "---" } if toStdout { fmt.Fprintf(os.Stdout, "%s%s\n", string(data), separator) } else if f != nil { // Write all content to a single file f if _, err := f.WriteString(fmt.Sprintf("%s%s\n", string(data), separator)); err != nil { logrus.Fatalf("Failed to write %s to file: %v", trailing, err) } f.Sync() } else { // Write content separately to each file if err := ioutil.WriteFile(file, []byte(data), 0644); err != nil { logrus.Fatalf("Failed to write %s: %v", trailing, err) } fmt.Fprintf(os.Stdout, "file %q created\n", file) } }