package generator import ( "fmt" "katenary/compose" "katenary/helm" "os" "strconv" "strings" "sync" "errors" ) var servicesMap = make(map[string]int) var serviceWaiters = make(map[string][]chan int) var locker = &sync.Mutex{} // Values is kept in memory to create a values.yaml file. var Values = make(map[string]map[string]interface{}) var VolumeValues = make(map[string]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" ` // Create a Deployment for a given compose.Service. It returns a list of objects: a Deployment and a possible Service (kubernetes represnetation as maps). func CreateReplicaObject(name string, s compose.Service) (ret []interface{}) { Magenta("Generating deployment for ", name) o := helm.NewDeployment(name) ret = append(ret, o) container := helm.NewContainer(name, s.Image, s.Environment, s.Labels) secretsFiles := make([]string, 0) if v, ok := s.Labels[helm.K+"/as-secret"]; ok { secretsFiles = strings.Split(v, ",") } for _, envfile := range s.EnvFiles { f := strings.ReplaceAll(envfile, "_", "-") f = strings.ReplaceAll(f, ".env", "") f = strings.ReplaceAll(f, ".", "-") cf := f + "-" + name isSecret := false for _, s := range secretsFiles { if s == envfile { isSecret = true } } var store helm.InlineConfig if !isSecret { Bluef("Generating configMap %s\n", cf) store = helm.NewConfigMap(cf) } else { Bluef("Generating secret %s\n", cf) store = helm.NewSecret(cf) } if err := store.AddEnvFile(envfile); err != nil { Red(err.Error()) os.Exit(2) } container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ "configMapRef": { "name": store.Metadata().Name, }, }) ret = append(ret, store) if isSecret { Greenf("Done secret %s\n", cf) } else { Greenf("Done configMap %s\n", cf) } } container.Image = "{{ .Values." + name + ".image }}" Values[name] = map[string]interface{}{ "image": s.Image, } exists := make(map[int]string) for _, port := range s.Ports { portNumber, _ := strconv.Atoi(port) portName := name for _, n := range exists { if name == n { portName = fmt.Sprintf("%s-%d", name, portNumber) } } container.Ports = append(container.Ports, &helm.ContainerPort{ Name: portName, ContainerPort: portNumber, }) exists[portNumber] = name } for _, port := range s.Expose { if _, exist := exists[port]; exist { continue } container.Ports = append(container.Ports, &helm.ContainerPort{ Name: name, ContainerPort: port, }) } volumes := make([]map[string]interface{}, 0) mountPoints := make([]interface{}, 0) for _, volume := range s.Volumes { parts := strings.Split(volume, ":") volname := parts[0] volepath := parts[1] if strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/") { Redf("You cannot, at this time, have local volume in %s service", name) os.Exit(1) } pvc := helm.NewPVC(name, volname) ret = append(ret, pvc) volumes = append(volumes, map[string]interface{}{ "name": volname, "persistentVolumeClaim": map[string]string{ "claimName": "{{ .Release.Name }}-" + volname, }, }) mountPoints = append(mountPoints, map[string]interface{}{ "name": volname, "mountPath": volepath, }) Yellow("Generate volume values for ", volname) locker.Lock() if _, ok := VolumeValues[name]; !ok { VolumeValues[name] = make(map[string]map[string]interface{}) } VolumeValues[name][volname] = map[string]interface{}{ "enabled": false, "capacity": "1Gi", } locker.Unlock() } container.VolumeMounts = mountPoints o.Spec.Template.Spec.Volumes = volumes 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 { Redf("No port exposed for %s that is in dependency", name) os.Exit(1) } c := helm.NewContainer("check-"+dp, "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...) } if len(VolumeValues[name]) > 0 { Values[name]["persistence"] = VolumeValues[name] } Green("Done deployment ", name) return } // Create a service (k8s). func createService(name string, s compose.Service) []interface{} { Magenta("Generating service for ", name) ks := helm.NewService(name) defaultPort := 0 names := make(map[int]int) 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)) names[target] = 1 if i == 0 { defaultPort = target detected(name, target) } } for i, p := range s.Expose { if _, ok := names[p]; ok { continue } 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) ret := make([]interface{}, 0) ret = append(ret, ks) if v, ok := s.Labels[helm.K+"/expose-ingress"]; ok && v == "true" { ing := createIngress(name, defaultPort, s) ret = append(ret, ing) } Green("Done service ", name) return ret } // Create an ingress. func createIngress(name string, port int, s compose.Service) *helm.Ingress { ingress := helm.NewIngress(name) Values[name]["ingress"] = map[string]interface{}{ "class": "nginx", "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, }, }, }, }}, }, }, } ingress.SetIngressClass(name) return ingress } // This function is called when a possible service is detected, it append the port in a map to make others 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) { 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") } // Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function. 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 }}", } }