├── .gitignore ├── LICENSE ├── README.md └── bootcamp ├── README.md ├── ex0 - setting up development environment ├── README.md └── images │ ├── default_region.png │ ├── docs.gif │ ├── email.png │ └── registration.png ├── ex1 - creating and invoking actions └── README.md ├── ex1.1 - using pre-compiled swift binaries └── README.md ├── ex1.2 - using action sequences └── README.md ├── ex1.3 - bundling NPM modules └── README.md ├── ex2 - managing actions with packages └── README.md ├── ex3 - connecting actions to event sources └── README.md ├── ex4 - exposing APIs from actions ├── README.md └── images │ ├── api-keys.png │ ├── apis.gif │ ├── auth-on.png │ └── rate-limit.png ├── ex5 - ibm cloud functions web ui ├── README.md └── images │ ├── action-editor.png │ ├── action-overview.png │ ├── api-details.png │ ├── apis-homepage.png │ ├── create-apis.gif │ ├── create-trigger.gif │ ├── creating-action.gif │ ├── homepage.gif │ ├── invoking-action.gif │ ├── monitoring.png │ ├── trigger-details.png │ └── triggers-overview.png ├── ex6 - building a weather bot ├── README.md └── images │ ├── incoming.png │ ├── london.png │ ├── outgoing.png │ ├── test_bot.png │ └── weather_bot.png └── ex7 - using the serverless framework ├── README.md └── images └── framework.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apache OpenWhisk & IBM Cloud Functions Workshops 2 | 3 | This respository is the home of different workshop to each you about [Apache OpenWhisk](http://openwhisk.org) and [IBM Cloud Functions](https://www.ibm.com/cloud-computing/bluemix/de/openwhisk). 4 | At the moment the following workshops are available: 5 | 6 | * [Full-blown bootcamp workshop teaching you how to develop serverless applications](https://github.com/IBM-Bluemix/openwhisk-workshops/tree/master/bootcamp) 7 | 8 | More workshops to come... 9 | -------------------------------------------------------------------------------- /bootcamp/README.md: -------------------------------------------------------------------------------- 1 | # OpenWhisk Bootcamp 2 | 3 | Hello 👋. 4 | 5 | 👩‍💻👨‍💻 *Welcome to the [IBM Cloud Functions](https://console.bluemix.net/openwhisk/) ([Apache OpenWhisk](http://openwhisk.incubator.apache.org/)) bootcamp!* 👩‍💻👨‍💻 6 | 7 | This workshop will teach you how to develop **serverless applications**, composed of loosely coupled microservice-like functions, using an open-source serverless platform. 8 | 9 | Starting with getting the development environment set up, it'll move onto creating, deploying and invoking serverless functions for multiple runtimes. Once you are comfortable creating serverless functions, the next step is to connect functions to events, like message queues, allowing microservices to fire automatically. Finally, we'll demonstrate how to expose serverless functions as public API endpoints, allowing to build serverless web applications. 10 | 11 | Welcome to the future of cloud development, you'll never want to manage another server again 😎. 12 | 13 | **You, ready? Let's go!** 🚗 14 | 15 | ## Exercises 16 | 17 | - [**exercise 0 - setting up development environment**](ex0%20-%20setting%20up%20development%20environment/README.md) - *This exercise will set up your local development environment to use IBM Cloud Functions.* 18 | - [**exercise 1 - creating and invoking actions**](ex1%20-%20creating%20and%20invoking%20actions/README.md) - *This exercise will introduce the concepts needed to create and use actions with IBM Cloud Functions.* 19 | - [**exercise 1.1 - using pre-compiled swift binaries**](ex1.1%20-%20using%20pre-compiled%20swift%20binaries) - *This exercise will explain how to improve the performance of Swift actions on IBM Cloud Functions. This additional exercise is for developers using the Swift runtime. If this isn't you, feel free to skip…* 20 | - [**exercise 1.2 - using action sequences**](ex1.2%20-%20using%20action%20sequences) - *This exercise will explain how to how to use sequences to compose new "meta-actions" from existing actions on IBM Cloud Functions.* 21 | - [**exercise 1.3 - bundling NPM modules**](ex1.3%20-%20bundling%20NPM%20modules) - *This exercise will explain how to use external NPM modules from Node.js actions on IBM Cloud Functions.. This additional exercise is for developers using the Node.js runtime. If this isn't you, feel free to skip…* 22 | - **[exercise 2 - managing actions with packages](ex2%20-%20managing%20actions%20with%20packages)** - *This exercise will introduce the concepts needed to create and use packages with IBM Cloud Functions.* 23 | - **[exercise 3 - connecting actions to event sources](ex3%20-%20connecting%20actions%20to%20event%20sources/)** - *This exercise introduces concepts (triggers and rules) used by the platform to integrate external event providers.* 24 | - **[exercise 4 - exposing APIs from actions](ex4%20-%20exposing%20APIs%20from%20actions)** - *This exercise shows you how to create public HTTP endpoints from actions.* 25 | - [**exercise 5 - ibm cloud functions web ui**](ex5%20-%20ibm%20cloud%20functions%20web%20ui) - *This exercise will introduce the [IBM Cloud Functions Web UI](https://console.bluemix.net/openwhisk/).* 26 | - [**exercise 6 - building a weather bot**](ex6%20-%20building%20a%20weather%20bot) - *This exercise shows you how to build a weather bot for Slack.* 27 | - [**exercise 7 - using the serverless framework**](ex7%20-%20using%20the%20serverless%20framework) - *This exercise shows you use IBM Cloud Functions with The Serverless Framework.* 28 | 29 | ## Feedback / Suggestions / Bugs? 😱 30 | 31 | Please [open an issue](https://github.com/IBM-Cloud/openwhisk-workshops/issues) in the Github repository if you have anything to share with us. 32 | -------------------------------------------------------------------------------- /bootcamp/ex0 - setting up development environment/README.md: -------------------------------------------------------------------------------- 1 | # setting up dev environment 2 | 3 | This exercise will set up your local development environment to use IBM Cloud Functions. These steps are a prerequisite before you can start building serverless applications. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Registered an IBM Cloud account.** 8 | - **Installed and configured IBM Cloud CLI tools.** 9 | - **Tested example IBM Cloud Functions action from the command-line.** 10 | 11 | Once this exercise is finished, we can start to develop serverless applications using IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Register IBM Cloud Account](#register-ibm-cloud-account) 16 | * [Check Default Region (Lite Account Users)](#check-default-region-(lite-account-users)) 17 | * [Install IBM Cloud CLI](#install-ibm-cloud-cli) 18 | * [Log Into IBM Cloud CLI](#log-into-ibm-cloud-cli) 19 | * [Install IBM Cloud Functions CLI plugin](#install-ibm-cloud-functions-cli-plugin) 20 | * [Test IBM Cloud Functions From The CLI](#test-ibm-cloud-functions-from-the-cli) 21 | 22 | ## Instructions 23 | 24 | ### Register IBM Cloud Account 25 | 26 | 1. Open a browser window 27 | 28 | 2. Navigate to [https://console.bluemix.net/registration/](https://ibm.biz/BdZf65) 29 | 30 | ![Registration page](images/registration.png) 31 | 32 | 3. Fill in registration form and follow link in the validation email when it arrives. 33 | 34 | ![Registration page](images/email.png) 35 | 36 | 4. [Login into IBM Cloud](https://console.bluemix.net/login) using the account credentials you have registered. 37 | 38 | ### Check Default Region (Lite Account Users) 39 | 40 | 🚨🚨🚨 **PLEASE READ THIS SECTION.** *We know it looks boring but trust us! People often skim this part and then complain they can't login into the CLI. These instructions will save you all that inevitable confusion...* 🚨🚨🚨 41 | 42 | New IBM Cloud accounts default to a [new "lite" account version](https://www.ibm.com/cloud/pricing). 43 | 44 | *This account provides free access to a subset of IBM Cloud resources, including IBM Cloud Functions. Lite accounts do not need a credit-card to sign up or expire after a set time period, i.e. 30 days.* 45 | 46 | Developers using "*Lite accounts*" are restricted to development within a single region. Accounts are automatically assigned to either `eu-gb` or `us-south` regions depending on user profile location. 47 | 48 | **When setting up the IBM Cloud CLI, choose the API endpoint for the default account region.** 49 | 50 | Follow these instructions to check which default region your lite account has been assigned. 51 | 52 | 1. Open the [IBM Cloud homepage](https://console.bluemix.net/). 53 | 2. Click the *"Manage"* menu from the page header. 54 | 3. Click the *"[Account > Cloud Foundry Orgs](https://console.bluemix.net/account/organizations)"* option from the drop-down menu. 55 | 4. From the [Cloud Foundry Organisations](https://console.bluemix.net/account/organizations) page, click the organisation name listed in the table. 56 | 5. Check the "*Region*" value listed in the organisation details table. 57 | 58 | ![Registration page](images/default_region.png) 59 | 60 | *Accounts which have been upgraded to "Pay-As-You-Go" or "Subscription" can choose any available region for IBM Cloud Functions.* 61 | 62 | 🚨🚨🚨 **DID YOU READ THIS SECTION?** *Good, just checking...* 🚨🚨🚨 63 | 64 | ### Install IBM Cloud CLI 65 | 66 | 1. Open the [IBM Cloud Docs](https://console.bluemix.net/docs/) page. 67 | 2. Open the *"[IBM Cloud Developer Tools (CLI)](https://console.bluemix.net/docs/cli/index.html#overview)"* link from the *"IBM Cloud"* section. 68 | 3. Click on the *"[Download and install IBM Cloud CLI](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install)"* link under *"HOW TO"* section in the left-hand menu. 69 | 4. Follow the steps listed under the *["Install from shell"](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#shell_install)* section to download and install the IBM Cloud CLI. 70 | 71 | - MacOS: `curl -fsSL https://clis.ng.bluemix.net/install/osx | sh` 72 | - Linux: `curl -fsSL https://clis.ng.bluemix.net/install/linux | sh` 73 | - Windows (Powershell): `iex(New-Object Net.WebClient).DownloadString('https://clis.ng.bluemix.net/install/powershell')` 74 | 75 | ![Registration page](images/docs.gif) 76 | 77 | 78 | 79 | ### Log Into IBM Cloud CLI 80 | 81 | 1. Use this command to authenticate the IBM Cloud CLI with your account credentials. 82 | 83 | ``` 84 | $ ibmcloud login 85 | ``` 86 | 87 | 2. Choose an API endpoint from the list. 88 | ***IBM Cloud Functions is available in the following regions: `eu-de`, `eu-gb` and `us-south`. Lite account users must choose their default account region.*** 89 | 90 | ``` 91 | Select an API endpoint: 92 | 1. eu-de - https://api.eu-de.bluemix.net 93 | 2. au-syd - https://api.au-syd.bluemix.net 94 | 3. us-east - https://api.us-east.bluemix.net 95 | 4. us-south - https://api.ng.bluemix.net 96 | 5. eu-gb - https://api.eu-gb.bluemix.net 97 | 6. Enter a different API endpoint 98 | Enter a number> 99 | ``` 100 | 101 | 3. Enter account credentials for your IBM Cloud account. 102 | 103 | ``` 104 | Email> user@email.com 105 | 106 | Password> 107 | Authenticating... 108 | OK 109 | 110 | Select an account (or press enter to skip): 111 | 1. John Smith's Account (xxx) 112 | Enter a number> 113 | 114 | API endpoint: https://api.eu-gb.bluemix.net (API version: 2.92.0) 115 | Region: eu-gb 116 | User: user@email.com 117 | Account: No account targeted, use 'bx target -c ACCOUNT_ID' 118 | Resource group: No resource group targeted, use 'bx target -g RESOURCE_GROUP' 119 | Org: 120 | Space: 121 | 122 | ``` 123 | 124 | 4. Run the following command to configure the organisation and space the CLI is targeting. 125 | 126 | ``` 127 | $ ibmcloud target --cf 128 | Targeted org user@email.com 129 | Targeted space dev 130 | 131 | API endpoint: https://api.eu-gb.bluemix.net (API version: 2.92.0) 132 | Region: eu-gb 133 | User: user@email.com 134 | Account: No account targeted, use 'bx target -c ACCOUNT_ID' 135 | Resource group: No resource group targeted, use 'bx target -g RESOURCE_GROUP' 136 | Org: user@email.com 137 | Space: dev 138 | ``` 139 | 140 | ### Install IBM Cloud Functions CLI plugin 141 | 142 | 1. Use this command to install the Cloud Functions plugin for the IBM Cloud CLI. 143 | 144 | ``` 145 | $ ibmcloud plugin install cloud-functions 146 | Looking up 'cloud-functions' from repository 'Bluemix'... 147 | Plug-in 'cloud-functions 1.0.7' found in repository 'Bluemix' 148 | Attempting to download the binary file... 149 | 11.13 MiB / 11.13 MiB [=================================================================================] 100.00% 9s 150 | 11665633 bytes downloaded 151 | Installing binary... 152 | OK 153 | Plug-in 'cloud-functions 1.0.7' was successfully installed into /home/user/.bluemix/plugins/cloud-functions. 154 | ``` 155 | 156 | *This plugin provides the [Apache OpenWhisk CLI](https://github.com/apache/incubator-openwhisk/blob/master/docs/cli.md) as a sub-command under the IBM Cloud CLI. Platform credentials are provided automatically by the IBM Cloud CLI.* 157 | 158 | ### Test IBM Cloud Functions From The CLI 159 | 160 | 1. Run the following command to invoke a test function from the command-line. 161 | 162 | ``` 163 | $ ibmcloud wsk action invoke whisk.system/utils/echo -p message hello --result 164 | { 165 | "message": "hello" 166 | } 167 | ``` 168 | 169 | *If this command executes successfully, you have verified that the IBM Cloud CLI and Cloud Functions plugin have been installed and configured correctly. If this does not work, please contact the workshop organiser to provide assistance!* 170 | 171 | 🎉🎉🎉 **Congratulations, you've successfully registered an IBM Cloud account, configured the IBM Cloud CLI for Cloud Functions development and executed your first serverless function! Let's start using the platform to create our own serverless applications…** 🎉🎉🎉 172 | -------------------------------------------------------------------------------- /bootcamp/ex0 - setting up development environment/images/default_region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex0 - setting up development environment/images/default_region.png -------------------------------------------------------------------------------- /bootcamp/ex0 - setting up development environment/images/docs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex0 - setting up development environment/images/docs.gif -------------------------------------------------------------------------------- /bootcamp/ex0 - setting up development environment/images/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex0 - setting up development environment/images/email.png -------------------------------------------------------------------------------- /bootcamp/ex0 - setting up development environment/images/registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex0 - setting up development environment/images/registration.png -------------------------------------------------------------------------------- /bootcamp/ex1 - creating and invoking actions/README.md: -------------------------------------------------------------------------------- 1 | # creating and invoking actions 2 | 3 | This exercise will introduce the concepts needed to create and use actions with IBM Cloud Functions. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Created and invoked actions.** 8 | - **Understood how to pass parameters to actions.** 9 | - **Created actions which return asynchronous results.** 10 | 11 | Once this exercise is finished, we will be able to create simple serverless functions using IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Creating And Invoking Actions](#creating-and-invoking-actions) 17 | * [Creating Node.js actions](#creating-node.js-actions) 18 | * [Creating Swift actions](#creating-swift-actions) 19 | * [Invoking actions](#invoking-actions) 20 | * [Using Action Parameters](#using-action-parameters) 21 | * [Passing parameters to an action (Node.js)](#passing-parameters-to-an-action-(node.js)) 22 | * [Passing parameters to an action (Swift)](#passing-parameters-to-an-action-(swift)) 23 | * [Setting default parameters](#setting-default-parameters) 24 | * [Retrieving Action Logs](#retrieving-action-logs) 25 | * [Creating Activation Logs](#creating-activation-logs) 26 | * [Accessing Activation Logs](#accessing-activation-logs) 27 | * [Polling Activation Logs](#polling-activation-logs) 28 | * [Calling Other Actions](#calling-other-actions) 29 | * [Proxy Example](#proxy-example) 30 | * [Asynchronous Actions](#asynchronous-actions) 31 | * [Returning asynchronous results (Node.js)](#returning-asynchronous-results-(node.js)) 32 | * [Returning asynchronous results (Swift)](#returning-asynchronous-results-(swift)) 33 | * [Using actions to call an external API](#using-actions-to-call-an-external-api) 34 | * [EXERCISES](#exercises) 35 | 36 | ## Instructions 37 | 38 | ### Background 39 | 40 | Actions are stateless code snippets that run on the OpenWhisk platform. An action can be written as a JavaScript, Swift, PHP, or Python function, a Java method, static binary or a custom executable packaged in a Docker container. For example, an action can be used to detect the faces in an image, respond to a database change, aggregate a set of API calls, or post a Tweet. 41 | 42 | Actions can be explicitly invoked, or run in response to an event. In either case, each run of an action results in an activation record that is identified by a unique activation ID. The input to an action and the result of an action are a dictionary of key-value pairs, where the key is a string and the value a valid JSON value. Actions can also be composed of calls to other actions or a defined sequence of actions. 43 | 44 | ### Creating And Invoking Actions 45 | 46 | #### Creating Node.js actions 47 | 48 | Review the following steps and examples to create your first JavaScript action. 49 | 50 | 1. Create a JavaScript file with the following content. For this example, the file name is 'hello.js'. 51 | 52 | ```javascript 53 | function main() { 54 | return {payload: 'Hello world'}; 55 | } 56 | ``` 57 | 58 | The JavaScript file might contain additional functions. However, by convention, a function called `main` must exist to provide the entry point for the action. 59 | 60 | 2. Create an action from the following JavaScript function. For this example, the action is called 'hello'. 61 | 62 | ``` 63 | $ ibmcloud wsk action create hello hello.js 64 | ok: created action hello 65 | ``` 66 | 67 | 3. List the actions that you have created: 68 | 69 | ``` 70 | $ ibmcloud wsk action list 71 | actions 72 | hello private 73 | ``` 74 | 75 | You can see the `hello` action you just created. 76 | 77 | #### Creating Swift actions 78 | 79 | Review the following steps and examples to create your first Swift action. 80 | 81 | 1. Create a Swift file with the following content. For this example, the file name is 'hello.swift'. 82 | 83 | ```swift 84 | func main(args: [String:Any]) -> [String:Any] { 85 | return [ "payload" : "Hello world" ] 86 | } 87 | ``` 88 | 89 | The Swift file might contain additional functions. However, by convention, a function called `main` must exist to provide the entry point for the action. 90 | 91 | 1. Create an action from the following Swift function. For this example, the action is called 'hello'. 92 | 93 | ``` 94 | $ ibmcloud wsk action create hello hello.swift 95 | ok: created action hello 96 | ``` 97 | 98 | 1. List the actions that you have created: 99 | 100 | ``` 101 | $ ibmcloud wsk action list 102 | actions 103 | /user@host.com_dev/hello private swift:3.1.1 104 | ``` 105 | 106 | You can see the `hello` action you just created. 107 | 108 | #### Invoking Actions 109 | 110 | **After you create your action, you can run it on IBM Cloud Functions with the 'invoke' command.** 111 | 112 | You can invoke actions with a *blocking* invocation (i.e., request/response style) or a *non-blocking* invocation by specifying a flag (`—blocking`) on the command-line. A blocking invocation request will *wait* for the activation result to be available. The wait period is the lesser of 60 seconds or the action's configured [time limit](https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#per-action-timeout-ms-default-60s). The result of the activation is returned if it is available within the wait period. Otherwise, the activation continues processing in the system and an activation ID is returned so that one may check for the result later, as with non-blocking requests (see [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/actions.md#watching-action-output) for tips on monitoring activations). 113 | 114 | 1. Invoke the `hello` action using the command-line as a blocking activation. 115 | 116 | ``` 117 | $ ibmcloud wsk action invoke --blocking hello 118 | ``` 119 | 120 | ``` 121 | ok: invoked hello with id 44794bd6aab74415b4e42a308d880e5b 122 | ``` 123 | 124 | ``` 125 | { 126 | "result": { 127 | "payload": "Hello world" 128 | }, 129 | "status": "success", 130 | "success": true 131 | } 132 | ``` 133 | 134 | The command outputs two important pieces of information: 135 | 136 | - The activation ID (`44794bd6aab74415b4e42a308d880e5b`) 137 | - The invocation result if it is available within the expected wait period 138 | 139 | The result in this case is the string `Hello world` returned by the JavaScript function. The activation ID can be used to retrieve the logs or result of the invocation at a future time. 140 | 141 | If you don't need the action result right away, you can omit the `—blocking` flag to make a non-blocking invocation. You can get the result later by using the activation ID. 142 | 143 | 2. Invoke the `hello` action using the command-line as a non-blocking activation. 144 | 145 | ``` 146 | $ ibmcloud wsk action invoke hello 147 | ok: invoked hello with id 6bf1f670ee614a7eb5af3c9fde813043 148 | ``` 149 | 150 | 3. Retrieve the activation result 151 | 152 | ``` 153 | $ ibmcloud wsk activation result 6bf1f670ee614a7eb5af3c9fde813043 154 | { 155 | "payload": "Hello world" 156 | } 157 | ``` 158 | 159 | To access the most recent activation record, activation results or activation logs, use the `--last` or `-l` flag. 160 | 161 | 4. Run the following command to get your last activation result. 162 | 163 | ``` 164 | $ ibmcloud wsk activation result --last 165 | { 166 | "payload": "Hello world" 167 | } 168 | ``` 169 | 170 | Note that you should not use an activation ID with the flag `--last`. 171 | 172 | 5. If you forget to record the activation ID, you can get a list of activations ordered from the most recent to the oldest. Run the following command to get a list of your activations: 173 | 174 | ``` 175 | $ ibmcloud wsk activation list 176 | activations 177 | 44794bd6aab74415b4e42a308d880e5b hello 178 | 6bf1f670ee614a7eb5af3c9fde813043 hello 179 | ``` 180 | 181 | 🎉🎉🎉 **Great work, you have now learned how to create, deploy and invoke your own serverless functions on IBM Cloud Functions. What about passing data into actions? Let's find out more…** 🎉🎉🎉 182 | 183 | ### Using Action Parameters 184 | 185 | Event parameters can be passed to the action when it is invoked. Let's look at a sample action which uses the parameters to calculate the return values. 186 | 187 | #### Passing parameters to an action (Node.js) 188 | 189 | 1. Update the file `hello.js` with the following following content: 190 | 191 | ``` 192 | function main(params) { 193 | return {payload: 'Hello, ' + params.name + ' from ' + params.place}; 194 | } 195 | ``` 196 | 197 | The input parameters are passed as a JSON object parameter to the `main` function. Notice how the `name` and `place` parameters are retrieved from the `params` object in this example. 198 | 199 | 2. Update the `hello` action with the new source code. 200 | 201 | ``` 202 | $ ibmcloud wsk action update hello hello.js 203 | ``` 204 | 205 | #### Passing parameters to an action (Swift) 206 | 207 | 1. Update the file `hello.swift` with the following following content: 208 | 209 | ```swift 210 | func main(args: [String:Any]) -> [String:Any] { 211 | if let name = args["name"] as? String, let place = args["place"] as? String { 212 | return [ "payload" : "Hello \(name) from \(place)" ] 213 | } else { 214 | return [ "payload" : "Hello stranger from nowhere" ] 215 | } 216 | } 217 | ``` 218 | 219 | The input parameters are passed as a Dictionary (`String:Any`) to the `main` function. Functions must return a Dictionary of the same type with response values. Notice how the `name` and `place` parameters are retrieved from the `args` dictionary in this example. 220 | 221 | 1. Update the `hello` action with the new source code. 222 | 223 | ``` 224 | $ ibmcloud wsk action update hello hello.swift 225 | ``` 226 | 227 | #### Invoking action with parameters 228 | 229 | When invoking actions through the command-line, parameter values can be passed as through explicit command-line parameters `—param/-p` or using an input file containing raw JSON. 230 | 231 | 3. Invoke the `hello` action using explicit command-line parameters. 232 | 233 | ``` 234 | $ ibmcloud wsk action invoke --result hello --param name Bernie --param place Vermont 235 | { 236 | "payload": "Hello, Bernie from Vermont" 237 | } 238 | ``` 239 | 240 | 4. Create a file (`parameters.json`) containing the following JSON. 241 | 242 | ``` 243 | { 244 | "name": "Bernie", 245 | "place": "Vermont" 246 | } 247 | ``` 248 | 249 | 5. Invoke the `hello` action using parameters from a JSON file. 250 | 251 | ``` 252 | $ ibmcloud wsk action invoke --result hello --param-file parameters.json 253 | { 254 | "payload": "Hello, Bernie from Vermont" 255 | } 256 | ``` 257 | 258 | *Notice the use of the `--result` option: it implies a blocking invocation where the CLI waits for the activation to complete and then displays only the result. For convenience, this option may be used without `--blocking` which is automatically inferred.* 259 | 260 | ##### nested parameters 261 | 262 | Parameter values can be any valid JSON value, including nested objects. Let's update our action to use child properties of the event parameters. 263 | 264 | 6. Create the `hello-person` action with the following source code. 265 | 266 | ##### Node.js 267 | 268 | ```javascript 269 | function main(params) { 270 | return {payload: 'Hello, ' + params.person.name + ' from ' + params.person.place}; 271 | } 272 | ``` 273 | 274 | ##### Swift 275 | 276 | ```swift 277 | func main(args: [String:Any]) -> [String:Any] { 278 | if let person = args["person"] as? [String: String], let name = person["name"] as? String, let place = person["place"] as? String { 279 | return [ "payload" : "Hello \(name) from \(place)" ] 280 | } else { 281 | return [ "payload" : "Hello stranger from nowhere" ] 282 | } 283 | } 284 | ``` 285 | 286 | Now the action expects a single `person` parameter to have fields `name` and `place`. 287 | 288 | 7. Invoke the action with a single `person` parameter that is valid JSON. 289 | 290 | ``` 291 | $ ibmcloud wsk action invoke --result hello-person -p person '{"name": "Bernie", "place": "Vermont"}' 292 | ``` 293 | 294 | The result is the same because the CLI automatically parses the `person` parameter value into the structured object that the action now expects: 295 | 296 | ``` 297 | { 298 | "payload": "Hello, Bernie from Vermont" 299 | } 300 | ``` 301 | 302 | 🎉🎉🎉 **That was pretty easy, huh? We can now pass parameters and access these values in our serverless functions. What about parameters that we need but don't want to manually pass in every time? Guess what, we have a trick for that…** 🎉🎉🎉 303 | 304 | #### Setting default parameters 305 | 306 | Actions can be invoked with multiple named parameters. Recall that the `hello` action from the previous example expects two parameters: the *name* of a person, and the *place* where they're from. 307 | 308 | Rather than pass all the parameters to an action every time, you can bind default parameters. Default parameters are stored in the platform and automatically passed in during each invocation. If the invocation includes the same event parameter, this will overwrite the default parameter value. 309 | 310 | Let's use the `hello` action from our previous example and bind a default value for the `place` parameter. 311 | 312 | 1. Update the action by using the `—param` option to bind default parameter values. 313 | 314 | ``` 315 | $ ibmcloud wsk action update hello --param place Vermont 316 | ``` 317 | 318 | Passing parameters from a file requires the creation of a file containing the desired content in JSON format. The filename must then be passed to the `-param-file` flag: 319 | 320 | Example parameter file called parameters.json: 321 | 322 | ```json 323 | { 324 | "place": "Vermont" 325 | } 326 | ``` 327 | 328 | ``` 329 | $ ibmcloud wsk action update hello --param-file parameters.json 330 | ``` 331 | 332 | 2. Invoke the action, passing only the `name` parameter this time. 333 | 334 | ``` 335 | $ ibmcloud wsk action invoke --result hello --param name Bernie 336 | ``` 337 | 338 | ``` 339 | { 340 | "payload": "Hello, Bernie from Vermont" 341 | } 342 | ``` 343 | 344 | Notice that you did not need to specify the place parameter when you invoked the action. Bound parameters can still be overwritten by specifying the parameter value at invocation time. 345 | 346 | 3. Invoke the action, passing both `name` and `place` values. The latter overwrites the value that is bound to the action. 347 | 348 | ``` 349 | $ ibmcloud wsk action invoke --result hello --param name Bernie --param place "Washington, DC" 350 | { 351 | "payload": "Hello, Bernie from Washington, DC" 352 | } 353 | ``` 354 | 355 | 🎉🎉🎉 **Default parameters are awesome for handling parameters like authentication keys for APIs. Letting the platform pass them in automatically means you don't have include these keys in invocation requests or include them in the action source code. Neat, huh?** 🎉🎉🎉 356 | 357 | ### Retrieving Action Logs 358 | 359 | Application logs are essential to debugging production issues. In IBM Cloud Functions, all output written by to `stdout` and `stderr` by actions is available in the activation records. 360 | 361 | #### Creating Activation Logs 362 | 363 | 1. Create a new action (`logs`) from the following source files. 364 | 365 | ##### Node.js 366 | 367 | ```javascript 368 | function main(params) { 369 | console.log("function called with params", params) 370 | console.error("this is an error message") 371 | return { result: true } 372 | } 373 | ``` 374 | 375 | ``` 376 | $ ibmcloud wsk action create logs logs.js 377 | ok: created action logs 378 | ``` 379 | 380 | ##### Swift 381 | 382 | ````javascript 383 | import Foundation 384 | 385 | var standardError = FileHandle.standardError 386 | 387 | extension FileHandle : TextOutputStream { 388 | public func write(_ string: String) { 389 | guard let data = string.data(using: .utf8) else { return } 390 | self.write(data) 391 | } 392 | } 393 | 394 | func main(args: [String:Any]) -> [String:Any] { 395 | print("function called with params", args) 396 | print("this is an error message",to:&standardError) 397 | return ["result": true] 398 | } 399 | ```` 400 | 401 | ``` 402 | $ ibmcloud wsk action create logs logs.swift 403 | ok: created action logs 404 | ``` 405 | 406 | 2. Invoke the `logs` action to generate some logs. 407 | 408 | ``` 409 | $ ibmcloud wsk action invoke -r logs -p hello world 410 | { 411 | "result": true 412 | } 413 | ``` 414 | 415 | #### Accessing Activation Logs 416 | 417 | 1. Retrieve activation record to verify logs have been recorded. 418 | 419 | ``` 420 | $ ibmcloud wsk activation get --last 421 | ok: got activation 9fc044881705479580448817053795bd 422 | { 423 | ... 424 | "logs": [ 425 | "2018-03-02T09:49:03.021Z stdout: function called with params { hello: 'world' }", 426 | "2018-03-02T09:49:03.021Z stderr: this is an error message" 427 | ], 428 | ... 429 | } 430 | ``` 431 | 432 | 3. Logs can also be retrieved without showing the whole activation record, using the `activation logs` command. 433 | 434 | ``` 435 | $ ibmcloud wsk activation logs --last 436 | 2018-03-02T09:49:03.021404683Z stdout: function called with params { hello: 'world' } 437 | 2018-03-02T09:49:03.021816473Z stderr: this is an error message 438 | ``` 439 | 440 | #### Polling Activation Logs 441 | 442 | Activation logs can be monitored in real-time, rather than manually retrieving individual activation records. 443 | 444 | 1. Run the following command to monitor logs from the `logs` actions. 445 | 446 | ``` 447 | $ ibmcloud wsk activation poll 448 | Enter Ctrl-c to exit. 449 | Polling for activation logs 450 | ``` 451 | 452 | 2. In another terminal, run the following command multiple times. 453 | 454 | ``` 455 | $ ibmcloud wsk action invoke logs -p hello world 456 | ok: invoked /_/logs with id 0e8d715393504f628d715393503f6227 457 | ``` 458 | 459 | 3. Check the output from the `poll` command to see the activation logs. 460 | 461 | ``` 462 | 463 | Activation: 'logs' (ae57d06630554ccb97d06630555ccb8b) 464 | [ 465 | "2018-03-02T09:56:17.8322445Z stdout: function called with params { hello: 'world' }", 466 | "2018-03-02T09:56:17.8324766Z stderr: this is an error message" 467 | ] 468 | 469 | Activation: 'logs' (0e8d715393504f628d715393503f6227) 470 | [ 471 | "2018-03-02T09:56:20.8992704Z stdout: function called with params { hello: 'world' }", 472 | "2018-03-02T09:56:20.8993178Z stderr: this is an error message" 473 | ] 474 | 475 | Activation: 'logs' (becbb9b0c37f45f98bb9b0c37fc5f9fc) 476 | [ 477 | "2018-03-02T09:56:44.6961581Z stderr: this is an error message", 478 | "2018-03-02T09:56:44.6964147Z stdout: function called with params { hello: 'world' }" 479 | ] 480 | ``` 481 | 482 | ### Calling Other Actions 483 | 484 | Using serverless platforms to implement reusable functions means you will often want to invoke one action from another. IBM Cloud Functions provides a [RESTful API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/openwhisk/openwhisk/master/core/controller/src/main/resources/apiv1swagger.json) to invoke actions programmatically. 485 | 486 | Rather than having to [manually construct the HTTP requests](https://github.com/apache/incubator-openwhisk/blob/master/docs/rest_api.md#actions) to invoke actions from within the IBM Cloud Functions runtime, client libraries are pre-installed to make this easier. 487 | 488 | These libraries make it simple to invoke other actions, fire triggers and access all other platform services. 489 | 490 | #### Proxy Example 491 | 492 | Let's look an example of creating a "proxy" action which invokes another action if a "password" is present in the input parameters. 493 | 494 | 1. Create the following new action `proxy` from the following source files. 495 | 496 | ##### Node.js 497 | 498 | ```javascript 499 | var openwhisk = require('openwhisk'); 500 | 501 | function main(params) { 502 | if (params.password !== 'secret') { 503 | throw new Error("Password incorrect!") 504 | } 505 | 506 | var ow = openwhisk(); 507 | return ow.actions.invoke({name: "hello", blocking: true, result: true, params: params}) 508 | } 509 | ``` 510 | 511 | *The JavaScript library for Apache OpenWhisk is here: [https://github.com/apache/incubator-openwhisk-client-js/](https://github.com/apache/incubator-openwhisk-client-js/).* *This library is pre-installed in the IBM Cloud Functions runtime and does not need to be manually included.* 512 | 513 | ``` 514 | $ ibmcloud wsk action create proxy proxy.js 515 | ``` 516 | 517 | ##### Swift 518 | 519 | ```swift 520 | func main(args: [String:Any]) -> [String:Any] { 521 | guard let password = args["password"] as? String, password == "secret" else { 522 | return ["error": "Password is incorrect!"] 523 | } 524 | 525 | let resp = Whisk.invoke(actionNamed: "hello", withParameters: args, blocking: true) 526 | 527 | guard let response = resp["response"] as? [String: Any], let result = response["result"] as? [String: Any] else { 528 | return ["error": "Unable to parse response result."] 529 | } 530 | 531 | return result 532 | } 533 | ``` 534 | 535 | This Swift file is included in the runtime to provide a small utility for invoking platform APIs: [https://github.com/apache/incubator-openwhisk-runtime-swift/blob/master/core/swift3.1.1Action/spm-build/_Whisk.swift](https://github.com/apache/incubator-openwhisk-runtime-swift/blob/master/core/swift3.1.1Action/spm-build/_Whisk.swift) 536 | 537 | ``` 538 | $ ibmcloud wsk action create proxy proxy.swift 539 | ``` 540 | 541 | 2. Invoke the proxy with an incorrect password. 542 | 543 | ``` 544 | $ ibmcloud wsk action invoke proxy -p password wrong -r 545 | { 546 | "error": "Password is incorrect!" 547 | } 548 | ``` 549 | 550 | 3. Invoke the proxy with the correct password. 551 | 552 | ``` 553 | $ ibmcloud wsk action invoke proxy -p password secret -p name Bernie -p place Vermont -r 554 | { 555 | "greeting": "Hello Bernie from Vermont" 556 | } 557 | ``` 558 | 559 | 4. Review the activations list to show both actions were invoked. 560 | 561 | ``` 562 | $ ibmcloud wsk activation list -l 2 563 | activations 564 | 8387302c81dc4d2d87302c81dc4d2dc6 hello 565 | e0c603c242c646978603c242c6c6977f proxy 566 | ``` 567 | 568 | *These libraries can also be used to invoke triggers to fire events from actions.* 569 | 570 | ### Asynchronous actions 571 | 572 | #### Returning asynchronous results (Node.js) 573 | 574 | JavaScript functions that run asynchronously may need to return the activation result after the `main` function has returned. You can accomplish this by returning a Promise in your action. 575 | 576 | 1. Save the following content in a file called `asyncAction.js`. 577 | 578 | ``` 579 | function main(args) { 580 | return new Promise(function(resolve, reject) { 581 | setTimeout(function() { 582 | resolve({ done: true }); 583 | }, 2000); 584 | }) 585 | } 586 | ``` 587 | 588 | Notice that the `main` function returns a Promise, which indicates that the activation hasn't completed yet, but is expected to in the future. 589 | 590 | The `setTimeout()` JavaScript function in this case waits for two seconds before calling the callback function. This represents the asynchronous code and goes inside the Promise's callback function. 591 | 592 | The Promise's callback takes two arguments, resolve and reject, which are both functions. The call to `resolve()`fulfills the Promise and indicates that the activation has completed normally. 593 | 594 | A call to `reject()` can be used to reject the Promise and signal that the activation has completed abnormally. 595 | 596 | #### Returning asynchronous results (Swift) 597 | 598 | Swift functions that use asynchronous processing must block returning until those asynchronous results are available. Here's an example which prints a message after an interval to demonstrate this approach. 599 | 600 | 1. Save the following content in a file called `asyncAction.swift`. 601 | 602 | ```swift 603 | import Foundation 604 | import CoreFoundation 605 | 606 | func main(args: [String:Any]) -> [String:Any] { 607 | Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in 608 | print("Timer fired.") 609 | CFRunLoopStop(CFRunLoopGetMain()) 610 | } 611 | print("Waiting on timer...") 612 | CFRunLoopRun() 613 | return [ "done" : true ] 614 | } 615 | ``` 616 | 617 | Using the `CFRunLoopRun` function, we can wait on the asynchronous timer to fire. Once the timer fires, the run loop is stopped. 618 | 619 | Other approaches to blocking on asynchronous results in Swift include semaphores, dispatch barriers and other concurrency primitives in the language. 620 | 621 | #### Testing asynchronous timeouts 622 | 623 | 1. Run the following commands to create the action and invoke it: 624 | 625 | ``` 626 | $ ibmcloud wsk action create asyncAction asyncAction.js 627 | // OR.... 628 | $ ibmcloud wsk action create asyncAction asyncAction.swift 629 | ``` 630 | 631 | ``` 632 | $ ibmcloud wsk action invoke --result asyncAction 633 | { 634 | "done": true 635 | } 636 | ``` 637 | 638 | Notice that you performed a blocking invocation of an asynchronous action. 639 | 640 | 2. Fetch the last activation log to see how long the async activation took to complete: 641 | 642 | ``` 643 | $ ibmcloud wsk activation get --last 644 | { 645 | "duration": 2026, 646 | ... 647 | } 648 | ``` 649 | 650 | Checking the `duration` field in the activation record, you can see that this activation took slightly over two seconds to complete. 651 | 652 | **Actions have a `timeout` parameter that enforces the maximum duration for an invocation.** This value defaults to 60 seconds and can be changed to a maximum of 5 minutes. 653 | 654 | Let's look at what happens when an action invocation takes longer than the `timeout`. 655 | 656 | 1. Update the `asyncAction` timeout to 1000ms. 657 | 658 | ``` 659 | $ ibmcloud wsk action update asyncAction --timeout 1000 660 | ok: updated action asyncAction 661 | ``` 662 | 663 | 2. Invoke the action and block on the result. 664 | 665 | ``` 666 | $ ibmcloud wsk action invoke asyncAction --result 667 | { 668 | "error": "The action exceeded its time limits of 1000 milliseconds." 669 | } 670 | ``` 671 | 672 | The error message returned by the platform indicates the action didn't return a response within the user-specified timeout. If we change the `timeout` back to a value higher than the artificial delay in the function, it should work again. 673 | 674 | 1. Update the `asyncAction` timeout to 10000ms. 675 | 676 | ``` 677 | $ ibmcloud wsk action update asyncAction --timeout 10000 678 | ok: updated action asyncAction 679 | ``` 680 | 681 | 2. Invoke the action and block on the result. 682 | 683 | ``` 684 | $ ibmcloud wsk action invoke asyncAction --result 685 | { 686 | "done": true 687 | } 688 | ``` 689 | 690 | 🎉🎉🎉 **Asynchronous actions are necessary for calling other APIs or cloud services. Don't forget about that timeout though! Let's have a look at using an asynchronous action to invoke another API…** 🎉🎉🎉 691 | 692 | ### EXERCISES 693 | 694 | Let's try out your new SERVERLESS SUPERPOWERS 💪 to build a real serverless function. 695 | 696 | ***Can you create a new action which takes a price and currency and returns a message contains the equivalent amount of Bitcoin?*** 697 | 698 | #### Input 699 | 700 | The action should take two parameters, `amount` and `currency`. `amount` is a number, `currency` is a three letter [currency code](https://www.iban.com/currency-codes.html). 701 | 702 | #### Output 703 | 704 | Return the following JSON template with the input parameters and bitcoin amounts. 705 | 706 | ```json 707 | { 708 | "amount": 1.5, 709 | "message": "10000 USD is worth 1.5 bitcoins." 710 | } 711 | ``` 712 | 713 | If either the `amount` or `currency` parameters are missing, return an error with details. 714 | 715 | #### Resources 716 | 717 | This [Coindesk API](https://api.coindesk.com/v1/bpi/currentprice.json) returns real-time bitcoin prices. This includes rates for the `USD`, `GBP` and `EUR` currencies. 718 | 719 | This [Currency Converter API](https://free.currencyconverterapi.com/) returns exchanges rates between traditional currencies. 720 | 721 | Use the `request-promise` [module](https://www.npmjs.com/package/request-promise) to make external API requests. It comes pre-installed in the runtime. 722 | 723 | #### Tests 724 | 725 | Test with currencies in the Coindeck API response. 726 | 727 | ``` 728 | $ ibmcloud wsk action invoke bitcoin -r -p amount 1000 -p currency USD 729 | { 730 | "amount": "0.160814", 731 | "label": "1000 USD is worth 0.160814 bitcoins." 732 | } 733 | $ ibmcloud wsk action invoke bitcoin -r -p amount 1000 -p currency EUR 734 | { 735 | "amount": "0.187235", 736 | "label": "1000 EUR is worth 0.187235 bitcoins." 737 | } 738 | $ ibmcloud wsk action invoke bitcoin -r -p amount 1000 -p currency GBP 739 | { 740 | "amount": "0.213012", 741 | "label": "1000 GBP is worth 0.213012 bitcoins." 742 | } 743 | ``` 744 | 745 | Test with currencies not in the Coindeck API response. 746 | 747 | ``` 748 | $ ibmcloud wsk action invoke bitcoin -r -p amount 1000 -p currency AUD 749 | { 750 | "amount": "0.10814", 751 | "label": "1000 AUD is worth 0.10814 bitcoins." 752 | } 753 | ``` 754 | 755 | Test with missing parameters. 756 | 757 | ``` 758 | $ ibmcloud wsk action invoke bitcoin -r -p amount 1000 759 | { 760 | "error": "Missing mandatory argument: currency" 761 | } 762 | $ ibmcloud wsk action invoke bitcoin -r -p currency GBP 763 | { 764 | "error": "Missing mandatory argument: amount" 765 | } 766 | ``` 767 | -------------------------------------------------------------------------------- /bootcamp/ex1.1 - using pre-compiled swift binaries/README.md: -------------------------------------------------------------------------------- 1 | # using pre-compiled swift binaries 2 | 3 | *This additional exercise is for developers using the Swift runtime. If this isn't you, feel free to skip…* 4 | 5 | This exercise will explain how to improve the performance of Swift actions on IBM Cloud Functions. 6 | 7 | *Once you have completed this exercise, you will have…* 8 | 9 | - **Understood how cold activations affect performance.** 10 | - **Created and deployed Swift binaries for the IBM Cloud Functions runtime.** 11 | - **Checked the impact of performance of binary actions.** 12 | 13 | Once this exercise is finished, we will be able to deploy Swift binaries to IBM Cloud Functions! 14 | 15 | ## Table Of Contents 16 | 17 | * [Background](#background) 18 | * [Cold Versus Warm Activations](#cold-versus-warm-activations) 19 | * [Deploying Swift Binaries](#deploying-swift-binaries) 20 | * [Compiling Swift Binaries](#compiling-swift-binaries) 21 | * [Creating Binary Deployments](#creating-binary-deployments) 22 | * [Checking Performance](#checking-performance) 23 | 24 | ## Instructions 25 | 26 | ### Background 27 | 28 | Swift is a static language, which uses a compiler to produce binaries to execute application code. When creating actions from Swift source files, the compilation phase happens during the first action execution. This compilation step introduces a delay for action invocation times, whilst the build process is completed. 29 | 30 | *Once the binary has been produced, it is cached for future invocations to improve performance.* 31 | 32 | Activations will trigger the compilation stage are known as "cold activations", whereas those which re-use the cached binary are known as "warm activations". 33 | 34 | If response time performance and predictability is a critical requirement for your application, you can significantly reduce the "cold activation" delay by removing the need for the platform to run the Swift build process…. 35 | 36 | Let's have a look at how this works! 37 | 38 | ### Cold Versus Warm Activations 39 | 40 | Creating a new Swift action and invoking multiples times will demonstrate the difference between "cold" and "warm" activations. 41 | 42 | 1. Creating a new action (`delay`) with the following source code. 43 | 44 | ```swift 45 | func main(args: [String:Any]) -> [String:Any] { 46 | return [ "greeting" : "Hello from Swift!" ] 47 | } 48 | ``` 49 | 50 | ``` 51 | $ bx wsk action create delay delay.swift 52 | ``` 53 | 54 | 2. Invoke the action for the first time, known as a "cold activation". 55 | 56 | ``` 57 | $ bx wsk action invoke delay -b 58 | ok: invoked /_/delay with id 2924f189a71a449ea4f189a71a149eee 59 | { 60 | .. 61 | "annotations": [ 62 | { 63 | "key": "initTime", 64 | "value": 2959 65 | } 66 | ... 67 | ], 68 | "duration": 2976, 69 | ... 70 | } 71 | ``` 72 | 73 | Cold activations record an annotation in the activation record (`initTime`) with the milliseconds needed to initialise the action. In the Swift runtime, this time includes the compilation process. 74 | 75 | Looking at the `duration` field, the invocation took 2976 milliseconds, with the `initTime` comprising almost all of that time. 76 | 77 | 3. Invoke the action again within five minutes. 78 | 79 | ``` 80 | ok: invoked /_/delay with id 42cf8636712c4fa58f8636712cdfa5f0 81 | { 82 | "duration": 17, 83 | ... 84 | } 85 | ``` 86 | 87 | This activation record does not contain a `initTime` duration, which indicates a "warm activation", where the binary from the previous build process is re-executed. This also improves the duration, reducing it down to just 17 milliseconds. 88 | 89 | *Runtime environments are kept in the cache for around five minutes. Environments do not process parallel requests, meaning if all the cached runtimes are in use, a "cold activation" will be triggered.* 90 | 91 | ### Deploying Swift Binaries 92 | 93 | If we run the Swift build process locally, we can deploy the binary rather than the source file. The build process will be skipped, removing almost all of the cold start delay. 94 | 95 | Making Swift binaries compatible with the IBM Cloud Functions needs you to compile for the correct platform architecture. There is also a shim file which handles invoking the function from event parameters which needs add to your code. 96 | 97 | Docker can be used to run the Swift build process inside the IBM Cloud Functions Swift runtime. This ensures the binary is generated for the correct platform architecture. 98 | 99 | There is also a Swift package which wraps the shim file into a normal library. This makes it simple to add this functionality to your code and register action handlers. 100 | 101 | Let's show you how to do this with the sample action we created above… 102 | 103 | #### Compiling Swift Binaries 104 | 105 | 1. Create a new Swift package that generates an executable. 106 | 107 | ``` 108 | $ mkdir Action && cd Action 109 | $ swift package init --type executable 110 | Creating executable package: Action 111 | Creating Package.swift 112 | Creating README.md 113 | Creating .gitignore 114 | Creating Sources/ 115 | Creating Sources/Action/main.swift 116 | Creating Tests/ 117 | ``` 118 | 119 | 2. Replace the package manifest with this source code (`Package.swift`). 120 | 121 | ```swift 122 | import PackageDescription 123 | 124 | let package = Package( 125 | name: "Action", 126 | dependencies: [ 127 | .Package(url: "https://github.com/jthomas/OpenWhiskAction.git", majorVersion: 0) 128 | ] 129 | ) 130 | ``` 131 | 132 | 3. Update the `main.swift` file under the `Sources/Action` directory with the following source code. 133 | 134 | ```swift 135 | import OpenWhiskAction 136 | 137 | func main(args: [String:Any]) -> [String:Any] { 138 | return [ "greeting" : "Hello from Swift!" ] 139 | } 140 | 141 | OpenWhiskAction(main: main) 142 | ``` 143 | 144 | OpenWhisk Swift actions use a [custom Docker image](https://hub.docker.com/r/openwhisk/action-swift-v3.1.1/) as the [runtime environment](https://github.com/apache/incubator-openwhisk-runtime-swift). 145 | 146 | This command will run the `swift build` command within a container from this image. The host filesystem is mounted into the container at `/swift-package`. Binaries and other build artifacts will be available in `./.build/release/` after the command has executed. 147 | 148 | 4. Build the Swift binary using the Docker runtime. 149 | 150 | ``` 151 | $ docker run --rm -it -v $(pwd):/swift-package openwhisk/action-swift-v3.1.1 bash -e -c "cd /swift-package && swift build -v -c release" 152 | ``` 153 | 154 | 5. Check the file type for the binary that has been produced. 155 | 156 | ``` 157 | $ file .build/release/Action 158 | .build/release/Action: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=18bfb5c83e819840a01fc1cc9f92de05e72320c3, not stripped 159 | ``` 160 | 161 | #### Creating Binary Deployments 162 | 163 | OpenWhisk actions can be created from a zip file containing action artifacts. The zip file will be expanded prior to execution. In the Swift environment, the Swift binary executed by the platform should be at `./.build/release/Action`. 164 | 165 | If an action is deployed from a zip file which contains this file, the runtime will execute this binary rather than compiling a new binary from source code within the zip file. 166 | 167 | 1. Add the binary into a new zip archive. 168 | 169 | ``` 170 | $ zip action.zip .build/release/Action 171 | adding: .build/release/Action (deflated 67%) 172 | ``` 173 | 174 | 2. Create an action from this archive file. 175 | 176 | ``` 177 | $ bx wsk action create warm-action --kind swift:default action.zip 178 | ``` 179 | 180 | #### Checking Performance 181 | 182 | 1. Invoke the new action for the first time. 183 | 184 | ``` 185 | $ bx wsk action invoke warm-action -b 186 | ok: invoked /_/warm-action with id cdb3dbcd4d014f19b3dbcd4d01af19b4 187 | { 188 | "annotations": [ 189 | { 190 | "key": "initTime", 191 | "value": 317 192 | } 193 | ], 194 | "duration": 494, 195 | ... 196 | } 197 | ``` 198 | 199 | The existence of the `initTime` annotation confirms this was a "cold activation". However, the `initTime` is dramatically reduced from over two seconds to around three hundred milliseconds. 200 | 201 | *Binary deployments also mean we can utilise additional external libraries through the Swift package manger. Libraries must support the Linux Swift runtime to be compatible.* 202 | 203 | 🎉🎉🎉 **Binary deployments are a fantastic way to improve the performance of all statically compiled languages. This approach should be used in production applications where performance is important…** 🎉🎉🎉 204 | -------------------------------------------------------------------------------- /bootcamp/ex1.2 - using action sequences/README.md: -------------------------------------------------------------------------------- 1 | # using action sequences 2 | 3 | This exercise will explain how to use sequences to compose new "meta-actions" from existing actions on IBM Cloud Functions. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Understood what action sequences do and how they work.** 8 | - **Created, deployed and invoked a sample action sequence.** 9 | - **Used deliberate errors to stop processing the action sequence.** 10 | 11 | Once this exercise is finished, we will be able to use action sequences on IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Action Sequences](#action-sequences) 17 | * [Creating Sequence Actions](#creating-sequence-actions) 18 | * [Handling Errors](#handling-errors) 19 | * [EXERCISES](#exercises) 20 | 21 | ## Instructions 22 | 23 | ### Background 24 | 25 | Developers often want to build actions as reusable components which can be combined to build "higher-order" serverless functions. 26 | 27 | For example, what if you have serverless functions to implement an external API and want to enforce HTTP authentication? Rather than manually replicating the same authentication code across all action handlers, you can build an authentication action which can be invoked programmatically at runtime. 28 | 29 | Unfortunately, this approach does incur additional charges. The application will be charged twice when the authentication function is called, as the calling action has to sit idle waiting for the response from the authentication function. 30 | 31 | *OpenWhisk has a special type of action which resolves this problem...* 32 | 33 | ### Action Sequences 34 | 35 | OpenWhisk supports a kind of action called a "sequence". Sequence actions are created using a list of existing actions. When the sequence action is invoked, each action in executed in order of the action parameter list. Input parameters are passed to the first action in the sequence. Output from each function in the sequence is passed as the input to the next function and so on. The output from the last action in the sequence is returned as the response result. 36 | 37 | Here's an example of defining a sequence (`my_sequence`) which will invoke three actions (`a, b, c`). 38 | 39 | ``` 40 | $ ibmcloud wsk action create my_sequence --sequence a,b,c 41 | ``` 42 | 43 | *Sequences behave like normal actions, you create, invoke and manage them as normal through the CLI.* 44 | 45 | Using a sequence can remove the need to manually invoke actions and sit idle waiting for a response. In the example above, a sequence would be created for each serverless function in the application, combing the action doing the authentication followed by the actual request processing action. 46 | 47 | Let's look at an example of using sequences. 48 | 49 | #### Node.js 50 | 51 | 1. Create the file (`funcs.js`) with the following contents: 52 | 53 | ```javascript 54 | function split(params) { 55 | var text = params.text || "" 56 | var words = text.split(' ') 57 | return { words: words } 58 | } 59 | 60 | function reverse(params) { 61 | var words = params.words || [] 62 | var reversed = words.map(word => word.split("").reverse().join("")) 63 | return { words: reversed } 64 | } 65 | 66 | function join(params) { 67 | var words = params.words || [] 68 | var text = words.join(' ') 69 | return { text: text } 70 | } 71 | ``` 72 | 73 | 1. Create the following three actions 74 | 75 | ``` 76 | $ ibmcloud wsk action create split funcs.js --main split 77 | $ ibmcloud wsk action create reverse funcs.js --main reverse 78 | $ ibmcloud wsk action create join funcs.js --main join 79 | ``` 80 | 81 | #### Swift 82 | 83 | 1. Create the file (`funcs.swift`) with the following contents: 84 | 85 | ```swift 86 | import Foundation 87 | 88 | func split(args: [String:Any]) -> [String:Any] { 89 | var words = [String]() 90 | 91 | if let text = args["text"] as? String { 92 | words = text.components(separatedBy: " ") 93 | } 94 | return ["words": words] 95 | } 96 | 97 | func reverse(args: [String:Any]) -> [String:Any] { 98 | guard let words = args["words"] as? [String] else { 99 | return ["words": []] 100 | } 101 | 102 | let reversed = words.map { String($0.characters.reversed()) } 103 | 104 | return ["words": reversed] 105 | } 106 | 107 | func join(args: [String:Any]) -> [String:Any] { 108 | guard let words = args["words"] as? [String] else { 109 | return ["words": ""] 110 | } 111 | 112 | return ["words": words.joined(separator: " ")] 113 | } 114 | ``` 115 | 116 | 1. Create the following three actions 117 | 118 | ``` 119 | $ bx wsk action create split funcs.swift --main split 120 | $ bx wsk action create reverse funcs.swift --main reverse 121 | $ bx wsk action create join funcs.swift --main join 122 | ``` 123 | 124 | #### Creating Sequence Actions 125 | 126 | 1. Test each action to verify it is working 127 | 128 | ``` 129 | $ ibmcloud wsk action invoke split --result --param text "Hello world" 130 | { 131 | "words": [ 132 | "Hello", 133 | "world" 134 | ] 135 | } 136 | $ ibmcloud wsk action invoke reverse --result --param words '["hello", "world"]' 137 | { 138 | "words": [ 139 | "olleh", 140 | "dlrow" 141 | ] 142 | } 143 | $ ibmcloud wsk action invoke join --result --param words '["hello", "world"]' 144 | { 145 | "text": "hello world" 146 | } 147 | ``` 148 | 149 | 2. Create the following action sequence. 150 | 151 | ``` 152 | $ ibmcloud wsk action create reverse_words --sequence split,reverse,join 153 | ``` 154 | 155 | 3. Test out the action sequence. 156 | 157 | ``` 158 | $ ibmcloud wsk action invoke reverse_words --result --param text "hello world" 159 | { 160 | "text": "olleh dlrow" 161 | } 162 | ``` 163 | 164 | Using sequences is a great way to develop re-usable action components that can be joined together into "high-order" actions to create serverless applications. 165 | 166 | #### Handling Errors 167 | 168 | What if you want to stop processing functions in a sequence? This might be due to an application error or because the pre-conditions to continue processing have not been met. In the authentication example above, we only want to proceed if the authentication check passes. 169 | 170 | If any action within the sequences returns an error, the platform returns immediately. The action error is returned as the response. No further actions within the sequence will be invoked. 171 | 172 | Let's look at how this work... 173 | 174 | ##### Node.js 175 | 176 | 1. Create the file (`funcs.js`) with the following contents: 177 | 178 | ```javascript 179 | function fail (params) { 180 | if (params.fail) { 181 | throw new Error("stopping sequence and returning.") 182 | } 183 | 184 | return params 185 | } 186 | 187 | function end (params) { 188 | return { message: "sequence finished." } 189 | } 190 | ``` 191 | 192 | 2. Create the following three actions 193 | 194 | ``` 195 | $ ibmcloud wsk action create fail funcs.js --main fail 196 | $ ibmcloud wsk action create end funcs.js --main end 197 | $ ibmcloud wsk action create example --sequence fail,end 198 | ``` 199 | 200 | 3. Test out the action sequence without `fail` parameter. 201 | 202 | ``` 203 | $ ibmcloud wsk action invoke example -r 204 | { 205 | "message": "sequence finished." 206 | } 207 | ``` 208 | 209 | 4. Test out the action sequence with `fail` parameter. 210 | 211 | ``` 212 | $ ibmcloud wsk action invoke example -r -p fail true 213 | { 214 | "error": "An error has occurred: Error: stopping sequence and returning." 215 | } 216 | ``` 217 | 218 | 219 | 220 | 🎉🎉🎉 **Sequences are an "advanced" OpenWhisk technique. Congratulations for getting this far! Now let's move on to something all together different, connecting functions to external event sources…** 🎉🎉🎉 221 | 222 | ### EXERCISES 223 | 224 | Let's try out your new SERVERLESS SUPERPOWERS 💪 to build a real serverless function. 225 | 226 | ***Can you use sequence error handling to add authentication to the existing `reverse_words` serverless function?*** 227 | 228 | #### Input 229 | 230 | The new `reverse_words_with_password` sequence action should take two parameters, `password` and `text`. `password` is authentication value, `text` is the sentence to reverse words in. 231 | 232 | #### Output 233 | 234 | Return the JSON from the existing `reverse_words` action. 235 | 236 | ```json 237 | { 238 | "text": "olleh dlrow" 239 | } 240 | ``` 241 | 242 | If the `password` parameter does match an expected value (`mysecret`), return an error. 243 | 244 | #### Resources 245 | 246 | Create a new action to handle checking the password. Join this with the `reverse_words` action using a new sequence. 247 | 248 | #### Tests 249 | 250 | Test with correct password. 251 | 252 | ``` 253 | $ ibmcloud wsk action invoke reverse_words_with_password -r -p password "mysecret" -p text "hello world" 254 | { 255 | "text": "olleh dlrow" 256 | } 257 | ``` 258 | 259 | Test with incorrect password. 260 | 261 | ``` 262 | $ ibmcloud wsk action invoke reverse_words_with_password -r -p text "hello world" 263 | { 264 | "error": "An error has occurred: Error: Password incorrect." 265 | } 266 | ``` 267 | -------------------------------------------------------------------------------- /bootcamp/ex1.3 - bundling NPM modules/README.md: -------------------------------------------------------------------------------- 1 | # bundling NPM modules 2 | 3 | *This additional exercise is for developers using the Node.js runtime. If this isn't you, feel free to skip…* 4 | 5 | This exercise will explain how to use external NPM modules from Node.js actions on IBM Cloud Functions. 6 | 7 | *Once you have completed this exercise, you will have…* 8 | 9 | - **Understood how to use zip files for actions.** 10 | - **Packaged Node.js module as an action.** 11 | - **Added NPM modules to deployment package.** 12 | 13 | Once this exercise is finished, you will know how to bundle external dependencies when creating actions. 14 | 15 | ## Table Of Contents 16 | 17 | * [Background](#background) 18 | * [Packaging actions as Node.js modules](#packaging-actions-as-node.js-modules) 19 | * [Adding NPM dependencies](#adding-npm-dependencies) 20 | 21 | ## Instructions 22 | 23 | ### Background 24 | 25 | OpenWhisk [supports creating Node.js Actions from a zip file](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#packaging-an-action-as-a-nodejs-module). The archive file will be extracted into the runtime environment by the platform. This allows us to split microservice logic across multiple files, use third-party [NPM modules](https://www.npmjs.com/) or include non-JavaScript assets (configuration files, images, HTML files). 26 | 27 | The Node.js runtime for OpenWhisk comes [built-in with a number of popular NPM modules](https://raw.githubusercontent.com/ibm-functions/runtime-nodejs/master/nodejs8/package.json). These libraries can be used without having to manually package up those dependences in the action handler. If you want to use additional libraries, these files needs to be bundled within a zip file. Actions can be created from Node.js modules, provided in that zip file. 28 | 29 | ### Packaging actions as Node.js modules 30 | 31 | Let’s look at a “Hello World” example of registering a serverless function from a zip file. 32 | 33 | Our archive will contain two files, the [package descriptor](https://docs.npmjs.com/files/package.json) and a JavaScript file. 34 | 35 | 1. Create a minimal `package.json` file required for loading a module from a directory. 36 | 37 | ```json 38 | { 39 | "main": "my_file.js" 40 | } 41 | ``` 42 | 43 | 2. Create a `my_file.js` file with the following code. Action handlers are exposed through the `main` property on the `exports` object. Handlers must [implementsthe Action interface.](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#creating-and-invoking-javascript-actions) 44 | 45 | ```javascript 46 | exports.main = function (params) { 47 | return {result: "Hello World"}; 48 | }; 49 | ``` 50 | 51 | 3. Creating a zip file from the current directory. 52 | 53 | ``` 54 | $ zip -r action.zip * 55 | ``` 56 | 57 | 4. Create a new action from the `wsk` CLI. The `kind` property needs to be set when using zip files. 58 | 59 | ``` 60 | $ ibmcloud wsk action create hello_world --kind nodejs:default action.zip 61 | ``` 62 | 63 | 5. Invoke the action. 64 | 65 | ``` 66 | $ ibmcloud wsk action invoke hello_world --result 67 | { 68 | "result": "Hello world" 69 | } 70 | ``` 71 | 72 | *When this Action is invoked, the archive will be unzipped into a temporary directory. OpenWhisk loads the directory as a Node.js module and invokes the function property on the module for each invocation.* 73 | 74 | ### Adding NPM dependencies 75 | 76 | ***This step requires you to have the [Node.js](https://nodejs.org/en/) and [NPM](https://www.npmjs.com/) development tools installed.*** 77 | 78 | Let's build a sample action which uses an external NPM dependency, not available in the runtime environment. 79 | 80 | 1. Create a `package.json` with the following JSON in: 81 | 82 | ```json 83 | { 84 | "name": "my-action", 85 | "main": "index.js", 86 | "dependencies" : { 87 | "left-pad" : "1.1.3" 88 | } 89 | } 90 | ``` 91 | 92 | 2. Create an `index.js `with the following code in: 93 | 94 | ```javascript 95 | function myAction(args) { 96 | const leftPad = require("left-pad") 97 | const lines = args.lines || []; 98 | return { padded: lines.map(l => leftPad(l, 30, ".")) } 99 | } 100 | 101 | exports.main = myAction; 102 | ``` 103 | 104 | Note that the action is exposed through `exports.main`; the action handler itself can have any name, as long as it conforms to the usual signature of accepting an object and returning an object (or a `Promise` of an object). Per Node.js convention, you must either name this file `index.js` or specify the file name you prefer as the `main`property in package.json. 105 | 106 | To create an OpenWhisk action from this package: 107 | 108 | 3. Install first all dependencies locally 109 | 110 | ``` 111 | $ npm install 112 | ``` 113 | 114 | 4. Create a `.zip` archive containing all files (including all dependencies): 115 | 116 | ``` 117 | $ zip -r action.zip * 118 | ``` 119 | 120 | > Please note: Using the Windows Explorer action for creating the zip file will result in an incorrect structure. OpenWhisk zip actions must have `package.json` at the root of the zip, while Windows Explorer will put it inside a nested folder. The safest option is to use the command line `zip` command as shown above. 121 | 122 | 5. Create the action: 123 | 124 | ``` 125 | $ ibmcloud wsk action create packageAction action.zip --kind nodejs:default 126 | ``` 127 | 128 | Note that when creating an action from a `.zip` archive using the CLI tool, you must explicitly provide a value for the `--kind` flag. 129 | 130 | 6. You can invoke the action like any other: 131 | 132 | ``` 133 | $ ibmcloud wsk action invoke --result packageAction --param lines "[\"and now\", \"for something completely\", \"different\" ]" 134 | { 135 | "padded": [ 136 | ".......................and now", 137 | "......for something completely", 138 | ".....................different" 139 | ] 140 | } 141 | ``` 142 | 143 | Finally, note that while most `npm` packages install JavaScript sources on `npm install`, some also install and compile binary artifacts. The archive file upload currently does not support binary dependencies but rather only JavaScript dependencies. Action invocations may fail if the archive includes binary dependencies. 144 | 145 | 🎉🎉🎉 **Node.js has a huge ecosystem of third-party packages which can be used on OpenWhisk with this method. The platform does come built-in with popular packages listed [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#javascript-runtime-environments) This method also works for any other runtime.** 🎉🎉🎉 146 | -------------------------------------------------------------------------------- /bootcamp/ex2 - managing actions with packages/README.md: -------------------------------------------------------------------------------- 1 | # managing actions with packages 2 | 3 | This exercise will introduce the concepts needed to create and use packages with IBM Cloud Functions. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Learnt how to find public packages.** 8 | - **Understood how to use public package actions and bindings.** 9 | - **Created and shared custom packages.** 10 | 11 | Once this exercise is finished, we will be able to create and share actions using packages using IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Browsing packages](#browsing-packages) 17 | * [Invoking actions in a package](#invoking-actions-in-a-package) 18 | * [Creating and using package bindings](#creating-and-using-package-bindings) 19 | * [Creating new packages](#creating-new-packages) 20 | * [Sharing packages](#sharing-packages) 21 | 22 | ## Instructions 23 | 24 | ### Background 25 | 26 | In IBM Cloud Functions, you can use *packages* to bundle together related actions and even share them with others. Packages can only contain *actions*. *Triggers* and *rules* are not supported at the moment. Package nesting is not allowed, i.e. packages cannot contain other packages. 27 | 28 | Packages also support default parameters. Package parameters are automatically passed into actions during invocations. This provides a convenient method to manage service credentials needed with multiple actions. 29 | 30 | The following sections describe how to use packages with actions… 31 | 32 | ### Browsing packages 33 | 34 | IBM Cloud Functions comes pre-installed with a number of public packages, which include trigger feeds used to register triggers with event sources. 35 | 36 | Actions in public packages can be used by anyone, the caller pays the invocation cost. 37 | 38 | Using `ibmcloud wsk` CLI you can get a list of packages in a namespace, list the entities in a package and get a description of the entities within a package. 39 | 40 | 1. Get a list of packages in the `/whisk.system` namespace. 41 | 42 | ``` 43 | $ ibmcloud wsk package list /whisk.system 44 | packages 45 | /whisk.system/combinators shared 46 | /whisk.system/websocket shared 47 | /whisk.system/watson-translator shared 48 | /whisk.system/watson-textToSpeech shared 49 | /whisk.system/github shared 50 | /whisk.system/utils shared 51 | /whisk.system/watson-speechToText shared 52 | /whisk.system/slack shared 53 | /whisk.system/samples shared 54 | /whisk.system/weather shared 55 | /whisk.system/pushnotifications shared 56 | /whisk.system/alarms shared 57 | /whisk.system/cloudant shared 58 | /whisk.system/messaging shared 59 | ``` 60 | 61 | 2. Get a list of entities in the `/whisk.system/cloudant` package. 62 | 63 | ``` 64 | $ ibmcloud wsk package get --summary /whisk.system/cloudant 65 | package /whisk.system/cloudant: Cloudant database service 66 | (parameters: *apihost, *bluemixServiceName, *dbname, *host, overwrite, *password, *username) 67 | action /whisk.system/cloudant/read: Read document from database 68 | (parameters: dbname, id, params) 69 | action /whisk.system/cloudant/write: Write document in database 70 | (parameters: dbname, doc) 71 | feed /whisk.system/cloudant/changes: Database change feed 72 | (parameters: dbname, filter, query_params) 73 | .... 74 | ``` 75 | 76 | **Note:** Parameters listed under the package with a prefix `*` are predefined, bound parameters. Parameters without a `*` are those listed under the annotations for each entity. Furthermore, any parameters with the prefix `**` are finalized bound parameters. This means that they are immutable, and cannot be changed by the user. Any entity listed under a package inherits specific bound parameters from the package. To view the list of known parameters of an entity belonging to a package, you will need to run a `get —summary` of the individual entity. 77 | 78 | This output shows that the Cloudant package provides the actions `read` and `write`, and the trigger feed called `changes`. The `changes` feed causes triggers to be fired when documents are added to the specified Cloudant database. 79 | 80 | The Cloudant package also defines the parameters `username`, `password`, `host`, and `dbname`. These parameters must be specified for the actions and feeds to be meaningful. The parameters allow the actions to operate on a specific Cloudant account, for example. 81 | 82 | 3. Get a description of the `/whisk.system/cloudant/read` action. 83 | 84 | ``` 85 | $ ibmcloud wsk action get --summary /whisk.system/cloudant/read 86 | action /whisk.system/cloudant/read: Read document from database 87 | (parameters: *apihost, *bluemixServiceName, *dbname, *host, *id, params, *password, *username) 88 | ``` 89 | 90 | This output shows that the Cloudant `read` action lists eight parameters, seven of which are predefined. These include the database and document ID to retrieve. 91 | 92 | ### Invoking actions in a package 93 | 94 | You can invoke actions in a package, just as with other actions. The next few steps show how to invoke the `greeting` action in the `/whisk.system/samples` package with different parameters. 95 | 96 | 1. Get a description of the `/whisk.system/samples/greeting` action. 97 | 98 | ``` 99 | $ ibmcloud wsk action get --summary /whisk.system/samples/greeting 100 | action /whisk.system/samples/greeting: Returns a friendly greeting 101 | (parameters: name, place) 102 | ``` 103 | 104 | Notice that the `greeting` action takes two parameters: `name` and `place`. 105 | 106 | 2. Invoke the action without any parameters. 107 | 108 | ``` 109 | $ ibmcloud wsk action invoke --result /whisk.system/samples/greeting 110 | { 111 | "payload": "Hello, stranger from somewhere!" 112 | } 113 | ``` 114 | 115 | The output is a generic message because no parameters were specified. 116 | 117 | 3. Invoke the action with parameters. 118 | 119 | ``` 120 | $ ibmcloud wsk action invoke --result /whisk.system/samples/greeting --param name Bernie --param place Vermont 121 | { 122 | "payload": "Hello, Bernie from Vermont!" 123 | } 124 | ``` 125 | 126 | Notice that the output uses the `name` and `place` parameters that were passed to the action. 127 | 128 | ### Creating and using package bindings 129 | 130 | Although you can use the entities in a package directly, you might find yourself passing the same parameters to the action every time. You can avoid this by binding to a package and specifying default parameters. These parameters are inherited by the actions in the package. 131 | 132 | For example, in the `/whisk.system/cloudant` package, you can set default `username`, `password`, and `dbname` values in a package binding and these values are automatically passed to any actions in the package. 133 | 134 | In the following simple example, you bind to the `/whisk.system/samples` package. 135 | 136 | 1. Bind to the `/whisk.system/samples` package and set a default `place` parameter value. 137 | 138 | ``` 139 | $ ibmcloud wsk package bind /whisk.system/samples valhallaSamples --param place Valhalla 140 | ok: created binding valhallaSamples 141 | ``` 142 | 143 | 2. Get a description of the package binding. 144 | 145 | ``` 146 | $ ibmcloud wsk package get --summary valhallaSamples 147 | package /namespace/valhallaSamples: Returns a result based on parameter place 148 | (parameters: *place) 149 | action /namespace/valhallaSamples/helloWorld: Demonstrates logging facilities 150 | (parameters: payload) 151 | action /namespace/valhallaSamples/greeting: Returns a friendly greeting 152 | (parameters: name, place) 153 | action /namespace/valhallaSamples/curl: Curl a host url 154 | (parameters: payload) 155 | action /namespace/valhallaSamples/wordCount: Count words in a string 156 | (parameters: payload) 157 | ``` 158 | 159 | Notice that all the actions in the `/whisk.system/samples` package are available in the `valhallaSamples` package binding. 160 | 161 | 3. Invoke an action in the package binding. 162 | 163 | ``` 164 | $ ibmcloud wsk action invoke --result valhallaSamples/greeting --param name Odin 165 | { 166 | "payload": "Hello, Odin from Valhalla!" 167 | } 168 | ``` 169 | 170 | Notice from the result that the action inherits the `place` parameter you set when you created the `valhallaSamples` package binding. 171 | 172 | 4. Invoke an action and overwrite the default parameter value. 173 | 174 | ``` 175 | $ ibmcloud wsk action invoke --result valhallaSamples/greeting --param name Odin --param place Asgard 176 | { 177 | "payload": "Hello, Odin from Asgard!" 178 | } 179 | ``` 180 | 181 | Notice that the `place` parameter value that is specified with the action invocation overwrites the default value set in the `valhallaSamples` package binding. 182 | 183 | ### Creating new packages 184 | 185 | Custom packages can be used to group your own actions, manage default parameters and share entities with other users. 186 | 187 | Let's demonstrate how to do this now using the `bx wsk` CLI tool… 188 | 189 | 1. Create a package called "custom". 190 | 191 | ``` 192 | $ ibmcloud wsk package create custom 193 | ok: created package custom 194 | ``` 195 | 196 | 2. Get a summary of the package. 197 | 198 | ``` 199 | $ ibmcloud wsk package get --summary custom 200 | package /myNamespace/custom 201 | (parameters: none defined) 202 | ``` 203 | Notice that the package is empty. 204 | 205 | 3. Create a file called `identity.js` that contains the following action code. This action returns all input parameters. 206 | 207 | ```javascript 208 | function main(args) { return args; } 209 | ``` 210 | 211 | 4. Create an `identity` action in the `custom` package. 212 | 213 | ``` 214 | $ ibmcloud wsk action create custom/identity identity.js 215 | ok: created action custom/identity 216 | ``` 217 | Creating an action in a package requires that you prefix the action name with a package name. 218 | 219 | 5. Get a summary of the package again. 220 | 221 | ``` 222 | $ ibmcloud wsk package get --summary custom 223 | ``` 224 | ``` 225 | package /myNamespace/custom 226 | (parameters: none defined) 227 | action /myNamespace/custom/identity 228 | (parameters: none defined) 229 | ``` 230 | 231 | You can see the `custom/identity` action in your namespace now. 232 | 233 | 6. Invoke the action in the package. 234 | 235 | ``` 236 | $ ibmcloud wsk action invoke --result custom/identity 237 | {} 238 | ``` 239 | 240 | 241 | *You can set default parameters for all the entities in a package. You do this by setting package-level parameters that are inherited by all actions in the package. To see how this works, try the following example:* 242 | 243 | 1. Update the `custom` package with two parameters: `city` and `country`. 244 | 245 | ``` 246 | $ ibmcloud wsk package update custom --param city Austin --param country USA 247 | ok: updated package custom 248 | ``` 249 | 250 | 2. Display the parameters in the package and action, and see how the `identity` action in the package inherits parameters from the package. 251 | 252 | ``` 253 | $ ibmcloud wsk package get custom 254 | ok: got package custom 255 | ... 256 | "parameters": [ 257 | { 258 | "key": "city", 259 | "value": "Austin" 260 | }, 261 | { 262 | "key": "country", 263 | "value": "USA" 264 | } 265 | ] 266 | ... 267 | ``` 268 | 269 | ``` 270 | $ ibmcloud wsk action get custom/identity 271 | ok: got action custom/identity 272 | ... 273 | "parameters": [ 274 | { 275 | "key": "city", 276 | "value": "Austin" 277 | }, 278 | { 279 | "key": "country", 280 | "value": "USA" 281 | } 282 | ] 283 | ... 284 | ``` 285 | 286 | 3. Invoke the identity action without any parameters to verify that the action indeed inherits the parameters. 287 | 288 | ``` 289 | $ ibmcloud wsk action invoke --result custom/identity 290 | ``` 291 | ``` 292 | { 293 | "city": "Austin", 294 | "country": "USA" 295 | } 296 | ``` 297 | 298 | 4. Invoke the identity action with some parameters. Invocation parameters are merged with the package parameters; the invocation parameters override the package parameters. 299 | 300 | ``` 301 | $ ibmcloud wsk action invoke --result custom/identity --param city Dallas --param state Texas 302 | ``` 303 | ``` 304 | { 305 | "city": "Dallas", 306 | "country": "USA", 307 | "state": "Texas" 308 | } 309 | ``` 310 | 311 | 312 | ### Sharing packages 313 | 314 | After the actions and feeds that comprise a package are debugged and tested, the package can be shared with all OpenWhisk users. Sharing the package makes it possible for the users to bind the package, invoke actions in the package, and author OpenWhisk rules and sequence actions. 315 | 316 | 1. Share the package with all users: 317 | 318 | ``` 319 | $ ibmcloud wsk package update custom --shared yes 320 | ok: updated package custom 321 | ``` 322 | 323 | 2. Display the `publish` property of the package to verify that it is now true. 324 | 325 | ``` 326 | $ ibmcloud wsk package get custom 327 | ok: got package custom 328 | ... 329 | "publish": true, 330 | ... 331 | ``` 332 | 333 | 334 | Others can now use your `custom` package, including binding to the package or directly invoking an action in it. Other users must know the fully qualified names of the package to bind it or invoke actions in it. Actions and feeds within a shared package are _public_. If the package is private, then all of its contents are also private. 335 | 336 | 1. Get a description of the package to show the fully qualified names of the package and action. 337 | 338 | ``` 339 | $ ibmcloud wsk package get --summary custom 340 | ``` 341 | ``` 342 | package /myNamespace/custom: Returns a result based on parameters city and country 343 | (parameters: *city, *country) 344 | action /myNamespace/custom/identity 345 | (parameters: none defined) 346 | ``` 347 | 348 | In the previous example, you're working with the `myNamespace` namespace, and this namespace appears in the fully qualified name. 349 | 350 | -------------------------------------------------------------------------------- /bootcamp/ex3 - connecting actions to event sources/README.md: -------------------------------------------------------------------------------- 1 | # connecting actions to event sources 2 | 3 | This exercise introduces concepts (triggers and rules) used by the platform to integrate external event providers. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Understood how event sources are integrated into the platform.** 8 | - **Created example triggers and bound to actions using rules.** 9 | - **Tested connecting triggers to external event sources.** 10 | 11 | Once this exercise is finished, we can start to develop event-driven serverless applications using IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Triggers](#triggers) 17 | * [Rules](#rules) 18 | * [Creating Triggers](#creating-triggers) 19 | * [Using Rules](#using-rules) 20 | * [Creating Rules](#creating-rules) 21 | * [Testing Rules](#testing-rules) 22 | * [Disabling Rules](#disabling-rules) 23 | * [Connecting Trigger Feeds](#connecting-trigger-feeds) 24 | * [EXERCISES](#exercises) 25 | 26 | ## Instructions 27 | 28 | ### Background 29 | 30 | Serverless applications are often described as "event-driven" because you can connect serverless functions to external event sources, like message queues and database changes. When these external events fire, the serverless functions are automatically invoked, without any manual intervention. 31 | 32 | In the previous example, we've been manually invoking actions using the command-line. Let's move onto connecting your actions to external event sources. OpenWhisk supports multiple external event sources like CouchDB, Apache Kafka, a cron-based scheduler and more. 33 | 34 | Before we jump into the details, let's review some concepts which explain how this feature works in Apache OpenWhisk. 35 | 36 | #### Triggers 37 | 38 | Triggers are a named channel for a class of events. The following are examples of triggers: 39 | 40 | - A trigger of location update events. 41 | - A trigger of document uploads to a website. 42 | - A trigger of incoming emails. 43 | 44 | Triggers can be *fired* (activated) by using a dictionary of key-value pairs. Sometimes this dictionary is referred to as the *event*. As with actions, each firing of a trigger results in an activation ID. 45 | 46 | Triggers can be explicitly fired by a user or by an external event source. A *feed* is a convenient way to configure an external event source to fire trigger events that can be consumed by IBM Cloud Functions. Examples of feeds include the following: 47 | 48 | - CouchDB data change feed that fires a trigger event each time a document in a database is added or modified. 49 | - A Git feed that fires a trigger event for every commit to a Git repository. 50 | 51 | 🎉🎉🎉 **Triggers are an implementation of the Observer pattern. Instances of triggers can be fired with parameters. Next we need to find out how to register observers. ** 🎉🎉🎉 52 | 53 | #### Rules 54 | 55 | A rule associates one trigger with one action, with every firing of the trigger causing the corresponding action to be invoked with the trigger event as input. 56 | 57 | With the appropriate set of rules, it's possible for a single trigger event to invoke multiple actions, or for an action to be invoked as a response to events from multiple triggers. 58 | 59 | For example, consider a system with the following actions: 60 | 61 | - `classifyImage` action that detects the objects in an image and classifies them. 62 | - `thumbnailImage` action that creates a thumbnail version of an image. 63 | 64 | Also, suppose that there are two event sources that are firing the following triggers: 65 | 66 | - `newTweet` trigger that is fired when a new tweet is posted. 67 | - `imageUpload` trigger that is fired when an image is uploaded to a website. 68 | 69 | You can set up rules so that a single trigger event invokes multiple actions, and have multiple triggers invoke the same action: 70 | 71 | - `newTweet -> classifyImage` rule. 72 | - `imageUpload -> classifyImage` rule. 73 | - `imageUpload -> thumbnailImage` rule. 74 | 75 | The three rules establish the following behavior: images in both tweets and uploaded images are classified, uploaded images are classified, and a thumbnail version is generated. 76 | 77 | 🎉🎉🎉 **Just remember rules allow you to register an observer on a trigger. That's all we need to know for now, let's look at using these new concepts… ** 🎉🎉🎉 78 | 79 | ### Creating Triggers 80 | 81 | *Triggers* represent a named "channel" for a stream of events. 82 | 83 | Let's create a trigger to send *location updates*: 84 | 85 | ``` 86 | $ ibmcloud wsk trigger create locationUpdate 87 | ok: created trigger locationUpdate 88 | ``` 89 | 90 | You can check that the trigger has been created like this: 91 | 92 | ``` 93 | $ ibmcloud wsk trigger list 94 | triggers 95 | locationUpdate private 96 | ``` 97 | 98 | So far we have only created a named channel to which events can be fired. 99 | 100 | Let's now fire the trigger by specifying its name and parameters: 101 | 102 | ``` 103 | $ ibmcloud wsk trigger fire locationUpdate -p name "Donald" -p place "Washington, D.C" 104 | ok: triggered locationUpdate 105 | ``` 106 | 107 | Triggers also support default parameters. Firing this trigger without any parameters will pass in the default values. 108 | 109 | ``` 110 | $ ibmcloud wsk trigger update locationUpdate -p name "Donald" -p place "Washington, D.C" 111 | ok: updated trigger locationUpdate 112 | $ ibmcloud wsk trigger fire locationUpdate 113 | ok: triggered locationUpdate 114 | ``` 115 | 116 | Events you fire to the `locationUpdate` trigger currently do not do anything. To be useful, we need to create a rule that associates the trigger with an action. 117 | 118 | 🎉🎉🎉 **That was easy? Let's keep going by connecting actions to triggers…** 🎉🎉🎉 119 | 120 | ### Using Rules 121 | 122 | Rules are used to associate a trigger with an action. Each time a trigger event is fired, the action is invoked with the event parameters. 123 | 124 | #### Creating Rules 125 | 126 | As an example, create a rule that calls the `hello` action whenever a location update is triggered. 127 | 128 | 1. Check the `hello` action exists and responds to the correct event parameters. 129 | 130 | ``` 131 | $ ibmcloud wsk action invoke --result hello --param name Bernie --param place Vermont 132 | { 133 | "payload": "Hello, Bernie from Vermont" 134 | } 135 | ``` 136 | 137 | 2. Check the trigger exists. 138 | 139 | ``` 140 | $ ibmcloud wsk trigger get locationUpdate 141 | ok: got trigger a 142 | { 143 | "namespace": "user@host.com_dev", 144 | "name": "locationUpdate", 145 | "version": "0.0.1", 146 | "limits": {}, 147 | "publish": false 148 | } 149 | ``` 150 | 151 | 3. Create the rule using the command-line. The three parameters are the name of the rule, the trigger, and the action. 152 | 153 | ``` 154 | $ ibmcloud wsk rule create myRule locationUpdate hello 155 | ok: created rule myRule 156 | ``` 157 | 158 | 4. Retrieve rule details to show the trigger and action bound by this rule. 159 | 160 | ``` 161 | $ ibmcloud wsk rule get myRule 162 | ok: got rule myRule 163 | { 164 | "namespace": "user@host.com_dev", 165 | "name": "myRule", 166 | "version": "0.0.1", 167 | "status": "active", 168 | "trigger": { 169 | "name": "locationUpdate", 170 | "path": "user@host.com_dev" 171 | }, 172 | "action": { 173 | "name": "hello", 174 | "path": "user@host.com_dev" 175 | }, 176 | "publish": false 177 | } 178 | ``` 179 | 180 | #### Testing Rules 181 | 182 | 1. Fire the `locationUpdate` trigger. Each time that you fire the trigger with an event, the `hello` action is called with the event parameters. 183 | 184 | ``` 185 | $ ibmcloud wsk trigger fire locationUpdate --param name Donald --param place "Washington, D.C." 186 | ok: triggered /_/locationUpdate with id 5c153c01d76d49dc953c01d76d99dc34 187 | ``` 188 | 189 | 2. Verify that the action was invoked by checking the activations list. 190 | 191 | ``` 192 | $ ibmcloud wsk activation list --limit 2 193 | activations 194 | 5ee74025c2384f30a74025c2382f30c1 hello 195 | 5c153c01d76d49dc953c01d76d99dc34 locationUpdate 196 | ``` 197 | 198 | We can see the trigger activation (`5c153c01d76d49dc953c01d76d99dc34`) is recorded, followed by the `hello` action activation (`5ee74025c2384f30a74025c2382f30c1`). 199 | 200 | 3. Retrieving the trigger activation record will show the actions and rules invoked from this activation. 201 | 202 | ``` 203 | $ ibmcloud wsk activation result 5ee74025c2384f30a74025c2382f30c1 204 | { 205 | "payload": "Hello, Donald from Washington, D.C." 206 | } 207 | ``` 208 | 209 | You can see that the hello action received the event payload and returned the expected string. 210 | 211 | Activation records for triggers store the rules and actions fired for an event and the event parameters. 212 | 213 | ``` 214 | $ ibmcloud wsk activation result 5c153c01d76d49dc953c01d76d99dc34 215 | { 216 | "name": "Donald", 217 | "place": "Washington, D.C." 218 | } 219 | $ ibmcloud wsk activation logs 5c153c01d76d49dc953c01d76d99dc34 220 | {"statusCode":0,"success":true,"activationId":"5ee74025c2384f30a74025c2382f30c1","rule":"user@host.com_dev/myRule","action":"user@host.com_dev/hello"} 221 | ``` 222 | 223 | You can create multiple rules that associate the same trigger with different actions. 224 | 225 | **Can you create another trigger and rule that calls the `hello` action? 🤔** 226 | 227 | You can also use rules with sequences. For example, one can create an action sequence `recordLocationAndHello`that is activated by the rule `anotherRule`. 228 | 229 | ``` 230 | $ ibmcloud wsk action create recordLocationAndHello --sequence /whisk.system/utils/echo,hello 231 | $ ibmcloud wsk rule create anotherRule locationUpdate recordLocationAndHello 232 | ``` 233 | 234 | #### Disabling Rules 235 | 236 | Rules are enabled upon creation but can be disabled and re-enabled using the command-line. 237 | 238 | 1. Disable the rule connecting the `locationUpdate` trigger and `hello` action. 239 | 240 | ``` 241 | $ ibmcloud wsk rule disable myRule 242 | ``` 243 | 244 | 2. Fire the trigger again. 245 | 246 | ``` 247 | $ ibmcloud wsk trigger fire locationUpdate --param name Donald --param place "Washington, D.C." 248 | ok: triggered /_/locationUpdate with id 53f85c39087d4c15b85c39087dac1571 249 | ``` 250 | 251 | 3. Check the activation list there are no new activation records. 252 | 253 | ``` 254 | $ ibmcloud wsk activation list --limit 2 255 | activations 256 | 5ee74025c2384f30a74025c2382f30c1 hello 257 | 5c153c01d76d49dc953c01d76d99dc34 locationUpdate 258 | ``` 259 | 260 | The latest activation records were from the previous example. 261 | 262 | *Activation records for triggers are only recorded when they are bound to an active rule.* 263 | 264 | 🎉🎉🎉 **Right, now we have a way to connect actions to events in OpenWhisk, how do we connect triggers to event sources like messages queues? Enter trigger feeds…** 🎉🎉🎉 265 | 266 | ### Connecting Trigger Feeds 267 | 268 | Trigger feeds allow you to connect triggers to external event sources. Event sources will fire registered triggers each time an event occurs. Here's a list of the event sources currently supported on IBM Cloud Functions: 269 | 270 | This example shows how to use a feed in the [Alarms package](https://github.com/apache/incubator-openwhisk-package-alarms/blob/master/README.md) to fire a trigger every minute, which invokes an action using a rule. 271 | 272 | 1. Get a description of the feeds in the `/whisk.system/alarms` package. 273 | 274 | ``` 275 | $ ibmcloud wsk package get --summary /whisk.system/alarms 276 | package /whisk.system/alarms: Alarms and periodic utility 277 | (parameters: *apihost, *trigger_payload) 278 | feed /whisk.system/alarms/interval: Fire trigger at specified interval 279 | (parameters: minutes, startDate, stopDate) 280 | feed /whisk.system/alarms/once: Fire trigger once when alarm occurs 281 | (parameters: date, deleteAfterFire) 282 | feed /whisk.system/alarms/alarm: Fire trigger when alarm occurs 283 | (parameters: cron, startDate, stopDate) 284 | ``` 285 | 286 | 2. Retrieve the details for the `alarms/interval` feed. 287 | 288 | ``` 289 | $ ibmcloud wsk action get --summary /whisk.system/alarms/interval 290 | action /whisk.system/alarms/interval: Fire trigger at specified interval 291 | (parameters: *apihost, *isInterval, minutes, startDate, stopDate, *trigger_payload) 292 | ``` 293 | 294 | The `/whisk.system/alarms/interval` feed has the following parameters we need to pass in: 295 | 296 | - `minutes`: An integer representing the length of the interval (in minutes) between trigger fires. 297 | - `trigger_payload`: The payload parameter value to set in each trigger event. 298 | 299 | 3. Create a trigger that fires every minute using this feed. 300 | 301 | ``` 302 | $ ibmcloud wsk trigger create everyMinute --feed /whisk.system/alarms/interval -p minutes 1 -p trigger_payload "{\"name\":\"Mork\", \"place\":\"Ork\"}" 303 | ok: invoked /whisk.system/alarms/interval with id b2b4c3cb38224f44b4c3cb38228f44be 304 | ... 305 | ok: created trigger everyMinute 306 | ``` 307 | 308 | 4. Connect this trigger to the `hello` action with a new rule. 309 | 310 | ``` 311 | $ ibmcloud wsk rule create everyMinuteRule everyMinute hello 312 | ok: created rule everyMinuteRule 313 | ``` 314 | 315 | 5. Check that the action is being invoked every minute by polling for activation logs. 316 | 317 | ``` 318 | $ ibmcloud wsk activation poll 319 | Activation: 'hello' (b2fc4b00c7be4143bc4b00c7bed1431c) 320 | [] 321 | Activation: 'everyMinute' (cec7eb38739c4d4287eb38739ccd42ef) 322 | [ 323 | "{\"statusCode\":0,\"success\":true,\"activationId\":\"b2fc4b00c7be4143bc4b00c7bed1431c\",\"rule\":\"james.thomas@uk.ibm.com_dev/everyMinuteRule\",\"action\":\"james.thomas@uk.ibm.com_dev/hello\"}" 324 | ] 325 | ``` 326 | 327 | You should see activations every minute the trigger and the action. The action receives the parameters `{"name":"Mork", "place":"Ork"}` on every invocation. 328 | 329 | **IMPORTANT: Let's delete the trigger and rule or this event will be running forever!** 330 | 331 | ``` 332 | $ ibmcloud wsk trigger delete everyMinute 333 | $ ibmcloud wsk rule delete everyMinuteRule 334 | ``` 335 | 336 | 🎉🎉🎉 **Understanding triggers and rules allows you to build event-driven applications on OpenWhisk. Create some actions, hook up events and let the platform take care of everything else, what could be easier?** 🎉🎉🎉 337 | 338 | #### EXERCISES 339 | 340 | Let's try out your new SERVERLESS SUPERPOWERS 💪 to build a real serverless application. 341 | 342 | ***Can you use triggers & feeds to build a notification system when new users are added to a system?*** 343 | 344 | #### Background 345 | 346 | Users are stored in a Cloudant database. When a new user is added to the database, we need to send them a welcome message. Users might provide an email address and/or phone number during registration. Welcome messages should be sent on all available channels. 347 | 348 | #### Resources 349 | 350 | IBM Cloud provides a free instance of the [Cloudant database](https://console.bluemix.net/catalog/services/cloudant) to Lite account users. 351 | 352 | Apache OpenWhisk supports listening to the [`changes` feed](http://docs.couchdb.org/en/2.0.0/api/database/changes.html) for a Cloudant database using this trigger feed: https://github.com/apache/incubator-openwhisk-package-cloudant 353 | 354 | SendGrid [provides an API](https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html) for sending emails. Twilio [provides an API](https://www.twilio.com/sms/api) for sending SMS messages. 355 | 356 | Triggers can be fired programatically from an action using the [JavaScript client library.](https://github.com/apache/incubator-openwhisk-client-js#fire-trigger) 357 | 358 | #### Architecture 359 | 360 | This serverless application will have three actions: `user_changes`, `send_welcome_email` and `send_welcome_sms`. It will also have three triggers: `db_changes`, `new_user_email` and `new_user_sms`. `db_changes` will be connected to the Cloudant trigger feed. 361 | 362 | `user_changes` will listen for database update using the `db_changes` trigger feed. When a new user record is received, it will check the record for email and phone number values. If either of those values exist, it will fire a custom trigger (`new_user_email` and `new_user_sms`) for each event type with the parameter value. 363 | 364 | `send_welcome_email` will be connected to the `new_user_email` trigger. It will send a welcome email to the email address from the event. 365 | 366 | `send_welcome_sms` will be connected to the `new_user_sms` trigger. It will send a welcome SMS message to the phone number from the event. 367 | 368 | #### Tests 369 | 370 | Test the email action by firing a trigger and checking for the email message. 371 | 372 | ``` 373 | $ ibmcloud wsk trigger fire new_user_email -p address "blah@blah.com" 374 | ``` 375 | 376 | Test the SMS action by firing a trigger and checking for the SMS message. 377 | 378 | ``` 379 | $ ibmcloud wsk trigger fire new_user_sms -p phone_number "XXXXX" 380 | ``` 381 | 382 | Test the DB listener action by firing a trigger with a sample database document. Provide a sample JSON document in the `doc.json` file. 383 | 384 | ``` 385 | $ ibmcloud wsk trigger fire db_changes -P doc.jsonTest with correct password. 386 | ``` 387 | 388 | Create some new user documents in the database and with contact details and check the welcome messages are received. 389 | 390 | #### Hint & Tips 391 | 392 | Start with building & testing the sms and email actions. Once you have tested and verified they work, move onto the database listener action. 393 | 394 | Don't create the trigger rules until you have verified all the actions work individually. -------------------------------------------------------------------------------- /bootcamp/ex4 - exposing APIs from actions/README.md: -------------------------------------------------------------------------------- 1 | # exposing APIs from actions 2 | 3 | This exercise shows you how to create public HTTP endpoints from actions. You will learn how to create web actions and use the integrated API gateway. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Understood how IBM Cloud Functions can expose HTTP endpoints.** 8 | - **Created numerous example Web Actions** 9 | - **Enabled API Gateway integration including authentication and rate-limiting.** 10 | 11 | Once this exercise is finished, you will be able to creating scalable HTTP APIs using IBM Cloud Functions! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Web Actions](#web-actions) 17 | * [API Gateway](#api-gateway) 18 | * [Web Actions](#web-actions) 19 | * [Content Extensions](#content-extensions) 20 | * [HTTP Request Properties](#http-request-properties) 21 | * [Controlling HTTP Responses](#controlling-http-responses) 22 | * [Additional Features](#additional-features) 23 | * [Example - HTTP Redirect](#example-—http-redirect) 24 | * [Example - JPEG Response](#example-—jpeg-response) 25 | * [Example - Manual JSON Response](#example—-manual-json-response) 26 | * [API Gateway](#api-gateway) 27 | * [Example](#example) 28 | * [Saving & Restoring](#saving-&-#restoring) 29 | * [API Management Features](#api-management-features) 30 | * [Authentication](#authentication) 31 | * [Rate Limiting](#rate-limiting) 32 | * [EXERCISES](#exercises) 33 | 34 | ## Instructions 35 | 36 | ### Background 37 | 38 | Serverless applications are a great solution for building public API endpoints. Developers are now building "serverless web applications" by hosting their static files on a CDN and then using serverless platforms for their APIs. 39 | 40 | OpenWhisk has a comprehensive RESTful API for the platform that allows you to invoke actions using authenticated HTTP requests. However, if you want to build APIs for public web sites or mobile applications, the authentication credentials will need embedding in client-side files. This is a terrible idea for obvious reasons…. but don't panic! 41 | 42 | OpenWhisk has a solution for creating public APIs to invoke your actions without exposing credentials. 😎 43 | 44 | Let's review some concepts which explain how this feature works in Apache OpenWhisk. 45 | 46 | #### Web Actions 47 | 48 | OpenWhisk actions can be annotated with a special flag at runtime to convert them into "web actions". Web actions can then be invoked through the public platform API using a HTTP request without user authentication. HTTP request parameters are automatically converted in event parameters. Values returned from the action are automatically serialised to a JSON response. 49 | 50 | Web actions are a simple way to expose public HTTP endpoints from OpenWhisk actions. If you want to implement user authentication, rate limiting or routing, web actions have to manually handle this in the OpenWhisk action code. If you are building high-traffic and enterprise APIs, you will want a better solution to implementing these API features, that doesn't require lots of boilerplate code… 51 | 52 | #### API Gateway 53 | 54 | OpenWhisk comes with an integrated API Gateway. This allows the developers to create new HTTP APIs which map incoming requests to web actions. The API Gateway handles capabilities like routing based on request properties (URI paths and HTTP method), user authentication, rate limiting and more. Developers do not need to implement this features within the web action code. 55 | 56 | Using the API Gateway is an extension to web actions that allows you to build enterprise high-traffic HTTP APIs with minimal effort using IBM Cloud Functions. 57 | 58 | ### Web Actions 59 | 60 | Let's turn the `hello` action into a web action. Once it has been converted, we can call this action using a normal HTTP request. 61 | 62 | 1. Update the action to set the `—web` flag to `true`. 63 | 64 | ``` 65 | $ ibmcloud wsk action update hello --web true 66 | ok: updated action hello 67 | ``` 68 | 69 | 2. Retrieve the web action URL exposed by the platform for this action. 70 | 71 | ``` 72 | $ ibmcloud wsk action get hello --url 73 | ok: got action hello 74 | https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/hello 75 | ``` 76 | 77 | 3. Invoke the web action URL with the JSON extension, passing in query parameters for `name` and `place`. 78 | 79 | ``` 80 | $ curl "https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/hello.json?name=Bernie" 81 | { 82 | "message": "Hello Bernie from Vermont!" 83 | } 84 | ``` 85 | 86 | 4. Disable web action support. 87 | 88 | ``` 89 | $ ibmcloud wsk action update hello --web false 90 | ok: updated action hello 91 | ``` 92 | 93 | 5. Verify the action is not externally accessible with authentication. 94 | 95 | ``` 96 | $ curl "https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/hello.json?name=Bernie" 97 | { 98 | "error": "The requested resource does not exist.", 99 | "code": 4452991 100 | } 101 | ``` 102 | 103 | #### Content Extensions 104 | 105 | Web actions invoked through the platform API need a content extension to tell the platform how to interpret the content returned from the action. In the example above, we were using the `.json` extension. This tells the platform to serialise the return value out to a JSON response. 106 | 107 | The platform supports the following content-types: `.json`, `.html`, `.http`, `.svg` or `.text`. If no content extension is provided, it defaults to `.http` which gives the action full control of the HTTP response. 108 | 109 | #### HTTP Request Properties 110 | 111 | All web actions, when invoked, receives additional HTTP request details as parameters to the action input argument. These include: 112 | 113 | 1. `__ow_method` (type: string). the HTTP method of the request. 114 | 2. `__ow_headers` (type: map string to string): A the request headers. 115 | 3. `__ow_path` (type: string): the unmatched path of the request (matching stops after consuming the action extension). 116 | 4. `__ow_user` (type: string): the namespace identifying the OpenWhisk authenticated subject 117 | 5. `__ow_body` (type: string): the request body entity, as a base64 encoded string when content is binary or JSON object/array, or plain string otherwise 118 | 6. `__ow_query` (type: string): the query parameters from the request as an unparsed string 119 | 120 | The `__ow_user` is only present when the web action is [annotated to require authentication](https://github.com/apache/incubator-openwhisk/blob/master/docs/annotations.md#annotations-specific-to-web-actions) and allows a web action to implement its own authorization policy. The `__ow_query` is available only when a web action elects to handle the ["raw" HTTP request](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md#raw-http-handling). It is a string containing the query parameters parsed from the URI (separated by `&`). The `__ow_body` property is present either when handling "raw" HTTP requests, or when the HTTP request entity is not a JSON object or form data. Web actions otherwise receive query and body parameters as first class properties in the action arguments with body parameters taking precedence over query parameters, which in turn take precedence over action and package parameters. 121 | 122 | #### Controlling HTTP Responses 123 | 124 | Web actions can return a JSON object with the following properties to directly control the HTTP response returned to the client. 125 | 126 | 1. `headers`: a JSON object where the keys are header-names and the values are string, number, or boolean values for those headers (default is no headers). To send multiple values for a single header, the header's value should be a JSON array of values. 127 | 2. `statusCode`: a valid HTTP status code (default is 200 OK if body is not empty otherwise 204 No Content). 128 | 3. `body`: a string which is either plain text, JSON object or array, or a base64 encoded string for binary data (default is empty response). 129 | 130 | The `body` is considered empty if it is `null`, the empty string `""` or undefined. 131 | 132 | If a `content-type header` is not declared in the action result’s `headers`, the body is interpreted as `application/json` for non-string values, and `text/html` otherwise. When the `content-type` is defined, the controller will determine if the response is binary data or plain text and decode the string using a base64 decoder as needed. Should the body fail to decoded correctly, an error is returned to the caller. 133 | 134 | #### Additional Features 135 | 136 | Web actions have a [lot more features](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md), see the documentation for full details on all these capabilities. 137 | 138 | #### Example - HTTP Redirect 139 | 140 | 1. Create a new web action from the following source code. 141 | 142 | ##### Node.js example 143 | 144 | ```javascript 145 | function main() { 146 | return { 147 | headers: { location: "http://openwhisk.org" }, 148 | statusCode: 302 149 | }; 150 | } 151 | ``` 152 | 153 | ``` 154 | $ ibmcloud wsk action create redirect action.js --web true 155 | ok: created action redirect 156 | ``` 157 | 158 | ##### Swift example 159 | 160 | ```swift 161 | func main(args: [String:Any]) -> [String:Any] { 162 | return [ 163 | "headers": ["location": "http://openwhisk.org"], 164 | "statusCode": 302 165 | ] 166 | } 167 | ``` 168 | 169 | ``` 170 | $ ibmcloud wsk action create redirect action.swift --web true 171 | ok: created action redirect 172 | ``` 173 | 174 | 2. Retrieve URL for new web action 175 | 176 | ``` 177 | $ ibmcloud wsk action get redirect --url 178 | ok: got action redirect 179 | https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/redirect 180 | ``` 181 | 182 | 3. Check HTTP response is HTTP redirect. 183 | 184 | ``` 185 | $ curl -v https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/redirect 186 | < HTTP/1.1 302 Found 187 | < X-Backside-Transport: OK OK 188 | < Connection: Keep-Alive 189 | < Transfer-Encoding: chunked 190 | < Server: nginx/1.11.13 191 | < Date: Fri, 23 Feb 2018 11:23:24 GMT 192 | < Access-Control-Allow-Origin: * 193 | < Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH 194 | < Access-Control-Allow-Headers: Authorization, Content-Type 195 | < location: http://openwhisk.org 196 | ``` 197 | 198 | #### Example - HTML Response 199 | 200 | 1. Create a new web action from the following source code. 201 | 202 | ##### Node.js example 203 | 204 | ```javascript 205 | function main() { 206 | let html = "Hello World!" 207 | return { headers: { "Content-Type": "text/html" }, 208 | statusCode: 200, 209 | body: html }; 210 | } 211 | ``` 212 | 213 | ``` 214 | $ ibmcloud wsk action create html action.js --web true 215 | ok: created action html 216 | ``` 217 | 218 | ##### Swift example 219 | 220 | ```swift 221 | func main(args: [String:Any]) -> [String:Any] { 222 | let html = "Hello World!" 223 | return [ 224 | "headers": ["Content-Type": "text/html"], 225 | "statusCode": 200, 226 | "body": html 227 | ] 228 | } 229 | ``` 230 | 231 | ``` 232 | $ ibmcloud wsk action create html action.swift --web true 233 | ok: created action html 234 | ``` 235 | 236 | 1. Retrieve URL for new web action 237 | 238 | ```swift 239 | $ ibmcloud wsk action get html --url 240 | ok: got action html 241 | https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/html 242 | ``` 243 | 244 | 3. Check HTTP response is HTML. 245 | 246 | ``` 247 | $ curl https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/html 248 | Hello World! 249 | ``` 250 | 251 | #### Example - JPEG Response 252 | 253 | 1. Download PNG image and generate base64 string. 254 | 255 | ``` 256 | $ wget https://static01.nytimes.com/newsgraphics/2015/01/30/candidate-tracker/assets/images/sanders-square-silo-150.png 257 | $ base64 sanders-square-silo-150.png 258 | ``` 259 | 260 | 2. Create a new web action from the following source code. 261 | 262 | ##### Node.js 263 | 264 | ```javascript 265 | function main() { 266 | let png = "" 267 | return { headers: { "Content-Type": "image/png" }, 268 | statusCode: 200, 269 | body: png }; 270 | } 271 | ``` 272 | 273 | ``` 274 | $ ibmcloud wsk action create image action.js --web true 275 | ok: created action image 276 | ``` 277 | 278 | ##### Swift 279 | 280 | ```swift 281 | func main(args: [String:Any]) -> [String:Any] { 282 | let png = "" 283 | 284 | return [ 285 | "headers": ["Content-Type": "image/png"], 286 | "statusCode": 200, 287 | "body": png 288 | ] 289 | } 290 | ``` 291 | 292 | ``` 293 | $ ibmcloud wsk action create image action.swift --web true 294 | ok: created action image 295 | ``` 296 | 297 | 3. Retrieve URL for new web action. 298 | 299 | ``` 300 | $ ibmcloud wsk action get image --url 301 | ok: got action image 302 | https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/image 303 | ``` 304 | 305 | 4. Open URL in web browser to check the following image is returned. 306 | 307 | ![Feel the Bern!](https://static01.nytimes.com/newsgraphics/2015/01/30/candidate-tracker/assets/images/sanders-square-silo-150.png) 308 | 309 | #### Example - Manual JSON response 310 | 311 | 1. Create a new web action from the following source code. 312 | 313 | ##### Node.js 314 | 315 | ```javascript 316 | function main(params) { 317 | return { 318 | statusCode: 200, 319 | headers: { 'Content-Type': 'application/json' }, 320 | body: params 321 | }; 322 | } 323 | ``` 324 | 325 | ``` 326 | $ ibmcloud wsk action create manual action.js --web true 327 | ok: created action manual 328 | ``` 329 | 330 | ##### Swift 331 | 332 | ```javascript 333 | func main(args: [String:Any]) -> [String:Any] { 334 | return [ 335 | "headers": ["Content-Type": "application/json"], 336 | "statusCode": 200, 337 | "body": args 338 | ] 339 | } 340 | ``` 341 | 342 | ``` 343 | $ bx wsk action create manual action.swift --web true 344 | ok: created action manual 345 | ``` 346 | 347 | 2. Retrieve URL for new web action 348 | 349 | ``` 350 | $ ibmcloud wsk action get manual --url 351 | ok: got action manual 352 | https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/manual 353 | ``` 354 | 355 | 3. Check HTTP response is JSON. 356 | 357 | ``` 358 | $ curl "https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/manual?hello=world" 359 | { 360 | "__ow_method": "get", 361 | "__ow_headers": { 362 | "accept": "*/*", 363 | "user-agent": "curl/7.54.0", 364 | "x-client-ip": "92.11.100.114", 365 | "x-forwarded-proto": "https", 366 | "host": "openwhisk.ng.bluemix.net:443", 367 | "cache-control": "no-transform", 368 | "via": "1.1 DwAAAD0oDAI-", 369 | "x-global-transaction-id": "2654586489", 370 | "x-forwarded-for": "92.11.100.114" 371 | }, 372 | "__ow_path": "", 373 | "hello": "world" 374 | ``` 375 | 376 | 4. Use other HTTP methods or URI paths to show the parameters change. 377 | 378 | ``` 379 | $ curl -XPOST "https://openwhisk.ng.bluemix.net/api/v1/web/user%40host.com_dev/default/manual/subpath" 380 | { 381 | "__ow_method": "post", 382 | "__ow_headers": { 383 | "accept": "*/*", 384 | "user-agent": "curl/7.54.0", 385 | "x-client-ip": "92.11.100.114", 386 | "x-forwarded-proto": "https", 387 | "host": "openwhisk.ng.bluemix.net:443", 388 | "via": "1.1 AgAAAB+7NgA-", 389 | "x-global-transaction-id": "2897764571", 390 | "x-forwarded-for": "92.11.100.114" 391 | }, 392 | "__ow_path": "/subpath", 393 | "hello": "world" 394 | ``` 395 | 396 | 🎉🎉🎉 **OpenWhisk Web Actions are an awesome feature. Exposing public APIs from actions is minimal effort. Let's finish off this section by looking at an additional approach, using an API Gateway.** 🎉🎉🎉 397 | 398 | ### API Gateway 399 | 400 | OpenWhisk web actions can benefit from being exposed using the API gateway. 401 | 402 | The API Gateway acts as a proxy to [Web Actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md) and provides them with additional features including HTTP method routing , client id/secrets, rate limiting and CORS. For more information on API Gateway feature you can read the [api management documentation](https://github.com/apache/incubator-openwhisk-apigateway/blob/master/doc/v2/management_interface_v2.md) 403 | 404 | #### example 405 | 406 | Let's look a short example of using the API Gateway service… 407 | 408 | 1. Ensure the `hello` action is enabled as a web action. 409 | 410 | ``` 411 | $ ibmcloud wsk action update hello --web true 412 | ok: updated action hello 413 | ``` 414 | 415 | 2. Create a new API Gateway endpoint for the web action. 416 | 417 | ``` 418 | $ ibmcloud wsk api create /api/hello get hello --response-type json --apiname "hello-world" 419 | ok: created API /api/hello GET for action /_/hello 420 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello 421 | ``` 422 | 423 | 3. Check HTTP API returns JSON response. 424 | 425 | ``` 426 | $ curl "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 427 | { 428 | "payload": "Hello, Bernie from Vermont" 429 | } 430 | ``` 431 | 432 | 4. Check other HTTP methods are not supported. 433 | 434 | ``` 435 | $ curl -XPOST "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 436 | {"status":404,"message":"Error: Whoops. Verb not supported."} 437 | ``` 438 | 439 | 5. Enable other endpoints and verbs. 440 | 441 | ``` 442 | $ ibmcloud wsk api create /api/hello/world get hello --response-type json --apiname "hello-world" 443 | ok: created API /api/hello/world GET for action /_/hello 444 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello/world 445 | $ curl "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello/world?name=Bernie" 446 | { 447 | "payload": "Hello, Bernie from Vermont" 448 | } 449 | ``` 450 | 451 | ``` 452 | $ ibmcloud wsk api create /api/hello post hello --response-type json --apiname "hello-world" 453 | ok: created API /api/hello POST for action /_/hello 454 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello 455 | $ curl -XPOST "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 456 | { 457 | "payload": "Hello, Bernie from Vermont" 458 | } 459 | ``` 460 | 461 | #### saving & restoring 462 | 463 | 1. List all the exposed API endpoints. 464 | 465 | ``` 466 | $ ibmcloud wsk api list 467 | ``` 468 | 469 | 2. Export the API definitions to a Swagger file. 470 | 471 | ``` 472 | $ ibmcloud wsk api get / > swagger.json 473 | ``` 474 | 475 | 3. Delete all the API definitions. 476 | 477 | ``` 478 | $ ibmcloud wsk api delete / 479 | ok: deleted API / 480 | ``` 481 | 482 | 4. Check there are no more APIs defined. 483 | 484 | ``` 485 | $ ibmcloud wsk api list 486 | ok: APIs 487 | Action Verb API Name URL 488 | ``` 489 | 490 | 5. Restore from Swagger file. 491 | 492 | ``` 493 | $ ibmcloud wsk api create --config-file swagger.json 494 | ``` 495 | 496 | 6. Confirm the APIs have been re-created. 497 | 498 | ``` 499 | $ ibmcloud wsk api list 500 | ``` 501 | 502 | ### API Management Features 503 | 504 | IBM Cloud Functions supports using the IBM API Management service to support more advanced features including authentication, rate limiting, CORS and more. These features are available when using the IBM Cloud Functions Web UI to create and modify the API endpoints. 505 | 506 | Let's look at setting up some of these features for the API endpoints we have already defined… 507 | 508 | 1. Open the [IBM Cloud Functions](https://console.bluemix.net/openwhisk/) homepage. 509 | 2. Navigate to the [APIs](https://console.bluemix.net/openwhisk/apimanagement) page. 510 | 3. Click the "hello world" API in the table. 511 | 4. Select the "Definition" link from the API details menu. 512 | 513 | ![API Management Web UI](images/apis.gif) 514 | 515 | #### Authentication 516 | 517 | Let's turn on custom authentication for our APIs to ensure only authorised users use them. 518 | 519 | 1. Under the "*Security and Rate Limiting*" section, toggle the switch to enable authentication. 520 | 2. Check "*Method*" is "*API key only*" and "*Location*" is "*Header*". 521 | 3. Set the "*Parameter name of API key*" to "*X-Auth-Key*" 522 | 4. Click the "*Save*" button to update the API definition. 523 | 524 | ![Authentication](images/auth-on.png) 525 | 526 | Once we have enabled API authentication, we need to create the API keys on the "*Sharing*" panel. 527 | 528 | 1. Under the "*Sharing Outside of Cloud Foundry organization*" section, select "*Create API key*" 529 | 530 | ![Authentication](images/api-keys.png) 531 | 532 | 2. Set the key name as "*sample-key*" and make a note of the API key value. 533 | 3. Click the "*Create*" button. 534 | 535 | If the key has been created correctly the table should now display the newly enabled key. 536 | 537 | Let's try out calling one of our API endpoints without an API key. 538 | 539 | ``` 540 | $ ibmcloud wsk api list 541 | ok: APIs 542 | Action Verb API Name URL 543 | /user@host.com_dev/hello get hello-world https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello 544 | /user@host.com_dev/hello post hello-world https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello 545 | /user@host.com_dev/hello get hello-world https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello/world 546 | $ curl https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello 547 | {"status":401,"message":"Error: Unauthorized"} 548 | ``` 549 | 550 | If we now call the same API endpoint with the authentication header, it should succeed. 551 | 552 | ``` 553 | $ curl -H X-Auth-Key: "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 554 | { 555 | "payload": "Hello, Bernie from Vermont" 556 | } 557 | ``` 558 | 559 | *Hurrah it works!* 560 | 561 | IBM API Management also provides authentication support including keys with secrets and OAuth integration. 562 | 563 | #### Rate Limiting 564 | 565 | Let's enable rate limiting on our APIs to ensure we don't have to pay too much! Rate limiting is only supported when authentication is enabled. Limits are on a per-key basis. 566 | 567 | 1. Under the "*Security and Rate Limiting*" section, toggle the "*Rate Limiting*" switch to on. 568 | 2. Set "*Maximum Calls*" to 1 and "*Unit of time*" to "*Minutes*" 569 | 3. Click the "*Save*" button to update the API definition. 570 | 571 | ![Authentication](images/rate-limit.png) 572 | 573 | Let's check rate limiting is working. 574 | 575 | 1. Call the authenticated endpoint twice in succession. 576 | 577 | ``` 578 | $ curl -H X-Auth-Key: "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 579 | { 580 | "payload": "Hello, Bernie from Vermont" 581 | } 582 | $ curl -H X-Auth-Key: "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/hello?name=Bernie" 583 | { 584 | "status":429, 585 | "message":"Error: Rate limit exceeded" 586 | } 587 | ``` 588 | 589 | On the first call, the response is returned as normal. On the second call, the rate limiting error should be returned. If you wait sixty seconds and try again, the request will be processed as normal. 590 | 591 | 🎉🎉🎉 **The API Gateway service add services like request routing, based on method and paths, rate limiting and pluggable authentication. It is perfect for high-traffic public APIs.** 🎉🎉🎉 592 | 593 | #### EXERCISES 594 | 595 | Let's try out your new SERVERLESS SUPERPOWERS 💪 to build a real serverless application. 596 | 597 | ***Can you use the API gateway to build REST-ful APIs for a TODO application?*** 598 | 599 | #### Background 600 | 601 | In this exercise, you are going to create HTTP endpoints for the backend of a Todo application. Todos can be created, retrieved, updated and deleted using the API. Todos are stored in a Cloudant database. 602 | 603 | #### Resources 604 | 605 | IBM Cloud provides a free instance of the [Cloudant database](https://console.bluemix.net/catalog/services/cloudant) to Lite account users. 606 | 607 | [Path parameters](https://github.com/apache/incubator-openwhisk/blob/master/docs/apigateway.md#exposing-multiple-web-actions) can be used to bind actions to HTTP endpoints with templated URLs. 608 | 609 | [Packages](https://github.com/apache/incubator-openwhisk/blob/master/docs/packages.md#creating-a-package) can be used to bind shared configuration packages between multiple packages. 610 | 611 | #### Architecture 612 | 613 | This serverless application will have five actions: `list_todos`, `create_todo`, `retrieve_todo`, `update_todo` and `delete_todo`. 614 | 615 | Action will be exposed through the following HTTP endpoint operations: 616 | 617 | - `GET /api/todos` -> `list_todos`: returns list of all todos. 618 | - `POST /api/todos` -> `create_todo`: create new todo from `text`. 619 | - `GET /api/todos/{id}` -> `retrieve_todo`: return todo with `id`. 620 | - `PUT /api/todos/{id}` -> `update_todo`: update todo `text` and `completed` with `id`. 621 | - `DELETE /api/todos/{id}` -> `delete_todo`: delete todo with `id`. 622 | 623 | Todos are referenced by the unique identifier generated by the service during creation. Todos have two body fields: `text` with the todo item text and `completed` a boolean to record completion. 624 | 625 | #### Tests 626 | 627 | Retrieve the empty list of existing todos. 628 | 629 | ``` 630 | $ curl -X GET https://${API_GW_SERVICE}/api/todos 631 | [] 632 | ``` 633 | 634 | Create a new todo item. 635 | 636 | ``` 637 | $ curl -X POST -d '{"text":"clean the house"}' -H "Content-Type: application/json" https://${API_GW_SERVICE}/api/todos 638 | {"text":"clean the house", "id": 12345, completed: false} 639 | ``` 640 | 641 | Retrieve the new list of existing todos. 642 | 643 | ``` 644 | $ curl -X GET https://${API_GW_SERVICE}/api/todos 645 | [ {"text":"clean the house", "id": 12345, completed: false} ] 646 | ``` 647 | 648 | Retrieve the new todo item. 649 | 650 | ``` 651 | curl -X GET https://${API_GW_SERVICE}/api/todos/12345 652 | {"text":"clean the house", "id": 12345, completed: false} 653 | ``` 654 | 655 | Complete the todo item. 656 | 657 | ``` 658 | $ curl -X PUT -d '{"text":"clean the house", completed: true} ' -H "Content-Type: application/json" https://${API_GW_SERVICE}/api/todos 659 | {"text":"clean the house", "id": 12345, completed: true} 660 | ``` 661 | 662 | Retrieve the completed todo. 663 | 664 | ``` 665 | $ curl -X GET https://${API_GW_SERVICE}/api/todos/12345 666 | {"text":"clean the house", "id": 12345, completed: true} 667 | ``` 668 | 669 | Remove the completed todo. 670 | 671 | ``` 672 | $ curl -X DELETE https://${API_GW_SERVICE}/api/todos/12345 673 | ``` 674 | 675 | Retrieve the empty list of remaining todos. 676 | 677 | ``` 678 | $ curl -X GET https://${API_GW_SERVICE}/api/todos 679 | [] 680 | ``` -------------------------------------------------------------------------------- /bootcamp/ex4 - exposing APIs from actions/images/api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex4 - exposing APIs from actions/images/api-keys.png -------------------------------------------------------------------------------- /bootcamp/ex4 - exposing APIs from actions/images/apis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex4 - exposing APIs from actions/images/apis.gif -------------------------------------------------------------------------------- /bootcamp/ex4 - exposing APIs from actions/images/auth-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex4 - exposing APIs from actions/images/auth-on.png -------------------------------------------------------------------------------- /bootcamp/ex4 - exposing APIs from actions/images/rate-limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex4 - exposing APIs from actions/images/rate-limit.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/README.md: -------------------------------------------------------------------------------- 1 | # ibm cloud functions web ui 2 | 3 | This exercise will introduce the [IBM Cloud Functions Web UI](https://console.bluemix.net/openwhisk/). This application helps to manage your IBM Cloud Functions applications from a web browser, rather than using the command-line. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Understood how to find and use the IBM Cloud Functions Web UI.** 8 | 9 | Once this exercise is finished, you will be able to use the web ui to build and manage serverless applications on IBM Cloud! 10 | 11 | ## Table Of Contents 12 | 13 | * [Background](#background) 14 | * [Navigating To The IBM Cloud Functions Homepage](#navigating-to-the-ibm-cloud-functions-homepage) 15 | * [Actions](#actions) 16 | * [Details Overview](#details-overview) 17 | * [Invoking Actions](#invoking-actions) 18 | * [Creating Actions](#creating-actions) 19 | * [Triggers](#triggers) 20 | - [Details Overview](#details-overview) 21 | - [Creating Triggers](#creating-triggers) 22 | * [Monitoring](#monitoring) 23 | * [APIs](#apis) 24 | - [Details Overview](#details-overview) 25 | - [Creating APIs](#creating-apis) 26 | 27 | ## Instructions 28 | 29 | ### Background 30 | 31 | IBM Cloud Functions comes with a Web UI to help developers manage their serverless applications. Common development tasks such as creating actions, monitoring invocations, setting up triggers and more can all be achieved using this web application. The web application is custom to IBM Cloud and not part of the open-source Apache OpenWhisk project. 32 | 33 | It can often be quicker to use the Web UI for certain development tasks, rather than typing repetitive CLI commands. The web UI integrates with the IBM Cloud interface, making it easy to provision and connect new cloud services to your applications. 34 | 35 | In this exercise, we'll show you the different features of the IBM Cloud Web UI…. 36 | 37 | ### Navigating To The IBM Cloud Functions Homepage 38 | 39 | 1. Open the [IBM Cloud homepage](https://console.bluemix.net). 40 | 41 | 2. Click to show the menu on the left-hand side. 42 | 43 | 3. Click "Functions" in the list to open the [IBM Cloud Functions homepage](https://console.bluemix.net/openwhisk/). 44 | 45 | ![](images/homepage.gif) 46 | 47 | ### Actions 48 | 49 | 1. Select "Actions" from the left-hand menu panel on the homepage. 50 | 51 | [This page](https://console.bluemix.net/openwhisk/actions) is the management page for actions. It shows actions within the chosen region, org and space. 52 | 53 | ![action details page](images/action-overview.png) 54 | 55 | 2. Select an action from the page to move to the action details page. 56 | 57 | #### Details Overview 58 | 59 | The action details page will show properties for the chosen action. 60 | 61 | For supported runtimes, action source code is shown in an editor which allows users to make changes live. 62 | 63 | ![action details page](images/action-editor.png) 64 | 65 | Using the menu on the left-hand side, different properties for the action can be accessed and modified. 66 | 67 | - *"Code"* - shows action source code in editor. 68 | - *"Parameters"* - shows default parameters for the action. 69 | - *"Runtime"* - shows the action runtime, timeout value and memory limit. 70 | - *"Endpoints"* - allows you to expose the action as web action. 71 | - *"Connected Triggers"* - shows the triggers action is connected to. 72 | - *"Enclosing Sequences"* - shows sequences which use this action. 73 | 74 | #### Invoking Actions 75 | 76 | 1. Click the "Invoke" button to invoke an action and display the resulting activation record. 77 | 78 | *Input parameters to invocations can be modified using the "Change Input" button.* 79 | 80 | ![Invoking an action](images/invoking-action.gif) 81 | 82 | #### Creating Actions 83 | 84 | From the [action overview page](https://console.bluemix.net/openwhisk/actions), new actions can be created by providing the source code through the browser-based editor. 85 | 86 | 1. Select the "Create" button from the page. 87 | 2. Choose "Create Action" from the list. 88 | 3. Fill in the "Action name" and choose the "Runtime". 89 | 4. Click "Create" 90 | 5. Fill in the editor with your action source code. 91 | 92 | ![Creating an action](images/creating-action.gif) 93 | 94 | ### Triggers 95 | 96 | 1. Select "Triggers" from the left-hand menu panel on the homepage. 97 | 98 | [This page](https://console.bluemix.net/openwhisk/triggers) is the management page for triggers. It shows triggers within the chosen region, org and space. 99 | 100 | ![Triggers Overview Page](images/triggers-overview.png) 101 | 102 | 2. Select a trigger from the page to move to the trigger details page. 103 | 104 | #### Details Overview 105 | 106 | The trigger details page will show properties for the chosen trigger. 107 | 108 | ![Triggers Overview Page](images/trigger-details.png) 109 | 110 | Using the menu on the left-hand side, different properties for the trigger can be accessed and modified. 111 | 112 | - *"Connected Actions"* - shows the actions this trigger is connected to. 113 | - *"Parameters"* - shows default parameters for the action. 114 | - *"Endpoints"* - show details on how to fire this trigger remotely. 115 | 116 | #### Creating Triggers 117 | 118 | From the [trigger overview page](https://console.bluemix.net/openwhisk/triggers), new triggers can be created. 119 | 120 | 1. Select the "Create" button from the page. 121 | 2. Choose "Create Trigger" from the list. 122 | 3. Choose "Trigger type" as "Custom Trigger" 123 | 4. Fill in "Trigger Name" and "Description" 124 | 5. Click "Create" 125 | 126 | ![](images/create-trigger.gif) 127 | 128 | ### Monitoring 129 | 130 | IBM Cloud Functions Web UI comes with a [comprehensive visualisation dashboard](https://console.bluemix.net/openwhisk/dashboard) for monitoring serverless applications. 131 | 132 | This dashboard shows activations within a region, org and space. Developers can see activation results, invocation times and logging output through the dashboard. Activations displayed can be filtered by name or time window. 133 | 134 | ![](images/monitoring.png) 135 | 136 | ### APIs 137 | 138 | HTTP endpoints for web actions can be created and managed through the IBM Cloud Functions Web UI. Using this interface is often a lot more intuitive than using the CLI tool for managing more complex APIs. 139 | 140 | 1. Select "APIs" from the left-hand menu panel on the homepage. 141 | 142 | ![API homepage](images/apis-homepage.png) 143 | 144 | #### Details Overview 145 | 146 | The API details page will show properties for the chosen API, including an API monitoring page showing invocations. 147 | 148 | ![API homepage](images/api-details.png) 149 | 150 | Using the menu on the left-hand side, different properties for the API can be accessed and modified. 151 | 152 | - *"Summary"* - API overview page and monitoring dashboard for API invocations. 153 | - *"Definition"* - API configuration properties, allows updating properties live. 154 | - *"Sharing"* - Configure exposing API with our internal users. 155 | - *"API Explorer"* - API documentation for your endpoints. 156 | 157 | #### Creating APIs 158 | 159 | 1. Click the "Create API" from the [APIs homepage](https://console.bluemix.net/openwhisk/apimanagement). 160 | 161 | In this page, API details can either be filled out manually or imported from an existing Swagger file. 162 | 163 | 2. Fill out the "API name" field as "myapi" 164 | 3. Fill out the "Base path for API" field as "/api" 165 | 166 | 167 | 4. Click on the "Create Operation" button to add new HTTP endpoints to this API. 168 | 5. Fill out the "Path" field as "/hello" 169 | 6. Choose an action from the drop-down list. 170 | 7. Click the "Create" button. 171 | 172 | Using the API management create page, security, rate limiting, CORS or oauth support can be configured. 173 | 174 | 8. Click the "Save" button to create your API. 175 | 176 | ![Creating an API](images/create-apis.gif) -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/action-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/action-editor.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/action-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/action-overview.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/api-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/api-details.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/apis-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/apis-homepage.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/create-apis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/create-apis.gif -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/create-trigger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/create-trigger.gif -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/creating-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/creating-action.gif -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/homepage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/homepage.gif -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/invoking-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/invoking-action.gif -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/monitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/monitoring.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/trigger-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/trigger-details.png -------------------------------------------------------------------------------- /bootcamp/ex5 - ibm cloud functions web ui/images/triggers-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex5 - ibm cloud functions web ui/images/triggers-overview.png -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/README.md: -------------------------------------------------------------------------------- 1 | # building a weather bot 2 | 3 | This exercise shows you how to build a weather bot for Slack. Combining actions and sequence with webhooks, this bot will allow you to get forecasts for locations on demand. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Implemented Weather Bot using independent multiple actions.** 8 | - **Exposed service as public HTTP endpoint.** 9 | - **Integrated with Slack using webhooks.** 10 | - **Connected to alarm trigger for recurring forecast.** 11 | 12 | Once this exercise is finished, you will be able to creating Slack bots using IBM Cloud Functions! 13 | 14 | ## Table Of Contents 15 | 16 | * [Background](#background) 17 | * [Actions](#actions) 18 | * [Address To Location Service](#address-to-location-service) 19 | * [Forecast From Location Service](#forecast-from-location-service) 20 | * [Sending Messages To Slack](#sending-messages-to-slack) 21 | * [Creating Weather Bot Using Sequences](#creating-weather-bot-using-sequences) 22 | * [Connecting To Slack](#connecting-to-slack) 23 | * [Exposing API for Weather Bot](#exposing-api-for-weather-bot) 24 | * [Slack Outgoing Webhook](#slack-outgoing-webhook) 25 | * [Connecting To Triggers](#connecting-to-triggers) 26 | * [Setting Up A Morning Forecast](#setting-up-a-morning-forecast) 27 | 28 | ## Instructions 29 | 30 | ### Background 31 | 32 | This exercise creates a bot for Slack that helps users with weather forecasts. Users can ask the bot for the forecast for a specific location by sending a chat message. The bot can also be configured to send the forecast for a location at regular intervals, e.g. everyday at 8am. 33 | 34 | Here's an example of weather bot in action… 35 | 36 | ![weather bot](./images/weather_bot.png) 37 | 38 | This bot needs to perform a few functions, e.g. convert addresses to locations, retrieve weather forecasts for locations and integrate with Slack. Rather than implementing the bot as a monolithic action, containing all the business logic for these features, we are going to create separate actions for each service. We can then use sequence to compose the "meta-action" from these independent services to create the bot. 39 | 40 | *Let's start by identifying the individual actions we need and creating them…* 41 | 42 | ### Actions 43 | 44 | #### Address To Location Service 45 | 46 | This service handles retrieving `latitude` and `longitude` coordinates for addresses using an external Geocoding API. 47 | 48 | The action (microservice) implements the application logic within a single function (`main`) just the way we have learned it earlier. The same way we did it before, parameters are passed in as an object argument (`params`) to the function call. The service makes an API call to the *Google* Geocoding API, returning the results from the calls' response for the first address match. 49 | 50 | Let's first create the action (name it `location_to_latlong`) using the following code. As said before, you can create it using the CLI or the OpenWhisk UI just the way you have learned it earlier: 51 | 52 | ##### Node.js 53 | 54 | ```javascript 55 | var request = require('request') 56 | 57 | function address (params) { 58 | return params.trigger_word 59 | ? params.text.slice(params.trigger_word.length).trim() : params.text 60 | } 61 | 62 | function main (params) { 63 | var options = { 64 | url: 'https://maps.googleapis.com/maps/api/geocode/json', 65 | qs: {address: address(params)}, 66 | json: true 67 | } 68 | 69 | return new Promise(function (resolve, reject) { 70 | request(options, function (err, resp) { 71 | if (err) { 72 | console.log(err) 73 | return reject({err: err}) 74 | } 75 | 76 | if (resp.body.status !== 'OK') { 77 | console.log(resp.body.status) 78 | return reject({err: resp.body.status}) 79 | } 80 | 81 | resolve(resp.body.results[0].geometry.location) 82 | }) 83 | }) 84 | } 85 | ``` 86 | 87 | ##### Swift 88 | 89 | ```swift 90 | import KituraNet 91 | import Foundation 92 | import SwiftyJSON 93 | 94 | func httpRequestOptions(address: String) -> [ClientRequest.Options] { 95 | let request: [ClientRequest.Options] = [ 96 | .method("GET"), 97 | .schema("https://"), 98 | .hostname("maps.googleapis.com"), 99 | .path("/maps/api/geocode/json?address=\(address)") 100 | ] 101 | 102 | return request 103 | } 104 | 105 | func addressToLocationJson (address: String) -> JSON? { 106 | var json: JSON = nil 107 | let req = HTTP.request(httpRequestOptions(address: address)) { resp in 108 | if let resp = resp, resp.statusCode == HTTPStatusCode.OK { 109 | do { 110 | var data = Data() 111 | try resp.readAllData(into: &data) 112 | json = JSON(data: data) 113 | } catch { 114 | print("Error \(error)") 115 | } 116 | } else { 117 | print(resp!.statusCode) 118 | print("Status error code or nil reponse received from geocoding server.") 119 | } 120 | } 121 | req.end() 122 | 123 | return json 124 | } 125 | 126 | func parseAddress(args: [String: Any]) -> String? { 127 | guard let text = args["text"] as? String else { 128 | return nil 129 | } 130 | 131 | if let trigger_word = args["trigger_word"] as? String { 132 | return String(text.characters.dropFirst(trigger_word.characters.count)).trimmingCharacters(in: .whitespacesAndNewlines) 133 | } 134 | 135 | return text 136 | } 137 | 138 | func main(args: [String:Any]) -> [String:Any] { 139 | guard let address = parseAddress(args: args) else { 140 | return [ "error": "Missing mandatory argument: address" ] 141 | } 142 | 143 | print("Searching for forecast in \(address)") 144 | guard let json = addressToLocationJson(address: address) else { 145 | return [ "error": "Unable to lookup location for address." ] 146 | } 147 | 148 | guard let location = json["results"][0]["geometry"]["location"].dictionaryObject else { 149 | return [ "error": "Location missing from results." ] 150 | } 151 | 152 | return location 153 | } 154 | ``` 155 | 156 | Let's deploy the action the way you learned it earlier: 157 | 158 | ``` 159 | $ ibmcloud wsk action create location_to_latlong location_to_latlong.xxx 160 | ok: created action location_to_latlong 161 | ``` 162 | 163 | Next, let's test the action: 164 | 165 | ``` 166 | $ ibmcloud wsk action invoke location_to_latlong -b -r -p text "London" 167 | { 168 | "lat": 51.5073509, 169 | "lng": -0.1277583 170 | } 171 | ``` 172 | 173 | #### Forecast From Location Service 174 | 175 | Now, let's implement the service for finding forecasts for locations. 176 | 177 | This service uses an external API to retrieve weather forecasts for locations, returning the text description for weather in the next twenty fours hours. 178 | 179 | **Please ask the workshop coordinator for the temporary API keys to use this service.** 180 | 181 | Again, let's create the action (name it `forecast_from_latlong`) using the following code: 182 | 183 | ##### Node.js 184 | 185 | ```javascript 186 | var request = require('request'); 187 | 188 | function main(params) { 189 | if (!params.lat) return Promise.reject("Missing latitude"); 190 | if (!params.lng) return Promise.reject("Missing longitude"); 191 | if (!params.username || !params.password) return Promise.reject("Missing credentials"); 192 | 193 | var url = "https://twcservice.mybluemix.net/api/weather/v1/geocode/"+params.lat+"/"+params.lng+"/forecast/daily/3day.json"; 194 | var options = { 195 | url: url, 196 | json: true, 197 | auth: { 198 | user: params.username, 199 | password: params.password 200 | } 201 | }; 202 | 203 | return new Promise(function (resolve, reject) { 204 | request(options, function (err, resp) { 205 | if (err) { 206 | return reject({err: err}) 207 | } 208 | 209 | resolve({text: resp.body.forecasts[0].narrative}); 210 | }); 211 | }); 212 | } 213 | ``` 214 | 215 | ##### Swift 216 | 217 | ```swift 218 | import KituraNet 219 | import Foundation 220 | import SwiftyJSON 221 | 222 | func httpRequestOptions(auth: (username: String, password: String), location: (lat: Double, lng: Double)) -> [ClientRequest.Options] { 223 | let request: [ClientRequest.Options] = [ 224 | .method("GET"), 225 | .schema("https://"), 226 | .hostname("twcservice.mybluemix.net"), 227 | .path("/api/weather/v1/geocode/\(location.lat)/\(location.lng)/forecast/daily/3day.json"), 228 | .username(auth.username), 229 | .password(auth.password) 230 | ] 231 | 232 | return request 233 | } 234 | 235 | func forecastForLocationJson(auth: (String, String), location: (Double, Double)) -> JSON? { 236 | var json: JSON = nil 237 | let req = HTTP.request(httpRequestOptions(auth: auth, location: location)) { resp in 238 | if let resp = resp, resp.statusCode == HTTPStatusCode.OK { 239 | do { 240 | var data = Data() 241 | try resp.readAllData(into: &data) 242 | json = JSON(data: data) 243 | } catch { 244 | print("Error \(error)") 245 | } 246 | } else { 247 | print("Status error code or nil reponse received from geocoding server.") 248 | } 249 | } 250 | req.end() 251 | 252 | return json 253 | } 254 | 255 | func parseLocation(args: [String:Any]) -> (Double, Double)? { 256 | guard let lat = args["lat"] as? Double else { 257 | return nil 258 | } 259 | 260 | guard let lng = args["lng"] as? Double else { 261 | return nil 262 | } 263 | 264 | return (lat, lng) 265 | } 266 | 267 | func parseAuthCredentials(args: [String:Any]) -> (String, String)? { 268 | guard let username = args["username"] as? String else { 269 | return nil 270 | } 271 | 272 | guard let password = args["password"] as? String else { 273 | return nil 274 | } 275 | 276 | return (username, password) 277 | } 278 | 279 | func main(args: [String:Any]) -> [String:Any] { 280 | guard let location = parseLocation(args: args) else { 281 | return [ "error": "Missing mandatory location arguments: lat, lng" ] 282 | } 283 | 284 | guard let auth = parseAuthCredentials(args: args) else { 285 | return [ "error": "Missing mandatory authentication arguments: username, password" ] 286 | } 287 | 288 | guard let json = forecastForLocationJson(auth: auth, location: location) else { 289 | return [ "error": "Unable to lookup forecast for location." ] 290 | } 291 | 292 | guard let forecast = json["forecasts"][0]["narrative"].string else { 293 | return [ "error": "Narrative forecast missing from results." ] 294 | } 295 | 296 | return ["text": forecast] 297 | } 298 | ``` 299 | 300 | Notice that the service expects four parameters, `lat` and `lng` coordinates along with the `API credentials` (the ones you noted down before). Passing in `API credentials` as parameters means you don't have to embed them within the code and can change them dynamically at runtime. 301 | 302 | Let's deploy this service and verify it's working… 303 | 304 | ``` 305 | $ ibmcloud wsk action create forecast_from_latlong forecast_from_latlong.xyz 306 | ok: created action forecast_from_latlong 307 | $ ibmcloud wsk action invoke forecast_from_latlong -p lat "51.50" -p lng "-0.12" -p username $WEATHER_USER -p password $WEATHER_PASS -b -r 308 | { 309 | "text": "Partly cloudy. Lows overnight in the low 60s." 310 | } 311 | ``` 312 | 313 | Yep, looks good. 314 | 315 | We don't want to pass in the API credentials with every request, so let's bind them as default parameters to the action. This means we only need to invoke the service with the latitude and longitude parameters, which matches the output from the previous service. 316 | 317 | ``` 318 | $ ibmcloud wsk action update forecast_from_latlong -p username $WEATHER_USER -p password $WEATHER_PASS 319 | ok: updated action forecast_from_latlong 320 | $ ibmcloud wsk action invoke forecast_from_latlong -p lat "51.50" -p lng "-0.12" -b -r 321 | { 322 | "text": "Partly cloudy. Lows overnight in the low 60s." 323 | } 324 | ``` 325 | 326 | Okay, great, that's the first two services working. 327 | 328 | #### Sending Messages To Slack 329 | 330 | Once we have a forecast, we need to send it to Slack as a message from our bot. Slack provides an easy method for writing simple bots using their webhook integration. [Incoming Webhooks](https://api.slack.com/incoming-webhooks) provide applications with URLs to send data to using normal HTTP requests. The contents of the JSON request body will be posted into the channel as a bot message. 331 | 332 | **Do you have access to a Slack team you can use for testing?** 333 | 334 | *If not, please visit open and click the `Create new team` link at the very top of the screen. Follow the instructions to create a new Slack team you can use with the weather bot.* 335 | 336 | We now need to configure a webhook to post messages from Slack to our bot. 337 | 338 | 1. Create a new [Incoming Webhook Integration](https://my.slack.com/services/new/incoming-webhook/) for your channel and copy the URL provided by Slack to use with our bot. 339 | ![Incoming Webhooks](./images/incoming.png) 340 | 341 | Once we have the webhook endpoint, we could write another action to send forecasts to that URL but OpenWhisk comes with integrations for a number of third-party systems meaning we don't have to! 342 | 343 | These integrations are available as _packages_, which bundle Actions and Trigger Feeds and make them available to all users in the system. We can see what packages are available using the following command... 344 | 345 | ``` 346 | $ bx wsk package list /whisk.system 347 | packages 348 | /whisk.system/samples shared 349 | /whisk.system/watson-textToSpeech shared 350 | /whisk.system/watson-translator shared 351 | /whisk.system/watson-speechToText shared 352 | /whisk.system/combinators shared 353 | /whisk.system/github shared 354 | /whisk.system/utils shared 355 | /whisk.system/websocket shared 356 | /whisk.system/slack shared 357 | /whisk.system/weather shared 358 | /whisk.system/pushnotifications shared 359 | /whisk.system/alarms shared 360 | /whisk.system/cloudant shared 361 | /whisk.system/messaging shared 362 | ``` 363 | 364 | Looks like there's a Slack integration already. Retrieving the package summary will tell us more. 365 | 366 | ``` 367 | $ ibmcloud wsk package get --summary /whisk.system/slack 368 | package /whisk.system/slack: This package interacts with the Slack messaging service 369 | (parameters: channel, token, url, username) 370 | action /whisk.system/slack/post: Post a message to Slack 371 | (parameters: text) 372 | ``` 373 | 374 | The `/whisk.system/slack/post` action allows us to post messages to Slack without writing any code. 375 | 376 | ``` 377 | $ ibmcloud wsk action invoke /whisk.system/slack/post -p text "Hello" -p channel $CHANNEL -p url $WEBHOOK_URL 378 | ok: invoked /whisk.system/slack/post with id 78070fe2acb54c70ae49c0fa047aee51 379 | ``` 380 | 381 | Seeing the message pop-up in our Slack channel verifies this works. 382 | 383 | We need to bind default parameters to configure the URL, channel, bot name and icon. This requires us to copy the action to over local workspace. 384 | 385 | ``` 386 | $ ibmcloud wsk action create --copy webhook /whisk.system/slack/post -p url $WEBHOOK -p channel $CHANNEL -p username "Weather Bot" -p icon_emoji ":sun_with_face:" 387 | ok: created action webhook 388 | ``` 389 | 390 | This customised Slack service can be invoked with just the _text_ parameter and gives us a friendly bot message in the #weather channel. 391 | 392 | ``` 393 | $ ibmcloud wsk action invoke webhook -p text "Hello" 394 | ``` 395 | 396 | ![Test Bot](./images/test_bot.png) 397 | 398 | ### Creating Weather Bot Using Sequences 399 | 400 | Right, we have the three microservices to handle the logic in our bot. How can we join them together to create our application? 401 | 402 | ***OpenWhisk comes with a feature to help with this, called Sequences.*** 403 | 404 | Sequences allow you to define actions that are composed by executing other actions in order, passing the output from one service as the input to the next. This is a bit like Unix pipes, where a single command executes multiple commands and pipes the output through the commands. 405 | 406 | Let's define a new sequence for our bot to join these services together. 407 | 408 | ``` 409 | $ ibmcloud wsk action create location_forecast --sequence location_to_latlong,forecast_from_latlong,webhook 410 | ok: created action location_forecast 411 | ``` 412 | 413 | With this meta-service defined, we can invoke the `location_forecast` action with the input parameter for the first service (`text`). As a result the forecast for that location should appear in *Slack*. 414 | 415 | Let's test this. The result should look similar to this: 416 | 417 | ``` 418 | $ ibmcloud wsk action invoke location_forecast -p text "London" -b 419 | ok: invoked location_forecast with id d63b40bb36c54cfbaf8262b6f7e5c2e9 420 | { 421 | "activationId": "d63b40bb36c54cfbaf8262b6f7e5c2e9", 422 | "annotations": [], 423 | "end": 1473179750086, 424 | "logs": [], 425 | "name": "location_forecast", 426 | "namespace": "andreas.nauerz@de.ibm.com", 427 | "publish": false, 428 | "response": { 429 | "result": {}, 430 | "status": "success", 431 | "success": true 432 | }, 433 | "start": 1473179746914, 434 | "subject": "user@host.com", 435 | "version": "0.0.1" 436 | } 437 | ``` 438 | 439 | ![](./images/london.png) 440 | 441 | ### Connecting To Slack 442 | 443 | How can users ask weather bot for forecasts about a location? 444 | 445 | Slack provides [Outgoing Webhooks](https://api.slack.com/outgoing-webhooks) that will post JSON messages to external URLs when keywords appear in channel messages. Setting up a new outgoing webhook for our channel will allow users to say "weather london" and have the bot respond. 446 | 447 | #### Exposing API for Weather Bot 448 | 449 | Exposing the Weather Bot service as an public API endpoint allows us to handle webhook notifications from Slack. 450 | 451 | 1. Update the `location_forecast` action to be a Web Action. 452 | 453 | ``` 454 | $ ibmcloud wsk action update location_forecast --web true 455 | ok: updated action location_forecast 456 | ``` 457 | 458 | 2. Create a new API endpoint (`/api/bot`) for this service that responds to POST requests. 459 | 460 | ``` 461 | $ ibmcloud wsk api create /api/bot POST location_forecast -n "weather bot" 462 | ok: created API /api/bot POST for action /_/location_forecast 463 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/bot 464 | ``` 465 | 466 | 3. Test the API endpoint with an example message. 467 | 468 | ``` 469 | $ curl -XPOST "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//api/bot?text=london" 470 | ``` 471 | 472 | Weather bot should post the forecast into your channel! 473 | 474 | ![](./images/london.png) 475 | 476 | #### Slack Outgoing Webhook 477 | 478 | Hence, we need to make sure that our action is being properly invoked once a message starting with a defined trigger word is being send via the `weather` channel we have created prior. 479 | 480 | 1. Browse to the "[App Directory](https://ets-hursley.slack.com/apps)" for your Slack team. 481 | 2. Search for "Outgoing WebHooks" 482 | 3. Click "Add Configuration" and then "Add Outgoing WebHooks integration" 483 | 4. Under the "Integration Settings" panel, select the following settings. 484 | - *Channel* -> Choose an existing channel 485 | - *Trigger word(s)* -> "weather" 486 | - *URL(s)* -> Weather Bot API URL. 487 | 5. Click "Saving Settings" 488 | 489 | ![](./images/outgoing.png) 490 | 491 | With the webhook integration configured, enter the channel you selected and type the following message: `weather london`. The bot should now respond with the current forecast! 492 | 493 | ![weather bot](./images/weather_bot.png) 494 | 495 | Test it out with different locations like `weather new york` or `weather san francisco`. 496 | 497 | ### Connecting To Triggers 498 | 499 | Triggers are used to represent event streams from the external world into OpenWhisk. They can be invoked manually, through the REST API, or automatically, after connecting to trigger feeds. 500 | 501 | Actions can be bound to Triggers using Rules. When a Trigger is fired, the Action is invoked with the request parameters. Multiple Actions can listen to the same Trigger. 502 | 503 | Let's now look at binding the bot service to a sample trigger and invoke it indirectly by firing that trigger: 504 | 505 | ``` 506 | $ ibmcloud wsk trigger create forecast 507 | ok: created trigger forecast 508 | 509 | $ ibmcloud wsk rule create forecast_rule forecast location_forecast 510 | ok: created rule forecast_rule 511 | 512 | $ ibmcloud wsk trigger fire forecast -p text "London" 513 | ok: triggered forecast with id 49914a20416d416d8c90282d59eebee3 514 | ``` 515 | 516 | Once we fired the trigger, passing in the `text` parameter, the bot was automatically invoked and posted the forecast for `London` to the channel. 517 | 518 | Now let's look at invoking the bot every morning to tell us the forecast before we set off for work. 519 | 520 | #### Setting Up A Morning Forecast 521 | 522 | Triggers can be registered to listen to exteral event sources, like messages on a queue or updates to a database. 523 | 524 | Whenever a new external event occurs, the trigger will be fired automatically. If we have Actions bound via Rules to those Triggers, they will also be invoked. 525 | 526 | Triggers bind to external event sources during creation by passing in a reference to the external trigger feed to connect to. OpenWhisk's public packages contain a number of trigger feeds that we can use for external event sources. 527 | 528 | One of those public trigger feeds is in the Alarm package. This alarm feed executes triggers at pre-specified intervals. Using this feed with our weather bot trigger, we could set it up to execute every morning for a particular address and tell us the forecast every for London before we set off for work. 529 | 530 | Let's do that now... 531 | 532 | ``` 533 | $ ibmcloud wsk package get /whisk.system/alarms --summary 534 | package /whisk.system/alarms: Alarms and periodic utility 535 | (parameters: cron trigger_payload) 536 | feed /whisk.system/alarms/alarm: Fire trigger when alarm occurs 537 | 538 | $ ibmcloud wsk trigger create regular_forecast --feed /whisk.system/alarms/alarm -p cron "*/10 * * * * *" -p trigger_payload "{\"text\":\"London\"}" 539 | ok: created trigger feed regular_forecast 540 | 541 | $ ibmcloud wsk rule create regular_forecast_rule regular_forecast location_forecast 542 | ok: created rule regular_forecast_rule 543 | ``` 544 | 545 | The trigger schedule is provided by the `cron` parameter, which we've set up to run every ten seconds to test it out. By binding this new trigger to our bot service, the forecast for London starts to appear in the channel. 546 | 547 | Okay, that's great but let's turn off this alarm before it drives us mad: 548 | 549 | ``` 550 | $ ibmcloud wsk rule disable regular_forecast_rule 551 | ok: rule regular_forecast_rule is inactive 552 | ``` -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/images/incoming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex6 - building a weather bot/images/incoming.png -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/images/london.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex6 - building a weather bot/images/london.png -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/images/outgoing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex6 - building a weather bot/images/outgoing.png -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/images/test_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex6 - building a weather bot/images/test_bot.png -------------------------------------------------------------------------------- /bootcamp/ex6 - building a weather bot/images/weather_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex6 - building a weather bot/images/weather_bot.png -------------------------------------------------------------------------------- /bootcamp/ex7 - using the serverless framework/README.md: -------------------------------------------------------------------------------- 1 | # using the serverless framework 2 | 3 | This exercise shows you use IBM Cloud Functions with The Serverless Framework. The Serverless Framework is the most popular framework for building serverless applications. It manages the build, packaging, configuration and deployment process for serverless applications and their resources. 4 | 5 | *Once you have completed this exercise, you will have…* 6 | 7 | - **Installed The Serverless Framework and OpenWhisk provider plugin.** 8 | - **Understood how to create, deploy and test projects from templates.** 9 | - **Modified projects to add actions and connect actions to event sources.** 10 | 11 | Once this exercise is finished, you will be able to use a popular development framework for creating serverless applications! 12 | 13 | ## Table Of Contents 14 | 15 | * [Background](#background) 16 | * [Install The Serverless Framework](#installing-the-serverless-framework) 17 | * [Creating Project Templates](#creating-project-templates) 18 | * [Serverless Framework Files](#serverless-framework-files) 19 | * [Deploying Projects](#deploying-projects) 20 | * [Testing Actions](#testing-actions) 21 | * [Adding Actions](#adding-actions) 22 | * [Connecting API Endpoints](#connecting-api-endpoints) 23 | * [Working with Triggers and Rules](#working-with-triggers-and-rules) 24 | * [Deploying Pre-compiled Binaries (Swift)](#deploying-pre-compiled-binaries-(swift)) 25 | * [Project Layout](#project-layout) 26 | * [serverless.yml](#serverless.yml) 27 | * [package.json](#package.json) 28 | * [Deploying](#deploying) 29 | 30 | ## Instructions 31 | 32 | ### Background 33 | 34 | *[The Serverless Framework](https://serverless.com/framework/)* is the most popular open-source framework for building serverless applications. Launching back in 2015, the framework has experienced tremendous growth and now has over twenty thousands stars on [Github](https://github.com/serverless/serverless). 35 | 36 | ![The Serverless Framework](./images/framework.png) 37 | 38 | Thousands of developers are using the tool to build serverless applications every day. 39 | 40 | Using a [simple manifest file](https://serverless.com/framework/docs/providers/openwhisk/guide/serverless.yml/), developers can define serverless functions, connect them to event sources and declare cloud services needed by their application. 41 | 42 | ```yaml 43 | service: swift-service 44 | 45 | provider: 46 | name: openwhisk 47 | runtime: swift 48 | 49 | functions: 50 | ping: 51 | handler: ping.main 52 | 53 | plugins: 54 | - serverless-openwhisk 55 | ``` 56 | 57 | The framework handles deploying these serverless applications to the cloud provider. It also allows developers to monitor services in production, roll-out updates and assist debugging issues. 58 | 59 | It also has a [vibrant ecosystem](https://github.com/serverless/serverless#plugins-v10) of third-party plugins to extend the functionality of the framework. 60 | 61 | With the aforementioned integration developers using the framework can now choose to deploy their serverless applications to any Apache OpenWhisk platform instance. Multi-provider support also means moving applications between platforms is much easier and developers can even develop multi-cloud serverless applications. 62 | 63 | ### Installing The Serverless Framework 64 | 65 | The framework is a Node.js [CLI application](https://www.npmjs.com/package/serverless) which can be installed with the [Node.js Package Manager](https://www.npmjs.com/) (NPM). Installing the project as a [global package](https://docs.npmjs.com/getting-started/installing-npm-packages-globally) makes the CLI tool (`serverless`) available as a terminal command. 66 | 67 | 1. Install Node.js and NPM if you don't have these runtimes installed. 68 | - https://github.com/creationix/nvm 69 | 2. Install The Serverless Framework with the OpenWhisk provider plugin. 70 | 71 | ``` 72 | $ npm install --global serverless 73 | ... 74 | + serverless@1.27.3 75 | added 339 packages in 11.545s 76 | ``` 77 | 78 | 3. Check the CLI tool is installed and working. 79 | 80 | ``` 81 | $ serverless --version 82 | 1.27.3 83 | ``` 84 | 85 | ### Creating Project Templates 86 | 87 | The Serverless Framework supports bootstrapping new serverless application from existing templates. The frameworks provides numerous templates for each serverless platform and runtime. Templates can be deployed without modification. 88 | 89 | *Templates for the following runtimes with Apache OpenWhisk are available:* `nodejs`, `php`, `python` and `swift`. 90 | 91 | Let's try out one of the OpenWhisk templates to show you the workflow when using the framework. 92 | 93 | - Use the `create` command to create a new service from one of the templates. 94 | 95 | ``` 96 | $ serverless create --template openwhisk-nodejs --path my_service 97 | // OR 98 | $ serverless create --template openwhisk-swift --path my_service 99 | ``` 100 | 101 | - Look at the templates files in the new `my_service` directory. 102 | 103 | ``` 104 | $ cd my_service; tree . 105 | . 106 | ├── README.md 107 | ├── package.json 108 | ├── ping.swift 109 | └── serverless.yml 110 | 111 | 0 directories, 4 files 112 | ``` 113 | 114 | - Install project dependencies using NPM. 115 | 116 | ``` 117 | $ npm install 118 | `-- serverless-openwhisk@0.13.0 119 | ... 120 | ``` 121 | 122 | #### Serverless Framework Files 123 | 124 | Application configuration for serverless functions and services is maintained in the `serverless.yml` file. Functions are defined in this file, along with event sources and the configuration parameters. 125 | 126 | Let's look at the example application defined by the template… 127 | 128 | ```yaml 129 | service: my_service 130 | 131 | provider: 132 | name: openwhisk 133 | runtime: swift 134 | 135 | functions: 136 | hello: 137 | handler: ping.main 138 | 139 | plugins: 140 | - serverless-openwhisk 141 | ``` 142 | 143 | This configuration defines a single action `hello` which uses the `main` function from the `ping.swift` file. 144 | 145 | ```swift 146 | func main(args: [String:Any]) -> [String:Any] { 147 | let formatter = DateFormatter() 148 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 149 | let now = formatter.string(from: Date()) 150 | 151 | if let name = args["name"] as? String { 152 | return [ "greeting" : "Hello \(name)! The time is \(now)" ] 153 | } else { 154 | return [ "greeting" : "Hello stranger! The time is \(now)" ] 155 | } 156 | } 157 | ``` 158 | 159 | #### Deploying Projects 160 | 161 | 1. Use the `deploy` command to deploy the serverless application using the framework. 162 | 163 | ``` 164 | $ serverless deploy 165 | ``` 166 | 167 | *If the deployment succeeds, the following messages will be printed to the console:* 168 | 169 | ``` 170 | Serverless: Packaging service... 171 | Serverless: Excluding development dependencies... 172 | Serverless: Compiling Functions... 173 | Serverless: Compiling API Gateway definitions... 174 | Serverless: Compiling Rules... 175 | Serverless: Compiling Triggers & Feeds... 176 | Serverless: Deploying Functions... 177 | Serverless: Deployment successful! 178 | 179 | Service Information 180 | platform: openwhisk.ng.bluemix.net 181 | namespace: _ 182 | service: my_service 183 | 184 | actions: 185 | my_service-dev-hello 186 | 187 | triggers: 188 | **no triggers deployed** 189 | 190 | rules: 191 | **no rules deployed** 192 | 193 | endpoints (api-gw): 194 | **no routes deployed** 195 | 196 | endpoints (web actions): 197 | **no web actions deployed** 198 | ``` 199 | 200 | #### Testing Actions 201 | 202 | The `invoke` command can be used to test actions once they have been deployed. 203 | 204 | 1. Test this out with the `hello` action you have just deployed… 205 | 206 | ``` 207 | $ serverless invoke --function hello 208 | { 209 | "greeting": "Hello stranger! The time is 2018-03-01 11:27:16" 210 | } 211 | ``` 212 | 213 | 2. Test again with parameters. 214 | 215 | ``` 216 | $ serverless invoke --function hello --data '{"name": "OpenWhisk"}' 217 | { 218 | "greeting": "Hello OpenWhisk! The time is 2018-03-01 11:27:26" 219 | } 220 | ``` 221 | 222 | ### Adding Actions 223 | 224 | If you want to add more actions to your project, create the source files and update the `serverless.yml`. 225 | 226 | 1. Add a new function `today` to `ping.swift` 227 | 228 | ```swift 229 | func today(args: [String:Any]) -> [String:Any] { 230 | let formatter = DateFormatter() 231 | formatter.dateFormat = "EEEE" 232 | let now = formatter.string(from: Date()) 233 | return [ "greeting" : "Today is \(now)!" ] 234 | } 235 | ``` 236 | 237 | 2. Add a new action to the `serverless.yml` configuration 238 | 239 | ```yaml 240 | service: my_service 241 | 242 | provider: 243 | name: openwhisk 244 | runtime: swift 245 | 246 | functions: 247 | hello: 248 | handler: ping.main 249 | today: 250 | handler: ping.today 251 | 252 | plugins: 253 | - serverless-openwhisk 254 | ``` 255 | 256 | 3. Deploy the updated service. 257 | 258 | ``` 259 | $ serverless deploy 260 | ... 261 | 262 | actions: 263 | my_service-dev-today my_service-dev-hello 264 | ``` 265 | 266 | 4. Invoke the `today` action. 267 | 268 | ``` 269 | $ serverless invoke -f today 270 | { 271 | "greeting": "Today is Thursday!" 272 | } 273 | ``` 274 | 275 | ### Connecting API Endpoints 276 | 277 | API endpoints can be defined directly in the `serverless.yml` configuration. The framework will configure the endpoints during the deployment process. Let's test this out now… 278 | 279 | 1. Open the `serverless.yml` file and define an API endpoint for the `hello` action. 280 | 281 | ```yaml 282 | functions: 283 | hello: 284 | handler: ping.main 285 | events: 286 | - http: GET /api/hello 287 | ``` 288 | 289 | 2. Redeploy the service. 290 | 291 | ``` 292 | $ serverless deploy 293 | ... 294 | endpoints (api-gw): 295 | GET https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//my_service/api/hello --> my_service-dev-hello 296 | 297 | endpoints (web actions): 298 | https://openwhisk.ng.bluemix.net/api/v1/web/user@host.com_dev/default/my_service-dev-hello 299 | ``` 300 | 301 | 3. Test the HTTP endpoint listed from the deploy command output. 302 | 303 | ``` 304 | $ curl https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api//my_service/api/hello 305 | { 306 | "greeting": "Hello stranger! The time is 2018-03-01 11:55:19" 307 | } 308 | ``` 309 | 310 | ### Working with Triggers and Rules 311 | 312 | Actions can be connected to other event sources using the same configuration section (`events`) in the `serverless.yml` file. 313 | 314 | Here's an example of some of the other [event sources](https://serverless.com/framework/docs/providers/openwhisk/events/) that are supported by the framework. 315 | 316 | ```yaml 317 | functions: 318 | [...] 319 | hello: 320 | handler: hello.handler 321 | events: 322 | - http: GET /api-demo/hello 323 | - schedule: cron(* * * * *) 324 | - trigger: triggerName 325 | - cloudant: 326 | host: xxx-yyy-zzz-bluemix.cloudant.com 327 | username: USERNAME 328 | password: PASSWORD 329 | db: db_name 330 | - message_hub: 331 | topic: my_kafka_topic 332 | brokers: afkaprod01.messagehub.services.us-south.bluemix.net:9093 333 | user: USERNAME 334 | password: PASSWORD 335 | ``` 336 | 337 | ### Deploying Pre-compiled Binaries (Swift) 338 | 339 | If we want to deploy pre-compiled binaries for Swift actions, rather than source files, to improve performance or add external libraries, the framework can automate this process. 340 | 341 | This [example project](https://github.com/serverless/examples/tree/master/openwhisk-swift-precompiled-binaries) shows you how to set up a project to generate binaries, compiled for the correct architecture, prior to deployment. Let's explain how this work… 342 | 343 | 1. Checkout the Git repository and enter the project template folder. 344 | 345 | ``` 346 | $ git clone git@github.com:serverless/examples.git 347 | $ cd examples/openwhisk-swift-precompiled-binaries/ 348 | ``` 349 | 350 | 2. Install project dependencies. 351 | 352 | ``` 353 | $ npm install 354 | ``` 355 | 356 | #### Project Layout 357 | 358 | 1. Review the template folder files. 359 | 360 | ``` 361 | $ tree . 362 | . 363 | ├── Package.swift 364 | ├── README.md 365 | ├── Sources 366 | │   ├── hello 367 | │   │   └── main.swift 368 | │   └── welcome 369 | │   └── main.swift 370 | ├── node_modules 371 | │   ... 372 | ├── package-lock.json 373 | ├── package.json 374 | └── serverless.yml 375 | 376 | 7 directories, 10 files 377 | ``` 378 | 379 | This folder implements a standard Swift module layout. Sources files are located in `Sources` with two `main.swift` files which will be converted into executable files. 380 | 381 | ```swift 382 | import OpenWhiskAction 383 | 384 | func hello(args: [String:Any]) -> [String:Any] { 385 | if let name = args["name"] as? String { 386 | return [ "greeting" : "Hello \(name)!" ] 387 | } else { 388 | return [ "greeting" : "Hello stranger!" ] 389 | } 390 | } 391 | 392 | OpenWhiskAction(main: hello) 393 | ``` 394 | 395 | Source files use the [external Swift library](https://github.com/jthomas/OpenWhiskAction) to register OpenWhisk actions. This dependency is listed in the Swift package manifest. 396 | 397 | ```swift 398 | import PackageDescription 399 | 400 | let package = Package( 401 | name: "Action", 402 | dependencies: [ 403 | .Package(url: "https://github.com/jthomas/OpenWhiskAction.git", majorVersion: 0) 404 | ] 405 | ) 406 | ``` 407 | 408 | #### serverless.yml 409 | 410 | ```yaml 411 | service: swift-packages 412 | 413 | provider: 414 | name: openwhisk 415 | runtime: swift 416 | 417 | functions: 418 | hello: 419 | handler: .build/release/hello 420 | welcome: 421 | handler: .build/release/welcome 422 | 423 | custom: 424 | scripts: 425 | hooks: 426 | 'package:initialize': npm run-script compile 427 | plugins: 428 | - serverless-openwhisk 429 | - serverless-plugin-scripts 430 | ``` 431 | 432 | This configuration file uses an external plugin (`serverless-plugin-scripts`) to set up a build script (`npm run-script compile`) that is invoked before deployments. 433 | 434 | Handlers defined for the `hello` and `welcome` actions refer to the build artefacts rather than source files. 435 | 436 | #### package.json 437 | 438 | ```json 439 | { 440 | "name": "openwhisk-swift-package-with-precompiled-binaries", 441 | "version": "1.0.0", 442 | "description": "Swift packages and pre-compiled binaries on OpenWhisk.", 443 | "main": "handler.js", 444 | "scripts": { 445 | "postinstall": "npm link serverless-openwhisk", 446 | "compile": "docker run --rm -it -v $(pwd):/swift-package openwhisk/action-swift-v3.1.1 bash -e -c 'cd /swift-package && swift build -v -c release'" 447 | }, 448 | "keywords": [ 449 | "serverless", 450 | "openwhisk" 451 | ], 452 | "dependencies": { 453 | "serverless-plugin-scripts": "^1.0.2" 454 | } 455 | } 456 | ``` 457 | 458 | The `package.json` contains the `compile` script definition which will use Docker to run the swift build process in the platform container. This will ensure binaries are generated for the correct runtime. 459 | 460 | #### Deploying 461 | 462 | 1. Use the framework to deploy the project without any manual steps. 463 | 464 | ``` 465 | $ serverless deploy 466 | > openwhisk-swift-package-with-precompiled-binaries@1.0.0 compile /private/tmp/examples/openwhisk-swift-precompiled-binaries 467 | > docker run --rm -it -v $(pwd):/swift-package openwhisk/action-swift-v3.1.1 bash -e -c 'cd /swift-package && swift build -v -c release' 468 | 469 | ... 470 | Serverless: Packaging service... 471 | Serverless: Excluding development dependencies... 472 | Serverless: Compiling Functions... 473 | Serverless: Compiling API Gateway definitions... 474 | Serverless: Compiling Rules... 475 | Serverless: Compiling Triggers & Feeds... 476 | Serverless: Deploying Functions... 477 | Serverless: Deployment successful! 478 | 479 | Service Information 480 | platform: openwhisk.ng.bluemix.net 481 | namespace: _ 482 | service: swift-packages 483 | 484 | actions: 485 | swift-packages-dev-hello swift-packages-dev-welcome 486 | ``` 487 | 488 | 2. Test out the actions. 489 | 490 | ``` 491 | $ serverless invoke -f hello 492 | { 493 | "greeting": "Hello stranger!" 494 | } 495 | $ serverless invoke -f welcome 496 | { 497 | "greeting": "Welcome stranger!" 498 | } 499 | ``` 500 | -------------------------------------------------------------------------------- /bootcamp/ex7 - using the serverless framework/images/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-Cloud/openwhisk-workshops/702b45d50d79914eb03c752ccc665b1c26448ce8/bootcamp/ex7 - using the serverless framework/images/framework.png --------------------------------------------------------------------------------