Allow to have shared templates across packages

This changeset allows to have shared templates in a static folder
"templates" present in each luet tree. If the directory is present, it
gets scanned and templated accordingly on top of each package. This
allows to use such folder to store custom blocks to share between
packages.

This is still experimental and subject to change, this is just a first
pass version to provide the feature. It needs to be refined still as it
would be more elegant to use the helm engine properly and map our
structure to the engine instead of adapting it roughly.

Fixes #224
This commit is contained in:
Ettore Di Giacinto
2021-08-04 16:16:54 +02:00
parent 86bd6c5fc0
commit 33b1c63815
13 changed files with 243 additions and 40 deletions

View File

@@ -160,6 +160,17 @@ Build packages specifying multiple definition trees:
opts.Options = solver.Options{Type: solver.SingleCoreSimple, Concurrency: concurrency}
}
templateFolders := []string{}
if !fromRepo {
for _, t := range treePaths {
templateFolders = append(templateFolders, filepath.Join(t, "templates"))
}
} else {
for _, s := range installer.SystemRepositories(LuetCfg) {
templateFolders = append(templateFolders, filepath.Join(s.TreePath, "templates"))
}
}
luetCompiler := compiler.NewLuetCompiler(compilerBackend, generalRecipe.GetDatabase(),
options.NoDeps(nodeps),
options.WithBackendType(backendType),
@@ -168,6 +179,7 @@ Build packages specifying multiple definition trees:
options.WithPullRepositories(pullRepo),
options.WithPushRepository(imageRepository),
options.Rebuild(rebuild),
options.WithTemplateFolder(templateFolders),
options.WithSolverOptions(*opts),
options.Wait(wait),
options.OnlyTarget(onlyTarget),

View File

@@ -42,6 +42,7 @@ import (
"github.com/mudler/luet/pkg/solver"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"helm.sh/helm/v3/pkg/chart"
)
const BuildFile = "build.yaml"
@@ -750,6 +751,7 @@ func (cs *LuetCompiler) inheritSpecBuildOptions(p *compilerspec.LuetCompilationS
p.BuildOptions.PullImageRepository = append(p.BuildOptions.PullImageRepository, cs.Options.PullImageRepository...)
Debug("Inheriting pull repository from PullImageRepository buildoptions", p.BuildOptions.PullImageRepository)
}
Debug(p.GetPackage().HumanReadableString(), "Build options after inherit", p.BuildOptions)
}
@@ -1128,6 +1130,14 @@ func (cs *LuetCompiler) compile(concurrency int, keepPermissions bool, generateF
type templatedata map[string]interface{}
func (cs *LuetCompiler) templatePackage(vals []map[string]interface{}, pack pkg.Package, dst templatedata) ([]byte, error) {
// Grab shared templates first
var chartFiles []*chart.File
if len(cs.Options.TemplatesFolder) != 0 {
c, err := helpers.ChartFiles(cs.Options.TemplatesFolder)
if err == nil {
chartFiles = c
}
}
var dataresult []byte
val := pack.Rel(DefinitionFile)
@@ -1165,7 +1175,7 @@ func (cs *LuetCompiler) templatePackage(vals []map[string]interface{}, pack pkg.
return nil, errors.Wrap(err, "merging values maps")
}
dat, err := helpers.RenderHelm(string(dataBuild), td, dst)
dat, err := helpers.RenderHelm(append(chartFiles, helpers.ChartFileB(dataBuild)...), td, dst)
if err != nil {
return nil, errors.Wrap(err, "rendering file "+pack.Rel(BuildFile))
}
@@ -1190,7 +1200,13 @@ func (cs *LuetCompiler) templatePackage(vals []map[string]interface{}, pack pkg.
bv = append([]string{f}, bv...)
}
}
out, err := helpers.RenderFiles(pack.Rel(BuildFile), val, bv...)
raw, err := ioutil.ReadFile(pack.Rel(BuildFile))
if err != nil {
return nil, err
}
out, err := helpers.RenderFiles(append(chartFiles, helpers.ChartFileB(raw)...), val, bv...)
if err != nil {
return nil, errors.Wrap(err, "rendering file "+pack.Rel(BuildFile))
}

View File

@@ -43,6 +43,9 @@ type Compiler struct {
BackendArgs []string
BackendType string
// TemplatesFolder. should default to tree/templates
TemplatesFolder []string
}
func NewDefaultCompiler() *Compiler {
@@ -87,6 +90,13 @@ func WithBackendType(r string) func(cfg *Compiler) error {
}
}
func WithTemplateFolder(r []string) func(cfg *Compiler) error {
return func(cfg *Compiler) error {
cfg.TemplatesFolder = r
return nil
}
}
func WithBuildValues(r []string) func(cfg *Compiler) error {
return func(cfg *Compiler) error {
cfg.BuildValuesFile = r

View File

@@ -27,6 +27,8 @@ import (
"strings"
"time"
fileHelper "github.com/mudler/luet/pkg/helpers/file"
pkg "github.com/mudler/luet/pkg/package"
solver "github.com/mudler/luet/pkg/solver"
@@ -108,15 +110,12 @@ type LuetSystemConfig struct {
}
func (s *LuetSystemConfig) SetRootFS(path string) error {
pathToSet := path
if !filepath.IsAbs(path) {
abs, err := filepath.Abs(path)
p, err := fileHelper.Rel2Abs(path)
if err != nil {
return err
}
pathToSet = abs
}
s.Rootfs = pathToSet
s.Rootfs = p
return nil
}

View File

@@ -287,3 +287,15 @@ func CopyDir(src string, dst string) (err error) {
Sync: true,
OnSymlink: func(string) copy.SymlinkAction { return copy.Shallow }})
}
func Rel2Abs(s string) (string, error) {
pathToSet := s
if !filepath.IsAbs(s) {
abs, err := filepath.Abs(s)
if err != nil {
return "", err
}
pathToSet = abs
}
return pathToSet, nil
}

View File

@@ -2,6 +2,8 @@ package helpers
import (
"io/ioutil"
"path/filepath"
"strings"
fileHelper "github.com/mudler/luet/pkg/helpers/file"
@@ -13,16 +15,85 @@ import (
"helm.sh/helm/v3/pkg/engine"
)
// ChartFileB is an helper that takes a slice of bytes and construct a chart.File slice from it
func ChartFileB(s []byte) []*chart.File {
return []*chart.File{
{Name: "templates", Data: s},
}
}
// ChartFileS is an helper that takes a string and construct a chart.File slice from it
func ChartFileS(s string) []*chart.File {
return []*chart.File{
{Name: "templates", Data: []byte(s)},
}
}
// ChartFile reads all the given files and returns a slice of []*chart.File
// containing the raw content and the file name for each file
func ChartFile(s ...string) []*chart.File {
files := []*chart.File{}
for _, c := range s {
raw, err := ioutil.ReadFile(c)
if err != nil {
return files
}
files = append(files, &chart.File{Name: c, Data: raw})
}
return files
}
// ChartFiles reads a list of paths and reads all yaml file inside. It returns a
// slice of pointers of chart.File(s) with the raw content of the yaml
func ChartFiles(path []string) ([]*chart.File, error) {
var chartFiles []*chart.File
for _, t := range path {
rel, err := fileHelper.Rel2Abs(t)
if err != nil {
return nil, err
}
if !fileHelper.Exists(rel) {
continue
}
files, err := fileHelper.ListDir(rel)
if err != nil {
return nil, err
}
for _, f := range files {
if strings.ToLower(filepath.Ext(f)) == ".yaml" {
raw, err := ioutil.ReadFile(f)
if err != nil {
return nil, err
}
chartFiles = append(chartFiles, &chart.File{Name: f, Data: raw})
}
}
}
return chartFiles, nil
}
// RenderHelm renders the template string with helm
func RenderHelm(template string, values, d map[string]interface{}) (string, error) {
func RenderHelm(files []*chart.File, values, d map[string]interface{}) (string, error) {
// We slurp all the files into one here. This is not elegant, but still works.
// As a reminder, the files passed here have on the head the templates in the 'templates/' folder
// of each luet tree, and it have at the bottom the package buildpsec to be templated.
// TODO: Replace by correctly populating the files so that the helm render engine templates it
// correctly
toTemplate := ""
for _, f := range files {
toTemplate += string(f.Data)
}
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "",
Version: "",
},
Templates: []*chart.File{
{Name: "templates", Data: []byte(template)},
},
Templates: ChartFileS(toTemplate),
Values: map[string]interface{}{"Values": values},
}
@@ -71,23 +142,18 @@ func reverse(s []string) []string {
return s
}
func RenderFiles(toTemplate, valuesFile string, defaultFile ...string) (string, error) {
raw, err := ioutil.ReadFile(toTemplate)
if err != nil {
return "", errors.Wrap(err, "reading file "+toTemplate)
}
func RenderFiles(files []*chart.File, valuesFile string, defaultFile ...string) (string, error) {
if !fileHelper.Exists(valuesFile) {
return "", errors.Wrap(err, "file not existing "+valuesFile)
return "", errors.New("file does not exist: " + valuesFile)
}
val, err := ioutil.ReadFile(valuesFile)
if err != nil {
return "", errors.Wrap(err, "reading file "+valuesFile)
return "", errors.Wrap(err, "reading file: "+valuesFile)
}
var values templatedata
if err = yaml.Unmarshal(val, &values); err != nil {
return "", errors.Wrap(err, "unmarshalling file "+toTemplate)
return "", errors.Wrap(err, "unmarshalling values")
}
dst, err := UnMarshalValues(defaultFile)
@@ -95,5 +161,5 @@ func RenderFiles(toTemplate, valuesFile string, defaultFile ...string) (string,
return "", errors.Wrap(err, "unmarshalling values")
}
return RenderHelm(string(raw), values, dst)
return RenderHelm(files, values, dst)
}

View File

@@ -30,21 +30,21 @@ func writeFile(path string, content string) {
Expect(err).ToNot(HaveOccurred())
}
var _ = Describe("Helpers", func() {
var _ = Describe("Helm", func() {
Context("RenderHelm", func() {
It("Renders templates", func() {
out, err := RenderHelm("{{.Values.Test}}{{.Values.Bar}}", map[string]interface{}{"Test": "foo"}, map[string]interface{}{"Bar": "bar"})
out, err := RenderHelm(ChartFileS("{{.Values.Test}}{{.Values.Bar}}"), map[string]interface{}{"Test": "foo"}, map[string]interface{}{"Bar": "bar"})
Expect(err).ToNot(HaveOccurred())
Expect(out).To(Equal("foobar"))
})
It("Renders templates with overrides", func() {
out, err := RenderHelm("{{.Values.Test}}{{.Values.Bar}}", map[string]interface{}{"Test": "foo", "Bar": "baz"}, map[string]interface{}{"Bar": "bar"})
out, err := RenderHelm(ChartFileS("{{.Values.Test}}{{.Values.Bar}}"), map[string]interface{}{"Test": "foo", "Bar": "baz"}, map[string]interface{}{"Bar": "bar"})
Expect(err).ToNot(HaveOccurred())
Expect(out).To(Equal("foobar"))
})
It("Renders templates", func() {
out, err := RenderHelm("{{.Values.Test}}{{.Values.Bar}}", map[string]interface{}{"Test": "foo", "Bar": "bar"}, map[string]interface{}{})
out, err := RenderHelm(ChartFileS("{{.Values.Test}}{{.Values.Bar}}"), map[string]interface{}{"Test": "foo", "Bar": "bar"}, map[string]interface{}{})
Expect(err).ToNot(HaveOccurred())
Expect(out).To(Equal("foobar"))
})
@@ -68,7 +68,7 @@ foo: "baz"
Expect(err).ToNot(HaveOccurred())
res, err := RenderFiles(toTemplate, values, d)
res, err := RenderFiles(ChartFile(toTemplate), values, d)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal("baz"))
@@ -93,7 +93,7 @@ faa: "baz"
Expect(err).ToNot(HaveOccurred())
res, err := RenderFiles(toTemplate, values, d)
res, err := RenderFiles(ChartFile(toTemplate), values, d)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal("bar"))
@@ -114,7 +114,7 @@ foo: "bar"
Expect(err).ToNot(HaveOccurred())
res, err := RenderFiles(toTemplate, values)
res, err := RenderFiles(ChartFile(toTemplate), values)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal("bar"))
})
@@ -145,11 +145,11 @@ bar: "nei"
Expect(err).ToNot(HaveOccurred())
res, err := RenderFiles(toTemplate, values, d2, d)
res, err := RenderFiles(ChartFile(toTemplate), values, d2, d)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal("bazneif"))
res, err = RenderFiles(toTemplate, values, d, d2)
res, err = RenderFiles(ChartFile(toTemplate), values, d, d2)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal("doneif"))
})
@@ -173,7 +173,7 @@ faa: "baz"
Expect(err).ToNot(HaveOccurred())
res, err := RenderFiles(toTemplate, values, d)
res, err := RenderFiles(ChartFile(toTemplate), values, d)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal(""))

View File

@@ -29,7 +29,7 @@ func (s *System) ExecuteFinalizers(packs []pkg.Package) error {
executedFinalizer := map[string]bool{}
for _, p := range packs {
if fileHelper.Exists(p.Rel(tree.FinalizerFile)) {
out, err := helpers.RenderFiles(p.Rel(tree.FinalizerFile), p.Rel(tree.DefinitionFile))
out, err := helpers.RenderFiles(helpers.ChartFile(p.Rel(tree.FinalizerFile)), p.Rel(tree.DefinitionFile))
if err != nil {
Warning("Failed rendering finalizer for ", p.HumanReadableString(), err.Error())
errs = multierror.Append(errs, err)

View File

@@ -76,6 +76,10 @@ func (r *CompilerRecipe) Load(path string) error {
//if err != nil {
// return err
//}
c, err := helpers.ChartFiles([]string{filepath.Join(path, "templates")})
if err != nil {
return err
}
//r.Tree().SetPackageSet(pkg.NewBoltDatabase(tmpfile.Name()))
// TODO: Handle cleaning after? Cleanup implemented in GetPackageSet().Clean()
@@ -104,8 +108,7 @@ func (r *CompilerRecipe) Load(path string) error {
// Instead of rdeps, have a different tree for build deps.
compileDefPath := pack.Rel(CompilerDefinitionFile)
if fileHelper.Exists(compileDefPath) {
dat, err := helpers.RenderFiles(compileDefPath, currentpath)
dat, err := helpers.RenderFiles(append(c, helpers.ChartFile(compileDefPath)...), currentpath)
if err != nil {
return errors.Wrap(err,
"Error templating file "+CompilerDefinitionFile+" from "+
@@ -157,7 +160,7 @@ func (r *CompilerRecipe) Load(path string) error {
if err != nil {
return errors.Wrap(err, "Error reading file "+currentpath)
}
dat, err := helpers.RenderHelm(string(buildyaml), raw, map[string]interface{}{})
dat, err := helpers.RenderHelm(append(c, helpers.ChartFileB(buildyaml)...), raw, map[string]interface{}{})
if err != nil {
return errors.Wrap(err,
"Error templating file "+CompilerDefinitionFile+" from "+
@@ -183,7 +186,7 @@ func (r *CompilerRecipe) Load(path string) error {
return nil
}
err := filepath.Walk(path, ff)
err = filepath.Walk(path, ff)
if err != nil {
return err
}

View File

@@ -0,0 +1,3 @@
{{ define "writefile" }}
- echo conflict > /foo/{{.}}
{{end}}

View File

@@ -0,0 +1,6 @@
image: "alpine"
prelude:
- mkdir /foo
steps:
{{ template "writefile" .Values.name }}
package_dir: /foo

View File

@@ -0,0 +1,7 @@
packages:
- category: "test"
name: "foo"
version: "1.0"
- category: "test"
name: "bar"
version: "1.0"

View File

@@ -0,0 +1,69 @@
#!/bin/bash
export LUET_NOLOCK=true
oneTimeSetUp() {
export tmpdir="$(mktemp -d)"
}
oneTimeTearDown() {
rm -rf "$tmpdir"
}
testBuild() {
mkdir $tmpdir/testbuild
luet build --tree "$ROOT_DIR/tests/fixtures/shared_templates" --destination $tmpdir/testbuild --compression gzip --all
buildst=$?
assertEquals 'builds successfully' "$buildst" "0"
assertTrue 'create packages' "[ -e '$tmpdir/testbuild/foo-test-1.0.package.tar.gz' ]"
assertTrue 'create packages' "[ -e '$tmpdir/testbuild/bar-test-1.0.package.tar.gz' ]"
}
testRepo() {
assertTrue 'no repository' "[ ! -e '$tmpdir/testbuild/repository.yaml' ]"
luet create-repo --tree "$ROOT_DIR/tests/fixtures/shared_templates" \
--output $tmpdir/testbuild \
--packages $tmpdir/testbuild \
--name "test" \
--descr "Test Repo" \
--urls $tmpdir/testrootfs \
--type disk > /dev/null
createst=$?
assertEquals 'create repo successfully' "$createst" "0"
assertTrue 'create repository' "[ -e '$tmpdir/testbuild/repository.yaml' ]"
}
testConfig() {
mkdir $tmpdir/testrootfs
cat <<EOF > $tmpdir/luet.yaml
general:
debug: true
system:
rootfs: $tmpdir/testrootfs
database_path: "/"
database_engine: "boltdb"
config_from_host: true
repositories:
- name: "main"
type: "disk"
enable: true
urls:
- "$tmpdir/testbuild"
EOF
luet config --config $tmpdir/luet.yaml
res=$?
assertEquals 'config test successfully' "$res" "0"
}
testInstall() {
luet install -y --config $tmpdir/luet.yaml test/foo test/bar
installst=$?
assertEquals 'install test failed' "$installst" "0"
assertTrue 'package bar installed' "[ -e '$tmpdir/testrootfs/bar' ]"
assertTrue 'package foo installed' "[ -e '$tmpdir/testrootfs/foo' ]"
}
# Load shUnit2.
. "$ROOT_DIR/tests/integration/shunit2"/shunit2