diff --git a/cmd/utils.go b/cmd/utils.go index 308eeed..6eaeda2 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -97,7 +97,7 @@ func Convert(composeFile, appVersion, appName, chartDir string, force bool) { fmt.Println("No compose file given") return } - _, err := os.Stat(ComposeFile) + _, err := os.Stat(composeFile) if err != nil { fmt.Println("No compose file found") os.Exit(1) diff --git a/compose/parser.go b/compose/parser.go index 294fa1e..7538d7f 100644 --- a/compose/parser.go +++ b/compose/parser.go @@ -3,11 +3,10 @@ package compose import ( "log" "os" - "strings" + "path/filepath" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" - "gopkg.in/yaml.v3" ) const ( @@ -16,7 +15,8 @@ const ( // Parser is a docker-compose parser. type Parser struct { - Data *types.Project + Data *types.Project + temporary *string } var Appname = "" @@ -24,30 +24,44 @@ var Appname = "" // NewParser create a Parser and parse the file given in filename. If filename is empty, we try to parse the content[0] argument that should be a valid YAML content. func NewParser(filename string, content ...string) *Parser { - c := NewCompose() - if filename != "" { - f, err := os.Open(filename) + p := &Parser{} + + if len(content) > 0 { + //write it in a temporary file + tmp, err := os.MkdirTemp(os.TempDir(), "katenary-") if err != nil { log.Fatal(err) } - dec := yaml.NewDecoder(f) - err = dec.Decode(c) + tmpfile, err := os.Create(filepath.Join(tmp, "tmp.yml")) if err != nil { log.Fatal(err) } - } else { - dec := yaml.NewDecoder(strings.NewReader(content[0])) - err := dec.Decode(c) - if err != nil { - log.Fatal(err) + tmpfile.WriteString(content[0]) + tmpfile.Close() + filename = tmpfile.Name() + p.temporary = &tmp + cli.DefaultFileNames = append([]string{filename}, cli.DefaultFileNames...) + } + // if filename is not in cli Default files, add it + if len(filename) > 0 { + found := false + for _, f := range cli.DefaultFileNames { + if f == filename { + found = true + break + } + } + // add the file at first position + if !found { + cli.DefaultFileNames = append([]string{filename}, cli.DefaultFileNames...) } } - - p := &Parser{} + log.Println(cli.DefaultFileNames) return p } +// Parse using compose-go parser, adapt a bit the Project and set Appname. func (p *Parser) Parse(appname string) { // Reminder: @@ -66,11 +80,12 @@ func (p *Parser) Parse(appname string) { proj, err := cli.ProjectFromOptions(options) if err != nil { - log.Fatal(err) + log.Fatal("Failed to create project", err) } Appname = proj.Name - p.Data = proj - + if p.temporary != nil { + defer os.RemoveAll(*p.temporary) + } } diff --git a/compose/types.go b/compose/types.go deleted file mode 100644 index f9c6238..0000000 --- a/compose/types.go +++ /dev/null @@ -1,44 +0,0 @@ -package compose - -// Compose is a complete docker-compse representation. -type Compose struct { - Version string `yaml:"version"` - Services map[string]*Service `yaml:"services"` - Volumes map[string]interface{} `yaml:"volumes"` -} - -// NewCompose resturs a Compose object. -func NewCompose() *Compose { - c := &Compose{} - c.Services = make(map[string]*Service) - c.Volumes = make(map[string]interface{}) - return c -} - -// HealthCheck manage generic type to handle TCP, HTTP and TCP health check. -type HealthCheck struct { - Test []string `yaml:"-"` - RawTest interface{} `yaml:"test"` - Interval string `yaml:"interval"` - Timeout string `yaml:"timeout"` - Retries int `yaml:"retries"` - StartPeriod string `yaml:"start_period"` -} - -// Service represent a "service" in a docker-compose file. -type Service struct { - Image string `yaml:"image"` - Ports []string `yaml:"ports"` - Environment map[string]string `yaml:"-"` - RawEnvironment interface{} `yaml:"environment"` - Labels map[string]string `yaml:"labels"` - DependsOn []string `yaml:"depends_on"` - Volumes []string `yaml:"volumes"` - Expose []int `yaml:"expose"` - EnvFiles []string `yaml:"-"` - RawEnvFiles interface{} `yaml:"env_file"` - RawBuild interface{} `yaml:"build"` - HealthCheck *HealthCheck `yaml:"healthcheck"` - Command []string `yaml:"-"` - RawCommand interface{} `yaml:"command"` -} diff --git a/generator/main.go b/generator/main.go index 14c56df..a013df6 100644 --- a/generator/main.go +++ b/generator/main.go @@ -11,9 +11,6 @@ import ( "strconv" "strings" "sync" - "time" - - "errors" "github.com/compose-spec/compose-go/types" ) @@ -123,33 +120,6 @@ func parseService(name string, s types.ServiceConfig, linked map[string]types.Se } } - // 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 { - // alert any current or **future** waiters that this service is not exposed - go func() { - defer func() { - // recover from panic - if r := recover(); r != nil { - // log the stack trace - fmt.Println(r) - } - }() - for { - select { - case <-time.Tick(1 * time.Millisecond): - locker.Lock() - for _, c := range serviceWaiters[name] { - c <- -1 - close(c) - } - locker.Unlock() - } - } - }() - } - // add the volumes in Values if len(VolumeValues[name]) > 0 { locker.Lock() @@ -166,6 +136,8 @@ func parseService(name string, s types.ServiceConfig, linked map[string]types.Se // prepareContainer assigns image, command, env, and labels to a container. func prepareContainer(container *helm.Container, service types.ServiceConfig, servicename string) { + locker.Lock() + defer locker.Unlock() // if there is no image name, this should fail! if service.Image == "" { log.Fatal(ICON_PACKAGE+" No image name for service ", servicename) @@ -186,11 +158,8 @@ func generateServicesAndIngresses(name string, s types.ServiceConfig) []interfac logger.Magenta(ICON_SERVICE+" Generating service for ", name) ks := helm.NewService(name) - for i, p := range s.Ports { + for _, p := range s.Ports { ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(int(p.Target), int(p.Target))) - if i == 0 { - detected(name, int(p.Target)) - } } ks.Spec.Selector = buildSelector(name, s) @@ -253,49 +222,6 @@ func createIngress(name string, port int, s types.ServiceConfig) *helm.Ingress { 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() - defer locker.Unlock() - if _, ok := servicesMap[name]; ok { - return - } - servicesMap[name] = port - go func() { - locker.Lock() - defer locker.Unlock() - if cx, ok := serviceWaiters[name]; ok { - for _, c := range cx { - c <- port - } - } - }() -} - -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() - defer locker.Unlock() - c := make(chan int, 0) - serviceWaiters[name] = append(serviceWaiters[name], c) - go func() { - locker.Lock() - defer locker.Unlock() - if v, ok := servicesMap[name]; ok { - c <- v - } - }() - return c -} - // Build the selector for the service. func buildSelector(name string, s types.ServiceConfig) map[string]string { return map[string]string{ @@ -355,6 +281,20 @@ func generateContainerPorts(s types.ServiceConfig, name string, container *helm. exists[int(port.Target)] = name } + // Get ports from label + if v, ok := s.Labels[helm.LABEL_PORT]; ok { + // split port by "," + ports := strings.Split(v, ",") + for _, port := range ports { + port, _ := strconv.Atoi(port) + container.Ports = append(container.Ports, &helm.ContainerPort{ + Name: name, + ContainerPort: port, + }) + exists[port] = name + } + } + // manage the "expose" section to be a NodePort in Kubernetes for _, expose := range s.Expose { @@ -506,9 +446,11 @@ func prepareInitContainers(name string, s types.ServiceConfig, container *helm.C command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp) foundPort := -1 - if defaultPort, err := getPort(dp); err != nil { - // BUG: Sometimes the chan remains opened - foundPort = <-waitPort(dp) + locker.Lock() + defer locker.Unlock() + if defaultPort, ok := servicesMap[dp]; !ok { + logger.Redf("Error while getting port for service %s\n", dp) + os.Exit(1) } else { foundPort = defaultPort } diff --git a/generator/main_test.go b/generator/main_test.go index dcd4bfc..df43dd0 100644 --- a/generator/main_test.go +++ b/generator/main_test.go @@ -5,10 +5,13 @@ import ( "katenary/compose" "katenary/helm" "katenary/logger" + "log" "os" "path/filepath" "strings" "testing" + + "github.com/compose-spec/compose-go/cli" ) const DOCKER_COMPOSE_YML = `version: '3' @@ -92,17 +95,19 @@ volumes: driver: local ` +var defaultCliFiles = cli.DefaultFileNames + func init() { logger.NOLOG = true } func setUp(t *testing.T) (string, *compose.Parser) { + cli.DefaultFileNames = defaultCliFiles p := compose.NewParser("", DOCKER_COMPOSE_YML) p.Parse("testapp") // create a temporary directory tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test") - t.Log("Generated ", tmp, "directory") if err != nil { t.Fatal(err) } @@ -115,7 +120,8 @@ func setUp(t *testing.T) (string, *compose.Parser) { // Check if the web2 service has got a command. func TestCommand(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + //defer os.RemoveAll(tmp) + log.Println(tmp) for _, service := range p.Data.Services { name := service.Name @@ -138,6 +144,7 @@ func TestCommand(t *testing.T) { commands = append(commands, line) } } + //log.Printf("%#v\n", commands) ok := 0 for _, command := range commands { if strings.Contains(command, "- /bin/sh") { @@ -242,7 +249,7 @@ func TestPorts(t *testing.T) { t.Log("Checking ", name, " service file") _, err := os.Stat(path) if err != nil { - t.Fatal(err) + t.Error(err) } } } diff --git a/generator/writer.go b/generator/writer.go index c0e40e5..55a0425 100644 --- a/generator/writer.go +++ b/generator/writer.go @@ -4,6 +4,7 @@ import ( "katenary/compose" "katenary/generator/writers" "katenary/helm" + "log" "os" "path/filepath" "regexp" @@ -26,33 +27,38 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFi // try to create the directory err := os.MkdirAll(templatesDir, 0755) if err != nil { - panic(err) + log.Fatal(err) } files := make(map[string]chan interface{}) - // list avoided services + // Manage services, avoid linked pods and store all services port in servicesMap avoids := make(map[string]bool) + linked := make(map[string]types.ServiceConfig, 0) for _, service := range p.Data.Services { n := service.Name + + // find port and store it in servicesMap + for _, port := range service.Ports { + target := int(port.Target) + if target != 0 { + servicesMap[n] = target + } + } + + // avoid linked pods if _, ok := service.Labels[helm.LABEL_SAMEPOD]; ok { avoids[n] = true } - } - for _, s := range p.Data.Services { - name := s.Name - - // Manage emptyDir volumes - if empty, ok := s.Labels[helm.LABEL_EMPTYDIRS]; ok { + // manage emptyDir volumes + if empty, ok := service.Labels[helm.LABEL_EMPTYDIRS]; ok { //split empty list by coma emptyDirs := strings.Split(empty, ",") //append them in EmptyDirs EmptyDirs = append(EmptyDirs, emptyDirs...) } - // fetch corresponding service in "links" - linked := make(map[string]types.ServiceConfig, 0) // find service linked to this one for _, service := range p.Data.Services { n := service.Name @@ -62,6 +68,11 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFi } } } + } + + // for all services in linked map, and not in avoids map, generate the service + for _, s := range p.Data.Services { + name := s.Name if _, found := avoids[name]; found { continue