diff --git a/cli/app/app.go b/cli/app/app.go index 1ab9132d..9ec0c94e 100644 --- a/cli/app/app.go +++ b/cli/app/app.go @@ -18,145 +18,33 @@ package app import ( "fmt" - "math/rand" - "os" - "path/filepath" - "strconv" - "strings" "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" - // install kubernetes api _ "k8s.io/kubernetes/pkg/api/install" _ "k8s.io/kubernetes/pkg/apis/extensions/install" - - "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/api/unversioned" - "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" - deployapi "github.com/openshift/origin/pkg/deploy/api" // 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/loader" + "github.com/skippbox/kompose/pkg/loader/bundle" + "github.com/skippbox/kompose/pkg/loader/compose" + "github.com/skippbox/kompose/pkg/transformer" + "github.com/skippbox/kompose/pkg/transformer/kubernetes" + "github.com/skippbox/kompose/pkg/transformer/openshift" ) const ( - letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789" 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 composeOptions = map[string]string{ - "Build": "build", - "CapAdd": "cap_add", - "CapDrop": "cap_drop", - "CPUSet": "cpuset", - "CPUShares": "cpu_shares", - "CPUQuota": "cpu_quota", - "CgroupParent": "cgroup_parent", - "Devices": "devices", - "DependsOn": "depends_on", - "DNS": "dns", - "DNSSearch": "dns_search", - "DomainName": "domainname", - "Entrypoint": "entrypoint", - "EnvFile": "env_file", - "Expose": "expose", - "Extends": "extends", - "ExternalLinks": "external_links", - "ExtraHosts": "extra_hosts", - "Hostname": "hostname", - "Ipc": "ipc", - "Logging": "logging", - "MacAddress": "mac_address", - "MemLimit": "mem_limit", - "MemSwapLimit": "memswap_limit", - "NetworkMode": "network_mode", - "Networks": "networks", - "Pid": "pid", - "SecurityOpt": "security_opt", - "ShmSize": "shm_size", - "StopSignal": "stop_signal", - "VolumeDriver": "volume_driver", - "VolumesFrom": "volumes_from", - "Uts": "uts", - "ReadOnly": "read_only", - "StdinOpen": "stdin_open", - "Tty": "tty", - "User": "user", - "Ulimits": "ulimits", - "Dockerfile": "dockerfile", - "Net": "net", - "Args": "args", -} - -// 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) -} +var inputFormat = "compose" // BeforeApp is an action that is executed before any cli command. func BeforeApp(c *cli.Context) error { @@ -301,846 +189,28 @@ func Scale(c *cli.Context) { //} } -// 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 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 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 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 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 -} - -// Init RS object -func initRS(name string, service ServiceConfig, replicas int) *extensions.ReplicaSet { - rs := &extensions.ReplicaSet{ - TypeMeta: unversioned.TypeMeta{ - Kind: "ReplicaSet", - APIVersion: "extensions/v1beta1", - }, - ObjectMeta: api.ObjectMeta{ - Name: name, - }, - Spec: extensions.ReplicaSetSpec{ - Replicas: int32(replicas), - Selector: &unversioned.LabelSelector{ - MatchLabels: map[string]string{"service": name}, - }, - Template: api.PodTemplateSpec{ - ObjectMeta: api.ObjectMeta{}, - Spec: api.PodSpec{ - Containers: []api.Container{ - { - Name: name, - Image: service.Image, - }, - }, - }, - }, - }, - } - return rs -} - -// initDeploymentConfig initialize OpenShifts DeploymentConfig object -func initDeploymentConfig(name string, service 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 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 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 ServiceConfig) []api.ContainerPort { - ports := []api.ContainerPort{} - for _, port := range service.Port { - var p api.Protocol - switch port.Protocol { - default: - p = api.ProtocolTCP - case ProtocolTCP: - p = api.ProtocolTCP - case 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 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 ProtocolTCP: - p = api.ProtocolTCP - case 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 -} - -// 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 - - // this will hold all the converted data - var allobjects []runtime.Object - for name, service := range komposeObject.ServiceConfigs { - var objects []runtime.Object - svcnames = append(svcnames, name) - sc := initSC(name, service) - - if opt.createD { - objects = append(objects, initDC(name, service, opt.replicas)) - } - if opt.createDS { - objects = append(objects, initDS(name, service)) - } - if opt.createRC { - objects = append(objects, initRC(name, service, opt.replicas)) - } - if opt.createDeploymentConfig { - objects = append(objects, 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 labels - 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 supported controller - for _, obj := range objects { - updateController(obj, fillTemplate, fillObjectMeta) - } - - // 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 { - objects = append(objects, sc) - } - allobjects = append(allobjects, objects...) - } - return allobjects -} - -// PrintList will take the data converted and decide on the commandline attributes given -func PrintList(objects []runtime.Object, opt convertOptions) error { - f := createOutFile(opt.outFile) - defer f.Close() - - var err error - var files []string - - // if asked to print to stdout or to put in single file - // we will create a list - if opt.toStdout || f != nil { - list := &api.List{} - list.Items = objects - - // version each object in the list - list.Items, err = ConvertToVersion(list.Items) - if err != nil { - return err - } - - // version list itself - listVersion := unversioned.GroupVersion{Group: "", Version: "v1"} - convertedList, err := api.Scheme.ConvertToVersion(list, listVersion) - if err != nil { - return err - } - data, err := marshal(convertedList, opt.generateYaml) - if err != nil { - return fmt.Errorf("Error in marshalling the List: %v", err) - } - files = append(files, print("", "", data, opt.toStdout, opt.generateYaml, f)) - } else { - var file string - // create a separate file for each provider - for _, v := range objects { - data, err := marshal(v, opt.generateYaml) - if err != nil { - return err - } - switch t := v.(type) { - case *api.ReplicationController: - file = print(t.Name, strings.ToLower(t.Kind), data, opt.toStdout, opt.generateYaml, f) - case *extensions.Deployment: - file = print(t.Name, strings.ToLower(t.Kind), data, opt.toStdout, opt.generateYaml, f) - case *extensions.DaemonSet: - file = print(t.Name, strings.ToLower(t.Kind), data, opt.toStdout, opt.generateYaml, f) - case *deployapi.DeploymentConfig: - file = print(t.Name, strings.ToLower(t.Kind), data, opt.toStdout, opt.generateYaml, f) - case *api.Service: - file = print(t.Name, strings.ToLower(t.Kind), data, opt.toStdout, opt.generateYaml, f) - } - files = append(files, file) - - } - } - if opt.createChart { - generateHelm(opt.inputFile, files) - } - return nil -} - -// marshal object runtime.Object and return byte array -func marshal(obj runtime.Object, yamlFormat bool) (data []byte, err error) { - // convert data to yaml or json - if yamlFormat { - data, err = yaml.Marshal(obj) - } else { - data, err = json.MarshalIndent(obj, "", " ") - } - if err != nil { - data = nil - } - return -} - -// Convert all objects in objs to versioned objects -func ConvertToVersion(objs []runtime.Object) ([]runtime.Object, error) { - ret := []runtime.Object{} - - for _, obj := range objs { - - objectVersion := obj.GetObjectKind().GroupVersionKind() - version := unversioned.GroupVersion{Group: objectVersion.Group, Version: objectVersion.Version} - convertedObject, err := api.Scheme.ConvertToVersion(obj, version) - if err != nil { - return nil, err - } - ret = append(ret, convertedObject) - } - - 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,79 +242,56 @@ 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, } validateFlags(opt, singleOutput, dabFile, inputFile) - komposeObject := KomposeObject{} + // loader parses input from file into komposeObject. + var l loader.Loader + switch inputFormat { + case "bundle": + l = new(bundle.Bundle) + case "compose": + l = new(compose.Compose) + default: + logrus.Fatalf("Input file format is not supported") + } - if len(dabFile) > 0 { - komposeObject = loadBundlesFile(dabFile) + komposeObject = l.LoadFile(file) + + // transformer maps komposeObject to provider's primitives + var t transformer.Transformer + if !createDeploymentConfig { + t = new(kubernetes.Kubernetes) } else { - komposeObject = loadComposeFile(inputFile) + t = new(openshift.OpenShift) } - // Convert komposeObject to K8S controllers - objects := komposeConvert(komposeObject, opt) + objects := t.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()]++ - } - } - } -} - -// Either print to stdout or to file/s -func print(name, trailing string, data []byte, toStdout, generateYaml bool, f *os.File) string { - - file := "" - if generateYaml { - file = fmt.Sprintf("%s-%s.yaml", name, trailing) - } else { - file = fmt.Sprintf("%s-%s.json", name, trailing) - } - if toStdout { - fmt.Fprintf(os.Stdout, "%s\n", string(data)) - return "" - } else if f != nil { - // Write all content to a single file f - if _, err := f.WriteString(fmt.Sprintf("%s\n", string(data))); 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) - } - logrus.Printf("file %q created", file) - } - return file + // Print output + kubernetes.PrintList(objects, opt) } // Up brings up deployment, svc. @@ -1262,52 +309,52 @@ func Up(c *cli.Context) { inputFile := c.String("file") dabFile := c.String("bundle") - komposeObject := KomposeObject{} - opt := convertOptions{ - replicas: 1, - createD: true, + komposeObject := kobject.KomposeObject{ + ServiceConfigs: make(map[string]kobject.ServiceConfig), + } + + file := inputFile + if len(dabFile) > 0 { + inputFormat = "bundle" + file = dabFile + } + + opt := kobject.ConvertOptions{ + Replicas: 1, + CreateD: true, } validateFlags(opt, false, dabFile, inputFile) - if len(dabFile) > 0 { - komposeObject = loadBundlesFile(dabFile) - } else { - komposeObject = loadComposeFile(inputFile) + // loader parses input from file into komposeObject. + var l loader.Loader + switch inputFormat { + case "bundle": + l = new(bundle.Bundle) + case "compose": + l = new(compose.Compose) + default: + logrus.Fatalf("Input file format is not supported") } + komposeObject = l.LoadFile(file) + + t := new(kubernetes.Kubernetes) //Convert komposeObject to K8S controllers - objects := komposeConvert(komposeObject, opt) - objects = sortServicesFirst(objects) + objects := t.Transform(komposeObject, opt) + sortServicesFirst(&objects) - for _, v := range objects { - switch t := v.(type) { - case *extensions.Deployment: - _, err := client.Deployments(api.NamespaceDefault).Create(t) - if err != nil { - logrus.Fatalf("Error: '%v' while creating deployment: %s", err, t.Name) - } - logrus.Infof("Successfully created deployment: %s", t.Name) - case *api.Service: - _, err := client.Services(api.NamespaceDefault).Create(t) - if err != nil { - logrus.Fatalf("Error: '%v' while creating service: %s", err, t.Name) - } - logrus.Infof("Successfully created service: %s", t.Name) - } - } - fmt.Println("\nApplication has been deployed to Kubernetes. You can run 'kubectl get deployment,svc' for details.") + //Submit objects to K8s endpoint + kubernetes.CreateObjects(client, objects) } // the objects that we get can be in any order this keeps services first // according to best practice kubernetes services should be created first // http://kubernetes.io/docs/user-guide/config-best-practices/ -func sortServicesFirst(objs []runtime.Object) []runtime.Object { - var svc []runtime.Object - var others []runtime.Object - var ret []runtime.Object +func sortServicesFirst(objs *[]runtime.Object) { + var svc, others, ret []runtime.Object - for _, obj := range objs { + for _, obj := range *objs { if obj.GetObjectKind().GroupVersionKind().Kind == "Service" { svc = append(svc, obj) } else { @@ -1316,29 +363,5 @@ func sortServicesFirst(objs []runtime.Object) []runtime.Object { } ret = append(ret, svc...) ret = append(ret, others...) - return ret -} - -// 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) - } + *objs = ret } diff --git a/cli/app/app_test.go b/cli/app/app_test.go index b02d9b81..b19aed54 100644 --- a/cli/app/app_test.go +++ b/cli/app/app_test.go @@ -19,6 +19,7 @@ package app import ( "fmt" "testing" + "github.com/skippbox/kompose/pkg/transformer" ) func TestParseVolume(t *testing.T) { @@ -99,7 +100,7 @@ func TestParseVolume(t *testing.T) { } for _, test := range tests { - name, host, container, mode, err := parseVolume(test.volume) + name, host, container, mode, err := transformer.ParseVolume(test.volume) if err != nil { t.Errorf("In test case %q, returned unexpected error %v", test.test, err) } diff --git a/cli/app/k8sutils.go b/cli/app/k8sutils.go deleted file mode 100644 index 30fc604b..00000000 --- a/cli/app/k8sutils.go +++ /dev/null @@ -1,110 +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 - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "text/template" - - "github.com/Sirupsen/logrus" -) - -/** - * Generate Helm Chart configuration - */ -func generateHelm(filename string, outFiles []string) error { - type ChartDetails struct { - Name string - } - - extension := filepath.Ext(filename) - dirName := filename[0 : len(filename)-len(extension)] - details := ChartDetails{dirName} - manifestDir := dirName + string(os.PathSeparator) + "templates" - dir, err := os.Open(dirName) - - /* Setup the initial directories/files */ - if err == nil { - _ = dir.Close() - } - - if err != nil { - err = os.Mkdir(dirName, 0755) - if err != nil { - return err - } - - err = os.Mkdir(manifestDir, 0755) - if err != nil { - return err - } - - /* Create the readme file */ - readme := "This chart was created by Kompose\n" - err = ioutil.WriteFile(dirName+string(os.PathSeparator)+"README.md", []byte(readme), 0644) - if err != nil { - return err - } - - /* Create the Chart.yaml file */ - chart := `name: {{.Name}} -description: A generated Helm Chart for {{.Name}} from Skippbox Kompose -version: 0.0.1 -keywords: - - {{.Name}} -sources: -home: -` - - t, err := template.New("ChartTmpl").Parse(chart) - if err != nil { - logrus.Fatalf("Failed to generate Chart.yaml template: %s\n", err) - } - var chartData bytes.Buffer - _ = t.Execute(&chartData, details) - - err = ioutil.WriteFile(dirName+string(os.PathSeparator)+"Chart.yaml", chartData.Bytes(), 0644) - if err != nil { - return err - } - } - - /* Copy all related json/yaml files into the newly created manifests directory */ - for _, filename := range outFiles { - if err = cpFileToChart(manifestDir, filename); err != nil { - logrus.Warningln(err) - } - if err = os.Remove(filename); err != nil { - logrus.Warningln(err) - } - } - logrus.Infof("chart created in %q\n", "."+string(os.PathSeparator)+dirName+string(os.PathSeparator)) - return nil -} - -func cpFileToChart(manifestDir, filename string) error { - infile, err := ioutil.ReadFile(filename) - if err != nil { - logrus.Warningf("Error reading %s: %s\n", filename, err) - return err - } - - return ioutil.WriteFile(manifestDir+string(os.PathSeparator)+filename, infile, 0644) -} 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..b3ae3e31 --- /dev/null +++ b/pkg/kobject/kobject.go @@ -0,0 +1,188 @@ +/* +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" + "github.com/fatih/structs" +) + +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 composeOptions = map[string]string{ + "Build": "build", + "CapAdd": "cap_add", + "CapDrop": "cap_drop", + "CPUSet": "cpuset", + "CPUShares": "cpu_shares", + "CPUQuota": "cpu_quota", + "CgroupParent": "cgroup_parent", + "Devices": "devices", + "DependsOn": "depends_on", + "DNS": "dns", + "DNSSearch": "dns_search", + "DomainName": "domainname", + "Entrypoint": "entrypoint", + "EnvFile": "env_file", + "Expose": "expose", + "Extends": "extends", + "ExternalLinks": "external_links", + "ExtraHosts": "extra_hosts", + "Hostname": "hostname", + "Ipc": "ipc", + "Logging": "logging", + "MacAddress": "mac_address", + "MemLimit": "mem_limit", + "MemSwapLimit": "memswap_limit", + "NetworkMode": "network_mode", + "Networks": "networks", + "Pid": "pid", + "SecurityOpt": "security_opt", + "ShmSize": "shm_size", + "StopSignal": "stop_signal", + "VolumeDriver": "volume_driver", + "VolumesFrom": "volumes_from", + "Uts": "uts", + "ReadOnly": "read_only", + "StdinOpen": "stdin_open", + "Tty": "tty", + "User": "user", + "Ulimits": "ulimits", + "Dockerfile": "dockerfile", + "Net": "net", + "Args": "args", +} + +// 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 + 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" +) + +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()]++ + } + } + } +} diff --git a/pkg/loader/bundle/bundle.go b/pkg/loader/bundle/bundle.go new file mode 100644 index 00000000..4a4c684e --- /dev/null +++ b/pkg/loader/bundle/bundle.go @@ -0,0 +1,150 @@ +/* +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 bundle + +import ( + "io/ioutil" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/client/bundlefile" + "github.com/skippbox/kompose/pkg/kobject" +) + +type Bundle struct { +} + +// 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 (b *Bundle) LoadFile(file string) kobject.KomposeObject { + komposeObject := kobject.KomposeObject{ + ServiceConfigs: make(map[string]kobject.ServiceConfig), + } + + 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 + } + + return komposeObject +} diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go new file mode 100644 index 00000000..d7d45eda --- /dev/null +++ b/pkg/loader/compose/compose.go @@ -0,0 +1,183 @@ +/* +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 compose + +import ( + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/config" + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/lookup" + "github.com/docker/libcompose/project" + "github.com/skippbox/kompose/pkg/kobject" +) + +type Compose struct { +} + +// 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 (c *Compose) LoadFile(file string) kobject.KomposeObject { + komposeObject := kobject.KomposeObject{ + ServiceConfigs: make(map[string]kobject.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 kobject.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 composeServiceConfig.Networks != nil && 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 + } + } + + return komposeObject +} diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go new file mode 100644 index 00000000..d4f07e19 --- /dev/null +++ b/pkg/loader/loader.go @@ -0,0 +1,23 @@ +/* +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/skippbox/kompose/pkg/kobject" + +type Loader interface { + LoadFile(file string) kobject.KomposeObject +} diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go new file mode 100644 index 00000000..ba421f5d --- /dev/null +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -0,0 +1,215 @@ +/* +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 kubernetes + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/Sirupsen/logrus" + "github.com/ghodss/yaml" + "github.com/skippbox/kompose/pkg/kobject" + "github.com/skippbox/kompose/pkg/transformer" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/runtime" + + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +/** + * Generate Helm Chart configuration + */ +func generateHelm(filename string, outFiles []string) error { + type ChartDetails struct { + Name string + } + + extension := filepath.Ext(filename) + dirName := filename[0 : len(filename)-len(extension)] + details := ChartDetails{dirName} + manifestDir := dirName + string(os.PathSeparator) + "templates" + dir, err := os.Open(dirName) + + /* Setup the initial directories/files */ + if err == nil { + _ = dir.Close() + } + + if err != nil { + err = os.Mkdir(dirName, 0755) + if err != nil { + return err + } + + err = os.Mkdir(manifestDir, 0755) + if err != nil { + return err + } + + /* Create the readme file */ + readme := "This chart was created by Kompose\n" + err = ioutil.WriteFile(dirName+string(os.PathSeparator)+"README.md", []byte(readme), 0644) + if err != nil { + return err + } + + /* Create the Chart.yaml file */ + chart := `name: {{.Name}} +description: A generated Helm Chart for {{.Name}} from Skippbox Kompose +version: 0.0.1 +keywords: + - {{.Name}} +sources: +home: +` + + t, err := template.New("ChartTmpl").Parse(chart) + if err != nil { + logrus.Fatalf("Failed to generate Chart.yaml template: %s\n", err) + } + var chartData bytes.Buffer + _ = t.Execute(&chartData, details) + + err = ioutil.WriteFile(dirName+string(os.PathSeparator)+"Chart.yaml", chartData.Bytes(), 0644) + if err != nil { + return err + } + } + + /* Copy all related json/yaml files into the newly created manifests directory */ + for _, filename := range outFiles { + if err = cpFileToChart(manifestDir, filename); err != nil { + logrus.Warningln(err) + } + if err = os.Remove(filename); err != nil { + logrus.Warningln(err) + } + } + logrus.Infof("chart created in %q\n", "."+string(os.PathSeparator)+dirName+string(os.PathSeparator)) + return nil +} + +func cpFileToChart(manifestDir, filename string) error { + infile, err := ioutil.ReadFile(filename) + if err != nil { + logrus.Warningf("Error reading %s: %s\n", filename, err) + return err + } + + return ioutil.WriteFile(manifestDir+string(os.PathSeparator)+filename, infile, 0644) +} + +// PrintList will take the data converted and decide on the commandline attributes given +func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { + f := transformer.CreateOutFile(opt.OutFile) + defer f.Close() + + var err error + var files []string + + // if asked to print to stdout or to put in single file + // we will create a list + if opt.ToStdout || f != nil { + list := &api.List{} + list.Items = objects + + // version each object in the list + list.Items, err = convertToVersion(list.Items) + if err != nil { + return err + } + + // version list itself + listVersion := unversioned.GroupVersion{Group: "", Version: "v1"} + convertedList, err := api.Scheme.ConvertToVersion(list, listVersion) + if err != nil { + return err + } + data, err := marshal(convertedList, opt.GenerateYaml) + if err != nil { + return fmt.Errorf("Error in marshalling the List: %v", err) + } + files = append(files, transformer.Print("", "", data, opt.ToStdout, opt.GenerateYaml, f)) + } else { + var file string + // create a separate file for each provider + for _, v := range objects { + data, err := marshal(v, opt.GenerateYaml) + if err != nil { + return err + } + switch t := v.(type) { + case *api.ReplicationController: + file = transformer.Print(t.Name, strings.ToLower(t.Kind), data, opt.ToStdout, opt.GenerateYaml, f) + case *extensions.Deployment: + file = transformer.Print(t.Name, strings.ToLower(t.Kind), data, opt.ToStdout, opt.GenerateYaml, f) + case *extensions.DaemonSet: + file = transformer.Print(t.Name, strings.ToLower(t.Kind), data, opt.ToStdout, opt.GenerateYaml, f) + case *deployapi.DeploymentConfig: + file = transformer.Print(t.Name, strings.ToLower(t.Kind), data, opt.ToStdout, opt.GenerateYaml, f) + case *api.Service: + file = transformer.Print(t.Name, strings.ToLower(t.Kind), data, opt.ToStdout, opt.GenerateYaml, f) + } + files = append(files, file) + } + } + if opt.CreateChart { + generateHelm(opt.InputFile, files) + } + return nil +} + +// marshal object runtime.Object and return byte array +func marshal(obj runtime.Object, yamlFormat bool) (data []byte, err error) { + // convert data to yaml or json + if yamlFormat { + data, err = yaml.Marshal(obj) + } else { + data, err = json.MarshalIndent(obj, "", " ") + } + if err != nil { + data = nil + } + return +} + +// Convert all objects in objs to versioned objects +func convertToVersion(objs []runtime.Object) ([]runtime.Object, error) { + ret := []runtime.Object{} + + for _, obj := range objs { + + objectVersion := obj.GetObjectKind().GroupVersionKind() + version := unversioned.GroupVersion{Group: objectVersion.Group, Version: objectVersion.Version} + convertedObject, err := api.Scheme.ConvertToVersion(obj, version) + if err != nil { + return nil, err + } + ret = append(ret, convertedObject) + } + + return ret, nil +} diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go new file mode 100644 index 00000000..a42b1cd4 --- /dev/null +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -0,0 +1,385 @@ +/* +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 kubernetes + +import ( + "fmt" + "strconv" + + "github.com/Sirupsen/logrus" + deployapi "github.com/openshift/origin/pkg/deploy/api" + "github.com/skippbox/kompose/pkg/kobject" + "github.com/skippbox/kompose/pkg/transformer" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/intstr" +) + +type Kubernetes struct { +} + +// 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 +} + +// 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 +} + +// 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 := transformer.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 = transformer.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 +} + +// 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 +} + +func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.ConvertOptions) []runtime.Object { + var svcnames []string + + // this will hold all the converted data + var allobjects []runtime.Object + + for name, service := range komposeObject.ServiceConfigs { + var objects []runtime.Object + svcnames = append(svcnames, name) + + sc := InitSC(name, service) + + if opt.CreateD { + objects = append(objects, InitDC(name, service, opt.Replicas)) + } + if opt.CreateDS { + objects = append(objects, InitDS(name, service)) + } + if opt.CreateRC { + objects = append(objects, InitRC(name, service, opt.Replicas)) + } + + // Configure the environment variables. + envs := ConfigEnvs(name, service) + + // Configure the container command. + cmds := transformer.ConfigCommands(service) + + // 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 := transformer.ConfigLabels(name) + sc.ObjectMeta.Labels = labels + + // Configure annotations + annotations := transformer.ConfigAnnotations(service) + 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 supported controller + for _, obj := range objects { + UpdateController(obj, fillTemplate, fillObjectMeta) + } + + // 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 { + objects = append(objects, sc) + } + allobjects = append(allobjects, objects...) + } + + return allobjects +} + +// 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.DaemonSet: + updateTemplate(&t.Spec.Template) + updateMeta(&t.ObjectMeta) + case *deployapi.DeploymentConfig: + updateTemplate(t.Spec.Template) + updateMeta(&t.ObjectMeta) + } +} + +func CreateObjects(client *client.Client, objects []runtime.Object) { + for _, v := range objects { + switch t := v.(type) { + case *extensions.Deployment: + _, err := client.Deployments(api.NamespaceDefault).Create(t) + if err != nil { + logrus.Fatalf("Error: '%v' while creating deployment: %s", err, t.Name) + } + logrus.Infof("Successfully created deployment: %s", t.Name) + case *api.Service: + _, err := client.Services(api.NamespaceDefault).Create(t) + if err != nil { + logrus.Fatalf("Error: '%v' while creating service: %s", err, t.Name) + } + logrus.Infof("Successfully created service: %s", t.Name) + } + } + fmt.Println("\nApplication has been deployed to Kubernetes. You can run 'kubectl get deployment,svc' for details.") +} diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go new file mode 100644 index 00000000..aa21fa09 --- /dev/null +++ b/pkg/transformer/openshift/openshift.go @@ -0,0 +1,165 @@ +/* +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 openshift + +import ( + "github.com/Sirupsen/logrus" + deployapi "github.com/openshift/origin/pkg/deploy/api" + "github.com/skippbox/kompose/pkg/kobject" + "github.com/skippbox/kompose/pkg/transformer" + "github.com/skippbox/kompose/pkg/transformer/kubernetes" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +type OpenShift struct { +} + +// 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 +} + +func (k *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.ConvertOptions) []runtime.Object { + var svcnames []string + + // this will hold all the converted data + var allobjects []runtime.Object + + for name, service := range komposeObject.ServiceConfigs { + var objects []runtime.Object + svcnames = append(svcnames, name) + + sc := kubernetes.InitSC(name, service) + + if opt.CreateD { + objects = append(objects, kubernetes.InitDC(name, service, opt.Replicas)) + } + if opt.CreateDS { + objects = append(objects, kubernetes.InitDS(name, service)) + } + if opt.CreateRC { + objects = append(objects, kubernetes.InitRC(name, service, opt.Replicas)) + } + if opt.CreateDeploymentConfig { + objects = append(objects, initDeploymentConfig(name, service, opt.Replicas)) // OpenShift DeploymentConfigs + } + + // Configure the environment variables. + envs := kubernetes.ConfigEnvs(name, service) + + // Configure the container command. + cmds := transformer.ConfigCommands(service) + + // Configure the container volumes. + volumesMount, volumes := kubernetes.ConfigVolumes(service) + + // Configure the container ports. + ports := kubernetes.ConfigPorts(name, service) + + // Configure the service ports. + servicePorts := kubernetes.ConfigServicePorts(name, service) + sc.Spec.Ports = servicePorts + + // Configure label + labels := transformer.ConfigLabels(name) + sc.ObjectMeta.Labels = labels + + // Configure annotations + annotations := transformer.ConfigAnnotations(service) + 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 supported controller + for _, obj := range objects { + kubernetes.UpdateController(obj, fillTemplate, fillObjectMeta) + } + + // 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 { + objects = append(objects, sc) + } + allobjects = append(allobjects, objects...) + } + + return allobjects +} diff --git a/pkg/transformer/transformer.go b/pkg/transformer/transformer.go new file mode 100644 index 00000000..bf332c56 --- /dev/null +++ b/pkg/transformer/transformer.go @@ -0,0 +1,26 @@ +/* +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 ( + "github.com/skippbox/kompose/pkg/kobject" + "k8s.io/kubernetes/pkg/runtime" +) + +type Transformer interface { + Transform(kobject.KomposeObject, kobject.ConvertOptions) []runtime.Object +} diff --git a/pkg/transformer/utils.go b/pkg/transformer/utils.go new file mode 100644 index 00000000..301b4d3b --- /dev/null +++ b/pkg/transformer/utils.go @@ -0,0 +1,153 @@ +package transformer + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/ghodss/yaml" + "github.com/skippbox/kompose/pkg/kobject" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "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 +} + +// Configure the container commands +func ConfigCommands(service kobject.ServiceConfig) []string { + var cmds []string + for _, cmd := range service.Command { + cmds = append(cmds, cmd) + } + + return cmds +} + +// 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 label +func ConfigLabels(name string) map[string]string { + return map[string]string{"service": name} +} + +// Configure annotations +func ConfigAnnotations(service kobject.ServiceConfig) map[string]string { + annotations := map[string]string{} + for key, value := range service.Annotations { + annotations[key] = value + } + + return annotations +} + +// Transform data to json/yaml +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} + 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 +} + +// Either print to stdout or to file/s +func Print(name, trailing string, data []byte, toStdout, generateYaml bool, f *os.File) string { + + file := "" + if generateYaml { + file = fmt.Sprintf("%s-%s.yaml", name, trailing) + } else { + file = fmt.Sprintf("%s-%s.json", name, trailing) + } + if toStdout { + fmt.Fprintf(os.Stdout, "%s\n", string(data)) + return "" + } else if f != nil { + // Write all content to a single file f + if _, err := f.WriteString(fmt.Sprintf("%s\n", string(data))); 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) + } + logrus.Printf("file %q created", file) + } + return file +} 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" )