13 Commits

Author SHA1 Message Date
691c1a3b78 We must lock inside the goroutines 2021-12-17 12:08:50 +01:00
332f7a8787 Fix some locks problem 2021-12-17 12:05:38 +01:00
6273e5531a Fix doc and syntax 2021-12-17 11:48:32 +01:00
e0382a8b83 Fix directory check for .git 2021-12-17 11:46:35 +01:00
3385b61272 Check if .git is a directory 2021-12-17 11:40:56 +01:00
a0e02af06e The version was overwritten to empty string 2021-12-17 11:04:43 +01:00
7adac3662e Autodetect git version/branch/hash for appversion 2021-12-17 10:59:57 +01:00
ca9ab8a13b Set all help message to lower case 2021-12-17 10:35:21 +01:00
b16897b875 Fix the section following the config type. 2021-12-17 10:29:08 +01:00
8ccdb2854b Remove annotation for ingres.class
This yield an error on new Kubernetes
TODO: check k8s version like does "helm create"?
2021-12-17 10:28:16 +01:00
df60c2c866 Refactorisation to writers in generator package 2021-12-05 10:13:11 +01:00
fe2a655796 Enhancements, see details
- Added a message to explain to the user that it needs to build and push
  images
- The Release Name string is now a constant ins source code, later we will be able to
  change it to (for example) a template call
- SHA256 injected label (from docker-compose file content) is a bit long, SHA1 is shorter
- We now add compose command line and generation date in the Values file
- Code cleanup, refactorisation and enhancements
- More documentation in the source code
2021-12-05 09:05:48 +01:00
714ccf771d Mount with SELinux label to build 2021-12-05 07:40:11 +01:00
18 changed files with 392 additions and 197 deletions

View File

@@ -27,7 +27,7 @@ build: katenary
katenary: *.go generator/*.go compose/*.go helm/*.go katenary: *.go generator/*.go compose/*.go helm/*.go
@echo Build using $(CTN) @echo Build using $(CTN)
ifeq ($(CTN),podman) ifeq ($(CTN),podman)
@podman run --rm -v $(PWD):/go/src/katenary -w /go/src/katenary --userns keep-id -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" . @podman run --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
else else
@docker run --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" . @docker run --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
endif endif

View File

@@ -10,6 +10,10 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const (
ICON_EXCLAMATION = "❕"
)
// Parser is a docker-compose parser. // Parser is a docker-compose parser.
type Parser struct { type Parser struct {
Data *Compose Data *Compose
@@ -72,6 +76,18 @@ func NewParser(filename string) *Parser {
log.Fatal(strings.Join(missing, "\n")) log.Fatal(strings.Join(missing, "\n"))
} }
// check the build element
for name, s := range c.Services {
if s.RawBuild == nil {
continue
}
fmt.Println(ICON_EXCLAMATION +
" \x1b[33myou will need to build and push your image named \"" + s.Image + "\"" +
" for the \"" + name + "\" service \x1b[0m")
}
return p return p
} }

View File

@@ -25,4 +25,5 @@ type Service struct {
Volumes []string `yaml:"volumes"` Volumes []string `yaml:"volumes"`
Expose []int `yaml:"expose"` Expose []int `yaml:"expose"`
EnvFiles []string `yaml:"env_file"` EnvFiles []string `yaml:"env_file"`
RawBuild interface{} `yaml:"build"`
} }

View File

@@ -29,6 +29,10 @@ const (
ICON_INGRESS = "🌐" ICON_INGRESS = "🌐"
) )
const (
RELEASE_NAME = helm.RELEASE_NAME
)
// Values is kept in memory to create a values.yaml file. // Values is kept in memory to create a values.yaml file.
var Values = make(map[string]map[string]interface{}) var Values = make(map[string]map[string]interface{})
var VolumeValues = make(map[string]map[string]map[string]interface{}) var VolumeValues = make(map[string]map[string]map[string]interface{})
@@ -38,7 +42,7 @@ OK=0
echo "Checking __service__ port" echo "Checking __service__ port"
while [ $OK != 1 ]; do while [ $OK != 1 ]; do
echo -n "." echo -n "."
nc -z {{ .Release.Name }}-__service__ __port__ && OK=1 nc -z ` + RELEASE_NAME + `-__service__ __port__ && OK=1
sleep 1 sleep 1
done done
echo echo
@@ -91,8 +95,14 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
ActivateColors = false ActivateColors = false
os.Exit(2) os.Exit(2)
} }
section := "configMapRef"
if isSecret {
section = "secretRef"
}
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
"configMapRef": { section: {
"name": store.Metadata().Name, "name": store.Metadata().Name,
}, },
}) })
@@ -172,7 +182,7 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
cm := buildCMFromPath(volname) cm := buildCMFromPath(volname)
volname = strings.Replace(volname, "./", "", 1) volname = strings.Replace(volname, "./", "", 1)
volname = strings.ReplaceAll(volname, ".", "-") volname = strings.ReplaceAll(volname, ".", "-")
cm.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + volname + "-" + name cm.K8sBase.Metadata.Name = RELEASE_NAME + "-" + volname + "-" + name
// build a configmap from the volume path // build a configmap from the volume path
volumes = append(volumes, map[string]interface{}{ volumes = append(volumes, map[string]interface{}{
"name": volname, "name": volname,
@@ -191,7 +201,7 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
volumes = append(volumes, map[string]interface{}{ volumes = append(volumes, map[string]interface{}{
"name": volname, "name": volname,
"persistentVolumeClaim": map[string]string{ "persistentVolumeClaim": map[string]string{
"claimName": "{{ .Release.Name }}-" + volname, "claimName": RELEASE_NAME + "-" + volname,
}, },
}) })
mountPoints = append(mountPoints, map[string]interface{}{ mountPoints = append(mountPoints, map[string]interface{}{
@@ -269,20 +279,20 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any // But... some other deployment can wait for it, so we alert that this deployment hasn't got any
// associated service. // associated service.
if len(s.Ports) == 0 { if len(s.Ports) == 0 {
locker.Lock()
// alert any current or **futur** waiters that this service is not exposed // alert any current or **futur** waiters that this service is not exposed
go func() { go func() {
for { for {
select { select {
case <-time.Tick(1 * time.Millisecond): case <-time.Tick(1 * time.Millisecond):
locker.Lock()
for _, c := range serviceWaiters[name] { for _, c := range serviceWaiters[name] {
c <- -1 c <- -1
close(c) close(c)
} }
locker.Unlock()
} }
} }
}() }()
locker.Unlock()
} }
// add the volumes in Values // add the volumes in Values
@@ -324,7 +334,7 @@ func createService(name string, s *compose.Service) []interface{} {
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok { if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
port, err := strconv.Atoi(v) port, err := strconv.Atoi(v)
if err != nil { if err != nil {
log.Fatalf("The given port \"%v\" as ingress port in %s service is not an integer\n", v, name) log.Fatalf("The given port \"%v\" as ingress port in \"%s\" service is not an integer\n", v, name)
} }
Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name) Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
ing := createIngress(name, port, s) ing := createIngress(name, port, s)
@@ -362,7 +372,7 @@ func createIngress(name string, port int, s *compose.Service) *helm.Ingress {
PathType: "Prefix", PathType: "Prefix",
Backend: helm.IngressBackend{ Backend: helm.IngressBackend{
Service: helm.IngressService{ Service: helm.IngressService{
Name: "{{ .Release.Name }}-" + name, Name: RELEASE_NAME + "-" + name,
Port: map[string]interface{}{ Port: map[string]interface{}{
"number": port, "number": port,
}, },
@@ -381,17 +391,20 @@ func createIngress(name string, port int, s *compose.Service) *helm.Ingress {
// to be able to get the service name. It also try to send the data to any "waiter" for this service. // to be able to get the service name. It also try to send the data to any "waiter" for this service.
func detected(name string, port int) { func detected(name string, port int) {
locker.Lock() locker.Lock()
defer locker.Unlock()
if _, ok := servicesMap[name]; ok {
return
}
servicesMap[name] = port servicesMap[name] = port
go func() { go func() {
cx := serviceWaiters[name] locker.Lock()
for _, c := range cx { defer locker.Unlock()
if v, ok := servicesMap[name]; ok { if cx, ok := serviceWaiters[name]; ok {
c <- v for _, c := range cx {
//close(c) c <- port
} }
} }
}() }()
locker.Unlock()
} }
func getPort(name string) (int, error) { func getPort(name string) (int, error) {
@@ -404,22 +417,23 @@ func getPort(name string) (int, error) {
// Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function. // Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function.
func waitPort(name string) chan int { func waitPort(name string) chan int {
locker.Lock() locker.Lock()
defer locker.Unlock()
c := make(chan int, 0) c := make(chan int, 0)
serviceWaiters[name] = append(serviceWaiters[name], c) serviceWaiters[name] = append(serviceWaiters[name], c)
go func() { go func() {
locker.Lock()
defer locker.Unlock()
if v, ok := servicesMap[name]; ok { if v, ok := servicesMap[name]; ok {
c <- v c <- v
//close(c)
} }
}() }()
locker.Unlock()
return c return c
} }
func buildSelector(name string, s *compose.Service) map[string]string { func buildSelector(name string, s *compose.Service) map[string]string {
return map[string]string{ return map[string]string{
"katenary.io/component": name, "katenary.io/component": name,
"katenary.io/release": "{{ .Release.Name }}", "katenary.io/release": RELEASE_NAME,
} }
} }

110
generator/writer.go Normal file
View File

@@ -0,0 +1,110 @@
package generator
import (
"katenary/compose"
"katenary/generator/writers"
"katenary/helm"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"gopkg.in/yaml.v3"
)
var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`)
func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFile, dirName string) {
// make the appname global (yes... ugly but easy)
helm.Appname = appName
helm.Version = katernayVersion
templatesDir := filepath.Join(dirName, "templates")
files := make(map[string]chan interface{})
for name, s := range p.Data.Services {
files[name] = CreateReplicaObject(name, s)
}
// to generate notes, we need to keep an Ingresses list
ingresses := make(map[string]*helm.Ingress)
for n, f := range files {
for c := range f {
if c == nil {
break
}
kind := c.(helm.Kinded).Get()
kind = strings.ToLower(kind)
// Add a SHA inside the generated file, it's only
// to make it easy to check it the compose file corresponds to the
// generated helm chart
c.(helm.Signable).BuildSHA(composeFile)
// Some types need special fixes in yaml generation
switch c := c.(type) {
case *helm.Storage:
// For storage, we need to add a "condition" to activate it
writers.BuildStorage(c, n, templatesDir)
case *helm.Deployment:
// for the deployment, we need to fix persitence volumes
// to be activated only when the storage is "enabled",
// either we use an "emptyDir"
writers.BuildDeployment(c, n, templatesDir)
case *helm.Service:
// Change the type for service if it's an "exposed" port
writers.BuildService(c, n, templatesDir)
case *helm.Ingress:
// we need to make ingresses "activable" from values
ingresses[n] = c // keep it to generate notes
writers.BuildIngress(c, n, templatesDir)
case *helm.ConfigMap, *helm.Secret:
// there could be several files, so let's force the filename
name := c.(helm.Named).Name()
name = PrefixRE.ReplaceAllString(name, "")
writers.BuildConfigMap(c, kind, n, name, templatesDir)
default:
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.Close()
}
}
}
// Create the values.yaml file
fp, _ := os.Create(filepath.Join(dirName, "values.yaml"))
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(Values)
fp.Close()
// Create tht Chart.yaml file
fp, _ = os.Create(filepath.Join(dirName, "Chart.yaml"))
fp.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n")
fp.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n")
enc = yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(map[string]interface{}{
"apiVersion": "v2",
"name": appName,
"description": "A helm chart for " + appName,
"type": "application",
"version": "0.1.0",
"appVersion": appVersion,
})
fp.Close()
// And finally, create a NOTE.txt file
fp, _ = os.Create(filepath.Join(templatesDir, "NOTES.txt"))
fp.WriteString(helm.GenerateNotesFile(ingresses))
fp.Close()
}

View File

@@ -0,0 +1,17 @@
package writers
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
func BuildConfigMap(c interface{}, kind, servicename, name, templatesDir string) {
fname := filepath.Join(templatesDir, servicename+"."+name+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.Close()
}

View File

@@ -0,0 +1,40 @@
package writers
import (
"bytes"
"katenary/helm"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
func BuildDeployment(deployment *helm.Deployment, name, templatesDir string) {
kind := "deployment"
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
fp, _ := os.Create(fname)
buffer := bytes.NewBuffer(nil)
enc := yaml.NewEncoder(buffer)
enc.SetIndent(2)
enc.Encode(deployment)
_content := string(buffer.Bytes())
content := strings.Split(string(_content), "\n")
dataname := ""
component := deployment.Spec.Selector["matchLabels"].(map[string]string)[helm.K+"/component"]
for _, line := range content {
if strings.Contains(line, "name:") {
dataname = strings.Split(line, ":")[1]
dataname = strings.TrimSpace(dataname)
} else if strings.Contains(line, "persistentVolumeClaim") {
line = " {{- if .Values." + component + ".persistence." + dataname + ".enabled }}\n" + line
} else if strings.Contains(line, "claimName") {
line += "\n {{ else }}"
line += "\n emptyDir: {}"
line += "\n {{- end }}"
}
fp.WriteString(line + "\n")
}
fp.Close()
}

View File

@@ -0,0 +1,40 @@
package writers
import (
"bytes"
"katenary/helm"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
func BuildIngress(ingress *helm.Ingress, name, templatesDir string) {
kind := "ingress"
buffer := bytes.NewBuffer(nil)
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
enc := yaml.NewEncoder(buffer)
enc.SetIndent(2)
buffer.WriteString("{{- if .Values." + name + ".ingress.enabled -}}\n")
enc.Encode(ingress)
buffer.WriteString("{{- end -}}")
fp, _ := os.Create(fname)
content := string(buffer.Bytes())
lines := strings.Split(content, "\n")
for _, l := range lines {
if strings.Contains(l, "ingressClassName") {
p := strings.Split(l, ":")
condition := p[1]
condition = strings.ReplaceAll(condition, "'", "")
condition = strings.ReplaceAll(condition, "{{", "")
condition = strings.ReplaceAll(condition, "}}", "")
condition = strings.TrimSpace(condition)
condition = "{{- if " + condition + " }}"
l = " " + condition + "\n" + l + "\n {{- end }}"
}
fp.WriteString(l + "\n")
}
fp.Close()
}

View File

@@ -0,0 +1,23 @@
package writers
import (
"katenary/helm"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
func BuildService(service *helm.Service, name, templatesDir string) {
kind := "service"
suffix := ""
if service.Spec.Type == "NodePort" {
suffix = "-external"
}
fname := filepath.Join(templatesDir, name+suffix+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(service)
fp.Close()
}

View File

@@ -0,0 +1,21 @@
package writers
import (
"katenary/helm"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
func BuildStorage(storage *helm.Storage, name, templatesDir string) {
kind := "pvc"
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
fp, _ := os.Create(fname)
volname := storage.K8sBase.Metadata.Labels[helm.K+"/pvc-name"]
fp.WriteString("{{ if .Values." + name + ".persistence." + volname + ".enabled }}\n")
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(storage)
fp.WriteString("{{- end -}}")
}

View File

@@ -22,7 +22,7 @@ func NewConfigMap(name string) *ConfigMap {
base := NewBase() base := NewBase()
base.ApiVersion = "v1" base.ApiVersion = "v1"
base.Kind = "ConfigMap" base.Kind = "ConfigMap"
base.Metadata.Name = "{{ .Release.Name }}-" + name base.Metadata.Name = RELEASE_NAME + "-" + name
base.Metadata.Labels[K+"/component"] = name base.Metadata.Labels[K+"/component"] = name
return &ConfigMap{ return &ConfigMap{
K8sBase: base, K8sBase: base,
@@ -66,7 +66,7 @@ func NewSecret(name string) *Secret {
base := NewBase() base := NewBase()
base.ApiVersion = "v1" base.ApiVersion = "v1"
base.Kind = "Secret" base.Kind = "Secret"
base.Metadata.Name = "{{ .Release.Name }}-" + name base.Metadata.Name = RELEASE_NAME + "-" + name
base.Metadata.Labels[K+"/component"] = name base.Metadata.Labels[K+"/component"] = name
return &Secret{ return &Secret{
K8sBase: base, K8sBase: base,

View File

@@ -10,7 +10,7 @@ type Deployment struct {
func NewDeployment(name string) *Deployment { func NewDeployment(name string) *Deployment {
d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()} d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()}
d.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name d.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
d.K8sBase.ApiVersion = "apps/v1" d.K8sBase.ApiVersion = "apps/v1"
d.K8sBase.Kind = "Deployment" d.K8sBase.Kind = "Deployment"
d.K8sBase.Metadata.Labels[K+"/component"] = name d.K8sBase.Metadata.Labels[K+"/component"] = name
@@ -67,7 +67,7 @@ func NewContainer(name, image string, environment, labels map[string]string) *Co
for n, v := range environment { for n, v := range environment {
for _, name := range toServices { for _, name := range toServices {
if name == n { if name == n {
v = "{{ .Release.Name }}-" + v v = RELEASE_NAME + "-" + v
} }
} }
container.Env[idx] = Value{Name: n, Value: v} container.Env[idx] = Value{Name: n, Value: v}

View File

@@ -8,7 +8,7 @@ type Ingress struct {
func NewIngress(name string) *Ingress { func NewIngress(name string) *Ingress {
i := &Ingress{} i := &Ingress{}
i.K8sBase = NewBase() i.K8sBase = NewBase()
i.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name i.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
i.K8sBase.Kind = "Ingress" i.K8sBase.Kind = "Ingress"
i.ApiVersion = "networking.k8s.io/v1" i.ApiVersion = "networking.k8s.io/v1"
i.K8sBase.Metadata.Labels[K+"/component"] = name i.K8sBase.Metadata.Labels[K+"/component"] = name
@@ -18,7 +18,6 @@ func NewIngress(name string) *Ingress {
func (i *Ingress) SetIngressClass(name string) { func (i *Ingress) SetIngressClass(name string) {
class := "{{ .Values." + name + ".ingress.class }}" class := "{{ .Values." + name + ".ingress.class }}"
i.Metadata.Annotations["kubernetes.io/ingress.class"] = class
i.Spec.IngressClassName = class i.Spec.IngressClassName = class
} }

View File

@@ -10,7 +10,7 @@ Your application is now deployed. This may take a while to be up and responding.
__list__ __list__
` `
func GenNotes(ingressess map[string]*Ingress) string { func GenerateNotesFile(ingressess map[string]*Ingress) string {
list := make([]string, 0) list := make([]string, 0)

View File

@@ -10,7 +10,7 @@ func NewService(name string) *Service {
K8sBase: NewBase(), K8sBase: NewBase(),
Spec: NewServiceSpec(), Spec: NewServiceSpec(),
} }
s.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name s.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name
s.K8sBase.Kind = "Service" s.K8sBase.Kind = "Service"
s.K8sBase.ApiVersion = "v1" s.K8sBase.ApiVersion = "v1"
s.K8sBase.Metadata.Labels[K+"/component"] = name s.K8sBase.Metadata.Labels[K+"/component"] = name

View File

@@ -11,7 +11,7 @@ func NewPVC(name, storageName string) *Storage {
pvc.K8sBase.Kind = "PersistentVolumeClaim" pvc.K8sBase.Kind = "PersistentVolumeClaim"
pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName
pvc.K8sBase.ApiVersion = "v1" pvc.K8sBase.ApiVersion = "v1"
pvc.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + storageName pvc.K8sBase.Metadata.Name = RELEASE_NAME + "-" + storageName
pvc.K8sBase.Metadata.Labels[K+"/component"] = name pvc.K8sBase.Metadata.Labels[K+"/component"] = name
pvc.Spec = &PVCSpec{ pvc.Spec = &PVCSpec{
Resouces: map[string]interface{}{ Resouces: map[string]interface{}{

View File

@@ -1,7 +1,7 @@
package helm package helm
import ( import (
"crypto/sha256" "crypto/sha1"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -9,6 +9,7 @@ import (
) )
const K = "katenary.io" const K = "katenary.io"
const RELEASE_NAME = "{{ .Release.Name }}"
const ( const (
LABEL_ENV_SECRET = K + "/secret-envfiles" LABEL_ENV_SECRET = K + "/secret-envfiles"
LABEL_PORT = K + "/ports" LABEL_PORT = K + "/ports"
@@ -17,9 +18,10 @@ const (
LABEL_VOL_CM = K + "/configmap-volumes" LABEL_VOL_CM = K + "/configmap-volumes"
) )
var Appname = "" var (
Appname = ""
var Version = "1.0" // should be set from main.Version Version = "1.0" // should be set from main.Version
)
type Kinded interface { type Kinded interface {
Get() string Get() string
@@ -58,16 +60,18 @@ func NewBase() *K8sBase {
b := &K8sBase{ b := &K8sBase{
Metadata: NewMetadata(), Metadata: NewMetadata(),
} }
// add some information of the build
b.Metadata.Labels[K+"/project"] = GetProjectName() b.Metadata.Labels[K+"/project"] = GetProjectName()
b.Metadata.Labels[K+"/release"] = "{{ .Release.Name }}" b.Metadata.Labels[K+"/release"] = RELEASE_NAME
b.Metadata.Annotations[K+"/version"] = Version b.Metadata.Annotations[K+"/version"] = Version
return b return b
} }
func (k *K8sBase) BuildSHA(filename string) { func (k *K8sBase) BuildSHA(filename string) {
c, _ := ioutil.ReadFile(filename) c, _ := ioutil.ReadFile(filename)
sum := sha256.Sum256(c) //sum := sha256.Sum256(c)
k.Metadata.Annotations[K+"/docker-compose-sha256"] = fmt.Sprintf("%x", string(sum[:])) sum := sha1.Sum(c)
k.Metadata.Annotations[K+"/docker-compose-sha1"] = fmt.Sprintf("%x", string(sum[:]))
} }
func (k *K8sBase) Get() string { func (k *K8sBase) Get() string {

236
main.go
View File

@@ -1,51 +1,96 @@
package main package main
import ( import (
"bytes" "errors"
"flag" "flag"
"fmt" "fmt"
"katenary/compose" "katenary/compose"
"katenary/generator" "katenary/generator"
"katenary/helm" "katenary/helm"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"gopkg.in/yaml.v3"
) )
var ComposeFile = "docker-compose.yaml" var ComposeFile = "docker-compose.yaml"
var AppName = "MyApp" var AppName = "MyApp"
var AppVersion = "0.0.1" var Version = "master" // set at build time to the git version/tag
var Version = "master"
var ChartsDir = "chart" var ChartsDir = "chart"
var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`) func detectGitVersion() (string, error) {
defaulVersion := "0.0.1"
// Check if .git directory exists
if s, err := os.Stat(".git"); err != nil {
// .git should be a directory
return defaulVersion, errors.New("no git repository found")
} else if !s.IsDir() {
// .git should be a directory
return defaulVersion, errors.New(".git is not a directory")
}
// check if "git" executable is callable
if _, err := exec.LookPath("git"); err != nil {
return defaulVersion, errors.New("git executable not found")
}
// get the latest commit hash
if out, err := exec.Command("git", "log", "-n1", "--pretty=format:%h").Output(); err == nil {
latestCommit := strings.TrimSpace(string(out))
// then get the current branch/tag
out, err := exec.Command("git", "branch", "--show-current").Output()
if err != nil {
return defaulVersion, errors.New("git branch --show-current failed")
} else {
currentBranch := strings.TrimSpace(string(out))
// finally, check if the current tag (if exists) correspond to the current commit
// git describe --exact-match --tags <latestCommit>
out, err := exec.Command("git", "describe", "--exact-match", "--tags", latestCommit).Output()
if err == nil {
return strings.TrimSpace(string(out)), nil
} else {
return currentBranch + "-" + latestCommit, nil
}
}
}
return defaulVersion, errors.New("git log failed")
}
func main() { func main() {
appVersion := "0.0.1"
helpMessageForAppversion := "The version of the application. " +
"Default is 0.0.1. If you are using git, it will be the git version. " +
"Otherwise, it will be the branch name and the commit hash."
if v, err := detectGitVersion(); err == nil {
appVersion = v
helpMessageForAppversion = "The version of the application. " +
"If not set, the version will be detected from git."
}
// flags
flag.StringVar(&ChartsDir, "chart-dir", ChartsDir, "set the chart directory") flag.StringVar(&ChartsDir, "chart-dir", ChartsDir, "set the chart directory")
flag.StringVar(&ComposeFile, "compose", ComposeFile, "set the compose file to parse") flag.StringVar(&ComposeFile, "compose", ComposeFile, "set the compose file to parse")
flag.StringVar(&AppName, "appname", helm.GetProjectName(), "set the helm chart app name") flag.StringVar(&AppName, "appname", helm.GetProjectName(), "set the helm chart app name")
flag.StringVar(&AppVersion, "appversion", AppVersion, "set the chart appVersion") flag.StringVar(&appVersion, "appversion", appVersion, helpMessageForAppversion)
version := flag.Bool("version", false, "Show version and exit") version := flag.Bool("version", false, "show version and exit")
force := flag.Bool("force", false, "force the removal of the chart-dir") force := flag.Bool("force", false, "force the removal of the chart-dir")
flag.Parse() flag.Parse()
// Only display the version
if *version { if *version {
fmt.Println(Version) fmt.Println(Version)
os.Exit(0) os.Exit(0)
} }
// make the appname global (yes...)
helm.Appname = AppName
dirname := filepath.Join(ChartsDir, AppName) dirname := filepath.Join(ChartsDir, AppName)
if _, err := os.Stat(dirname); err == nil && !*force { if _, err := os.Stat(dirname); err == nil && !*force {
response := "" response := ""
for response != "y" && response != "n" { for response != "y" && response != "n" {
response = "n" response = "n"
fmt.Printf("The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\nDo you really want to continue ? [y/N]: ", dirname) fmt.Printf(""+
"The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\n"+
"Do you really want to continue? [y/N]: ", dirname)
fmt.Scanf("%s", &response) fmt.Scanf("%s", &response)
response = strings.ToLower(response) response = strings.ToLower(response)
} }
@@ -55,159 +100,24 @@ func main() {
} }
} }
os.RemoveAll(dirname) // cleanup and create the chart directory (until "templates")
templatesDir := filepath.Join(dirname, "templates") if err := os.RemoveAll(dirname); err != nil {
os.MkdirAll(templatesDir, 0755) fmt.Printf("Error removing %s: %s\n", dirname, err)
os.Exit(1)
}
helm.Version = Version // create the templates directory
templatesDir := filepath.Join(dirname, "templates")
if err := os.MkdirAll(templatesDir, 0755); err != nil {
fmt.Printf("Error creating %s: %s\n", templatesDir, err)
os.Exit(1)
}
// Parse the compose file now
p := compose.NewParser(ComposeFile) p := compose.NewParser(ComposeFile)
p.Parse(AppName) p.Parse(AppName)
files := make(map[string]chan interface{}) // start generator
generator.Generate(p, Version, AppName, appVersion, ComposeFile, dirname)
//wait := sync.WaitGroup{}
for name, s := range p.Data.Services {
//wait.Add(1)
// it's mandatory to build in goroutines because some dependencies can
// wait for a port number discovery.
// So the entire services are built in parallel.
//go func(name string, s compose.Service) {
// defer wait.Done()
o := generator.CreateReplicaObject(name, s)
files[name] = o
//}(name, s)
}
//wait.Wait()
// to generate notes, we need to keep an Ingresses list
ingresses := make(map[string]*helm.Ingress)
for n, f := range files {
for c := range f {
if c == nil {
break
}
kind := c.(helm.Kinded).Get()
kind = strings.ToLower(kind)
c.(helm.Signable).BuildSHA(ComposeFile)
switch c := c.(type) {
case *helm.Storage:
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
fp, _ := os.Create(fname)
volname := c.K8sBase.Metadata.Labels[helm.K+"/pvc-name"]
fp.WriteString("{{ if .Values." + n + ".persistence." + volname + ".enabled }}\n")
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.WriteString("{{- end -}}")
case *helm.Deployment:
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
fp, _ := os.Create(fname)
buffer := bytes.NewBuffer(nil)
enc := yaml.NewEncoder(buffer)
enc.SetIndent(2)
enc.Encode(c)
_content := string(buffer.Bytes())
content := strings.Split(string(_content), "\n")
dataname := ""
component := c.Spec.Selector["matchLabels"].(map[string]string)[helm.K+"/component"]
for _, line := range content {
if strings.Contains(line, "name:") {
dataname = strings.Split(line, ":")[1]
dataname = strings.TrimSpace(dataname)
} else if strings.Contains(line, "persistentVolumeClaim") {
line = " {{- if .Values." + component + ".persistence." + dataname + ".enabled }}\n" + line
} else if strings.Contains(line, "claimName") {
line += "\n {{ else }}"
line += "\n emptyDir: {}"
line += "\n {{- end }}"
}
fp.WriteString(line + "\n")
}
fp.Close()
case *helm.Service:
suffix := ""
if c.Spec.Type == "NodePort" {
suffix = "-external"
}
fname := filepath.Join(templatesDir, n+suffix+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.Close()
case *helm.Ingress:
buffer := bytes.NewBuffer(nil)
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
ingresses[n] = c // keep it to generate notes
enc := yaml.NewEncoder(buffer)
enc.SetIndent(2)
buffer.WriteString("{{- if .Values." + n + ".ingress.enabled -}}\n")
enc.Encode(c)
buffer.WriteString("{{- end -}}")
fp, _ := os.Create(fname)
content := string(buffer.Bytes())
lines := strings.Split(content, "\n")
for _, l := range lines {
if strings.Contains(l, "ingressClassName") {
p := strings.Split(l, ":")
condition := p[1]
condition = strings.ReplaceAll(condition, "'", "")
condition = strings.ReplaceAll(condition, "{{", "")
condition = strings.ReplaceAll(condition, "}}", "")
condition = strings.TrimSpace(condition)
condition = "{{- if " + condition + " }}"
l = " " + condition + "\n" + l + "\n {{- end }}"
}
fp.WriteString(l + "\n")
}
fp.Close()
case *helm.ConfigMap, *helm.Secret:
// there could be several files, so let's force the filename
name := c.(helm.Named).Name()
name = PrefixRE.ReplaceAllString(name, "")
fname := filepath.Join(templatesDir, n+"."+name+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.Close()
default:
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
fp, _ := os.Create(fname)
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(c)
fp.Close()
}
}
}
fp, _ := os.Create(filepath.Join(dirname, "values.yaml"))
enc := yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(generator.Values)
fp.Close()
fp, _ = os.Create(filepath.Join(dirname, "Chart.yaml"))
enc = yaml.NewEncoder(fp)
enc.SetIndent(2)
enc.Encode(map[string]interface{}{
"apiVersion": "v2",
"name": AppName,
"description": "A helm chart for " + AppName,
"type": "application",
"version": "0.1.0",
"appVersion": AppVersion,
})
fp.Close()
fp, _ = os.Create(filepath.Join(templatesDir, "NOTES.txt"))
fp.WriteString(helm.GenNotes(ingresses))
fp.Close()
} }