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

3
.gitignore vendored
View File

@@ -1,7 +1,8 @@
dist/* dist/*
.cache/* .cache/*
chart/* chart/*
docker-compose.yaml *.yaml
*.yml
./katenary ./katenary
*.env *.env
docker-compose* docker-compose*

View File

@@ -2,9 +2,9 @@
<img src="./misc/logo.png" alt="Katenary Logo" style="max-width: 90%" align="center"/> <img src="./misc/logo.png" alt="Katenary Logo" style="max-width: 90%" align="center"/>
</div> </div>
Katenary is a tool to help transforming `docker-compose` files to a working Helm Chart for Kubernetes. Katenary is a tool to help to transform `docker-compose` files to a working Helm Chart for Kubernetes.
> **Important Note:** Katenary is a tool to help building Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart. > **Important Note:** Katenary is a tool to help to build Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart.
This project is partially made at [Smile](https://www.smile.eu) This project is partially made at [Smile](https://www.smile.eu)
@@ -59,7 +59,7 @@ Then place the `katenary` binary file inside your PATH.
We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file you use. We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file you use.
E.g. : E.g.:
```bash ```bash
# bash in ~/.bashrc file # bash in ~/.bashrc file
@@ -122,11 +122,11 @@ What can be interpreted by Katenary:
- Services with "image" section (cannot work with "build" section) - Services with "image" section (cannot work with "build" section)
- **Named Volumes** are transformed to persistent volume claims - note that local volume will break the transformation to Helm Chart because there is (for now) no way to make it working (see below for resolution) - **Named Volumes** are transformed to persistent volume claims - note that local volume will break the transformation to Helm Chart because there is (for now) no way to make it working (see below for resolution)
- if `ports` and/or `expose` section, katenary will create Services and bind the port to the corresponding container port - if `ports` and/or `expose` section, katenary will create Services and bind the port to the corresponding container port
- `depends_on` will add init containers to wait for the depending service (using the first port) - `depends_on` will add init containers to wait for the depending on service (using the first port)
- `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now) - `env_file` list will create a configMap object per environemnt file (⚠ to-do: the "to-service" label doesn't work with configMap for now)
- some labels can help to bind values, for example: - some labels can help to bind values, for example:
- `katenary.io/ingress: 80` will expose the port 80 in a ingress - `katenary.io/ingress: 80` will expose the port 80 in an ingress
- `katenary.io/mapenv: |`: allow to map environment to something else than the given value in the compose file - `katenary.io/mapenv: |`: allow mapping environment to something else than the given value in the compose file
Exemple of a possible `docker-compose.yaml` file: Exemple of a possible `docker-compose.yaml` file:
@@ -173,20 +173,32 @@ services:
These labels could be found by `katenary show-labels`, and can be placed as "labels" inside your docker-compose file: 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 # Labels
katenary.io/secret-vars : secret variables to push on a secret file katenary.io/ignore : ignore the container, it will not yied any object in the helm chart (bool)
katenary.io/secret-envfiles : set the given file names as a secret instead of configmap katenary.io/secret-vars : secret variables to push on a secret file (coma separated)
katenary.io/mapenv : map environment variable to a template string (yaml style) katenary.io/secret-envfiles : set the given file names as a secret instead of configmap (coma separated)
katenary.io/mapenv : map environment variable to a template string (yaml style, object)
katenary.io/ports : set the ports to expose as a service (coma separated) 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/ingress : set the port to expose in an ingress (coma separated)
katenary.io/configmap-volumes : specifies that the volumes points on a configmap (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/same-pod : specifies that the pod should be deployed in the same pod than the
katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated) given service name (string)
katenary.io/healthcheck : specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**. katenary.io/volume-from : specifies that the volumes to be mounted from the given service (yaml style)
katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of
persistentVolumeClaim (coma separated)
katenary.io/crontabs : specifies a cronjobs to create (yaml style, array) - this will create a
cronjob, a service account, a role and a rolebinding to start the command with "kubectl"
The form is the following:
- command: the command to run
schedule: the schedule to run the command (e.g. "@daily" or "*/1 * * * *")
image: the image to use for the command (default to "bitnami/kubectl")
allPods: true if you want to run the command on all pods (default to false)
katenary.io/healthcheck : specifies that the container should be monitored by a healthcheck,
**it overrides the docker-compose healthcheck**.
You can use these form of label values: You can use these form of label values:
- "http://[not used address][:port][/path]" to specify an http healthcheck -> http://[ignored][:port][/path] to specify an http healthcheck
- "tcp://[not used address]:port" to specify a tcp healthcheck -> tcp://[ignored]:port to specify a tcp healthcheck
- other string is condidered as a "command" healthcheck -> other string is condidered as a "command" healthcheck
``` ```
# What a name... # What a name...

View File

@@ -50,6 +50,7 @@ func main() {
} }
// convert command, need some flags // convert command, need some flags
var composeFiles *[]string
convertCmd := &cobra.Command{ convertCmd := &cobra.Command{
Use: "convert", Use: "convert",
Short: "Convert docker-compose to helm chart", Short: "Convert docker-compose to helm chart",
@@ -61,9 +62,7 @@ func main() {
"- if it's not defined, so the 0.0.1 version is used", "- if it's not defined, so the 0.0.1 version is used",
Run: func(c *cobra.Command, args []string) { Run: func(c *cobra.Command, args []string) {
force := c.Flag("force").Changed force := c.Flag("force").Changed
// TODO: is there a way to get typed values from cobra?
appversion := c.Flag("app-version").Value.String() appversion := c.Flag("app-version").Value.String()
composeFile := c.Flag("compose-file").Value.String()
appName := c.Flag("app-name").Value.String() appName := c.Flag("app-name").Value.String()
chartVersion := c.Flag("chart-version").Value.String() chartVersion := c.Flag("chart-version").Value.String()
chartDir := c.Flag("output-dir").Value.String() chartDir := c.Flag("output-dir").Value.String()
@@ -71,17 +70,17 @@ func main() {
if err != nil { if err != nil {
writers.IndentSize = indentation writers.IndentSize = indentation
} }
Convert(composeFile, appversion, appName, chartDir, chartVersion, force) Convert(*composeFiles, appversion, appName, chartDir, chartVersion, force)
}, },
} }
composeFiles = convertCmd.Flags().StringArrayP(
"compose-file", "c", []string{ComposeFile}, "compose file to convert, can be use several times to override previous file. Order is important!")
convertCmd.Flags().BoolP( convertCmd.Flags().BoolP(
"force", "f", false, "force overwrite of existing output files") "force", "f", false, "force overwrite of existing output files")
convertCmd.Flags().StringP( convertCmd.Flags().StringP(
"app-version", "a", AppVersion, "app version") "app-version", "a", AppVersion, "app version")
convertCmd.Flags().StringP( convertCmd.Flags().StringP(
"chart-version", "v", ChartVersion, "chart version") "chart-version", "v", ChartVersion, "chart version")
convertCmd.Flags().StringP(
"compose-file", "c", ComposeFile, "docker compose file")
convertCmd.Flags().StringP( convertCmd.Flags().StringP(
"app-name", "n", AppName, "application name") "app-name", "n", AppName, "application name")
convertCmd.Flags().StringP( convertCmd.Flags().StringP(

View File

@@ -93,19 +93,24 @@ func detectGitVersion() (string, error) {
return defaulVersion, errors.New("git log failed") return defaulVersion, errors.New("git log failed")
} }
func Convert(composeFile, appVersion, appName, chartDir, chartVersion string, force bool) { func Convert(composeFile []string, appVersion, appName, chartDir, chartVersion string, force bool) {
if len(composeFile) == 0 { if len(composeFile) == 0 {
fmt.Println("No compose file given") fmt.Println("No compose file given")
return return
} }
_, err := os.Stat(composeFile)
if err != nil { composeFiles := composeFile
fmt.Println("No compose file found") ComposeFile = composeFiles[0]
os.Exit(1)
for _, cf := range composeFiles {
if _, err := os.Stat(cf); err != nil {
fmt.Printf("Compose file %s not found\n", cf)
return
}
} }
// Parse the compose file now // Parse the compose file now
p := compose.NewParser(composeFile) p := compose.NewParser(composeFiles)
p.Parse(appName) p.Parse(appName)
dirname := filepath.Join(chartDir, appName) dirname := filepath.Join(chartDir, appName)

View File

@@ -26,32 +26,37 @@ var (
) )
// 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. // 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 { func NewParser(filename []string, content ...string) *Parser {
p := &Parser{} p := &Parser{}
if len(content) > 0 { // mainly for the tests... if len(content) > 0 { // mainly for the tests...
dir := filepath.Dir(filename) dir := filepath.Dir(filename[0])
err := os.MkdirAll(dir, 0755) err := os.MkdirAll(dir, 0755)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
p.temporary = &dir p.temporary = &dir
ioutil.WriteFile(filename, []byte(content[0]), 0644) ioutil.WriteFile(filename[0], []byte(content[0]), 0644)
cli.DefaultFileNames = []string{filename} cli.DefaultFileNames = filename
} }
// if filename is not in cli Default files, add it // if filename is not in cli Default files, add it
if len(filename) > 0 { if len(filename) > 0 {
found := false found := false
for _, f := range cli.DefaultFileNames { for _, defaultFileName := range cli.DefaultFileNames {
if f == filename { for _, givenFileName := range filename {
if defaultFileName == givenFileName {
found = true found = true
break break
} }
} }
}
// add the file at first position // add the file at first position
if !found { if !found {
cli.DefaultFileNames = append([]string{filename}, cli.DefaultFileNames...) cli.DefaultFileNames = append([]string{filename[0]}, cli.DefaultFileNames...)
}
if len(filename) > 1 {
cli.DefaultOverrideFileNames = append(filename[1:], cli.DefaultOverrideFileNames...)
} }
} }

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/compose"
"katenary/helm" "katenary/helm"
"katenary/logger" "katenary/logger"
"katenary/tools"
"log" "log"
"net/url" "net/url"
"os" "os"
@@ -28,6 +29,8 @@ const (
ICON_CONF = "📝" ICON_CONF = "📝"
ICON_STORE = "⚡" ICON_STORE = "⚡"
ICON_INGRESS = "🌐" ICON_INGRESS = "🌐"
ICON_RBAC = "🔑"
ICON_CRON = "🕒"
) )
// Values is kept in memory to create a values.yaml file. // 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 // Add selectors
selectors := buildSelector(name, s) selectors := buildSelector(name, s)
selectors[helm.K+"/resource"] = "deployment"
deployment.Spec.Selector = map[string]interface{}{ deployment.Spec.Selector = map[string]interface{}{
"matchLabels": selectors, "matchLabels": selectors,
} }
@@ -270,7 +274,7 @@ func buildConfigMapFromPath(name, path string) *helm.ConfigMap {
files[filename] = string(c) files[filename] = string(c)
} }
cm := helm.NewConfigMap(name, GetRelPath(path)) cm := helm.NewConfigMap(name, tools.GetRelPath(path))
cm.Data = files cm.Data = files
return cm return cm
} }
@@ -335,7 +339,7 @@ func prepareVolumes(deployment, name string, s *types.ServiceConfig, container *
isConfigMap := false isConfigMap := false
for _, cmVol := range configMapsVolumes { for _, cmVol := range configMapsVolumes {
if GetRelPath(volname) == cmVol { if tools.GetRelPath(volname) == cmVol {
isConfigMap = true isConfigMap = true
break 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 // the volume is a path and it's explicitally asked to be a configmap in labels
cm := buildConfigMapFromPath(name, volname) 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 // build a configmapRef for this volume
volname := PathToName(volname) volname := tools.PathToName(volname)
volumes = append(volumes, map[string]interface{}{ volumes = append(volumes, map[string]interface{}{
"name": volname, "name": volname,
"configMap": map[string]string{ "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) // manage environment files (env_file in compose)
for _, envfile := range s.EnvFile { for _, envfile := range s.EnvFile {
f := PathToName(envfile) f := tools.PathToName(envfile)
f = strings.ReplaceAll(f, ".env", "") f = strings.ReplaceAll(f, ".env", "")
isSecret := false isSecret := false
for _, s := range secretsFiles { 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, ...). // 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. // 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) container := helm.NewContainer(containerName, s.Image, s.Environment, s.Labels)
applyEnvMapLabel(s, container) applyEnvMapLabel(s, container)
@@ -841,6 +852,33 @@ func newContainerForDeployment(deployName, containerName string, deployment *hel
prepareInitContainers(containerName, s, container)..., 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 return container
} }
@@ -867,7 +905,7 @@ func addVolumeFrom(deployment *helm.Deployment, container *helm.Container, s *ty
for name, volumes := range volumesFrom { for name, volumes := range volumesFrom {
for volumeName := range volumes { for volumeName := range volumes {
initianame := volumeName initianame := volumeName
volumeName = PathToName(volumeName) volumeName = tools.PathToName(volumeName)
// get the volume from the deployment container "name" // get the volume from the deployment container "name"
var ctn *helm.Container var ctn *helm.Container
for _, c := range deployment.Spec.Template.Spec.Containers { 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") 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 // create envfile for "useenvfile" service
err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777) err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777)

View File

@@ -4,6 +4,7 @@ import (
"katenary/compose" "katenary/compose"
"katenary/generator/writers" "katenary/generator/writers"
"katenary/helm" "katenary/helm"
"katenary/tools"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -128,6 +129,7 @@ func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVers
n := service.Name n := service.Name
if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name { if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name {
linked[n] = service 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: case *helm.ConfigMap, *helm.Secret:
// there could be several files, so let's force the filename // 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 := c.GetPathRessource()
suffix = PathToName(suffix) suffix = tools.PathToName(suffix)
name += suffix name += suffix
name = PrefixRE.ReplaceAllString(name, "") name = PrefixRE.ReplaceAllString(name, "")
writers.BuildConfigMap(c, kind, n, name, templatesDir) writers.BuildConfigMap(c, kind, n, name, templatesDir)
default: 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) fp, err := os.Create(fname)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

1
go.mod
View File

@@ -3,6 +3,7 @@ module katenary
go 1.16 go 1.16
require ( require (
github.com/alessio/shellescape v1.4.1
github.com/compose-spec/compose-go v1.2.5 github.com/compose-spec/compose-go v1.2.5
github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 // indirect github.com/distribution/distribution/v3 v3.0.0-20220505155552-985711c1f414 // indirect
github.com/kr/pretty v0.2.0 // indirect github.com/kr/pretty v0.2.0 // indirect

2
go.sum
View File

@@ -16,6 +16,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= 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/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/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 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.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"katenary/tools"
"strings" "strings"
) )
@@ -31,8 +32,7 @@ func NewConfigMap(name, path string) *ConfigMap {
base.Metadata.Name = ReleaseNameTpl + "-" + name base.Metadata.Name = ReleaseNameTpl + "-" + name
base.Metadata.Labels[K+"/component"] = name base.Metadata.Labels[K+"/component"] = name
if path != "" { if path != "" {
//base.Metadata.Labels[K+"/path"] = path base.Metadata.Labels[K+"/path"] = tools.PathToName(path)
base.Metadata.Labels[K+"/path"] = `{{ "` + path + `" | quote }}`
} }
return &ConfigMap{ return &ConfigMap{
K8sBase: base, K8sBase: base,
@@ -97,7 +97,7 @@ func NewSecret(name, path string) *Secret {
base.Metadata.Name = ReleaseNameTpl + "-" + name base.Metadata.Name = ReleaseNameTpl + "-" + name
base.Metadata.Labels[K+"/component"] = name base.Metadata.Labels[K+"/component"] = name
if path != "" { if path != "" {
base.Metadata.Labels[K+"/path"] = `{{ "` + path + `" | quote }}` base.Metadata.Labels[K+"/path"] = tools.PathToName(path)
} }
return &Secret{ return &Secret{
K8sBase: base, K8sBase: base,

70
helm/cronTab.go Normal file
View File

@@ -0,0 +1,70 @@
package helm
type CronTab struct {
*K8sBase `yaml:",inline"`
Spec CronSpec `yaml:"spec"`
}
type CronSpec struct {
Schedule string `yaml:"schedule"`
JobTemplate JobTemplate `yaml:"jobTemplate"`
SuccessfulJobsHistoryLimit int `yaml:"successfulJobsHistoryLimit"`
FailedJobsHistoryLimit int `yaml:"failedJobsHistoryLimit"`
ConcurrencyPolicy string `yaml:"concurrencyPolicy"`
}
type JobTemplate struct {
Spec JobSpecDescription `yaml:"spec"`
}
type JobSpecDescription struct {
Template JobSpecTemplate `yaml:"template"`
}
type JobSpecTemplate struct {
Metadata Metadata `yaml:"metadata"`
Spec Job `yaml:"spec"`
}
type Job struct {
ServiceAccount string `yaml:"serviceAccount,omitempty"`
ServiceAccountName string `yaml:"serviceAccountName,omitempty"`
Containers []Container `yaml:"containers"`
RestartPolicy string `yaml:"restartPolicy,omitempty"`
}
func NewCrontab(name, image, command, schedule string, serviceAccount *ServiceAccount) *CronTab {
cron := &CronTab{
K8sBase: NewBase(),
}
cron.K8sBase.ApiVersion = "batch/v1"
cron.K8sBase.Kind = "CronJob"
cron.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name
cron.K8sBase.Metadata.Labels[K+"/component"] = name
cron.Spec.Schedule = schedule
cron.Spec.SuccessfulJobsHistoryLimit = 3
cron.Spec.FailedJobsHistoryLimit = 3
cron.Spec.ConcurrencyPolicy = "Forbid"
cron.Spec.JobTemplate.Spec.Template.Metadata = Metadata{
Labels: cron.K8sBase.Metadata.Labels,
}
cron.Spec.JobTemplate.Spec.Template.Spec = Job{
ServiceAccount: serviceAccount.Name(),
ServiceAccountName: serviceAccount.Name(),
RestartPolicy: "OnFailure",
}
if command != "" {
cron.AddCommand(command, image, name)
}
return cron
}
// AddCommand adds a command to the cron job
func (c *CronTab) AddCommand(command, image, name string) {
container := Container{
Name: name,
Image: image,
Command: []string{"sh", "-c", command},
}
c.Spec.JobTemplate.Spec.Template.Spec.Containers = append(c.Spec.JobTemplate.Spec.Template.Spec.Containers, container)
}

View File

@@ -12,6 +12,7 @@ func NewDeployment(name string) *Deployment {
d.K8sBase.ApiVersion = "apps/v1" d.K8sBase.ApiVersion = "apps/v1"
d.K8sBase.Kind = "Deployment" d.K8sBase.Kind = "Deployment"
d.K8sBase.Metadata.Labels[K+"/component"] = name d.K8sBase.Metadata.Labels[K+"/component"] = name
d.K8sBase.Metadata.Labels[K+"/resource"] = "deployment"
return d return d
} }
@@ -24,6 +25,13 @@ type DepSpec struct {
func NewDepSpec() *DepSpec { func NewDepSpec() *DepSpec {
return &DepSpec{ return &DepSpec{
Replicas: 1, Replicas: 1,
Template: PodTemplate{
Metadata: Metadata{
Labels: map[string]string{
K + "/resource": "deployment",
},
},
},
} }
} }

View File

@@ -10,6 +10,7 @@ const (
LABEL_MAP_ENV = K + "/mapenv" LABEL_MAP_ENV = K + "/mapenv"
LABEL_ENV_SECRET = K + "/secret-envfiles" LABEL_ENV_SECRET = K + "/secret-envfiles"
LABEL_PORT = K + "/ports" LABEL_PORT = K + "/ports"
LABEL_CONTAINER_PORT = K + "/container-ports"
LABEL_INGRESS = K + "/ingress" LABEL_INGRESS = K + "/ingress"
LABEL_VOL_CM = K + "/configmap-volumes" LABEL_VOL_CM = K + "/configmap-volumes"
LABEL_HEALTHCHECK = K + "/healthcheck" LABEL_HEALTHCHECK = K + "/healthcheck"
@@ -18,6 +19,7 @@ const (
LABEL_EMPTYDIRS = K + "/empty-dirs" LABEL_EMPTYDIRS = K + "/empty-dirs"
LABEL_IGNORE = K + "/ignore" LABEL_IGNORE = K + "/ignore"
LABEL_SECRETVARS = K + "/secret-vars" LABEL_SECRETVARS = K + "/secret-vars"
LABEL_CRON = K + "/crontabs"
//deprecated: use LABEL_MAP_ENV instead //deprecated: use LABEL_MAP_ENV instead
LABEL_ENV_SERVICE = K + "/env-to-service" LABEL_ENV_SERVICE = K + "/env-to-service"
@@ -25,28 +27,38 @@ const (
// GetLabelsDocumentation returns the documentation for the labels. // GetLabelsDocumentation returns the documentation for the labels.
func GetLabelsDocumentation() string { func GetLabelsDocumentation() string {
t, _ := template.New("labels").Parse(` t, _ := template.New("labels").Parse(`# Labels
# Labels {{.LABEL_IGNORE | printf "%-33s"}}: ignore the container, it will not yied any object in the helm chart (bool)
{{.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 (coma separated)
{{.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 (coma separated)
{{.LABEL_ENV_SECRET | printf "%-33s"}}: set the given file names as a secret instead of configmap contaienr in {{.LABEL_MAP_ENV | printf "%-33s"}}: map environment variable to a template string (yaml style, object)
{{.LABEL_MAP_ENV | printf "%-33s"}}: map environment variable to a template string (yaml style) {{.LABEL_PORT | printf "%-33s"}}: set the ports to assign on the container in pod + expose as a service (coma separated)
{{.LABEL_PORT | printf "%-33s"}}: set the ports to expose as a service (coma separated) {{.LABEL_CONTAINER_PORT | printf "%-33s"}}: set the ports to assign on the contaienr in pod but avoid service (coma separated)
{{.LABEL_INGRESS | printf "%-33s"}}: set the port to expose in an ingress (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_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_SAMEPOD | printf "%-33s"}}: specifies that the pod should be deployed in the same pod than the
{{ printf "%-34s" ""} } given service name (string)
{{.LABEL_VOLUMEFROM | printf "%-33s"}}: specifies that the volumes to be mounted from the given service (yaml style) {{.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_EMPTYDIRS | printf "%-33s"}}: specifies that the given volume names should be "emptyDir" instead of
{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**. {{ printf "%-34s" ""} } persistentVolumeClaim (coma separated)
{{.LABEL_CRON | printf "%-33s"}}: specifies a cronjobs to create (yaml style, array) - this will create a
{{ printf "%-34s" ""}} cronjob, a service account, a role and a rolebinding to start the command with "kubectl"
{{ printf "%-34s" ""}} The form is the following:
{{ printf "%-34s" ""}} - command: the command to run
{{ printf "%-34s" ""}} schedule: the schedule to run the command (e.g. "@daily" or "*/1 * * * *")
{{ printf "%-34s" ""}} image: the image to use for the command (default to "bitnami/kubectl")
{{ printf "%-34s" ""}} allPods: true if you want to run the command on all pods (default to false)
{{.LABEL_HEALTHCHECK | printf "%-33s"}}: specifies that the container should be monitored by a healthcheck,
{{ printf "%-34s" ""}} **it overrides the docker-compose healthcheck**.
{{ printf "%-34s" ""}} You can use these form of label values: {{ 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" ""}} -> http://[ignored][:port][/path] to specify an http healthcheck
{{ printf "%-35s" ""}}- "tcp://[not used address]:port" to specify a tcp healthcheck {{ printf "%-35s" ""}} -> tcp://[ignored]:port to specify a tcp healthcheck
{{ printf "%-35s" ""}}- other string is condidered as a "command" healthcheck {{ printf "%-35s" ""}} -> other string is condidered as a "command" healthcheck`)
`)
buff := bytes.NewBuffer(nil) buff := bytes.NewBuffer(nil)
t.Execute(buff, map[string]string{ t.Execute(buff, map[string]string{
"LABEL_ENV_SECRET": LABEL_ENV_SECRET, "LABEL_ENV_SECRET": LABEL_ENV_SECRET,
"LABEL_PORT": LABEL_PORT, "LABEL_PORT": LABEL_PORT,
"LABEL_CONTAINER_PORT": LABEL_CONTAINER_PORT,
"LABEL_INGRESS": LABEL_INGRESS, "LABEL_INGRESS": LABEL_INGRESS,
"LABEL_VOL_CM": LABEL_VOL_CM, "LABEL_VOL_CM": LABEL_VOL_CM,
"LABEL_HEALTHCHECK": LABEL_HEALTHCHECK, "LABEL_HEALTHCHECK": LABEL_HEALTHCHECK,
@@ -56,6 +68,7 @@ func GetLabelsDocumentation() string {
"LABEL_IGNORE": LABEL_IGNORE, "LABEL_IGNORE": LABEL_IGNORE,
"LABEL_MAP_ENV": LABEL_MAP_ENV, "LABEL_MAP_ENV": LABEL_MAP_ENV,
"LABEL_SECRETVARS": LABEL_SECRETVARS, "LABEL_SECRETVARS": LABEL_SECRETVARS,
"LABEL_CRON": LABEL_CRON,
}) })
return buff.String() return buff.String()
} }

38
helm/role.go Normal file
View File

@@ -0,0 +1,38 @@
package helm
type Rule struct {
ApiGroup []string `yaml:"apiGroups,omitempty"`
Resources []string `yaml:"resources,omitempty"`
Verbs []string `yaml:"verbs,omitempty"`
}
type Role struct {
*K8sBase `yaml:",inline"`
Rules []Rule `yaml:"rules,omitempty"`
}
func NewCronRole(name string) *Role {
role := &Role{
K8sBase: NewBase(),
}
role.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-executor"
role.K8sBase.Kind = "Role"
role.K8sBase.ApiVersion = "rbac.authorization.k8s.io/v1"
role.K8sBase.Metadata.Labels[K+"/component"] = name
role.Rules = []Rule{
{
ApiGroup: []string{""},
Resources: []string{"pods", "pods/log"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
ApiGroup: []string{""},
Resources: []string{"pods/exec"},
Verbs: []string{"create"},
},
}
return role
}

44
helm/roleBinding.go Normal file
View File

@@ -0,0 +1,44 @@
package helm
type RoleRef struct {
Kind string `yaml:"kind"`
Name string `yaml:"name"`
APIGroup string `yaml:"apiGroup"`
}
type Subject struct {
Kind string `yaml:"kind"`
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
}
type RoleBinding struct {
*K8sBase `yaml:",inline"`
RoleRef RoleRef `yaml:"roleRef,omitempty"`
Subjects []Subject `yaml:"subjects,omitempty"`
}
func NewRoleBinding(name string, user *ServiceAccount, role *Role) *RoleBinding {
rb := &RoleBinding{
K8sBase: NewBase(),
}
rb.K8sBase.Kind = "RoleBinding"
rb.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-allow"
rb.K8sBase.ApiVersion = "rbac.authorization.k8s.io/v1"
rb.K8sBase.Metadata.Labels[K+"/component"] = name
rb.RoleRef.Kind = "Role"
rb.RoleRef.Name = role.Metadata.Name
rb.RoleRef.APIGroup = "rbac.authorization.k8s.io"
rb.Subjects = []Subject{
{
Kind: "ServiceAccount",
Name: user.Metadata.Name,
Namespace: "{{ .Release.Namespace }}",
},
}
return rb
}

View File

@@ -1,5 +1,7 @@
package helm package helm
import "strconv"
// Service is a Kubernetes service. // Service is a Kubernetes service.
type Service struct { type Service struct {
*K8sBase `yaml:",inline"` *K8sBase `yaml:",inline"`
@@ -24,6 +26,7 @@ type ServicePort struct {
Protocol string `yaml:"protocol"` Protocol string `yaml:"protocol"`
Port int `yaml:"port"` Port int `yaml:"port"`
TargetPort int `yaml:"targetPort"` TargetPort int `yaml:"targetPort"`
Name string `yaml:"name"`
} }
// NewServicePort creates a new initialized service port. // NewServicePort creates a new initialized service port.
@@ -32,6 +35,7 @@ func NewServicePort(port, target int) *ServicePort {
Protocol: "TCP", Protocol: "TCP",
Port: port, Port: port,
TargetPort: port, TargetPort: port,
Name: "port-" + strconv.Itoa(target),
} }
} }

18
helm/serviceAccount.go Normal file
View File

@@ -0,0 +1,18 @@
package helm
// ServiceAccount defines a service account
type ServiceAccount struct {
*K8sBase `yaml:",inline"`
}
// NewServiceAccount creates a new service account with a given name.
func NewServiceAccount(name string) *ServiceAccount {
sa := &ServiceAccount{
K8sBase: NewBase(),
}
sa.K8sBase.Kind = "ServiceAccount"
sa.K8sBase.ApiVersion = "v1"
sa.K8sBase.Metadata.Name = ReleaseNameTpl + "-" + name + "-cron-user"
sa.K8sBase.Metadata.Labels[K+"/component"] = name
return sa
}

View File

@@ -1,4 +1,4 @@
package generator package tools
import ( import (
"katenary/compose" "katenary/compose"

View File

@@ -1,4 +1,4 @@
package generator package tools
import ( import (
"katenary/compose" "katenary/compose"