Skip to content

Commit

Permalink
WIP Procedures for Kustomize-like overlays
Browse files Browse the repository at this point in the history
  • Loading branch information
squaremo committed Dec 23, 2018
1 parent 0ff19ce commit b081921
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"jest": {
"testMatch": [
"<rootDir>/tests/**"
"<rootDir>/tests/*.test.js"
],
"transform": {
".*": "<rootDir>/node_modules/babel-jest"
Expand Down
30 changes: 15 additions & 15 deletions src/cons/data.js
Original file line number Diff line number Diff line change
@@ -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 };
57 changes: 57 additions & 0 deletions src/cons/overlay.js
Original file line number Diff line number Diff line change
@@ -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;
64 changes: 31 additions & 33 deletions tests/data.test.js
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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],
])));
});
35 changes: 35 additions & 0 deletions tests/mock.js
Original file line number Diff line number Diff line change
@@ -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 };
11 changes: 11 additions & 0 deletions tests/overlay.test.js
Original file line number Diff line number Diff line change
@@ -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([]);
});
});

0 comments on commit b081921

Please sign in to comment.