Skip to content

Commit

Permalink
add "bake.New" function to split a into defaults based tile source
Browse files Browse the repository at this point in the history
  • Loading branch information
crhntr committed Sep 27, 2024
1 parent af455fb commit 6e45141
Show file tree
Hide file tree
Showing 6 changed files with 423 additions and 0 deletions.
17 changes: 17 additions & 0 deletions pkg/bake/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package bake

const (
DefaultFilepathIconImage = "icon.png"
DefaultFilepathKilnfile = "Kilnfile"
DefaultFilepathKilnfileLock = DefaultFilepathKilnfile + ".lock"
DefaultFilepathBaseYML = "base.yml"

DefaultDirectoryReleases = "releases"
DefaultDirectoryForms = "forms"
DefaultDirectoryInstanceGroups = "instance_groups"
DefaultDirectoryJobs = "jobs"
DefaultDirectoryMigrations = "migrations"
DefaultDirectoryProperties = "properties"
DefaultDirectoryRuntimeConfigs = "runtime_configs"
DefaultDirectoryBOSHVariables = "bosh_variables"
)
82 changes: 82 additions & 0 deletions pkg/bake/defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package bake_test

import (
"reflect"
"testing"

"github.com/stretchr/testify/require"

"github.com/pivotal-cf/kiln/internal/commands"
"github.com/pivotal-cf/kiln/pkg/bake"
)

func TestBakeOptions(t *testing.T) {
options := reflect.TypeOf(commands.BakeOptions{})

for _, tt := range []struct {
Constant string
LongFlag string
}{
{
Constant: bake.DefaultFilepathIconImage,
LongFlag: "icon",
},
{
Constant: bake.DefaultFilepathKilnfile,
LongFlag: "Kilnfile",
},
{
Constant: bake.DefaultFilepathBaseYML,
LongFlag: "metadata",
},
{
Constant: bake.DefaultDirectoryReleases,
LongFlag: "releases-directory",
},
{
Constant: bake.DefaultDirectoryForms,
LongFlag: "forms-directory",
},
{
Constant: bake.DefaultDirectoryInstanceGroups,
LongFlag: "instance-groups-directory",
},
{
Constant: bake.DefaultDirectoryJobs,
LongFlag: "jobs-directory",
},
{
Constant: bake.DefaultDirectoryMigrations,
LongFlag: "migrations-directory",
},
{
Constant: bake.DefaultDirectoryProperties,
LongFlag: "properties-directory",
},
{
Constant: bake.DefaultDirectoryRuntimeConfigs,
LongFlag: "runtime-configs-directory",
},
{
Constant: bake.DefaultDirectoryBOSHVariables,
LongFlag: "bosh-variables-directory",
},
} {
t.Run(tt.Constant, func(t *testing.T) {
field, found := fieldByTag(options, "long", "icon")
require.True(t, found)
require.Equal(t, field.Tag.Get("default"), bake.DefaultFilepathIconImage)
})
}
}

func fieldByTag(tp reflect.Type, tagName, tagValue string) (reflect.StructField, bool) {
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i)
value, ok := field.Tag.Lookup(tagName)
if ok && value == tagValue {
return field, true
}
}
return reflect.StructField{}, false
}
200 changes: 200 additions & 0 deletions pkg/bake/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package bake

import (
"archive/zip"
"encoding/base64"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"

"github.com/crhntr/yamlutil/yamlnode"
"gopkg.in/yaml.v3"

"github.com/pivotal-cf/kiln/pkg/cargo"
"github.com/pivotal-cf/kiln/pkg/tile"
)

const (
fileMode = 0644
dirMode = 0744

gitKeepFilename = ".gitkeep"
defaultGitIgnore =
/* language=gitignore */ `
*.pivotal
releases/*.tgz
releases/*.tar.gz
*.out
`
)

func New(outputDirectory, tilePath string, spec cargo.Kilnfile) error {
f, openErr := os.Open(tilePath)
info, statErr := os.Stat(tilePath)
if err := errors.Join(statErr, openErr); err != nil {
return err
}
defer closeAndIgnoreError(f)
return newFromReader(outputDirectory, f, info.Size(), spec)
}

func newFromReader(outputDirectory string, r io.ReaderAt, size int64, spec cargo.Kilnfile) error {
zr, err := zip.NewReader(r, size)
if err != nil {
return err
}
return newFromFS(outputDirectory, zr, spec)
}

func newFromFS(outputDirectory string, dir fs.FS, spec cargo.Kilnfile) error {
productTemplateBuffer, err := tile.ReadMetadataFromFS(dir)
if err != nil {
return err
}
productTemplate, err := newFromProductTemplate(outputDirectory, productTemplateBuffer)
if err != nil {
return err
}
if err := extractMigrations(outputDirectory, dir); err != nil {
return err
}
releaseLocks, err := extractReleases(outputDirectory, productTemplate, dir)
if err != nil {
return err
}
if err := newKilnfiles(outputDirectory, spec, releaseLocks); err != nil {
return err
}
baseYML, err := yaml.Marshal(productTemplate)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(outputDirectory, DefaultFilepathBaseYML), baseYML, fileMode); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(outputDirectory, ".gitignore"), []byte(defaultGitIgnore), fileMode); err != nil {
return err
}
return nil
}

func newKilnfiles(outputDirectory string, spec cargo.Kilnfile, releaseTarballs []cargo.BOSHReleaseTarball) error {
var lock cargo.KilnfileLock
for _, tarball := range releaseTarballs {
lock.Releases = append(lock.Releases, cargo.BOSHReleaseTarballLock{
Name: tarball.Manifest.Name,
Version: tarball.Manifest.Version,
SHA1: tarball.SHA1,
})
}
slices.SortFunc(lock.Releases, func(a, b cargo.BOSHReleaseTarballLock) int {
return strings.Compare(a.Name, b.Name)
})
spec.Releases = spec.Releases[:0]
for _, lock := range spec.Releases {
spec.Releases = append(spec.Releases, cargo.BOSHReleaseTarballSpecification{
Name: lock.Name,
Version: lock.Version,
DeGlazeBehavior: cargo.LockPatch,
FloatAlways: false,
})
}
kilnfileLock, err := yaml.Marshal(lock)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(outputDirectory, DefaultFilepathKilnfileLock), kilnfileLock, fileMode)
}

func newFromProductTemplate(outputDirectory string, productTemplate []byte) (*yaml.Node, error) {
var productTemplateNode yaml.Node
if err := yaml.Unmarshal(productTemplate, &productTemplateNode); err != nil {
return &productTemplateNode, fmt.Errorf("failed to parse product template: %w", err)
}
return &productTemplateNode, errors.Join(writeIconPNG(outputDirectory, &productTemplateNode))
}

func extractMigrations(outputDirectory string, dir fs.FS) error {
migrations, err := fs.Glob(dir, "migrations/*.js")
if err != nil {
return err
}
for _, migration := range migrations {
outPath := filepath.Join(outputDirectory, filepath.FromSlash(migration))
if err := copyFile(outPath, dir, migration); err != nil {
return err

}
}
return nil
}

func extractReleases(outputDirectory string, productTemplate *yaml.Node, dir fs.FS) ([]cargo.BOSHReleaseTarball, error) {
releases, err := fs.Glob(dir, "releases/*.tgz")
if err != nil {
return nil, err
}
var tarballs []cargo.BOSHReleaseTarball

if err := os.MkdirAll(filepath.Join(outputDirectory, DefaultDirectoryReleases), dirMode); err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(outputDirectory, DefaultDirectoryReleases, gitKeepFilename), nil, fileMode); err != nil {
return nil, err
}
for _, release := range releases {
outPath := filepath.Join(outputDirectory, filepath.FromSlash(release))
if err := copyFile(outPath, dir, release); err != nil {
return nil, err
}
releaseTarball, err := cargo.OpenBOSHReleaseTarball(outPath)
if err != nil {
return nil, err
}
tarballs = append(tarballs, releaseTarball)
}
releasesNode, found := yamlnode.LookupKey(productTemplate, "releases")
if !found {
return nil, err
}
var releasesList []string
for _, tarball := range tarballs {
releasesList = append(releasesList, fmt.Sprintf("{{ release %q }}", tarball.Manifest.Name))
}
return tarballs, releasesNode.Encode(&releasesList)
}

func copyFile(out string, dir fs.FS, p string) error {
srcFile, err := dir.Open(p)
if err != nil {
return err
}
defer closeAndIgnoreError(srcFile)

dstFile, err := os.Create(out)
if err != nil {
return err
}
defer closeAndIgnoreError(dstFile)

_, err = io.Copy(dstFile, srcFile)
return err
}

func writeIconPNG(outputDirectory string, productTemplate *yaml.Node) error {
iconImageNode, found := yamlnode.LookupKey(productTemplate, "icon_image")
if !found {
return fmt.Errorf("icon_image not found in product template")
}
iconImage, err := base64.StdEncoding.DecodeString(strings.TrimSpace(iconImageNode.Value))
if err != nil {
return fmt.Errorf("failed to decode icon_image: %w", err)
}
iconImageNode.Value = `{{ icon }}`
return os.WriteFile(filepath.Join(outputDirectory, DefaultFilepathIconImage), iconImage, fileMode)
}
79 changes: 79 additions & 0 deletions pkg/bake/new_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package bake

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/pivotal-cf/kiln/pkg/cargo"
)

func Test_shatterFromFS(t *testing.T) {
t.Run("missing product template", func(t *testing.T) {
zip := os.DirFS(t.TempDir())
output := t.TempDir()
err := newFromFS(output, zip, cargo.Kilnfile{})
require.ErrorContains(t, err, "metadata file not found")
})
}

func Test_shatterProductTemplate(t *testing.T) {
t.Run("when the product template is valid", func(t *testing.T) {
const productTemplateYAML = `}:`
output := t.TempDir()
_, err := newFromProductTemplate(output, []byte(productTemplateYAML))
require.ErrorContains(t, err, "failed to parse product template")
})
}

func Test_writeIconPNG(t *testing.T) {
t.Run("when the field is valid base64", func(t *testing.T) {
const productTemplateYAML =
/* language=yaml */ `---
icon_image: cmVsYXRpbmcgdG8gb3Igb2YgdGhlIG5hdHVyZSBvZiBhbiBpY29u
`
output := t.TempDir()
productTemplate := parseProductTemplateNode(t, productTemplateYAML)
err := writeIconPNG(output, productTemplate)
require.NoError(t, err)

expOutput := filepath.Join(output, DefaultFilepathIconImage)
require.FileExists(t, expOutput)
buf, err := os.ReadFile(expOutput)
require.NoError(t, err)
require.Equal(t, "relating to or of the nature of an icon", string(buf),
"it gets written to the file")
})

t.Run("missing icon", func(t *testing.T) {
const productTemplateYAML =
/* language=yaml */ `---
ping: pong
`
output := t.TempDir()
productTemplate := parseProductTemplateNode(t, productTemplateYAML)
err := writeIconPNG(output, productTemplate)
require.ErrorContains(t, err, "icon_image not found in product template")
})

t.Run("not base64", func(t *testing.T) {
const productTemplateYAML =
/* language=yaml */ `---
icon_image: $
`
output := t.TempDir()
productTemplate := parseProductTemplateNode(t, productTemplateYAML)
err := writeIconPNG(output, productTemplate)
require.ErrorContains(t, err, "failed to decode icon_image")
})
}

func parseProductTemplateNode(t *testing.T, productTemplateYAML string) *yaml.Node {
t.Helper()
var productTemplate yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(productTemplateYAML), &productTemplate))
return &productTemplate
}
Loading

0 comments on commit 6e45141

Please sign in to comment.