Add healtcheck + some fixes
- better docs - add healtcheck based on docker-compoe commands or labels - fix some problems on secret names - better dependency check
This commit is contained in:
@@ -15,6 +15,15 @@ func NewCompose() *Compose {
|
|||||||
return c
|
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.
|
// Service represent a "service" in a docker-compose file.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
@@ -26,4 +35,5 @@ type Service struct {
|
|||||||
Expose []int `yaml:"expose"`
|
Expose []int `yaml:"expose"`
|
||||||
EnvFiles []string `yaml:"env_file"`
|
EnvFiles []string `yaml:"env_file"`
|
||||||
RawBuild interface{} `yaml:"build"`
|
RawBuild interface{} `yaml:"build"`
|
||||||
|
HealthCheck *HealthCheck `yaml:"healthcheck"`
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"katenary/compose"
|
"katenary/compose"
|
||||||
"katenary/helm"
|
"katenary/helm"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/google/shlex"
|
||||||
)
|
)
|
||||||
|
|
||||||
var servicesMap = make(map[string]int)
|
var servicesMap = make(map[string]int)
|
||||||
@@ -42,8 +45,7 @@ OK=0
|
|||||||
echo "Checking __service__ port"
|
echo "Checking __service__ port"
|
||||||
while [ $OK != 1 ]; do
|
while [ $OK != 1 ]; do
|
||||||
echo -n "."
|
echo -n "."
|
||||||
nc -z ` + RELEASE_NAME + `-__service__ __port__ && OK=1
|
nc -z ` + RELEASE_NAME + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1
|
||||||
sleep 1
|
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "Done"
|
echo "Done"
|
||||||
@@ -63,53 +65,8 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
|
|||||||
o := helm.NewDeployment(name)
|
o := helm.NewDeployment(name)
|
||||||
container := helm.NewContainer(name, s.Image, s.Environment, s.Labels)
|
container := helm.NewContainer(name, s.Image, s.Environment, s.Labels)
|
||||||
|
|
||||||
// prepare secrets
|
// prepare cm and secrets
|
||||||
secretsFiles := make([]string, 0)
|
prepareEnvFromFiles(name, s, container, ret)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the image, and make it "variable" in values.yaml
|
// check the image, and make it "variable" in values.yaml
|
||||||
container.Image = "{{ .Values." + name + ".image }}"
|
container.Image = "{{ .Values." + name + ".image }}"
|
||||||
@@ -117,164 +74,30 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
|
|||||||
"image": s.Image,
|
"image": s.Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manage the healthcheck property, if any
|
||||||
|
prepareProbes(name, s, container)
|
||||||
// manage ports
|
// manage ports
|
||||||
exists := make(map[int]string)
|
generateContainerPorts(s, name, container)
|
||||||
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
|
// Set the container to the deployment
|
||||||
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
|
|
||||||
o.Spec.Template.Spec.Containers = []*helm.Container{container}
|
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{}{
|
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...
|
// Now, for "depends_on" section, it's a bit tricky to get dependencies, see the function below.
|
||||||
// We need to detect "others" services, but we probably not have parsed them yet, so
|
o.Spec.Template.Spec.InitContainers = prepareInitContainers(name, s, container)
|
||||||
// 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
|
// Then, create Services and possible Ingresses for ingress labels, "ports" and "expose" section
|
||||||
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
|
|
||||||
if len(s.Ports) > 0 || len(s.Expose) > 0 {
|
if len(s.Ports) > 0 || len(s.Expose) > 0 {
|
||||||
for _, s := range createService(name, s) {
|
for _, s := range generateServicesAndIngresses(name, s) {
|
||||||
ret <- 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
|
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any
|
||||||
// associated service.
|
// associated service.
|
||||||
if len(s.Ports) == 0 {
|
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() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -314,9 +137,9 @@ func parseService(name string, s *compose.Service, ret chan interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a service (k8s).
|
// 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)
|
Magenta(ICON_SERVICE+" Generating service for ", name)
|
||||||
ks := helm.NewService(name)
|
ks := helm.NewService(name)
|
||||||
|
|
||||||
@@ -434,6 +257,7 @@ func waitPort(name string) chan int {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the selector for the service.
|
||||||
func buildSelector(name string, s *compose.Service) map[string]string {
|
func buildSelector(name string, s *compose.Service) map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"katenary.io/component": name,
|
"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 {
|
func buildCMFromPath(path string) *helm.ConfigMap {
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -472,3 +297,279 @@ func buildCMFromPath(path string) *helm.ConfigMap {
|
|||||||
cm.Data = files
|
cm.Data = files
|
||||||
return cm
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5
go.mod
5
go.mod
@@ -2,4 +2,7 @@ module katenary
|
|||||||
|
|
||||||
go 1.16
|
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
|
||||||
|
)
|
||||||
|
2
go.sum
2
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
@@ -40,13 +40,46 @@ type ContainerPort struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
Ports []*ContainerPort `yaml:"ports,omitempty"`
|
Ports []*ContainerPort `yaml:"ports,omitempty"`
|
||||||
Env []Value `yaml:"env,omitempty"`
|
Env []Value `yaml:"env,omitempty"`
|
||||||
EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"`
|
EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"`
|
||||||
Command []string `yaml:"command,omitempty"`
|
Command []string `yaml:"command,omitempty"`
|
||||||
VolumeMounts []interface{} `yaml:"volumeMounts,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 {
|
func NewContainer(name, image string, environment, labels map[string]string) *Container {
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package helm
|
package helm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const K = "katenary.io"
|
const K = "katenary.io"
|
||||||
@@ -16,8 +18,35 @@ const (
|
|||||||
LABEL_INGRESS = K + "/ingress"
|
LABEL_INGRESS = K + "/ingress"
|
||||||
LABEL_ENV_SERVICE = K + "/env-to-service"
|
LABEL_ENV_SERVICE = K + "/env-to-service"
|
||||||
LABEL_VOL_CM = K + "/configmap-volumes"
|
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 (
|
var (
|
||||||
Appname = ""
|
Appname = ""
|
||||||
Version = "1.0" // should be set from main.Version
|
Version = "1.0" // should be set from main.Version
|
||||||
|
8
main.go
8
main.go
@@ -88,8 +88,16 @@ func main() {
|
|||||||
flag.StringVar(&appVersion, "appversion", appVersion, helpMessageForAppversion)
|
flag.StringVar(&appVersion, "appversion", appVersion, helpMessageForAppversion)
|
||||||
version := flag.Bool("version", false, "show version and exit")
|
version := flag.Bool("version", false, "show version and exit")
|
||||||
force := flag.Bool("force", false, "force the removal of the chart-dir")
|
force := flag.Bool("force", false, "force the removal of the chart-dir")
|
||||||
|
showLabels := flag.Bool("labels", false, "show possible labels and exit")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *showLabels {
|
||||||
|
// display labels from helm/types.go
|
||||||
|
fmt.Println(helm.GetLabelsDocumentation())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Only display the version
|
// Only display the version
|
||||||
if *version {
|
if *version {
|
||||||
fmt.Println(Version)
|
fmt.Println(Version)
|
||||||
|
Reference in New Issue
Block a user