299 lines
7.2 KiB
Go
299 lines
7.2 KiB
Go
package generator
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"katenary/helm"
|
|
"katenary/logger"
|
|
"katenary/tools"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/compose-spec/compose-go/types"
|
|
)
|
|
|
|
type EnvVal = helm.EnvValue
|
|
|
|
const (
|
|
ICON_PACKAGE = "📦"
|
|
ICON_SERVICE = "🔌"
|
|
ICON_SECRET = "🔏"
|
|
ICON_CONF = "📝"
|
|
ICON_STORE = "⚡"
|
|
ICON_INGRESS = "🌐"
|
|
ICON_RBAC = "🔑"
|
|
ICON_CRON = "🕒"
|
|
)
|
|
|
|
// Values is kept in memory to create a values.yaml file.
|
|
var (
|
|
EmptyDirs = []string{}
|
|
servicesMap = make(map[string]int)
|
|
locker = &sync.Mutex{}
|
|
|
|
dependScript = `
|
|
OK=0
|
|
echo "Checking __service__ port"
|
|
while [ $OK != 1 ]; do
|
|
echo -n "."
|
|
nc -z ` + helm.ReleaseNameTpl + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1
|
|
done
|
|
echo
|
|
echo "Done"
|
|
`
|
|
|
|
madeDeployments = make(map[string]helm.Deployment, 0)
|
|
)
|
|
|
|
// Create a Deployment for a given compose.Service. It returns a list chan
|
|
// of HelmFileGenerator which will be used to generate the files (deployment, secrets, configMap...).
|
|
func CreateReplicaObject(name string, s types.ServiceConfig, linked map[string]types.ServiceConfig) HelmFileGenerator {
|
|
ret := make(chan HelmFile, runtime.NumCPU())
|
|
// there is a bug woth typs.ServiceConfig if we use the pointer. So we need to dereference it.
|
|
go buildDeployment(name, &s, linked, ret)
|
|
return ret
|
|
}
|
|
|
|
// Create a service (k8s).
|
|
func generateServicesAndIngresses(name string, s *types.ServiceConfig) []HelmFile {
|
|
|
|
ret := make([]HelmFile, 0) // can handle helm.Service or helm.Ingress
|
|
logger.Magenta(ICON_SERVICE+" Generating service for ", name)
|
|
ks := helm.NewService(name)
|
|
|
|
for _, p := range s.Ports {
|
|
target := int(p.Target)
|
|
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(target, target))
|
|
}
|
|
ks.Spec.Selector = buildSelector(name, s)
|
|
|
|
ret = append(ret, ks)
|
|
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
|
|
port, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
log.Fatalf("The given port \"%v\" as ingress port in \"%s\" service is not an integer\n", v, name)
|
|
}
|
|
logger.Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
|
|
ing := createIngress(name, port, s)
|
|
ret = append(ret, ing)
|
|
}
|
|
|
|
if len(s.Expose) > 0 {
|
|
logger.Magenta(ICON_SERVICE+" Generating service for ", name+"-external")
|
|
ks := helm.NewService(name + "-external")
|
|
ks.Spec.Type = "NodePort"
|
|
for _, expose := range s.Expose {
|
|
|
|
p, _ := strconv.Atoi(expose)
|
|
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(p, p))
|
|
}
|
|
ks.Spec.Selector = buildSelector(name, s)
|
|
ret = append(ret, ks)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// Create an ingress.
|
|
func createIngress(name string, port int, s *types.ServiceConfig) *helm.Ingress {
|
|
ingress := helm.NewIngress(name)
|
|
|
|
annotations := map[string]string{}
|
|
ingressVal := map[string]interface{}{
|
|
"class": "nginx",
|
|
"host": name + "." + helm.Appname + ".tld",
|
|
"enabled": false,
|
|
"annotations": annotations,
|
|
}
|
|
|
|
// add Annotations in values
|
|
AddValues(name, map[string]EnvVal{"ingress": ingressVal})
|
|
|
|
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: helm.ReleaseNameTpl + "-" + name,
|
|
Port: map[string]interface{}{
|
|
"number": port,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
ingress.SetIngressClass(name)
|
|
|
|
return ingress
|
|
}
|
|
|
|
// Build the selector for the service.
|
|
func buildSelector(name string, s *types.ServiceConfig) map[string]string {
|
|
return map[string]string{
|
|
"katenary.io/component": name,
|
|
"katenary.io/release": helm.ReleaseNameTpl,
|
|
}
|
|
}
|
|
|
|
// buildConfigMapFromPath generates a ConfigMap from a path.
|
|
func buildConfigMapFromPath(name, path string) *helm.ConfigMap {
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
files := make(map[string]string, 0)
|
|
if stat.IsDir() {
|
|
found, _ := filepath.Glob(path + "/*")
|
|
for _, f := range found {
|
|
if s, err := os.Stat(f); err != nil || s.IsDir() {
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "An error occured reading volume path %s\n", err.Error())
|
|
} else {
|
|
logger.ActivateColors = true
|
|
logger.Yellowf("Warning, %s is a directory, at this time we only "+
|
|
"can create configmap for first level file list\n", f)
|
|
logger.ActivateColors = false
|
|
}
|
|
continue
|
|
}
|
|
_, filename := filepath.Split(f)
|
|
c, _ := ioutil.ReadFile(f)
|
|
files[filename] = string(c)
|
|
}
|
|
} else {
|
|
c, _ := ioutil.ReadFile(path)
|
|
_, filename := filepath.Split(path)
|
|
files[filename] = string(c)
|
|
}
|
|
|
|
cm := helm.NewConfigMap(name, tools.GetRelPath(path))
|
|
cm.Data = files
|
|
return cm
|
|
}
|
|
|
|
// prepareProbes generate http/tcp/command probes for a service.
|
|
func prepareProbes(name string, s *types.ServiceConfig, container *helm.Container) {
|
|
// first, check if there a label for the probe
|
|
if check, ok := s.Labels[helm.LABEL_HEALTHCHECK]; ok {
|
|
check = strings.TrimSpace(check)
|
|
p := helm.NewProbeFromService(s)
|
|
// get the port of the "url" check
|
|
if checkurl, err := url.Parse(check); err == nil {
|
|
if err == nil {
|
|
container.LivenessProbe = buildProtoProbe(p, checkurl)
|
|
}
|
|
} else {
|
|
// it's a command
|
|
container.LivenessProbe = p
|
|
container.LivenessProbe.Exec = &helm.Exec{
|
|
Command: []string{
|
|
"sh",
|
|
"-c",
|
|
check,
|
|
},
|
|
}
|
|
}
|
|
return // label overrides everything
|
|
}
|
|
|
|
// if not, we will use the default one
|
|
if s.HealthCheck != nil {
|
|
container.LivenessProbe = buildCommandProbe(s)
|
|
}
|
|
}
|
|
|
|
// buildProtoProbe builds a probe from a url that can be http or tcp.
|
|
func buildProtoProbe(probe *helm.Probe, u *url.URL) *helm.Probe {
|
|
port, err := strconv.Atoi(u.Port())
|
|
if err != nil {
|
|
port = 80
|
|
}
|
|
|
|
path := "/"
|
|
if u.Path != "" {
|
|
path = u.Path
|
|
}
|
|
|
|
switch u.Scheme {
|
|
case "http", "https":
|
|
probe.HttpGet = &helm.HttpGet{
|
|
Path: path,
|
|
Port: port,
|
|
}
|
|
case "tcp":
|
|
probe.TCP = &helm.TCP{
|
|
Port: port,
|
|
}
|
|
default:
|
|
logger.Redf("Error while parsing healthcheck url %s\n", u.String())
|
|
os.Exit(1)
|
|
}
|
|
return probe
|
|
}
|
|
|
|
func buildCommandProbe(s *types.ServiceConfig) *helm.Probe {
|
|
|
|
// Get the first element of the command from ServiceConfig
|
|
first := s.HealthCheck.Test[0]
|
|
|
|
p := helm.NewProbeFromService(s)
|
|
switch first {
|
|
case "CMD", "CMD-SHELL":
|
|
// CMD or CMD-SHELL
|
|
p.Exec = &helm.Exec{
|
|
Command: s.HealthCheck.Test[1:],
|
|
}
|
|
return p
|
|
default:
|
|
// badly made but it should work...
|
|
p.Exec = &helm.Exec{
|
|
Command: []string(s.HealthCheck.Test),
|
|
}
|
|
return p
|
|
}
|
|
}
|
|
|
|
func setSecretVar(name string, s *types.ServiceConfig, c *helm.Container) *helm.Secret {
|
|
locker.Lock()
|
|
defer locker.Unlock()
|
|
// get the list of secret vars
|
|
secretvars, ok := s.Labels[helm.LABEL_SECRETVARS]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
store := helm.NewSecret(name, "")
|
|
for _, secretvar := range strings.Split(secretvars, ",") {
|
|
secretvar = strings.TrimSpace(secretvar)
|
|
// get the value from env
|
|
_, ok := s.Environment[secretvar]
|
|
if !ok {
|
|
continue
|
|
}
|
|
// add the secret
|
|
store.AddEnv(secretvar, ".Values."+name+".environment."+secretvar)
|
|
for i, env := range c.Env {
|
|
if env.Name == secretvar {
|
|
c.Env = append(c.Env[:i], c.Env[i+1:]...)
|
|
i--
|
|
}
|
|
}
|
|
// remove env from ServiceConfig
|
|
delete(s.Environment, secretvar)
|
|
}
|
|
return store
|
|
}
|