2021-11-30 12:04:28 +01:00
package generator
import (
"fmt"
2021-12-01 08:31:51 +01:00
"katenary/compose"
"katenary/helm"
2021-12-01 16:50:32 +01:00
"log"
2021-11-30 15:35:32 +01:00
"os"
2021-11-30 12:04:28 +01:00
"strconv"
"strings"
"sync"
2021-12-02 10:21:05 +00:00
"time"
2021-11-30 12:04:28 +01:00
"errors"
)
var servicesMap = make ( map [ string ] int )
var serviceWaiters = make ( map [ string ] [ ] chan int )
var locker = & sync . Mutex { }
2021-11-30 15:45:36 +01:00
2021-12-02 10:21:05 +00:00
const (
ICON_PACKAGE = "📦"
ICON_SERVICE = "🔌"
ICON_SECRET = "🔏"
ICON_CONF = "📝"
ICON_STORE = "⚡"
ICON_INGRESS = "🌐"
)
2021-11-30 15:45:36 +01:00
// Values is kept in memory to create a values.yaml file.
2021-11-30 12:04:28 +01:00
var Values = make ( map [ string ] map [ string ] interface { } )
2021-11-30 17:29:42 +01:00
var VolumeValues = make ( map [ string ] map [ string ] map [ string ] interface { } )
2021-11-30 12:04:28 +01:00
2021-11-30 15:45:36 +01:00
var dependScript = `
2021-11-30 12:04:28 +01:00
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"
`
2021-11-30 15:45:36 +01:00
// Create a Deployment for a given compose.Service. It returns a list of objects: a Deployment and a possible Service (kubernetes represnetation as maps).
2021-12-02 10:21:05 +00:00
func CreateReplicaObject ( name string , s compose . Service ) chan interface { } {
// fetch label to specific exposed port, and add them in "ports" section
if portlabel , ok := s . Labels [ helm . K + "/service-ports" ] ; ok {
services := strings . Split ( portlabel , "," )
for _ , serviceport := range services {
portexists := false
for _ , found := range s . Ports {
if found == serviceport {
portexists = true
}
}
if ! portexists {
s . Ports = append ( s . Ports , serviceport )
}
}
}
2021-11-30 12:04:28 +01:00
2021-12-02 10:21:05 +00:00
ret := make ( chan interface { } , len ( s . Ports ) + len ( s . Expose ) + 1 )
go parseService ( name , s , ret )
return ret
}
// This function will try to yied deployment and services based on a service from the compose file structure.
func parseService ( name string , s compose . Service , ret chan interface { } ) {
Magenta ( ICON_PACKAGE + " Generating deployment for " , name )
2021-11-30 12:04:28 +01:00
2021-12-02 10:21:05 +00:00
o := helm . NewDeployment ( name )
2021-11-30 12:04:28 +01:00
container := helm . NewContainer ( name , s . Image , s . Environment , s . Labels )
2021-12-02 10:21:05 +00:00
// prepare secrets
2021-12-01 13:55:22 +01:00
secretsFiles := make ( [ ] string , 0 )
if v , ok := s . Labels [ helm . K + "/as-secret" ] ; ok {
secretsFiles = strings . Split ( v , "," )
}
2021-12-02 10:21:05 +00:00
// manage environment files (env_file in compose)
2021-12-01 11:53:10 +01:00
for _ , envfile := range s . EnvFiles {
2021-12-01 12:02:44 +01:00
f := strings . ReplaceAll ( envfile , "_" , "-" )
f = strings . ReplaceAll ( f , ".env" , "" )
2021-12-01 13:55:22 +01:00
f = strings . ReplaceAll ( f , "." , "-" )
2021-12-01 12:02:44 +01:00
cf := f + "-" + name
2021-12-01 13:55:22 +01:00
isSecret := false
for _ , s := range secretsFiles {
if s == envfile {
isSecret = true
}
}
var store helm . InlineConfig
if ! isSecret {
2021-12-02 10:21:05 +00:00
Bluef ( ICON_CONF + " Generating configMap %s\n" , cf )
2021-12-01 13:55:22 +01:00
store = helm . NewConfigMap ( cf )
} else {
2021-12-02 10:21:05 +00:00
Bluef ( ICON_SECRET + " Generating secret %s\n" , cf )
2021-12-01 13:55:22 +01:00
store = helm . NewSecret ( cf )
}
if err := store . AddEnvFile ( envfile ) ; err != nil {
2021-12-01 11:53:10 +01:00
Red ( err . Error ( ) )
os . Exit ( 2 )
}
container . EnvFrom = append ( container . EnvFrom , map [ string ] map [ string ] string {
"configMapRef" : {
2021-12-01 13:55:22 +01:00
"name" : store . Metadata ( ) . Name ,
2021-12-01 11:53:10 +01:00
} ,
} )
2021-12-02 10:21:05 +00:00
ret <- store
2021-12-01 14:06:06 +01:00
}
2021-12-01 11:53:10 +01:00
2021-12-02 10:21:05 +00:00
// check the image, and make it "variable" in values.yaml
2021-11-30 12:04:28 +01:00
container . Image = "{{ .Values." + name + ".image }}"
Values [ name ] = map [ string ] interface { } {
"image" : s . Image ,
}
2021-12-02 10:21:05 +00:00
// manage ports
2021-11-30 15:35:32 +01:00
exists := make ( map [ int ] string )
2021-11-30 12:04:28 +01:00
for _ , port := range s . Ports {
2021-12-01 16:50:32 +01:00
_p := strings . Split ( port , ":" )
port = _p [ 0 ]
if len ( _p ) > 1 {
port = _p [ 1 ]
}
2021-11-30 12:04:28 +01:00
portNumber , _ := strconv . Atoi ( port )
2021-11-30 15:35:32 +01:00
portName := name
for _ , n := range exists {
if name == n {
portName = fmt . Sprintf ( "%s-%d" , name , portNumber )
}
}
2021-11-30 12:04:28 +01:00
container . Ports = append ( container . Ports , & helm . ContainerPort {
2021-11-30 15:35:32 +01:00
Name : portName ,
2021-11-30 12:04:28 +01:00
ContainerPort : portNumber ,
} )
2021-11-30 15:35:32 +01:00
exists [ portNumber ] = name
2021-11-30 12:04:28 +01:00
}
2021-12-02 10:21:05 +00:00
// manage the "expose" section to be a NodePort in Kubernetes
2021-11-30 12:04:28 +01:00
for _ , port := range s . Expose {
2021-11-30 15:35:32 +01:00
if _ , exist := exists [ port ] ; exist {
continue
}
2021-11-30 12:04:28 +01:00
container . Ports = append ( container . Ports , & helm . ContainerPort {
Name : name ,
ContainerPort : port ,
} )
}
2021-11-30 17:29:42 +01:00
2021-12-02 10:21:05 +00:00
// Prepare volumes
2021-11-30 17:29:42 +01:00
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 , "/" ) {
2021-12-02 10:21:05 +00:00
// local volume cannt be mounted
// TODO: propose a way to make configMap for some files or directory
2021-11-30 17:29:42 +01:00
Redf ( "You cannot, at this time, have local volume in %s service" , name )
2021-12-02 10:21:05 +00:00
continue
//os.Exit(1)
2021-11-30 17:29:42 +01:00
}
pvc := helm . NewPVC ( name , volname )
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 ,
} )
2021-12-02 10:21:05 +00:00
Yellow ( ICON_STORE + " Generate volume values for " , volname , " in deployment " , name )
2021-11-30 17:29:42 +01:00
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 ( )
2021-12-02 10:21:05 +00:00
ret <- pvc
2021-11-30 17:29:42 +01:00
}
container . VolumeMounts = mountPoints
o . Spec . Template . Spec . Volumes = volumes
2021-11-30 12:04:28 +01:00
o . Spec . Template . Spec . Containers = [ ] * helm . Container { container }
2021-12-02 10:21:05 +00:00
// Add some labels
2021-11-30 12:04:28 +01:00
o . Spec . Selector = map [ string ] interface { } {
"matchLabels" : buildSelector ( name , s ) ,
}
o . Spec . Template . Metadata . Labels = buildSelector ( name , s )
2021-12-02 10:21:05 +00:00
// Now, for "depends_on" section, it's a bit tricky...
// We need to detect "others" services, but we probably not have parsed them yet, so
// we will wait for them for a while.
2021-11-30 12:04:28 +01:00
initContainers := make ( [ ] * helm . Container , 0 )
for _ , dp := range s . DependsOn {
2021-11-30 15:35:32 +01:00
c := helm . NewContainer ( "check-" + dp , "busybox" , nil , s . Labels )
2021-11-30 15:45:36 +01:00
command := strings . ReplaceAll ( strings . TrimSpace ( dependScript ) , "__service__" , dp )
2021-11-30 12:04:28 +01:00
2021-12-02 10:21:05 +00:00
foundPort := - 1
if defaultPort , err := getPort ( dp ) ; err != nil {
// BUG: Sometimes the chan remains opened
foundPort := <- waitPort ( dp )
if foundPort == - 1 {
log . Fatalf (
"ERROR, the %s service is waiting for %s port number, " +
"but it is never discovered. You must declare at least one port in " +
"the \"ports\" section of the service in the docker-compose file" ,
name ,
dp ,
)
2021-11-30 12:04:28 +01:00
}
2021-12-02 10:21:05 +00:00
} else {
foundPort = defaultPort
}
command = strings . ReplaceAll ( command , "__port__" , strconv . Itoa ( foundPort ) )
2021-11-30 12:04:28 +01:00
2021-12-02 10:21:05 +00:00
c . Command = [ ] string {
"sh" ,
"-c" ,
command ,
}
initContainers = append ( initContainers , c )
2021-11-30 12:04:28 +01:00
}
o . Spec . Template . Spec . InitContainers = initContainers
2021-12-02 10:21:05 +00:00
// Then, create services for "ports" and "expose" section
2021-11-30 12:04:28 +01:00
if len ( s . Ports ) > 0 || len ( s . Expose ) > 0 {
2021-12-02 10:21:05 +00:00
for _ , s := range createService ( name , s ) {
ret <- s
}
2021-11-30 12:04:28 +01:00
}
2021-12-02 10:21:05 +00:00
// Special case, it there is no "ports", so there is no associated services...
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any
// associated service.
if len ( s . Ports ) == 0 {
locker . Lock ( )
// alert any current or **futur** waiters that this service is not exposed
go func ( ) {
for {
select {
case <- time . Tick ( 1 * time . Millisecond ) :
for _ , c := range serviceWaiters [ name ] {
c <- - 1
close ( c )
}
}
}
} ( )
locker . Unlock ( )
}
// add the volumes in Values
2021-11-30 17:29:42 +01:00
if len ( VolumeValues [ name ] ) > 0 {
2021-12-02 10:21:05 +00:00
locker . Lock ( )
2021-11-30 17:29:42 +01:00
Values [ name ] [ "persistence" ] = VolumeValues [ name ]
2021-12-02 10:21:05 +00:00
locker . Unlock ( )
2021-11-30 17:29:42 +01:00
}
2021-12-02 10:21:05 +00:00
// the deployment is ready, give it
ret <- o
2021-11-30 15:35:32 +01:00
2021-12-02 10:21:05 +00:00
// and then, we can say that it's the end
ret <- nil
2021-11-30 12:04:28 +01:00
}
2021-11-30 15:45:36 +01:00
// Create a service (k8s).
2021-12-01 08:31:51 +01:00
func createService ( name string , s compose . Service ) [ ] interface { } {
2021-11-30 12:04:28 +01:00
2021-12-01 16:50:32 +01:00
ret := make ( [ ] interface { } , 0 )
2021-12-02 10:21:05 +00:00
Magenta ( ICON_SERVICE + " Generating service for " , name )
2021-12-01 15:17:34 +01:00
ks := helm . NewService ( name )
2021-11-30 15:35:32 +01:00
2021-11-30 12:04:28 +01:00
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 ] )
}
2021-12-01 16:50:32 +01:00
ks . Spec . Ports = append ( ks . Spec . Ports , helm . NewServicePort ( target , target ) )
2021-11-30 12:04:28 +01:00
if i == 0 {
detected ( name , target )
}
}
ks . Spec . Selector = buildSelector ( name , s )
2021-12-01 08:31:51 +01:00
ret = append ( ret , ks )
2021-12-02 10:21:05 +00:00
if v , ok := s . Labels [ helm . K + "/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 )
}
Cyanf ( ICON_INGRESS + " Create an ingress for port %d on %s service\n" , port , name )
ing := createIngress ( name , port , s )
2021-12-01 08:31:51 +01:00
ret = append ( ret , ing )
2021-11-30 12:04:28 +01:00
}
2021-12-01 16:50:32 +01:00
if len ( s . Expose ) > 0 {
2021-12-02 10:21:05 +00:00
Magenta ( ICON_SERVICE + " Generating service for " , name + "-external" )
2021-12-01 16:50:32 +01:00
ks := helm . NewService ( name + "-external" )
ks . Spec . Type = "NodePort"
for _ , p := range s . Expose {
ks . Spec . Ports = append ( ks . Spec . Ports , helm . NewServicePort ( p , p ) )
}
ks . Spec . Selector = buildSelector ( name , s )
ret = append ( ret , ks )
}
2021-12-01 08:31:51 +01:00
return ret
2021-11-30 12:04:28 +01:00
}
2021-11-30 15:45:36 +01:00
// Create an ingress.
2021-12-01 08:31:51 +01:00
func createIngress ( name string , port int , s compose . Service ) * helm . Ingress {
2021-11-30 12:04:28 +01:00
ingress := helm . NewIngress ( name )
Values [ name ] [ "ingress" ] = map [ string ] interface { } {
2021-11-30 15:35:32 +01:00
"class" : "nginx" ,
2021-12-02 10:21:05 +00:00
"host" : name + "." + helm . Appname + ".tld" ,
2021-11-30 12:04:28 +01:00
"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 ,
} ,
} ,
} ,
} } ,
} ,
} ,
}
2021-11-30 15:35:32 +01:00
ingress . SetIngressClass ( name )
2021-11-30 12:04:28 +01:00
2021-12-01 08:31:51 +01:00
return ingress
2021-11-30 12:04:28 +01:00
}
2021-12-02 10:21:05 +00:00
// 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.
2021-11-30 12:04:28 +01:00
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
2021-12-02 10:21:05 +00:00
close ( c )
2021-11-30 12:04:28 +01:00
}
}
} ( )
locker . Unlock ( )
}
func getPort ( name string ) ( int , error ) {
if v , ok := servicesMap [ name ] ; ok {
return v , nil
}
return - 1 , errors . New ( "Not found" )
}
2021-11-30 15:45:36 +01:00
// Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function.
2021-11-30 12:04:28 +01:00
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
2021-12-02 10:21:05 +00:00
close ( c )
2021-11-30 12:04:28 +01:00
}
} ( )
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 }}" ,
}
}