Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for templating in init-container #204

Merged
merged 10 commits into from
May 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ job/
**/report.json
**/report.json
node_modules/
**/vendor/
**/vendor/
init-container/token.txt
init-container/decrypted/**
1 change: 1 addition & 0 deletions init-container/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/**
47 changes: 47 additions & 0 deletions init-container/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Running the init container locally

1. Make sure you have a working Kubernetes cluster (run `kubectl get pods` to check).
2. Start by running the decryptor API - run the following in the decryptor folder (`src/decrypt-api`):
```
dotnet run
```
3. Run the following command to get a valid service account token:
```
kubectl get secret $(kubectl get sa default -o jsonpath='{.secrets[0].name}') -o jsonpath='{.data.token}' | base64 -D > token.txt
```
4. Set the following env var for the init container to work correctly:
```
export set TOKEN_FILE_PATH=$(pwd)/token.txt
export set KAMUS_URL=http://localhost:5000
```
5. Now you can run the init container:
```
node index.js -e encrypted -d decrypted -n output.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain what is encrypted ? is a folder? is it a file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a folder - I put it there with some ready-made encrypted content that is ready for debugging

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, it needs to be more clear in the MD.

```

The first argument (`-e`) point to the folder with the encrypted files, the second (`-d`) point to the folder that will contain the decrypted items and (`-n`) is the decrypted file name.

And you should see a file name `output.json` created under `decrypted` folder with the decrypted content.
Now you can run the init continer locally, debug it and add features.

Having more questions? Something unclear? Reach out to us via [Slack](https://join.slack.com/t/k8s-kamus/shared_invite/enQtNTQwMjc2MzIxMTM3LTgyYTcwMTUxZjJhN2JiMTljMjNmOTBmYjEyNWNmZTRiNjVhNTUyYjMwZDQ0YWQ3Y2FmMTBlODA5MzFlYjYyNWE) or [file an issue](/~https://github.com/Soluto/kamus/issues/new)

## Tests
The tests are using [WireMock](http://wiremock.org/) to mock the decryptor api (you can find ore about it [here](https://www.omerlh.info/2019/02/06/wiremock-for-fun-and-mocking/)).
Running the tests is simple:
1. Build the init container docker image:
```
yarn run build
```
2. Set the environment variable so the tests will use the local image:
```
export set INIT_CONTAINER_IMAGE=init-container
```
3. Run the tests
```
yarn run test
```

Repeat steps 1 & 3 each time you change the code.

The tests simply take various parameters and compare the generated file from the init container with the expected files.
34 changes: 33 additions & 1 deletion init-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,36 @@ The init container accept the following environmenmt variables:
| `-e/--encrypted-folders <path>` | true | Encrypted files folder paths, comma seperated (the volumes mounted with the config map) | |
| `-d/--decrypted-path <path>` | false | Decrypted file/s folder path mounted. Pass this argument to create one decrypted file per encrypted secret | |
| `-n/--decrypted-file-name <name>` | false | Decrypted file name. Pass this argument to create one configuration file with the encrypted secrets. | |
| `-f/--output-format <format>` | false | The format of the output file. Supported types: json, cfg, cfg-strict (surround strings with quotation marks), files | JSON |
| `-f/--output-format <format>` | false | The format of the output file. Supported types: json, cfg, cfg-strict (surround strings with quotation marks), files, custom (see above for more bellow) | JSON |

## Custom templating support
In case you need something more complicated than the support output format, you can provide your own template.
The init container support [EJS](http://ejs.co/) templates, a powerful template engine for nodejs.
To use it, provide a key in the supplied config map called "template.ejs":
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: encrypted-secrets-cm
data:
key: 4AD7lM6lc4dGvE3oF+5w8g==:WrcckiNNOAlMhuWHaM0kTw==
template.ejs: |
<%= secrets["key"] %>
hello
```

This will result in the following file created by the init container:
```
<decrypted value>
hello
```

Look on EJS docummentation for more details, or on one of the existing [templates](/~https://github.com/Soluto/kamus/tree/master/init-container/templates) for ideas on how you can use it. The template input is:
```
{
"secrets": [] //array of the decretyped items, key value pairs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear what is the template input is ?

"stringifyIfJson": function //apply JSON.stringify if the value is object.
}
```

Because the init container support multiple config maps, you can create shared template using config map and mount them where needed. Have a common template? we'll appreciate PRs!
1 change: 1 addition & 0 deletions init-container/encrypted/key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ezBR+Ew+Itwg6fA/tQjxzg==:/DH+kSV3UN8eRUxT/cJp5w==
73 changes: 39 additions & 34 deletions init-container/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@ const readFileAsync = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const axios = require('axios');
const path = require('path');
let ejs = require('ejs');

program
.version('0.1.0')
.option('-e, --encrypted-folders <path>', 'Encrypted files folder paths, comma separated')
.option('-d, --decrypted-path <path>', 'Decrypted file/s folder path')
.option('-n, --decrypted-file-name <name>', 'Decrypted file name' )
.option('-f, --output-format <format>', 'The format of the output file, default to JSON. Supported types: json, cfg, files', /^(json|cfg|cfg-strict|files)$/i, 'json')
.option('-f, --output-format <format>', 'The format of the output file, default to JSON. Supported types: json, cfg, files, custom', /^(json|cfg|cfg-strict|files|custom)$/i, 'json')
.parse(process.argv);

//Source: https://blog.raananweber.com/2015/12/15/check-if-a-directory-exists-in-node-js/
function checkDirectorySync(directory) {
try {
fs.statSync(directory);
} catch(e) {
fs.mkdirSync(directory);
}
}


const getEncryptedFiles = async (folder) => {
return await readfiles(folder, function (err, filename, contents) {
if (err) throw err;
Expand All @@ -30,7 +41,12 @@ const getKamusUrl = () => {
}

const getBarerToken = async () => {
return await readFileAsync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
var tokenFilePath = process.env.TOKEN_FILE_PATH;
if (tokenFilePath == null || tokenFilePath == "")
{
tokenFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
}
return await readFileAsync(tokenFilePath, "utf8");
}

const stringifyIfJson = (secretValue) =>{
Expand All @@ -46,37 +62,12 @@ const decryptFile = async (httpClient, filePath, folder) => {
} catch (e) {
throw new Error(`request to decrypt API failed: ${e.response ? e.response.status : e.message}`)
}
return response.data;
}

const serializeToCfgFormat = (secrets) => {
var output = "";
Object.keys(secrets).forEach(key => {
output += `${key}=${stringifyIfJson(secrets[key])}\n`;
});

output = output.substring(0, output.lastIndexOf('\n'));

return output;
}

const serializeToCfgFormatStrict = (secrets) => {
var output = "";
Object.keys(secrets).forEach(key => {
switch(typeof(secrets[key]))
{
case "string":
output += `${key}="${secrets[key]}"\n`
break;
default:
output += `${key}=${stringifyIfJson(secrets[key])}\n`
}

});

output = output.substring(0, output.lastIndexOf('\n'));

return output;
const writeFileWithTemplate = async (secrets, templateName, outputFile) => {
var template = await readFileAsync(templateName, "utf-8");
var rendered = ejs.render(template, {secrets, stringifyIfJson}, {});
await writeFile(outputFile, rendered);
}

async function innerRun() {
Expand All @@ -90,29 +81,43 @@ async function innerRun() {
});

let secrets = {};
var templatePath = "";
for (let folder of program.encryptedFolders.split(",")) {
let files = await getEncryptedFiles(folder);
for (let file of files) {
if (file === "template.ejs" && program.outputFormat === "custom"){
templatePath = path.join(folder, file)
continue;
}
secrets[file] = await decryptFile(httpClient, file, folder);
}
}

checkDirectorySync(program.decryptedPath);

const outputFile = path.join(program.decryptedPath, program.decryptedFileName);
console.log(`Writing output format using ${program.outputFormat} format to file ${outputFile}`);

switch(program.outputFormat.toLowerCase()){
case "json":
await writeFile(outputFile, JSON.stringify(secrets));
await writeFileWithTemplate(secrets, "templates/json.ejs", outputFile);
break;
case "cfg":
await writeFile(outputFile, serializeToCfgFormat(secrets));
await writeFileWithTemplate(secrets, "templates/cfg.ejs", outputFile);
break;
case "cfg-strict":
await writeFile(outputFile, serializeToCfgFormatStrict(secrets));
await writeFileWithTemplate(secrets, "templates/cfg-strict.ejs", outputFile);
break;
case "files":
await Promise.all(Object.keys(secrets).map(secretName => writeFile(path.join(program.decryptedPath, secretName), stringifyIfJson(secrets[secretName]))));
break;
case "custom":
if (templatePath == "")
{
throw new Error(`Missing template file, cannot write output`);
}
await writeFileWithTemplate(secrets, templatePath, outputFile);
break;
default:
throw new Error(`Unsupported output format: ${program.outputFormat}`);
}
Expand Down
9 changes: 7 additions & 2 deletions init-container/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
{
"name": "init-decryptor",
"version": "1.0.2",
"version": "1.1.0",
"description": "Meant to be used inside init container to read encrypted values from a given folder and decrypt to them into a json in a given folder",
"main": "index.js",
"scripts": {
"build": "docker build . -t init-container && export set INIT_CONTAINER_IMAGE=init-container",
"test": "cd tests && ./run_test.sh",
"snyk-protect": "snyk protect",
"prepublish": "npm run snyk-protect"
"prepublish": "npm run snyk-protect",
"pretest": "ejslint templates/*",
"ejslint": "ejslint templates/*"
},
"author": "Soluto",
"license": "MIT",
"dependencies": {
"axios": "^0.18.0",
"commander": "^2.19.0",
"ejs": "^2.6.1",
"ejs-lint": "^0.3.0",
"node-readfiles": "^0.2.0",
"snyk": "^1.161.1"
},
Expand Down
7 changes: 7 additions & 0 deletions init-container/templates/cfg-strict.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%_ Object.keys(secrets).forEach(function(key){ -%>
<%_ if(typeof(secrets[key]) === "string") { -%>
<%= key %>="<%- secrets[key] %>"
<%_ } else { -%>
<%= key %>=<%- stringifyIfJson(secrets[key]) %>
<%_ } -%>
<%_ }); -%>
3 changes: 3 additions & 0 deletions init-container/templates/cfg.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%_ Object.keys(secrets).forEach(function(key){ -%>
<%= key %>=<%- stringifyIfJson(secrets[key]) %>
<%_ }); -%>
9 changes: 9 additions & 0 deletions init-container/templates/json.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
<%_ Object.keys(secrets).forEach(function(key, index){ -%>
<%_ if(typeof(secrets[key]) === "object") { -%>
"<%= key %>":<%- JSON.stringify(secrets[key]) -%> <%_ if(Object.keys(secrets).length - 1 > index) { -%> , <%_ } %>
<%_ } else { -%>
"<%= key %>":"<%- secrets[key] -%>" <%_ if(Object.keys(secrets).length - 1 > index) { -%> , <%_ } %>
<%_ } -%>
<%_ }); -%>
}
2 changes: 2 additions & 0 deletions init-container/tests/expected-custom.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
super-secret-from-value
hello
2 changes: 1 addition & 1 deletion init-container/tests/expected-strict.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
key.json={"secret_key":"secret_value"}
key1="super-secret-from-value"
key2="super-secret"
key2="super-secret"
2 changes: 1 addition & 1 deletion init-container/tests/expected.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
key.json={"secret_key":"secret_value"}
key1=super-secret-from-value
key2=super-secret
key2=super-secret
6 changes: 5 additions & 1 deletion init-container/tests/expected.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{"key.json":{"secret_key":"secret_value"},"key1":"super-secret-from-value","key2":"super-secret"}
{
"key.json":{"secret_key":"secret_value"} ,
"key1":"super-secret-from-value" ,
"key2":"super-secret"
}
11 changes: 11 additions & 0 deletions init-container/tests/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ diff -q output/out.cfg-strict expected-strict.cfg

rm -rf output

echo "running decryptor - custom format"

cp templates/template.ejs encrypted/
OUTPUT_FORMAT=custom docker-compose run decryptor
rm -rf encrypted/template.ejs
echo "comparing out.custom and expected-custom.txt files"

diff -q output/out.custom expected-custom.txt

rm -rf output

echo "running decryptor - files format"

OUTPUT_FORMAT=files docker-compose run decryptor
Expand Down
2 changes: 2 additions & 0 deletions init-container/tests/templates/template.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<%= secrets["key1"] %>
hello
Loading