commit f095f39eafb6565b04905c342c28301e1da5316e Author: Patrice Ferlet Date: Tue Nov 30 12:04:28 2021 +0100 Initial version diff --git a/compose/parser.go b/compose/parser.go new file mode 100644 index 0000000..0a34aa5 --- /dev/null +++ b/compose/parser.go @@ -0,0 +1,33 @@ +package compose + +import ( + "log" + "os" + + "gopkg.in/yaml.v3" +) + +type Parser struct { + Data *Compose +} + +var Appname = "" + +func NewParser(filename string) *Parser { + + f, err := os.Open(filename) + if err != nil { + log.Fatal(err) + } + c := NewCompose() + dec := yaml.NewDecoder(f) + dec.Decode(c) + + p := &Parser{Data: c} + + return p +} + +func (p *Parser) Parse(appname string) { + Appname = appname +} diff --git a/compose/types.go b/compose/types.go new file mode 100644 index 0000000..9cca9d6 --- /dev/null +++ b/compose/types.go @@ -0,0 +1,24 @@ +package compose + +type Service struct { + Image string `yaml:"image"` + Ports []string `yaml:"ports"` + Environment map[string]string `yaml:"environment"` + Labels map[string]string `yaml:"labels"` + DependsOn []string `yaml:"depends_on"` + Volumes []string `yaml:"volumes"` + Expose []int `yaml:"expose"` +} + +type Compose struct { + Version string `yaml:"version"` + Services map[string]Service `yaml:"services"` + Volumes map[string]interface{} `yaml:"volumes"` +} + +func NewCompose() *Compose { + c := &Compose{} + c.Services = make(map[string]Service) + c.Volumes = make(map[string]interface{}) + return c +} diff --git a/generator/main.go b/generator/main.go new file mode 100644 index 0000000..f09fd90 --- /dev/null +++ b/generator/main.go @@ -0,0 +1,215 @@ +package generator + +import ( + "fmt" + "helm-compose/compose" + "helm-compose/helm" + "log" + "strconv" + "strings" + "sync" + + "errors" +) + +var servicesMap = make(map[string]int) +var serviceWaiters = make(map[string][]chan int) +var locker = &sync.Mutex{} +var serviceTick = make(chan int, 0) + +var Ingresses = make([]*helm.Ingress, 0) +var Values = make(map[string]map[string]interface{}) + +var DependScript = ` +OK=0 +echo "Checking __service__ port" +while [ $OK != 1 ]; do + echo -n "." + nc -z {{ .Release.Name }}-__service__ __port__ && OK=1 + sleep 1 +done +echo +echo "Done" +` + +func CreateReplicaObject(name string, s compose.Service) (ret []interface{}) { + + o := helm.NewDeployment() + ret = append(ret, o) + o.Metadata.Name = "{{ .Release.Name }}-" + name + + container := helm.NewContainer(name, s.Image, s.Environment, s.Labels) + + container.Image = "{{ .Values." + name + ".image }}" + Values[name] = map[string]interface{}{ + "image": s.Image, + } + + for _, port := range s.Ports { + portNumber, _ := strconv.Atoi(port) + container.Ports = append(container.Ports, &helm.ContainerPort{ + Name: name, + ContainerPort: portNumber, + }) + } + for _, port := range s.Expose { + container.Ports = append(container.Ports, &helm.ContainerPort{ + Name: name, + ContainerPort: port, + }) + } + o.Spec.Template.Spec.Containers = []*helm.Container{container} + + o.Spec.Selector = map[string]interface{}{ + "matchLabels": buildSelector(name, s), + } + + o.Spec.Template.Metadata.Labels = buildSelector(name, s) + + wait := &sync.WaitGroup{} + initContainers := make([]*helm.Container, 0) + for _, dp := range s.DependsOn { + if len(s.Ports) == 0 && len(s.Expose) == 0 { + log.Fatalf("Sorry, you need to expose or declare at least one port for the %s service to check \"depends_on\"", name) + } + c := helm.NewContainer("check-"+name, "busybox", nil, s.Labels) + command := strings.ReplaceAll(strings.TrimSpace(DependScript), "__service__", dp) + + wait.Add(1) + go func(dp string) { + defer wait.Done() + p := -1 + if defaultPort, err := getPort(dp); err != nil { + p = <-waitPort(dp) + } else { + p = defaultPort + } + command = strings.ReplaceAll(command, "__port__", strconv.Itoa(p)) + + c.Command = []string{ + "sh", + "-c", + command, + } + initContainers = append(initContainers, c) + }(dp) + } + wait.Wait() + o.Spec.Template.Spec.InitContainers = initContainers + + if len(s.Ports) > 0 || len(s.Expose) > 0 { + ks := createService(name, s) + ret = append(ret, ks) + } + + return +} + +func createService(name string, s compose.Service) *helm.Service { + + ks := helm.NewService() + ks.Metadata.Name = "{{ .Release.Name }}-" + name + defaultPort := 0 + for i, p := range s.Ports { + port := strings.Split(p, ":") + src, _ := strconv.Atoi(port[0]) + target := src + if len(port) > 1 { + target, _ = strconv.Atoi(port[1]) + } + ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(src, target)) + if i == 0 { + defaultPort = target + detected(name, target) + } + } + for i, p := range s.Expose { + ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(p, p)) + if i == 0 { + defaultPort = p + detected(name, p) + } + } + + ks.Spec.Selector = buildSelector(name, s) + + if v, ok := s.Labels[helm.K+"/expose-ingress"]; ok && v == "true" { + log.Println("Expose ingress for ", name) + createIngress(name, defaultPort, s) + } + + return ks +} + +func createIngress(name string, port int, s compose.Service) { + ingress := helm.NewIngress(name) + Values[name]["ingress"] = map[string]interface{}{ + "host": "chart.example.tld", + "enabled": false, + } + ingress.Spec.Rules = []helm.IngressRule{ + { + Host: fmt.Sprintf("{{ .Values.%s.ingress.host }}", name), + Http: helm.IngressHttp{ + Paths: []helm.IngressPath{{ + Path: "/", + PathType: "Prefix", + Backend: helm.IngressBackend{ + Service: helm.IngressService{ + Name: "{{ .Release.Name }}-" + name, + Port: map[string]interface{}{ + "number": port, + }, + }, + }, + }}, + }, + }, + } + + locker.Lock() + Ingresses = append(Ingresses, ingress) + locker.Unlock() +} + +func detected(name string, port int) { + locker.Lock() + servicesMap[name] = port + go func() { + cx := serviceWaiters[name] + for _, c := range cx { + if v, ok := servicesMap[name]; ok { + c <- v + } + } + }() + locker.Unlock() +} + +func getPort(name string) (int, error) { + if v, ok := servicesMap[name]; ok { + return v, nil + } + return -1, errors.New("Not found") +} + +func waitPort(name string) chan int { + locker.Lock() + c := make(chan int, 0) + serviceWaiters[name] = append(serviceWaiters[name], c) + go func() { + if v, ok := servicesMap[name]; ok { + c <- v + } + }() + locker.Unlock() + return c +} + +func buildSelector(name string, s compose.Service) map[string]string { + + return map[string]string{ + "katenary.io/component": name, + "katenary.io/release": "{{ .Release.Name }}", + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d652a3a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module helm-compose + +go 1.16 + +require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/helm/deployment.go b/helm/deployment.go new file mode 100644 index 0000000..cb892f4 --- /dev/null +++ b/helm/deployment.go @@ -0,0 +1,80 @@ +package helm + +import "strings" + +type Deployment struct { + *K8sBase `yaml:",inline"` + Spec *DepSpec `yaml:"spec"` +} + +func NewDeployment() *Deployment { + d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()} + d.K8sBase.ApiVersion = "apps/v1" + d.K8sBase.Kind = "Deployment" + return d +} + +type DepSpec struct { + Replicas int `yaml:"replicas"` + Selector map[string]interface{} `yaml:"selector"` + Template PodTemplate `yaml:"template"` +} + +func NewDepSpec() *DepSpec { + return &DepSpec{ + Replicas: 1, + } +} + +type Value struct { + Name string `yaml:"name"` + Value interface{} `yaml:"value"` +} + +type ContainerPort struct { + Name string + ContainerPort int `yaml:"containerPort"` +} + +type Container struct { + Name string `yaml:"name,omitempty"` + Image string `yaml:"image"` + Ports []*ContainerPort `yaml:"ports,omitempty"` + Env []Value `yaml:"env,omitempty"` + Command []string `yaml:"command,omitempty"` +} + +func NewContainer(name, image string, environment, labels map[string]string) *Container { + container := &Container{ + Image: image, + Name: name, + Env: make([]Value, len(environment)), + } + + toServices := make([]string, 0) + if bound, ok := labels[K+"/to-services"]; ok { + toServices = strings.Split(bound, ",") + } + + idx := 0 + for n, v := range environment { + for _, name := range toServices { + if name == n { + v = "{{ .Release.Name }}-" + v + } + } + container.Env[idx] = Value{Name: n, Value: v} + idx++ + } + return container +} + +type PodSpec struct { + InitContainers []*Container `yaml:"initContainers,omitempty"` + Containers []*Container `yaml:"containers"` +} + +type PodTemplate struct { + Metadata Metadata `yaml:"metadata"` + Spec PodSpec `yaml:"spec"` +} diff --git a/helm/ingress.go b/helm/ingress.go new file mode 100644 index 0000000..640d259 --- /dev/null +++ b/helm/ingress.go @@ -0,0 +1,44 @@ +package helm + +type Ingress struct { + *K8sBase + Spec IngressSpec +} + +func NewIngress(name string) *Ingress { + i := &Ingress{} + i.K8sBase = NewBase() + i.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name + i.K8sBase.Kind = "Ingress" + i.ApiVersion = "networking.k8s.io/v1" + + return i +} + +type IngressSpec struct { + Rules []IngressRule +} + +type IngressRule struct { + Host string + Http IngressHttp +} + +type IngressHttp struct { + Paths []IngressPath +} + +type IngressPath struct { + Path string + PathType string + Backend IngressBackend +} + +type IngressBackend struct { + Service IngressService +} + +type IngressService struct { + Name string + Port map[string]interface{} +} diff --git a/helm/service.go b/helm/service.go new file mode 100644 index 0000000..46032c9 --- /dev/null +++ b/helm/service.go @@ -0,0 +1,42 @@ +package helm + +type Service struct { + *K8sBase `yaml:",inline"` + Spec *ServiceSpec `yaml:"spec"` +} + +func NewService() *Service { + s := &Service{ + K8sBase: NewBase(), + Spec: NewServiceSpec(), + } + s.K8sBase.Kind = "Service" + s.K8sBase.ApiVersion = "v1" + return s +} + +type ServicePort struct { + Protocol string `yaml:"protocol"` + Port int `yaml:"port"` + TargetPort int `yaml:"targetPort"` +} + +func NewServicePort(port, target int) *ServicePort { + return &ServicePort{ + Protocol: "TCP", + Port: port, + TargetPort: port, + } +} + +type ServiceSpec struct { + Selector map[string]string + Ports []*ServicePort +} + +func NewServiceSpec() *ServiceSpec { + return &ServiceSpec{ + Selector: make(map[string]string), + Ports: make([]*ServicePort, 0), + } +} diff --git a/helm/types.go b/helm/types.go new file mode 100644 index 0000000..ac0d13c --- /dev/null +++ b/helm/types.go @@ -0,0 +1,55 @@ +package helm + +import ( + "os" + "strings" +) + +const K = "katenary.io" + +var Version = "1.0" + +type Kinded interface { + Get() string +} + +type Metadata struct { + Name string `yaml:"name,omitempty"` + Labels map[string]string `yaml:"labels"` + Annotations map[string]string `yaml:"annotations,omitempty"` +} + +func NewMetadata() *Metadata { + return &Metadata{ + Name: "", + Labels: make(map[string]string), + Annotations: make(map[string]string), + } +} + +type K8sBase struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata *Metadata `yaml:"metadata"` +} + +func NewBase() *K8sBase { + + b := &K8sBase{ + Metadata: NewMetadata(), + } + b.Metadata.Labels[K+"/project"] = getProjectName() + b.Metadata.Labels[K+"/release"] = "{{ .Release.Name }}" + b.Metadata.Annotations[K+"/version"] = Version + return b +} + +func (k K8sBase) Get() string { + return k.Kind +} + +func getProjectName() string { + p, _ := os.Getwd() + path := strings.Split(p, string(os.PathSeparator)) + return path[len(path)-1] +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f1cddec --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "fmt" + "helm-compose/compose" + "helm-compose/generator" + "helm-compose/helm" + "os" + "path/filepath" + "sync" + + "gopkg.in/yaml.v3" +) + +var ComposeFile = "docker-compose.yaml" +var AppName = "MyApp" + +func main() { + + flag.StringVar(&ComposeFile, "compose", ComposeFile, "set the compose file to parse") + flag.StringVar(&AppName, "appname", AppName, "Give the helm chart app name") + flag.Parse() + + p := compose.NewParser(ComposeFile) + p.Parse(AppName) + wait := sync.WaitGroup{} + + files := make(map[string][]interface{}) + + for name, s := range p.Data.Services { + wait.Add(1) + go func(name string, s compose.Service) { + o := generator.CreateReplicaObject(name, s) + files[name] = o + wait.Done() + }(name, s) + } + wait.Wait() + + dirname := filepath.Join("chart", AppName) + templatesDir := filepath.Join(dirname, "templates") + os.MkdirAll(templatesDir, 0755) + + for n, f := range files { + for _, c := range f { + kind := c.(helm.Kinded).Get() + fname := filepath.Join(templatesDir, n+"."+kind+".yaml") + fp, _ := os.Create(fname) + enc := yaml.NewEncoder(fp) + enc.SetIndent(2) + enc.Encode(c) + fp.Close() + } + } + + for _, ing := range generator.Ingresses { + + fmt.Println("---") + enc := yaml.NewEncoder(os.Stdout) + enc.SetIndent(2) + enc.Encode(ing) + } + + enc := yaml.NewEncoder(os.Stdout) + enc.SetIndent(2) + enc.Encode(generator.Values) + +}