From b081921489a1cff2d5a2c7fd560019fe923e69d3 Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Sun, 23 Dec 2018 22:49:13 +0000 Subject: [PATCH] WIP Procedures for Kustomize-like overlays --- package.json | 2 +- src/cons/data.js | 30 ++++++++++---------- src/cons/overlay.js | 57 ++++++++++++++++++++++++++++++++++++++ tests/data.test.js | 64 +++++++++++++++++++++---------------------- tests/mock.js | 35 +++++++++++++++++++++++ tests/overlay.test.js | 11 ++++++++ 6 files changed, 150 insertions(+), 49 deletions(-) create mode 100644 src/cons/overlay.js create mode 100644 tests/mock.js create mode 100644 tests/overlay.test.js diff --git a/package.json b/package.json index 61d6ccc..b07df47 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "jest": { "testMatch": [ - "/tests/**" + "/tests/*.test.js" ], "transform": { ".*": "/node_modules/babel-jest" diff --git a/src/cons/data.js b/src/cons/data.js index 09da092..b911c86 100644 --- a/src/cons/data.js +++ b/src/cons/data.js @@ -1,20 +1,20 @@ -const obj = (name, value) => { - const o = {}; - o[name] = value; - return o; -}; +// dataFromFiles reads the contents of each file given and returns a +// Map of the filenames to file contents. +async function dataFromFiles(readEncoded, files) { + const result = new Map(); + await Promise.all(files.map(f => readEncoded(f).then(c => { + result.set(f, c); + }))); + return result; +} -// Given `dir` and `read` procedures, construct a map of filename to -// file contents (as strings). +// Given `dir` and `read` procedures, construct a map of file basename +// to file contents (as strings). const dataFromDir = ({ dir, read, Encoding }) => async function data(path) { const d = dir(path); - const files = d.files.filter(({ isdir }) => !isdir); - const data0 = files.map( - ({ name }) => read(`${path}/${name}`, { encoding: Encoding.String }) - .then(str => obj(name, str)), - ); - const data1 = await Promise.all(data0); - return data1.reduce((a, b) => Object.assign(a, b), {}); + const files = d.files.filter(({ isdir }) => !isdir).map(({ name }) => name); + const readFile = f => read(`${path}/${f}`, { encoding: Encoding.String }); + return dataFromFiles(readFile, files); }; -export default dataFromDir; +export { dataFromFiles, dataFromDir }; diff --git a/src/cons/overlay.js b/src/cons/overlay.js new file mode 100644 index 0000000..8621fc5 --- /dev/null +++ b/src/cons/overlay.js @@ -0,0 +1,57 @@ +// This module provides procedures for applying Kustomize-like +// [overlays](https://github.com/kubernetes-sigs/kustomize/blob/master/docs/kustomization.yaml). + +// In Kustomize, a configuration is given in a `kustomize.yaml` file; +// here we'll interpret an object (which can of course be loaded from +// a file). In a `kustomize.yaml` you refer to files from which to +// load or generate resource manifests, and transformations to apply +// to all or some resources. +// +// The mechanism for composing configurations is to name `bases` in +// the `kustomize.yaml` file; these are evaluated and included in the +// resources. +// +// Promise [Resource] +// +// There are different kinds of transformations, but they amount to +// +// Resource -> Resource +// +// The approach taken here is +// 0. load the file +// 1. assemble all the transformations mentioned in various ways in the kustomize object; +// 2. assemble all the resources, mentioned in various ways, in the kustomize object; +// 2. run each resource through the transformations. +// +// Easy peasy! + +// overlay constructs an interpreter which takes an overlay object (as +// would be parsed from a `kustomize.yaml`) and constructs a set of +// resources to write out. +const overlay = ({ read, Encoding }) => async function assemble(path, config) { + const readObj = f => read(`${path}/${f}`, { encoding: Encoding.JSON }); + const { + resources: resourceFiles = [], + bases: baseFiles = [], + patches: patchFiles = [], + } = config; + + const transforms = []; + patchFiles.forEach(f => { + transforms.append(readObj(f).then(interpretPatch)); + }); + + // TODO: add the other kinds of transformation: imageTags, + // globalAnnotations, etc. + + let resources = []; + baseFiles.forEach(f => { + const obj = readObj(`${path}/${f}/kustomize.yaml`); + resources = resources.concat(obj.then(o => assemble(`${path}/${f}`, o))); + }); + + resourceFiles.forEach(f => resources.append(readObj(f))); + return await Promise.all(resources); +} + +export default overlay; diff --git a/tests/data.test.js b/tests/data.test.js index 7005e8a..48086be 100644 --- a/tests/data.test.js +++ b/tests/data.test.js @@ -1,19 +1,4 @@ -import dataFromDir from '../src/cons/data'; - -function dir(path) { - if (path !== 'config') { - throw new Error(`asked for dir of ${path}`); - } - return { - files: [ - {name: 'foo.yaml', isdir: false}, - {name: 'bar.yaml', isdir: false}, - {name: 'baz', isdir: true} - ] - }; -} - -const Encoding = { 'String': "string" }; +import { dataFromFiles, dataFromDir } from '../src/cons/data'; const foo = `--- conf: @@ -28,25 +13,38 @@ stuff: - 3 `; -async function read(path, { encoding }) { - if (encoding != 'string') { - throw new Error(`asked for wrong encoding ${encoding}`); - } - switch (path) { - case 'config/foo.yaml': - return foo; - case 'config/bar.yaml': - return bar; - default: - throw new Error(`asked for not-a-file ${path}`); - } -} +import { fs, Encoding } from './mock'; + +const { dir, read } = fs({ + 'config': { + files: [ + {name: 'foo.yaml', isdir: false}, + {name: 'bar.yaml', isdir: false}, + {name: 'baz', isdir: true} + ] + }, +}, { + 'config/foo.yaml': { string: foo }, + 'config/bar.yaml': { string: bar }, +}); + +test('data from files', () => { + const files = ['config/foo.yaml', 'config/bar.yaml'] + const readFile = f => read(f, { encoding: Encoding.String }); + expect.assertions(1); + dataFromFiles(readFile, files).then(v => { + expect(v).toEqual(new Map([ + ['config/foo.yaml', foo], + ['config/bar.yaml', bar], + ])); + }); +}); test('generate data from dir', () => { expect.assertions(1); const data = dataFromDir({ dir, read, Encoding }); - return data('config').then(d => expect(d).toEqual({ - 'foo.yaml': foo, - 'bar.yaml': bar - })); + return data('config').then(d => expect(d).toEqual(new Map([ + ['foo.yaml', foo], + ['bar.yaml', bar], + ]))); }); diff --git a/tests/mock.js b/tests/mock.js new file mode 100644 index 0000000..51fae14 --- /dev/null +++ b/tests/mock.js @@ -0,0 +1,35 @@ +const dir = dirs => path => { + if (path in dirs) { + return dirs[path]; + } + throw new Error(`path not found ${path}`); +}; + +const read = files => async function r(path, { encoding }) { + if (path in files) { + const encodings = files[path]; + if (encoding in encodings) { + return encodings[encoding]; + } + throw new Error(`no value for encoding "${encoding}"`); + } + throw new Error(`file not found ${path}`); +}; + +function fs(dirs, files) { + return { + dir: dir(dirs), + read: read(files), + Encoding: Encoding, + }; +} + +// It's not important what the values are, just that we use them +// consistently. +const Encoding = Object.freeze({ + String: 'string', + JSON: 'json', + Bytes: 'bytes', +}); + +export { fs, Encoding }; diff --git a/tests/overlay.test.js b/tests/overlay.test.js new file mode 100644 index 0000000..3e93ff9 --- /dev/null +++ b/tests/overlay.test.js @@ -0,0 +1,11 @@ +import overlay from '../src/cons/overlay'; +import { fs, Encoding } from './mock'; + +test('trivial overlay: no bases, resources, patches', () => { + const o = overlay({}); + const { dir, read } = fs({}, {}); + expect.assertions(1); + o('config', {dir, read }).then(v => { + expect(v).toEqual([]); + }); +});