Initial version

This commit is contained in:
2021-11-30 12:04:28 +01:00
commit f095f39eaf
9 changed files with 567 additions and 0 deletions

33
compose/parser.go Normal file
View File

@@ -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
}

24
compose/types.go Normal file
View File

@@ -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
}

215
generator/main.go Normal file
View File

@@ -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 }}",
}
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module helm-compose
go 1.16
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b

80
helm/deployment.go Normal file
View File

@@ -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"`
}

44
helm/ingress.go Normal file
View File

@@ -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{}
}

42
helm/service.go Normal file
View File

@@ -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),
}
}

55
helm/types.go Normal file
View File

@@ -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]
}

69
main.go Normal file
View File

@@ -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)
}