Big refactorization

- reduce complexity
- use better tools to format the code
- add more tests
- and too many things to list here

We are rewriting for V3, so these commits are sometimes big and not
fully detailed. Of course, further work will be more documented.
This commit is contained in:
2024-05-06 21:11:36 +02:00
parent d98268f45b
commit 4367a01769
26 changed files with 582 additions and 513 deletions

View File

@@ -12,13 +12,12 @@ import (
"strings"
"time"
"github.com/compose-spec/compose-go/types"
"katenary/generator/extrafiles"
"katenary/generator/labelStructs"
"katenary/parser"
"katenary/utils"
"github.com/compose-spec/compose-go/types"
goyaml "gopkg.in/yaml.v3"
)
const headerHelp = `# This file is autogenerated by katenary
@@ -76,10 +75,11 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
// check if the chart directory exists
// if yes, prevent the user from overwriting it and ask for confirmation
if _, err := os.Stat(config.OutputDir); err == nil {
fmt.Print(utils.IconWarning, " The chart directory "+config.OutputDir+" already exists, do you want to overwrite it? [y/N] ")
var answer string
fmt.Scanln(&answer)
if strings.ToLower(answer) != "y" {
overwrite := utils.Confirm(
"The chart directory "+config.OutputDir+" already exists, do you want to overwrite it?",
utils.IconWarning,
)
if !overwrite {
fmt.Println("Aborting")
os.Exit(126) // 126 is the exit code for "Command invoked cannot execute"
}
@@ -109,168 +109,27 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
os.Exit(1)
}
for name, template := range chart.Templates {
t := template.Content
t = removeNewlinesInsideBrackets(t)
t = removeUnwantedLines(t)
t = addModeline(t)
// write the templates to the disk
chart.SaveTemplates(templateDir)
kind := utils.GetKind(name)
var icon utils.Icon
switch kind {
case "deployment":
icon = utils.IconPackage
case "service":
icon = utils.IconPlug
case "ingress":
icon = utils.IconWorld
case "volumeclaim":
icon = utils.IconCabinet
case "configmap":
icon = utils.IconConfig
case "secret":
icon = utils.IconSecret
default:
icon = utils.IconInfo
}
// write the Chart.yaml file
buildCharYamlFile(chart, project, chartPath)
servicename := template.Servicename
if err := os.MkdirAll(filepath.Join(templateDir, servicename), 0o755); err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
fmt.Println(icon, "Creating", kind, servicename)
// if the name is a path, create the directory
if strings.Contains(name, string(filepath.Separator)) {
name = filepath.Join(templateDir, name)
err := os.MkdirAll(filepath.Dir(name), 0o755)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
} else {
// remove the serivce name from the template name
name = strings.Replace(name, servicename+".", "", 1)
name = filepath.Join(templateDir, servicename, name)
}
f, err := os.Create(name)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
// build and write the values.yaml file
buildValues(chart, project, valuesPath)
f.Write(t)
f.Close()
}
// calculate the sha1 hash of the services
buf := bytes.NewBuffer(nil)
encoder := goyaml.NewEncoder(buf)
encoder.SetIndent(2)
if err := encoder.Encode(chart); err != nil {
fmt.Println(err)
os.Exit(1)
}
yamlChart := buf.Bytes()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// concat chart adding a comment with hash of services on top
yamlChart = append([]byte(fmt.Sprintf("# compose hash (sha1): %s\n", *chart.composeHash)), yamlChart...)
// add the list of compose files
files := []string{}
for _, file := range project.ComposeFiles {
base := filepath.Base(file)
files = append(files, base)
}
yamlChart = append([]byte(fmt.Sprintf("# compose files: %s\n", strings.Join(files, ", "))), yamlChart...)
// add generated date
yamlChart = append([]byte(fmt.Sprintf("# generated at: %s\n", time.Now().Format(time.RFC3339))), yamlChart...)
// document Chart.yaml file
yamlChart = addChartDoc(yamlChart, project)
f, err := os.Create(chartPath)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
f.Write(yamlChart)
f.Close()
buf.Reset()
encoder = goyaml.NewEncoder(buf)
encoder.SetIndent(2)
if err = encoder.Encode(&chart.Values); err != nil {
fmt.Println(err)
os.Exit(1)
}
values := buf.Bytes()
values = addDescriptions(values, *project)
values = addDependencyDescription(values, chart.Dependencies)
values = addCommentsToValues(values)
values = addStorageClassHelp(values)
values = addImagePullSecretsHelp(values)
values = addImagePullPolicyHelp(values)
values = addVariablesDoc(values, project)
values = addMainTagAppDoc(values, project)
values = addResourceHelp(values)
values = addYAMLSelectorPath(values)
values = append([]byte(headerHelp), values...)
f, err = os.Create(valuesPath)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
f.Write(values)
f.Close()
f, err = os.Create(helpersPath)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
f.Write([]byte(chart.Helper))
f.Close()
// write the _helpers.tpl to the disk
writeContent(helpersPath, []byte(chart.Helper))
// write the readme to the disk
readme := extrafiles.ReadMeFile(chart.Name, chart.Description, chart.Values)
f, err = os.Create(readmePath)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
f.Write([]byte(readme))
f.Close()
writeContent(readmePath, []byte(readme))
services := make([]string, 0)
for _, service := range project.Services {
services = append(services, service.Name)
}
notes := extrafiles.NotesFile(services)
f, err = os.Create(notesPath)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
f.Write([]byte(notes))
f.Close()
// get the list of services to write in the notes
buildNotesFile(project, notesPath)
executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) {
if err := fn(config); err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
fmt.Println(utils.IconSuccess, message)
}
if config.HelmUpdate {
executeAndHandleError(helmUpdate, config, "Helm dependencies updated")
executeAndHandleError(helmLint, config, "Helm chart linted")
fmt.Println(utils.IconSuccess, "Helm chart created successfully")
}
// call helm update if needed
callHelmUpdate(config)
}
const ingressClassHelp = `# Default value for ingress.class annotation
@@ -501,31 +360,38 @@ func addResourceHelp(values []byte) []byte {
func addVariablesDoc(values []byte, project *types.Project) []byte {
lines := strings.Split(string(values), "\n")
currentService := ""
for _, service := range project.Services {
variables := utils.GetValuesFromLabel(service, LabelValues)
for i, line := range lines {
if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) {
currentService = service.Name
lines = addDocToVariable(service, lines)
}
return []byte(strings.Join(lines, "\n"))
}
func addDocToVariable(service types.ServiceConfig, lines []string) []string {
currentService := ""
variables := utils.GetValuesFromLabel(service, LabelValues)
for i, line := range lines {
// if the line is a service, it is a name followed by a colon
if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) {
currentService = service.Name
}
// for each variable in the service, add the description
for varname, variable := range variables {
if variable == nil {
continue
}
for varname, variable := range variables {
if variable == nil {
continue
}
spaces := utils.CountStartingSpaces(line)
if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name {
spaces := utils.CountStartingSpaces(line)
if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name {
// add # to the beginning of the Description
doc := strings.ReplaceAll("\n"+variable.Description, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
doc = strings.TrimRight(doc, " ")
doc += "\n" + line
// add # to the beginning of the Description
doc := strings.ReplaceAll("\n"+variable.Description, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
doc = strings.TrimRight(doc, " ")
doc += "\n" + line
lines[i] = doc
}
lines[i] = doc
}
}
}
return []byte(strings.Join(lines, "\n"))
return lines
}
const mainTagAppDoc = `This is the version of the main application.
@@ -535,8 +401,6 @@ func addMainTagAppDoc(values []byte, project *types.Project) []byte {
lines := strings.Split(string(values), "\n")
for _, service := range project.Services {
inService := false
inRegistry := false
// read the label LabelMainApp
if v, ok := service.Labels[LabelMainApp]; !ok {
continue
@@ -546,29 +410,36 @@ func addMainTagAppDoc(values []byte, project *types.Project) []byte {
fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name)
}
for i, line := range lines {
if regexp.MustCompile(`^` + service.Name + `:`).MatchString(line) {
inService = true
}
if inService && regexp.MustCompile(`^\s*repository:.*`).MatchString(line) {
inRegistry = true
}
if inService && inRegistry {
if regexp.MustCompile(`^\s*tag: .*`).MatchString(line) {
spaces := utils.CountStartingSpaces(line)
doc := strings.ReplaceAll(mainTagAppDoc, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
doc = strings.Repeat(" ", spaces) + "# " + doc
lines[i] = doc + "\n" + line + "\n"
break
}
}
}
lines = addMainAppDoc(lines, service)
}
return []byte(strings.Join(lines, "\n"))
}
func addMainAppDoc(lines []string, service types.ServiceConfig) []string {
inService := false
inRegistry := false
for i, line := range lines {
if regexp.MustCompile(`^` + service.Name + `:`).MatchString(line) {
inService = true
}
if inService && regexp.MustCompile(`^\s*repository:.*`).MatchString(line) {
inRegistry = true
}
if inService && inRegistry {
if regexp.MustCompile(`^\s*tag: .*`).MatchString(line) {
spaces := utils.CountStartingSpaces(line)
doc := strings.ReplaceAll(mainTagAppDoc, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
doc = strings.Repeat(" ", spaces) + "# " + doc
lines[i] = doc + "\n" + line + "\n"
break
}
}
}
return lines
}
func removeNewlinesInsideBrackets(values []byte) []byte {
re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`)
if err != nil {
@@ -715,3 +586,89 @@ func addYAMLSelectorPath(values []byte) []byte {
}
return []byte(strings.Join(toReturn, "\n"))
}
func writeContent(path string, content []byte) {
f, err := os.Create(path)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
defer f.Close()
f.Write(content)
}
func buildValues(chart *HelmChart, project *types.Project, valuesPath string) {
values, err := utils.EncodeBasicYaml(&chart.Values)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
values = addDescriptions(values, *project)
values = addDependencyDescription(values, chart.Dependencies)
values = addCommentsToValues(values)
values = addStorageClassHelp(values)
values = addImagePullSecretsHelp(values)
values = addImagePullPolicyHelp(values)
values = addVariablesDoc(values, project)
values = addMainTagAppDoc(values, project)
values = addResourceHelp(values)
values = addYAMLSelectorPath(values)
values = append([]byte(headerHelp), values...)
// add vim modeline
values = append(values, []byte("\n# vim: ft=yaml\n")...)
// write the values to the disk
writeContent(valuesPath, values)
}
func buildNotesFile(project *types.Project, notesPath string) {
// get the list of services to write in the notes
services := make([]string, 0)
for _, service := range project.Services {
services = append(services, service.Name)
}
// write the notes to the disk
notes := extrafiles.NotesFile(services)
writeContent(notesPath, []byte(notes))
}
func buildCharYamlFile(chart *HelmChart, project *types.Project, chartPath string) {
// calculate the sha1 hash of the services
yamlChart, err := utils.EncodeBasicYaml(chart)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// concat chart adding a comment with hash of services on top
yamlChart = append([]byte(fmt.Sprintf("# compose hash (sha1): %s\n", *chart.composeHash)), yamlChart...)
// add the list of compose files
files := []string{}
for _, file := range project.ComposeFiles {
base := filepath.Base(file)
files = append(files, base)
}
yamlChart = append([]byte(fmt.Sprintf("# compose files: %s\n", strings.Join(files, ", "))), yamlChart...)
// add generated date
yamlChart = append([]byte(fmt.Sprintf("# generated at: %s\n", time.Now().Format(time.RFC3339))), yamlChart...)
// document Chart.yaml file
yamlChart = addChartDoc(yamlChart, project)
writeContent(chartPath, yamlChart)
}
func callHelmUpdate(config ConvertOptions) {
executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) {
if err := fn(config); err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
fmt.Println(utils.IconSuccess, message)
}
if config.HelmUpdate {
executeAndHandleError(helmUpdate, config, "Helm dependencies updated")
executeAndHandleError(helmLint, config, "Helm chart linted")
fmt.Println(utils.IconSuccess, "Helm chart created successfully")
}
}