From 418a0a80294e549f878d98a1d9f1aa1ff7080229 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 8 May 2022 09:55:25 +0200 Subject: [PATCH] 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... --- .gitignore | 2 +- Makefile | 6 +- README.md | 17 +- cmd/{ => katenary}/main.go | 5 +- cmd/{ => katenary}/utils.go | 9 +- compose/parser.go | 257 ++---- compose/parser_test.go | 199 ----- compose/types.go | 44 - examples/basic/README.md | 2 +- examples/basic/docker-compose.yaml | 6 +- examples/ghost/README.md | 9 + examples/ghost/chart/ghost/Chart.yaml | 8 + .../ghost/chart/ghost/templates/NOTES.txt | 8 + .../ghost/templates/blog.deployment.yaml | 33 + .../chart/ghost/templates/blog.ingress.yaml | 42 + .../chart/ghost/templates/blog.service.yaml | 19 + examples/ghost/chart/ghost/values.yaml | 6 + examples/ghost/docker-compose.yaml | 30 + generator/main.go | 775 +++++++++++------- generator/main_test.go | 123 ++- generator/utils.go | 22 + generator/writer.go | 175 ++-- generator/writers/configmap.go | 3 +- generator/writers/deployment.go | 1 + generator/writers/ingress.go | 51 +- generator/writers/service.go | 1 + generator/writers/storage.go | 12 +- generator/writers/utils.go | 2 +- go.mod | 6 +- go.sum | 274 ++++++- helm/{configMap.go => configAndSecretMap.go} | 35 +- helm/container.go | 65 ++ helm/deployment.go | 84 +- helm/ingress.go | 3 +- helm/k8sbase.go | 73 ++ helm/labels.go | 61 ++ helm/notes.go | 1 + helm/probe.go | 104 +++ helm/service.go | 8 +- helm/storage.go | 26 +- helm/types.go | 102 +-- 41 files changed, 1688 insertions(+), 1021 deletions(-) rename cmd/{ => katenary}/main.go (95%) rename cmd/{ => katenary}/utils.go (90%) delete mode 100644 compose/parser_test.go delete mode 100644 compose/types.go create mode 100644 examples/ghost/README.md create mode 100644 examples/ghost/chart/ghost/Chart.yaml create mode 100644 examples/ghost/chart/ghost/templates/NOTES.txt create mode 100644 examples/ghost/chart/ghost/templates/blog.deployment.yaml create mode 100644 examples/ghost/chart/ghost/templates/blog.ingress.yaml create mode 100644 examples/ghost/chart/ghost/templates/blog.service.yaml create mode 100644 examples/ghost/chart/ghost/values.yaml create mode 100644 examples/ghost/docker-compose.yaml create mode 100644 generator/utils.go rename helm/{configMap.go => configAndSecretMap.go} (64%) create mode 100644 helm/container.go create mode 100644 helm/k8sbase.go create mode 100644 helm/labels.go create mode 100644 helm/probe.go diff --git a/.gitignore b/.gitignore index 7d31128..22c7e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ dist/* .cache/* chart/* docker-compose.yaml -katenary +./katenary *.env docker-compose* !examples/**/docker-compose* diff --git a/Makefile b/Makefile index bf0a0f5..5b9903f 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PREFIX=~/.local GO=container OUT=katenary -BLD_CMD=go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(OUT) ./cmd/*.go +BLD_CMD=go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(OUT) ./cmd/katenary/*.go GOOS=linux GOARCH=amd64 @@ -106,10 +106,10 @@ ifeq ($(GO),local) $(BLD_CMD) else ifeq ($(CTN),podman) @podman run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \ - --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it docker.io/golang $(BLD_CMD) + --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it $(BUILD_IMAGE) $(BLD_CMD) else @docker run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \ - --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it docker.io/golang $(BLD_CMD) + --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it $(BUILD_IMAGE) $(BLD_CMD) endif echo "=> Stripping if possible" strip $(OUT) 2>/dev/null || echo "=> No strip available" diff --git a/README.md b/README.md index 1682cfb..d7472ed 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This project is partially made at [Smile](https://www.smile.eu) You can download the binaries from the [Release](https://github.com/metal3d/katenary/releases) section. Copy the binary and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command. +You can of course get the binary with `go install -u github.com/metal3d/katenary/cmd/katenary/...` but the `main` branch is continuously updated. It's preferable to use releases. You can use this commands on Linux: @@ -125,7 +126,7 @@ What can be interpreted by Katenary: - `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now) - some labels can help to bind values, for example: - `katenary.io/ingress: 80` will expose the port 80 in a ingress - - `katenary.io/env-to-service: VARNAME` will convert the value to a variable `{{ .Release.Name }}-VARNAME` - it's usefull when you want to pass the name of a service as a variable (think about the service name for mysql to pass to a container that wants to connect to this) + - `katenary.io/mapenv: |`: allow to map environment to something else than the given value in the compose file Exemple of a possible `docker-compose.yaml` file: @@ -144,21 +145,27 @@ services: # because it's the "exposed" port - database labels: - # explain to katenary that "DB_HOST" value is variable (using release name) - katenary.io/env-to-service: DB_HOST # expose the port 80 as an ingress katenary.io/ingress: 80 + # make adaptations, DB_HOST environment is actually the service name + # to hit (note the yaml style, start with "|") + katenary.io/mapenv: | + DB_HOST: {{ .Release.Name }}-database database: image: mariadb:10 env_file: # this will create a configMap - my_env.env environment: + MARIADB_USER: foo MARIADB_ROOT_PASSWORD: foobar + MARIADB_PASSWORD: bar labels: # no need to declare this port in docker-compose # but katenary will need it katenary.io/ports: 3306 + # these variables are secrets + katenary.io/secret-vars: MARIADB_ROOT_PASSWORD, MARIADB_PASSWORD ``` # Labels @@ -166,10 +173,12 @@ services: These labels could be found by `katenary show-labels`, and can be placed as "labels" inside your docker-compose file: ``` +katenary.io/ignore : ignore the container, it will not yied any object in the helm chart +katenary.io/secret-vars : secret variables to push on a secret file katenary.io/secret-envfiles : set the given file names as a secret instead of configmap +katenary.io/mapenv : map environment variable to a template string (yaml style) katenary.io/ports : set the ports to expose as a service (coma separated) katenary.io/ingress : set the port to expose in an ingress (coma separated) -katenary.io/env-to-service : specifies that the environment variable points on a service name (coma separated) katenary.io/configmap-volumes : specifies that the volumes points on a configmap (coma separated) katenary.io/same-pod : specifies that the pod should be deployed in the same pod than the given service name katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated) diff --git a/cmd/main.go b/cmd/katenary/main.go similarity index 95% rename from cmd/main.go rename to cmd/katenary/main.go index 1b0df68..712f057 100644 --- a/cmd/main.go +++ b/cmd/katenary/main.go @@ -65,18 +65,21 @@ func main() { appversion := c.Flag("app-version").Value.String() composeFile := c.Flag("compose-file").Value.String() appName := c.Flag("app-name").Value.String() + chartVersion := c.Flag("chart-version").Value.String() chartDir := c.Flag("output-dir").Value.String() indentation, err := strconv.Atoi(c.Flag("indent-size").Value.String()) if err != nil { writers.IndentSize = indentation } - Convert(composeFile, appversion, appName, chartDir, force) + Convert(composeFile, appversion, appName, chartDir, chartVersion, force) }, } convertCmd.Flags().BoolP( "force", "f", false, "force overwrite of existing output files") convertCmd.Flags().StringP( "app-version", "a", AppVersion, "app version") + convertCmd.Flags().StringP( + "chart-version", "v", ChartVersion, "chart version") convertCmd.Flags().StringP( "compose-file", "c", ComposeFile, "docker compose file") convertCmd.Flags().StringP( diff --git a/cmd/utils.go b/cmd/katenary/utils.go similarity index 90% rename from cmd/utils.go rename to cmd/katenary/utils.go index 308eeed..bc0d8b4 100644 --- a/cmd/utils.go +++ b/cmd/katenary/utils.go @@ -12,11 +12,12 @@ import ( ) var ( - composeFiles = []string{"docker-compose.yaml", "docker-compose.yml"} + composeFiles = []string{"compose.yml", "compose.yaml", "docker-compose.yaml", "docker-compose.yml"} ComposeFile = "" AppName = "MyApp" ChartsDir = "chart" AppVersion = "0.0.1" + ChartVersion = "0.1.0" ) func init() { @@ -92,12 +93,12 @@ func detectGitVersion() (string, error) { return defaulVersion, errors.New("git log failed") } -func Convert(composeFile, appVersion, appName, chartDir string, force bool) { +func Convert(composeFile, appVersion, appName, chartDir, chartVersion string, force bool) { if len(composeFile) == 0 { 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) @@ -138,6 +139,6 @@ func Convert(composeFile, appVersion, appName, chartDir string, force bool) { } // start generator - generator.Generate(p, Version, appName, appVersion, ComposeFile, dirname) + generator.Generate(p, Version, appName, appVersion, chartVersion, ComposeFile, dirname) } diff --git a/compose/parser.go b/compose/parser.go index 03d68d5..86b0cb5 100644 --- a/compose/parser.go +++ b/compose/parser.go @@ -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 - - // fix ugly types - for _, s := range p.Data.Services { - parseEnv(s) - parseCommand(s) - parseEnvFiles(s) - parseHealthCheck(s) - } - - 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") - } + // Reminder: + // - set Appname + // - loas services -} + options, err := cli.NewProjectOptions(nil, + cli.WithDefaultConfigPath, + cli.WithNormalization(true), + cli.WithInterpolation(true), + cli.WithResolvedPaths(true), + ) -// 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") + if err != nil { + log.Fatal(err) } - s.Environment = env -} -func parseCommand(s *Service) { - - if s.RawCommand == nil { - return + proj, err := cli.ProjectFromOptions(options) + if err != nil { + log.Fatal("Failed to create project", err) } - // 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") - } + Appname = proj.Name + p.Data = proj + CURRENT_DIR = p.Data.WorkingDir } -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 } diff --git a/compose/parser_test.go b/compose/parser_test.go deleted file mode 100644 index 904b00a..0000000 --- a/compose/parser_test.go +++ /dev/null @@ -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]) - } - } - } -} 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/examples/basic/README.md b/examples/basic/README.md index 893b7ee..1b3974a 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -5,6 +5,6 @@ This is a basic example of what can do Katenary with standard docker-compose fil In this example: - `depends_on` yield a `initContainer` in the webapp ddeployment to wait for database -- so we need to declare the listened port inside `database` container as we don't use it with docker-compose- also, we needed to declare that `DB_HOST` is actually a service name +- so we need to declare the listened port inside `database` container as we don't use it with docker-compose- also, we needed to declare that `DB_HOST` is actually a service name using `mapenv` label Take a look on [chart/basic](chart/basic) directory to see what `katenary convert` command has generated. diff --git a/examples/basic/docker-compose.yaml b/examples/basic/docker-compose.yaml index ce9dd35..3e9b3fc 100644 --- a/examples/basic/docker-compose.yaml +++ b/examples/basic/docker-compose.yaml @@ -1,7 +1,8 @@ version: "3" +# this example is absolutely not working, it's an example to see how it is converted +# by Katenary services: - webapp: image: php:7-apache environment: @@ -12,7 +13,8 @@ services: # expose an ingress katenary.io/ingress: 80 # DB_HOST is actually a service name - katenary.io/env-to-service: DB_HOST + katenary.io/mapenv: | + DB_HOST: {{ .Release.Name }}-database depends_on: - database diff --git a/examples/ghost/README.md b/examples/ghost/README.md new file mode 100644 index 0000000..58135d4 --- /dev/null +++ b/examples/ghost/README.md @@ -0,0 +1,9 @@ +# Example with Ghost + +[Ghost](https://ghost.org/) is a simple but powerfull blog engine. It is very nice to test some behaviors with Docker or Podman. + +The given `docker-compose.yaml` file here declares a stand-alone blog service. To help using it, we use [Patwae](https://pathwae.net) reverse-proxy to listend http://ghost.example.localhost + +The problem to solve is that the `url` environment variable correspond to the Ingress host when we will convert it to Helm Chart. So, we use the `mapenv` label to declare that `url` is actually `{{ .Values.blog.ingress.host }}` value. + +Note that we also `ignore` pathwae because we don't need it in our Helm Chart. diff --git a/examples/ghost/chart/ghost/Chart.yaml b/examples/ghost/chart/ghost/Chart.yaml new file mode 100644 index 0000000..f4732b0 --- /dev/null +++ b/examples/ghost/chart/ghost/Chart.yaml @@ -0,0 +1,8 @@ +# Create on 2022-05-05T14:16:27+02:00 +# Katenary command line: /tmp/go-build669507924/b001/exe/main convert +apiVersion: v2 +appVersion: 0.0.1 +description: A helm chart for ghost +name: ghost +type: application +version: 0.1.0 diff --git a/examples/ghost/chart/ghost/templates/NOTES.txt b/examples/ghost/chart/ghost/templates/NOTES.txt new file mode 100644 index 0000000..10ce5b3 --- /dev/null +++ b/examples/ghost/chart/ghost/templates/NOTES.txt @@ -0,0 +1,8 @@ + +Congratulations, + +Your application is now deployed. This may take a while to be up and responding. + +{{ if .Values.blog.ingress.enabled -}} +- blog is accessible on : http://{{ .Values.blog.ingress.host }} +{{- end }} diff --git a/examples/ghost/chart/ghost/templates/blog.deployment.yaml b/examples/ghost/chart/ghost/templates/blog.deployment.yaml new file mode 100644 index 0000000..6378e0d --- /dev/null +++ b/examples/ghost/chart/ghost/templates/blog.deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: '{{ .Release.Name }}-blog' + labels: + katenary.io/component: blog + katenary.io/project: ghost + katenary.io/release: '{{ .Release.Name }}' + annotations: + katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 + katenary.io/version: master +spec: + replicas: 1 + selector: + matchLabels: + katenary.io/component: blog + katenary.io/release: '{{ .Release.Name }}' + template: + metadata: + labels: + katenary.io/component: blog + katenary.io/release: '{{ .Release.Name }}' + spec: + containers: + - name: blog + image: '{{ .Values.blog.image }}' + ports: + - name: blog + containerPort: 2368 + env: + - name: url + value: http://{{ .Values.blog.ingress.host }} + diff --git a/examples/ghost/chart/ghost/templates/blog.ingress.yaml b/examples/ghost/chart/ghost/templates/blog.ingress.yaml new file mode 100644 index 0000000..43c804d --- /dev/null +++ b/examples/ghost/chart/ghost/templates/blog.ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.blog.ingress.enabled -}} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: '{{ .Release.Name }}-blog' + labels: + katenary.io/component: blog + katenary.io/project: ghost + katenary.io/release: '{{ .Release.Name }}' + annotations: + katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 + katenary.io/version: master +spec: + {{- if and .Values.blog.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: '{{ .Values.blog.ingress.class }}' + {{- end }} + rules: + - host: '{{ .Values.blog.ingress.host }}' + http: + paths: + - path: / + {{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }} + pathType: Prefix + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} + service: + name: '{{ .Release.Name }}-blog' + port: + number: 2368 + {{- else }} + serviceName: '{{ .Release.Name }}-blog' + servicePort: 2368 + {{- end }} + +{{- end -}} diff --git a/examples/ghost/chart/ghost/templates/blog.service.yaml b/examples/ghost/chart/ghost/templates/blog.service.yaml new file mode 100644 index 0000000..5c54299 --- /dev/null +++ b/examples/ghost/chart/ghost/templates/blog.service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: '{{ .Release.Name }}-blog' + labels: + katenary.io/component: blog + katenary.io/project: ghost + katenary.io/release: '{{ .Release.Name }}' + annotations: + katenary.io/docker-compose-sha1: 0c2bbf548ff569c3dc5d77dc158e98bbe86fb5d4 + katenary.io/version: master +spec: + selector: + katenary.io/component: blog + katenary.io/release: '{{ .Release.Name }}' + ports: + - protocol: TCP + port: 2368 + targetPort: 2368 diff --git a/examples/ghost/chart/ghost/values.yaml b/examples/ghost/chart/ghost/values.yaml new file mode 100644 index 0000000..6ef57af --- /dev/null +++ b/examples/ghost/chart/ghost/values.yaml @@ -0,0 +1,6 @@ +blog: + image: ghost + ingress: + class: nginx + enabled: false + host: blog.ghost.tld diff --git a/examples/ghost/docker-compose.yaml b/examples/ghost/docker-compose.yaml new file mode 100644 index 0000000..67472f7 --- /dev/null +++ b/examples/ghost/docker-compose.yaml @@ -0,0 +1,30 @@ +version: "3" + +services: + blog: + image: ghost + environment: + # this is OK for local test, but not with Helm + # because the URL depends on Ingress + url: http://ghost.example.localhost + labels: + katenary.io/ports: 2368 + katenary.io/ingress: 2368 + # ... so we declare that "url" is actually + # the ingress host + katenary.io/mapenv: | + url: http://{{ .Values.blog.ingress.host }} + + proxy: + # A simple proxy for localhost + image: quay.io/pathwae/proxy + environment: + CONFIG: | + ghost.example.localhost: + to: http://blog:2368 + ports: + - 80:80 + labels: + # we don't want this in Helm because we will use + # an ingress + katenary.io/ignore: true diff --git a/generator/main.go b/generator/main.go index e23bd47..4d2a252 100644 --- a/generator/main.go +++ b/generator/main.go @@ -10,19 +10,16 @@ import ( "net/url" "os" "path/filepath" + "runtime" "strconv" "strings" "sync" - "time" - "errors" - - "github.com/google/shlex" + "github.com/compose-spec/compose-go/types" + "gopkg.in/yaml.v3" ) -var servicesMap = make(map[string]int) -var serviceWaiters = make(map[string][]chan int) -var locker = &sync.Mutex{} +type EnvVal = helm.EnvValue const ( ICON_PACKAGE = "📦" @@ -33,71 +30,57 @@ const ( ICON_INGRESS = "🌐" ) -const ( - RELEASE_NAME = helm.RELEASE_NAME -) - // 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 EmptyDirs = []string{} - -var dependScript = ` +var ( + Values = make(map[string]map[string]interface{}) + VolumeValues = make(map[string]map[string]map[string]EnvVal) + 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 ` + RELEASE_NAME + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1 + nc -z ` + helm.ReleaseNameTpl + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1 done echo echo "Done" ` -var madeDeployments = make(map[string]helm.Deployment, 0) + madeDeployments = make(map[string]helm.Deployment, 0) +) -// 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, linked map[string]*compose.Service) chan interface{} { - ret := make(chan interface{}, len(s.Ports)+len(s.Expose)+1) - go parseService(name, s, linked, ret) +// 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 } // 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, linked map[string]*compose.Service, ret chan interface{}) { - logger.Magenta(ICON_PACKAGE+" Generating deployment for ", name) +func buildDeployment(name string, s *types.ServiceConfig, linked map[string]types.ServiceConfig, fileGeneratorChan HelmFileGenerator) { - o := helm.NewDeployment(name) - - container := helm.NewContainer(name, s.Image, s.Environment, s.Labels) - prepareContainer(container, s, name) - prepareEnvFromFiles(name, s, container, ret) - - // Set the container to the deployment - o.Spec.Template.Spec.Containers = []*helm.Container{container} - - // Prepare volumes - madePVC := make(map[string]bool) - o.Spec.Template.Spec.Volumes = prepareVolumes(name, name, s, container, madePVC, ret) + logger.Magenta(ICON_PACKAGE+" Generating deployment for ", name) + deployment := helm.NewDeployment(name) - // Now, for "depends_on" section, it's a bit tricky to get dependencies, see the function below. - o.Spec.Template.Spec.InitContainers = prepareInitContainers(name, s, container) + newContainerForDeployment(name, name, deployment, s, fileGeneratorChan) // Add selectors selectors := buildSelector(name, s) - o.Spec.Selector = map[string]interface{}{ + deployment.Spec.Selector = map[string]interface{}{ "matchLabels": selectors, } - o.Spec.Template.Metadata.Labels = selectors + deployment.Spec.Template.Metadata.Labels = selectors - // Now, the linked services + // Now, the linked services (same pod) for lname, link := range linked { - container := helm.NewContainer(lname, link.Image, link.Environment, link.Labels) - prepareContainer(container, link, lname) - prepareEnvFromFiles(lname, link, container, ret) - o.Spec.Template.Spec.Containers = append(o.Spec.Template.Spec.Containers, container) - o.Spec.Template.Spec.Volumes = append(o.Spec.Template.Spec.Volumes, prepareVolumes(name, lname, link, container, madePVC, ret)...) - o.Spec.Template.Spec.InitContainers = append(o.Spec.Template.Spec.InitContainers, prepareInitContainers(lname, link, container)...) - //append ports and expose ports to the deployment, to be able to generate them in the Service file + newContainerForDeployment(name, lname, deployment, &link, fileGeneratorChan) + // append ports and expose ports to the deployment, + // to be able to generate them in the Service file if len(link.Ports) > 0 || len(link.Expose) > 0 { s.Ports = append(s.Ports, link.Ports...) s.Expose = append(s.Expose, link.Expose...) @@ -107,7 +90,7 @@ func parseService(name string, s *compose.Service, linked map[string]*compose.Se // Remove duplicates in volumes volumes := make([]map[string]interface{}, 0) done := make(map[string]bool) - for _, vol := range o.Spec.Template.Spec.Volumes { + for _, vol := range deployment.Spec.Template.Spec.Volumes { name := vol["name"].(string) if _, ok := done[name]; ok { continue @@ -116,89 +99,68 @@ func parseService(name string, s *compose.Service, linked map[string]*compose.Se volumes = append(volumes, vol) } } - o.Spec.Template.Spec.Volumes = volumes + deployment.Spec.Template.Spec.Volumes = volumes // Then, create Services and possible Ingresses for ingress labels, "ports" and "expose" section if len(s.Ports) > 0 || len(s.Expose) > 0 { for _, s := range generateServicesAndIngresses(name, s) { - ret <- s - } - } - - // 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() - } + if s != nil { + fileGeneratorChan <- s } - }() + } } // add the volumes in Values if len(VolumeValues[name]) > 0 { - locker.Lock() - Values[name]["persistence"] = VolumeValues[name] - locker.Unlock() + AddValues(name, map[string]EnvVal{"persistence": VolumeValues[name]}) } // the deployment is ready, give it - ret <- o + fileGeneratorChan <- deployment // and then, we can say that it's the end - ret <- nil + fileGeneratorChan <- nil } // prepareContainer assigns image, command, env, and labels to a container. -func prepareContainer(container *helm.Container, service *compose.Service, servicename string) { +func prepareContainer(container *helm.Container, service *types.ServiceConfig, servicename string) { // if there is no image name, this should fail! if service.Image == "" { log.Fatal(ICON_PACKAGE+" No image name for service ", servicename) } - container.Image = "{{ .Values." + servicename + ".image }}" - container.Command = service.Command - Values[servicename] = map[string]interface{}{ - "image": service.Image, + + // Get the image tag + imageParts := strings.Split(service.Image, ":") + tag := "" + if len(imageParts) == 2 { + container.Image = imageParts[0] + tag = imageParts[1] } + + vtag := ".Values." + servicename + ".repository.tag" + container.Image = `{{ .Values.` + servicename + `.repository.image }}` + + `{{ if ne ` + vtag + ` "" }}:{{ ` + vtag + ` }}{{ end }}` + container.Command = service.Command + AddValues(servicename, map[string]EnvVal{ + "repository": map[string]EnvVal{ + "image": imageParts[0], + "tag": tag, + }, + }) prepareProbes(servicename, service, container) generateContainerPorts(service, servicename, container) } // Create a service (k8s). -func generateServicesAndIngresses(name string, s *compose.Service) []interface{} { +func generateServicesAndIngresses(name string, s *types.ServiceConfig) []HelmFile { - ret := make([]interface{}, 0) // can handle helm.Service or helm.Ingress + ret := make([]HelmFile, 0) // can handle helm.Service or helm.Ingress logger.Magenta(ICON_SERVICE+" Generating service for ", name) ks := helm.NewService(name) - 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]) - } + for _, p := range s.Ports { + target := int(p.Target) ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(target, target)) - if i == 0 { - detected(name, target) - } } ks.Spec.Selector = buildSelector(name, s) @@ -217,7 +179,9 @@ func generateServicesAndIngresses(name string, s *compose.Service) []interface{} logger.Magenta(ICON_SERVICE+" Generating service for ", name+"-external") ks := helm.NewService(name + "-external") ks.Spec.Type = "NodePort" - for _, p := range s.Expose { + 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) @@ -228,13 +192,20 @@ func generateServicesAndIngresses(name string, s *compose.Service) []interface{} } // Create an ingress. -func createIngress(name string, port int, s *compose.Service) *helm.Ingress { +func createIngress(name string, port int, s *types.ServiceConfig) *helm.Ingress { ingress := helm.NewIngress(name) - Values[name]["ingress"] = map[string]interface{}{ - "class": "nginx", - "host": name + "." + helm.Appname + ".tld", - "enabled": false, + + 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), @@ -244,7 +215,7 @@ func createIngress(name string, port int, s *compose.Service) *helm.Ingress { PathType: "Prefix", Backend: &helm.IngressBackend{ Service: helm.IngressService{ - Name: RELEASE_NAME + "-" + name, + Name: helm.ReleaseNameTpl + "-" + name, Port: map[string]interface{}{ "number": port, }, @@ -259,59 +230,16 @@ func createIngress(name string, port int, s *compose.Service) *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 *compose.Service) map[string]string { +func buildSelector(name string, s *types.ServiceConfig) map[string]string { return map[string]string{ "katenary.io/component": name, - "katenary.io/release": RELEASE_NAME, + "katenary.io/release": helm.ReleaseNameTpl, } } -// buildCMFromPath generates a ConfigMap from a path. -func buildCMFromPath(path string) *helm.ConfigMap { +// buildConfigMapFromPath generates a ConfigMap from a path. +func buildConfigMapFromPath(name, path string) *helm.ConfigMap { stat, err := os.Stat(path) if err != nil { return nil @@ -338,37 +266,34 @@ func buildCMFromPath(path string) *helm.ConfigMap { } } - cm := helm.NewConfigMap("") + cm := helm.NewConfigMap(name, GetRelPath(path)) cm.Data = files return cm } // generateContainerPorts add the container ports of a service. -func generateContainerPorts(s *compose.Service, name string, container *helm.Container) { +func generateContainerPorts(s *types.ServiceConfig, name string, container *helm.Container) { exists := make(map[int]string) for _, port := range s.Ports { - _p := strings.Split(port, ":") - port = _p[0] - if len(_p) > 1 { - port = _p[1] - } - portNumber, _ := strconv.Atoi(port) portName := name for _, n := range exists { if name == n { - portName = fmt.Sprintf("%s-%d", name, portNumber) + portName = fmt.Sprintf("%s-%d", name, port.Target) } } container.Ports = append(container.Ports, &helm.ContainerPort{ Name: portName, - ContainerPort: portNumber, + ContainerPort: int(port.Target), }) - exists[portNumber] = name + exists[int(port.Target)] = name } // manage the "expose" section to be a NodePort in Kubernetes - for _, port := range s.Expose { + for _, expose := range s.Expose { + + port, _ := strconv.Atoi(expose) + if _, exist := exists[port]; exist { continue } @@ -380,45 +305,54 @@ func generateContainerPorts(s *compose.Service, name string, container *helm.Con } // prepareVolumes add the volumes of a service. -func prepareVolumes(deployment, name string, s *compose.Service, container *helm.Container, madePVC map[string]bool, ret chan interface{}) []map[string]interface{} { +func prepareVolumes(deployment, name string, s *types.ServiceConfig, container *helm.Container, fileGeneratorChan HelmFileGenerator) []map[string]interface{} { volumes := make([]map[string]interface{}, 0) mountPoints := make([]interface{}, 0) configMapsVolumes := make([]string, 0) if v, ok := s.Labels[helm.LABEL_VOL_CM]; ok { configMapsVolumes = strings.Split(v, ",") + for i, cm := range configMapsVolumes { + configMapsVolumes[i] = strings.TrimSpace(cm) + } } - for _, volume := range s.Volumes { + for _, vol := range s.Volumes { - parts := strings.Split(volume, ":") - if len(parts) == 1 { - // this is a volume declaration for Docker only, avoid it + volname := vol.Source + volepath := vol.Target + + if volname == "" { + logger.ActivateColors = true + logger.Yellowf("Warning, volume source to %s is empty for %s -- skipping\n", volepath, name) + logger.ActivateColors = false continue } - volname := parts[0] - volepath := parts[1] - - isCM := false + isConfigMap := false for _, cmVol := range configMapsVolumes { - cmVol = strings.TrimSpace(cmVol) - if volname == cmVol { - isCM = true + if GetRelPath(volname) == cmVol { + isConfigMap = true break } } - if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) { - // local volume cannt be mounted + // local volume cannt be mounted + if !isConfigMap && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) { logger.ActivateColors = true logger.Redf("You cannot, at this time, have local volume in %s deployment\n", name) logger.ActivateColors = false continue } - if isCM { + if isConfigMap { // check if the volname path points on a file, if so, we need to add subvolume to the interface - stat, _ := os.Stat(volname) + stat, err := os.Stat(volname) + if err != nil { + logger.ActivateColors = true + logger.Redf("An error occured reading volume path %s\n", err.Error()) + logger.ActivateColors = false + continue + } pointToFile := "" if !stat.IsDir() { pointToFile = filepath.Base(volname) @@ -426,13 +360,10 @@ func prepareVolumes(deployment, name string, s *compose.Service, container *helm } // the volume is a path and it's explicitally asked to be a configmap in labels - cm := buildCMFromPath(volname) - volname = strings.Replace(volname, "./", "", 1) - volname = strings.ReplaceAll(volname, "/", "-") - volname = strings.ReplaceAll(volname, ".", "-") - cm.K8sBase.Metadata.Name = RELEASE_NAME + "-" + volname + "-" + name + cm := buildConfigMapFromPath(name, volname) + cm.K8sBase.Metadata.Name = helm.ReleaseNameTpl + "-" + name + "-" + PathToName(volname) - // build a configmap from the volume path + // build a configmapRef for this volume volumes = append(volumes, map[string]interface{}{ "name": volname, "configMap": map[string]string{ @@ -451,7 +382,9 @@ func prepareVolumes(deployment, name string, s *compose.Service, container *helm "mountPath": volepath, }) } - ret <- cm + if cm != nil { + fileGeneratorChan <- cm + } } else { // rmove minus sign from volume name volname = strings.ReplaceAll(volname, "-", "") @@ -468,7 +401,7 @@ func prepareVolumes(deployment, name string, s *compose.Service, container *helm "name": volname, "mountPath": volepath, }) - container.VolumeMounts = mountPoints + container.VolumeMounts = append(container.VolumeMounts, mountPoints...) isEmptyDir = true break } @@ -480,7 +413,7 @@ func prepareVolumes(deployment, name string, s *compose.Service, container *helm volumes = append(volumes, map[string]interface{}{ "name": volname, "persistentVolumeClaim": map[string]string{ - "claimName": RELEASE_NAME + "-" + volname, + "claimName": helm.ReleaseNameTpl + "-" + volname, }, }) mountPoints = append(mountPoints, map[string]interface{}{ @@ -489,44 +422,40 @@ func prepareVolumes(deployment, name string, s *compose.Service, container *helm }) logger.Yellow(ICON_STORE+" Generate volume values", volname, "for container named", name, "in deployment", deployment) - locker.Lock() - if _, ok := VolumeValues[deployment]; !ok { - VolumeValues[deployment] = make(map[string]map[string]interface{}) - } - VolumeValues[deployment][volname] = map[string]interface{}{ + AddVolumeValues(deployment, volname, map[string]EnvVal{ "enabled": false, "capacity": "1Gi", - } - locker.Unlock() + }) - if _, ok := madePVC[deployment+volname]; !ok { - madePVC[deployment+volname] = true - pvc := helm.NewPVC(deployment, volname) - ret <- pvc + if pvc := helm.NewPVC(deployment, volname); pvc != nil { + fileGeneratorChan <- pvc } } } - container.VolumeMounts = mountPoints + // add the volume in the container and return the volume definition to add in Deployment + container.VolumeMounts = append(container.VolumeMounts, mountPoints...) return volumes } // prepareInitContainers add the init containers of a service. -func prepareInitContainers(name string, s *compose.Service, container *helm.Container) []*helm.Container { +func prepareInitContainers(name string, s *types.ServiceConfig, container *helm.Container) []*helm.Container { // We need to detect others services, but we probably not have parsed them yet, so // we will wait for them for a while. initContainers := make([]*helm.Container, 0) - for _, dp := range s.DependsOn { + for dp := range s.DependsOn { c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels) 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() + if defaultPort, ok := servicesMap[dp]; !ok { + logger.Redf("Error while getting port for service %s\n", dp) + os.Exit(1) } else { foundPort = defaultPort } + locker.Unlock() if foundPort == -1 { log.Fatalf( "ERROR, the %s service is waiting for %s port number, "+ @@ -549,79 +478,89 @@ func prepareInitContainers(name string, s *compose.Service, container *helm.Cont } // prepareProbes generate http/tcp/command probes for a service. -func prepareProbes(name string, s *compose.Service, container *helm.Container) { - - // manage the healthcheck property, if any - if s.HealthCheck != nil { - if s.HealthCheck.Interval == "" { - s.HealthCheck.Interval = "10s" - } - interval, err := time.ParseDuration(s.HealthCheck.Interval) - - if err != nil { - log.Fatal(err) - } - if s.HealthCheck.StartPeriod == "" { - s.HealthCheck.StartPeriod = "0s" - } - - initialDelaySeconds, err := time.ParseDuration(s.HealthCheck.StartPeriod) - if err != nil { - log.Fatal(err) +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 + } - probe := helm.NewProbe(int(interval.Seconds()), int(initialDelaySeconds.Seconds()), 1, s.HealthCheck.Retries) + // if not, we will use the default one + if s.HealthCheck != nil { + container.LivenessProbe = buildCommandProbe(s) + } +} - healthCheckLabel := s.Labels[helm.LABEL_HEALTHCHECK] +// 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 + } - if healthCheckLabel != "" { + path := "/" + if u.Path != "" { + path = u.Path + } - path := "/" - port := 80 + 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 +} - u, err := url.Parse(healthCheckLabel) - if err == nil { - path = u.Path - port, _ = strconv.Atoi(u.Port()) - } else { - path = "/" - port = 80 - } +func buildCommandProbe(s *types.ServiceConfig) *helm.Probe { - if strings.HasPrefix(healthCheckLabel, "http://") { - probe.HttpGet = &helm.HttpGet{ - Path: path, - Port: port, - } - } else if strings.HasPrefix(healthCheckLabel, "tcp://") { - if err != nil { - log.Fatal(err) - } - probe.TCP = &helm.TCP{ - Port: port, - } - } else { - c, _ := shlex.Split(healthCheckLabel) - probe.Exec = &helm.Exec{ + // Get the first element of the command from ServiceConfig + first := s.HealthCheck.Test[0] - Command: c, - } - } - } else if s.HealthCheck.Test[0] == "CMD" || s.HealthCheck.Test[0] == "CMD-SHELL" { - probe.Exec = &helm.Exec{ - Command: s.HealthCheck.Test[1:], - } - } else { - probe.Exec = &helm.Exec{ - Command: s.HealthCheck.Test, - } + p := helm.NewProbeFromService(s) + switch first { + case "CMD", "CMD-SHELL": + // CMD or CMD-SHELL + p.Exec = &helm.Exec{ + Command: s.HealthCheck.Test[1:], } - container.LivenessProbe = probe + return p + default: + // badly made but it should work... + p.Exec = &helm.Exec{ + Command: []string(s.HealthCheck.Test), + } + return p } } // prepareEnvFromFiles generate configMap or secrets from environment files. -func prepareEnvFromFiles(name string, s *compose.Service, container *helm.Container, ret chan interface{}) { +func prepareEnvFromFiles(name string, s *types.ServiceConfig, container *helm.Container, fileGeneratorChan HelmFileGenerator) { // prepare secrets secretsFiles := make([]string, 0) @@ -630,26 +569,26 @@ func prepareEnvFromFiles(name string, s *compose.Service, container *helm.Contai } // manage environment files (env_file in compose) - for _, envfile := range s.EnvFiles { - f := strings.ReplaceAll(envfile, "_", "-") + for _, envfile := range s.EnvFile { + f := PathToName(envfile) f = strings.ReplaceAll(f, ".env", "") - f = strings.ReplaceAll(f, ".", "") - f = strings.ReplaceAll(f, "/", "") - cf := f + "-" + name isSecret := false for _, s := range secretsFiles { + s = strings.TrimSpace(s) if s == envfile { isSecret = true } } var store helm.InlineConfig if !isSecret { - logger.Bluef(ICON_CONF+" Generating configMap %s\n", cf) - store = helm.NewConfigMap(cf) + logger.Bluef(ICON_CONF+" Generating configMap from %s\n", envfile) + store = helm.NewConfigMap(name, envfile) } else { - logger.Bluef(ICON_SECRET+" Generating secret %s\n", cf) - store = helm.NewSecret(cf) + logger.Bluef(ICON_SECRET+" Generating secret from %s\n", envfile) + store = helm.NewSecret(name, envfile) } + + envfile = filepath.Join(compose.GetCurrentDir(), envfile) if err := store.AddEnvFile(envfile); err != nil { logger.ActivateColors = true logger.Red(err.Error()) @@ -668,6 +607,286 @@ func prepareEnvFromFiles(name string, s *compose.Service, container *helm.Contai }, }) - ret <- store + // read the envfile and remove them from the container environment or secret + envs := readEnvFile(envfile) + for varname := range envs { + if !isSecret { + // remove varname from container + for i, s := range container.Env { + if s.Name == varname { + container.Env = append(container.Env[:i], container.Env[i+1:]...) + i-- + } + } + } + } + + if store != nil { + fileGeneratorChan <- store.(HelmFile) + } + } +} + +// AddValues adds values to the values.yaml map. +func AddValues(servicename string, values map[string]EnvVal) { + locker.Lock() + defer locker.Unlock() + + if _, ok := Values[servicename]; !ok { + Values[servicename] = make(map[string]interface{}) + } + + for k, v := range values { + Values[servicename][k] = v + } +} + +// AddVolumeValues add a volume to the values.yaml map for the given deployment name. +func AddVolumeValues(deployment string, volname string, values map[string]EnvVal) { + locker.Lock() + defer locker.Unlock() + + if _, ok := VolumeValues[deployment]; !ok { + VolumeValues[deployment] = make(map[string]map[string]EnvVal) + } + VolumeValues[deployment][volname] = values +} + +func readEnvFile(envfilename string) map[string]EnvVal { + env := make(map[string]EnvVal) + content, err := ioutil.ReadFile(envfilename) + if err != nil { + logger.ActivateColors = true + logger.Red(err.Error()) + logger.ActivateColors = false + os.Exit(2) + } + // each value is on a separate line with KEY=value + lines := strings.Split(string(content), "\n") + for _, line := range lines { + if strings.Contains(line, "=") { + kv := strings.SplitN(line, "=", 2) + env[kv[0]] = kv[1] + } + } + return env +} + +// applyEnvMapLabel will get all LABEL_MAP_ENV to rebuild the env map with tpl. +func applyEnvMapLabel(s *types.ServiceConfig, c *helm.Container) { + + locker.Lock() + defer locker.Unlock() + mapenv, ok := s.Labels[helm.LABEL_MAP_ENV] + if !ok { + return + } + + // the mapenv is a YAML string + var envmap map[string]EnvVal + err := yaml.Unmarshal([]byte(mapenv), &envmap) + if err != nil { + logger.ActivateColors = true + logger.Red(err.Error()) + logger.ActivateColors = false + return + } + + // add in envmap + for k, v := range envmap { + vstring := fmt.Sprintf("%v", v) + s.Environment[k] = &vstring + touched := false + if c.Env != nil { + c.Env = make([]*helm.Value, 0) + } + for _, env := range c.Env { + if env.Name == k { + env.Value = v + touched = true + } + } + if !touched { + c.Env = append(c.Env, &helm.Value{Name: k, Value: v}) + } + } +} + +// setEnvToValues will set the environment variables to the values.yaml map. +func setEnvToValues(name string, s *types.ServiceConfig, c *helm.Container) { + // crete the "environment" key + + env := make(map[string]EnvVal) + for k, v := range s.Environment { + env[k] = v + } + if len(env) == 0 { + return + } + + AddValues(name, map[string]EnvVal{"environment": env}) + for k := range env { + v := "{{ tpl .Values." + name + ".environment." + k + " . }}" + s.Environment[k] = &v + touched := false + for _, c := range c.Env { + if c.Name == k { + c.Value = v + touched = true + } + } + if !touched { + c.Env = append(c.Env, &helm.Value{Name: k, Value: v}) + } + } +} + +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 +} + +// Generate a container in deployment with all needed objects (volumes, secrets, env, ...). +// The deployName shoud be the name of the deployment, we cannot get it from Metadata as this is a variable name. +func newContainerForDeployment(deployName, containerName string, deployment *helm.Deployment, s *types.ServiceConfig, fileGeneratorChan HelmFileGenerator) *helm.Container { + container := helm.NewContainer(containerName, s.Image, s.Environment, s.Labels) + + applyEnvMapLabel(s, container) + if secretFile := setSecretVar(containerName, s, container); secretFile != nil { + fileGeneratorChan <- secretFile + container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ + "secretRef": { + "name": secretFile.Metadata().Name, + }, + }) + } + setEnvToValues(containerName, s, container) + prepareContainer(container, s, containerName) + prepareEnvFromFiles(deployName, s, container, fileGeneratorChan) + + // add the container in deployment + if deployment.Spec.Template.Spec.Containers == nil { + deployment.Spec.Template.Spec.Containers = make([]*helm.Container, 0) + } + deployment.Spec.Template.Spec.Containers = append( + deployment.Spec.Template.Spec.Containers, + container, + ) + + // add the volumes + if deployment.Spec.Template.Spec.Volumes == nil { + deployment.Spec.Template.Spec.Volumes = make([]map[string]interface{}, 0) + } + // manage LABEL_VOLUMEFROM + addVolumeFrom(deployment, container, s) + // and then we can add other volumes + deployment.Spec.Template.Spec.Volumes = append( + deployment.Spec.Template.Spec.Volumes, + prepareVolumes(deployName, containerName, s, container, fileGeneratorChan)..., + ) + + // add init containers + if deployment.Spec.Template.Spec.InitContainers == nil { + deployment.Spec.Template.Spec.InitContainers = make([]*helm.Container, 0) + } + deployment.Spec.Template.Spec.InitContainers = append( + deployment.Spec.Template.Spec.InitContainers, + prepareInitContainers(containerName, s, container)..., + ) + + return container +} + +// addVolumeFrom takes the LABEL_VOLUMEFROM to get volumes from another container. This can only work with +// container that has got LABEL_SAMEPOD as we need to get the volumes from another container in the same deployment. +func addVolumeFrom(deployment *helm.Deployment, container *helm.Container, s *types.ServiceConfig) { + labelfrom, ok := s.Labels[helm.LABEL_VOLUMEFROM] + if !ok { + return + } + + // decode Yaml from the label + var volumesFrom map[string]map[string]string + err := yaml.Unmarshal([]byte(labelfrom), &volumesFrom) + if err != nil { + logger.ActivateColors = true + logger.Red(err.Error()) + logger.ActivateColors = false + return + } + + // for each declared volume "from", we will find it from the deployment volumes and add it to the container. + // Then, to avoid duplicates, we will remove it from the ServiceConfig object. + for name, volumes := range volumesFrom { + for volumeName := range volumes { + initianame := volumeName + volumeName = PathToName(volumeName) + // get the volume from the deployment container "name" + var ctn *helm.Container + for _, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == name { + ctn = c + break + } + } + if ctn == nil { + logger.ActivateColors = true + logger.Redf("VolumeFrom: container %s not found", name) + logger.ActivateColors = false + continue + } + // get the volume from the container + for _, v := range ctn.VolumeMounts { + switch v := v.(type) { + case map[string]interface{}: + if v["name"] == volumeName { + if container.VolumeMounts == nil { + container.VolumeMounts = make([]interface{}, 0) + } + // make a copy of the volume mount and then add it to the VolumeMounts + var mountpoint = make(map[string]interface{}) + for k, v := range v { + mountpoint[k] = v + } + container.VolumeMounts = append(container.VolumeMounts, mountpoint) + + // remove the volume from the ServiceConfig + for i, vol := range s.Volumes { + if vol.Source == initianame { + s.Volumes = append(s.Volumes[:i], s.Volumes[i+1:]...) + i-- + break + } + } + } + } + } + } } } diff --git a/generator/main_test.go b/generator/main_test.go index 583f0b0..7a57006 100644 --- a/generator/main_test.go +++ b/generator/main_test.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/compose-spec/compose-go/cli" ) const DOCKER_COMPOSE_YML = `version: '3' @@ -48,7 +50,8 @@ services: ANOTHER_ENV_VAR: another_value DB_HOST: database labels: - katenary.io/env-to-service: DB_HOST + katenary.io/mapenv: | + DB_HOST: {{ .Release.Name }}-database database: image: mysql:5.7 @@ -87,37 +90,85 @@ services: - SOME_ENV_VAR=some_value - ANOTHER_ENV_VAR=another_value + # use environment file + useenvfile: + image: nginx + env_file: + - config/env + volumes: data: - driver: local ` +var defaultCliFiles = cli.DefaultFileNames +var TMP_DIR = "" +var TMPWORK_DIR = "" + func init() { - logger.NOLOG = true + logger.NOLOG = len(os.Getenv("NOLOG")) < 1 } func setUp(t *testing.T) (string, *compose.Parser) { - p := compose.NewParser("", DOCKER_COMPOSE_YML) - p.Parse("testapp") + + // cleanup "made" files + helm.ResetMadePVC() + + cli.DefaultFileNames = defaultCliFiles // create a temporary directory - tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test") - t.Log("Generated ", tmp, "directory") + tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test-") + if err != nil { + t.Fatal(err) + } + + tmpwork, err := os.MkdirTemp(os.TempDir(), "katenary-test-work-") + if err != nil { + t.Fatal(err) + } + + composefile := filepath.Join(tmpwork, "docker-compose.yaml") + p := compose.NewParser(composefile, DOCKER_COMPOSE_YML) + + // create envfile for "useenvfile" service + err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777) if err != nil { t.Fatal(err) } + envfile := filepath.Join(tmpwork, "config", "env") + fp, err := os.Create(envfile) + if err != nil { + t.Fatal("MKFILE", err) + } + fp.WriteString("FILEENV1=some_value\n") + fp.WriteString("FILEENV2=another_value\n") + fp.Close() + + TMP_DIR = tmp + TMPWORK_DIR = tmpwork + + p.Parse("testapp") - Generate(p, "test-0", "testapp", "1.2.3", DOCKER_COMPOSE_YML, tmp) + Generate(p, "test-0", "testapp", "1.2.3", "4.5.6", DOCKER_COMPOSE_YML, tmp) return tmp, p } +func tearDown() { + if len(TMP_DIR) > 0 { + os.RemoveAll(TMP_DIR) + } + if len(TMPWORK_DIR) > 0 { + os.RemoveAll(TMPWORK_DIR) + } +} + // Check if the web2 service has got a command. func TestCommand(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name if name == "web2" { // Ensure that the command is correctly set // The command should be a string array @@ -159,9 +210,10 @@ func TestCommand(t *testing.T) { // Check if environment is correctly set. func TestEnvs(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name if name == "php" { // the "DB_HOST" environment variable inside the template must be set to '{{ .Release.Name }}-database' @@ -173,20 +225,20 @@ func TestEnvs(t *testing.T) { lines, _ := ioutil.ReadAll(fp) next := false for _, line := range strings.Split(string(lines), "\n") { - if strings.Contains(line, "DB_HOST") { + if !next && strings.Contains(line, "name: DB_HOST") { next = true continue - } - if next { + } else if next && strings.Contains(line, "value:") { matched = true - if !strings.Contains(line, helm.RELEASE_NAME+"-database") { - t.Error("DB_HOST variable should be set to " + helm.RELEASE_NAME + "-database") + if !strings.Contains(line, "{{ tpl .Values.php.environment.DB_HOST . }}") { + t.Error("DB_HOST variable should be set to {{ tpl .Values.php.environment.DB_HOST . }}", line, string(lines)) } break } } if !matched { t.Error("DB_HOST variable not found in ", path) + t.Log(string(lines)) } } } @@ -195,9 +247,10 @@ func TestEnvs(t *testing.T) { // Check if the same pod is not deployed twice. func TestSamePod(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name, service := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name path := filepath.Join(tmp, "templates", name+".deployment.yaml") if _, found := service.Labels[helm.LABEL_SAMEPOD]; found { @@ -220,9 +273,10 @@ func TestSamePod(t *testing.T) { // Check if the ports are correctly set. func TestPorts(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name, service := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name path := "" // if the service has a port found in helm.LABEL_PORT or ports, so the service file should exist @@ -238,7 +292,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) } } } @@ -247,9 +301,10 @@ func TestPorts(t *testing.T) { // Check if the volumes are correctly set. func TestPVC(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name path := filepath.Join(tmp, "templates", name+"-data.pvc.yaml") // the "database" service should have a pvc file in templates (name-data.pvc.yaml) @@ -258,6 +313,8 @@ func TestPVC(t *testing.T) { t.Log("Checking ", name, " pvc file") _, err := os.Stat(path) if err != nil { + list, _ := filepath.Glob(tmp + "/templates/*") + t.Log(list) t.Fatal(err) } } @@ -267,9 +324,10 @@ func TestPVC(t *testing.T) { //Check if web service has got a ingress. func TestIngress(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name path := filepath.Join(tmp, "templates", name+".ingress.yaml") // the "web" service should have a ingress file in templates (name.ingress.yaml) @@ -287,9 +345,10 @@ func TestIngress(t *testing.T) { // Check unmapped volumes func TestUnmappedVolumes(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() - for name := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name if name == "novol" { path := filepath.Join(tmp, "templates", name+".deployment.yaml") fp, _ := os.Open(path) @@ -307,10 +366,11 @@ func TestUnmappedVolumes(t *testing.T) { // Check if service using equal sign for environment works func TestEqualSignOnEnv(t *testing.T) { tmp, p := setUp(t) - defer os.RemoveAll(tmp) + defer tearDown() // if the name is eqenv, the service should habe environment - for name, _ := range p.Data.Services { + for _, service := range p.Data.Services { + name := service.Name if name == "eqenv" { path := filepath.Join(tmp, "templates", name+".deployment.yaml") fp, _ := os.Open(path) @@ -328,8 +388,9 @@ func TestEqualSignOnEnv(t *testing.T) { match++ } } - if match != 2 { + if match != 4 { // because the value points on .Values... t.Error("eqenv service should have 2 environment variables") + t.Log(string(lines)) } } } diff --git a/generator/utils.go b/generator/utils.go new file mode 100644 index 0000000..a862035 --- /dev/null +++ b/generator/utils.go @@ -0,0 +1,22 @@ +package generator + +import ( + "katenary/compose" + "regexp" + "strings" +) + +// replaceChars replaces some chars in a string. +const replaceChars = `[^a-zA-Z0-9._]` + +// GetRelPath return the relative path from the root of the project. +func GetRelPath(path string) string { + return strings.Replace(path, compose.GetCurrentDir(), ".", 1) +} + +// PathToName transform a path to a yaml name. +func PathToName(path string) string { + path = strings.TrimPrefix(GetRelPath(path), "./") + path = regexp.MustCompile(replaceChars).ReplaceAllString(path, "-") + return path +} diff --git a/generator/writer.go b/generator/writer.go index 01f5e14..9afec43 100644 --- a/generator/writer.go +++ b/generator/writer.go @@ -4,18 +4,42 @@ import ( "katenary/compose" "katenary/generator/writers" "katenary/helm" + "log" "os" "path/filepath" "regexp" + "strconv" "strings" "time" + "github.com/compose-spec/compose-go/types" "gopkg.in/yaml.v3" ) +// HelmFile represents a helm file from helm package that has got some necessary methods +// to generate a helm file. +type HelmFile interface { + GetType() string + GetPathRessource() string +} + +// HelmFileGenerator is a chanel of HelmFile. +type HelmFileGenerator chan HelmFile + var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`) -func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFile, dirName string) { +func portExists(port int, ports []types.ServicePortConfig) bool { + for _, p := range ports { + if p.Target == uint32(port) { + log.Println("portExists:", port, p.Target) + return true + } + } + return false +} + +// Generate get a parsed compose file, and generate the helm files. +func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVersion, composeFile, dirName string) { // make the appname global (yes... ugly but easy) helm.Appname = appName @@ -25,64 +49,108 @@ 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{}) + generators := make(map[string]HelmFileGenerator) + + // remove skipped services from the parsed data + for i, service := range p.Data.Services { + if v, ok := service.Labels[helm.LABEL_IGNORE]; !ok || v != "true" { + continue + } + p.Data.Services = append(p.Data.Services[:i], p.Data.Services[i+1:]...) + i-- - // list avoided services - avoids := make(map[string]bool) - for n, service := range p.Data.Services { - if _, ok := service.Labels[helm.LABEL_SAMEPOD]; ok { - avoids[n] = true + // find this service in others as "depends_on" and remove it + for _, service2 := range p.Data.Services { + delete(service2.DependsOn, service.Name) } } - for name, s := range p.Data.Services { + for i, service := range p.Data.Services { + n := service.Name + + // if the service port is declared in labels, add it to the service. + if ports, ok := service.Labels[helm.LABEL_PORT]; ok { + if service.Ports == nil { + service.Ports = make([]types.ServicePortConfig, 0) + } + for _, port := range strings.Split(ports, ",") { + target, err := strconv.Atoi(port) + if err != nil { + log.Fatal(err) + } + if portExists(target, service.Ports) { + continue + } + service.Ports = append(service.Ports, types.ServicePortConfig{ + Target: uint32(target), + }) + } + } + // find port and store it in servicesMap + for _, port := range service.Ports { + target := int(port.Target) + if target != 0 { + servicesMap[n] = target + break + } + } - // 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, ",") + for i, emptyDir := range emptyDirs { + emptyDirs[i] = strings.TrimSpace(emptyDir) + } //append them in EmptyDirs EmptyDirs = append(EmptyDirs, emptyDirs...) } + p.Data.Services[i] = service - // fetch corresponding service in "links" - linked := make(map[string]*compose.Service, 0) - // find service linked to this one - for n, service := range p.Data.Services { - if _, ok := service.Labels[helm.LABEL_SAMEPOD]; ok { - if service.Labels[helm.LABEL_SAMEPOD] == name { - linked[n] = service - } - } - } + } - if _, found := avoids[name]; found { + // for all services in linked map, and not in samePods map, generate the service + for _, s := range p.Data.Services { + name := s.Name + + // do not make a deployment for services declared to be in the same pod than another + if _, ok := s.Labels[helm.LABEL_SAMEPOD]; ok { continue } - files[name] = CreateReplicaObject(name, s, linked) + + // find services that is in the same pod + linked := make(map[string]types.ServiceConfig, 0) + for _, service := range p.Data.Services { + n := service.Name + if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name { + linked[n] = service + } + } + + generators[name] = CreateReplicaObject(name, s, linked) } // to generate notes, we need to keep an Ingresses list ingresses := make(map[string]*helm.Ingress) - for n, f := range files { - for c := range f { - if c == nil { + for n, generator := range generators { // generators is a map : name -> generator + for helmFile := range generator { // generator is a chan + if helmFile == nil { // generator finished break } - kind := c.(helm.Kinded).Get() + kind := helmFile.(helm.Kinded).Get() kind = strings.ToLower(kind) // Add a SHA inside the generated file, it's only // to make it easy to check it the compose file corresponds to the // generated helm chart - c.(helm.Signable).BuildSHA(composeFile) + helmFile.(helm.Signable).BuildSHA(composeFile) // Some types need special fixes in yaml generation - switch c := c.(type) { + switch c := helmFile.(type) { case *helm.Storage: // For storage, we need to add a "condition" to activate it writers.BuildStorage(c, n, templatesDir) @@ -104,45 +172,60 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, composeFi case *helm.ConfigMap, *helm.Secret: // there could be several files, so let's force the filename - name := c.(helm.Named).Name() + name := c.(helm.Named).Name() + "-" + c.GetType() + suffix := c.GetPathRessource() + suffix = PathToName(suffix) + name += suffix name = PrefixRE.ReplaceAllString(name, "") writers.BuildConfigMap(c, kind, n, name, templatesDir) default: fname := filepath.Join(templatesDir, n+"."+kind+".yaml") - fp, _ := os.Create(fname) + fp, err := os.Create(fname) + if err != nil { + log.Fatal(err) + } + defer fp.Close() enc := yaml.NewEncoder(fp) - enc.SetIndent(2) + enc.SetIndent(writers.IndentSize) enc.Encode(c) - fp.Close() } } } // Create the values.yaml file - fp, _ := os.Create(filepath.Join(dirName, "values.yaml")) - enc := yaml.NewEncoder(fp) - enc.SetIndent(2) + valueFile, err := os.Create(filepath.Join(dirName, "values.yaml")) + if err != nil { + log.Fatal(err) + } + defer valueFile.Close() + enc := yaml.NewEncoder(valueFile) + enc.SetIndent(writers.IndentSize) enc.Encode(Values) - fp.Close() // Create tht Chart.yaml file - fp, _ = os.Create(filepath.Join(dirName, "Chart.yaml")) - fp.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n") - fp.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n") - enc = yaml.NewEncoder(fp) + chartFile, err := os.Create(filepath.Join(dirName, "Chart.yaml")) + if err != nil { + log.Fatal(err) + } + defer chartFile.Close() + chartFile.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n") + chartFile.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n") + enc = yaml.NewEncoder(chartFile) enc.SetIndent(writers.IndentSize) enc.Encode(map[string]interface{}{ "apiVersion": "v2", "name": appName, "description": "A helm chart for " + appName, "type": "application", - "version": "0.1.0", + "version": chartVersion, "appVersion": appVersion, }) - fp.Close() // And finally, create a NOTE.txt file - fp, _ = os.Create(filepath.Join(templatesDir, "NOTES.txt")) - fp.WriteString(helm.GenerateNotesFile(ingresses)) - fp.Close() + noteFile, err := os.Create(filepath.Join(templatesDir, "NOTES.txt")) + if err != nil { + log.Fatal(err) + } + defer noteFile.Close() + noteFile.WriteString(helm.GenerateNotesFile(ingresses)) } diff --git a/generator/writers/configmap.go b/generator/writers/configmap.go index d17a889..d045f1d 100644 --- a/generator/writers/configmap.go +++ b/generator/writers/configmap.go @@ -7,8 +7,9 @@ import ( "gopkg.in/yaml.v3" ) +// BuildConfigMap writes the configMap. func BuildConfigMap(c interface{}, kind, servicename, name, templatesDir string) { - fname := filepath.Join(templatesDir, servicename+"."+name+"."+kind+".yaml") + fname := filepath.Join(templatesDir, name+"."+kind+".yaml") fp, _ := os.Create(fname) enc := yaml.NewEncoder(fp) enc.SetIndent(IndentSize) diff --git a/generator/writers/deployment.go b/generator/writers/deployment.go index ca09249..7f594ca 100644 --- a/generator/writers/deployment.go +++ b/generator/writers/deployment.go @@ -10,6 +10,7 @@ import ( "gopkg.in/yaml.v3" ) +// BuildDeployment builds a deployment. func BuildDeployment(deployment *helm.Deployment, name, templatesDir string) { kind := "deployment" fname := filepath.Join(templatesDir, name+"."+kind+".yaml") diff --git a/generator/writers/ingress.go b/generator/writers/ingress.go index b11a552..fbfdc60 100644 --- a/generator/writers/ingress.go +++ b/generator/writers/ingress.go @@ -10,9 +10,20 @@ import ( "gopkg.in/yaml.v3" ) -const classAndVersionCondition = `{{- if and .Values.__name__.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}` + "\n" -const versionCondition = `{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}` + "\n" +const ( + classAndVersionCondition = `{{- if and .Values.__name__.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}` + "\n" + versionCondition118 = `{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}` + "\n" + versionCondition119 = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}` + "\n" + apiVersion = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }}` +) +// BuildIngress generates the ingress yaml file with conditions. func BuildIngress(ingress *helm.Ingress, name, templatesDir string) { // Set the backend for 1.18 for _, b := range ingress.Spec.Rules { @@ -32,22 +43,49 @@ func BuildIngress(ingress *helm.Ingress, name, templatesDir string) { enc.Encode(ingress) buffer.WriteString("{{- end -}}") - fp, _ := os.Create(fname) + fp, err := os.Create(fname) + if err != nil { + panic(err) + } + defer fp.Close() + content := string(buffer.Bytes()) lines := strings.Split(content, "\n") backendHit := false for _, l := range lines { + // apiVersion is a pain... + if strings.Contains(l, "apiVersion:") { + l = apiVersion + } + + // add annotations linked to the Values + if strings.Contains(l, "annotations:") { + n := CountSpaces(l) + IndentSize + l += "\n" + strings.Repeat(" ", n) + "{{- range $k, $v := .Values.__name__.ingress.annotations }}\n" + l += strings.Repeat(" ", n) + "{{ $k }}: {{ $v }}\n" + l += strings.Repeat(" ", n) + "{{- end }}" + l = strings.ReplaceAll(l, "__name__", name) + } + + // pathTyype is ony for 1.19+ + if strings.Contains(l, "pathType:") { + n := CountSpaces(l) + l = strings.Repeat(" ", n) + versionCondition118 + + l + "\n" + + strings.Repeat(" ", n) + "{{- end }}" + } + if strings.Contains(l, "ingressClassName") { // should be set only if the version of Kubernetes is 1.18-0 or higher cond := strings.ReplaceAll(classAndVersionCondition, "__name__", name) l = ` ` + cond + l + "\n" + ` {{- end }}` } - // manage the backend format following the Kubernetes 1.18-0 version or higher + // manage the backend format following the Kubernetes 1.19-0 version or higher if strings.Contains(l, "service:") { n := CountSpaces(l) - l = strings.Repeat(" ", n) + versionCondition + l + l = strings.Repeat(" ", n) + versionCondition119 + l } if strings.Contains(l, "serviceName:") || strings.Contains(l, "servicePort:") { n := CountSpaces(l) @@ -58,9 +96,6 @@ func BuildIngress(ingress *helm.Ingress, name, templatesDir string) { } backendHit = true } - fp.WriteString(l + "\n") } - - fp.Close() } diff --git a/generator/writers/service.go b/generator/writers/service.go index 2ba9b82..c898e27 100644 --- a/generator/writers/service.go +++ b/generator/writers/service.go @@ -8,6 +8,7 @@ import ( "gopkg.in/yaml.v3" ) +// BuildService writes the service (external or not). func BuildService(service *helm.Service, name, templatesDir string) { kind := "service" suffix := "" diff --git a/generator/writers/storage.go b/generator/writers/storage.go index aa1593b..2201c01 100644 --- a/generator/writers/storage.go +++ b/generator/writers/storage.go @@ -2,23 +2,31 @@ package writers import ( "katenary/helm" + "log" "os" "path/filepath" "gopkg.in/yaml.v3" ) +// BuildStorage writes the persistentVolumeClaim. func BuildStorage(storage *helm.Storage, name, templatesDir string) { kind := "pvc" name = storage.Metadata.Labels[helm.K+"/component"] pvcname := storage.Metadata.Labels[helm.K+"/pvc-name"] fname := filepath.Join(templatesDir, name+"-"+pvcname+"."+kind+".yaml") - fp, _ := os.Create(fname) + fp, err := os.Create(fname) + if err != nil { + log.Fatal(err) + } + defer fp.Close() volname := storage.K8sBase.Metadata.Labels[helm.K+"/pvc-name"] fp.WriteString("{{ if .Values." + name + ".persistence." + volname + ".enabled }}\n") enc := yaml.NewEncoder(fp) enc.SetIndent(IndentSize) - enc.Encode(storage) + if err := enc.Encode(storage); err != nil { + log.Fatal(err) + } fp.WriteString("{{- end -}}") } diff --git a/generator/writers/utils.go b/generator/writers/utils.go index b057945..5bfa607 100644 --- a/generator/writers/utils.go +++ b/generator/writers/utils.go @@ -1,6 +1,6 @@ package writers -// IndentSize set the indentation size for yaml output. +// IndentSize set the indentation size for yaml output. Could ba changed by command line argument. var IndentSize = 2 // CountSpaces returns the number of spaces from the begining of the line. diff --git a/go.mod b/go.mod index edf4df7..6685316 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,14 @@ module katenary go 1.16 require ( - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/compose-spec/compose-go v1.2.4 + github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 // indirect github.com/kr/pretty v0.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/spf13/cobra v1.4.0 + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect golang.org/x/mod v0.5.1 + golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index 75b2588..153f8ab 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,304 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/compose-spec/compose-go v1.2.4 h1:nzTFqM8+2J7Veao5Pq5U451thinv3U1wChIvcjX59/A= +github.com/compose-spec/compose-go v1.2.4/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e/go.mod h1:xpWTC2KnJMiDLkoawhsPQcXjvwATEBcbq0xevG2YR9M= +github.com/distribution/distribution/v3 v3.0.0-20220504180456-7a6b9e3042bd h1:KRoLSsR7wZ4H2dueR/O6BGBIXDxfOxUVmaMiu1QiQPw= +github.com/distribution/distribution/v3 v3.0.0-20220504180456-7a6b9e3042bd/go.mod h1:qLi7jGj1b5TUaYTB3ekkHiocxHJi8+3CFhXM54MGKBs= +github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 h1:KfVB1Z5fm10trO24Rn5Zzocd8sTm5k/gS24ijxQ1aJU= +github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414/go.mod h1:2oyLKljQFnsI1tzJxjUg4GI+HEpDfzFP3LrGM04rKg0= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helm/configMap.go b/helm/configAndSecretMap.go similarity index 64% rename from helm/configMap.go rename to helm/configAndSecretMap.go index c2bdb3c..86ff0b7 100644 --- a/helm/configMap.go +++ b/helm/configAndSecretMap.go @@ -10,30 +10,38 @@ import ( // InlineConfig is made to represent a configMap or a secret type InlineConfig interface { AddEnvFile(filename string) error + AddEnv(key, val string) error Metadata() *Metadata } +// ConfigMap is made to represent a configMap with data. type ConfigMap struct { *K8sBase `yaml:",inline"` Data map[string]string `yaml:"data"` } -func NewConfigMap(name string) *ConfigMap { +// NewConfigMap returns a new initialzed ConfigMap. +func NewConfigMap(name, path string) *ConfigMap { base := NewBase() base.ApiVersion = "v1" base.Kind = "ConfigMap" - base.Metadata.Name = RELEASE_NAME + "-" + name + base.Metadata.Name = ReleaseNameTpl + "-" + name base.Metadata.Labels[K+"/component"] = name + if path != "" { + base.Metadata.Labels[K+"/path"] = path + } return &ConfigMap{ K8sBase: base, Data: make(map[string]string), } } +// Metadata returns the metadata of the configMap. func (c *ConfigMap) Metadata() *Metadata { return c.K8sBase.Metadata } +// AddEnvFile adds an environment file to the configMap. func (c *ConfigMap) AddEnvFile(file string) error { content, err := ioutil.ReadFile(file) if err != nil { @@ -52,28 +60,37 @@ func (c *ConfigMap) AddEnvFile(file string) error { } c.Data[parts[0]] = parts[1] } - return nil +} +func (c *ConfigMap) AddEnv(key, val string) error { + c.Data[key] = val + return nil } +// Secret is made to represent a secret with data. type Secret struct { *K8sBase `yaml:",inline"` Data map[string]string `yaml:"data"` } -func NewSecret(name string) *Secret { +// NewSecret returns a new initialzed Secret. +func NewSecret(name, path string) *Secret { base := NewBase() base.ApiVersion = "v1" base.Kind = "Secret" - base.Metadata.Name = RELEASE_NAME + "-" + name + base.Metadata.Name = ReleaseNameTpl + "-" + name base.Metadata.Labels[K+"/component"] = name + if path != "" { + base.Metadata.Labels[K+"/path"] = path + } return &Secret{ K8sBase: base, Data: make(map[string]string), } } +// AddEnvFile adds an environment file to the secret. func (s *Secret) AddEnvFile(file string) error { content, err := ioutil.ReadFile(file) if err != nil { @@ -96,6 +113,14 @@ func (s *Secret) AddEnvFile(file string) error { return nil } + +// Metadata returns the metadata of the secret. func (s *Secret) Metadata() *Metadata { return s.K8sBase.Metadata } + +// AddEnv adds an environment variable to the secret. +func (s *Secret) AddEnv(key, val string) error { + s.Data[key] = fmt.Sprintf(`{{ %s | b64enc }}`, val) + return nil +} diff --git a/helm/container.go b/helm/container.go new file mode 100644 index 0000000..05441fa --- /dev/null +++ b/helm/container.go @@ -0,0 +1,65 @@ +package helm + +import ( + "katenary/logger" + "strings" + + "github.com/compose-spec/compose-go/types" +) + +type EnvValue interface{} + +// ContainerPort represent a port mapping. +type ContainerPort struct { + Name string + ContainerPort int `yaml:"containerPort"` +} + +// Value represent a environment variable with name and value. +type Value struct { + Name string `yaml:"name"` + Value EnvValue `yaml:"value"` +} + +// Container represent a container with name, image, and environment variables. It is used in Deployment. +type Container struct { + Name string `yaml:"name,omitempty"` + Image string `yaml:"image"` + Ports []*ContainerPort `yaml:"ports,omitempty"` + Env []*Value `yaml:"env,omitempty"` + EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"` + Command []string `yaml:"command,omitempty"` + VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"` + LivenessProbe *Probe `yaml:"livenessProbe,omitempty"` +} + +// NewContainer creates a new container with name, image, labels and environment variables. +func NewContainer(name, image string, environment types.MappingWithEquals, labels map[string]string) *Container { + container := &Container{ + Image: image, + Name: name, + EnvFrom: make([]map[string]map[string]string, 0), + } + + // find bound environment variable to a service + toServices := make([]string, 0) + if bound, ok := labels[LABEL_ENV_SERVICE]; ok { + toServices = strings.Split(bound, ",") + } + if len(toServices) > 0 { + // warn, it's deprecated now + logger.ActivateColors = true + logger.Yellowf( + "[deprecated] in \"%s\" service: label %s is deprecated and **ignored**, please use %s instead\n"+ + "e.g.\n"+ + " labels:\n"+ + " FOO: {{ .Release.Name }}-fooservice\n", + name, + LABEL_ENV_SERVICE, + LABEL_MAP_ENV, + ) + logger.ActivateColors = false + } + + return container +} diff --git a/helm/deployment.go b/helm/deployment.go index 9d6f28b..d1dec8a 100644 --- a/helm/deployment.go +++ b/helm/deployment.go @@ -1,7 +1,5 @@ package helm -import "strings" - // Deployment is a k8s deployment. type Deployment struct { *K8sBase `yaml:",inline"` @@ -10,7 +8,7 @@ type Deployment struct { func NewDeployment(name string) *Deployment { d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()} - d.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name + d.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name d.K8sBase.ApiVersion = "apps/v1" d.K8sBase.Kind = "Deployment" d.K8sBase.Metadata.Labels[K+"/component"] = name @@ -29,86 +27,6 @@ func NewDepSpec() *DepSpec { } } -type Value struct { - Name string `yaml:"name"` - Value interface{} `yaml:"value"` -} - -type ContainerPort struct { - Name string - ContainerPort int `yaml:"containerPort"` -} - -type Container struct { - Name string `yaml:"name,omitempty"` - Image string `yaml:"image"` - Ports []*ContainerPort `yaml:"ports,omitempty"` - Env []Value `yaml:"env,omitempty"` - EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"` - Command []string `yaml:"command,omitempty"` - VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"` - LivenessProbe *Probe `yaml:"livenessProbe,omitempty"` -} - -type HttpGet struct { - Path string `yaml:"path"` - Port int `yaml:"port"` -} - -type Exec struct { - Command []string `yaml:"command"` -} - -type TCP struct { - Port int `yaml:"port"` -} - -type Probe struct { - HttpGet *HttpGet `yaml:"httpGet,omitempty"` - Exec *Exec `yaml:"exec,omitempty"` - TCP *TCP `yaml:"tcp,omitempty"` - Period int `yaml:"periodSeconds"` - Success int `yaml:"successThreshold"` - Failure int `yaml:"failureThreshold"` - InitialDelay int `yaml:"initialDelaySeconds"` -} - -func NewProbe(period, initialDelaySeconds, success, failure int) *Probe { - return &Probe{ - Period: period, - Success: success, - Failure: failure, - InitialDelay: initialDelaySeconds, - } -} - -func NewContainer(name, image string, environment, labels map[string]string) *Container { - container := &Container{ - Image: image, - Name: name, - Env: make([]Value, len(environment)), - EnvFrom: make([]map[string]map[string]string, 0), - } - - // find bound environment variable to a service - toServices := make([]string, 0) - if bound, ok := labels[LABEL_ENV_SERVICE]; ok { - toServices = strings.Split(bound, ",") - } - - idx := 0 - for n, v := range environment { - for _, name := range toServices { - if name == n { - v = RELEASE_NAME + "-" + v - } - } - container.Env[idx] = Value{Name: n, Value: v} - idx++ - } - return container -} - type PodSpec struct { InitContainers []*Container `yaml:"initContainers,omitempty"` Containers []*Container `yaml:"containers"` diff --git a/helm/ingress.go b/helm/ingress.go index 8301c06..f2ad8e8 100644 --- a/helm/ingress.go +++ b/helm/ingress.go @@ -1,5 +1,6 @@ package helm +// Ingress is the kubernetes ingress object. type Ingress struct { *K8sBase `yaml:",inline"` Spec IngressSpec @@ -8,7 +9,7 @@ type Ingress struct { func NewIngress(name string) *Ingress { i := &Ingress{} i.K8sBase = NewBase() - i.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name + i.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name i.K8sBase.Kind = "Ingress" i.ApiVersion = "networking.k8s.io/v1" i.K8sBase.Metadata.Labels[K+"/component"] = name diff --git a/helm/k8sbase.go b/helm/k8sbase.go new file mode 100644 index 0000000..df95877 --- /dev/null +++ b/helm/k8sbase.go @@ -0,0 +1,73 @@ +package helm + +import ( + "crypto/sha1" + "fmt" + "io/ioutil" + "strings" +) + +// Metadata is the metadata for a kubernetes object. +type Metadata struct { + Name string `yaml:"name,omitempty"` + Labels map[string]string `yaml:"labels"` + Annotations map[string]string `yaml:"annotations,omitempty"` +} + +func NewMetadata() *Metadata { + return &Metadata{ + Name: "", + Labels: make(map[string]string), + Annotations: make(map[string]string), + } +} + +// K8sBase is the base for all kubernetes objects. +type K8sBase struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata *Metadata `yaml:"metadata"` +} + +// NewBase is a factory for creating a new base object with metadata, labels and annotations set to the default. +func NewBase() *K8sBase { + b := &K8sBase{ + Metadata: NewMetadata(), + } + // add some information of the build + b.Metadata.Labels[K+"/project"] = "{{ .Chart.Name }}" + b.Metadata.Labels[K+"/release"] = ReleaseNameTpl + b.Metadata.Annotations[K+"/version"] = Version + return b +} + +func (k *K8sBase) BuildSHA(filename string) { + c, _ := ioutil.ReadFile(filename) + //sum := sha256.Sum256(c) + sum := sha1.Sum(c) + k.Metadata.Annotations[K+"/docker-compose-sha1"] = fmt.Sprintf("%x", string(sum[:])) +} + +// Get returns the Kind. +func (k *K8sBase) Get() string { + return k.Kind +} + +// Name returns the name of the object from Metadata. +func (k *K8sBase) Name() string { + return k.Metadata.Name +} + +func (k *K8sBase) GetType() string { + if n, ok := k.Metadata.Labels[K+"/type"]; ok { + return n + } + return strings.ToLower(k.Kind) +} + +func (k *K8sBase) GetPathRessource() string { + if p, ok := k.Metadata.Labels[K+"/path"]; ok { + return p + } + return "" +} diff --git a/helm/labels.go b/helm/labels.go new file mode 100644 index 0000000..d9fc397 --- /dev/null +++ b/helm/labels.go @@ -0,0 +1,61 @@ +package helm + +import ( + "bytes" + "html/template" +) + +const ReleaseNameTpl = "{{ .Release.Name }}" +const ( + LABEL_MAP_ENV = K + "/mapenv" + LABEL_ENV_SECRET = K + "/secret-envfiles" + LABEL_PORT = K + "/ports" + LABEL_INGRESS = K + "/ingress" + LABEL_VOL_CM = K + "/configmap-volumes" + LABEL_HEALTHCHECK = K + "/healthcheck" + LABEL_SAMEPOD = K + "/same-pod" + LABEL_VOLUMEFROM = K + "/volume-from" + LABEL_EMPTYDIRS = K + "/empty-dirs" + LABEL_IGNORE = K + "/ignore" + LABEL_SECRETVARS = K + "/secret-vars" + + //deprecated: use LABEL_MAP_ENV instead + LABEL_ENV_SERVICE = K + "/env-to-service" +) + +// GetLabelsDocumentation returns the documentation for the labels. +func GetLabelsDocumentation() string { + t, _ := template.New("labels").Parse(` +# Labels +{{.LABEL_IGNORE | printf "%-33s"}}: ignore the container, it will not yied any object in the helm chart +{{.LABEL_SECRETVARS | printf "%-33s"}}: secret variables to push on a secret file +{{.LABEL_ENV_SECRET | printf "%-33s"}}: set the given file names as a secret instead of configmap +{{.LABEL_MAP_ENV | printf "%-33s"}}: map environment variable to a template string (yaml style) +{{.LABEL_PORT | printf "%-33s"}}: set the ports to expose as a service (coma separated) +{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress (coma separated) +{{.LABEL_VOL_CM | printf "%-33s"}}: specifies that the volumes points on a configmap (coma separated) +{{.LABEL_SAMEPOD | printf "%-33s"}}: specifies that the pod should be deployed in the same pod than the given service name +{{.LABEL_VOLUMEFROM | printf "%-33s"}}: specifies that the volumes to be mounted from the given service (yaml style) +{{.LABEL_EMPTYDIRS | printf "%-33s"}}: specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated) +{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**. +{{ printf "%-34s" ""}} You can use these form of label values: +{{ printf "%-35s" ""}}- "http://[not used address][:port][/path]" to specify an http healthcheck +{{ printf "%-35s" ""}}- "tcp://[not used address]:port" to specify a tcp healthcheck +{{ printf "%-35s" ""}}- other string is condidered as a "command" healthcheck + `) + buff := bytes.NewBuffer(nil) + t.Execute(buff, map[string]string{ + "LABEL_ENV_SECRET": LABEL_ENV_SECRET, + "LABEL_PORT": LABEL_PORT, + "LABEL_INGRESS": LABEL_INGRESS, + "LABEL_VOL_CM": LABEL_VOL_CM, + "LABEL_HEALTHCHECK": LABEL_HEALTHCHECK, + "LABEL_SAMEPOD": LABEL_SAMEPOD, + "LABEL_VOLUMEFROM": LABEL_VOLUMEFROM, + "LABEL_EMPTYDIRS": LABEL_EMPTYDIRS, + "LABEL_IGNORE": LABEL_IGNORE, + "LABEL_MAP_ENV": LABEL_MAP_ENV, + "LABEL_SECRETVARS": LABEL_SECRETVARS, + }) + return buff.String() +} diff --git a/helm/notes.go b/helm/notes.go index ce3ad2e..54a33ec 100644 --- a/helm/notes.go +++ b/helm/notes.go @@ -10,6 +10,7 @@ Your application is now deployed. This may take a while to be up and responding. __list__ ` +// GenerateNotesFile generates the notes file for the helm chart. func GenerateNotesFile(ingressess map[string]*Ingress) string { list := make([]string, 0) diff --git a/helm/probe.go b/helm/probe.go new file mode 100644 index 0000000..bc80e4b --- /dev/null +++ b/helm/probe.go @@ -0,0 +1,104 @@ +package helm + +import ( + "time" + + "github.com/compose-spec/compose-go/types" +) + +// Probe is a struct that can be used to create a Liveness or Readiness probe. +type Probe struct { + HttpGet *HttpGet `yaml:"httpGet,omitempty"` + Exec *Exec `yaml:"exec,omitempty"` + TCP *TCP `yaml:"tcp,omitempty"` + Period float64 `yaml:"periodSeconds"` + InitialDelay float64 `yaml:"initialDelaySeconds"` + Success uint64 `yaml:"successThreshold"` + Failure uint64 `yaml:"failureThreshold"` +} + +// Create a new Probe object that can be apply to HttpProbe or TCPProbe. +func NewProbe(period, initialDelaySeconds float64, success, failure uint64) *Probe { + probe := &Probe{ + Period: period, + Success: success, + Failure: failure, + InitialDelay: initialDelaySeconds, + } + + // fix default values from + // https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + if period == 0 { + probe.Period = 10 + } + if success == 0 { + probe.Success = 1 + } + if failure == 0 { + probe.Failure = 3 + } + return probe +} + +// NewProbeWithDuration creates a new Probe object with the given duration from types. +func NewProbeWithDuration(period, initialDelaySeconds *types.Duration, success, failure *uint64) *Probe { + + if period == nil { + d := types.Duration(0 * time.Second) + period = &d + } + + if initialDelaySeconds == nil { + d := types.Duration(0 * time.Second) + initialDelaySeconds = &d + } + + if success == nil { + s := uint64(0) + success = &s + } + + if failure == nil { + f := uint64(0) + failure = &f + } + + p, err := time.ParseDuration(period.String()) + if err != nil { + p = time.Second * 10 + } + + i, err := time.ParseDuration(initialDelaySeconds.String()) + if err != nil { + i = time.Second * 0 + } + + return NewProbe(p.Seconds(), i.Seconds(), *success, *failure) + +} + +// NewProbeFromService creates a new Probe object from a ServiceConfig. +func NewProbeFromService(s *types.ServiceConfig) *Probe { + if s == nil || s.HealthCheck == nil { + return NewProbe(0, 0, 0, 0) + } + + return NewProbeWithDuration(s.HealthCheck.Interval, s.HealthCheck.StartPeriod, nil, s.HealthCheck.Retries) + +} + +// HttpGet is a Probe configuration to check http health. +type HttpGet struct { + Path string `yaml:"path"` + Port int `yaml:"port"` +} + +// Execis a Probe configuration to check exec health. +type Exec struct { + Command []string `yaml:"command"` +} + +// TCP is a Probe configuration to check tcp health. +type TCP struct { + Port int `yaml:"port"` +} diff --git a/helm/service.go b/helm/service.go index 6080904..f1a7ec1 100644 --- a/helm/service.go +++ b/helm/service.go @@ -1,28 +1,32 @@ package helm +// Service is a Kubernetes service. type Service struct { *K8sBase `yaml:",inline"` Spec *ServiceSpec `yaml:"spec"` } +// NewService creates a new initialized service. func NewService(name string) *Service { s := &Service{ K8sBase: NewBase(), Spec: NewServiceSpec(), } - s.K8sBase.Metadata.Name = RELEASE_NAME + "-" + name + s.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name s.K8sBase.Kind = "Service" s.K8sBase.ApiVersion = "v1" s.K8sBase.Metadata.Labels[K+"/component"] = name return s } +// ServicePort is a port on a service. type ServicePort struct { Protocol string `yaml:"protocol"` Port int `yaml:"port"` TargetPort int `yaml:"targetPort"` } +// NewServicePort creates a new initialized service port. func NewServicePort(port, target int) *ServicePort { return &ServicePort{ Protocol: "TCP", @@ -31,12 +35,14 @@ func NewServicePort(port, target int) *ServicePort { } } +// ServiceSpec is the spec for a service. type ServiceSpec struct { Selector map[string]string Ports []*ServicePort Type string `yaml:"type,omitempty"` } +// NewServiceSpec creates a new initialized service spec. func NewServiceSpec() *ServiceSpec { return &ServiceSpec{ Selector: make(map[string]string), diff --git a/helm/storage.go b/helm/storage.go index c10ef3b..e09e82f 100644 --- a/helm/storage.go +++ b/helm/storage.go @@ -1,17 +1,40 @@ package helm +import "sync" + +var ( + made = make(map[string]bool) + locker = sync.Mutex{} +) + +// ResetMadePVC resets the cache of made PVCs. +// Useful in tests only. +func ResetMadePVC() { + locker.Lock() + defer locker.Unlock() + made = make(map[string]bool) +} + +// Storage is a struct for a PersistentVolumeClaim. type Storage struct { *K8sBase `yaml:",inline"` Spec *PVCSpec } +// NewPVC creates a new PersistentVolumeClaim object. func NewPVC(name, storageName string) *Storage { + locker.Lock() + defer locker.Unlock() + if _, ok := made[name+storageName]; ok { + return nil + } + made[name+storageName] = true pvc := &Storage{} pvc.K8sBase = NewBase() pvc.K8sBase.Kind = "PersistentVolumeClaim" pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName pvc.K8sBase.ApiVersion = "v1" - pvc.K8sBase.Metadata.Name = RELEASE_NAME + "-" + storageName + pvc.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + storageName pvc.K8sBase.Metadata.Labels[K+"/component"] = name pvc.Spec = &PVCSpec{ Resouces: map[string]interface{}{ @@ -24,6 +47,7 @@ func NewPVC(name, storageName string) *Storage { return pvc } +// PVCSpec is a struct for a PersistentVolumeClaim spec. type PVCSpec struct { Resouces map[string]interface{} `yaml:"resources"` AccessModes []string `yaml:"accessModes"` diff --git a/helm/types.go b/helm/types.go index bf4ccfc..9fcd3d7 100644 --- a/helm/types.go +++ b/helm/types.go @@ -1,122 +1,36 @@ package helm import ( - "bytes" - "crypto/sha1" - "fmt" - "io/ioutil" "os" "strings" - "text/template" ) const K = "katenary.io" -const RELEASE_NAME = "{{ .Release.Name }}" -const ( - LABEL_ENV_SECRET = K + "/secret-envfiles" - LABEL_PORT = K + "/ports" - LABEL_INGRESS = K + "/ingress" - LABEL_ENV_SERVICE = K + "/env-to-service" - LABEL_VOL_CM = K + "/configmap-volumes" - LABEL_HEALTHCHECK = K + "/healthcheck" - LABEL_SAMEPOD = K + "/same-pod" - LABEL_EMPTYDIRS = K + "/empty-dirs" -) - -func GetLabelsDocumentation() string { - t, _ := template.New("labels").Parse(` -# Labels -{{.LABEL_ENV_SECRET | printf "%-33s"}}: set the given file names as a secret instead of configmap -{{.LABEL_PORT | printf "%-33s"}}: set the ports to expose as a service (coma separated) -{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress (coma separated) -{{.LABEL_ENV_SERVICE | printf "%-33s"}}: specifies that the environment variable points on a service name (coma separated) -{{.LABEL_VOL_CM | printf "%-33s"}}: specifies that the volumes points on a configmap (coma separated) -{{.LABEL_SAMEPOD | printf "%-33s"}}: specifies that the pod should be deployed in the same pod than the given service name -{{.LABEL_EMPTYDIRS | printf "%-33s"}}: specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated) -{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**. -{{ printf "%-34s" ""}} You can use these form of label values: -{{ printf "%-35s" ""}}- "http://[not used address][:port][/path]" to specify an http healthcheck -{{ printf "%-35s" ""}}- "tcp://[not used address]:port" to specify a tcp healthcheck -{{ printf "%-35s" ""}}- other string is condidered as a "command" healthcheck - `) - buff := bytes.NewBuffer(nil) - t.Execute(buff, map[string]string{ - "LABEL_ENV_SECRET": LABEL_ENV_SECRET, - "LABEL_ENV_SERVICE": LABEL_ENV_SERVICE, - "LABEL_PORT": LABEL_PORT, - "LABEL_INGRESS": LABEL_INGRESS, - "LABEL_VOL_CM": LABEL_VOL_CM, - "LABEL_HEALTHCHECK": LABEL_HEALTHCHECK, - "LABEL_SAMEPOD": LABEL_SAMEPOD, - "LABEL_EMPTYDIRS": LABEL_EMPTYDIRS, - }) - return buff.String() -} var ( - Appname = "" + Appname = "" // set at runtime Version = "1.0" // should be set from main.Version ) +// Kinded represent an object with a kind. type Kinded interface { + // Get must resturn the kind name. Get() string } +// Signable represents an object with a signature. type Signable interface { + // BuildSHA must return the signature. BuildSHA(filename string) } +// Named represents an object with a name. type Named interface { + // Name must return the name of the object (from metadata). Name() string } -type Metadata struct { - Name string `yaml:"name,omitempty"` - Labels map[string]string `yaml:"labels"` - Annotations map[string]string `yaml:"annotations,omitempty"` -} - -func NewMetadata() *Metadata { - return &Metadata{ - Name: "", - Labels: make(map[string]string), - Annotations: make(map[string]string), - } -} - -type K8sBase struct { - ApiVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata *Metadata `yaml:"metadata"` -} - -func NewBase() *K8sBase { - - b := &K8sBase{ - Metadata: NewMetadata(), - } - // add some information of the build - b.Metadata.Labels[K+"/project"] = GetProjectName() - b.Metadata.Labels[K+"/release"] = RELEASE_NAME - b.Metadata.Annotations[K+"/version"] = Version - return b -} - -func (k *K8sBase) BuildSHA(filename string) { - c, _ := ioutil.ReadFile(filename) - //sum := sha256.Sum256(c) - sum := sha1.Sum(c) - k.Metadata.Annotations[K+"/docker-compose-sha1"] = fmt.Sprintf("%x", string(sum[:])) -} - -func (k *K8sBase) Get() string { - return k.Kind -} - -func (k *K8sBase) Name() string { - return k.Metadata.Name -} - +// GetProjectName returns the name of the project. func GetProjectName() string { if len(Appname) > 0 { return Appname