Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit 8e8fc7b

Browse files
committed
add wsk api support
Fixes #973
1 parent afa123f commit 8e8fc7b

File tree

5 files changed

+320
-8
lines changed

5 files changed

+320
-8
lines changed

app/content/js/repl.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const formatOneListResult = options => (entity, idx, A) => {
8686
prefix.appendChild(prettyType)*/
8787

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

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

101-
if (value) {
101+
if (fontawesome) {
102+
const icon = document.createElement('i')
103+
inner.appendChild(icon)
104+
icon.className = fontawesome
105+
inner.setAttribute('data-value', value) // in case tests need the actual value, not the icon
106+
107+
} else if (value) {
102108
Promise.resolve(value)
103109
.then(value => inner.appendChild(value.nodeName ? value : document.createTextNode(value.toString())))
104110
} else {
@@ -174,7 +180,7 @@ const formatOneListResult = options => (entity, idx, A) => {
174180

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

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

224230
} else if (entity.type === 'actions') {
225231
// action-specific cells

app/plugins/ui/commands/openwhisk-core.js

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ let localStorageKey = 'wsk.apihost',
8686
localStorageKeyIgnoreCerts = 'wsk.apihost.ignoreCerts',
8787
apiHost = process.env.__OW_API_HOST || wskprops.APIHOST || localStorage.getItem(localStorageKey) || 'https://openwhisk.ng.bluemix.net',
8888
auth = process.env.__OW_API_KEY || wskprops.AUTH,
89+
apigw_token = process.env.__OW_APIGW_TOKEN || wskprops.APIGW_ACCESS_TOKEN || 'localhostNeedsSomething', // localhost needs some non-empty string
90+
apigw_space_guid = process.env.__OW_APIGW_SPACE_GUID || wskprops.APIGW_SPACE_GUID,
8991
ow
9092

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

97-
debug('initOW')
9899
const initOW = () => {
99-
ow = self.ow = openwhisk({
100+
const owConfig = {
100101
apihost: apiHost,
101102
api_key: auth,
103+
apigw_token, apigw_space_guid,
102104
ignore_certs: ignoreCerts(apiHost)
103-
})
105+
}
106+
debug('initOW', owConfig)
107+
ow = self.ow = openwhisk(owConfig)
108+
ow.api = ow.routes
109+
delete ow.routes
110+
debug('initOW done')
104111
}
105112
if (apiHost && auth) initOW()
106-
debug('initOW done')
107113

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

523+
/** flatten an array of arrays */
524+
const flatten = arrays => [].concat.apply([], arrays)
525+
526+
/** api gateway actions */
527+
specials.api = {
528+
get: (options, argv) => {
529+
if (!options) return
530+
const maybeVerb = argv[1]
531+
const split = options.name.split('/')
532+
let path = options.name
533+
if (split.length > 0) {
534+
options.name = `/${split[1]}`
535+
path = `/${split[2]}`
536+
}
537+
return {
538+
postprocess: res => {
539+
// we need to present the user with an entity of some
540+
// sort; the "api create" api does not return a usual
541+
// entity, as does the rest of the openwhisk API; so
542+
// we have to manufacture something reasonable here
543+
debug('raw output of api get', res)
544+
const { apidoc } = res.apis[0].value
545+
const { basePath } = apidoc
546+
const apipath = apidoc.paths[path]
547+
const verb = maybeVerb || Object.keys(apipath)[0]
548+
const { action:name, namespace } = apipath[verb]['x-openwhisk']
549+
debug('api details', namespace, name, verb)
550+
551+
// our "something reasonable" is the action impl, but
552+
// decorated with the name of the API and the verb
553+
return repl.qexec(`wsk action get "/${namespace}/${name}"`)
554+
.then(action => Object.assign(action, {
555+
name, namespace,
556+
packageName: `${verb} ${basePath}${path}`
557+
}))
558+
}
559+
}
560+
},
561+
create: (options, argv) => {
562+
if (argv && argv.length === 3) {
563+
options.basepath = options.name
564+
options.relpath = argv[0]
565+
options.operation = argv[1]
566+
options.action = argv[2]
567+
} else if (argv && argv.length === 2) {
568+
options.relpath = options.name
569+
options.operation = argv[0]
570+
options.action = argv[1]
571+
} else if (options && options['config-file']) {
572+
//fs.readFileSync(options['config-file'])
573+
throw new Error('config-file support not yet implemented')
574+
}
575+
576+
return {
577+
preprocess: _ => {
578+
// we need to confirm that the action is web-exported
579+
580+
// this is the desired action impl for the api
581+
const name = argv[argv.length - 1]
582+
debug('fetching action', name)
583+
584+
return ow.actions.get(owOpts({ name }))
585+
.then(action => {
586+
const isWebExported = action.annotations.find(({key}) => key === 'web-export')
587+
if (!isWebExported) {
588+
const error = new Error(`Action '${name}' is not a web action. Issue 'wsk action update "${name}" --web true' to convert the action to a web action.`)
589+
error.code = 412 // precondition failed
590+
throw error
591+
}
592+
})
593+
.then(() => _) // on success, return whatever preprocess was given as input
594+
.catch(err => {
595+
if (err.statusCode === 404) {
596+
const error = new Error(`Unable to get action '${name}': The requested resource does not exist.`)
597+
error.code = 404 // not found
598+
throw error
599+
} else {
600+
throw err
601+
}
602+
})
603+
},
604+
postprocess: ({apidoc}) => {
605+
const { basePath } = apidoc
606+
const path = Object.keys(apidoc.paths)[0]
607+
const api = apidoc.paths[path]
608+
const verb = Object.keys(api)[0]
609+
const { action:name, namespace} = api[verb]['x-openwhisk']
610+
611+
// manufacture an entity-like object
612+
return repl.qexec(`wsk action get "/${namespace}/${name}"`)
613+
.then(action => Object.assign(action, {
614+
name, namespace,
615+
packageName: `${verb} ${basePath}${path}`
616+
}))
617+
}
618+
}
619+
},
620+
list: () => {
621+
return {
622+
// turn the result into an entity tuple model
623+
postprocess: res => {
624+
debug('raw output of api list', res)
625+
626+
// main list for each api
627+
return flatten((res.apis || []).map(({value}) => {
628+
// one sublist for each path
629+
const basePath = value.apidoc.basePath
630+
const baseUrl = value.gwApiUrl
631+
632+
return flatten(Object.keys(value.apidoc.paths).map(path => {
633+
const api = value.apidoc.paths[path]
634+
635+
// one sub-sublist for each verb of the api
636+
return Object.keys(api).map(verb => {
637+
const { action, namespace } = api[verb]['x-openwhisk']
638+
const name = `${basePath}${path}`
639+
const url = `${baseUrl}${path}`
640+
const actionFqn = `/${namespace}/${action}`
641+
642+
// here is the entity for that api/path/verb:
643+
return {
644+
name, namespace,
645+
onclick: () => {
646+
return repl.pexec(`wsk api get ${repl.encodeComponent(name)} ${verb}`)
647+
},
648+
attributes: [
649+
{ key: 'verb', value: verb },
650+
{ key: 'action', value: action, onclick: () => repl.pexec(`wsk action get ${repl.encodeComponent(actionFqn)}`) },
651+
{ key: 'url', value: url, fontawesome: 'fas fa-external-link-square-alt',
652+
css: 'clickable clickable-blatant', onclick: () => window.open(url, '_blank') },
653+
{ key: 'copy', fontawesome: 'fas fa-clipboard', css: 'clickable clickable-blatant',
654+
onclick: evt => {
655+
const target = evt.currentTarget
656+
require('electron').clipboard.writeText(url)
657+
658+
const svg = target.querySelector('svg')
659+
svg.classList.remove('fa-clipboard')
660+
svg.classList.add('fa-clipboard-check')
661+
662+
setTimeout(() => {
663+
const svg = target.querySelector('svg')
664+
svg.classList.remove('fa-clipboard-check')
665+
svg.classList.add('fa-clipboard')
666+
}, 1500)
667+
}
668+
}
669+
]
670+
}
671+
})
672+
}))
673+
}))
674+
}
675+
}
676+
}
677+
}
678+
517679
const actionSpecificModes = [{ mode: 'code', defaultMode: true }, { mode: 'limits' }]
518680
specials.actions = {
519681
get: standardViewModes(actionSpecificModes),
@@ -845,6 +1007,10 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block
8451007
}
8461008
}
8471009

1010+
// pre and post-process the output of openwhisk; default is do nothing
1011+
let postprocess = x=>x
1012+
let preprocess = x=>x
1013+
8481014
if (specials[entity] && specials[entity][verb]) {
8491015
const res = specials[entity][verb](options, argv.slice(restIndex), verb)
8501016
if (res && res.verb) {
@@ -857,6 +1023,16 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block
8571023
if (res && res.options) {
8581024
options = res.options
8591025
}
1026+
1027+
if (res && res.preprocess) {
1028+
// postprocess the output of openwhisk
1029+
preprocess = res.preprocess
1030+
}
1031+
1032+
if (res && res.postprocess) {
1033+
// postprocess the output of openwhisk
1034+
postprocess = res.postprocess
1035+
}
8601036
}
8611037
// process the entity-naming "nominal" argument
8621038
//if (!(syn_options && syn_options.noNominalArgument) && argv_without_options[idx]) {
@@ -932,7 +1108,9 @@ const executor = (_entity, _verb, verbSynonym, commandTree, preflight) => (block
9321108
})
9331109

9341110
return preflight(verb, options)
1111+
.then(preprocess)
9351112
.then(options => ow[entity][verb](options))
1113+
.then(postprocess)
9361114
.then(response => {
9371115
// amend the history entry with a selected subset of the response
9381116
if (execOptions && execOptions.history) {

app/plugins/ui/commands/openwhisk-usage.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,64 @@ module.exports = {
240240
related: all.except('wsk rule')
241241
},
242242

243+
api: { title: 'API Gateway operations',
244+
header: 'These commands will help you to work with routes and the API Gateway.',
245+
example: 'wsk api <command>',
246+
commandPrefix: 'wsk api',
247+
nRowsInViewport: 4, // list all four, since we have a short list
248+
available: [{ command: 'list', strict: 'list',
249+
docs: 'list all APIs',
250+
example: 'wsk api list',
251+
optional: skipAndLimit,
252+
parents: context('api')
253+
},
254+
{ command: 'get', strict: 'get',
255+
docs: 'get API details',
256+
example: 'wsk api get <api>',
257+
oneof: [
258+
{ name: 'api', docs: 'the name of an API' },
259+
{ name: 'path', docs: 'the full base/path route an API' }
260+
],
261+
optional: [
262+
{ name: 'verb', positional: true, docs: 'the verb to show' },
263+
{ name: '--format', docs: 'specify the API output TYPE, either json or yaml',
264+
allowed: ['json', 'yaml'], defaultValue: 'json'},
265+
{ name: '--full', alias: '-f', docs: 'display full API configuration details' }
266+
],
267+
parents: context('api')
268+
},
269+
{ command: 'delete', strict: 'delete',
270+
docs: 'delete an API',
271+
example: 'wsk api delete <api>',
272+
required: [
273+
{ name: 'api', docs: 'the name of an API' }
274+
],
275+
optional: [
276+
{ name: 'path', positional: true, docs: 'the path of the API' },
277+
{ name: 'verb', positional: true, docs: 'the verb of the API' }
278+
],
279+
parents: context('api')
280+
},
281+
{ command: 'create', strict: 'create',
282+
docs: 'create a new API',
283+
example: 'wsk api create <[base] path verb action>',
284+
required: [
285+
{ name: 'path', docs: 'path for the API' },
286+
{ name: 'verb', docs: 'the HTTP method' },
287+
{ name: 'action', docs: 'the OpenWhisk action to invoke' }
288+
],
289+
optional: [
290+
{ name: 'base', positional: true, docs: 'base path for the API' },
291+
{ name: '--apiname', alias: '-n', docs: 'friendly name of the API' },
292+
{ name: '--config-file', alias: '-c', docs: 'file containing API configuration in swagger JSON format' },
293+
{ name: '--response-type', docs: 'set the web action response type',
294+
allowed: ['http', 'json', 'text', 'svg'], defaultValue: 'json' },
295+
],
296+
parents: context('api')
297+
}
298+
]
299+
},
300+
243301
triggers: { title: 'Trigger operations',
244302
header: 'These commands will help you to work with triggers.',
245303
example: 'wsk trigger <command>',

tests/lib/ui.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,5 @@ const waitForActivationOrSession = entityType => (app, activationId, { name='' }
460460
}
461461
exports.waitForActivation = waitForActivationOrSession('activation')
462462
exports.waitForSession = waitForActivationOrSession('session')
463+
464+
exports.apiHost = constants.API_HOST

0 commit comments

Comments
 (0)