package helm import ( "embed" "errors" "fmt" "io" "os" "path/filepath" "text/template" "freeleaps.com/gitops/initializer/api/v1alpha1" ) //go:embed templates/* var embeddedTemplates embed.FS type HelmGenerator struct { io.Closer Instance *v1alpha1.ProjectInitialize workingDir string generated bool } const leftDelim = "[[" const rightDelim = "]]" func (hg *HelmGenerator) prepareWorkingDir() error { path, err := os.MkdirTemp("", fmt.Sprintf("helm-gen-%s-*", hg.Instance.Name)) if err != nil { return fmt.Errorf("failed to create helm generator temp dir: %w", err) } hg.workingDir = path return nil } func (hg *HelmGenerator) readTemplate(name string) (*template.Template, error) { tpl := template.New(name) templateBytes, err := embeddedTemplates.ReadFile(name) if err != nil { return nil, fmt.Errorf("failed to read template %s: %w", name, err) } tpl, err = tpl. Delims(leftDelim, rightDelim). Funcs(funcMap). Parse(string(templateBytes)) if err != nil { return nil, fmt.Errorf("failed to parse template %s: %w", name, err) } return tpl, nil } func (hg *HelmGenerator) createMetaFiles() error { // create Chart.yaml tpl, err := hg.readTemplate("templates/metadata/Chart.yaml.tpl") if err != nil { return err } chartYamlFd, err := os.Create(hg.workingDir + "/Chart.yaml") if err != nil { return fmt.Errorf("failed to create Chart.yaml file: %w", err) } defer chartYamlFd.Close() if err := tpl.Execute(chartYamlFd, hg.Instance.Spec.Helm); err != nil { return fmt.Errorf("failed to rendering Chart.yaml with template: %w", err) } // create values.yaml tpl, err = hg.readTemplate("templates/metadata/values.yaml.tpl") if err != nil { return err } valuesYamlFd, err := os.Create(hg.workingDir + "/values.yaml") if err != nil { return fmt.Errorf("failed to create values.yaml file: %w", err) } defer valuesYamlFd.Close() if err := tpl.Execute(valuesYamlFd, hg.Instance.Spec.Helm); err != nil { return fmt.Errorf("failed to rendering values.yaml with template: %w", err) } return nil } func (hg *HelmGenerator) createComponentManifests(component v1alpha1.HelmComponentSpec) error { workRoot := hg.workingDir + "/templates/" + component.Name if err := os.MkdirAll(workRoot, 0755); err != nil { return fmt.Errorf("failed to create component directory: %w", err) } // create deployment.yaml tpl, err := hg.readTemplate("templates/deployment/deployment.yaml.tpl") if err != nil { return err } deploymentYamlFd, err := os.Create(workRoot + "/deployment.yaml") if err != nil { return fmt.Errorf("failed to create deployment.yaml file: %w", err) } defer deploymentYamlFd.Close() if err := tpl.Execute(deploymentYamlFd, component); err != nil { return fmt.Errorf("failed to rendering deployment.yaml with template: %w", err) } // create service.yaml if there has services if len(component.Services) > 0 { tpl, err = hg.readTemplate("templates/service/service.yaml.tpl") if err != nil { return err } serviceYamlFd, err := os.Create(workRoot + "/service.yaml") if err != nil { return fmt.Errorf("failed to create service.yaml file: %w", err) } defer serviceYamlFd.Close() if err := tpl.Execute(serviceYamlFd, component); err != nil { return fmt.Errorf("failed to rendering service.yaml with template: %w", err) } } // create ingress.yaml if there has ingress if len(component.Ingresses) > 0 { tpl, err = hg.readTemplate("templates/ingress/ingress.yaml.tpl") if err != nil { return err } ingressYamlFd, err := os.Create(workRoot + "/ingress.yaml") if err != nil { return fmt.Errorf("failed to create ingress.yaml file: %w", err) } defer ingressYamlFd.Close() if err := tpl.Execute(ingressYamlFd, component); err != nil { return fmt.Errorf("failed to rendering ingress.yaml with template: %w", err) } } // create secret.yaml if there has configs if len(component.Configs) > 0 { for _, config := range component.Configs { tpl, err = hg.readTemplate("templates/secret/secret.yaml.tpl") if err != nil { return err } secretYamlFd, err := os.Create(workRoot + "/" + config.Name + ".yaml") if err != nil { return fmt.Errorf("failed to create secret.yaml file: %w", err) } defer secretYamlFd.Close() if err := tpl.Execute(secretYamlFd, struct { ComponentName string Config v1alpha1.HelmComponentConfigSpec }{ ComponentName: component.Name, Config: config, }); err != nil { return fmt.Errorf("failed to rendering secret.yaml with template: %w", err) } } } // create certificate if ingress need to sign with tls if len(component.Ingresses) > 0 { needSign := false for _, ingress := range component.Ingresses { if !ingress.TLS.Exists { needSign = true break } } if needSign { tpl, err = hg.readTemplate("templates/certificate/certificate.yaml.tpl") if err != nil { return err } certificateYamlFd, err := os.Create(workRoot + "/certificate.yaml") if err != nil { return fmt.Errorf("failed to create certificate.yaml file: %w", err) } defer certificateYamlFd.Close() if err := tpl.Execute(certificateYamlFd, component); err != nil { return fmt.Errorf("failed to rendering certificate.yaml with template: %w", err) } } } return nil } func (hg *HelmGenerator) Generate() error { if hg.Instance == nil { return errors.New("instance is nil") } // create temp working dir if err := hg.prepareWorkingDir(); err != nil { return err } // create meta files (Chart.yaml, values.yaml, .helmignore) if err := hg.createMetaFiles(); err != nil { return err } // create manifests for each component for _, componentSpec := range hg.Instance.Spec.Helm.Components { if err := hg.createComponentManifests(componentSpec); err != nil { return err } } hg.generated = true return nil } type HelmGeneratedFile struct { QualifiedPath string } func (hgf *HelmGeneratedFile) ReadAll() ([]byte, error) { return os.ReadFile(hgf.QualifiedPath) } type HelmGeneratedFileCallback func(f *HelmGeneratedFile) error func (hg *HelmGenerator) Walk(cb HelmGeneratedFileCallback) error { if !hg.generated { return errors.New("helm package not generated") } return filepath.Walk(hg.workingDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { return cb(&HelmGeneratedFile{QualifiedPath: path}) } return nil }) } func (hg *HelmGenerator) Close() error { if hg.workingDir != "" { return os.RemoveAll(hg.workingDir) } return nil }