diff --git a/compose/compose.go b/compose/compose.go new file mode 100644 index 000000000..203b3b8a2 --- /dev/null +++ b/compose/compose.go @@ -0,0 +1,96 @@ +package compose + +import ( + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "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" + "github.com/docker/helm-prototype/pkg/compose/internal/convert" + "github.com/docker/helm-prototype/pkg/compose/internal/helm" + utils "github.com/docker/helm-prototype/pkg/compose/internal/utils" +) + +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() +} + +// Orchestrator is "kubernetes" or "swarm" +type Orchestrator string + +const ( + // Kubernetes specifies to use kubernetes. + Kubernetes Orchestrator = "kubernetes" + // Swarm specifies to use Docker swarm. + Swarm Orchestrator = "swarm" +) + +type ProjectOptions struct { + ConfigPaths []string + Name string +} + +type Project struct { + Config *types.Config + ProjectDir string + Name string `yaml:"-" json:"-"` + Orchestrator Orchestrator +} + +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 +} + +// projectFromOptions load a compose project based on command line options +func ProjectFromOptions(options *ProjectOptions) (*Project, error) { + workingDir, configs, err := utils.GetConfigs( + options.Name, + options.ConfigPaths, + ) + if err != nil { + return nil, err + } + + return NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + }, options.Name) +} + +func (p *Project) GenerateCharts(path string) error { + objects, err := convert.MapToKubernetesObjects(p.Config, p.Name) + if err != nil { + return err + } + err = helm.Write(p.Name, objects, path) + if err != nil { + return err + } + return nil +} +func (p *Project) InstallCommand(options *ProjectOptions) error { + return nil +} diff --git a/compose/docker.go b/compose/docker.go deleted file mode 100644 index ada6e3b89..000000000 --- a/compose/docker.go +++ /dev/null @@ -1,23 +0,0 @@ -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/convert/kube.go b/compose/internal/convert/kube.go similarity index 87% rename from convert/kube.go rename to compose/internal/convert/kube.go index abfa97e04..884d56935 100644 --- a/convert/kube.go +++ b/compose/internal/convert/kube.go @@ -6,7 +6,6 @@ import ( "time" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,19 +13,19 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) -func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { +func MapToKubernetesObjects(model *types.Config, name string) (map[string]runtime.Object, error) { objects := map[string]runtime.Object{} for _, service := range model.Services { objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) if service.Deploy != nil && service.Deploy.Mode == "global" { - daemonset, err := mapToDaemonset(service, model) + daemonset, err := mapToDaemonset(service, model, name) if err != nil { return nil, err } objects[fmt.Sprintf("%s-daemonset.yaml", service.Name)] = daemonset } else { - deployment, err := mapToDeployment(service, model) + deployment, err := mapToDeployment(service, model, name) if err != nil { return nil, err } @@ -41,7 +40,7 @@ func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, return objects, nil } -func mapToService(model *compose.Project, service types.ServiceConfig) *core.Service { +func mapToService(model *types.Config, service types.ServiceConfig) *core.Service { ports := []core.ServicePort{} for _, p := range service.Ports { ports = append(ports, @@ -69,7 +68,7 @@ func mapToService(model *compose.Project, service types.ServiceConfig) *core.Ser } } -func mapServiceToServiceType(service types.ServiceConfig, model *compose.Project) core.ServiceType { +func mapServiceToServiceType(service types.ServiceConfig, model *types.Config) core.ServiceType { serviceType := core.ServiceTypeClusterIP if len(service.Networks) == 0 { // service is implicitly attached to "default" network @@ -88,10 +87,10 @@ func mapServiceToServiceType(service types.ServiceConfig, model *compose.Project return serviceType } -func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps.Deployment, error) { +func mapToDeployment(service types.ServiceConfig, model *types.Config, name string) (*apps.Deployment, error) { labels := map[string]string{ "com.docker.compose.service": service.Name, - "com.docker.compose.project": model.Name, + "com.docker.compose.project": name, } podTemplate, err := toPodTemplate(service, labels, model) if err != nil { @@ -120,10 +119,10 @@ func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps }, nil } -func mapToDaemonset(service types.ServiceConfig, model *compose.Project) (*apps.DaemonSet, error) { +func mapToDaemonset(service types.ServiceConfig, model *types.Config, name string) (*apps.DaemonSet, error) { labels := map[string]string{ "com.docker.compose.service": service.Name, - "com.docker.compose.project": model.Name, + "com.docker.compose.project": name, } podTemplate, err := toPodTemplate(service, labels, model) if err != nil { diff --git a/convert/placement.go b/compose/internal/convert/placement.go similarity index 100% rename from convert/placement.go rename to compose/internal/convert/placement.go diff --git a/convert/placement_test.go b/compose/internal/convert/placement_test.go similarity index 100% rename from convert/placement_test.go rename to compose/internal/convert/placement_test.go diff --git a/convert/pod.go b/compose/internal/convert/pod.go similarity index 98% rename from convert/pod.go rename to compose/internal/convert/pod.go index 03e91cc42..2842bba54 100644 --- a/convert/pod.go +++ b/compose/internal/convert/pod.go @@ -9,7 +9,6 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/docker/docker/api/types/swarm" - "github.com/docker/helm-prototype/pkg/compose" "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" @@ -17,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *compose.Project) (apiv1.PodTemplateSpec, error) { +func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *types.Config) (apiv1.PodTemplateSpec, error) { tpl := apiv1.PodTemplateSpec{} nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) if err != nil { diff --git a/convert/pod_test.go b/compose/internal/convert/pod_test.go similarity index 98% rename from convert/pod_test.go rename to compose/internal/convert/pod_test.go index 3e50dd0e5..078a8f839 100644 --- a/convert/pod_test.go +++ b/compose/internal/convert/pod_test.go @@ -8,13 +8,12 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) -func loadYAML(yaml string) (*compose.Project, error) { +func loadYAML(yaml string) (*loader.Config, error) { dict, err := loader.ParseYAML([]byte(yaml)) if err != nil { return nil, err @@ -23,12 +22,17 @@ func loadYAML(yaml string) (*compose.Project, error) { if err != nil { panic(err) } - return compose.NewProject(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: []types.ConfigFile{ - {Filename: "compose.yaml", Config: dict}, - }, - }, "test") + configs := ConfigFiles: []types.ConfigFile{ + {Filename: "compose.yaml", Config: dict}, + }, + + config := types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + } + model, err := loader.Load(config) + return model } func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { @@ -38,11 +42,12 @@ func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { } func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { - project, err := loadYAML(yaml) + model, err := loadYAML(yaml) if err != nil { return apiv1.PodTemplateSpec{}, err } - return toPodTemplate(project.Services[0], nil, project) + + return toPodTemplate(model.Services[0], nil, model) } func TestToPodWithDockerSocket(t *testing.T) { diff --git a/convert/volumes.go b/compose/internal/convert/volumes.go similarity index 93% rename from convert/volumes.go rename to compose/internal/convert/volumes.go index 57eb5bb70..bcc38eb3c 100644 --- a/convert/volumes.go +++ b/compose/internal/convert/volumes.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" @@ -30,7 +29,7 @@ func hasPersistentVolumes(s types.ServiceConfig) bool { return false } -func toVolumeSpecs(s types.ServiceConfig, model *compose.Project) ([]volumeSpec, error) { +func toVolumeSpecs(s types.ServiceConfig, model *types.Config) ([]volumeSpec, error) { var specs []volumeSpec for i, m := range s.Volumes { var source *apiv1.VolumeSource @@ -114,7 +113,7 @@ func or(v string, defaultValue string) string { return defaultValue } -func toVolumeMounts(s types.ServiceConfig, model *compose.Project) ([]apiv1.VolumeMount, error) { +func toVolumeMounts(s types.ServiceConfig, model *types.Config) ([]apiv1.VolumeMount, error) { var mounts []apiv1.VolumeMount specs, err := toVolumeSpecs(s, model) if err != nil { @@ -126,7 +125,7 @@ func toVolumeMounts(s types.ServiceConfig, model *compose.Project) ([]apiv1.Volu return mounts, nil } -func toVolumes(s types.ServiceConfig, model *compose.Project) ([]apiv1.Volume, error) { +func toVolumes(s types.ServiceConfig, model *types.Config) ([]apiv1.Volume, error) { var volumes []apiv1.Volume specs, err := toVolumeSpecs(s, model) if err != nil { diff --git a/compose/internal/helm/action.go b/compose/internal/helm/action.go new file mode 100644 index 000000000..e500abba8 --- /dev/null +++ b/compose/internal/helm/action.go @@ -0,0 +1,34 @@ +package helm + +import ( + "os" + "sync" + + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +type KubeConfig struct { + namespace string + config genericclioptions.RESTClientGetter + configOnce sync.Once + + // KubeConfig is the path to the kubeconfig file + KubeConfig string + // KubeContext is the name of the kubeconfig context. + KubeContext string + // Bearer KubeToken used for authentication + KubeToken string + // Kubernetes API Server Endpoint for authentication + KubeAPIServer string +} + +func New() *KubeConfig { + + env := KubeConfig{ + namespace: "", + KubeContext: os.Getenv("COMPOSE_KUBECONTEXT"), + KubeToken: os.Getenv("COMPOSE_KUBETOKEN"), + KubeAPIServer: os.Getenv("COMPOSE_KUBEAPISERVER"), + } + return &env +} diff --git a/helm/output.go b/compose/internal/helm/output.go similarity index 100% rename from helm/output.go rename to compose/internal/helm/output.go diff --git a/compose/internal/utils/config.go b/compose/internal/utils/config.go new file mode 100644 index 000000000..12974553b --- /dev/null +++ b/compose/internal/utils/config.go @@ -0,0 +1,124 @@ +package utils + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/prometheus/common/log" +) + +var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, error) { + configPath, err := getConfigPaths(configPaths) + if err != nil { + return "", nil, err + } + + if name == "" { + name = os.Getenv("COMPOSE_PROJECT_NAME") + } + + workingDir := filepath.Dir(configPath[0]) + + if name == "" { + r := regexp.MustCompile(`[^a-z0-9\\-_]+`) + name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") + } + + configs, err := parseConfigs(configPath) + if err != nil { + return "", nil, err + } + return workingDir, configs, nil +} + +func getConfigPaths(configPaths []string) ([]string, error) { + paths := []string{} + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + if len(configPaths) != 0 { + for _, f := range configPaths { + if f == "-" { + paths = append(paths, f) + continue + } + if !filepath.IsAbs(f) { + f = filepath.Join(pwd, f) + } + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil + } + + sep := os.Getenv("COMPOSE_FILE_SEPARATOR") + if sep == "" { + sep = string(os.PathListSeparator) + } + f := os.Getenv("COMPOSE_FILE") + if f != "" { + return strings.Split(f, sep), nil + } + + for { + candidates := []string{} + for _, n := range SupportedFilenames { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + log.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + log.Warnf("Using %s\n", winner) + } + return []string{winner}, nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") + } + pwd = parent + } +} + +func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { + files := []types.ConfigFile{} + for _, f := range configPaths { + var ( + b []byte + err error + ) + if f == "-" { + b, err = ioutil.ReadAll(os.Stdin) + } else { + if _, err := os.Stat(f); err != nil { + return nil, err + } + b, err = ioutil.ReadFile(f) + } + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(b) + if err != nil { + return nil, err + } + files = append(files, types.ConfigFile{Filename: f, Config: config}) + } + return files, nil +} diff --git a/compose/internal/utils/env.go b/compose/internal/utils/env.go new file mode 100644 index 000000000..3044742bb --- /dev/null +++ b/compose/internal/utils/env.go @@ -0,0 +1,20 @@ +package utils + +import ( + "os" + "strings" +) + +func Environment() map[string]string { + return getAsEqualsMap(os.Environ()) +} + +// getAsEqualsMap split key=value formatted strings into a key : value map +func getAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} diff --git a/compose/errors.go b/compose/internal/utils/errors.go similarity index 91% rename from compose/errors.go rename to compose/internal/utils/errors.go index 3f9e290c4..07a903e6c 100644 --- a/compose/errors.go +++ b/compose/internal/utils/errors.go @@ -1,11 +1,11 @@ -package compose +package utils import ( "fmt" "strings" ) -func combine(errors []error) error { +func CombineErrors(errors []error) error { if len(errors) == 0 { return nil } diff --git a/compose/labels.go b/compose/internal/utils/labels.go similarity index 98% rename from compose/labels.go rename to compose/internal/utils/labels.go index 8ef7d4985..4ff180c54 100644 --- a/compose/labels.go +++ b/compose/internal/utils/labels.go @@ -1,4 +1,4 @@ -package compose +package utils const ( LabelDockerComposePrefix = "com.docker.compose" diff --git a/compose/project.go b/compose/project.go deleted file mode 100644 index 901b19bcb..000000000 --- a/compose/project.go +++ /dev/null @@ -1,26 +0,0 @@ -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 -}