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:
2022-06-10 16:15:18 +02:00
committed by GitHub
parent 7203928d95
commit f9fd6332d6
21 changed files with 465 additions and 93 deletions

110
generator/crontabs.go Normal file
View 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
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)