From 5a4d9e396d42467983aa33a5c004f11f7c82057d Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Mon, 14 Feb 2022 14:37:09 +0100 Subject: [PATCH] Add healtcheck + some fixes - better docs - add healtcheck based on docker-compoe commands or labels - fix some problems on secret names - better dependency check --- compose/types.go | 10 + generator/main.go | 503 +++++++++++++++++++++++++++------------------ go.mod | 5 +- go.sum | 2 + helm/deployment.go | 47 ++++- helm/types.go | 29 +++ main.go | 8 + 7 files changed, 395 insertions(+), 209 deletions(-) diff --git a/compose/types.go b/compose/types.go index db540b5..279fd12 100644 --- a/compose/types.go +++ b/compose/types.go @@ -15,6 +15,15 @@ func NewCompose() *Compose { return c } +// HealthCheck manage generic type to handle TCP, HTTP and TCP health check. +type HealthCheck struct { + Test []string `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"` @@ -26,4 +35,5 @@ type Service struct { Expose []int `yaml:"expose"` EnvFiles []string `yaml:"env_file"` RawBuild interface{} `yaml:"build"` + HealthCheck *HealthCheck `yaml:"healthcheck"` } diff --git a/generator/main.go b/generator/main.go index 26d6ad3..30f73fe 100644 --- a/generator/main.go +++ b/generator/main.go @@ -6,6 +6,7 @@ import ( "katenary/compose" "katenary/helm" "log" + "net/url" "os" "path/filepath" "strconv" @@ -14,6 +15,8 @@ import ( "time" "errors" + + "github.com/google/shlex" ) var servicesMap = make(map[string]int) @@ -42,8 +45,7 @@ OK=0 echo "Checking __service__ port" while [ $OK != 1 ]; do echo -n "." - nc -z ` + RELEASE_NAME + `-__service__ __port__ && OK=1 - sleep 1 + nc -z ` + RELEASE_NAME + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1 done echo echo "Done" @@ -63,53 +65,8 @@ func parseService(name string, s *compose.Service, ret chan interface{}) { o := helm.NewDeployment(name) container := helm.NewContainer(name, s.Image, s.Environment, s.Labels) - // prepare secrets - secretsFiles := make([]string, 0) - if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok { - secretsFiles = strings.Split(v, ",") - } - - // manage environment files (env_file in compose) - for _, envfile := range s.EnvFiles { - f := strings.ReplaceAll(envfile, "_", "-") - f = strings.ReplaceAll(f, ".env", "") - f = strings.ReplaceAll(f, ".", "") - f = strings.ReplaceAll(f, "/", "") - cf := f + "-" + name - isSecret := false - for _, s := range secretsFiles { - if s == envfile { - isSecret = true - } - } - var store helm.InlineConfig - if !isSecret { - Bluef(ICON_CONF+" Generating configMap %s\n", cf) - store = helm.NewConfigMap(cf) - } else { - Bluef(ICON_SECRET+" Generating secret %s\n", cf) - store = helm.NewSecret(cf) - } - if err := store.AddEnvFile(envfile); err != nil { - ActivateColors = true - Red(err.Error()) - ActivateColors = false - os.Exit(2) - } - - section := "configMapRef" - if isSecret { - section = "secretRef" - } - - container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ - section: { - "name": store.Metadata().Name, - }, - }) - - ret <- store - } + // prepare cm and secrets + prepareEnvFromFiles(name, s, container, ret) // check the image, and make it "variable" in values.yaml container.Image = "{{ .Values." + name + ".image }}" @@ -117,164 +74,30 @@ func parseService(name string, s *compose.Service, ret chan interface{}) { "image": s.Image, } + // manage the healthcheck property, if any + prepareProbes(name, s, container) // manage ports - 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) - } - } - container.Ports = append(container.Ports, &helm.ContainerPort{ - Name: portName, - ContainerPort: portNumber, - }) - exists[portNumber] = name - } + generateContainerPorts(s, name, container) - // manage the "expose" section to be a NodePort in Kubernetes - for _, port := range s.Expose { - if _, exist := exists[port]; exist { - continue - } - container.Ports = append(container.Ports, &helm.ContainerPort{ - Name: name, - ContainerPort: port, - }) - } - - // Prepare volumes - 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 _, volume := range s.Volumes { - parts := strings.Split(volume, ":") - volname := parts[0] - volepath := parts[1] - - isCM := false - for _, cmVol := range configMapsVolumes { - cmVol = strings.TrimSpace(cmVol) - if volname == cmVol { - isCM = true - break - } - } - - if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) { - // local volume cannt be mounted - ActivateColors = true - Redf("You cannot, at this time, have local volume in %s deployment\n", name) - ActivateColors = false - continue - } - if isCM { - // 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, ".", "-") - cm.K8sBase.Metadata.Name = RELEASE_NAME + "-" + volname + "-" + name - // build a configmap from the volume path - volumes = append(volumes, map[string]interface{}{ - "name": volname, - "configMap": map[string]string{ - "name": cm.K8sBase.Metadata.Name, - }, - }) - mountPoints = append(mountPoints, map[string]interface{}{ - "name": volname, - "mountPath": volepath, - }) - ret <- cm - } else { - - // rmove minus sign from volume name - volname = strings.ReplaceAll(volname, "-", "") - - pvc := helm.NewPVC(name, volname) - volumes = append(volumes, map[string]interface{}{ - "name": volname, - "persistentVolumeClaim": map[string]string{ - "claimName": RELEASE_NAME + "-" + volname, - }, - }) - mountPoints = append(mountPoints, map[string]interface{}{ - "name": volname, - "mountPath": volepath, - }) - - Yellow(ICON_STORE+" Generate volume values for ", volname, " in deployment ", name) - locker.Lock() - if _, ok := VolumeValues[name]; !ok { - VolumeValues[name] = make(map[string]map[string]interface{}) - } - VolumeValues[name][volname] = map[string]interface{}{ - "enabled": false, - "capacity": "1Gi", - } - locker.Unlock() - ret <- pvc - } - } - container.VolumeMounts = mountPoints - - o.Spec.Template.Spec.Volumes = volumes + // Set the container to the deployment o.Spec.Template.Spec.Containers = []*helm.Container{container} - // Add some labels + // Prepare volumes + o.Spec.Template.Spec.Volumes = prepareVolumes(name, s, container, ret) + + // Add selectors + selectors := buildSelector(name, s) o.Spec.Selector = map[string]interface{}{ - "matchLabels": buildSelector(name, s), + "matchLabels": selectors, } - o.Spec.Template.Metadata.Labels = buildSelector(name, s) + o.Spec.Template.Metadata.Labels = selectors - // Now, for "depends_on" section, it's a bit tricky... - // We need to detect "others" services, but we probably not have parsed them yet, so - // we will wait for them for a while. - initContainers := make([]*helm.Container, 0) - for _, dp := range s.DependsOn { - c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels) - command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp) + // 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) - foundPort := -1 - if defaultPort, err := getPort(dp); err != nil { - // BUG: Sometimes the chan remains opened - foundPort = <-waitPort(dp) - } else { - foundPort = defaultPort - } - if foundPort == -1 { - log.Fatalf( - "ERROR, the %s service is waiting for %s port number, "+ - "but it is never discovered. You must declare at least one port in "+ - "the \"ports\" section of the service in the docker-compose file", - name, - dp, - ) - } - command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort)) - - c.Command = []string{ - "sh", - "-c", - command, - } - initContainers = append(initContainers, c) - } - o.Spec.Template.Spec.InitContainers = initContainers - - // Then, create services for "ports" and "expose" section + // 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 createService(name, s) { + for _, s := range generateServicesAndIngresses(name, s) { ret <- s } } @@ -283,7 +106,7 @@ func parseService(name string, s *compose.Service, ret chan interface{}) { // 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 **futur** waiters that this service is not exposed + // alert any current or **future** waiters that this service is not exposed go func() { for { select { @@ -314,9 +137,9 @@ func parseService(name string, s *compose.Service, ret chan interface{}) { } // Create a service (k8s). -func createService(name string, s *compose.Service) []interface{} { +func generateServicesAndIngresses(name string, s *compose.Service) []interface{} { - ret := make([]interface{}, 0) + ret := make([]interface{}, 0) // can handle helm.Service or helm.Ingress Magenta(ICON_SERVICE+" Generating service for ", name) ks := helm.NewService(name) @@ -434,6 +257,7 @@ func waitPort(name string) chan int { return c } +// Build the selector for the service. func buildSelector(name string, s *compose.Service) map[string]string { return map[string]string{ "katenary.io/component": name, @@ -441,6 +265,7 @@ func buildSelector(name string, s *compose.Service) map[string]string { } } +// buildCMFromPath generates a ConfigMap from a path. func buildCMFromPath(path string) *helm.ConfigMap { stat, err := os.Stat(path) if err != nil { @@ -472,3 +297,279 @@ func buildCMFromPath(path string) *helm.ConfigMap { cm.Data = files return cm } + +// generateContainerPorts add the container ports of a service. +func generateContainerPorts(s *compose.Service, 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) + } + } + container.Ports = append(container.Ports, &helm.ContainerPort{ + Name: portName, + ContainerPort: portNumber, + }) + exists[portNumber] = name + } + + // manage the "expose" section to be a NodePort in Kubernetes + for _, port := range s.Expose { + if _, exist := exists[port]; exist { + continue + } + container.Ports = append(container.Ports, &helm.ContainerPort{ + Name: name, + ContainerPort: port, + }) + } +} + +// prepareVolumes add the volumes of a service. +func prepareVolumes(name string, s *compose.Service, container *helm.Container, ret chan interface{}) []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 _, volume := range s.Volumes { + parts := strings.Split(volume, ":") + volname := parts[0] + volepath := parts[1] + + isCM := false + for _, cmVol := range configMapsVolumes { + cmVol = strings.TrimSpace(cmVol) + if volname == cmVol { + isCM = true + break + } + } + + if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) { + // local volume cannt be mounted + ActivateColors = true + Redf("You cannot, at this time, have local volume in %s deployment\n", name) + ActivateColors = false + continue + } + if isCM { + // 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, ".", "-") + cm.K8sBase.Metadata.Name = RELEASE_NAME + "-" + volname + "-" + name + // build a configmap from the volume path + volumes = append(volumes, map[string]interface{}{ + "name": volname, + "configMap": map[string]string{ + "name": cm.K8sBase.Metadata.Name, + }, + }) + mountPoints = append(mountPoints, map[string]interface{}{ + "name": volname, + "mountPath": volepath, + }) + ret <- cm + } else { + + // rmove minus sign from volume name + volname = strings.ReplaceAll(volname, "-", "") + + pvc := helm.NewPVC(name, volname) + volumes = append(volumes, map[string]interface{}{ + "name": volname, + "persistentVolumeClaim": map[string]string{ + "claimName": RELEASE_NAME + "-" + volname, + }, + }) + mountPoints = append(mountPoints, map[string]interface{}{ + "name": volname, + "mountPath": volepath, + }) + + Yellow(ICON_STORE+" Generate volume values for ", volname, " in deployment ", name) + locker.Lock() + if _, ok := VolumeValues[name]; !ok { + VolumeValues[name] = make(map[string]map[string]interface{}) + } + VolumeValues[name][volname] = map[string]interface{}{ + "enabled": false, + "capacity": "1Gi", + } + locker.Unlock() + ret <- pvc + } + } + 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 { + + // 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 { + 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) + } else { + foundPort = defaultPort + } + if foundPort == -1 { + log.Fatalf( + "ERROR, the %s service is waiting for %s port number, "+ + "but it is never discovered. You must declare at least one port in "+ + "the \"ports\" section of the service in the docker-compose file", + name, + dp, + ) + } + command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort)) + + c.Command = []string{ + "sh", + "-c", + command, + } + initContainers = append(initContainers, c) + } + return initContainers +} + +// 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) + } + + probe := helm.NewProbe(int(interval.Seconds()), int(initialDelaySeconds.Seconds()), 1, s.HealthCheck.Retries) + + healthCheckLabel := s.Labels[helm.LABEL_HEALTHCHECK] + + if healthCheckLabel != "" { + + path := "/" + port := 80 + + u, err := url.Parse(healthCheckLabel) + if err == nil { + path = u.Path + port, _ = strconv.Atoi(u.Port()) + } else { + path = "/" + port = 80 + } + + 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{ + + Command: c, + } + } + } else if s.HealthCheck.Test[0] == "CMD" { + probe.Exec = &helm.Exec{ + Command: s.HealthCheck.Test[1:], + } + } + container.LivenessProbe = probe + } +} + +// prepareEnvFromFiles generate configMap or secrets from environment files. +func prepareEnvFromFiles(name string, s *compose.Service, container *helm.Container, ret chan interface{}) { + + // prepare secrets + secretsFiles := make([]string, 0) + if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok { + secretsFiles = strings.Split(v, ",") + } + + // manage environment files (env_file in compose) + for _, envfile := range s.EnvFiles { + f := strings.ReplaceAll(envfile, "_", "-") + f = strings.ReplaceAll(f, ".env", "") + f = strings.ReplaceAll(f, ".", "") + f = strings.ReplaceAll(f, "/", "") + cf := f + "-" + name + isSecret := false + for _, s := range secretsFiles { + if s == envfile { + isSecret = true + } + } + var store helm.InlineConfig + if !isSecret { + Bluef(ICON_CONF+" Generating configMap %s\n", cf) + store = helm.NewConfigMap(cf) + } else { + Bluef(ICON_SECRET+" Generating secret %s\n", cf) + store = helm.NewSecret(cf) + } + if err := store.AddEnvFile(envfile); err != nil { + ActivateColors = true + Red(err.Error()) + ActivateColors = false + os.Exit(2) + } + + section := "configMapRef" + if isSecret { + section = "secretRef" + } + + container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{ + section: { + "name": store.Metadata().Name, + }, + }) + + ret <- store + } +} diff --git a/go.mod b/go.mod index c62950f..8ea47f8 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module katenary go 1.16 -require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +require ( + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +) diff --git a/go.sum b/go.sum index e387ff0..71fe9d3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= diff --git a/helm/deployment.go b/helm/deployment.go index c58f50e..9d6f28b 100644 --- a/helm/deployment.go +++ b/helm/deployment.go @@ -40,13 +40,46 @@ type ContainerPort struct { } 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"` + 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 { diff --git a/helm/types.go b/helm/types.go index 6bfbfea..3d7396d 100644 --- a/helm/types.go +++ b/helm/types.go @@ -1,11 +1,13 @@ package helm import ( + "bytes" "crypto/sha1" "fmt" "io/ioutil" "os" "strings" + "text/template" ) const K = "katenary.io" @@ -16,8 +18,35 @@ const ( LABEL_INGRESS = K + "/ingress" LABEL_ENV_SERVICE = K + "/env-to-service" LABEL_VOL_CM = K + "/configmap-volumes" + LABEL_HEALTHCHECK = K + "/healthcheck" ) +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 port to expose as a service +{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress +{{.LABEL_ENV_SERVICE | printf "%-33s"}}: specifies that the environment variable points on a service name +{{.LABEL_VOL_CM | printf "%-33s"}}: specifies that the volume points on a configmap +{{.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, + }) + return buff.String() +} + var ( Appname = "" Version = "1.0" // should be set from main.Version diff --git a/main.go b/main.go index 5b3faab..7b58572 100644 --- a/main.go +++ b/main.go @@ -88,8 +88,16 @@ func main() { flag.StringVar(&appVersion, "appversion", appVersion, helpMessageForAppversion) version := flag.Bool("version", false, "show version and exit") force := flag.Bool("force", false, "force the removal of the chart-dir") + showLabels := flag.Bool("labels", false, "show possible labels and exit") + flag.Parse() + if *showLabels { + // display labels from helm/types.go + fmt.Println(helm.GetLabelsDocumentation()) + os.Exit(0) + } + // Only display the version if *version { fmt.Println(Version)