Feat cronjob (#23)
Make possible to declare cronTabs inside docker-compose file. ⇒ Also, add multiple compose file injection with `-c` arguments ⇒ Also, fixes “ignore depends on” for same pod ⇒ Also fixes * fix [Be able to specify compose.yml files and its override #21](https://github.com/metal3d/katenary/issues/21) * fix [Be able to ignore ports to expose in a katenary.io/ports list #16](https://github.com/metal3d/katenary/issues/16) And more fixes… (later, we will use branches in a better way, that was a hard, long fix process)
This commit is contained in:
110
generator/crontabs.go
Normal file
110
generator/crontabs.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"log"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
cronMulti = `pods=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[*].metadata.name}')`
|
||||
cronMultiCmd = `
|
||||
for pod in $pods; do
|
||||
kubectl exec -i $pod -c %s -- sh -c %s
|
||||
done`
|
||||
cronSingle = `pod=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[0].metadata.name}')`
|
||||
cronCmd = `
|
||||
kubectl exec -i $pod -c %s -- sh -c %s`
|
||||
)
|
||||
|
||||
type CronDef struct {
|
||||
Command string `yaml:"command"`
|
||||
Schedule string `yaml:"schedule"`
|
||||
Image string `yaml:"image"`
|
||||
Multi bool `yaml:"allPods,omitempty"`
|
||||
}
|
||||
|
||||
func buildCrontab(deployName string, deployment *helm.Deployment, s *types.ServiceConfig, fileGeneratorChan HelmFileGenerator) {
|
||||
// get the cron label from the service
|
||||
var crondef string
|
||||
var ok bool
|
||||
if crondef, ok = s.Labels[helm.LABEL_CRON]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// parse yaml
|
||||
crons := []CronDef{}
|
||||
err := yaml.Unmarshal([]byte(crondef), &crons)
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
if len(crons) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// create a serviceAccount
|
||||
sa := helm.NewServiceAccount(deployName)
|
||||
// create a role
|
||||
role := helm.NewCronRole(deployName)
|
||||
|
||||
// create a roleBinding
|
||||
roleBinding := helm.NewRoleBinding(deployName, sa, role)
|
||||
|
||||
// make generation
|
||||
logger.Magenta(ICON_RBAC, "Generating ServiceAccount, Role and RoleBinding for cron jobs", deployName)
|
||||
fileGeneratorChan <- sa
|
||||
fileGeneratorChan <- role
|
||||
fileGeneratorChan <- roleBinding
|
||||
|
||||
numcron := len(crons) - 1
|
||||
index := 1
|
||||
|
||||
// create crontabs
|
||||
for _, cron := range crons {
|
||||
escaped := shellescape.Quote(cron.Command)
|
||||
var cmd, podget string
|
||||
if cron.Multi {
|
||||
podget = cronMulti
|
||||
cmd = cronMultiCmd
|
||||
} else {
|
||||
podget = cronSingle
|
||||
cmd = cronCmd
|
||||
}
|
||||
podget = fmt.Sprintf(podget, helm.K, deployName, helm.K)
|
||||
cmd = fmt.Sprintf(cmd, s.Name, escaped)
|
||||
cmd = podget + cmd
|
||||
|
||||
if cron.Image == "" {
|
||||
cron.Image = `bitnami/kubectl:{{ printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor }}`
|
||||
}
|
||||
|
||||
name := deployName
|
||||
if numcron > 0 {
|
||||
name = fmt.Sprintf("%s-%d", deployName, index)
|
||||
}
|
||||
|
||||
// add crontab
|
||||
suffix := ""
|
||||
if numcron > 0 {
|
||||
suffix = fmt.Sprintf("%d", index)
|
||||
}
|
||||
cronTab := helm.NewCrontab(
|
||||
name,
|
||||
cron.Image,
|
||||
cmd,
|
||||
cron.Schedule,
|
||||
sa,
|
||||
)
|
||||
logger.Magenta(ICON_CRON, "Generating crontab", deployName, suffix)
|
||||
fileGeneratorChan <- cronTab
|
||||
index++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -6,6 +6,7 @@ import (
|
||||
"katenary/compose"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"katenary/tools"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -28,6 +29,8 @@ const (
|
||||
ICON_CONF = "📝"
|
||||
ICON_STORE = "⚡"
|
||||
ICON_INGRESS = "🌐"
|
||||
ICON_RBAC = "🔑"
|
||||
ICON_CRON = "🕒"
|
||||
)
|
||||
|
||||
// Values is kept in memory to create a values.yaml file.
|
||||
@@ -71,6 +74,7 @@ func buildDeployment(name string, s *types.ServiceConfig, linked map[string]type
|
||||
|
||||
// Add selectors
|
||||
selectors := buildSelector(name, s)
|
||||
selectors[helm.K+"/resource"] = "deployment"
|
||||
deployment.Spec.Selector = map[string]interface{}{
|
||||
"matchLabels": selectors,
|
||||
}
|
||||
@@ -270,7 +274,7 @@ func buildConfigMapFromPath(name, path string) *helm.ConfigMap {
|
||||
files[filename] = string(c)
|
||||
}
|
||||
|
||||
cm := helm.NewConfigMap(name, GetRelPath(path))
|
||||
cm := helm.NewConfigMap(name, tools.GetRelPath(path))
|
||||
cm.Data = files
|
||||
return cm
|
||||
}
|
||||
@@ -335,7 +339,7 @@ func prepareVolumes(deployment, name string, s *types.ServiceConfig, container *
|
||||
|
||||
isConfigMap := false
|
||||
for _, cmVol := range configMapsVolumes {
|
||||
if GetRelPath(volname) == cmVol {
|
||||
if tools.GetRelPath(volname) == cmVol {
|
||||
isConfigMap = true
|
||||
break
|
||||
}
|
||||
@@ -364,10 +368,10 @@ func prepareVolumes(deployment, name string, s *types.ServiceConfig, container *
|
||||
|
||||
// the volume is a path and it's explicitally asked to be a configmap in labels
|
||||
cm := buildConfigMapFromPath(name, volname)
|
||||
cm.K8sBase.Metadata.Name = helm.ReleaseNameTpl + "-" + name + "-" + PathToName(volname)
|
||||
cm.K8sBase.Metadata.Name = helm.ReleaseNameTpl + "-" + name + "-" + tools.PathToName(volname)
|
||||
|
||||
// build a configmapRef for this volume
|
||||
volname := PathToName(volname)
|
||||
volname := tools.PathToName(volname)
|
||||
volumes = append(volumes, map[string]interface{}{
|
||||
"name": volname,
|
||||
"configMap": map[string]string{
|
||||
@@ -584,7 +588,7 @@ func prepareEnvFromFiles(name string, s *types.ServiceConfig, container *helm.Co
|
||||
|
||||
// manage environment files (env_file in compose)
|
||||
for _, envfile := range s.EnvFile {
|
||||
f := PathToName(envfile)
|
||||
f := tools.PathToName(envfile)
|
||||
f = strings.ReplaceAll(f, ".env", "")
|
||||
isSecret := false
|
||||
for _, s := range secretsFiles {
|
||||
@@ -795,7 +799,14 @@ func setSecretVar(name string, s *types.ServiceConfig, c *helm.Container) *helm.
|
||||
|
||||
// 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 {
|
||||
func newContainerForDeployment(
|
||||
deployName, containerName string,
|
||||
deployment *helm.Deployment,
|
||||
s *types.ServiceConfig,
|
||||
fileGeneratorChan HelmFileGenerator) *helm.Container {
|
||||
|
||||
buildCrontab(deployName, deployment, s, fileGeneratorChan)
|
||||
|
||||
container := helm.NewContainer(containerName, s.Image, s.Environment, s.Labels)
|
||||
|
||||
applyEnvMapLabel(s, container)
|
||||
@@ -841,6 +852,33 @@ func newContainerForDeployment(deployName, containerName string, deployment *hel
|
||||
prepareInitContainers(containerName, s, container)...,
|
||||
)
|
||||
|
||||
// check if there is containerPort assigned in label, add it, and do
|
||||
// not create service for this.
|
||||
if ports, ok := s.Labels[helm.LABEL_CONTAINER_PORT]; ok {
|
||||
for _, port := range strings.Split(ports, ",") {
|
||||
func(port string, container *helm.Container, s *types.ServiceConfig) {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
return
|
||||
}
|
||||
portNumber, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// avoid already declared ports
|
||||
for _, p := range s.Ports {
|
||||
if int(p.Target) == portNumber {
|
||||
return
|
||||
}
|
||||
}
|
||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||
Name: deployName + "-" + port,
|
||||
ContainerPort: portNumber,
|
||||
})
|
||||
}(port, container, s)
|
||||
}
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
@@ -867,7 +905,7 @@ func addVolumeFrom(deployment *helm.Deployment, container *helm.Container, s *ty
|
||||
for name, volumes := range volumesFrom {
|
||||
for volumeName := range volumes {
|
||||
initianame := volumeName
|
||||
volumeName = PathToName(volumeName)
|
||||
volumeName = tools.PathToName(volumeName)
|
||||
// get the volume from the deployment container "name"
|
||||
var ctn *helm.Container
|
||||
for _, c := range deployment.Spec.Template.Spec.Containers {
|
||||
|
@@ -127,7 +127,7 @@ func setUp(t *testing.T) (string, *compose.Parser) {
|
||||
}
|
||||
|
||||
composefile := filepath.Join(tmpwork, "docker-compose.yaml")
|
||||
p := compose.NewParser(composefile, DOCKER_COMPOSE_YML)
|
||||
p := compose.NewParser([]string{composefile}, DOCKER_COMPOSE_YML)
|
||||
|
||||
// create envfile for "useenvfile" service
|
||||
err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777)
|
||||
|
@@ -1,22 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/compose"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_PathToName(t *testing.T) {
|
||||
path := compose.GetCurrentDir() + "/envéfoo.file"
|
||||
name := PathToName(path)
|
||||
if name != "env-foo-file" {
|
||||
t.Error("Expected env-foo-file, got ", name)
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import (
|
||||
"katenary/compose"
|
||||
"katenary/generator/writers"
|
||||
"katenary/helm"
|
||||
"katenary/tools"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -128,6 +129,7 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVers
|
||||
n := service.Name
|
||||
if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name {
|
||||
linked[n] = service
|
||||
delete(s.DependsOn, n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,15 +175,17 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVers
|
||||
|
||||
case *helm.ConfigMap, *helm.Secret:
|
||||
// there could be several files, so let's force the filename
|
||||
name := c.(helm.Named).Name() + "-" + c.GetType()
|
||||
name := c.(helm.Named).Name() + "." + c.GetType()
|
||||
suffix := c.GetPathRessource()
|
||||
suffix = PathToName(suffix)
|
||||
suffix = tools.PathToName(suffix)
|
||||
name += suffix
|
||||
name = PrefixRE.ReplaceAllString(name, "")
|
||||
writers.BuildConfigMap(c, kind, n, name, templatesDir)
|
||||
|
||||
default:
|
||||
fname := filepath.Join(templatesDir, n+"."+kind+".yaml")
|
||||
name := c.(helm.Named).Name() + "." + c.GetType()
|
||||
name = PrefixRE.ReplaceAllString(name, "")
|
||||
fname := filepath.Join(templatesDir, name+".yaml")
|
||||
fp, err := os.Create(fname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
Reference in New Issue
Block a user