| /* |
| * 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 |
| |
| public enum WhiskNetworkError: Error { |
| case malformedUrlString(url: String, cause: String) |
| case qualifiedNameFormat(description: String) |
| } |
| |
| |
| /* Type of Whisk operation requested */ |
| enum WhiskCallType { |
| case action |
| case trigger |
| case package |
| case rule |
| case sequence |
| } |
| |
| public struct WhiskCredentials { |
| // whisk credentials |
| public var accessKey: String! |
| public var accessToken: String! |
| |
| public init(accessKey: String?, accessToken: String?) { |
| self.accessToken = accessToken |
| self.accessKey = accessKey |
| } |
| |
| public func getBase64AuthString() -> String { |
| // set authorization string |
| let loginString = (accessKey+":"+accessToken) as NSString |
| |
| let loginData: Data = loginString.data(using: String.Encoding.utf8.rawValue)! |
| let base64LoginString = loginData.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) |
| |
| return base64LoginString |
| } |
| |
| } |
| |
| |
| class WhiskAPI { |
| |
| // Default value for Whisk backend |
| var DefaultBaseURL = "https://openwhisk.ng.bluemix.net/api/v1/" |
| |
| // supported Feeds |
| let AlarmTriggerFeed = "/whisk.system/alarms/alarm" |
| |
| // user settable backend |
| var whiskBaseURL: String? |
| |
| // credentials |
| let whiskCredentials: WhiskCredentials! |
| |
| // network classes |
| let networkManager: WhiskNetworkManager! |
| // return base URL of backend including common path for all API calls |
| var baseURL: String? { |
| set { |
| if let url = newValue { |
| let c = url.characters.last |
| let separater = c == "/" ? "" : "/" |
| whiskBaseURL = url + separater + "api/v1/" |
| } else { |
| whiskBaseURL = nil |
| } |
| } |
| get { |
| return whiskBaseURL |
| } |
| } |
| |
| // Initialize with credentials, region currently not used |
| init(credentials: WhiskCredentials, session: URLSession? = nil) { |
| // initialize |
| whiskCredentials = credentials |
| |
| let sess: URLSession! |
| if let _ = session { |
| sess = session |
| } else { |
| let sessConfig = URLSessionConfiguration.default |
| sess = URLSession(configuration: sessConfig) |
| } |
| |
| networkManager = WhiskNetworkManager(credentials: credentials, session: sess) |
| } |
| |
| /* Convert qualified name string into component parts of action or trigger call */ |
| func processQualifiedName(qName: String) throws -> (namespace:String, package: String?, name: String) { |
| var namespace = "_" |
| var package: String? = nil |
| var name = "" |
| var doesSpecifyNamespace = false |
| |
| if qName.characters.first == "/" { |
| doesSpecifyNamespace = true |
| } |
| |
| let pathParts = qName.characters.split { $0 == "/" }.map(String.init) |
| |
| if doesSpecifyNamespace == true { |
| if pathParts.count == 2 { |
| namespace = pathParts[0] |
| name = pathParts[1] |
| } else if pathParts.count == 3 { |
| namespace = pathParts[0] |
| package = pathParts[1] |
| name = pathParts[2] |
| } else { |
| throw WhiskNetworkError.qualifiedNameFormat(description: "Cannot parse \(qName)") |
| } |
| } else { |
| if pathParts.count == 1 { |
| name = pathParts[0] |
| } else if pathParts.count == 2 { |
| package = pathParts[0] |
| name = pathParts[1] |
| } else { |
| throw WhiskNetworkError.qualifiedNameFormat(description: "Cannot parse \(qName)") |
| } |
| } |
| |
| return (namespace, package, name) |
| } |
| |
| func createTrigger(name: String, namespace: String, parameters: Array<[String:AnyObject]>? = nil, group: DispatchGroup) throws { |
| |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| |
| let path = "namespaces/\(namespace)/triggers/\(name)" |
| var whiskParameters = [String:AnyObject]() |
| |
| if let parameters = parameters { |
| |
| var paramArray = Array<[String:AnyObject]>() |
| for param in parameters { |
| |
| for (key, value) in param { |
| var pair = [String:AnyObject]() |
| pair["key"] = key as AnyObject |
| pair["value"] = value |
| paramArray.append(pair) |
| } |
| } |
| |
| whiskParameters["parameters"] = paramArray as AnyObject |
| } |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: whiskParameters, group: group) |
| |
| } |
| |
| func createFeed(name: String, namespace: String, trigger: Trigger, group: DispatchGroup) throws { |
| |
| switch trigger.feed as! String { |
| case AlarmTriggerFeed: |
| try createAlarmsFeed(name: name, namespace: namespace, trigger: trigger, group: group) |
| default: |
| throw WhiskProjectError.unsupportedFeedType(cause: "Feed trigger \(trigger.feed) not supported") |
| } |
| } |
| |
| func createAlarmsFeed(name: String, namespace: String, trigger: Trigger, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| let path = "namespaces/\(namespace)/triggers/\(name)" |
| |
| let annotations: [String:AnyObject] = ["value": trigger.feed!, "key": "feed" as AnyObject] |
| let parameters: [String: [[String:AnyObject]]] = ["annotations": [annotations]] |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: parameters as [String : AnyObject]?, group: group) { response, error in |
| |
| if let error = error { |
| print("Error creating trigger \(name) for feed \(trigger.feed), error: \(error)") |
| } else { |
| group.enter() |
| //DispatchQueue.main.after(when: DispatchTime.now() + 0.5) { |
| |
| let feedPath = "namespaces/whisk.system/actions/alarms/alarm" |
| do { |
| |
| var params: [String:AnyObject]? = nil |
| |
| if let feedParams = trigger.parameters { |
| params = [String:AnyObject]() |
| for obj in feedParams { |
| let dict = obj as [String:AnyObject] |
| for (name, value) in dict { |
| if name.lowercased() == "cron" { |
| params?["cron"] = value |
| } else if name == "trigger_payload" { |
| params?["trigger_payload"] = value |
| } |
| } |
| } |
| |
| params?["authKey"] = "\(self.networkManager.whiskCredentials.accessKey):\(self.networkManager.whiskCredentials.accessToken)" as AnyObject |
| params?["lifecycleEvent"] = "CREATE" as AnyObject |
| params?["triggerName"] = ("/"+namespace+"/"+name) as AnyObject |
| } |
| |
| |
| try self.networkManager.postCall(url: urlStr, path: feedPath, parameters: params, group: group) { |
| response, error in |
| |
| if let error = error { |
| print("Error creating feed for trigger \(name), error: \(error)") |
| } |
| |
| } |
| } catch { |
| print("Error creating feed for trigger \(name), error: \(error)") |
| } |
| |
| |
| // } |
| } |
| |
| } |
| } |
| |
| func createAction(qualifiedName: String, kind: String, code: String, parameters: Array<[String:AnyObject]>? = nil, group: DispatchGroup) throws { |
| |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let nameParts = try processQualifiedName(qName: qualifiedName) |
| |
| var path = "" |
| if nameParts.package != nil { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.package!)/\(nameParts.name)" |
| } else { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.name)" |
| } |
| |
| let exec = ["kind":kind, "code": code] as [String:String] |
| let limits = ["timeout": 30000 as AnyObject, "memory":256 as AnyObject] as [String:AnyObject] |
| |
| var whiskParameters: [String:AnyObject] = ["exec":exec as AnyObject, "limits":limits as AnyObject] |
| |
| if let parameters = parameters { |
| |
| var paramArray = Array<[String:AnyObject]>() |
| for param in parameters { |
| |
| for (key, value) in param { |
| var pair = [String:AnyObject]() |
| pair["key"] = key as AnyObject |
| pair["value"] = value |
| paramArray.append(pair) |
| |
| } |
| |
| } |
| |
| whiskParameters["parameters"] = paramArray as AnyObject |
| } |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: whiskParameters, group: group) |
| |
| |
| } |
| |
| func createPackage(name: String, bindTo:String? = nil, namespace: String, parameters: Array<[String:AnyObject]>? = nil, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let path = "namespaces/\(namespace)/packages/\(name)" |
| |
| var whiskParameters = [String:AnyObject]() |
| |
| if let parameters = parameters { |
| |
| var paramArray = Array<[String:AnyObject]>() |
| for param in parameters { |
| for (key, value) in param { |
| var pair = [String:AnyObject]() |
| pair["key"] = key as AnyObject |
| pair["value"] = value |
| paramArray.append(pair) |
| } |
| } |
| |
| whiskParameters["parameters"] = paramArray as AnyObject |
| } |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: whiskParameters, group: group) |
| |
| } |
| |
| |
| func createRule(name: String, namespace: String, triggerName: String, actionName: String, group: DispatchGroup) throws { |
| |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| |
| let path = "namespaces/\(namespace)/rules/\(name)" |
| let whiskParameters = ["action": actionName,"trigger":triggerName] |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: whiskParameters as [String : AnyObject], group: group) { |
| response, error in |
| if let error = error { |
| print("Error creating rule \(name), error \(error)") |
| } else { |
| print("Created rule response \(response)") |
| } |
| } |
| |
| } |
| |
| func enableRule(name: String, namespace: String, triggerName: String, actionName: String, group: DispatchGroup) throws { |
| |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let path = "namespaces/\(namespace)/rules/\(name)" |
| |
| group.enter() |
| |
| try networkManager.postCall(url: urlStr, path: path, parameters: ["status":"active" as AnyObject], group: group) { |
| response, error in |
| if let error = error { |
| print("Error enabling rule \(name), error \(error)") |
| } |
| } |
| |
| } |
| |
| func createSequence(qualifiedName: String, actions:[String], group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let nameParts = try processQualifiedName(qName: qualifiedName) |
| |
| var path = "" |
| if nameParts.package != nil { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.package!)/\(nameParts.name)" |
| } else { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.name)" |
| } |
| |
| let exec = ["kind":"nodejs", "code": SequenceCode.getSequenceCode()] as [String:String] |
| let limits = ["timeout": 30000 as AnyObject, "memory":256 as AnyObject] as [String:AnyObject] |
| |
| var whiskParameters: [String:AnyObject] = ["exec":exec as AnyObject, "limits":limits as AnyObject] |
| |
| var paramArray = Array<[String:AnyObject]>() |
| let actionList = ["key": "_actions", "value": actions as AnyObject] as [String : Any] |
| paramArray.append(actionList as [String : AnyObject]) |
| whiskParameters["parameters"] = paramArray as AnyObject |
| |
| |
| group.enter() |
| try networkManager.putCall(url: urlStr, path: path, parameters: whiskParameters, group: group) |
| |
| |
| } |
| |
| func deleteAction(qualifiedName: String, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let nameParts = try processQualifiedName(qName: qualifiedName) |
| |
| var path = "" |
| if nameParts.package != nil { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.package!)/\(nameParts.name)" |
| } else { |
| path = "namespaces/\(nameParts.namespace)/actions/\(nameParts.name)" |
| } |
| |
| group.enter() |
| try networkManager.deleteCall(url: urlStr, path: path, group: group) |
| |
| } |
| |
| |
| func deletePackage(name: String, namespace: String, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let path = "namespaces/\(namespace)/packages/\(name)" |
| |
| group.enter() |
| try networkManager.deleteCall(url: urlStr, path: path, group: group) |
| |
| } |
| |
| func deleteTrigger(name: String, namespace: String, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let path = "namespaces/\(namespace)/triggers/\(name)" |
| |
| group.enter() |
| try networkManager.deleteCall(url: urlStr, path: path, group: group) { response, error in |
| |
| if let error = error { |
| print("Error deleting trigger \(name), \(error)") |
| } else if let response = response { |
| |
| if let annotations = response["annotations"] as? [[String:AnyObject]] { |
| for note in annotations { |
| |
| var isFeed = false |
| var feed = "" |
| for (att, value) in note { |
| if att == "key" { |
| if value as! String == "feed" { |
| isFeed = true |
| } |
| } else if att == "value" { |
| feed = value as! String |
| } |
| } |
| |
| if isFeed == true { |
| // delete the alarm |
| if feed == self.AlarmTriggerFeed { |
| do { |
| try self.deleteAlarmsFeed(namespace: namespace, name: name, group: group) |
| } catch { |
| print("Error deleting trigger feed \(name): \(error)") |
| } |
| } |
| } |
| |
| } |
| } |
| |
| } |
| |
| } |
| |
| } |
| |
| func deleteAlarmsFeed(namespace: String, name: String, group: DispatchGroup) throws { |
| var params = [String:AnyObject]() |
| |
| params["authKey"] = (self.networkManager.whiskCredentials.accessKey+":"+self.networkManager.whiskCredentials.accessToken) as AnyObject |
| params["lifecycleEvent"] = "DELETE" as AnyObject |
| params["triggerName"] = (namespace+"/"+name) as AnyObject |
| |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| let path = "namespaces/whisk.system/actions/alarms/alarm" |
| |
| group.enter() |
| try networkManager.postCall(url: urlStr, path: path, parameters: params, group: group) { response, error in |
| |
| if let error = error { |
| print("Error deleting alarm feed \(error)") |
| } else { |
| print("Succes deleting alarm feed \(response)") |
| } |
| |
| } |
| |
| } |
| |
| func deleteRule(name: String, namespace: String, group: DispatchGroup) throws { |
| let urlStr: String = whiskBaseURL != nil ? whiskBaseURL! : DefaultBaseURL |
| |
| let path = "namespaces/\(namespace)/rules/\(name)" |
| |
| group.enter() |
| try networkManager.postCall(url: urlStr, path: path, parameters: ["status":"inactive" as AnyObject], group: group) { response, error in |
| |
| if let error = error { |
| print("Error disabling rule \(name), error: \(error)") |
| } else { |
| |
| group.enter() |
| //DispatchQueue.main.after(when: DispatchTime.now() + 0.5) { |
| |
| do { |
| try self.networkManager.deleteCall(url: urlStr, path: path, group: group) |
| } catch { |
| print("Error deleting rule \(name), error: \(error)") |
| } |
| |
| |
| // } |
| } |
| |
| } |
| |
| } |
| |
| |
| |
| } |
| |
| class WhiskNetworkManager { |
| |
| let whiskCredentials: WhiskCredentials! |
| let urlSession: URLSession! |
| |
| init(credentials: WhiskCredentials, session: URLSession) { |
| self.whiskCredentials = credentials |
| self.urlSession = session |
| } |
| |
| func putCall(url: String, path: String, parameters: [String:AnyObject]? = nil, group: DispatchGroup, callback: (([String:Any]?, Error?) -> Void)? = nil) throws { |
| |
| let overwritePath = path+"?overwrite=true" |
| |
| // encode path |
| guard let encodedPath = overwritePath.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot encode url path \(path)") |
| } |
| |
| // create request |
| guard let nsUrl = URL(string:url+encodedPath) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot create URL from url String") |
| } |
| |
| var request = URLRequest(url: nsUrl) |
| request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") |
| request.addValue("Basic \(whiskCredentials.getBase64AuthString())", forHTTPHeaderField: "Authorization") |
| request.httpMethod = "PUT" |
| |
| |
| if let parameters = parameters { |
| request.httpBody = try JSONSerialization.data(withJSONObject: parameters as AnyObject, options: JSONSerialization.WritingOptions()) |
| } |
| |
| let task = urlSession.dataTask(with: request) { |
| data, response, error in |
| |
| let statusCode: Int! |
| if let httpResponse = response as? HTTPURLResponse { |
| statusCode = httpResponse.statusCode |
| } else { |
| statusCode = -1 |
| } |
| |
| if let error = error { |
| if let callback = callback { |
| callback(nil, error) |
| } else { |
| return |
| } |
| |
| } else { |
| if let callback = callback { |
| do { |
| let respMsg = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions()) |
| callback(["status":statusCode, "msg":respMsg], nil) |
| } catch { |
| print("Error serialzing server response \(error)") |
| } |
| } |
| } |
| |
| group.leave() |
| |
| } |
| |
| task.resume() |
| |
| } |
| |
| func deleteCall(url: String, path: String,group: DispatchGroup, callback: (([String:AnyObject]?, Error?) -> Void)? = nil) throws { |
| |
| // encode path |
| guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot encode url path \(path)") |
| } |
| |
| // create request |
| guard let nsUrl = URL(string:url+encodedPath) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot create URL from url String") |
| } |
| |
| var request = URLRequest(url: nsUrl) |
| request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") |
| request.addValue("Basic \(whiskCredentials.getBase64AuthString())", forHTTPHeaderField: "Authorization") |
| request.httpMethod = "DELETE" |
| |
| let task = urlSession.dataTask(with: request) { |
| data, response, error in |
| |
| let statusCode: Int! |
| if let httpResponse = response as? HTTPURLResponse { |
| statusCode = httpResponse.statusCode |
| } else { |
| statusCode = -1 |
| } |
| |
| if let error = error { |
| print("Error performing network call \(error), status: \(statusCode)") |
| return |
| |
| } else { |
| } |
| |
| |
| |
| if let callback = callback { |
| if let data = data { |
| do { |
| if let resp = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String:AnyObject] { |
| callback(resp, nil) |
| } |
| } catch { |
| print("Error in DELETE \(error)") |
| } |
| } else { |
| callback(["status":statusCode as AnyObject], nil) |
| } |
| |
| } |
| |
| group.leave() |
| |
| } |
| |
| task.resume() |
| |
| } |
| |
| func postCall(url: String, path: String, parameters: [String:AnyObject]?, group: DispatchGroup?, callback: @escaping ([String:Any]?, Error?) -> Void) throws { |
| |
| // encode path |
| guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot encode url path \(path)") |
| } |
| |
| // create request |
| guard let nsUrl = URL(string:url+encodedPath) else { |
| throw WhiskNetworkError.malformedUrlString(url: url, cause: "Cannot create URL from url String") |
| } |
| |
| var request = URLRequest(url: nsUrl) |
| request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") |
| request.addValue("Basic \(whiskCredentials.getBase64AuthString())", forHTTPHeaderField: "Authorization") |
| request.httpMethod = "POST" |
| |
| if let parameters = parameters { |
| request.httpBody = try JSONSerialization.data(withJSONObject: parameters as AnyObject, options: JSONSerialization.WritingOptions()) |
| } |
| |
| let task = urlSession.dataTask(with: request) { |
| data, response, error in |
| |
| let statusCode: Int! |
| if let httpResponse = response as? HTTPURLResponse { |
| statusCode = httpResponse.statusCode |
| } else { |
| statusCode = -1 |
| } |
| |
| if let error = error { |
| print("Error performing network call \(error), status: \(statusCode)") |
| callback(nil, error) |
| return |
| |
| } else { |
| |
| callback(["status":statusCode, "description":"Post call success"], nil) |
| } |
| |
| if let group = group { |
| group.leave() |
| } |
| |
| } |
| |
| task.resume() |
| |
| } |
| } |