Use compose-go + improvements (#9)
Use compose-go https://github.com/compose-spec/compose-go to make Katenary parsing compose file the official way. Add labels: - `volume-from` (with `same-pod`) to avoid volume repetition - `ignore` to ignore a service - `mapenv` (replaces the `env-to-service`) to map environment to helm variable (as a template string) - `secret-vars` declares variables as secret values More: - Now, environment (as secret vars) are set in values.yaml - Ingress has got annotations in values.yaml - Probes (liveness probe) are improved - fixed code to optimize - many others fixes about path, bad volume check, refactorisation, tests...
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/helm"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,232 +16,76 @@ const (
|
||||
|
||||
// Parser is a docker-compose parser.
|
||||
type Parser struct {
|
||||
Data *Compose
|
||||
Data *types.Project
|
||||
temporary *string
|
||||
}
|
||||
|
||||
var Appname = ""
|
||||
var (
|
||||
Appname = ""
|
||||
CURRENT_DIR, _ = os.Getwd()
|
||||
)
|
||||
|
||||
// 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 { // mainly for the tests...
|
||||
dir := filepath.Dir(filename)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dec := yaml.NewDecoder(f)
|
||||
err = dec.Decode(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
p.temporary = &dir
|
||||
ioutil.WriteFile(filename, []byte(content[0]), 0644)
|
||||
cli.DefaultFileNames = []string{filename}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dec := yaml.NewDecoder(strings.NewReader(content[0]))
|
||||
err := dec.Decode(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
// add the file at first position
|
||||
if !found {
|
||||
cli.DefaultFileNames = append([]string{filename}, cli.DefaultFileNames...)
|
||||
}
|
||||
}
|
||||
|
||||
p := &Parser{Data: c}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse using compose-go parser, adapt a bit the Project and set Appname.
|
||||
func (p *Parser) Parse(appname string) {
|
||||
Appname = appname
|
||||
|
||||
services := make(map[string][]string)
|
||||
// get the service list, to be sure that everything is ok
|
||||
// Reminder:
|
||||
// - set Appname
|
||||
// - loas services
|
||||
|
||||
// fix ugly types
|
||||
for _, s := range p.Data.Services {
|
||||
parseEnv(s)
|
||||
parseCommand(s)
|
||||
parseEnvFiles(s)
|
||||
parseHealthCheck(s)
|
||||
options, err := cli.NewProjectOptions(nil,
|
||||
cli.WithDefaultConfigPath,
|
||||
cli.WithNormalization(true),
|
||||
cli.WithInterpolation(true),
|
||||
cli.WithResolvedPaths(true),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
c := p.Data
|
||||
for name, s := range c.Services {
|
||||
if portlabel, ok := s.Labels[helm.LABEL_PORT]; 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.Ports) > 0 {
|
||||
services[name] = s.Ports
|
||||
}
|
||||
}
|
||||
|
||||
// check if dependencies are resolved
|
||||
missing := []string{}
|
||||
for name, s := range c.Services {
|
||||
for _, dep := range s.DependsOn {
|
||||
if _, ok := services[dep]; !ok {
|
||||
missing = append(missing, fmt.Sprintf(
|
||||
"The service \"%s\" hasn't got "+
|
||||
"declared port for dependency from \"%s\" - please "+
|
||||
"append a %s label or a \"ports\" section in the docker-compose file",
|
||||
dep, name, helm.LABEL_PORT),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
log.Fatal(strings.Join(missing, "\n"))
|
||||
}
|
||||
|
||||
// check if all "image" properties are set
|
||||
missing = []string{}
|
||||
for name, s := range c.Services {
|
||||
if s.Image == "" {
|
||||
missing = append(missing, fmt.Sprintf(
|
||||
"The service \"%s\" hasn't got "+
|
||||
"an image property - please "+
|
||||
"append an image property in the docker-compose file",
|
||||
name,
|
||||
))
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
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")
|
||||
|
||||
proj, err := cli.ProjectFromOptions(options)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create project", err)
|
||||
}
|
||||
|
||||
Appname = proj.Name
|
||||
p.Data = proj
|
||||
CURRENT_DIR = p.Data.WorkingDir
|
||||
}
|
||||
|
||||
// manage environment variables, if the type is map[string]string so we can use it, else we need to split "=" sign
|
||||
// and apply this in env variable
|
||||
func parseEnv(s *Service) {
|
||||
env := make(map[string]string)
|
||||
if s.RawEnvironment == nil {
|
||||
return
|
||||
}
|
||||
switch s.RawEnvironment.(type) {
|
||||
case map[string]string:
|
||||
env = s.RawEnvironment.(map[string]string)
|
||||
case map[string]interface{}:
|
||||
for k, v := range s.RawEnvironment.(map[string]interface{}) {
|
||||
// force to string
|
||||
env[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range s.RawEnvironment.([]interface{}) {
|
||||
// Splot the value of the env variable with "="
|
||||
parts := strings.Split(v.(string), "=")
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
case string:
|
||||
parts := strings.Split(s.RawEnvironment.(string), "=")
|
||||
env[parts[0]] = parts[1]
|
||||
default:
|
||||
log.Printf("%+v, %T", s.RawEnvironment, s.RawEnvironment)
|
||||
log.Fatal("Environment type not supported")
|
||||
}
|
||||
s.Environment = env
|
||||
}
|
||||
|
||||
func parseCommand(s *Service) {
|
||||
|
||||
if s.RawCommand == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// following the command type, it can be a "slice" or a simple sting, so we need to check it
|
||||
switch v := s.RawCommand.(type) {
|
||||
case string:
|
||||
// use shlex to parse the command
|
||||
command, err := shlex.Split(v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.Command = command
|
||||
case []string:
|
||||
s.Command = v
|
||||
case []interface{}:
|
||||
for _, v := range v {
|
||||
s.Command = append(s.Command, v.(string))
|
||||
}
|
||||
default:
|
||||
log.Printf("%+v %T", s.RawCommand, s.RawCommand)
|
||||
log.Fatal("Command type not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvFiles(s *Service) {
|
||||
// Same than parseEnv, but for env files
|
||||
if s.RawEnvFiles == nil {
|
||||
return
|
||||
}
|
||||
envfiles := make([]string, 0)
|
||||
switch v := s.RawEnvFiles.(type) {
|
||||
case []string:
|
||||
envfiles = v
|
||||
case []interface{}:
|
||||
for _, v := range v {
|
||||
envfiles = append(envfiles, v.(string))
|
||||
}
|
||||
case string:
|
||||
envfiles = append(envfiles, v)
|
||||
default:
|
||||
log.Printf("%+v %T", s.RawEnvFiles, s.RawEnvFiles)
|
||||
log.Fatal("EnvFile type not supported")
|
||||
}
|
||||
s.EnvFiles = envfiles
|
||||
}
|
||||
|
||||
func parseHealthCheck(s *Service) {
|
||||
// HealthCheck command can be a string or slice of strings
|
||||
if s.HealthCheck == nil {
|
||||
return
|
||||
}
|
||||
if s.HealthCheck.RawTest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch v := s.HealthCheck.RawTest.(type) {
|
||||
case string:
|
||||
c, err := shlex.Split(v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.HealthCheck = &HealthCheck{
|
||||
Test: c,
|
||||
}
|
||||
|
||||
case []string:
|
||||
s.HealthCheck = &HealthCheck{
|
||||
Test: v,
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
for _, v := range v {
|
||||
s.HealthCheck.Test = append(s.HealthCheck.Test, v.(string))
|
||||
}
|
||||
default:
|
||||
log.Printf("%+v %T", s.HealthCheck.RawTest, s.HealthCheck.RawTest)
|
||||
log.Fatal("HealthCheck type not supported")
|
||||
}
|
||||
func GetCurrentDir() string {
|
||||
return CURRENT_DIR
|
||||
}
|
||||
|
@@ -1,199 +0,0 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"katenary/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const DOCKER_COMPOSE_YML1 = `
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
# first service, very basic
|
||||
web:
|
||||
image: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
FOO: bar
|
||||
BAZ: qux
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
|
||||
database:
|
||||
image: postgres
|
||||
networks:
|
||||
- frontend
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: mysecretpassword
|
||||
POSTGRES_DB: mydb
|
||||
labels:
|
||||
katenary.io/ports: "5432"
|
||||
|
||||
commander1:
|
||||
image: foo
|
||||
command: ["/bin/sh", "-c", "echo 'hello world'"]
|
||||
|
||||
commander2:
|
||||
image: foo
|
||||
command: echo "hello world"
|
||||
|
||||
hc1:
|
||||
image: foo
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "echo 'hello world1'"]
|
||||
|
||||
hc2:
|
||||
image: foo
|
||||
healthcheck:
|
||||
test: echo "hello world2"
|
||||
|
||||
hc3:
|
||||
image: foo
|
||||
healthcheck:
|
||||
test: ["CMD", "echo 'hello world3'"]
|
||||
|
||||
|
||||
`
|
||||
|
||||
func init() {
|
||||
logger.NOLOG = true
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
p := NewParser("", DOCKER_COMPOSE_YML1)
|
||||
p.Parse("test")
|
||||
|
||||
// check if the "web" and "database" service is parsed correctly
|
||||
// by checking if the "ports" and "environment"
|
||||
for name, service := range p.Data.Services {
|
||||
if name == "web" {
|
||||
if len(service.Ports) != 1 {
|
||||
t.Errorf("Expected 1 port, got %d", len(service.Ports))
|
||||
}
|
||||
if service.Ports[0] != "80:80" {
|
||||
t.Errorf("Expected port 80:80, got %s", service.Ports[0])
|
||||
}
|
||||
if len(service.Environment) != 2 {
|
||||
t.Errorf("Expected 2 environment variables, got %d", len(service.Environment))
|
||||
}
|
||||
if service.Environment["FOO"] != "bar" {
|
||||
t.Errorf("Expected FOO=bar, got %s", service.Environment["FOO"])
|
||||
}
|
||||
if service.Environment["BAZ"] != "qux" {
|
||||
t.Errorf("Expected BAZ=qux, got %s", service.Environment["BAZ"])
|
||||
}
|
||||
}
|
||||
// same for the "database" service
|
||||
if name == "database" {
|
||||
if len(service.Ports) != 1 {
|
||||
t.Errorf("Expected 1 port, got %d", len(service.Ports))
|
||||
}
|
||||
if service.Ports[0] != "5432" {
|
||||
t.Errorf("Expected port 5432, got %s", service.Ports[0])
|
||||
}
|
||||
if len(service.Environment) != 3 {
|
||||
t.Errorf("Expected 3 environment variables, got %d", len(service.Environment))
|
||||
}
|
||||
if service.Environment["POSTGRES_USER"] != "postgres" {
|
||||
t.Errorf("Expected POSTGRES_USER=postgres, got %s", service.Environment["POSTGRES_USER"])
|
||||
}
|
||||
if service.Environment["POSTGRES_PASSWORD"] != "mysecretpassword" {
|
||||
t.Errorf("Expected POSTGRES_PASSWORD=mysecretpassword, got %s", service.Environment["POSTGRES_PASSWORD"])
|
||||
}
|
||||
if service.Environment["POSTGRES_DB"] != "mydb" {
|
||||
t.Errorf("Expected POSTGRES_DB=mydb, got %s", service.Environment["POSTGRES_DB"])
|
||||
}
|
||||
// check labels
|
||||
if len(service.Labels) != 1 {
|
||||
t.Errorf("Expected 1 label, got %d", len(service.Labels))
|
||||
}
|
||||
// is label katenary.io/ports correct?
|
||||
if service.Labels["katenary.io/ports"] != "5432" {
|
||||
t.Errorf("Expected katenary.io/ports=5432, got %s", service.Labels["katenary.io/ports"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommand(t *testing.T) {
|
||||
p := NewParser("", DOCKER_COMPOSE_YML1)
|
||||
p.Parse("test")
|
||||
|
||||
for name, s := range p.Data.Services {
|
||||
if name == "commander1" {
|
||||
t.Log(s.Command)
|
||||
if len(s.Command) != 3 {
|
||||
t.Errorf("Expected 3 command, got %d", len(s.Command))
|
||||
}
|
||||
if s.Command[0] != "/bin/sh" {
|
||||
t.Errorf("Expected /bin/sh, got %s", s.Command[0])
|
||||
}
|
||||
if s.Command[1] != "-c" {
|
||||
t.Errorf("Expected -c, got %s", s.Command[1])
|
||||
}
|
||||
if s.Command[2] != "echo 'hello world'" {
|
||||
t.Errorf("Expected echo 'hello world', got %s", s.Command[2])
|
||||
}
|
||||
}
|
||||
if name == "commander2" {
|
||||
t.Log(s.Command)
|
||||
if len(s.Command) != 2 {
|
||||
t.Errorf("Expected 1 command, got %d", len(s.Command))
|
||||
}
|
||||
if s.Command[0] != "echo" {
|
||||
t.Errorf("Expected echo, got %s", s.Command[0])
|
||||
}
|
||||
if s.Command[1] != "hello world" {
|
||||
t.Errorf("Expected hello world, got %s", s.Command[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthChecks(t *testing.T) {
|
||||
p := NewParser("", DOCKER_COMPOSE_YML1)
|
||||
p.Parse("test")
|
||||
|
||||
for name, s := range p.Data.Services {
|
||||
if name != "hc1" && name != "hc2" && name != "hc3" {
|
||||
continue
|
||||
}
|
||||
|
||||
if name == "hc1" {
|
||||
if len(s.HealthCheck.Test) != 2 {
|
||||
t.Errorf("Expected 2 healthcheck tests, got %d", len(s.HealthCheck.Test))
|
||||
}
|
||||
if s.HealthCheck.Test[0] != "CMD-SHELL" {
|
||||
t.Errorf("Expected CMD-SHELL, got %s", s.HealthCheck.Test[0])
|
||||
}
|
||||
if s.HealthCheck.Test[1] != "echo 'hello world1'" {
|
||||
t.Errorf("Expected echo 'hello world1', got %s", s.HealthCheck.Test[1])
|
||||
}
|
||||
}
|
||||
if name == "hc2" {
|
||||
if len(s.HealthCheck.Test) != 2 {
|
||||
t.Errorf("Expected 2 healthcheck tests, got %d", len(s.HealthCheck.Test))
|
||||
}
|
||||
if s.HealthCheck.Test[0] != "echo" {
|
||||
t.Errorf("Expected echo, got %s", s.HealthCheck.Test[1])
|
||||
}
|
||||
if s.HealthCheck.Test[1] != "hello world2" {
|
||||
t.Errorf("Expected echo 'hello world2', got %s", s.HealthCheck.Test[1])
|
||||
}
|
||||
}
|
||||
if name == "hc3" {
|
||||
if len(s.HealthCheck.Test) != 2 {
|
||||
t.Errorf("Expected 2 healthcheck tests, got %d", len(s.HealthCheck.Test))
|
||||
}
|
||||
if s.HealthCheck.Test[0] != "CMD" {
|
||||
t.Errorf("Expected CMD, got %s", s.HealthCheck.Test[0])
|
||||
}
|
||||
if s.HealthCheck.Test[1] != "echo 'hello world3'" {
|
||||
t.Errorf("Expected echo 'hello world3', got %s", s.HealthCheck.Test[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"`
|
||||
}
|
Reference in New Issue
Block a user