├── .eslintrc.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── example
├── simple.js
└── subchart.js
├── package.json
├── src
├── chart.js
├── index.js
├── kubernetes.js
├── snapshot.js
├── transform
│ ├── deployment.js
│ ├── deployment.spec.js
│ ├── index.js
│ ├── noop.js
│ ├── pod.js
│ ├── pod.spec.js
│ ├── secret.js
│ ├── secret.spec.js
│ ├── service.js
│ ├── service.spec.js
│ └── utils.js
└── yaml.js
└── test
└── setup.js
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: airbnb-base
3 | env:
4 | node: true
5 | mocha: true
6 | es6: true
7 | parserOptions:
8 | sourceType: strict
9 | rules:
10 | generator-star-spacing:
11 | - 2
12 | - before: true
13 | after: true
14 | no-shadow: 0
15 | require-yield: 0
16 | no-param-reassign: 0
17 | comma-dangle:
18 | - error
19 | - never
20 | no-underscore-dangle: 0
21 | import/no-extraneous-dependencies:
22 | - 2
23 | - devDependencies: true
24 | import/order:
25 | - error
26 | func-names: 0
27 | no-unused-expressions: 0
28 | prefer-arrow-callback: 1
29 | no-use-before-define:
30 | - 2
31 | - functions: false
32 | space-before-function-paren:
33 | - 2
34 | - always
35 | max-len:
36 | - 2
37 | - 120
38 | - 2
39 | semi:
40 | - 2
41 | - never
42 | strict:
43 | - 2
44 | - global
45 | arrow-parens:
46 | - 2
47 | - always
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | output
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## 1.0.3 (2017-06-20)
3 |
4 | * chore(package): update dependencies ([aaae29d](https://github.com/RisingStack/anchor/commit/aaae29d))
5 | * Update pod.js ([6df4552](https://github.com/RisingStack/anchor/commit/6df4552))
6 |
7 |
8 |
9 |
10 | ## 1.0.2 (2017-05-23)
11 |
12 | * fix(transform/secret): data key ([6256c37](https://github.com/RisingStack/anchor/commit/6256c37))
13 | * test(transform/pod): cover with tests ([ce1214c](https://github.com/RisingStack/anchor/commit/ce1214c))
14 | * test(transform/secret): cover with tests ([bc0d1f1](https://github.com/RisingStack/anchor/commit/bc0d1f1))
15 | * test(transform/service): cover with tests ([482b1c4](https://github.com/RisingStack/anchor/commit/482b1c4))
16 | * chore(package): add CHANGELOG.md ([b8adc6d](https://github.com/RisingStack/anchor/commit/b8adc6d))
17 |
18 |
19 |
20 |
21 | ## 1.0.1 (2017-05-22)
22 |
23 | * chore(package): add scope ([68b0f68](https://github.com/RisingStack/anchor/commit/68b0f68))
24 | * chore(package): bump version to 1.0.1 ([d685e4c](https://github.com/RisingStack/anchor/commit/d685e4c))
25 | * test(transform/deployment): cover with tests ([ccff429](https://github.com/RisingStack/anchor/commit/ccff429))
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 RisingStack, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Anchor
2 |
3 | Backups Kubernetes resources as a Helm chart.
4 | Extract configurations from resources and save them as values and templates in a reproducible [Helm](https://github.com/kubernetes/helm) chart.
5 |
6 | 
7 |
8 | ## How to use it?
9 |
10 | ### Requirements
11 |
12 | - Node.js, `>= v6`
13 | - kubectl
14 |
15 | ```sh
16 | npm install @risingstack/anchor
17 | ```
18 |
19 | ### Example
20 |
21 | For more detailed examples like sub-charts check out the `./example` folder.
22 |
23 | ```js
24 | const anchor = require('@risingstack/anchor')
25 |
26 | anchor.snapshot({
27 | resources: [
28 | 'deployment/my-app',
29 | 'deployment/my-worker'
30 | ]
31 | })
32 | .then(() => console.log('Snapshot finished'))
33 | .catch((err) => console.error('Snapshot error', err))
34 | ```
35 |
36 | ## API
37 |
38 | ### anchor.snapshot(options)
39 |
40 | Backup Kubernetes resources as a Helm chart and returns a `Promise`.
41 |
42 | - `options.resources`: Kubernetes resources to snapshot
43 | - **required**
44 | - example: `['deployment/my-app', 'deployment/my-worker']`
45 | - `options.namespace`: Kubernetes namespace for `kubectl`
46 | - *optional*
47 | - default: `default`
48 | - `options.name`: name of the Helm chart
49 | - *optional*
50 | - default: `my-chart`
51 | - `options.description`: description of the Helm chart
52 | - *optional*
53 | - default: `''`
54 | - `options.version`: version of the Helm chart
55 | - *optional*
56 | - default: `0.0.1`
57 | - `options.overwrite`: overwrite output directory
58 | - *optional*
59 | - default: `false`
60 | - `options.outputPath`: defines chart path, throws error when exist but overwrite is `false`
61 | - *optional*
62 | - default: `./output`
63 |
64 | ## How does it work?
65 |
66 | 1. Download Kubernetes resource via `kubectl`
67 | 2. Parse resource, extract values and transform to template
68 | 3. Outputs a Helm chart: YAML templates and `values.yaml`
69 |
70 | ## What can it extract as a template?
71 |
72 | - Containers with name or single container
73 | - Container environment variables with value
74 | - Container image with tag
75 | - Deployment replicas
76 | - Secret data
77 | - Service type
78 |
79 | **TODO:**
80 |
81 | - ConfigMap
82 | - PVC
83 | - Ingress
84 | - Job
85 |
86 | ## Output
87 |
88 | The `./output` directory will contains the templates under the `./output/templates` folder.
89 | Your `Values.yaml` file will look like the following:
90 |
91 | ```yaml
92 | deploymentMyApp:
93 | image: my-company/my-app
94 | imageTag: 1f40c1f
95 | envLogLevel: info
96 | resourcesLimitsCPU: 150m
97 | resourcesLimitsMemory: 1536Mi
98 | resourcesRequestsCPU: 10m
99 | resourcesRequestsMemory: 128Mi
100 | replicas: 2
101 | deploymentMyWorker:
102 | containers:
103 | myWorker:
104 | image: my-company/my-worker
105 | imageTag: 295a9c2
106 | envLogLevel: warning
107 | envTraceServiceName: my-worker
108 | resourcesLimitsCPU: 200m
109 | resourcesLimitsMemory: 1536Mi
110 | resourcesRequestsCPU: 20m
111 | resourcesRequestsMemory: 128Mi
112 | mySidecar:
113 | image: my-company/metrics-exporter
114 | imageTag: aa1c434
115 | replicas: 2
116 | ```
117 |
118 | ## Feature ideas:
119 |
120 | - Versioning
121 | - Snapshot reload by date
122 | - Auto-sync *(auto snapshot)*
123 |
--------------------------------------------------------------------------------
/example/simple.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const anchor = require('../src')
4 |
5 | /*
6 | * In this example we create a snapshot from our resources.
7 | *
8 | * "Security" is our imaginary application group: with one service, one secret and two deployments.
9 | * Our resources are: service/security, ssecret/ecurity-pg, deployment/security-web and deployment/security-worker
10 | */
11 |
12 | anchor.snapshot({
13 | overwrite: true,
14 | name: 'my-chart',
15 | description: 'Backup of my-chart',
16 | version: '1.0.0',
17 | namespace: 'staging',
18 | resources: [
19 | 'deployment/access-web',
20 | 'deployment/security-worker',
21 | 'secret/security-pg',
22 | 'service/security'
23 | ]
24 | })
25 | .then((chartResources) => {
26 | const resourceNames = chartResources.map((chartResource) => chartResource.resource)
27 | // eslint-disable-next-line no-console
28 | console.info(`Snapshot finished for: ${resourceNames.join(', ')}`)
29 | })
30 | // eslint-disable-next-line no-console
31 | .catch((err) => console.error('Snapshot error', err))
32 |
--------------------------------------------------------------------------------
/example/subchart.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const anchor = require('../src')
5 |
6 | const DEFAULT_OUTPUT_PATH = path.join(__dirname, '../output')
7 |
8 | /*
9 | * In this example we create a main chart with common resources and two sub-charts for the deployments.
10 | *
11 | * "Security" is our imaginary application group: with one service, one secret and two deployments.
12 | * Our resources are: service/security, ssecret/ecurity-pg, deployment/security-web and deployment/security-worker
13 | */
14 |
15 | Promise.all([
16 | // main-chart: security (contains common resources: secret)
17 | anchor.snapshot({
18 | outputPath: DEFAULT_OUTPUT_PATH,
19 | overwrite: true,
20 | name: 'my-chart',
21 | description: 'Backup of security',
22 | version: '1.0.0',
23 | namespace: 'staging',
24 | resources: [
25 | 'secret/security-pg'
26 | ]
27 | }),
28 | // sub-chart: security/charts/web (contains web related resources: deployment and service)
29 | anchor.snapshot({
30 | outputPath: path.join(DEFAULT_OUTPUT_PATH, '/charts/web'),
31 | overwrite: true,
32 | name: 'web',
33 | description: 'Backup of security web',
34 | version: '1.0.0',
35 | namespace: 'staging',
36 | resources: [
37 | 'service/security',
38 | 'deployment/security-web'
39 | ]
40 | }),
41 | // sub-chart: security/charts/worker (contains worker related resources: deployment)
42 | anchor.snapshot({
43 | outputPath: path.join(DEFAULT_OUTPUT_PATH, '/charts/worker'),
44 | overwrite: true,
45 | name: 'worker',
46 | description: 'Backup of security worker',
47 | version: '1.0.0',
48 | namespace: 'staging',
49 | resources: [
50 | 'deployment/security-worker'
51 | ]
52 | })
53 | ])
54 | .then(() => {
55 | // eslint-disable-next-line no-console
56 | console.info('Snapshot finished')
57 | process.exit(0)
58 | })
59 | .catch((err) => {
60 | // eslint-disable-next-line no-console
61 | console.error('Snapshot error', err)
62 | process.exit(-1)
63 | })
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@risingstack/anchor",
3 | "version": "1.0.3",
4 | "description": "Creates Helm charts from Kubernetes resources.",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "mocha test/setup.js 'src/**/*.spec.js'",
8 | "lint": "eslint test src"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/RisingStack/anchor.git"
13 | },
14 | "keywords": [
15 | "kubernetes",
16 | "k8s",
17 | "helm",
18 | "helm chart",
19 | "backup"
20 | ],
21 | "author": "RisingStack, Inc.",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/RisingStack/anchor/issues"
25 | },
26 | "homepage": "https://github.com/RisingStack/anchor#readme",
27 | "dependencies": {
28 | "js-yaml": "3.8.4",
29 | "lodash": "4.17.4",
30 | "mkdirp": "0.5.1",
31 | "rimraf": "2.6.1"
32 | },
33 | "devDependencies": {
34 | "chai": "4.0.2",
35 | "eslint": "4.0.0",
36 | "eslint-config-airbnb-base": "11.2.0",
37 | "eslint-plugin-import": "2.3.0",
38 | "eslint-plugin-promise": "3.5.0",
39 | "mocha": "3.4.2",
40 | "pre-commit": "1.2.2",
41 | "sinon": "2.3.5",
42 | "sinon-chai": "2.11.0"
43 | },
44 | "pre-commit": [
45 | "lint",
46 | "test"
47 | ],
48 | "engines": {
49 | "node": ">=6.0.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/chart.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const fs = require('fs')
5 | const rimraf = require('rimraf')
6 | const mkdirp = require('mkdirp')
7 | const { toYAML } = require('./yaml')
8 |
9 | const DEFAULT_OUTPUT_PATH = path.join(__dirname, '../output')
10 |
11 | function init ({
12 | overwrite = false,
13 | outputPath = DEFAULT_OUTPUT_PATH,
14 | name = 'my-chart',
15 | description = '',
16 | version = '0.1.0'
17 | }) {
18 | const outputTemplatePath = path.join(outputPath, 'templates')
19 | const filePath = path.join(outputPath, 'Chart.yaml')
20 | const chart = {
21 | apiVersion: 'v1',
22 | description,
23 | name,
24 | version
25 | }
26 |
27 | // Recreate folder
28 | if (overwrite) {
29 | rimraf.sync(outputPath)
30 | mkdirp.sync(outputPath)
31 | }
32 | mkdirp.sync(outputTemplatePath)
33 |
34 | return saveToYAMLFile(filePath, chart)
35 | }
36 |
37 | function saveTemplate (outputPath = DEFAULT_OUTPUT_PATH, chartResource) {
38 | const fileName = `templates/${chartResource.resourceType}-${chartResource.resourceName}.yaml`
39 | const filePath = path.join(outputPath, fileName)
40 | return saveToYAMLFile(filePath, chartResource.chart)
41 | }
42 |
43 | function saveValues (outputPath = DEFAULT_OUTPUT_PATH, values) {
44 | const filePath = path.join(outputPath, 'values.yaml')
45 | return saveToYAMLFile(filePath, values)
46 | }
47 |
48 | function saveToYAMLFile (filePath, data) {
49 | const output = toYAML(data)
50 | return saveToFile(filePath, output)
51 | }
52 |
53 | function saveToFile (filePath, output) {
54 | fs.writeFileSync(filePath, output, 'utf-8')
55 | return Promise.resolve()
56 | }
57 |
58 | module.exports = {
59 | init,
60 | saveTemplate,
61 | saveValues
62 | }
63 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const snapshot = require('./snapshot')
4 |
5 | module.exports = {
6 | snapshot
7 | }
8 |
--------------------------------------------------------------------------------
/src/kubernetes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const childProcess = require('child_process')
4 |
5 | // TODO: use API
6 | function getResource (namespace, resource) {
7 | const output = childProcess.execSync(
8 | `kubectl --namespace=${namespace} get ${resource} --output=json --export`
9 | )
10 | let data
11 |
12 | try {
13 | data = JSON.parse(output)
14 | } catch (err) {
15 | return Promise.reject(err)
16 | }
17 |
18 | return Promise.resolve(data)
19 | }
20 |
21 | module.exports = {
22 | getResource
23 | }
24 |
--------------------------------------------------------------------------------
/src/snapshot.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fp = require('lodash/fp')
4 | const kubernetes = require('./kubernetes')
5 | const transform = require('./transform')
6 | const chart = require('./chart')
7 |
8 | function snapshot ({
9 | overwrite = false,
10 | outputPath,
11 | name,
12 | description,
13 | version,
14 | namespace = 'default',
15 | resources
16 | }) {
17 | const getAndTransformResources = resources.map((resource) =>
18 | kubernetes.getResource(namespace, resource)
19 | .then((data) => {
20 | const resourceTmp = resource.split('/')
21 | const resourceType = resourceTmp[0]
22 | const resourceName = resourceTmp[1]
23 | const scope = fp.camelCase(resource)
24 | const resourceTransformer = transform[resourceType] || transform.noop
25 | const chart = transform.toChart([resourceTransformer], data, scope)
26 | // FIXME: workaround to skip undefined values
27 | const output = JSON.parse(JSON.stringify(chart))
28 |
29 | return Promise.resolve(Object.assign(output, {
30 | resource,
31 | resourceType,
32 | resourceName,
33 | scope
34 | }))
35 | })
36 | )
37 |
38 | return Promise.all(getAndTransformResources)
39 | .then((chartResources) => chart.init({ overwrite, outputPath, name, description, version })
40 | .then(() => chartResources)
41 | )
42 | .then((chartResources) => {
43 | const values = chartResources.reduce((values, chartResource) =>
44 | // Support both scoped and unscoped values (sub-charts)
45 | Object.assign(values, chartResource.scope
46 | ? {
47 | [chartResource.scope]: chartResource.values
48 | }
49 | : chartResource.values
50 | ), {})
51 |
52 | const saveTemplates = chartResources.map((chartResource) =>
53 | chart.saveTemplate(outputPath, chartResource)
54 | )
55 |
56 | return Promise.all([
57 | chart.saveValues(outputPath, values),
58 | saveTemplates
59 | ])
60 | .then(() => chartResources)
61 | })
62 | }
63 |
64 | module.exports = snapshot
65 |
--------------------------------------------------------------------------------
/src/transform/deployment.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fp = require('lodash/fp')
4 | const pod = require('./pod')
5 |
6 | function transform (inputValues, inputChart, scope) {
7 | const valuesPrefix = scope ? `.${scope}` : ''
8 | let values = Object.assign({}, inputValues)
9 | let chart = Object.assign({}, inputChart)
10 |
11 | // Pod
12 | const deploymentPod = pod.transform(inputValues, inputChart.spec.template, scope)
13 |
14 | values = fp.merge(values)(deploymentPod.values)
15 | chart = fp.merge(chart)({
16 | spec: {
17 | template: deploymentPod.chart
18 | }
19 | })
20 |
21 | // Replicas
22 | if (chart.spec.replicas) {
23 | values.replicas = chart.spec.replicas
24 | chart.spec.replicas = `{{ .Values${valuesPrefix}.replicas | default ${values.replicas} }}`
25 | }
26 |
27 | chart.status = undefined
28 | chart.metadata.selfLink = undefined
29 | chart.metadata.creationTimestamp = undefined
30 |
31 | return {
32 | values,
33 | chart
34 | }
35 | }
36 |
37 | module.exports = transform
38 | module.exports.transform = transform
39 |
--------------------------------------------------------------------------------
/src/transform/deployment.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { expect } = require('chai')
4 | const deploymentTransform = require('./deployment')
5 | const pod = require('./pod')
6 |
7 | describe('Transform Deployment', () => {
8 | let deploymentChart
9 |
10 | beforeEach(() => {
11 | deploymentChart = {
12 | metadata: {
13 | selfLink: 'self/deployment/foo',
14 | creationTimestamp: 1234
15 | },
16 | spec: {
17 | replicas: 2,
18 | template: {
19 | spec: {
20 | name: 'My Pod',
21 | containers: [
22 | {
23 | image: 'risingstack/foo:v1'
24 | }
25 | ]
26 | }
27 | }
28 | }
29 | }
30 | })
31 |
32 | describe('spec.template transform', () => {
33 | it('should call pod transform with default values', function () {
34 | this.sandbox.spy(pod, 'transform')
35 |
36 | deploymentTransform({}, deploymentChart)
37 | expect(pod.transform).to.be.calledWith({}, deploymentChart.spec.template, undefined)
38 | })
39 |
40 | it('should call pod transform with initial values', function () {
41 | this.sandbox.spy(pod, 'transform')
42 |
43 | deploymentTransform({ foo: 'bar' }, deploymentChart)
44 | expect(pod.transform).to.be.calledWith({ foo: 'bar' }, deploymentChart.spec.template, undefined)
45 | })
46 |
47 | it('should call pod transform with scope', function () {
48 | this.sandbox.spy(pod, 'transform')
49 |
50 | deploymentTransform({}, deploymentChart, 'myScope')
51 | expect(pod.transform).to.be.calledWith({}, deploymentChart.spec.template, 'myScope')
52 | })
53 |
54 | it('should merge pod transform values and chart', function () {
55 | this.sandbox.stub(pod, 'transform').returns({
56 | values: { pod: 'pod-value' },
57 | chart: { spec: { containers: [] } }
58 | })
59 |
60 | const { values, chart } = deploymentTransform({ initial: 'initial-value' }, deploymentChart)
61 | expect(chart.spec.template.spec).to.be.eql({
62 | name: 'My Pod',
63 | containers: [
64 | {
65 | image: 'risingstack/foo:v1'
66 | }
67 | ]
68 | })
69 | expect(values).to.have.property('initial', 'initial-value')
70 | expect(values).to.have.property('pod', 'pod-value')
71 | })
72 | })
73 |
74 | describe('spec.replicas transform', () => {
75 | it('should transform replicas', () => {
76 | const { values, chart } = deploymentTransform({}, deploymentChart)
77 |
78 | expect(values.replicas).to.be.equal(2)
79 | expect(chart.spec.replicas).to.be.equal('{{ .Values.replicas | default 2 }}')
80 | })
81 |
82 | it('should respect scope', () => {
83 | const { values, chart } = deploymentTransform({}, deploymentChart, 'myDeployment')
84 |
85 | expect(values.replicas).to.be.equal(2)
86 | expect(chart.spec.replicas).to.be.equal('{{ .Values.myDeployment.replicas | default 2 }}')
87 | })
88 | })
89 |
90 | describe('meta and status', () => {
91 | it('should cleanup meta and status', () => {
92 | const { chart } = deploymentTransform({}, deploymentChart)
93 |
94 | expect(chart.metadata.selfLink).to.be.equal(undefined)
95 | expect(chart.metadata.creationTimestamp).to.be.equal(undefined)
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/transform/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const deployment = require('./deployment')
4 | const pod = require('./pod')
5 | const secret = require('./secret')
6 | const service = require('./service')
7 | const noop = require('./noop')
8 | const utils = require('./utils')
9 |
10 | module.exports = {
11 | deployment,
12 | pod,
13 | secret,
14 | service,
15 | noop,
16 | toChart: utils.toChart
17 | }
18 |
--------------------------------------------------------------------------------
/src/transform/noop.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function transform (inputValues, inputChart) {
4 | return {
5 | values: inputValues,
6 | chart: inputChart
7 | }
8 | }
9 |
10 | module.exports = transform
11 |
--------------------------------------------------------------------------------
/src/transform/pod.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fp = require('lodash/fp')
4 |
5 | function transformContainers (inputValues, inputChart, scope = '') {
6 | const valuesPrefix = scope ? `.Values.${scope}` : '.Values'
7 | const values = Object.assign({}, inputValues)
8 | const chart = Object.assign({}, inputChart)
9 |
10 | chart.spec.containers = chart.spec.containers.map((container, key) => {
11 | const containerName = fp.camelCase(container.name) || key.toString()
12 | let templateValuePathPrefix
13 | let containerValues
14 |
15 | // Simplify with one container
16 | if (chart.spec.containers.length > 1) {
17 | templateValuePathPrefix = `${valuesPrefix}.containers.${containerName}`
18 |
19 | values.containers = values.containers || {}
20 | values.containers[containerName] = values.containers[containerName] || {}
21 | containerValues = values.containers[containerName]
22 | } else {
23 | templateValuePathPrefix = `${valuesPrefix}`
24 | containerValues = values
25 | }
26 |
27 | // Image
28 | const tmp = container.image.split(':')
29 | const image = tmp[0]
30 | const imageTag = tmp[1]
31 |
32 | containerValues.image = image
33 | if (imageTag) {
34 | // prevent casting to Number
35 | containerValues.imageTag = imageTag.toString()
36 | }
37 |
38 | container.image = `"{{ ${templateValuePathPrefix}.image }}:`
39 | + `{{ ${templateValuePathPrefix}.imageTag }}"`
40 |
41 | // Environment variables
42 | if (container.env) {
43 | container.env = container.env.map((env) => {
44 | if (!env.value) {
45 | return env
46 | }
47 |
48 | const envName = fp.camelCase(`env_${env.name}`)
49 |
50 | containerValues[envName] = env.value
51 |
52 | return Object.assign({}, env, {
53 | value: `{{ ${templateValuePathPrefix}.${envName} | quote }}`
54 | })
55 | })
56 | }
57 |
58 | // Resources
59 | if (container.resources) {
60 | if (container.resources.limits) {
61 | if (container.resources.limits.cpu) {
62 | containerValues.resourcesLimitsCPU = container.resources.limits.cpu
63 | container.resources.limits.cpu = `{{ ${templateValuePathPrefix}`
64 | + `.resourcesLimitsCPU | default "${container.resources.limits.cpu}" }}`
65 | }
66 | if (container.resources.limits.memory) {
67 | containerValues.resourcesLimitsMemory = container.resources.limits.memory
68 | container.resources.limits.memory = `{{ ${templateValuePathPrefix}`
69 | + `.resourcesLimitsMemory | default "${container.resources.limits.memory}" }}`
70 | }
71 | }
72 |
73 | if (container.resources.requests) {
74 | if (container.resources.requests.cpu) {
75 | containerValues.resourcesRequestsCPU = container.resources.requests.cpu
76 | container.resources.requests.cpu = `{{ ${templateValuePathPrefix}`
77 | + `.resourcesRequestsCPU | default "${container.resources.requests.cpu}" }}`
78 | }
79 | if (container.resources.requests.memory) {
80 | containerValues.resourcesRequestsMemory = container.resources.requests.memory
81 | container.resources.requests.memory = `{{ ${templateValuePathPrefix}`
82 | + `.resourcesRequestsMemory | default "${container.resources.requests.memory}" }}`
83 | }
84 | }
85 | }
86 |
87 | return container
88 | })
89 |
90 | if (chart.metadata) {
91 | chart.metadata.creationTimestamp = undefined
92 | }
93 |
94 | return {
95 | values,
96 | chart
97 | }
98 | }
99 |
100 | function transform (inputValues, inputChart, scope = '') {
101 | const containers = transformContainers(inputValues, inputChart, scope)
102 | const values = Object.assign({}, containers.values)
103 | const chart = Object.assign({}, containers.chart)
104 |
105 | return {
106 | values,
107 | chart
108 | }
109 | }
110 |
111 | module.exports = transform
112 | module.exports.transform = transform
113 |
--------------------------------------------------------------------------------
/src/transform/pod.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { expect } = require('chai')
4 | const podTransform = require('./pod')
5 |
6 | describe('Transform Pod', () => {
7 | let podChart
8 | let container1
9 | let container2
10 |
11 | beforeEach(() => {
12 | container1 = {
13 | name: 'my-service',
14 | image: 'risingstack/my-service:v1',
15 | env: [
16 | {
17 | name: 'NODE_ENV',
18 | valur: 'production'
19 | },
20 | {
21 | name: 'REDIS_URI',
22 | valur: 'redis://myredis'
23 | }
24 | ],
25 | resources: {
26 | limits: { cpu: '50m', memory: '1536Mi' },
27 | requests: { cpu: '40m', memory: '256Mi' }
28 | }
29 | }
30 |
31 | container2 = {
32 | name: 'foo',
33 | image: 'risingstack/foo:v2',
34 | env: [
35 | {
36 | name: 'PG_URI',
37 | valur: 'postgres://mypg'
38 | }
39 | ],
40 | resources: {
41 | limits: { cpu: '100m', memory: '1536Mi' },
42 | requests: { cpu: '80m', memory: '128Mi' }
43 | }
44 | }
45 |
46 | podChart = {
47 | metadata: {
48 | creationTimestamp: 1234
49 | },
50 | spec: {
51 | containers: [
52 | container1,
53 | container2
54 | ]
55 | }
56 | }
57 | })
58 |
59 | describe('spec.containers transform', () => {
60 | describe('with multiple containers', () => {
61 | it('should extract image and resources', () => {
62 | const { values, chart } = podTransform({}, podChart)
63 |
64 | expect(values.containers).to.be.eql({
65 | myService: {
66 | image: 'risingstack/my-service',
67 | imageTag: 'v1',
68 | resourcesLimitsCPU: '50m',
69 | resourcesLimitsMemory: '1536Mi',
70 | resourcesRequestsCPU: '40m',
71 | resourcesRequestsMemory: '256Mi'
72 | },
73 | foo: {
74 | image: 'risingstack/foo',
75 | imageTag: 'v2',
76 | resourcesLimitsCPU: '100m',
77 | resourcesLimitsMemory: '1536Mi',
78 | resourcesRequestsCPU: '80m',
79 | resourcesRequestsMemory: '128Mi'
80 | }
81 | })
82 |
83 | // Container 0
84 | expect(chart.spec.containers[0].image).to.be
85 | .equal('"{{ .Values.containers.myService.image }}:{{ .Values.containers.myService.imageTag }}"')
86 | expect(chart.spec.containers[0].resources).to.be.eql({
87 | limits: {
88 | cpu: '{{ .Values.containers.myService.resourcesLimitsCPU | default "50m" }}',
89 | memory: '{{ .Values.containers.myService.resourcesLimitsMemory | default "1536Mi" }}'
90 | },
91 | requests: {
92 | cpu: '{{ .Values.containers.myService.resourcesRequestsCPU | default "40m" }}',
93 | memory: '{{ .Values.containers.myService.resourcesRequestsMemory | default "256Mi" }}'
94 | }
95 | })
96 |
97 | // Container 1
98 | expect(chart.spec.containers[1].image).to.be
99 | .equal('"{{ .Values.containers.foo.image }}:{{ .Values.containers.foo.imageTag }}"')
100 | expect(chart.spec.containers[1].resources).to.be.eql({
101 | limits: {
102 | cpu: '{{ .Values.containers.foo.resourcesLimitsCPU | default "100m" }}',
103 | memory: '{{ .Values.containers.foo.resourcesLimitsMemory | default "1536Mi" }}'
104 | },
105 | requests: {
106 | cpu: '{{ .Values.containers.foo.resourcesRequestsCPU | default "80m" }}',
107 | memory: '{{ .Values.containers.foo.resourcesRequestsMemory | default "128Mi" }}'
108 | }
109 | })
110 | })
111 |
112 | it('should respect scope', () => {
113 | const { chart } = podTransform({}, podChart, 'myPod')
114 |
115 | // Container 0
116 | expect(chart.spec.containers[0].image).to.be
117 | .equal('"{{ .Values.myPod.containers.myService.image }}:{{ .Values.myPod.containers.myService.imageTag }}"')
118 | expect(chart.spec.containers[0].resources).to.be.eql({
119 | limits: {
120 | cpu: '{{ .Values.myPod.containers.myService.resourcesLimitsCPU | default "50m" }}',
121 | memory: '{{ .Values.myPod.containers.myService.resourcesLimitsMemory | default "1536Mi" }}'
122 | },
123 | requests: {
124 | cpu: '{{ .Values.myPod.containers.myService.resourcesRequestsCPU | default "40m" }}',
125 | memory: '{{ .Values.myPod.containers.myService.resourcesRequestsMemory | default "256Mi" }}'
126 | }
127 | })
128 |
129 | // Container 1
130 | expect(chart.spec.containers[1].image).to.be
131 | .equal('"{{ .Values.myPod.containers.foo.image }}:{{ .Values.myPod.containers.foo.imageTag }}"')
132 | expect(chart.spec.containers[1].resources).to.be.eql({
133 | limits: {
134 | cpu: '{{ .Values.myPod.containers.foo.resourcesLimitsCPU | default "100m" }}',
135 | memory: '{{ .Values.myPod.containers.foo.resourcesLimitsMemory | default "1536Mi" }}'
136 | },
137 | requests: {
138 | cpu: '{{ .Values.myPod.containers.foo.resourcesRequestsCPU | default "80m" }}',
139 | memory: '{{ .Values.myPod.containers.foo.resourcesRequestsMemory | default "128Mi" }}'
140 | }
141 | })
142 | })
143 | })
144 |
145 | describe('with single container', () => {
146 | beforeEach(() => {
147 | podChart.spec.containers = [container1]
148 | })
149 |
150 | it('should extract image and resources', () => {
151 | const { values, chart } = podTransform({}, podChart)
152 |
153 | expect(values).to.be.eql({
154 | image: 'risingstack/my-service',
155 | imageTag: 'v1',
156 | resourcesLimitsCPU: '50m',
157 | resourcesLimitsMemory: '1536Mi',
158 | resourcesRequestsCPU: '40m',
159 | resourcesRequestsMemory: '256Mi'
160 | })
161 |
162 | expect(chart.spec.containers[0].image).to.be.equal('"{{ .Values.image }}:{{ .Values.imageTag }}"')
163 | expect(chart.spec.containers[0].resources).to.be.eql({
164 | limits: {
165 | cpu: '{{ .Values.resourcesLimitsCPU | default "50m" }}',
166 | memory: '{{ .Values.resourcesLimitsMemory | default "1536Mi" }}'
167 | },
168 | requests: {
169 | cpu: '{{ .Values.resourcesRequestsCPU | default "40m" }}',
170 | memory: '{{ .Values.resourcesRequestsMemory | default "256Mi" }}'
171 | }
172 | })
173 | })
174 |
175 | it('should respect scope', () => {
176 | const { chart } = podTransform({}, podChart, 'myPod')
177 |
178 | expect(chart.spec.containers[0].image).to.be.equal('"{{ .Values.myPod.image }}:{{ .Values.myPod.imageTag }}"')
179 | expect(chart.spec.containers[0].resources).to.be.eql({
180 | limits: {
181 | cpu: '{{ .Values.myPod.resourcesLimitsCPU | default "50m" }}',
182 | memory: '{{ .Values.myPod.resourcesLimitsMemory | default "1536Mi" }}'
183 | },
184 | requests: {
185 | cpu: '{{ .Values.myPod.resourcesRequestsCPU | default "40m" }}',
186 | memory: '{{ .Values.myPod.resourcesRequestsMemory | default "256Mi" }}'
187 | }
188 | })
189 | })
190 | })
191 | })
192 |
193 | describe('meta', () => {
194 | it('should cleanup meta', () => {
195 | const { chart } = podTransform({}, podChart)
196 |
197 | expect(chart.metadata.creationTimestamp).to.be.equal(undefined)
198 | })
199 | })
200 | })
201 |
--------------------------------------------------------------------------------
/src/transform/secret.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const _ = require('lodash')
4 |
5 | function transform (inputValues, inputChart, scope) {
6 | const valuesPrefix = scope ? `.${scope}` : ''
7 | const values = Object.assign({}, inputValues)
8 | const chart = Object.assign({}, inputChart)
9 |
10 | chart.data = _.reduce(chart.data, (data, dataValue, dataKey) => {
11 | const key = _.camelCase(dataKey)
12 |
13 | values[key] = new Buffer(dataValue, 'base64').toString('utf-8')
14 | data[dataKey] = `{{ .Values${valuesPrefix}.${key} | b64enc }}`
15 |
16 | return data
17 | }, {})
18 |
19 | chart.metadata.selfLink = undefined
20 | chart.metadata.creationTimestamp = undefined
21 |
22 | return {
23 | values,
24 | chart
25 | }
26 | }
27 |
28 | module.exports = transform
29 |
--------------------------------------------------------------------------------
/src/transform/secret.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { expect } = require('chai')
4 | const secretTransform = require('./secret')
5 |
6 | describe('Transform Secret', () => {
7 | let secretChart
8 |
9 | beforeEach(() => {
10 | secretChart = {
11 | name: 'my-secret',
12 | metadata: {
13 | creationTimestamp: 1234,
14 | selfLink: '/self/secret'
15 | },
16 | data: {
17 | 'so-secret': new Buffer('so-secret').toString('base64'),
18 | foo_bar: new Buffer('foo-secret').toString('base64'),
19 | suchWow: new Buffer('such-secret').toString('base64')
20 | }
21 | }
22 | })
23 |
24 | describe('spec.data transform', () => {
25 | it('should extract data', () => {
26 | const { values, chart } = secretTransform({}, secretChart)
27 |
28 | expect(values).to.be.eql({
29 | soSecret: 'so-secret',
30 | fooBar: 'foo-secret',
31 | suchWow: 'such-secret'
32 | })
33 |
34 | expect(chart.data).to.be.eql({
35 | 'so-secret': '{{ .Values.soSecret | b64enc }}',
36 | foo_bar: '{{ .Values.fooBar | b64enc }}',
37 | suchWow: '{{ .Values.suchWow | b64enc }}'
38 | })
39 | })
40 |
41 | it('should respect scope', () => {
42 | const { chart } = secretTransform({}, secretChart, 'mySecret')
43 |
44 | expect(chart.data).to.be.eql({
45 | 'so-secret': '{{ .Values.mySecret.soSecret | b64enc }}',
46 | foo_bar: '{{ .Values.mySecret.fooBar | b64enc }}',
47 | suchWow: '{{ .Values.mySecret.suchWow | b64enc }}'
48 | })
49 | })
50 | })
51 |
52 | describe('meta', () => {
53 | it('should cleanup meta', () => {
54 | const { chart } = secretTransform({}, secretChart)
55 |
56 | expect(chart.metadata.selfLink).to.be.equal(undefined)
57 | expect(chart.metadata.creationTimestamp).to.be.equal(undefined)
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/src/transform/service.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function transform (inputValues, inputChart, scope) {
4 | const valuesPrefix = scope ? `.${scope}` : ''
5 | const values = Object.assign({}, inputValues)
6 | const chart = Object.assign({}, inputChart)
7 |
8 | values.type = chart.spec.type
9 | chart.spec.type = `{{ .Values${valuesPrefix}.type | default "${values.type}" }}`
10 |
11 | chart.metadata.selfLink = undefined
12 | chart.metadata.creationTimestamp = undefined
13 | chart.status = undefined
14 | chart.spec.clusterIP = undefined
15 |
16 | return {
17 | values,
18 | chart
19 | }
20 | }
21 |
22 | module.exports = transform
23 |
--------------------------------------------------------------------------------
/src/transform/service.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { expect } = require('chai')
4 | const serviceTransform = require('./service')
5 |
6 | describe('Transform Service', () => {
7 | let serviceChart
8 |
9 | beforeEach(() => {
10 | serviceChart = {
11 | name: 'my-service',
12 | metadata: {
13 | creationTimestamp: 1234,
14 | selfLink: '/self/service'
15 | },
16 | spec: {
17 | type: 'LoadBalancer',
18 | clusterIP: '1.2.3.4'
19 | },
20 | status: ''
21 | }
22 | })
23 |
24 | describe('spec.data transform', () => {
25 | it('should extract data', () => {
26 | const { values, chart } = serviceTransform({}, serviceChart)
27 |
28 | expect(values).to.be.eql({
29 | type: 'LoadBalancer'
30 | })
31 |
32 | expect(chart.spec.type).to.be.equal('{{ .Values.type | default "LoadBalancer" }}')
33 | })
34 |
35 | it('should respect scope', () => {
36 | const { chart } = serviceTransform({}, serviceChart, 'myService')
37 |
38 | expect(chart.spec.type).to.be.equal('{{ .Values.myService.type | default "LoadBalancer" }}')
39 | })
40 | })
41 |
42 | describe('meta, status', () => {
43 | it('should cleanup meta, clusterIP and status', () => {
44 | const { chart } = serviceTransform({}, serviceChart)
45 |
46 | expect(chart.metadata.selfLink).to.be.equal(undefined)
47 | expect(chart.metadata.creationTimestamp).to.be.equal(undefined)
48 | expect(chart.spec.clusterIP).to.be.equal(undefined)
49 | expect(chart.status).to.be.equal(undefined)
50 | })
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/src/transform/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function toChart (transformers, inputChart, scope = '') {
4 | const result = transformers.reduce((result, transformer) => {
5 | const transformResult = transformer(result.values, result.chart, scope)
6 |
7 | const values = Object.assign(result.values, transformResult.values)
8 | const chart = Object.assign(result.chart, transformResult.chart)
9 |
10 | return { values, chart }
11 | }, {
12 | values: {},
13 | chart: inputChart
14 | })
15 |
16 | return result
17 | }
18 |
19 | module.exports = {
20 | toChart
21 | }
22 |
--------------------------------------------------------------------------------
/src/yaml.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const yaml = require('js-yaml')
4 |
5 | function toYAML (data) {
6 | const output = yaml.dump(data, {
7 | lineWidth: Infinity // prevent line breaks
8 | })
9 | return fixYAML(output)
10 | }
11 |
12 | // Remove unnecessary quotes
13 | function fixYAML (str) {
14 | return str
15 | .replace(/'{{/g, '{{')
16 | .replace(/}}'/g, '}}')
17 | }
18 |
19 | module.exports = {
20 | toYAML
21 | }
22 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const sinon = require('sinon')
4 | const chai = require('chai')
5 | const sinonChai = require('sinon-chai')
6 |
7 | before(() => {
8 | chai.use(sinonChai)
9 | })
10 |
11 | beforeEach(function beforeEach () {
12 | this.sandbox = sinon.sandbox.create()
13 | })
14 |
15 | afterEach(function afterEach () {
16 | this.sandbox.restore()
17 | })
18 |
--------------------------------------------------------------------------------