├── .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 | ![Anchor - Kubernetes backup](https://cloud.githubusercontent.com/assets/1764512/26036522/775a4ef2-38df-11e7-8ee0-a45e70578495.png) 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 | --------------------------------------------------------------------------------