diff --git a/compose/docker.go b/compose/docker.go new file mode 100644 index 000000000..ada6e3b89 --- /dev/null +++ b/compose/docker.go @@ -0,0 +1,23 @@ +package compose + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + registry "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/client" +) + +var ( + Client client.APIClient + RegistryClient registry.RegistryClient + ConfigFile *configfile.ConfigFile + Stdout *streams.Out +) + +func WithDockerCli(cli command.Cli) { + Client = cli.Client() + RegistryClient = cli.RegistryClient(false) + ConfigFile = cli.ConfigFile() + Stdout = cli.Out() +} diff --git a/compose/errors.go b/compose/errors.go new file mode 100644 index 000000000..3f9e290c4 --- /dev/null +++ b/compose/errors.go @@ -0,0 +1,38 @@ +package compose + +import ( + "fmt" + "strings" +) + +func combine(errors []error) error { + if len(errors) == 0 { + return nil + } + if len(errors) == 1 { + return errors[0] + } + err := combinedError{} + for _, e := range errors { + if c, ok := e.(combinedError); ok { + err.errors = append(err.errors, c.errors...) + } else { + err.errors = append(err.errors, e) + } + } + return combinedError{errors} +} + +type combinedError struct { + errors []error +} + +func (c combinedError) Error() string { + points := make([]string, len(c.errors)) + for i, err := range c.errors { + points[i] = fmt.Sprintf("* %s", err.Error()) + } + return fmt.Sprintf( + "%d errors occurred:\n\t%s", + len(c.errors), strings.Join(points, "\n\t")) +} diff --git a/compose/labels.go b/compose/labels.go new file mode 100644 index 000000000..c230fca5d --- /dev/null +++ b/compose/labels.go @@ -0,0 +1,17 @@ +package compose + +const ( + LABEL_DOCKER_COMPOSE_PREFIX = "com.docker.compose" + LABEL_SERVICE = LABEL_DOCKER_COMPOSE_PREFIX + ".service" + LABEL_VERSION = LABEL_DOCKER_COMPOSE_PREFIX + ".version" + LABEL_CONTAINER_NUMBER = LABEL_DOCKER_COMPOSE_PREFIX + ".container-number" + LABEL_ONE_OFF = LABEL_DOCKER_COMPOSE_PREFIX + ".oneoff" + LABEL_NETWORK = LABEL_DOCKER_COMPOSE_PREFIX + ".network" + LABEL_SLUG = LABEL_DOCKER_COMPOSE_PREFIX + ".slug" + LABEL_VOLUME = LABEL_DOCKER_COMPOSE_PREFIX + ".volume" + LABEL_CONFIG_HASH = LABEL_DOCKER_COMPOSE_PREFIX + ".config-hash" + LABEL_PROJECT = LABEL_DOCKER_COMPOSE_PREFIX + ".project" + LABEL_WORKING_DIR = LABEL_DOCKER_COMPOSE_PREFIX + ".working_dir" + LABEL_CONFIG_FILES = LABEL_DOCKER_COMPOSE_PREFIX + ".config_files" + LABEL_ENVIRONMENT_FILE = LABEL_DOCKER_COMPOSE_PREFIX + ".environment_file" +) diff --git a/compose/project.go b/compose/project.go new file mode 100644 index 000000000..901b19bcb --- /dev/null +++ b/compose/project.go @@ -0,0 +1,26 @@ +package compose + +import ( + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" +) + +type Project struct { + types.Config + projectDir string + Name string `yaml:"-" json:"-"` +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: *model, + projectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} diff --git a/helm/output.go b/helm/output.go new file mode 100644 index 000000000..11cc78671 --- /dev/null +++ b/helm/output.go @@ -0,0 +1,96 @@ +package helm + +import ( + "bytes" + "encoding/json" + "gopkg.in/yaml.v3" + "html/template" + "io/ioutil" + "k8s.io/apimachinery/pkg/runtime" + "os" + "path/filepath" +) + +func Write(project string, objects map[string]runtime.Object, target string) error { + out := Outputer{ target } + + if err := out.Write("README.md", []byte("This chart was created by converting a Compose file")); err != nil { + return err + } + + chart := `name: {{.Name}} +description: A generated Helm Chart for {{.Name}} from Skippbox Kompose +version: 0.0.1 +apiVersion: v1 +keywords: + - {{.Name}} +sources: +home: +` + + t, err := template.New("ChartTmpl").Parse(chart) + if err != nil { + return err + } + type ChartDetails struct { + Name string + } + var chartData bytes.Buffer + _ = t.Execute(&chartData, ChartDetails{project}) + + + if err := out.Write("Chart.yaml", chartData.Bytes()); err != nil { + return err + } + + for name, o := range objects { + j, err := json.Marshal(o) + if err != nil { + return err + } + b, err := jsonToYaml(j, 2) + if err != nil { + return err + } + if err := out.Write(filepath.Join("templates", name), b); err != nil { + return err + } + } + return nil +} + +type Outputer struct { + Dir string +} + +func (o Outputer) Write(path string, content []byte) error { + out := filepath.Join(o.Dir, path) + os.MkdirAll(filepath.Dir(out), 0744) + return ioutil.WriteFile(out, content, 0644) +} + +// Convert JSON to YAML. +func jsonToYaml(j []byte, spaces int) ([]byte, error) { + // Convert the JSON to an object. + var jsonObj interface{} + // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the + // Go JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshling to interface{}, it just picks float64 + // universally. go-yaml does go through the effort of picking the right + // number type, so we can preserve number type throughout this process. + err := yaml.Unmarshal(j, &jsonObj) + if err != nil { + return nil, err + } + + var b bytes.Buffer + encoder := yaml.NewEncoder(&b) + encoder.SetIndent(spaces) + if err := encoder.Encode(jsonObj); err != nil { + return nil, err + } + return b.Bytes(), nil + + // Marshal this object into YAML. + // return yaml.Marshal(jsonObj) +} \ No newline at end of file diff --git a/transform/kube.go b/transform/kube.go new file mode 100644 index 000000000..2aa738354 --- /dev/null +++ b/transform/kube.go @@ -0,0 +1,72 @@ +package transform + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + core "k8s.io/api/core/v1" + apps "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { + objects := map[string]runtime.Object{} + for _, service := range model.Services { + objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(service) + objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = mapToDeployment(service) + for _, vol := range service.Volumes { + if vol.Type == "volume" { + objects[fmt.Sprintf("%s-persistentvolumeclain.yaml", service.Name)] = mapToPVC(service, vol) + } + } + } + return objects, nil +} + +func mapToService(service types.ServiceConfig) *core.Service { + return &core.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + }, + Spec: core.ServiceSpec{ + Selector: map[string]string{"com.docker.compose.service": service.Name}, + }, + } +} + +func mapToDeployment(service types.ServiceConfig) *apps.Deployment { + return &apps.Deployment{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: apps.DeploymentSpec{ + Template: core.PodTemplateSpec{ + ObjectMeta: meta.ObjectMeta{ + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PodSpec{ + Containers: []core.Container{ + { + Name: service.Name, + Image: service.Image, + }, + }, + }, + }, + }, + } +} + +func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { + return &core.PersistentVolumeClaim{ + ObjectMeta: meta.ObjectMeta{ + Name: vol.Source, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PersistentVolumeClaimSpec{ + VolumeName: vol.Source, + }, + } +}