Skip to content

Commit

Permalink
add wsk api support
Browse files Browse the repository at this point in the history
  • Loading branch information
starpit committed May 24, 2018
1 parent 632aec4 commit 9dbe88f
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 8 deletions.
14 changes: 10 additions & 4 deletions app/content/js/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const formatOneListResult = options => (entity, idx, A) => {
prefix.appendChild(prettyType)*/

/** add a cell to the current row of the list view we] are generating. "entityName" is the current row */
const addCell = (className, value, innerClassName='', parent=entityName, onclick, watch, key) => {
const addCell = (className, value, innerClassName='', parent=entityName, onclick, watch, key, fontawesome) => {
const cell = document.createElement('span'),
inner = document.createElement('span')

Expand All @@ -98,7 +98,13 @@ const formatOneListResult = options => (entity, idx, A) => {
inner.setAttribute('data-key', key)
}

if (value) {
if (fontawesome) {
const icon = document.createElement('i')
inner.appendChild(icon)
icon.className = fontawesome
inner.setAttribute('data-value', value) // in case tests need the actual value, not the icon

} else if (value) {
Promise.resolve(value)
.then(value => inner.appendChild(value.nodeName ? value : document.createTextNode(value.toString())))
} else {
Expand Down Expand Up @@ -174,7 +180,7 @@ const formatOneListResult = options => (entity, idx, A) => {

// add any attributes that should appear *before* the name column
if (entity.beforeAttributes) {
entity.beforeAttributes.forEach(({key, value, css='', outerCSS='', onclick}) => addCell(outerCSS, value, css, undefined, onclick, undefined, key))
entity.beforeAttributes.forEach(({key, value, css='', outerCSS='', onclick, fontawesome}) => addCell(outerCSS, value, css, undefined, onclick, undefined, key, fontawesome))
}

// now add the clickable name
Expand Down Expand Up @@ -219,7 +225,7 @@ const formatOneListResult = options => (entity, idx, A) => {
// case-specific cells
//
if (entity.attributes) {
entity.attributes.forEach(({key, value, css='', outerCSS='', watch, onclick}) => addCell(outerCSS, value, css, undefined, onclick, watch, key))
entity.attributes.forEach(({key, value, css='', outerCSS='', watch, onclick, fontawesome}) => addCell(outerCSS, value, css, undefined, onclick, watch, key, fontawesome))

} else if (entity.type === 'actions') {
// action-specific cells
Expand Down
145 changes: 141 additions & 4 deletions app/plugins/ui/commands/openwhisk-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ let localStorageKey = 'wsk.apihost',
localStorageKeyIgnoreCerts = 'wsk.apihost.ignoreCerts',
apiHost = process.env.__OW_API_HOST || wskprops.APIHOST || localStorage.getItem(localStorageKey) || 'https://openwhisk.ng.bluemix.net',
auth = process.env.__OW_API_KEY || wskprops.AUTH,
apigw_token = process.env.__OW_APIGW_TOKEN || wskprops.APIGW_ACCESS_TOKEN || 'localhostNeedsSomething', // localhost needs some non-empty string
apigw_space_guid = process.env.__OW_APIGW_SPACE_GUID || wskprops.APIGW_SPACE_GUID,
ow

let userRequestedIgnoreCerts = localStorage.getItem(localStorageKeyIgnoreCerts) !== undefined
Expand All @@ -94,16 +96,20 @@ let ignoreCerts = apiHost => userRequestedIgnoreCerts || apiHost.indexOf('localh
/** these are the module's exported functions */
let self = {}

debug('initOW')
const initOW = () => {
ow = self.ow = openwhisk({
const owConfig = {
apihost: apiHost,
api_key: auth,
apigw_token, apigw_space_guid,
ignore_certs: ignoreCerts(apiHost)
})
}
debug('initOW', owConfig)
ow = self.ow = openwhisk(owConfig)
ow.api = ow.routes
delete ow.routes
debug('initOW done')
}
if (apiHost && auth) initOW()
debug('initOW done')

/** is a given entity type CRUDable? i.e. does it have get and update operations, and parameters and annotations properties? */
const isCRUDable = {
Expand Down Expand Up @@ -514,6 +520,128 @@ const standardViewModes = (defaultMode, fn) => {
}
}

/** flatten an array of arrays */
const flatten = arrays => [].concat.apply([], arrays)

/** api gateway actions */
specials.api = {
get: (options, argv) => {
if (!options) return
const maybeVerb = argv[1]
const split = options.name.split('/')
let path = options.name
if (split.length > 0) {
options.name = `/${split[1]}`
path = `/${split[2]}`
}
return {
postprocess: res => {
debug('raw output of api get', res)
const { apidoc } = res.apis[0].value
const { basePath } = apidoc
const apipath = apidoc.paths[path]
const verb = maybeVerb || Object.keys(apipath)[0]
const { action:name, namespace } = apipath[verb]['x-openwhisk']
debug('api details', namespace, name, verb)

return repl.qexec(`wsk action get "/${namespace}/${name}"`)
.then(action => Object.assign(action, {
name, namespace,
packageName: `${verb} ${basePath}${path}`
}))
}
}
},
create: (options, argv) => {
if (argv && argv.length === 3) {
options.basepath = options.name
options.relpath = argv[0]
options.operation = argv[1]
options.action = argv[2]
} else if (argv && argv.length === 2) {
options.relpath = options.name
options.operation = argv[0]
options.action = argv[1]
} else if (options && options['config-file']) {
//fs.readFileSync(options['config-file'])
throw new Error('config-file support not yet implemented')
}

return {
postprocess: ({apidoc}) => {
const { basePath } = apidoc
const path = Object.keys(apidoc.paths)[0]
const api = apidoc.paths[path]
const verb = Object.keys(api)[0]
const { action:name, namespace} = api[verb]['x-openwhisk']

return repl.qexec(`wsk action get "/${namespace}/${name}"`)
.then(action => Object.assign(action, {
name, namespace,
packageName: `${verb} ${basePath}${path}`
}))
}
}
},
list: () => {
return {
// turn the result into an entity tuple model
postprocess: res => {
debug('raw output of api list', res)

// main list for each api
return flatten((res.apis || []).map(({value}) => {
// one sublist for each path
const basePath = value.apidoc.basePath
const baseUrl = value.gwApiUrl

return flatten(Object.keys(value.apidoc.paths).map(path => {
const api = value.apidoc.paths[path]

// one sub-sublist for each verb of the api
return Object.keys(api).map(verb => {
const { action, namespace } = api[verb]['x-openwhisk']
const name = `${basePath}${path}`
const url = `${baseUrl}${path}`
const actionFqn = `/${namespace}/${action}`

// here is the entity for that api/path/verb:
return {
name, namespace,
onclick: () => {
return repl.pexec(`wsk api get ${repl.encodeComponent(name)} ${verb}`)
},
attributes: [
{ key: 'verb', value: verb },
{ key: 'action', value: action, onclick: () => repl.pexec(`wsk action get ${repl.encodeComponent(actionFqn)}`) },
{ key: 'url', value: url, fontawesome: 'fas fa-external-link-square-alt',
css: 'clickable clickable-blatant', onclick: () => window.open(url, '_blank') },
{ key: 'copy', fontawesome: 'fas fa-clipboard', css: 'clickable clickable-blatant',
onclick: evt => {
const target = evt.currentTarget
require('electron').clipboard.writeText(url)

const svg = target.querySelector('svg')
svg.classList.remove('fa-clipboard')
svg.classList.add('fa-clipboard-check')

setTimeout(() => {
const svg = target.querySelector('svg')
svg.classList.remove('fa-clipboard-check')
svg.classList.add('fa-clipboard')
}, 1500)
}
}
]
}
})
}))
}))
}
}
}
}

const actionSpecificModes = [{ mode: 'code', defaultMode: true }, { mode: 'limits' }]
specials.actions = {
get: standardViewModes(actionSpecificModes),
Expand Down Expand Up @@ -845,6 +973,9 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block
}
}

// postprocess the output of openwhisk; default is do nothing
let postprocess = x=>x

if (specials[entity] && specials[entity][verb]) {
const res = specials[entity][verb](options, argv.slice(restIndex), verb)
if (res && res.verb) {
Expand All @@ -857,6 +988,11 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block
if (res && res.options) {
options = res.options
}

if (res && res.postprocess) {
// postprocess the output of openwhisk
postprocess = res.postprocess
}
}
// process the entity-naming "nominal" argument
//if (!(syn_options && syn_options.noNominalArgument) && argv_without_options[idx]) {
Expand Down Expand Up @@ -933,6 +1069,7 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block

return preflight(verb, options)
.then(options => ow[entity][verb](options))
.then(postprocess)
.then(response => {
// amend the history entry with a selected subset of the response
if (execOptions && execOptions.history) {
Expand Down
58 changes: 58 additions & 0 deletions app/plugins/ui/commands/openwhisk-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,64 @@ module.exports = {
related: all.except('wsk rule')
},

api: { title: 'API Gateway operations',
header: 'These commands will help you to work with routes and the API Gateway.',
example: 'wsk api <command>',
commandPrefix: 'wsk api',
nRowsInViewport: 4, // list all four, since we have a short list
available: [{ command: 'list', strict: 'list',
docs: 'list all APIs',
example: 'wsk api list',
optional: skipAndLimit,
parents: context('api')
},
{ command: 'get', strict: 'get',
docs: 'get API details',
example: 'wsk api get <api>',
oneof: [
{ name: 'api', docs: 'the name of an API' },
{ name: 'path', docs: 'the full base/path route an API' }
],
optional: [
{ name: 'verb', positional: true, docs: 'the verb to show' },
{ name: '--format', docs: 'specify the API output TYPE, either json or yaml',
allowed: ['json', 'yaml'], defaultValue: 'json'},
{ name: '--full', alias: '-f', docs: 'display full API configuration details' }
],
parents: context('api')
},
{ command: 'delete', strict: 'delete',
docs: 'delete an API',
example: 'wsk api delete <api>',
required: [
{ name: 'api', docs: 'the name of an API' }
],
optional: [
{ name: 'path', positional: true, docs: 'the path of the API' },
{ name: 'verb', positional: true, docs: 'the verb of the API' }
],
parents: context('api')
},
{ command: 'create', strict: 'create',
docs: 'create a new API',
example: 'wsk api create <[base] path verb action>',
required: [
{ name: 'path', docs: 'path for the API' },
{ name: 'verb', docs: 'the HTTP method' },
{ name: 'action', docs: 'the OpenWhisk action to invoke' }
],
optional: [
{ name: 'base', positional: true, docs: 'base path for the API' },
{ name: '--apiname', alias: '-n', docs: 'friendly name of the API' },
{ name: '--config-file', alias: '-c', docs: 'file containing API configuration in swagger JSON format' },
{ name: '--response-type', docs: 'set the web action response type',
allowed: ['http', 'json', 'text', 'svg'], defaultValue: 'json' },
],
parents: context('api')
}
]
},

triggers: { title: 'Trigger operations',
header: 'These commands will help you to work with triggers.',
example: 'wsk trigger <command>',
Expand Down
2 changes: 2 additions & 0 deletions tests/lib/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,5 @@ const waitForActivationOrSession = entityType => (app, activationId, { name='' }
}
exports.waitForActivation = waitForActivationOrSession('activation')
exports.waitForSession = waitForActivationOrSession('session')

exports.apiHost = constants.API_HOST
60 changes: 60 additions & 0 deletions tests/tests/passes/04/apis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2018 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const common = require('../../../lib/common'),
rp = common.rp,
openwhisk = require('../../../lib/openwhisk'),
ui = require('../../../lib/ui'),
assert = require('assert'),
keys = ui.keys,
cli = ui.cli,
sidecar = ui.sidecar,
actionName1 = 'foo1'

describe('Create api gateway', function() {
before(common.before(this))
after(common.after(this))

it('should have an active repl', () => cli.waitForRepl(this.app))

it('should create an echo action', () => cli.do(`let echo = x=>x`, this.app)
.then(cli.expectOK)
.then(sidecar.expectOpen)
.then(sidecar.expectShowing('echo'))
.catch(common.oops(this)))

it('should webbify the action', () => cli.do(`webbify`, this.app)
.then(cli.expectOK)
.then(sidecar.expectOpen)
.then(sidecar.expectShowing('echo'))
.catch(common.oops(this)))

it('should create the api', () => cli.do(`wsk api create /hello /world get echo`, this.app)
.then(cli.expectOK)
.then(sidecar.expectOpen)
.then(sidecar.expectShowing('echo'))
.catch(common.oops(this)))

it('should list and invoke the api', () => cli.do(`wsk api list`, this.app)
.then(res => cli.expectOKWithOnly('/hello/world')(res)
.then(() => res.count)
.then(N => this.app.client.getAttribute(`${ui.selectors.LIST_RESULTS_N(N)} [data-key="url"]`, "data-value")))
.then(href => href.replace(/(http:\/\/)?172\.17\.0\.1/, ui.apiHost.replace('https://', 'http://')))
.then(href => { console.error('api href', href); return href; })
.then(href => rp({ url: `${href}?foo=bar`, rejectUnauthorized: false }))
.then(ui.expectSubset({foo: 'bar'}))
.catch(common.oops(this)))
})

0 comments on commit 9dbe88f

Please sign in to comment.