Initial version
This commit is contained in:
33
compose/parser.go
Normal file
33
compose/parser.go
Normal 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
24
compose/types.go
Normal 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
215
generator/main.go
Normal 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
5
go.mod
Normal 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
80
helm/deployment.go
Normal 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
44
helm/ingress.go
Normal 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
42
helm/service.go
Normal 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
55
helm/types.go
Normal 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
69
main.go
Normal 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)
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user