Skip to content

Commit

Permalink
feat: Add the ability to deploy a node app to Kubernetes(Minikube)
Browse files Browse the repository at this point in the history
* This feature adds the ability to deploy a node application to a vanilla kubernetes cluster.  Only Minikube is supported at the moment.
  • Loading branch information
lholmquist committed Jan 4, 2021
1 parent 3873041 commit 3e03c00
Show file tree
Hide file tree
Showing 29 changed files with 1,140 additions and 54 deletions.
22 changes: 21 additions & 1 deletion README.md
Expand Up @@ -5,7 +5,7 @@

## What is it

Nodeshift is an opinionated command line application and programmable API that you can use to deploy Node.js projects to OpenShift.
Nodeshift is an opinionated command line application and programmable API that you can use to deploy Node.js projects to OpenShift and Kubernetes(minikube).

## Prerequisites

Expand Down Expand Up @@ -49,6 +49,8 @@ By default, if you run just `nodeshift`, it will run the `deploy` goal, which is

The `.nodeshift` directory contains your resource fragments. These are `.yml` files that describe your services, deployments, routes, etc. By default, nodeshift will create a `Service` and `DeploymentConfig` in memory, if none are provided. A `Route` resource fragment should be provided or use the `expose` flag if you want to expose your application to the outside world.

For kubernetes based deployments, a `Service` and `Deployment` will be created by default, if none are provided. The `Service` is of a `LoadBalancer` type, so no `Ingress` is needed to expose the application.

### Resource Fragments

OpenShift resource fragments are user provided YAML files which describe and enhance your deployed resources. They are enriched with metadata, labels and more by nodeshift.
Expand Down Expand Up @@ -150,6 +152,18 @@ nodeshift.deploy().then((response) => {
````
_please note: Currently, once a route, service, deployment config, build config, and imagestream config are created, those are re-used. The only thing that changes from deployment to deployment is the source code. For application resources, you can update them by undeploying and then deploying again. BuildConfigs and Imagestreams can be re-created using the --build.recreate flag_

#### Using with Kubernetes

Nodeshift can deploy Node.js applications to a Kubernetes Cluster using the `--kube` flag. At the moment, there is only support for [minikube](https://minikube.sigs.k8s.io/docs/start/).

Nodeshift expects that your code has a Dockerfile in its root directory. Then deploying to Minikube is as easy as running:

`npx nodeshift --kube`

This connect to Minikubes docker server, create a new container and then deploy and expose that container with a `Deployment` and `Service`



## Advanced Options

While nodeshift is very opinionated about deployment parameters, both the CLI and the API accept options that allow you to customize nodeshift's behavior.
Expand Down Expand Up @@ -219,6 +233,9 @@ Flag to deploy the application using a Deployment instead of a DeploymentConfig.
EXPERIMENTAL. Flag to deploy an application as a Knative Serving Service. Defaults to false
Since this feature is experimental, it is subject to change without a Major version release until it is fully stable.
#### kube
Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported.
#### help
Shows the below help
Expand All @@ -234,6 +251,9 @@ Shows the below help
Options:
--version Show version number [boolean]
--projectLocation change the default location of the project [string]
--kube Flag to deploy an application to a vanilla kubernetes
cluster. At the moment only Minikube is supported.
[boolean]
--configLocation change the default location of the config [string]
--imageTag The tag of the docker image to use for the deployed
application. [string] [default: "latest"]
Expand Down
11 changes: 10 additions & 1 deletion bin/cli.js
Expand Up @@ -18,13 +18,15 @@

'use strict';

const nodeshiftConfig = require('../lib/nodeshift-config');
const nodeshiftConfig = require('../lib/config/nodeshift-config');
const resourceGoal = require('../lib/goals/resource');
const buildGoal = require('../lib/goals/build');
const applyResources = require('../lib/goals/apply-resources');
const undeployGoal = require('../lib/goals/undeploy');
const namespace = require('../lib/namespace');

const kubeUrl = require('../lib/kube-url');

/**
This module is where everything is orchestrated. Both the command line process and the public API call this modules run function
Expand All @@ -49,6 +51,10 @@ module.exports = async function run (options) {
case 'apply-resource':
response.resources = await resourceGoal(config);
response.appliedResources = await applyResources(config, response.resources);

if (options.kube) {
kubeUrl(config, response.appliedResources);
}
break;
case 'undeploy':
if (config.namespace && config.namespace.remove) {
Expand All @@ -65,6 +71,9 @@ module.exports = async function run (options) {
response.build = await buildGoal(config);
response.resources = await resourceGoal(config);
response.appliedResources = await applyResources(config, response.resources);
if (options.kube) {
kubeUrl(config, response.appliedResources);
}
break;
default:
throw new TypeError(`Unexpected command: ${options.cmd}`);
Expand Down
6 changes: 6 additions & 0 deletions bin/nodeshift
Expand Up @@ -42,6 +42,10 @@ yargs
describe: 'change the default location of the project',
type: 'string'
})
.option('kube', {
describe: 'Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported.',
type: 'boolean'
})
.option('configLocation', {
describe: 'change the default location of the config',
type: 'string'
Expand Down Expand Up @@ -186,6 +190,8 @@ function createOptions (argv) {

options.knative = argv.knative === true || argv.knative === 'true';

options.kube = argv.kube === true || argv.kube === 'true';

options.projectLocation = argv.projectLocation;
options.dockerImage = argv.dockerImage;
options.imageTag = argv.imageTag;
Expand Down
5 changes: 5 additions & 0 deletions index.js
Expand Up @@ -33,6 +33,7 @@ const cli = require('./bin/cli');
@param {array} [options.definedProperties] - Array of objects with the format { key: value }. Used for template substitution
@param {boolean} [options.useDeployment] - Flag to deploy the application using a Deployment instead of a DeploymentConfig. Defaults to false
@param {boolean} [options.knative] - EXPERIMENTAL. flag to deploy an application as a Knative Serving Service. Defaults to false
@param {boolean} [options.kube] - Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported. Defaults to false
@returns {Promise<object>} - Returns a JSON Object
*/
function deploy (options = {}) {
Expand Down Expand Up @@ -61,6 +62,7 @@ function deploy (options = {}) {
@param {array} [options.definedProperties] - Array of objects with the format { key: value }. Used for template substitution
@param {boolean} [options.useDeployment] - Flag to deploy the application using a Deployment instead of a DeploymentConfig. Defaults to false
@param {boolean} [options.knative] - EXPERIMENTAL. flag to deploy an application as a Knative Serving Service. Defaults to false
@param {boolean} [options.kube] - Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported. Defaults to false
@returns {Promise<object>} - Returns a JSON Object
*/
function resource (options = {}) {
Expand Down Expand Up @@ -92,6 +94,7 @@ function resource (options = {}) {
@param {array} [options.definedProperties] - Array of objects with the format { key: value }. Used for template substitution
@param {boolean} [options.useDeployment] - Flag to deploy the application using a Deployment instead of a DeploymentConfig. Defaults to false
@param {boolean} [options.knative] - EXPERIMENTAL. flag to deploy an application as a Knative Serving Service. Defaults to false
@param {boolean} [options.kube] - Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported. Defaults to false
@returns {Promise<object>} - Returns a JSON Object
*/
function applyResource (options = {}) {
Expand Down Expand Up @@ -123,6 +126,7 @@ function applyResource (options = {}) {
@param {array} [options.definedProperties] - Array of objects with the format { key: value }. Used for template substitution
@param {boolean} [options.useDeployment] - Flag to deploy the application using a Deployment instead of a DeploymentConfig. Defaults to false
@param {boolean} [options.knative] - EXPERIMENTAL. flag to deploy an application as a Knative Serving Service. Defaults to false
@param {boolean} [options.kube] - Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported. Defaults to false
@returns {Promise<object>} - Returns a JSON Object
*/
function undeploy (options = {}) {
Expand All @@ -149,6 +153,7 @@ function undeploy (options = {}) {
@param {boolean} [options.build.forcePull] - flag to make your BuildConfig always pull a new image from dockerhub or not. Defaults to false
@param {Array} [options.build.env] - an array of objects to pass build config environment variables. [{name: NAME_PROP, value: VALUE}]
@param {array} [options.definedProperties] - Array of objects with the format { key: value }. Used for template substitution
@param {boolean} [options.kube] - Flag to deploy an application to a vanilla kubernetes cluster. At the moment only Minikube is supported. Defaults to false
@returns {Promise<object>} - Returns a JSON Object
*/
function build (options = {}) {
Expand Down
20 changes: 20 additions & 0 deletions lib/config/docker-config.js
@@ -0,0 +1,20 @@
'use strict';

const { readFileSync } = require('fs');
const Docker = require('dockerode');

function dockerClientSetup (options = {}, kubeEnvVars) {
const url = new URL(kubeEnvVars.DOCKER_HOST);

const docker = new Docker({
host: url.hostname,
port: url.port || 2375,
ca: readFileSync(`${kubeEnvVars.DOCKER_CERT_PATH}/ca.pem`),
cert: readFileSync(`${kubeEnvVars.DOCKER_CERT_PATH}/cert.pem`),
key: readFileSync(`${kubeEnvVars.DOCKER_CERT_PATH}/key.pem`)
});

return docker;
}

module.exports = dockerClientSetup;
38 changes: 38 additions & 0 deletions lib/config/kubernetes-config.js
@@ -0,0 +1,38 @@
'use strict';

// export DOCKER_TLS_VERIFY="1"
// export DOCKER_HOST="tcp://192.168.39.50:2376"
// export DOCKER_CERT_PATH="/home/lucasholmquist/.minikube/certs"
// export MINIKUBE_ACTIVE_DOCKERD="minikube"
const util = require('util');
const exec = util.promisify(require('child_process').exec);

async function getMinikubeEnvironmentVariables () {
// Make a call to minikube docker-env
try {
const { stdout } = await exec('minikube docker-env');
const envs = stdout
.split('\n') // Split on newline
.filter((v) => { // Then only return the lines that have the export
return v.includes('export');
})
.map((v) => { // Now make those lines into an Array of objects
const splitExport = v.split('export')[1].trim();
const splitObject = splitExport.split('=');
return { [splitObject[0]]: splitObject[1].replace(/"/g, '') };
})
.reduce((acc, x) => { // Flatten the array of objects into one object
return { ...acc, ...x };
}, {});

return envs;
} catch (err) {
return Promise.reject(new Error(err));
}
}

async function getKubernetesEnv () {
return getMinikubeEnvironmentVariables();
}

module.exports = getKubernetesEnv;
22 changes: 20 additions & 2 deletions lib/nodeshift-config.js → lib/config/nodeshift-config.js
Expand Up @@ -18,7 +18,9 @@

'use strict';

const logger = require('./common-log')();
const logger = require('../common-log')();
const kubernetesConfig = require('./kubernetes-config');
const dockerConfig = require('./docker-config');
const { OpenshiftClient: openshiftRestClient } = require('openshift-rest-client');
const fs = require('fs');
const { promisify } = require('util');
Expand Down Expand Up @@ -67,6 +69,21 @@ async function setup (options = {}) {
delete options.namespace;
}

let dockerClient;

if (options.kube) {
logger.info('Using the kubernetes flag.');

// Assume Default namespace for now
config.namespace.name = 'default';

// Assume minikube for now
// TODO(lholmquist): other kube flavors
const kubeEnvVars = await kubernetesConfig();
// Pass these kube envs to the docker client setup thingy
dockerClient = dockerConfig(options, kubeEnvVars);
}

logger.info(`using namespace ${config.namespace.name} at ${kubeConfig.getCurrentCluster().server}`);

if (!projectPackage.name.match(/^[a-z][0-9a-z-]+[0-9a-z]$/)) {
Expand Down Expand Up @@ -102,7 +119,8 @@ async function setup (options = {}) {
// Since we are only doing s2i builds(atm), append the s2i bit to the end
buildName: `${projectPackage.name}-s2i`, // TODO(lholmquist): this should probably change?
// Load an instance of the Openshift Rest Client, https://www.npmjs.com/package/openshift-rest-client
openshiftRestClient: restClient
openshiftRestClient: restClient,
dockerClient
}, options);

return result;
Expand Down
3 changes: 2 additions & 1 deletion lib/enrich-resources.js
Expand Up @@ -22,6 +22,7 @@ const loadEnrichers = require('./load-enrichers');
const defaultEnrichers = require('./resource-enrichers/default-enrichers.json');
const knativeEnrichers = require('./resource-enrichers/knative-enrichers.json');
const deploymentEnrichers = require('./resource-enrichers/deployment-enrichers.json');
const kubeEnrichers = require('./resource-enrichers/kubernetes-enrichers.json');

// TODO: Add Knative Serving Enrichers to run instead?
// Split the defaults to be "normal" and "knative"?
Expand All @@ -33,7 +34,7 @@ module.exports = async (config, resourceList) => {
// Loop through those and then enrich the items from the resourceList
let enrichedList = resourceList;
// the defaultEnrichers list will have the correct order
for (const enricher of (config.knative ? knativeEnrichers : config.useDeployment ? deploymentEnrichers : defaultEnrichers)) {
for (const enricher of (config.knative ? knativeEnrichers : config.useDeployment ? deploymentEnrichers : config.kube ? kubeEnrichers : defaultEnrichers)) {
const fn = loadedEnrichers[enricher];
if (typeof fn === 'function') {
enrichedList = await fn(config, enrichedList);
Expand Down
10 changes: 10 additions & 0 deletions lib/goals/build.js
Expand Up @@ -27,6 +27,16 @@ const imageStreamConfigurator = require('../image-stream');
const binaryBuild = require('../binary-build');

module.exports = async function build (config) {
if (config.kube) {
// The build command for just kubernetes will create a container image using the source code
// Not sure if we should be creating the image as latest, or some other version?
// This Docker image will then be tagged with the hash from the ImageID, or Do we need to do that?

// The return value of this goal should be the hash id of the image
const imageId = await projectArchiver.createContainer(config);

return imageId;
}
// archive the application source
await projectArchiver.archiveAndTar(config);

Expand Down
18 changes: 16 additions & 2 deletions lib/goals/undeploy.js
Expand Up @@ -89,8 +89,22 @@ module.exports = async function (config) {

// Check for a flag for deleting the builds/BuildConfig and Imagestream
if (config.removeAll) {
await removeBuildsAndBuildConfig(config);
await removeImageStream(config);
if (config.kube) {
// Delete the docker image that we created
// image should be the {projectname}:latest
const image = config.dockerClient.getImage(config.projectName);
try {
await image.remove({ name: config.projectName });
logger.trace(`${config.projectName} image has been removed`);
} catch (err) {
if (err.statusCode !== 404) {
logger.error(err.json.message);
}
}
} else {
await removeBuildsAndBuildConfig(config);
await removeImageStream(config);
}
}

return response;
Expand Down
19 changes: 19 additions & 0 deletions lib/kube-url.js
@@ -0,0 +1,19 @@
'use strict';

const logger = require('./common-log')();

function outputKubeUrl (config, appliedResources) {
const deployedService = appliedResources.find((v) => { return v.body.kind === 'Service'; });

if (deployedService) {
const kubeUrl = new URL(config.openshiftRestClient.backend.requestOptions.baseUrl);

// Output the host:port where the running application is?
const parsedURL = `http://${kubeUrl.hostname}:${deployedService.body.spec.ports[0].nodePort}`;
logger.info(`Application running at: ${parsedURL}`);
return parsedURL;
}
logger.warn('No Deployed Service Found');
}

module.exports = outputKubeUrl;
4 changes: 4 additions & 0 deletions lib/namespace.js
Expand Up @@ -22,6 +22,10 @@ const logger = require('./common-log')();
const { awaitRequest } = require('./helpers');

async function create (config) {
if (config.kube) {
logger.warn('This feature is not available using the --kube flag');
return;
}
// Check that the namespace exists first
// There is actually a permission error trying to find just 1 project/namespace, so we need to get all and filter
// If you were an admin user, this might not be a probably, but....
Expand Down

0 comments on commit 3e03c00

Please sign in to comment.