From 9bdc4cf2dd516590135cd12bd6bf27816f0bbffd Mon Sep 17 00:00:00 2001 From: ngtuna Date: Wed, 29 Jun 2016 00:14:42 +0700 Subject: [PATCH] initial cli, picked up from kompose-lite --- cli/app/app.go | 780 ++++++++++++++++++++++++++++++++++++++ cli/app/k8sutils.go | 110 ++++++ cli/app/types.go | 28 ++ cli/command/command.go | 165 ++++++++ cli/docker/app/factory.go | 39 ++ cli/main/main.go | 49 +++ version/version.go | 25 ++ 7 files changed, 1196 insertions(+) create mode 100644 cli/app/app.go create mode 100644 cli/app/k8sutils.go create mode 100644 cli/app/types.go create mode 100644 cli/command/command.go create mode 100644 cli/docker/app/factory.go create mode 100644 cli/main/main.go create mode 100644 version/version.go diff --git a/cli/app/app.go b/cli/app/app.go new file mode 100644 index 00000000..77ddd111 --- /dev/null +++ b/cli/app/app.go @@ -0,0 +1,780 @@ +/* +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 ( + "fmt" + "math/rand" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/urfave/cli" + + "github.com/docker/libcompose/project" + + "encoding/json" + "io/ioutil" + + "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/util/intstr" + + "github.com/ghodss/yaml" +) + +type ProjectAction func(project *project.Project, c *cli.Context) + +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) +} + +// BeforeApp is an action that is executed before any cli command. +func BeforeApp(c *cli.Context) error { + if c.GlobalBool("verbose") { + logrus.SetLevel(logrus.DebugLevel) + } + // logrus.Warning("Note: This is an experimental alternate implementation of the Docker Compose CLI (https://github.com/docker/compose)") + return nil +} + +// WithProject is an helper function to create a cli.Command action with a ProjectFactory. +func WithProject(factory ProjectFactory, action ProjectAction) func(context *cli.Context) { + return func(context *cli.Context) { + p, err := factory.Create(context) + if err != nil { + logrus.Fatalf("Failed to read project: %v", err) + } + action(p, context) + } +} + +// ProjectKuberPS lists all rc, svc. +func ProjectKuberPS(p *project.Project, c *cli.Context) { + factory := cmdutil.NewFactory(nil) + clientConfig, err := factory.ClientConfig() + if err != nil { + logrus.Fatalf("Failed to get Kubernetes client config: %v", err) + } + client := client.NewOrDie(clientConfig) + + if c.BoolT("svc") { + fmt.Printf("%-20s%-20s%-20s%-20s\n", "Name", "Cluster IP", "Ports", "Selectors") + for name := range p.Configs { + var ports string + var selectors string + services, err := client.Services(api.NamespaceDefault).Get(name) + + if err != nil { + logrus.Debugf("Cannot find service for: ", name) + } else { + + for i := range services.Spec.Ports { + p := strconv.Itoa(services.Spec.Ports[i].Port) + ports += ports + string(services.Spec.Ports[i].Protocol) + "(" + p + ")," + } + + for k, v := range services.ObjectMeta.Labels { + selectors += selectors + k + "=" + v + "," + } + + ports = strings.TrimSuffix(ports, ",") + selectors = strings.TrimSuffix(selectors, ",") + + fmt.Printf("%-20s%-20s%-20s%-20s\n", services.ObjectMeta.Name, + services.Spec.ClusterIP, ports, selectors) + } + + } + } + + if c.BoolT("rc") { + fmt.Printf("%-15s%-15s%-30s%-10s%-20s\n", "Name", "Containers", "Images", + "Replicas", "Selectors") + for name := range p.Configs { + var selectors string + var containers string + var images string + rc, err := client.ReplicationControllers(api.NamespaceDefault).Get(name) + + /* Should grab controller, container, image, selector, replicas */ + + if err != nil { + logrus.Debugf("Cannot find rc for: ", string(name)) + } else { + + for k, v := range rc.Spec.Selector { + selectors += selectors + k + "=" + v + "," + } + + for i := range rc.Spec.Template.Spec.Containers { + c := rc.Spec.Template.Spec.Containers[i] + containers += containers + c.Name + "," + images += images + c.Image + "," + } + selectors = strings.TrimSuffix(selectors, ",") + containers = strings.TrimSuffix(containers, ",") + images = strings.TrimSuffix(images, ",") + + fmt.Printf("%-15s%-15s%-30s%-10d%-20s\n", rc.ObjectMeta.Name, containers, + images, rc.Spec.Replicas, selectors) + } + } + } + +} + +// ProjectKuberDelete deletes all rc, svc. +func ProjectKuberDelete(p *project.Project, c *cli.Context) { + factory := cmdutil.NewFactory(nil) + clientConfig, err := factory.ClientConfig() + if err != nil { + logrus.Fatalf("Failed to get Kubernetes client config: %v", err) + } + client := client.NewOrDie(clientConfig) + + for name := range p.Configs { + if len(c.String("name")) > 0 && name != c.String("name") { + continue + } + + if c.BoolT("svc") { + err := client.Services(api.NamespaceDefault).Delete(name) + if err != nil { + logrus.Fatalf("Unable to delete service %s: %s\n", name, err) + } + } else if c.BoolT("rc") { + err := client.ReplicationControllers(api.NamespaceDefault).Delete(name) + if err != nil { + logrus.Fatalf("Unable to delete replication controller %s: %s\n", name, err) + } + } + } +} + +// ProjectKuberScale scales rc. +func ProjectKuberScale(p *project.Project, c *cli.Context) { + factory := cmdutil.NewFactory(nil) + clientConfig, err := factory.ClientConfig() + if err != nil { + logrus.Fatalf("Failed to get Kubernetes client config: %v", err) + } + client := client.NewOrDie(clientConfig) + + if c.Int("scale") <= 0 { + logrus.Fatalf("Scale must be defined and a positive number") + } + + for name := range p.Configs { + if len(c.String("rc")) == 0 || c.String("rc") == name { + s, err := client.ExtensionsClient.Scales(api.NamespaceDefault).Get("ReplicationController", name) + if err != nil { + logrus.Fatalf("Error retrieving scaling data: %s\n", err) + } + + s.Spec.Replicas = c.Int("scale") + + s, err = client.ExtensionsClient.Scales(api.NamespaceDefault).Update("ReplicationController", s) + if err != nil { + logrus.Fatalf("Error updating scaling data: %s\n", err) + } + + fmt.Printf("Scaling %s to: %d\n", name, s.Spec.Replicas) + } + } +} + +// ProjectKuberConvert tranforms docker compose to k8s objects +func ProjectKuberConvert(p *project.Project, c *cli.Context) { + generateYaml := false + composeFile := c.String("file") + + p = project.NewProject(&project.Context{ + ProjectName: "kube", + ComposeFile: composeFile, + }) + + if err := p.Parse(); err != nil { + logrus.Fatalf("Failed to parse the compose project from %s: %v", composeFile, err) + } + + if c.BoolT("yaml") { + generateYaml = true + } + + var mServices map[string]api.Service = make(map[string]api.Service) + var serviceLinks []string + + for name, service := range p.Configs { + rc := &api.ReplicationController{ + TypeMeta: unversioned.TypeMeta{ + Kind: "ReplicationController", + APIVersion: "v1", + }, + ObjectMeta: api.ObjectMeta{ + Name: name, + //Labels: map[string]string{"service": name}, + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 1, + Selector: map[string]string{"service": name}, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + //Labels: map[string]string{"service": name}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: name, + Image: service.Image, + }, + }, + }, + }, + }, + } + 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}, + }, + } + 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: 1, + 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, + }, + }, + }, + }, + }, + } + 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, + }, + }, + }, + }, + }, + } + rs := &extensions.ReplicaSet{ + TypeMeta: unversioned.TypeMeta{ + Kind: "ReplicaSet", + APIVersion: "extensions/v1beta1", + }, + ObjectMeta: api.ObjectMeta{ + Name: name, + }, + Spec: extensions.ReplicaSetSpec{ + Replicas: 1, + 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, + }, + }, + }, + }, + }, + } + + // Configure the environment variables. + var envs []api.EnvVar + for _, env := range service.Environment.Slice() { + var character string = "=" + 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, api.EnvVar{ + Name: name, + Value: value, + }) + } else { + character = ":" + if strings.Contains(env, character) { + var charQuote string = "'" + 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, api.EnvVar{ + Name: name, + Value: value, + }) + } else { + logrus.Fatalf("Invalid container env %s for service %s", env, name) + } + } + } + + rc.Spec.Template.Spec.Containers[0].Env = envs + dc.Spec.Template.Spec.Containers[0].Env = envs + ds.Spec.Template.Spec.Containers[0].Env = envs + rs.Spec.Template.Spec.Containers[0].Env = envs + + // Configure the container command. + var cmds []string + for _, cmd := range service.Command.Slice() { + cmds = append(cmds, cmd) + } + rc.Spec.Template.Spec.Containers[0].Command = cmds + dc.Spec.Template.Spec.Containers[0].Command = cmds + ds.Spec.Template.Spec.Containers[0].Command = cmds + rs.Spec.Template.Spec.Containers[0].Command = cmds + + // Configure the container working dir. + rc.Spec.Template.Spec.Containers[0].WorkingDir = service.WorkingDir + dc.Spec.Template.Spec.Containers[0].WorkingDir = service.WorkingDir + ds.Spec.Template.Spec.Containers[0].WorkingDir = service.WorkingDir + rs.Spec.Template.Spec.Containers[0].WorkingDir = service.WorkingDir + + // Configure the container volumes. + var volumesMount []api.VolumeMount + var volumes []api.Volume + for _, volume := range service.Volumes { + var character string = ":" + if strings.Contains(volume, character) { + hostDir := volume[0:strings.Index(volume, character)] + hostDir = strings.TrimSpace(hostDir) + containerDir := volume[strings.Index(volume, character)+1:] + containerDir = strings.TrimSpace(containerDir) + + // check if ro/rw mode is defined + var readonly bool = true + if strings.Index(volume, character) != strings.LastIndex(volume, character) { + mode := volume[strings.LastIndex(volume, character)+1:] + if strings.Compare(mode, "rw") == 0 { + readonly = false + } + containerDir = containerDir[0:strings.Index(containerDir, character)] + } + + // volumeName = random string of 20 chars + volumeName := RandStringBytes(20) + + volumesMount = append(volumesMount, api.VolumeMount{Name: volumeName, ReadOnly: readonly, MountPath: containerDir}) + p := &api.HostPathVolumeSource{ + Path: hostDir, + } + //p.Path = hostDir + volumeSource := api.VolumeSource{HostPath: p} + volumes = append(volumes, api.Volume{Name: volumeName, VolumeSource: volumeSource}) + } + } + + rc.Spec.Template.Spec.Containers[0].VolumeMounts = volumesMount + dc.Spec.Template.Spec.Containers[0].VolumeMounts = volumesMount + ds.Spec.Template.Spec.Containers[0].VolumeMounts = volumesMount + rs.Spec.Template.Spec.Containers[0].VolumeMounts = volumesMount + + rc.Spec.Template.Spec.Volumes = volumes + dc.Spec.Template.Spec.Volumes = volumes + ds.Spec.Template.Spec.Volumes = volumes + rs.Spec.Template.Spec.Volumes = volumes + + // Configure the container privileged mode + if service.Privileged == true { + securitycontexts := &api.SecurityContext{ + Privileged: &service.Privileged, + } + rc.Spec.Template.Spec.Containers[0].SecurityContext = securitycontexts + dc.Spec.Template.Spec.Containers[0].SecurityContext = securitycontexts + ds.Spec.Template.Spec.Containers[0].SecurityContext = securitycontexts + rs.Spec.Template.Spec.Containers[0].SecurityContext = securitycontexts + } + + // Configure the container ports. + var ports []api.ContainerPort + for _, port := range service.Ports { + var character string = ":" + if strings.Contains(port, character) { + //portNumber := port[0:strings.Index(port, character)] + targetPortNumber := port[strings.Index(port, character)+1:] + targetPortNumber = strings.TrimSpace(targetPortNumber) + targetPortNumberInt, err := strconv.Atoi(targetPortNumber) + if err != nil { + logrus.Fatalf("Invalid container port %s for service %s", port, name) + } + ports = append(ports, api.ContainerPort{ContainerPort: targetPortNumberInt}) + } else { + portNumber, err := strconv.Atoi(port) + if err != nil { + logrus.Fatalf("Invalid container port %s for service %s", port, name) + } + ports = append(ports, api.ContainerPort{ContainerPort: portNumber}) + } + } + + rc.Spec.Template.Spec.Containers[0].Ports = ports + dc.Spec.Template.Spec.Containers[0].Ports = ports + ds.Spec.Template.Spec.Containers[0].Ports = ports + rs.Spec.Template.Spec.Containers[0].Ports = ports + + // Configure the service ports. + var servicePorts []api.ServicePort + for _, port := range service.Ports { + var character string = ":" + if strings.Contains(port, character) { + portNumber := port[0:strings.Index(port, character)] + portNumber = strings.TrimSpace(portNumber) + targetPortNumber := port[strings.Index(port, character)+1:] + targetPortNumber = strings.TrimSpace(targetPortNumber) + portNumberInt, err := strconv.Atoi(portNumber) + if err != nil { + logrus.Fatalf("Invalid container port %s for service %s", port, name) + } + targetPortNumberInt, err1 := strconv.Atoi(targetPortNumber) + if err1 != nil { + logrus.Fatalf("Invalid container port %s for service %s", port, name) + } + var targetPort intstr.IntOrString + targetPort.StrVal = targetPortNumber + targetPort.IntVal = int32(targetPortNumberInt) + servicePorts = append(servicePorts, api.ServicePort{Port: portNumberInt, Name: portNumber, Protocol: "TCP", TargetPort: targetPort}) + } else { + portNumber, err := strconv.Atoi(port) + if err != nil { + logrus.Fatalf("Invalid container port %s for service %s", port, name) + } + var targetPort intstr.IntOrString + targetPort.StrVal = strconv.Itoa(portNumber) + targetPort.IntVal = int32(portNumber) + servicePorts = append(servicePorts, api.ServicePort{Port: portNumber, Name: strconv.Itoa(portNumber), Protocol: "TCP", TargetPort: targetPort}) + } + } + sc.Spec.Ports = servicePorts + + // Configure label + labels := map[string]string{"service": name} + for key, value := range service.Labels.MapParts() { + labels[key] = value + } + rc.Spec.Template.ObjectMeta.Labels = labels + dc.Spec.Template.ObjectMeta.Labels = labels + ds.Spec.Template.ObjectMeta.Labels = labels + rs.Spec.Template.ObjectMeta.Labels = labels + + rc.ObjectMeta.Labels = labels + dc.ObjectMeta.Labels = labels + ds.ObjectMeta.Labels = labels + rs.ObjectMeta.Labels = labels + + sc.ObjectMeta.Labels = labels + + // Configure the container restart policy. + switch service.Restart { + case "", "always": + rc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyAlways + dc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyAlways + ds.Spec.Template.Spec.RestartPolicy = api.RestartPolicyAlways + rs.Spec.Template.Spec.RestartPolicy = api.RestartPolicyAlways + case "no": + rc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever + dc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever + ds.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever + rs.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever + case "on-failure": + rc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyOnFailure + dc.Spec.Template.Spec.RestartPolicy = api.RestartPolicyOnFailure + ds.Spec.Template.Spec.RestartPolicy = api.RestartPolicyOnFailure + rs.Spec.Template.Spec.RestartPolicy = api.RestartPolicyOnFailure + default: + logrus.Fatalf("Unknown restart policy %s for service %s", service.Restart, name) + } + + // convert datarc to json / yaml + datarc, err := json.MarshalIndent(rc, "", " ") + if generateYaml == true { + datarc, err = yaml.Marshal(rc) + } + + if err != nil { + logrus.Fatalf("Failed to marshal the replication controller: %v", err) + } + logrus.Debugf("%s\n", datarc) + + // convert datadc to json / yaml + datadc, err := json.MarshalIndent(dc, "", " ") + if generateYaml == true { + datadc, err = yaml.Marshal(dc) + } + + if err != nil { + logrus.Fatalf("Failed to marshal the deployment container: %v", err) + } + + logrus.Debugf("%s\n", datadc) + + //convert datads to json / yaml + datads, err := json.MarshalIndent(ds, "", " ") + if generateYaml == true { + datads, err = yaml.Marshal(ds) + } + + if err != nil { + logrus.Fatalf("Failed to marshal the daemonSet: %v", err) + } + + logrus.Debugf("%s\n", datads) + + //convert datars to json / yaml + datars, err := json.MarshalIndent(rs, "", " ") + if generateYaml == true { + datads, err = yaml.Marshal(rs) + } + + if err != nil { + logrus.Fatalf("Failed to marshal the replicaSet: %v", err) + } + + logrus.Debugf("%s\n", datars) + + mServices[name] = *sc + + if len(service.Links.Slice()) > 0 { + for i := 0; i < len(service.Links.Slice()); i++ { + var data string = service.Links.Slice()[i] + if len(serviceLinks) == 0 { + serviceLinks = append(serviceLinks, data) + } else { + for _, v := range serviceLinks { + if v != data { + serviceLinks = append(serviceLinks, data) + } + } + } + } + } + + fileRC := fmt.Sprintf("%s-rc.json", name) + if generateYaml == true { + fileRC = fmt.Sprintf("%s-rc.yaml", name) + } + if err := ioutil.WriteFile(fileRC, []byte(datarc), 0644); err != nil { + logrus.Fatalf("Failed to write replication controller: %v", err) + } + + /* Create the deployment container */ + if c.BoolT("deployment") { + fileDC := fmt.Sprintf("%s-deployment.json", name) + if generateYaml == true { + fileDC = fmt.Sprintf("%s-deployment.yaml", name) + } + if err := ioutil.WriteFile(fileDC, []byte(datadc), 0644); err != nil { + logrus.Fatalf("Failed to write deployment container: %v", err) + } + } + + /* Create the daemonset container */ + if c.BoolT("daemonset") { + fileDS := fmt.Sprintf("%s-daemonset.json", name) + if generateYaml == true { + fileDS = fmt.Sprintf("%s-daemonset.yaml", name) + } + if err := ioutil.WriteFile(fileDS, []byte(datads), 0644); err != nil { + logrus.Fatalf("Failed to write daemonset: %v", err) + } + } + + /* Create the replicaset container */ + if c.BoolT("replicaset") { + fileRS := fmt.Sprintf("%s-replicaset.json", name) + if generateYaml == true { + fileRS = fmt.Sprintf("%s-replicaset.yaml", name) + } + if err := ioutil.WriteFile(fileRS, []byte(datars), 0644); err != nil { + logrus.Fatalf("Failed to write replicaset: %v", err) + } + } + + for k, v := range mServices { + for i := 0; i < len(serviceLinks); i++ { + // convert datasvc to json / yaml + datasvc, er := json.MarshalIndent(v, "", " ") + if generateYaml == true { + datasvc, er = yaml.Marshal(v) + } + if er != nil { + logrus.Fatalf("Failed to marshal the service controller: %v", er) + } + + logrus.Debugf("%s\n", datasvc) + + fileSVC := fmt.Sprintf("%s-svc.json", k) + if generateYaml == true { + fileSVC = fmt.Sprintf("%s-svc.yaml", k) + } + + if err := ioutil.WriteFile(fileSVC, []byte(datasvc), 0644); err != nil { + logrus.Fatalf("Failed to write service controller: %v", err) + } + } + } + } + + /* Need to iterate through one more time to ensure we capture all service/rc */ + for name := range p.Configs { + if c.BoolT("chart") { + err := generateHelm(composeFile, name) + if err != nil { + logrus.Fatalf("Failed to create Chart data: %s\n", err) + } + } + } +} + +// ProjectKuberUp brings up rc, svc. +func ProjectKuberUp(p *project.Project, c *cli.Context) { + factory := cmdutil.NewFactory(nil) + clientConfig, err := factory.ClientConfig() + if err != nil { + logrus.Fatalf("Failed to get Kubernetes client config: %v", err) + } + client := client.NewOrDie(clientConfig) + + files, err := ioutil.ReadDir(".") + if err != nil { + logrus.Fatalf("Failed to load rc, svc manifest files: %s\n", err) + } + + // submit svc first + sc := &api.Service{} + for _, file := range files { + if strings.Contains(file.Name(), "svc") { + datasvc, err := ioutil.ReadFile(file.Name()) + + if err != nil { + logrus.Fatalf("Failed to load %s: %s\n", file.Name(), err) + } + + if strings.Contains(file.Name(), "json") { + err := json.Unmarshal(datasvc, &sc) + if err != nil { + logrus.Fatalf("Failed to unmarshal file %s to svc object: %s\n", file.Name(), err) + } + } + if strings.Contains(file.Name(), "yaml") { + err := yaml.Unmarshal(datasvc, &sc) + if err != nil { + logrus.Fatalf("Failed to unmarshal file %s to svc object: %s\n", file.Name(), err) + } + } + // submit sc to k8s + scCreated, err := client.Services(api.NamespaceDefault).Create(sc) + if err != nil { + fmt.Println(err) + } + logrus.Debugf("%s\n", scCreated) + } + } + + // then submit rc + rc := &api.ReplicationController{} + for _, file := range files { + if strings.Contains(file.Name(), "rc") { + datarc, err := ioutil.ReadFile(file.Name()) + + if err != nil { + logrus.Fatalf("Failed to load %s: %s\n", file.Name(), err) + } + + if strings.Contains(file.Name(), "json") { + err := json.Unmarshal(datarc, &rc) + if err != nil { + logrus.Fatalf("Failed to unmarshal file %s to rc object: %s\n", file.Name(), err) + } + } + if strings.Contains(file.Name(), "yaml") { + err := yaml.Unmarshal(datarc, &rc) + if err != nil { + logrus.Fatalf("Failed to unmarshal file %s to rc object: %s\n", file.Name(), err) + } + } + // submit rc to k8s + rcCreated, err := client.ReplicationControllers(api.NamespaceDefault).Create(rc) + if err != nil { + fmt.Println(err) + } + logrus.Debugf("%s\n", rcCreated) + } + } + +} diff --git a/cli/app/k8sutils.go b/cli/app/k8sutils.go new file mode 100644 index 00000000..83023ef3 --- /dev/null +++ b/cli/app/k8sutils.go @@ -0,0 +1,110 @@ +/* +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" + "strings" + "text/template" + + "github.com/Sirupsen/logrus" +) + +/* Ancilliary helper functions to interface with the commands interface */ + +/** + * Generate Helm Chart configuration + */ +func generateHelm(filename string, svcname string) error { + type ChartDetails struct { + Name string + } + + dirName := strings.Replace(filename, ".yml", "", 1) + details := ChartDetails{dirName} + manifestDir := dirName + string(os.PathSeparator) + "manifests" + 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 from Skippbox Kompose +version: 0.0.1 +source: +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 yaml files into the newly created manifests directory */ + infile, err := ioutil.ReadFile(svcname + "-rc.json") + if err != nil { + logrus.Infof("Error reading %s: %s\n", svcname+"-rc.yaml", err) + return err + } + + err = ioutil.WriteFile(manifestDir+string(os.PathSeparator)+svcname+"-rc.json", infile, 0644) + if err != nil { + return err + } + + /* The svc file is optional */ + infile, err = ioutil.ReadFile(svcname + "-svc.json") + if err == nil { + err = ioutil.WriteFile(manifestDir+string(os.PathSeparator)+svcname+"-svc.json", infile, 0644) + if err != nil { + return err + } + } + + return nil +} diff --git a/cli/app/types.go b/cli/app/types.go new file mode 100644 index 00000000..9dc62bb3 --- /dev/null +++ b/cli/app/types.go @@ -0,0 +1,28 @@ +/* +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 ( + "github.com/urfave/cli" + "github.com/docker/libcompose/project" +) + +// ProjectFactory is an interface that helps creating libcompose project. +type ProjectFactory interface { + // Create creates a libcompose project from the command line options (codegangsta cli context). + Create(c *cli.Context) (*project.Project, error) +} diff --git a/cli/command/command.go b/cli/command/command.go new file mode 100644 index 00000000..579b424a --- /dev/null +++ b/cli/command/command.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 command + +import ( + "github.com/urfave/cli" + "github.com/docker/libcompose/project" + "github.com/skippbox/kompose2/cli/app" +) + +// ConvertCommand defines the kompose convert subcommand. +func ConvertCommand(factory app.ProjectFactory) cli.Command { + return cli.Command{ + Name: "convert", + Usage: "Convert docker-compose.yml to Kubernetes objects", + Action: app.WithProject(factory, app.ProjectKuberConvert), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file,f", + Usage: "Specify an alternate compose file (default: docker-compose.yml)", + Value: "docker-compose.yml", + EnvVar: "COMPOSE_FILE", + }, + cli.BoolFlag{ + Name: "deployment,d", + Usage: "Generate a deployment resource file", + }, + cli.BoolFlag{ + Name: "daemonset,ds", + Usage: "Generate a daemonset resource file", + }, + cli.BoolFlag{ + Name: "replicaset,rs", + Usage: "Generate a replicaset resource file", + }, + cli.BoolFlag{ + Name: "chart,c", + Usage: "Create a chart deployment", + }, + cli.BoolFlag{ + Name: "yaml, y", + Usage: "Generate resource file in yaml format", + }, + }, + } +} + +// UpCommand defines the kompose up subcommand. +func UpCommand(factory app.ProjectFactory) cli.Command { + return cli.Command{ + Name: "up", + Usage: "Submit rc, svc objects to kubernetes API endpoint", + Action: app.WithProject(factory, app.ProjectKuberUp), + } +} + +// PsCommand defines the kompose ps subcommand. +func PsCommand(factory app.ProjectFactory) cli.Command { + return cli.Command{ + Name: "ps", + Usage: "Get active data in the kubernetes cluster", + Action: app.WithProject(factory, app.ProjectKuberPS), + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "service,svc", + Usage: "Get active services", + }, + cli.BoolFlag{ + Name: "replicationcontroller,rc", + Usage: "Get active replication controller", + }, + }, + } +} + +// DeleteCommand defines the kompose delete subcommand. +func DeleteCommand(factory app.ProjectFactory) cli.Command { + return cli.Command{ + Name: "delete", + Usage: "Remove instantiated services/rc from kubernetes", + Action: app.WithProject(factory, app.ProjectKuberDelete), + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "replicationcontroller,rc", + Usage: "Remove active replication controllers", + }, + cli.BoolFlag{ + Name: "service,svc", + Usage: "Remove active services", + }, + cli.StringFlag{ + Name: "name", + Usage: "Name of the object to remove", + }, + }, + } +} + +// ScaleCommand defines the kompose up subcommand. +func ScaleCommand(factory app.ProjectFactory) cli.Command { + return cli.Command{ + Name: "scale", + Usage: "Globally scale instantiated replication controllers", + Action: app.WithProject(factory, app.ProjectKuberScale), + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "scale", + Usage: "New number of replicas", + }, + cli.StringFlag{ + Name: "replicationcontroller,rc", + Usage: "A specific replication controller to scale", + }, + }, + } +} + +// CommonFlags defines the flags that are in common for all subcommands. +func CommonFlags() []cli.Flag { + return []cli.Flag{ + cli.BoolFlag{ + Name: "verbose,debug", + }, + cli.StringFlag{ + Name: "file,f", + Usage: "Specify an alternate compose file (default: docker-compose.yml)", + Value: "docker-compose.yml", + EnvVar: "COMPOSE_FILE", + }, + cli.StringFlag{ + Name: "project-name,p", + Usage: "Specify an alternate project name (default: directory name)", + }, + } +} + +// Populate updates the specified project context based on command line arguments and subcommands. +func Populate(context *project.Context, c *cli.Context) { + context.ComposeFile = c.GlobalString("file") + context.ProjectName = c.GlobalString("project-name") + + if c.Command.Name == "logs" { + context.Log = true + } else if c.Command.Name == "up" { + context.Log = !c.Bool("d") + context.NoRecreate = c.Bool("no-recreate") + context.ForceRecreate = c.Bool("force-recreate") + } else if c.Command.Name == "scale" { + context.Timeout = uint(c.Int("timeout")) + } +} diff --git a/cli/docker/app/factory.go b/cli/docker/app/factory.go new file mode 100644 index 00000000..5c09802a --- /dev/null +++ b/cli/docker/app/factory.go @@ -0,0 +1,39 @@ +/* +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 ( + "github.com/urfave/cli" + "github.com/skippbox/kompose2/cli/command" + "github.com/docker/libcompose/cli/logger" + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" +) + +// ProjectFactory is a struct that hold the app.ProjectFactory implementation. +type ProjectFactory struct { +} + +// Create implements ProjectFactory.Create using docker client. +func (p *ProjectFactory) Create(c *cli.Context) (*project.Project, error) { + context := &docker.Context{} + context.LoggerFactory = logger.NewColorLoggerFactory() + //Populate(context, c) + command.Populate(&context.Context, c) + + return docker.NewProject(context) +} diff --git a/cli/main/main.go b/cli/main/main.go new file mode 100644 index 00000000..4f0083ab --- /dev/null +++ b/cli/main/main.go @@ -0,0 +1,49 @@ +/* +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 main + +import ( + "os" + + "github.com/urfave/cli" + dockerApp "github.com/skippbox/kompose2/cli/docker/app" + "github.com/skippbox/kompose2/version" + "github.com/skippbox/kompose2/cli/command" + cliApp "github.com/skippbox/kompose2/cli/app" +) + +func main() { + factory := &dockerApp.ProjectFactory{} + + app := cli.NewApp() + app.Name = "kompose" + app.Usage = "Command line interface for Skippbox." + app.Version = version.VERSION + " (" + version.GITCOMMIT + ")" + app.Author = "Skippbox Compose Contributors" + app.Email = "https://github.com/skippbox/kompose" + app.Before = cliApp.BeforeApp + app.Flags = append(command.CommonFlags()) + app.Commands = []cli.Command{ + command.ConvertCommand(factory), + command.UpCommand(factory), + command.PsCommand(factory), + command.DeleteCommand(factory), + command.ScaleCommand(factory), + } + + app.Run(os.Args) +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..b9ab6dcc --- /dev/null +++ b/version/version.go @@ -0,0 +1,25 @@ +/* +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 version + +var ( +// VERSION should be updated by hand at each release + VERSION = "0.0.5-dev" + +// GITCOMMIT will be overwritten automatically by the build system + GITCOMMIT = "HEAD" +)