chore(refacto): fix secret and use katenary schema

- add possibility to use a katenary.yaml file to setup values
- fix secret generation
This commit is contained in:
2024-11-18 17:12:12 +01:00
parent 14877fbfa3
commit cc1019b5a8
39 changed files with 375 additions and 155 deletions

View File

@@ -0,0 +1,235 @@
package labels
import (
"bytes"
_ "embed"
"fmt"
"katenary/utils"
"regexp"
"sort"
"strings"
"text/tabwriter"
"text/template"
"sigs.k8s.io/yaml"
)
const KatenaryLabelPrefix = "katenary.v3"
// Known labels.
const (
LabelMainApp Label = KatenaryLabelPrefix + "/main-app"
LabelValues Label = KatenaryLabelPrefix + "/values"
LabelSecrets Label = KatenaryLabelPrefix + "/secrets"
LabelPorts Label = KatenaryLabelPrefix + "/ports"
LabelIngress Label = KatenaryLabelPrefix + "/ingress"
LabelMapEnv Label = KatenaryLabelPrefix + "/map-env"
LabelHealthCheck Label = KatenaryLabelPrefix + "/health-check"
LabelSamePod Label = KatenaryLabelPrefix + "/same-pod"
LabelDescription Label = KatenaryLabelPrefix + "/description"
LabelIgnore Label = KatenaryLabelPrefix + "/ignore"
LabelDependencies Label = KatenaryLabelPrefix + "/dependencies"
LabelConfigMapFiles Label = KatenaryLabelPrefix + "/configmap-files"
LabelCronJob Label = KatenaryLabelPrefix + "/cronjob"
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
)
var (
// Set the documentation of labels here
//
//go:embed katenaryLabelsDoc.yaml
labelFullHelpYAML []byte
// parsed yaml
labelFullHelp map[string]Help
)
// Label is a katenary label to find in compose files.
type Label = string
func LabelName(name string) Label {
return Label(KatenaryLabelPrefix + "/" + name)
}
// Help is the documentation of a label.
type Help struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Type string `yaml:"type"`
}
// GetLabelNames returns a sorted list of all katenary label names.
func GetLabelNames() []string {
var names []string
for name := range labelFullHelp {
names = append(names, name)
}
sort.Strings(names)
return names
}
func init() {
if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil {
panic(err)
}
}
// Generate the help for the labels.
func GetLabelHelp(asMarkdown bool) string {
names := GetLabelNames() // sorted
if !asMarkdown {
return generatePlainHelp(names)
}
return generateMarkdownHelp(names)
}
// GetLabelHelpFor returns the help for a specific label.
func GetLabelHelpFor(labelname string, asMarkdown bool) string {
help, ok := labelFullHelp[labelname]
if !ok {
return "No help available for " + labelname + "."
}
help.Long = strings.TrimPrefix(help.Long, "\n")
help.Example = strings.TrimPrefix(help.Example, "\n")
help.Short = strings.TrimPrefix(help.Short, "\n")
// get help template
helpTemplate := getHelpTemplate(asMarkdown)
if asMarkdown {
// enclose templates in backticks
help.Long = regexp.MustCompile(`\{\{(.*?)\}\}`).ReplaceAllString(help.Long, "`{{$1}}`")
help.Long = strings.ReplaceAll(help.Long, "__APP__", "`__APP__`")
} else {
help.Long = strings.ReplaceAll(help.Long, " \n", "\n")
help.Long = strings.ReplaceAll(help.Long, "`", "")
help.Long = strings.ReplaceAll(help.Long, "<code>", "")
help.Long = strings.ReplaceAll(help.Long, "</code>", "")
help.Long = utils.WordWrap(help.Long, 80)
}
var buf bytes.Buffer
template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct {
KatenaryPrefix string
}{
KatenaryPrefix: KatenaryLabelPrefix,
})
help.Long = buf.String()
buf.Reset()
template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct {
KatenaryPrefix string
}{
KatenaryPrefix: KatenaryLabelPrefix,
})
help.Example = buf.String()
buf.Reset()
template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct {
Name string
Help Help
KatenaryPrefix string
}{
Name: labelname,
Help: help,
KatenaryPrefix: KatenaryLabelPrefix,
})
return buf.String()
}
func generateMarkdownHelp(names []string) string {
var builder strings.Builder
var maxNameLength, maxDescriptionLength, maxTypeLength int
max := func(a, b int) int {
if a > b {
return a
}
return b
}
for _, name := range names {
help := labelFullHelp[name]
maxNameLength = max(maxNameLength, len(name)+2+len(KatenaryLabelPrefix))
maxDescriptionLength = max(maxDescriptionLength, len(help.Short))
maxTypeLength = max(maxTypeLength, len(help.Type))
}
fmt.Fprintf(&builder, "%s\n", generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength))
fmt.Fprintf(&builder, "%s\n", generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength))
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "| %-*s | %-*s | %-*s |\n",
maxNameLength, "`"+LabelName(name)+"`", // enclose in backticks
maxDescriptionLength, help.Short,
maxTypeLength, help.Type,
)
}
return builder.String()
}
func generatePlainHelp(names []string) string {
var builder strings.Builder
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "%s:\t%s\t%s\n", LabelName(name), help.Type, help.Short)
}
// use tabwriter to align the help text
buf := new(strings.Builder)
w := tabwriter.NewWriter(buf, 0, 8, 0, '\t', tabwriter.AlignRight)
fmt.Fprintln(w, builder.String())
w.Flush()
head := "To get more information about a label, use `katenary help-label <name_without_prefix>\ne.g. katenary help-label dependencies\n\n"
return head + buf.String()
}
func generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %-*s | %-*s | %-*s |",
maxNameLength, "Label name",
maxDescriptionLength, "Description",
maxTypeLength, "Type",
)
}
func generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %s | %s | %s |",
strings.Repeat("-", maxNameLength),
strings.Repeat("-", maxDescriptionLength),
strings.Repeat("-", maxTypeLength),
)
}
func getHelpTemplate(asMarkdown bool) string {
if asMarkdown {
return `## {{ .KatenaryPrefix }}/{{ .Name }}
{{ .Help.Short }}
**Type**: ` + "`" + `{{ .Help.Type }}` + "`" + `
{{ .Help.Long }}
**Example:**` + "\n\n```yaml\n" + `{{ .Help.Example }}` + "\n```\n"
}
return `{{ .KatenaryPrefix }}/{{ .Name }}: {{ .Help.Short }}
Type: {{ .Help.Type }}
{{ .Help.Long }}
Example:
{{ .Help.Example }}
`
}
func Prefix() string {
return KatenaryLabelPrefix
}

View File

@@ -0,0 +1,287 @@
# Labels documentation.
#
# To create a label documentation:
#
# "labelname":
# type: the label type (bool, string, array, object...)
# short: a short description
# long: |-
# A multiline description to explain the label behavior
# example: |-
# yamlsyntax: here
#
# This file is embed in the Katenary binary and parsed in kanetaryLabels.go init() function.
#
# Note:
# - The short and long texts are parsed with text/template, so you can use template syntax.
# That means that if you want to display double brackets, you need to enclose them to
# prevent template to try to expand the content, for example :
# This is an {{ "{{ example }}" }}.
#
# This will display "This is an {{ exemple }}" in the output.
# - Use {{ .KatenaryPrefix }} to let Katenary replace it with the label prefix (e.g. "katenary.v3")
"main-app":
short: "Mark the service as the main app."
long: |-
This makes the service to be the main application. Its image tag is
considered to be the Chart appVersion and to be the defaultvalue in Pod
container image attribute.
!!! Warning
This label cannot be repeated in others services. If this label is
set in more than one service as true, Katenary will return an error.
example: |-
ghost:
image: ghost:1.25.5
labels:
# The chart is now named ghost, and the appVersion is 1.25.5.
# In Deployment, the image attribute is set to ghost:1.25.5 if
# you don't change the "tag" attribute in values.yaml
{{ .KatenaryPrefix }}/main-app: true
type: "bool"
"values":
short: "Environment variables to be added to the values.yaml"
long: |-
By default, all environment variables in the "env" and environment
files are added to configmaps with the static values set. This label
allows adding environment variables to the values.yaml file.
Note that the value inside the configmap is {{ "{{ tpl vaname . }}" }}, so
you can set the value to a template that will be rendered with the
values.yaml file.
The value can be set with a documentation. This may help to understand
the purpose of the variable.
example: |-
env:
FOO: bar
DB_NAME: mydb
TO_CONFIGURE: something that can be changed in values.yaml
A_COMPLEX_VALUE: example
labels:
{{ .KatenaryPrefix }}/values: |-
# simple values, set as is in values.yaml
- TO_CONFIGURE
# complex values, set as a template in values.yaml with a documentation
- A_COMPLEX_VALUE: |-
This is the documentation for the variable to
configure in values.yaml.
It can be, of course, a multiline text.
type: "list of string or map"
"secrets":
short: "Env vars to be set as secrets."
long: |-
This label allows setting the environment variables as secrets. The variable
is removed from the environment and added to a secret object.
The variable can be set to the {{ printf "%s/%s" .KatenaryPrefix "values"}} too,
so the secret value can be configured in values.yaml
example: |-
env:
PASSWORD: a very secret password
NOT_A_SECRET: a public value
labels:
{{ .KatenaryPrefix }}/secrets: |-
- PASSWORD
type: "list of string"
"ports":
short: "Ports to be added to the service."
long: |-
Only useful for services without exposed port. It is mandatory if the
service is a dependency of another service.
example: |-
labels:
{{ .KatenaryPrefix }}/ports: |-
- 8080
- 8081
type: "list of uint32"
"ingress":
short: "Ingress rules to be added to the service."
long: |-
Declare an ingress rule for the service. The port should be exposed or
declared with {{ printf "%s/%s" .KatenaryPrefix "ports" }}.
example: |-
labels:
{{ .KatenaryPrefix }}/ingress: |-
port: 80
hostname: mywebsite.com (optional)
type: "object"
"map-env":
short: "Map env vars from the service to the deployment."
long: |-
Because you may need to change the variable for Kubernetes, this label
forces the value to another. It is also particullary helpful to use a template
value instead. For example, you could bind the value to a service name
with Helm attributes:
{{ "{{ tpl .Release.Name . }}" }}.
If you use __APP__ in the value, it will be replaced by the Chart name.
example: |-
env:
DB_HOST: database
RUNNING: docker
OTHER: value
labels:
{{ .KatenaryPrefix }}/map-env: |-
RUNNING: kubernetes
DB_HOST: '{{ "{{ include \"__APP__.fullname\" . }}" }}-database'
type: "object"
"health-check":
short: "Health check to be added to the deployment."
long: "Health check to be added to the deployment."
example: |-
labels:
{{ .KatenaryPrefix }}/health-check: |-
livenessProbe:
httpGet:
path: /health
port: 8080
type: "object"
"same-pod":
short: "Move the same-pod deployment to the target deployment."
long: |-
This will make the service to be included in another service pod. Some services
must work together in the same pod, like a sidecar or a proxy or nginx + php-fpm.
Note that volume and VolumeMount are copied from the source to the target
deployment.
example: |-
web:
image: nginx:1.19
php:
image: php:7.4-fpm
labels:
{{ .KatenaryPrefix }}/same-pod: web
type: "string"
"description":
short: "Description of the service"
long: |-
This replaces the default comment in values.yaml file to the given description.
It is useful to document the service and configuration.
The value can be set with a documentation in multiline format.
example: |-
labels:
{{ .KatenaryPrefix }}/description: |-
This is a description of the service.
It can be multiline.
type: "string"
"ignore":
short: "Ignore the service"
long: "Ingoring a service to not be exported in helm chart."
example: "labels:\n {{ .KatenaryPrefix }}/ignore: \"true\""
type: "bool"
"dependencies":
short: "Add Helm dependencies to the service."
long: |-
Set the service to be, actually, a Helm dependency. This means that the
service will not be exported as template. The dependencies are added to
the Chart.yaml file and the values are added to the values.yaml file.
It's a list of objects with the following attributes:
- name: the name of the dependency
- repository: the repository of the dependency
- alias: the name of the dependency in values.yaml (optional)
- values: the values to be set in values.yaml (optional)
!!! Info
Katenary doesn't update the helm depenedencies by default.
Use `--helm-update` (or `-u`) flag to update the dependencies.
example: <code>katenary convert -u</code>
By setting an alias, it is possible to change the name of the dependency
in values.yaml.
example: |-
labels:
{{ .KatenaryPrefix }}/dependencies: |-
- name: mariadb
repository: oci://registry-1.docker.io/bitnamicharts
## optional, it changes the name of the section in values.yaml
# alias: mydatabase
## optional, it adds the values to values.yaml
values:
auth:
database: mydatabasename
username: myuser
password: the secret password
type: "list of objects"
"configmap-files":
short: "Add files to the configmap."
long: |-
It makes a file or directory to be converted to one or more ConfigMaps
and mounted in the pod. The file or directory is relative to the
service directory.
If it is a directory, all files inside it are added to the ConfigMap.
If the directory as subdirectories, so one configmap per subpath are created.
!!! Warning
It is not intended to be used to store an entire project in configmaps.
It is intended to be used to store configuration files that are not managed
by the application, like nginx configuration files. Keep in mind that your
project sources should be stored in an application image or in a storage.
example: |-
volumes
- ./conf.d:/etc/nginx/conf.d
labels:
{{ .KatenaryPrefix }}/configmap-files: |-
- ./conf.d
type: "list of strings"
"cronjob":
short: "Create a cronjob from the service."
long: |-
This adds a cronjob to the chart.
The label value is a YAML object with the following attributes:
- command: the command to be executed
- schedule: the cron schedule (cron format or @every where "every" is a
duration like 1h30m, daily, hourly...)
- rbac: false (optionnal), if true, it will create a role, a rolebinding and
a serviceaccount to make your cronjob able to connect the Kubernetes API
example: |-
labels:
{{ .KatenaryPrefix }}/cronjob: |-
command: echo "hello world"
schedule: "* */1 * * *" # or @hourly for example
type: "object"
"env-from":
short: "Add environment variables from antoher service."
type: "list of strings"
long: |-
It adds environment variables from another service to the current service.
example: |-
service1:
image: nginx:1.19
environment:
FOO: bar
service2:
image: php:7.4-fpm
labels:
# get the congigMap from service1 where FOO is
# defined inside this service too
{{ .KatenaryPrefix }}/env-from: |-
- myservice1
# vim: ft=gotmpl.yaml

View File

@@ -0,0 +1,78 @@
package labels
import (
_ "embed"
"reflect"
"testing"
)
var testingKatenaryPrefix = Prefix()
const mainAppLabel = "main-app"
func TestPrefix(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "TestPrefix",
want: "katenary.v3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Prefix(); got != tt.want {
t.Errorf("Prefix() = %v, want %v", got, tt.want)
}
})
}
}
func TestLabelName(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
want Label
}{
{
name: "Test_labelName",
args: args{
name: mainAppLabel,
},
want: testingKatenaryPrefix + "/" + mainAppLabel,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := LabelName(tt.args.name); !reflect.DeepEqual(got, tt.want) {
t.Errorf("labelName() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetLabelHelp(t *testing.T) {
help := GetLabelHelp(false)
if help == "" {
t.Errorf("GetLabelHelp() = %v, want %v", help, "Help")
}
help = GetLabelHelp(true)
if help == "" {
t.Errorf("GetLabelHelp() = %v, want %v", help, "Help")
}
}
func TestGetLabelHelpFor(t *testing.T) {
help := GetLabelHelpFor(mainAppLabel, false)
if help == "" {
t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help")
}
help = GetLabelHelpFor("main-app", true)
if help == "" {
t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help")
}
}

View File

@@ -0,0 +1,13 @@
package labelStructs
import "gopkg.in/yaml.v3"
type ConfigMapFile []string
func ConfigMapFileFrom(data string) (ConfigMapFile, error) {
var mapping ConfigMapFile
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,18 @@
package labelStructs
import "gopkg.in/yaml.v3"
type CronJob struct {
Image string `yaml:"image,omitempty" json:"image,omitempty"`
Command string `yaml:"command" json:"command,omitempty"`
Schedule string `yaml:"schedule" json:"schedule,omitempty"`
Rbac bool `yaml:"rbac" json:"rbac,omitempty"`
}
func CronJobFrom(data string) (*CronJob, error) {
var mapping CronJob
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return &mapping, nil
}

View File

@@ -0,0 +1,21 @@
package labelStructs
import "gopkg.in/yaml.v3"
// Dependency is a dependency of a chart to other charts.
type Dependency struct {
Values map[string]any `yaml:"-" json:"values,omitempty"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
Repository string `yaml:"repository" json:"repository"`
Alias string `yaml:"alias,omitempty" json:"alias,omitempty"`
}
// DependenciesFrom returns a slice of dependencies from the given string.
func DependenciesFrom(data string) ([]Dependency, error) {
var mapping []Dependency
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,2 @@
// labelStructs is a package that contains the structs used to represent the labels in the yaml files.
package labelStructs

View File

@@ -0,0 +1,14 @@
package labelStructs
import "gopkg.in/yaml.v3"
type EnvFrom []string
// EnvFromFrom returns a EnvFrom from the given string.
func EnvFromFrom(data string) (EnvFrom, error) {
var mapping EnvFrom
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,33 @@
package labelStructs
import "gopkg.in/yaml.v3"
type TLS struct {
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
}
type Ingress struct {
Port *int32 `yaml:"port,omitempty" jsonschema:"nullable" json:"port,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty" jsonschema:"nullable" json:"annotations,omitempty"`
Hostname string `yaml:"hostname" json:"hostname,omitempty"`
Path string `yaml:"path" json:"path,omitempty"`
Class string `yaml:"class" json:"class,omitempty" jsonschema:"default:-"`
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"`
}
// IngressFrom creates a new Ingress from a compose service.
func IngressFrom(data string) (*Ingress, error) {
mapping := Ingress{
Hostname: "",
Path: "/",
Enabled: false,
Class: "-",
Port: nil,
TLS: &TLS{Enabled: true},
}
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return &mapping, nil
}

View File

@@ -0,0 +1,14 @@
package labelStructs
import "gopkg.in/yaml.v3"
type MapEnv map[string]string
// MapEnvFrom returns a MapEnv from the given string.
func MapEnvFrom(data string) (MapEnv, error) {
var mapping MapEnv
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,14 @@
package labelStructs
import "gopkg.in/yaml.v3"
type Ports []uint32
// PortsFrom returns a Ports from the given string.
func PortsFrom(data string) (Ports, error) {
var mapping Ports
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,56 @@
package labelStructs
import (
"encoding/json"
"log"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
)
type HealthCheck struct {
LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"`
ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty" json:"readinessProbe,omitempty"`
}
func ProbeFrom(data string) (*HealthCheck, error) {
mapping := HealthCheck{}
tmp := map[string]any{}
err := yaml.Unmarshal([]byte(data), &tmp)
if err != nil {
return nil, err
}
if livenessProbe, ok := tmp["livenessProbe"]; ok {
livenessProbeBytes, err := json.Marshal(livenessProbe)
if err != nil {
log.Printf("Error marshalling livenessProbe: %v", err)
return nil, err
}
livenessProbe := &corev1.Probe{}
err = json.Unmarshal(livenessProbeBytes, livenessProbe)
if err != nil {
log.Printf("Error unmarshalling livenessProbe: %v", err)
return nil, err
}
mapping.LivenessProbe = livenessProbe
}
if readinessProbe, ok := tmp["readinessProbe"]; ok {
readinessProbeBytes, err := json.Marshal(readinessProbe)
if err != nil {
log.Printf("Error marshalling readinessProbe: %v", err)
return nil, err
}
readinessProbe := &corev1.Probe{}
err = json.Unmarshal(readinessProbeBytes, readinessProbe)
if err != nil {
log.Printf("Error unmarshalling readinessProbe: %v", err)
return nil, err
}
mapping.ReadinessProbe = readinessProbe
}
return &mapping, err
}

View File

@@ -0,0 +1,13 @@
package labelStructs
import "gopkg.in/yaml.v3"
type Secrets []string
func SecretsFrom(data string) (Secrets, error) {
var mapping Secrets
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}