diff --git a/samples/weather_function/.gitignore b/samples/weather_function/.gitignore new file mode 100644 index 00000000000..a3d033f76a7 --- /dev/null +++ b/samples/weather_function/.gitignore @@ -0,0 +1,16 @@ +# Additional files/dirs to unignore. + +# Additional files/dirs to ignore. +c_interop/ +*.kexe +*.bak +openweathermap_key.txt +temp.txt +temp.json +.gradle/ +build/ +weather +.konan/ +gradle/ +gradlew +template/ diff --git a/samples/weather_function/function/Dockerfile b/samples/weather_function/function/Dockerfile new file mode 100644 index 00000000000..31f74a11794 --- /dev/null +++ b/samples/weather_function/function/Dockerfile @@ -0,0 +1,32 @@ +FROM openjdk:8u171-jdk-stretch as builder +WORKDIR /opt +ENV kotlin_native_ver "0.7.1" +ENV kotlin_native_home "/opt/kotlin-native-linux-$kotlin_native_ver" +RUN wget "https://github.com/JetBrains/kotlin-native/releases/download/v$kotlin_native_ver/kotlin-native-linux-$kotlin_native_ver.tar.gz" \ + && tar -xzf kotlin-native-linux-$kotlin_native_ver.tar.gz && rm kotlin-native-linux-$kotlin_native_ver.tar.gz \ + && apt-get -qy update && apt-get install libcurl4-openssl-dev \ + && mkdir -p /app/src/main/kotlin/org/example/weather_func \ + && mkdir -p /app/lib/cJSON-1.7.7/include/cjson && mkdir -p /app/lib/cJSON-1.7.7/lib/x86_64-linux-gnu \ + && mkdir -p /app/gradle/wrapper +WORKDIR /app + +COPY gradle/wrapper/gradle-wrapper.properties gradle/wrapper/gradle-wrapper.jar /app/gradle/wrapper/ +COPY gradlew openweathermap_key.txt /app/ +COPY .konan/ /root/.konan +COPY *.def *.kts /app/ +COPY src/ /app/src +COPY lib/cJSON-1.7.7/include/cjson/cJSON.h /app/lib/cJSON-1.7.7/include/cJSON.h +COPY lib/cJSON-1.7.7/lib/x86_64-linux-gnu/libcjson.a /app/lib/cJSON-1.7.7/lib/libcjson.a +RUN ./gradlew build && cp build/konan/bin/linux_x64/weather.kexe weather + +FROM debian:stretch +ADD https://github.com/openfaas/faas/releases/download/0.8.2/fwatchdog /usr/bin +RUN apt-get update && apt-get -qy install libcurl4-openssl-dev \ + && chmod +x /usr/bin/fwatchdog && mkdir -p /app +WORKDIR /app +COPY --from=builder /app/openweathermap_key.txt . +COPY --from=builder /app/weather . +RUN chmod +x weather +ENV fprocess "./weather" +HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1 +CMD ["fwatchdog"] diff --git a/samples/weather_function/function/build.gradle.kts b/samples/weather_function/function/build.gradle.kts new file mode 100644 index 00000000000..f68c99718c7 --- /dev/null +++ b/samples/weather_function/function/build.gradle.kts @@ -0,0 +1,24 @@ +group = "org.example" +version = "0.1-SNAPSHOT" + +plugins { + id("konan") +} + +konanArtifacts { + interop("curl") { + defFile("curl.def") + } + + interop("cjson") { + defFile("cjson.def") + } + + program("weather") { + entryPoint("org.example.weather_func.main") + libraries { + artifact("cjson") + artifact("curl") + } + } +} diff --git a/samples/weather_function/function/cjson.def b/samples/weather_function/function/cjson.def new file mode 100644 index 00000000000..f542fc7b5b1 --- /dev/null +++ b/samples/weather_function/function/cjson.def @@ -0,0 +1,6 @@ +package = cjson +headers = cJSON.h +compilerOpts = -std=c89 +compilerOpts.linux = -I/app/lib/cJSON-1.7.7/include +staticLibraries = libcjson.a +libraryPaths = /app/lib/cJSON-1.7.7/lib diff --git a/samples/weather_function/function/curl.def b/samples/weather_function/function/curl.def new file mode 100644 index 00000000000..f239ca09c9a --- /dev/null +++ b/samples/weather_function/function/curl.def @@ -0,0 +1,5 @@ +package = curl +headers = curl.h +linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lcurl +compilerOpts = -std=c89 +compilerOpts.linux = -I/usr/include/x86_64-linux-gnu/curl diff --git a/samples/weather_function/function/settings.gradle.kts b/samples/weather_function/function/settings.gradle.kts new file mode 100644 index 00000000000..96151aef755 --- /dev/null +++ b/samples/weather_function/function/settings.gradle.kts @@ -0,0 +1,20 @@ +rootProject.name = "weather-function" + +pluginManagement { + repositories { + gradlePluginPortal() + jcenter() + maven("https://dl.bintray.com/jetbrains/kotlin-native-dependencies") + } + + // This way we can map plugin to standard maven artifact + // for maven repositories without plugin descriptor + resolutionStrategy { + eachPlugin { + if (requested.id.id == "konan") { + val kotlinNativeVer = "0.7.1" + useModule("org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlinNativeVer") + } + } + } +} diff --git a/samples/weather_function/function/src/main/kotlin/org/example/weather_func/Weather.kt b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/Weather.kt new file mode 100644 index 00000000000..6be28beafdb --- /dev/null +++ b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/Weather.kt @@ -0,0 +1,14 @@ +package org.example.weather_func + +/** Models the current weather for a location. */ +internal data class Weather( + val placeName: String, + val countryCode: String, + val windSpeed: Double = 0.0, + val windDegrees: Int = 0, + val temp: Int, + val minTemp: Int, + val maxTemp: Int, + val humidity: Int, + val conditions: Array> +) diff --git a/samples/weather_function/function/src/main/kotlin/org/example/weather_func/curl.kt b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/curl.kt new file mode 100644 index 00000000000..188f0868654 --- /dev/null +++ b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/curl.kt @@ -0,0 +1,89 @@ +package org.example.weather_func + +import org.example.weather_func.Event +import platform.posix.size_t + +import curl.curl_easy_setopt +import curl.CURLOPT_URL +import curl.CURLOPT_HEADERFUNCTION +import curl.CURLOPT_HEADERDATA +import curl.CURLOPT_WRITEFUNCTION +import curl.CURLOPT_WRITEDATA +import curl.curl_easy_cleanup +import curl.curl_easy_init +import curl.CURLE_OK +import curl.curl_easy_strerror +import curl.curl_easy_perform + +import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.readBytes + +/** + * Provides basic HTTP client functionality through the libCurl library. +*/ +internal class CUrl(val url: String) { + private val stableRef = StableRef.create(this) + private val curlObj = curl_easy_init() + val header = Event() + val body = Event() + + init { + val header = staticCFunction(::headerCallback) + val writeData = staticCFunction(::writeCallback) + + // Setup Curl. + curl_easy_setopt(curlObj, CURLOPT_URL, url) + curl_easy_setopt(curlObj, CURLOPT_HEADERFUNCTION, header) + curl_easy_setopt(curlObj, CURLOPT_HEADERDATA, stableRef.asCPointer()) + curl_easy_setopt(curlObj, CURLOPT_WRITEFUNCTION, writeData) + curl_easy_setopt(curlObj, CURLOPT_WRITEDATA, stableRef.asCPointer()) + } + + fun fetch() { + // Have Curl do a HTTP/HTTPS request and store the response (status). + val response = curl_easy_perform(curlObj) + val maxChars = 50 + // utfCharSize in bytes. + val utfCharSize = 4 + // length in bytes. + val length = utfCharSize * maxChars + // Print the error message if the Curl status code isn't OK (CURLE_OK). + if (response != CURLE_OK) println("Curl HTTP/S request failed: ${curl_easy_strerror(response)?.toKString(length)}") + } + + fun close() { + curl_easy_cleanup(curlObj) + stableRef.dispose() + } +} + +fun headerCallback(buffer: CPointer?, size: size_t, totalItems: size_t, userData: COpaquePointer?): size_t { + var responseSize = 0L + if (buffer != null && userData != null) { + val header = buffer.toKString((size * totalItems).toInt()).trim() + val curlRef = userData.asStableRef().get() + + curlRef.header(header) + responseSize = size * totalItems + } + return responseSize +} + +fun writeCallback(buffer: CPointer?, size: size_t, totalItems: size_t, userData: COpaquePointer?): size_t { + var responseSize = 0L + if (buffer != null && userData != null) { + val data = buffer.toKString((size * totalItems).toInt()).trim() + val curlRef = userData.asStableRef().get() + + curlRef.body(data) + responseSize = size * totalItems + } + return responseSize +} + +private fun CPointer.toKString(length: Int) = readBytes(length).stringFromUtf8() diff --git a/samples/weather_function/function/src/main/kotlin/org/example/weather_func/events.kt b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/events.kt new file mode 100644 index 00000000000..ba275ecb2e8 --- /dev/null +++ b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/events.kt @@ -0,0 +1,31 @@ +package org.example.weather_func + +internal typealias EventHandler = (T) -> Unit + +/** + * Covers event handling for the program. +*/ +internal class Event { + private var handlers = emptyList>() + + fun subscribe(handler: EventHandler) { + handlers += handler + } + + fun unsubscribe(handler: EventHandler) { + handlers -= handler + } + + operator fun plusAssign(handler: EventHandler) = subscribe(handler) + operator fun minusAssign(handler: EventHandler) = unsubscribe(handler) + + operator fun invoke(value: T) { + var exception: Throwable? = null + for (handler in handlers) { + try { handler(value) } + catch (ex: Throwable) { exception = ex } + } + // If the exception isn't null then throw it. + exception?.let { throw it } + } +} diff --git a/samples/weather_function/function/src/main/kotlin/org/example/weather_func/json.kt b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/json.kt new file mode 100644 index 00000000000..9cf7d8392eb --- /dev/null +++ b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/json.kt @@ -0,0 +1,162 @@ +package org.example.weather_func + +import cjson.cJSON_CreateObject as createJsonObject +import cjson.cJSON_AddStringToObject as addStringToObject +import cjson.cJSON_AddNumberToObject as addNumberToObject +import cjson.cJSON_AddArrayToObject as addArrayToObject +import cjson.cJSON_AddItemToArray as addItemToArray +import cjson.cJSON_AddItemToObjectCS as addItemToObject + +import cjson.cJSON_Delete as deleteObject +import cjson.cJSON_Print as jsonString +import cjson.cJSON_Parse as parseJson +import cjson.cJSON_GetObjectItemCaseSensitive as jsonObjectItem +import cjson.cJSON_IsString as jsonValueIsString + +import cjson.cJSON_IsNumber as jsonValueIsNumber +import cjson.cJSON_GetErrorPtr as getErrorPointer +import cjson.cJSON_GetStringValue as jsonStringValue +import cjson.cJSON_GetArraySize as jsonArraySize +import cjson.cJSON_GetArrayItem as jsonArrayItem +import cjson.cJSON + +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.toKString +import kotlinx.cinterop.pointed +import kotlin.system.exitProcess + +private val trueValue = 1 + +internal fun createWeatherFromJson(jsonStr: String): Weather { + val rootObj = parseJson(jsonStr) + checkForError() + val result = createWeather( + placeName = jsonObjectItem(rootObj, "name").stringValue(), + countryCode = extractCountryCode(rootObj), + windInfo = extractWindInfo(rootObj), + conditions = extractConditions(rootObj), + tempInfo = extractTempInfo(rootObj), + humidity = extractHumidity(rootObj) + ) + if (rootObj != null) deleteObject(rootObj) + return result +} + +/** + * Creates a new Weather object. + * @param placeName Name of the place. + * @param countryCode Unique country code. + * @param windInfo A Pair with wind speed as 1st item, and wind degrees as 2nd item. + * @param conditions A Array of Pair. Each Pair has name as 1st item, and desc as 2nd item. + * @param humidity General humidity of the atmosphere. + * @param tempInfo A Triple with temperature as 1st item, min temperature as 2nd item, and max temperature as 3rd item. + * @return A instance of Weather. +*/ +private fun createWeather( + placeName: String, + countryCode: String, + windInfo: Pair, + conditions: Array>, + humidity: Int, + tempInfo: Triple +) = Weather( + placeName = placeName, + countryCode = countryCode, + conditions = conditions, + humidity = humidity, + windSpeed = windInfo.first, + windDegrees = windInfo.second, + temp = tempInfo.first, + minTemp = tempInfo.second, + maxTemp = tempInfo.third +) + +private fun extractTempInfo(rootObj: CPointer?): Triple { + val mainObj = jsonObjectItem(rootObj, "main") + val temp = jsonObjectItem(mainObj, "temp").intValue() + val minTemp = jsonObjectItem(mainObj, "temp_min").intValue() + val maxTemp = jsonObjectItem(mainObj, "temp_max").intValue() + + return Triple(temp, minTemp, maxTemp) +} + +private fun extractHumidity(rootObj: CPointer?): Int { + val mainObj = jsonObjectItem(rootObj, "main") + + return jsonObjectItem(mainObj, "humidity").intValue() +} + +private fun extractConditions(rootObj: CPointer?): Array> { + val tmp = mutableListOf>() + val weatherObj = jsonObjectItem(rootObj, "weather") + + for (pos in 0..(jsonArraySize(weatherObj) - 1)) { + val item = jsonArrayItem(weatherObj, pos) + val name = jsonObjectItem(item, "main").stringValue() + val desc = jsonObjectItem(item, "description").stringValue() + + tmp += (name to desc) + } + return tmp.toTypedArray() +} + +private fun extractWindInfo(rootObj: CPointer?): Pair { + val windObj = jsonObjectItem(rootObj, "wind") + val windSpeed = jsonObjectItem(windObj, "speed").doubleValue() + val windDegrees = jsonObjectItem(windObj, "deg").intValue() + + return (windSpeed to windDegrees) +} + +private fun extractCountryCode(rootObj: CPointer?): String { + val sys = jsonObjectItem(rootObj, "sys") + + return jsonObjectItem(sys, "country").stringValue() +} + +internal fun weatherToJsonString(weather: Weather) : String { + val rootObj = createJsonObject() + addStringToObject(rootObj, "placeName", weather.placeName) + addStringToObject(rootObj, "countryCode", weather.countryCode) + addNumberToObject(rootObj, "humidity", weather.humidity.toDouble()) + addTempInfoArray(rootObj, temp = weather.temp, minTemp = weather.minTemp, maxTemp = weather.maxTemp) + addConditionsArray(rootObj, weather.conditions) + val result = jsonString(rootObj)?.toKString() ?: "" + deleteObject(rootObj) + return result +} + +private fun addTempInfoArray(rootObj: CPointer?, temp: Int, minTemp: Int, maxTemp: Int) { + val tmpObj = createJsonObject() + + addNumberToObject(tmpObj, "temp", temp.toDouble()) + addNumberToObject(tmpObj, "minTemp", minTemp.toDouble()) + addNumberToObject(tmpObj, "maxTemp", maxTemp.toDouble()) + addItemToObject(rootObj, "tempInfo", tmpObj) +} + +private fun addConditionsArray(rootObj: CPointer?, conditions: Array>) { + val tmpArr = addArrayToObject(rootObj, "conditions") + + conditions.forEach { (name, desc) -> + val tmpObj = createJsonObject() + addStringToObject(tmpObj, "name", name) + addStringToObject(tmpObj, "desc", desc) + addItemToArray(tmpArr, tmpObj) + } +} + +private fun checkForError() { + val errorMsg = getErrorPointer()?.toKString() ?: "" + + if (errorMsg.isNotEmpty()) { + println("JSON error before: $errorMsg\n") + exitProcess(-1) + } +} + +private fun CPointer?.intValue() = this?.pointed?.valueint ?: 0 + +private fun CPointer?.doubleValue() = this?.pointed?.valuedouble ?: 0.0 + +private fun CPointer?.stringValue() = jsonStringValue(this)?.toKString() ?: "" diff --git a/samples/weather_function/function/src/main/kotlin/org/example/weather_func/main.kt b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/main.kt new file mode 100644 index 00000000000..154b33c7557 --- /dev/null +++ b/samples/weather_function/function/src/main/kotlin/org/example/weather_func/main.kt @@ -0,0 +1,118 @@ +package org.example.weather_func + +import platform.posix.* +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import kotlinx.cinterop.refTo +import kotlin.system.exitProcess + +private val API_KEY by lazy { fetchApiKey() } + +fun main(args: Array) { + val input = readLine() + val location = fetchLocationArg(input) + val jsonFile = fetchJsonFileArg(input) + + if (location.isNotEmpty()) { + printFromWeatherService(location) + } else if (jsonFile.isNotEmpty()) { + checkFile(jsonFile) + printJsonFile(jsonFile) + } else { + handleEmptyInput() + } + println("Exiting...") +} + +private fun handleEmptyInput() { + println( + """ + | Weather - A OpenFaaS Serverless Function that outputs weather information. + | Usage examples (pass one of the following as input to the function): + | * Location (uses the Open Weather Map service), eg: + | -l="christchurch,nz" + | * JSON File, eg: -f="weather.json" + """.trimMargin() + ) + exitProcess(0) +} + +private fun checkFile(file: String) { + val error = -1 + if (access(file, F_OK) == error) { + println("File $file doesn't exist!") + exitProcess(error) + } +} + +private fun fetchJson(jsonFile: String): String { + var result = "" + // Open the file using the fopen function and store the file handle. + val file = fopen(jsonFile, "r") + fseek(file, 0, SEEK_END) + val fileSize = ftell(file) + fseek(file, 0, SEEK_SET) + + memScoped { + val buffer = allocArray(fileSize) + // Read the entire file and store the contents into the buffer. + fread(buffer, fileSize, 1, file) + result = buffer.toKString() + } + // Close the file. + fclose(file) + return result +} + +private fun printJsonFile(jsonFile: String) { + println("Printing from JSON file ($jsonFile)...") + val weather = createWeatherFromJson(fetchJson(jsonFile)) + println("Weather Object:\n$weather") + println("Weather JSON:\n${weatherToJsonString(weather)}") +} + +private fun printFromWeatherService(location: String) { + println("Fetching weather information (for $location)...") + val curl = CUrl(createUrl(location)).apply { + header += { if(it.startsWith("HTTP")) println("Response Status: $it") } + body += { data -> + val weather = createWeatherFromJson(data) + println("Weather information:\n${weatherToJsonString(weather)}") + } + } + curl.fetch() + curl.close() +} + +private fun fetchJsonFileArg(input: String?): String { + val flag = "-f" + return if (input != null && input.startsWith("$flag=")) input.replace("$flag=", "").replace("\"", "") + else "" +} + +private fun fetchLocationArg(input: String?): String { + val flag = "-l" + return if (input != null && input.startsWith("$flag=")) input.replace("$flag=", "").replace("\"", "") + else "" +} + +private fun createUrl(location: String): String { + val baseUrl = "http://api.openweathermap.org/data/2.5/weather" + return "$baseUrl?q=$location&units=metric&appid=$API_KEY" +} + +private fun fetchApiKey(): String { + var result = "" + val maxChars = 50 + // utfCharSize in bytes. + val utfCharSize = 4 + // Open the file using the fopen function and store the file handle. + val file = fopen("openweathermap_key.txt", "r") + val buffer = ByteArray(utfCharSize * maxChars) + if (file != null) result = fgets(buffer.refTo(0), buffer.size, file)?.toKString()?.trim() ?: "" + // Close the file. + fclose(file) + return result +} diff --git a/samples/weather_function/readme.md b/samples/weather_function/readme.md new file mode 100644 index 00000000000..ba837545a3e --- /dev/null +++ b/samples/weather_function/readme.md @@ -0,0 +1,32 @@ +# Weather Function Sample + +This is a Serverless Function that fetches the current weather information from the Open Weather Map [service](https://openweathermap.org/current) via HTTP. Part of the Function's output includes HTTP response headers, and the HTTP response body. This sample is designed to be deployed on OpenFaaS via a Docker image, in conjunction with Docker Swarm. + + +# Initial Setup + +If the **~/.konan/cache** directory doesn't exist then you will need to compile a basic Linux amd64 program with konan before proceeding. + +1. Clone the Kotlin Native Git repository: `git clone https://github.com/JetBrains/kotlin-native ~/repos/kotlin-native` +2. Change working directory to the sample: `cd ~/repos/kotlin-native/samples/weather_function` +3. Copy the Konan cache to the sample: `mkdir -p function/.konan || cp -R ~/.konan/cache function/.konan` +4. Copy the Gradle Wrapper to the sample: `cp ../gradle function` +5. Copy the **gradlew** file to the sample: `cp ../gradlew function` +6. Create the **openweathermap_key.txt** file in the **function** directory: `touch function/openweathermap_key.txt` +7. Append your [Open Weather Map API key](https://openweathermap.org/appid) to **function/openweathermap_key.txt** + + +# Building Docker Image + +It will be assumed that [Docker](https://store.docker.com/search?type=edition&offering=community), and [OpenFaaS](https://docs.openfaas.com/deployment/) is already installed along with the [OpenFaaS CLI](https://github.com/openfaas/faas-cli). Build the *weather* Docker image using the **fass-cli** tool: `faas-cli build -f weather.yml` + + +# Usage + +Make sure that you have completed the *Building Docker Image* section before proceeding. + +1. Start OpenFaaS +2. Deploy the **weather** image using the **faas-cli** tool: `faas-cli deploy --image weather --name weather` +3. Invoke the **weather** function (including passing through the location argument) by running the following: `echo '-l="christchurch,nz"' | faas-cli invoke weather` + +**Note:** The program (weather) can print weather information from a JSON file, eg: `./weather -f="current_weather.json"`. This functionality isn't available in the Serverless Function unless the file is generated in the **~/repos/kotlin-native/samples/weather_function/function** directory, the Dockerfile is updated to include the file in the Docker image, and the image is built (refer to the *Building Docker Image* section) before deploying the image (refer to the *Usage* section). diff --git a/samples/weather_function/weather.yml b/samples/weather_function/weather.yml new file mode 100644 index 00000000000..6a631e72c1e --- /dev/null +++ b/samples/weather_function/weather.yml @@ -0,0 +1,9 @@ +provider: + name: faas + gateway: http://127.0.0.1:8080 + +functions: + function: + lang: dockerfile + handler: function + image: weather