diff --git a/README.md b/README.md index 3a36e2f9..99cb1339 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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. @@ -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 @@ -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"] diff --git a/bin/cli.js b/bin/cli.js index ad37fa8c..48291311 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -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 @@ -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) { @@ -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}`); diff --git a/bin/nodeshift b/bin/nodeshift index 99eb079b..582c8517 100755 --- a/bin/nodeshift +++ b/bin/nodeshift @@ -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' @@ -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; diff --git a/index.js b/index.js index 1895b85b..a318ed7e 100644 --- a/index.js +++ b/index.js @@ -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} - Returns a JSON Object */ function deploy (options = {}) { @@ -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} - Returns a JSON Object */ function resource (options = {}) { @@ -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} - Returns a JSON Object */ function applyResource (options = {}) { @@ -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} - Returns a JSON Object */ function undeploy (options = {}) { @@ -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} - Returns a JSON Object */ function build (options = {}) { diff --git a/lib/config/docker-config.js b/lib/config/docker-config.js new file mode 100644 index 00000000..1d91d96e --- /dev/null +++ b/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; diff --git a/lib/config/kubernetes-config.js b/lib/config/kubernetes-config.js new file mode 100644 index 00000000..740a2f70 --- /dev/null +++ b/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; diff --git a/lib/nodeshift-config.js b/lib/config/nodeshift-config.js similarity index 87% rename from lib/nodeshift-config.js rename to lib/config/nodeshift-config.js index 553bac64..b2461c71 100644 --- a/lib/nodeshift-config.js +++ b/lib/config/nodeshift-config.js @@ -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'); @@ -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]$/)) { @@ -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; diff --git a/lib/enrich-resources.js b/lib/enrich-resources.js index 3b5eaf2a..fe344d1e 100644 --- a/lib/enrich-resources.js +++ b/lib/enrich-resources.js @@ -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"? @@ -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); diff --git a/lib/goals/build.js b/lib/goals/build.js index 03abffa7..0e5c96a1 100644 --- a/lib/goals/build.js +++ b/lib/goals/build.js @@ -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); diff --git a/lib/goals/undeploy.js b/lib/goals/undeploy.js index ac0d608a..bee81168 100644 --- a/lib/goals/undeploy.js +++ b/lib/goals/undeploy.js @@ -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; diff --git a/lib/kube-url.js b/lib/kube-url.js new file mode 100644 index 00000000..8dfdfa5b --- /dev/null +++ b/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; diff --git a/lib/namespace.js b/lib/namespace.js index a5a66691..5e5fb560 100644 --- a/lib/namespace.js +++ b/lib/namespace.js @@ -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.... diff --git a/lib/project-archiver.js b/lib/project-archiver.js index ff6036ad..71cb95a6 100644 --- a/lib/project-archiver.js +++ b/lib/project-archiver.js @@ -26,8 +26,7 @@ const helpers = require('./helpers'); const DEFAULT_NODESHIFT_DIR = 'tmp/nodeshift'; const DEFAULT_BUILD_LOCATION = `${DEFAULT_NODESHIFT_DIR}/build`; -async function createArchive (config) { - // tar the working directories code. +async function loadFiles (config) { const includedFiles = []; // Look to see if any files are specified in the package.json if (config.projectPackage.files && config.projectPackage.files.length > 0) { @@ -81,7 +80,15 @@ async function createArchive (config) { }); includedFiles.push.apply(includedFiles, filteredOut); } + logger.info(`creating archive of ${includedFiles.join(', ')}`); + + return includedFiles; +} + +async function createArchive (config) { + // tar the working directories code. + const includedFiles = await loadFiles(config); await helpers.createDir(`${config.projectLocation}/${DEFAULT_BUILD_LOCATION}`); return tar.create({ cwd: config.projectLocation, // Don't forget to be in the projectLocation @@ -94,7 +101,58 @@ async function archiveAndTar (config) { return createArchive(config); } +async function createContainer (config) { + // Create a container image with the docker client + const includedFiles = await loadFiles(config); + + return new Promise((resolve, reject) => { + let imageId; + + logger.info('Building Docker Image'); + config.dockerClient.buildImage({ + context: process.cwd(), + src: includedFiles + }, { + t: `${config.projectName}`, // probably make this the package name and something for the version other than latest? + q: false // true supresses verbose output + }, function (error, stream) { + if (error) return reject(error); + + stream.on('data', function (chunk) { + const chunkAsString = chunk.toString('utf8').trim(); + logger.trace(chunkAsString); + // Find the output that has the image id sha + if (chunkAsString.includes('sha')) { + // Parse that into JSON + const parsedChunk = JSON.parse(chunkAsString); + // Depending on if we have verbose output on or off, the location of the sha is differnt + // traverse the entries to find where the sha is, then split on : and take the last value + + imageId = findShaID(parsedChunk); + } + }); + + stream.on('end', function () { + return resolve(imageId); + }); + }); + }); +} + +function findShaID (value) { + for (const [, entry] of Object.entries(value)) { + if (entry.includes && entry.includes('sha')) { + // split on : + const splity = entry.split(':'); + return splity[splity.length - 1].trim(); + } else { + return findShaID(entry); + } + } +} + module.exports = exports = { archiveAndTar: archiveAndTar, + createContainer: createContainer, DEFAULT_BUILD_LOCATION: DEFAULT_BUILD_LOCATION }; diff --git a/lib/resource-enrichers/deployment-enricher.js b/lib/resource-enrichers/deployment-enricher.js index f975cc5b..57695663 100644 --- a/lib/resource-enrichers/deployment-enricher.js +++ b/lib/resource-enrichers/deployment-enricher.js @@ -1,6 +1,7 @@ 'use strict'; const _ = require('lodash'); +const { generateUUID } = require('../helpers'); const baseDeployment = { apiVersion: 'apps/v1', @@ -10,12 +11,26 @@ const baseDeployment = { }; function defaultDeployment (config) { - /* eslint no-useless-escape: "off" */ - const trigger = [{ from: { kind: 'ImageStreamTag', name: `${config.outputImageStreamName}:latest`, namespace: config.namespace.name }, fieldPath: `spec.template.spec.containers[?(@.name==\"${config.projectName}\")].image` }]; - - const metaAnnotations = { - 'image.openshift.io/triggers': JSON.stringify(trigger) + let metaAnnotations; + let containerImage; + const labels = { + app: config.projectName }; + + if (!config.kube) { + /* eslint no-useless-escape: "off" */ + const trigger = [{ from: { kind: 'ImageStreamTag', name: `${config.outputImageStreamName}:latest`, namespace: config.namespace.name }, fieldPath: `spec.template.spec.containers[?(@.name==\"${config.projectName}\")].image` }]; + + metaAnnotations = { + 'image.openshift.io/triggers': JSON.stringify(trigger) + }; + labels.deploymentconfig = config.projectName; + containerImage = `image-registry.openshift-image-registry.svc:5000/${config.namespace.name}/${config.outputImageStreamName}`; + } else { + labels.appId = generateUUID(); + containerImage = `${config.projectName}:latest`; // TODO(lholmquist): Do we really need to use a tag other than latest? + } + const spec = { selector: { matchLabels: { @@ -24,16 +39,13 @@ function defaultDeployment (config) { }, template: { metadata: { - labels: { - app: config.projectName, - deploymentconfig: config.projectName - } + labels: labels }, spec: { containers: [ { name: config.projectName, - image: `image-registry.openshift-image-registry.svc:5000/${config.namespace.name}/${config.outputImageStreamName}`, + image: containerImage, imagePullPolicy: 'IfNotPresent', ports: [ { @@ -49,7 +61,7 @@ function defaultDeployment (config) { return { ...baseDeployment, spec: spec, metadata: { name: config.projectName, annotations: metaAnnotations } }; } -function createDeploymentResource (config, resourceList) { +async function createDeploymentResource (config, resourceList) { // First check to see if we have a Deployment if (_.filter(resourceList, { kind: 'Deployment' }).length < 1) { // create the default deployment config and add in to the resource list diff --git a/lib/resource-enrichers/kubernetes-enrichers.json b/lib/resource-enrichers/kubernetes-enrichers.json new file mode 100644 index 00000000..3921355f --- /dev/null +++ b/lib/resource-enrichers/kubernetes-enrichers.json @@ -0,0 +1,3 @@ +[ + "deployment", "service" +] diff --git a/lib/resource-enrichers/service-enricher.js b/lib/resource-enrichers/service-enricher.js index 1730264d..c53eb6ec 100644 --- a/lib/resource-enrichers/service-enricher.js +++ b/lib/resource-enrichers/service-enricher.js @@ -33,16 +33,23 @@ const baseServiceConfig = { function defaultService (config) { const serviceConfig = _.merge({}, baseServiceConfig); + const specSelector = {}; + if (config.kube) { + specSelector.app = config.projectName; + serviceConfig.spec.type = 'LoadBalancer'; + } else { + specSelector.project = config.projectName; + specSelector.provider = 'nodeshift'; + serviceConfig.spec.type = 'ClusterIP'; + } + // Apply MetaData serviceConfig.metadata = objectMetadata({ name: config.projectName, namespace: config.namespace.name }); - serviceConfig.spec.selector = { - project: config.projectName, - provider: 'nodeshift' - }; + serviceConfig.spec.selector = specSelector; serviceConfig.spec.ports = [ { @@ -53,8 +60,6 @@ function defaultService (config) { } ]; - serviceConfig.spec.type = 'ClusterIP'; - return serviceConfig; } diff --git a/package-lock.json b/package-lock.json index 44a4f30d..0eaae978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4035,6 +4035,28 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "body": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", @@ -4938,7 +4960,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -5132,6 +5153,38 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "docker-modem": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", + "integrity": "sha512-vDTzZjjO1sXMY7m0xKjGdFMMZL7vIUerkC3G4l6rnrpOET2M6AOufM8ajmQoOB+6RfSn6I/dlikCUq/Y91Q1sQ==", + "requires": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "dockerode": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz", + "integrity": "sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw==", + "requires": { + "docker-modem": "^2.1.0", + "tar-fs": "~2.0.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6764,6 +6817,11 @@ "null-check": "^1.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -8957,6 +9015,11 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -9042,8 +9105,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stream": { "version": "0.0.8", @@ -11092,6 +11154,11 @@ "through": "2" } }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -11121,6 +11188,24 @@ "integrity": "sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ==", "dev": true }, + "ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "requires": { + "ssh2-streams": "~0.4.10" + } + }, + "ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -11372,6 +11457,11 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -11756,6 +11846,48 @@ } } }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + } + } + }, + "tar-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index 65cb686b..ec37f2f5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "homepage": "https://github.com/nodeshift/nodeshift#readme", "dependencies": { "chalk": "^3.0.0", + "dockerode": "~3.2.1", "git-repo-info": "^2.0.0", "js-yaml": "~3.14.1", "lodash": "^4.17.20", diff --git a/test/cli-test.js b/test/cli-test.js index f7078f4a..dc551762 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -12,7 +12,7 @@ test('export test', (t) => { test('default goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -34,9 +34,36 @@ test('default goal', (t) => { }); }); +test('default goal - using kube flag', (t) => { + const cli = proxyquire('../bin/cli', { + '../lib/config/nodeshift-config': () => { + return Promise.resolve({}); + }, + '../lib/goals/resource': (config) => { + t.pass('should be here for the default goal'); + return Promise.resolve(); + }, + '../lib/goals/build': (config) => { + t.pass('should be here for the default goal'); + return Promise.resolve(); + }, + '../lib/goals/apply-resources': (config) => { + t.pass('should be here for the default goal'); + return Promise.resolve(); + }, + '../lib/kube-url': (config) => { + t.pass('should be here for the apply-resource goal'); + } + }); + + cli({ cmd: 'deploy', kube: true }).then(() => { + t.end(); + }); +}); + test('default goal - with namespace', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({ namespace: { create: true } }); }, '../lib/goals/resource': (config) => { @@ -66,7 +93,7 @@ test('default goal - with namespace', (t) => { test('resource goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -90,7 +117,7 @@ test('resource goal', (t) => { test('apply-resource goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -112,9 +139,36 @@ test('apply-resource goal', (t) => { }); }); +test('apply-resource goal - kube flag', (t) => { + const cli = proxyquire('../bin/cli', { + '../lib/config/nodeshift-config': () => { + return Promise.resolve({}); + }, + '../lib/goals/resource': (config) => { + t.pass('should be here for the apply-resource goal'); + return Promise.resolve(); + }, + '../lib/goals/build': (config) => { + t.fail('should not be here for the apply-resource goal'); + return Promise.resolve(); + }, + '../lib/goals/apply-resources': (config) => { + t.pass('should be here for the apply-resource goal'); + return Promise.resolve(); + }, + '../lib/kube-url': (config) => { + t.pass('should be here for the apply-resource goal'); + } + }); + + cli({ cmd: 'apply-resource', kube: true }).then(() => { + t.end(); + }); +}); + test('no goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -139,7 +193,7 @@ test('no goal', (t) => { test('undeploy goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -167,7 +221,7 @@ test('undeploy goal', (t) => { test('undeploy goal - with namespace', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({ namespace: { remove: true } }); }, '../lib/goals/resource': (config) => { @@ -201,7 +255,7 @@ test('undeploy goal - with namespace', (t) => { test('build goal', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({}); }, '../lib/goals/resource': (config) => { @@ -229,7 +283,7 @@ test('build goal', (t) => { test('build goal - with namespace', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.resolve({ namespace: { create: true } }); }, '../lib/goals/resource': (config) => { @@ -263,7 +317,7 @@ test('build goal - with namespace', (t) => { test('error', (t) => { const cli = proxyquire('../bin/cli', { - '../lib/nodeshift-config': () => { + '../lib/config/nodeshift-config': () => { return Promise.reject(new Error('error')); } }); diff --git a/test/config-tests/docker-config-test.js b/test/config-tests/docker-config-test.js new file mode 100644 index 00000000..77ef28f6 --- /dev/null +++ b/test/config-tests/docker-config-test.js @@ -0,0 +1,42 @@ +'use strict'; + +const test = require('tape'); +const proxyquire = require('proxyquire'); + +test('docker-config', (t) => { + const dockerClientSetup = proxyquire('../../lib/config/docker-config', { + fs: { + readFileSync: (path) => { + t.pass(); + } + } + }); + + const kubeEnvVars = { + DOCKER_HOST: 'tcp://192.168.39.50:2376', + DOCKER_CERT_PATH: '/home/lucasholmquist/.minikube/certs' + }; + dockerClientSetup({}, kubeEnvVars); + + t.pass(); + t.end(); +}); + +test('docker-config - no port', (t) => { + const dockerClientSetup = proxyquire('../../lib/config/docker-config', { + fs: { + readFileSync: (path) => { + t.pass(); + } + } + }); + + const kubeEnvVars = { + DOCKER_HOST: 'tcp://192.168.39.50', + DOCKER_CERT_PATH: '/home/lucasholmquist/.minikube/certs' + }; + dockerClientSetup({}, kubeEnvVars); + + t.pass(); + t.end(); +}); diff --git a/test/config-tests/kubernetes-config-test.js b/test/config-tests/kubernetes-config-test.js new file mode 100644 index 00000000..131db82a --- /dev/null +++ b/test/config-tests/kubernetes-config-test.js @@ -0,0 +1,53 @@ +'use strict'; + +const test = require('tape'); +const proxyquire = require('proxyquire'); + +test('kubernetes-config - minikube', (t) => { + const getKubernetesEnv = proxyquire('../../lib/config/kubernetes-config', { + child_process: { + exec: function (cmd, cb) { + t.equal(cmd, 'minikube docker-env'); + const envs = 'export DOCKER_TLS_VERIFY="1" \n' + + 'export DOCKER_HOST="tcp://192.168.39.50:2376 \n' + + 'export DOCKER_CERT_PATH="/home/lucasholmquist/.minikube/certs \n' + + 'export MINIKUBE_ACTIVE_DOCKERD="minikube"'; + return cb(null, { stdout: envs }); + } + } + }); + + const p = getKubernetesEnv().then((env) => { + t.pass(); + t.equal(env.DOCKER_HOST, 'tcp://192.168.39.50:2376'); + t.equal(env.DOCKER_CERT_PATH, '/home/lucasholmquist/.minikube/certs'); + t.end(); + }).catch(t.fail); + + t.equal(p instanceof Promise, true, 'should return a Promise'); +}); + +test('kubernetes-config - minikube - error', (t) => { + const getKubernetesEnv = proxyquire('../../lib/config/kubernetes-config', { + child_process: { + exec: function (cmd, cb) { + t.equal(cmd, 'minikube docker-env'); + const envs = 'export DOCKER_TLS_VERIFY="1" \n' + + 'export DOCKER_HOST="tcp://192.168.39.50:2376 \n' + + 'export DOCKER_CERT_PATH="/home/lucasholmquist/.minikube/certs \n' + + 'export MINIKUBE_ACTIVE_DOCKERD="minikube"'; + return cb(new Error('error'), { stdout: envs }); + } + } + }); + + const p = getKubernetesEnv().then((env) => { + t.fail(); + t.end(); + }).catch(() => { + t.pass(); + t.end(); + }); + + t.equal(p instanceof Promise, true, 'should return a Promise'); +}); diff --git a/test/nodeshift-config-test.js b/test/config-tests/nodeshift-config-test.js similarity index 85% rename from test/nodeshift-config-test.js rename to test/config-tests/nodeshift-config-test.js index cf7c9174..29989ba1 100644 --- a/test/nodeshift-config-test.js +++ b/test/config-tests/nodeshift-config-test.js @@ -4,7 +4,7 @@ const test = require('tape'); const proxyquire = require('proxyquire'); test('nodeshift-config basic setup', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -40,7 +40,7 @@ test('nodeshift-config basic setup', (t) => { }); test('nodeshift-config basic setup with deploy option', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -76,7 +76,7 @@ test('nodeshift-config basic setup with deploy option', (t) => { }); test('nodeshift-config other project location and nodeshiftDir', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -107,7 +107,7 @@ test('nodeshift-config other project location and nodeshiftDir', (t) => { }); test('nodeshift-config no project Version', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -138,7 +138,7 @@ test('nodeshift-config no project Version', (t) => { }); test('nodeshift-config no package.json', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -169,7 +169,7 @@ test('nodeshift-config no package.json', (t) => { }); test('nodeshift-config invalid "name" in package.json', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -207,7 +207,7 @@ test('nodeshift-config invalid "name" in package.json', (t) => { JSON.stringify( Object.assign( {}, - require('../examples/sample-project/package.json'), + require('../../examples/sample-project/package.json'), { name: '@invalid-package-name' } @@ -226,7 +226,7 @@ test('nodeshift-config options configLocation', (t) => { configLocation: '../examples/sample-project' }; - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: (settings) => { t.equal(settings.config, options.configLocation, 'should be passed in'); @@ -254,7 +254,7 @@ test('nodeshift-config options configLocation', (t) => { }); test('nodeshift-config options for the config loader - change the namespace', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -288,7 +288,7 @@ test('nodeshift-config options for the config loader - change the namespace', (t }); test('nodeshift-config options for the config loader - change the namespace, format correctly', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -321,7 +321,7 @@ test('nodeshift-config options for the config loader - change the namespace, for }); test('nodeshift-config options for the config loader - use namspace object format, no name', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -354,7 +354,7 @@ test('nodeshift-config options for the config loader - use namspace object forma }); test('nodeshift-config options for the config loader - using namespace object format', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -390,7 +390,7 @@ test('nodeshift-config options for the config loader - using namespace object fo }); test('nodeshift-config options - change outputImageStreamTag and outputImageStreamName', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -423,7 +423,7 @@ test('nodeshift-config options - change outputImageStreamTag and outputImageStre }); test('nodeshift-config options - not recognized build strategy', (t) => { - const nodeshiftConfig = proxyquire('../lib/nodeshift-config', { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { 'openshift-rest-client': { OpenshiftClient: () => { return Promise.resolve({ @@ -454,3 +454,44 @@ test('nodeshift-config options - not recognized build strategy', (t) => { t.end(); }); }); + +test('nodeshift-config basic setup kube option', (t) => { + const nodeshiftConfig = proxyquire('../../lib/config/nodeshift-config', { + './kubernetes-config': () => { + t.pass(); + return Promise.resolve(); + }, + './docker-config': () => { + t.pass(); + return Promise.resolve(); + }, + 'openshift-rest-client': { + OpenshiftClient: () => { + return Promise.resolve({ + kubeconfig: { + getCurrentContext: () => { + return 'nodey/ip/other'; + }, + getCurrentCluster: () => { + return { server: 'http://mock-cluster' }; + }, + getContexts: () => { + return [{ name: 'nodey/ip/other', namespace: 'test-namespace' }]; + } + } + }); + } + } + }); + + const options = { + kube: true + }; + + const p = nodeshiftConfig(options).then((config) => { + t.equal(config.namespace.name, 'default', 'Kube flag uses default by default'); + t.end(); + }).catch(t.fail); + + t.equal(p instanceof Promise, true, 'should return a Promise'); +}); diff --git a/test/enricher-tests/deployment-enricher-test.js b/test/enricher-tests/deployment-enricher-test.js new file mode 100644 index 00000000..9fd1db6e --- /dev/null +++ b/test/enricher-tests/deployment-enricher-test.js @@ -0,0 +1,100 @@ +'use strict'; + +const test = require('tape'); + +const deploymentEnricher = require('../../lib/resource-enrichers/deployment-enricher'); +const config = { + projectName: 'Project Name', + version: '1.0.0', + namespace: { + name: 'namespace' + } +}; + +test('deployment enricher - no deployment', (t) => { + const resourceList = [ + { + kind: 'Service', + metadata: { + name: 'service meta' + } + } + ]; + + t.ok(deploymentEnricher.enrich, 'has an enrich property'); + t.equal(typeof deploymentEnricher.enrich, 'function', 'is a function'); + + t.ok(deploymentEnricher.name, 'has an name property'); + t.equal(deploymentEnricher.name, 'deployment'); + + const p = deploymentEnricher.enrich(config, resourceList); + t.ok(p instanceof Promise, 'enricher should return a promise'); + + p.then((dce) => { + t.ok(dce[1].metadata.annotations, 'has annotations'); + t.ok(dce[1].spec.template.metadata.labels.deploymentconfig); + t.equal(Array.isArray(dce), true, 'should return an array'); + t.equal(dce.length, 2, 'array should have 2 things'); + t.equal(dce[1].kind, 'Deployment', 'should have the deployment type'); + t.end(); + }); +}); + +test('deployment enricher - deployment', async (t) => { + const resourceList = [ + { + kind: 'Service', + metadata: { + name: 'service meta' + } + }, + { + kind: 'Deployment', + metadata: { + name: 'deploymentName' + } + } + ]; + + const dce = await deploymentEnricher.enrich(config, resourceList); + + t.equal(Array.isArray(dce), true, 'should return an array'); + t.notEqual(dce, resourceList, 'should not be equal'); + t.equal(dce[1].kind, 'Deployment', 'should have the deployment type'); + t.end(); +}); + +test('deployment enricher - no deployment - kube flag', (t) => { + const resourceList = [ + { + kind: 'Service', + metadata: { + name: 'service meta' + } + } + ]; + + t.ok(deploymentEnricher.enrich, 'has an enrich property'); + t.equal(typeof deploymentEnricher.enrich, 'function', 'is a function'); + + t.ok(deploymentEnricher.name, 'has an name property'); + t.equal(deploymentEnricher.name, 'deployment'); + + const config = { + kube: true, + projectName: 'Project Name', + version: '1.0.0', + namespace: { + name: 'namespace' + } + }; + const p = deploymentEnricher.enrich(config, resourceList); + t.ok(p instanceof Promise, 'enricher should return a promise'); + + p.then((dce) => { + t.equal(Array.isArray(dce), true, 'should return an array'); + t.equal(dce.length, 2, 'array should have 2 things'); + t.equal(dce[1].kind, 'Deployment', 'should have the deployment type'); + t.end(); + }); +}); diff --git a/test/enricher-tests/service-enricher-test.js b/test/enricher-tests/service-enricher-test.js index 0f9fd12f..30c8b9b9 100644 --- a/test/enricher-tests/service-enricher-test.js +++ b/test/enricher-tests/service-enricher-test.js @@ -73,3 +73,39 @@ test('service enricher test - service', async (t) => { t.equal(se[0].spec.ports[0].name, 'http'); t.end(); }); + +test('service enricher test - no service - kube flag', (t) => { + const resourceList = []; + + t.ok(serviceEnricher.enrich, 'has an enrich property'); + t.equal(typeof serviceEnricher.enrich, 'function', 'is a function'); + t.ok(serviceEnricher.name, 'has an name property'); + t.equal(serviceEnricher.name, 'service', 'name property is service'); + + const config = { + kube: true, + projectName: 'Project Name', + version: '1.0.0', + namespace: { + name: 'the namespace' + }, + port: 8080 + }; + + const p = serviceEnricher.enrich(config, resourceList); + t.ok(p instanceof Promise, 'enricher should return a Promise'); + + p.then((se) => { + t.equal(Array.isArray(se), true, 'should return an array'); + t.equal(resourceList.length, 1, 'resourceList size increases by 1'); + t.ok(se[0].spec.selector, 'selector prop should be here'); + t.equal(se[0].spec.selector.app, config.projectName, `spec.selector.app should be ${config.projectName}`); + t.ok(se[0].spec.ports, 'ports prop should be here'); + t.equal(se[0].spec.ports[0].port, 8080, 'port should be 8080'); + t.equal(se[0].spec.ports[0].targetPort, 8080, 'targetPort should be 8080'); + t.ok(Array.isArray(se[0].spec.ports), 'ports prop should be here'); + t.ok(se[0].spec.type, 'type prop should be here'); + t.equal(se[0].spec.type, 'LoadBalancer', 'spec.type should be LoadBalancer'); + t.end(); + }); +}); diff --git a/test/goals/build-test.js b/test/goals/build-test.js index a67751ec..df882fa4 100644 --- a/test/goals/build-test.js +++ b/test/goals/build-test.js @@ -35,3 +35,27 @@ test('build goal function', (t) => { t.equal(b instanceof Promise, true, 'returns a promise'); }); + +test('build goal function - kube flag', (t) => { + const build = proxyquire('../../lib/goals/build', { + '../project-archiver': { + createContainer: (config) => { + t.pass(); + return '12345'; + } + } + }); + + const config = { + projectLocation: 'location', + kube: true + }; + + const b = build(config).then((imageId) => { + t.pass(); + t.equal(imageId, '12345', 'should have the 12345 image ID'); + t.end(); + }); + + t.equal(b instanceof Promise, true, 'returns a promise'); +}); diff --git a/test/goals/undeploy-test.js b/test/goals/undeploy-test.js index 2b1e8d83..1e5c423b 100644 --- a/test/goals/undeploy-test.js +++ b/test/goals/undeploy-test.js @@ -277,3 +277,170 @@ test('remove build and image stream', (t) => { t.end(); }); }); + +test('removeAll with Kube flag', (t) => { + t.plan(4); + const config = { + removeAll: true, + projectName: 'projectName', + kube: true, + dockerClient: { + getImage: (projectName) => { + return { + remove: (options) => { + t.pass('should be here'); + t.equal(options.name, projectName, 'name passed in and project name should be equal'); + return Promise.resolve(); + } + }; + } + } + }; + + const resourceList = { + kind: 'List', + items: [] + }; + + const undeploy = proxyquire('../../lib/goals/undeploy', { + fs: { + readFile: (location, cb) => { return cb(null, JSON.stringify(resourceList)); } + }, + '../common-log': () => ({ + info: (message) => {}, + trace: (message) => { + t.equal(message, `${config.projectName} image has been removed`); + } + }), + '../deployment-config': { + undeploy: () => { return Promise.resolve(); } + }, + '../build-config': { + removeBuildsAndBuildConfig: () => { + t.fail('should not land here'); + } + }, + '../image-stream': { + removeImageStream: () => { + t.fail('should not land here'); + } + } + }); + + undeploy(config).then(() => { + t.pass('this should pass'); + t.end(); + }); +}); + +test('removeAll with Kube flag - 404 on the image', (t) => { + /* eslint prefer-promise-reject-errors: "off" */ + t.plan(3); + const config = { + removeAll: true, + projectName: 'projectName', + kube: true, + dockerClient: { + getImage: (projectName) => { + return { + remove: (options) => { + t.pass('should be here'); + t.equal(options.name, projectName, 'name passed in and project name should be equal'); + return Promise.reject({ statusCode: 404 }); + } + }; + } + } + }; + + const resourceList = { + kind: 'List', + items: [] + }; + + const undeploy = proxyquire('../../lib/goals/undeploy', { + fs: { + readFile: (location, cb) => { return cb(null, JSON.stringify(resourceList)); } + }, + '../common-log': () => ({ + info: (message) => {}, + trace: (message) => { + t.equal(message, `${config.projectName} image has been removed`); + } + }), + '../deployment-config': { + undeploy: () => { return Promise.resolve(); } + }, + '../build-config': { + removeBuildsAndBuildConfig: () => { + t.fail('should not land here'); + } + }, + '../image-stream': { + removeImageStream: () => { + t.fail('should not land here'); + } + } + }); + + undeploy(config).then(() => { + t.pass('this should pass'); + t.end(); + }); +}); + +test('removeAll with Kube flag - some error on the image', (t) => { + /* eslint prefer-promise-reject-errors: "off" */ + t.plan(4); + const config = { + removeAll: true, + projectName: 'projectName', + kube: true, + dockerClient: { + getImage: (projectName) => { + return { + remove: (options) => { + t.pass('should be here'); + t.equal(options.name, projectName, 'name passed in and project name should be equal'); + return Promise.reject({ statusCode: 401, json: { message: 'This is an error' } }); + } + }; + } + } + }; + + const resourceList = { + kind: 'List', + items: [] + }; + + const undeploy = proxyquire('../../lib/goals/undeploy', { + fs: { + readFile: (location, cb) => { return cb(null, JSON.stringify(resourceList)); } + }, + '../common-log': () => ({ + info: (message) => {}, + error: (message) => { + t.equal(message, 'This is an error'); + } + }), + '../deployment-config': { + undeploy: () => { return Promise.resolve(); } + }, + '../build-config': { + removeBuildsAndBuildConfig: () => { + t.fail('should not land here'); + } + }, + '../image-stream': { + removeImageStream: () => { + t.fail('should not land here'); + } + } + }); + + undeploy(config).then(() => { + t.pass('this should pass'); + t.end(); + }); +}); diff --git a/test/kube-url-test.js b/test/kube-url-test.js new file mode 100644 index 00000000..e86c7312 --- /dev/null +++ b/test/kube-url-test.js @@ -0,0 +1,65 @@ +'use strict'; + +const test = require('tape'); +const proxyquire = require('proxyquire'); + +test('finds the kube url', (t) => { + const config = { + openshiftRestClient: { + backend: { + requestOptions: { + baseUrl: 'http://localhost' + } + } + } + }; + + const appliedResources = [{ + body: { + kind: 'Service', + spec: { + ports: [{ + nodePort: 3000 + }] + } + } + }]; + + const outputKubeUrl = proxyquire('../lib/kube-url', { + './common-log': () => ({ + info: (message) => { + t.equal(message, 'Application running at: http://localhost:3000'); + t.pass(); + } + }) + }); + + outputKubeUrl(config, appliedResources); + t.end(); +}); + +test('finds the kube url - No Services', (t) => { + const config = { + openshiftRestClient: { + backend: { + requestOptions: { + baseUrl: 'http://localhost' + } + } + } + }; + + const outputKubeUrl = proxyquire('../lib/kube-url', { + './common-log': () => ({ + info: (message) => { + t.fail('should not reach this'); + }, + warn: (message) => { + t.equal(message, 'No Deployed Service Found'); + } + }) + }); + + outputKubeUrl(config, []); + t.end(); +}); diff --git a/test/namespace-test.js b/test/namespace-test.js index ec053cec..48f77a5e 100644 --- a/test/namespace-test.js +++ b/test/namespace-test.js @@ -1,9 +1,11 @@ 'use strict'; const test = require('tape'); -const namespace = require('../lib/namespace'); +const proxyquire = require('proxyquire'); test('namespace', t => { + const namespace = require('../lib/namespace'); + t.ok(namespace.create, 'should have a create method'); t.ok(namespace.remove, 'should have a remove method'); t.equal(typeof namespace.create, 'function', 'should be a function'); @@ -13,6 +15,7 @@ test('namespace', t => { }); test('namespace - create the namespace', t => { + const namespace = require('../lib/namespace'); const config = { namespace: { name: 'projectname' @@ -48,6 +51,8 @@ test('namespace - create the namespace', t => { }); test('namespace - create the namespace, others exist', t => { + const namespace = require('../lib/namespace'); + const config = { namespace: { name: 'projectname' @@ -83,6 +88,8 @@ test('namespace - create the namespace, others exist', t => { }); test('namespace - namespace exists', t => { + const namespace = require('../lib/namespace'); + const config = { namespace: { name: 'projectname' @@ -119,6 +126,8 @@ test('namespace - namespace exists', t => { }); test('namespace - namespace exists but is Terminating', t => { + const namespace = require('../lib/namespace'); + const config = { namespace: { name: 'projectname' @@ -155,6 +164,8 @@ test('namespace - namespace exists but is Terminating', t => { }); test('namespace - remove the namespace', t => { + const namespace = require('../lib/namespace'); + const config = { namespace: { name: 'projectname' @@ -186,3 +197,28 @@ test('namespace - remove the namespace', t => { t.end(); }); }); + +test('namespace - create the namespace kube flag', t => { + const namespace = proxyquire('../lib/namespace', { + './common-log': () => ({ + warn: (message) => { + t.equal(message, 'This feature is not available using the --kube flag'); + } + }) + }); + const config = { + namespace: { + name: 'projectname' + }, + kube: true + }; + + const n = namespace.create(config); + + t.equal(n instanceof Promise, true, 'instanceof a Promise'); + + n.then(() => { + t.pass(); + t.end(); + }); +}); diff --git a/test/project-archiver-test.js b/test/project-archiver-test.js index 5adfd410..b48f0b4b 100644 --- a/test/project-archiver-test.js +++ b/test/project-archiver-test.js @@ -270,3 +270,96 @@ test('change in project location', (t) => { t.end(); }); }); + +test('test build with kube flag - Error building image', (t) => { + const projectArchiver = proxyquire('../lib/project-archiver', { + './helpers': { + createDir: () => { + t.fail(); + return Promise.resolve(); + }, + cleanUp: () => { + t.fail(); + return Promise.resolve(); + }, + listFiles: () => { + return Promise.resolve([]); + } + }, + tar: { + create: () => { + t.fail(); + return Promise.resolve(); + } + } + }); + + const config = { + projectPackage: {}, + kube: true, + dockerClient: { + buildImage: (arg1, arg2, cb) => { + return cb(new Error('error')); + } + } + }; + + projectArchiver.createContainer(config).then(() => { + t.fail('This should fail if here'); + t.end(); + }).catch(() => { + t.pass('Succesfully caught error'); + t.end(); + }); +}); + +test('test build with kube flag ', (t) => { + const projectArchiver = proxyquire('../lib/project-archiver', { + './helpers': { + createDir: () => { + t.fail(); + return Promise.resolve(); + }, + cleanUp: () => { + t.fail(); + return Promise.resolve(); + }, + listFiles: () => { + return Promise.resolve([]); + } + }, + tar: { + create: () => { + t.fail(); + return Promise.resolve(); + } + } + }); + const { PassThrough } = require('stream'); + const mockReadable = new PassThrough(); + + const config = { + projectPackage: {}, + projectName: 'project Name', + kube: true, + dockerClient: { + buildImage: (options, otherOpitons, cb) => { + t.equal(Array.isArray(options.src), true, 'src should be an array'); + t.equal(otherOpitons.t, config.projectName, 'should be equal'); + t.equal(otherOpitons.q, false, 'verbose output is not suppressed'); + return cb(null, mockReadable); + } + } + }; + + setTimeout(() => { + mockReadable.emit('data', 'beep'); + mockReadable.emit('data', '{"aux":{"ID":"sha256:85ba84706d6a68157db30baed19a376f9656aa52bd0209f788de0ccbb762e4ab"}}'); + mockReadable.emit('end'); + }, 100); + + projectArchiver.createContainer(config).then((imageId) => { + t.equal(imageId, '85ba84706d6a68157db30baed19a376f9656aa52bd0209f788de0ccbb762e4ab'); + t.end(); + }).catch(t.fail); +});