blob: d1124eddb660574ade72500334e20ffafb6222bf [file] [log] [blame]
/*
* Copyright 2015-2016 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.
*/
import Foundation
import KituraNet
import Dispatch
class Whisk {
class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {
let parsedAction = parseQualifiedName(name: action)
let strBlocking = blocking ? "true" : "false"
let path = "/api/v1/namespaces/\(parsedAction.namespace)/actions/\(parsedAction.name)?blocking=\(strBlocking)"
return postSyncronish(uriPath: path, params: params)
}
class func trigger(eventNamed event : String, withParameters params : [String:Any]) -> [String:Any] {
let parsedEvent = parseQualifiedName(name: event)
let path = "/api/v1/namespaces/\(parsedEvent.namespace)/triggers/\(parsedEvent.name)?blocking=true"
return postSyncronish(uriPath: path, params: params)
}
// handle the GCD dance to make the post async, but then obtain/return
// the result from this function sync
private class func postSyncronish(uriPath path: String, params : [String:Any]) -> [String:Any] {
var response : [String:Any]!
let queue = DispatchQueue.global()
let invokeGroup = DispatchGroup()
invokeGroup.enter()
queue.async {
post(uriPath: path, params: params, group: invokeGroup) { result in
response = result
}
}
// On one hand, FOREVER seems like an awfully long time...
// But on the other hand, I think we can rely on the system to kill this
// if it exceeds a reasonable execution time.
switch invokeGroup.wait(timeout: DispatchTime.distantFuture) {
case DispatchTimeoutResult.success:
break
case DispatchTimeoutResult.timedOut:
break
}
return response
}
/**
* Initializes with host, port and authKey determined from environment variables
* __OW_API_HOST and __OW_API_KEY, respectively.
*/
private class func initializeCommunication() -> (httpType: String, host : String, port : Int16, authKey : String) {
let env = ProcessInfo.processInfo.environment
var edgeHost : String!
if let edgeHostEnv : String = env["__OW_API_HOST"] {
edgeHost = "\(edgeHostEnv)"
} else {
fatalError("__OW_API_HOST environment variable was not set.")
}
var protocolIndex = edgeHost.startIndex
//default to https
var httpType = "https://"
var port : Int16 = 443
// check if protocol is included in environment variable
if edgeHost.hasPrefix("https://") {
protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 8)
} else if edgeHost.hasPrefix("http://") {
protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 7)
httpType = "http://"
port = 80
}
let hostname = edgeHost.substring(from: protocolIndex)
let hostComponents = hostname.components(separatedBy: ":")
let host = hostComponents[0]
if hostComponents.count == 2 {
port = Int16(hostComponents[1])!
}
var authKey = "authKey"
if let authKeyEnv : String = env["__OW_API_KEY"] {
authKey = authKeyEnv
}
return (httpType, host, port, authKey)
}
// actually do the POST call to the specified OpenWhisk URI path
private class func post(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
let communicationDetails = initializeCommunication()
let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
let base64EncodedAuthKey = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let headers = ["Content-Type" : "application/json",
"Authorization" : "Basic \(base64EncodedAuthKey)"]
guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
callback(["error": "Error encoding uri path to make openwhisk REST call."])
return
}
// TODO vary the schema based on the port?
let requestOptions = [ClientRequest.Options.schema(communicationDetails.httpType),
ClientRequest.Options.method("post"),
ClientRequest.Options.hostname(communicationDetails.host),
ClientRequest.Options.port(communicationDetails.port),
ClientRequest.Options.path(encodedPath),
ClientRequest.Options.headers(headers),
ClientRequest.Options.disableSSLVerification]
let request = HTTP.request(requestOptions) { response in
// exit group after we are done
defer {
group.leave()
}
if response != nil {
do {
// this is odd, but that's just how KituraNet has you get
// the response as NSData
var jsonData = Data()
try response!.readAllData(into: &jsonData)
switch WhiskJsonUtils.getJsonType(jsonData: jsonData) {
case .Dictionary:
if let resp = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
callback(resp)
} else {
callback(["error": "Could not parse a valid JSON response."])
}
case .Array:
if WhiskJsonUtils.jsonDataToArray(jsonData: jsonData) != nil {
callback(["error": "Response is an array, expecting dictionary."])
} else {
callback(["error": "Could not parse a valid JSON response."])
}
case .Undefined:
callback(["error": "Could not parse a valid JSON response."])
}
} catch {
callback(["error": "Could not parse a valid JSON response."])
}
} else {
callback(["error": "Did not receive a response."])
}
}
// turn params into JSON data
if let jsonData = WhiskJsonUtils.dictionaryToJsonString(jsonDict: params) {
request.write(from: jsonData)
request.end()
} else {
callback(["error": "Could not parse parameters."])
group.leave()
}
}
/**
* This function is currently unused but ready when we want to switch to using URLSession instead of KituraNet.
*/
private class func postUrlSession(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
let communicationDetails = initializeCommunication()
guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
callback(["error": "Error encoding uri path to make openwhisk REST call."])
return
}
let urlStr = "\(communicationDetails.httpType)\(communicationDetails.host):\(communicationDetails.port)\(encodedPath)"
if let url = URL(string: urlStr) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
do {
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: params)
let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
let base64EncodedAuthKey = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
request.addValue("Basic \(base64EncodedAuthKey)", forHTTPHeaderField: "Authorization")
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
// exit group after we are done
defer {
group.leave()
}
if let error = error {
callback(["error":error.localizedDescription])
} else {
if let data = data {
do {
let respJson = try JSONSerialization.jsonObject(with: data)
if respJson is [String:Any] {
callback(respJson as! [String:Any])
} else {
callback(["error":" response from server is not a dictionary"])
}
} catch {
callback(["error":"Error creating json from response: \(error)"])
}
}
}
})
task.resume()
} catch {
callback(["error":"Got error creating params body: \(error)"])
}
}
}
// separate an OpenWhisk qualified name (e.g. "/whisk.system/samples/date")
// into namespace and name components
private class func parseQualifiedName(name qualifiedName : String) -> (namespace : String, name : String) {
let defaultNamespace = "_"
let delimiter = "/"
let segments :[String] = qualifiedName.components(separatedBy: delimiter)
if segments.count > 2 {
return (segments[1], Array(segments[2..<segments.count]).joined(separator: delimiter))
} else {
// allow both "/theName" and "theName"
let name = qualifiedName.hasPrefix(delimiter) ? segments[1] : segments[0]
return (defaultNamespace, name)
}
}
}