├── .github └── workflows │ └── main.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── clean.sh ├── cmd ├── manager │ ├── main.go │ ├── rproxy-darwin-arm64.go │ ├── rproxy-linux-amd64.go │ ├── rproxy-linux-arm.go │ └── rproxy-linux-arm64.go └── rproxy │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── coap │ └── coap.go ├── docker │ ├── handler.go │ ├── runtimes-amd64.go │ ├── runtimes-arm.go │ ├── runtimes-arm64.go │ └── runtimes │ │ ├── binary │ │ ├── Dockerfile │ │ ├── build.Dockerfile │ │ └── functionhandler.go │ │ ├── go │ │ ├── Dockerfile │ │ ├── build.Dockerfile │ │ └── functionhandler.go │ │ ├── nodejs │ │ ├── Dockerfile │ │ ├── build.Dockerfile │ │ ├── functionhandler.js │ │ └── package.json │ │ └── python3 │ │ ├── Dockerfile │ │ ├── build.Dockerfile │ │ └── functionhandler.py ├── grpc │ ├── grpc.go │ └── tinyfaas │ │ ├── requirements.txt │ │ ├── tinyfaas.pb.go │ │ ├── tinyfaas.proto │ │ ├── tinyfaas_grpc.pb.go │ │ ├── tinyfaas_pb2.py │ │ ├── tinyfaas_pb2.pyi │ │ └── tinyfaas_pb2_grpc.py ├── http │ └── http.go ├── manager │ └── manager.go ├── rproxy │ └── rproxy.go └── util │ ├── copy.go │ ├── copyembed.go │ ├── name.go │ └── zip.go ├── scripts ├── delete.sh ├── get_logs.sh ├── list.sh ├── logs.sh ├── upload.sh ├── uploadURL.sh └── wipe-functions.sh └── test ├── .gitignore ├── README.md ├── fns ├── echo-binary │ └── fn.sh ├── echo-js │ ├── index.js │ └── package.json ├── echo │ ├── fn.py │ └── requirements.txt ├── hello-go │ ├── fn.go │ └── go.mod ├── show-headers-js │ ├── index.js │ └── package.json ├── show-headers │ ├── fn.py │ └── requirements.txt └── sieve-of-eratosthenes │ ├── index.js │ └── package.json ├── requirements.txt └── test_all.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "1.22" 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v5 22 | with: 23 | version: "~> v2" 24 | args: release --clean 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tinyfaas-* 2 | cmd/manager/rproxy-*.bin 3 | tmp/ 4 | pkg/docker/runtimes-*/ 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/go,node,macos,python,visualstudiocode,intellij 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,node,macos,python,visualstudiocode,intellij 8 | 9 | ### Go ### 10 | # If you prefer the allow list template instead of the deny list, see community template: 11 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 12 | # 13 | # Binaries for programs and plugins 14 | *.exe 15 | *.exe~ 16 | *.dll 17 | *.so 18 | *.dylib 19 | 20 | # Test binary, built with `go test -c` 21 | *.test 22 | 23 | # Output of the go coverage tool, specifically when used with LiteIDE 24 | *.out 25 | 26 | # Dependency directories (remove the comment below to include it) 27 | # vendor/ 28 | 29 | # Go workspace file 30 | go.work 31 | 32 | ### Intellij ### 33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 34 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 35 | 36 | # User-specific stuff 37 | .idea/**/workspace.xml 38 | .idea/**/tasks.xml 39 | .idea/**/usage.statistics.xml 40 | .idea/**/dictionaries 41 | .idea/**/shelf 42 | 43 | # AWS User-specific 44 | .idea/**/aws.xml 45 | 46 | # Generated files 47 | .idea/**/contentModel.xml 48 | 49 | # Sensitive or high-churn files 50 | .idea/**/dataSources/ 51 | .idea/**/dataSources.ids 52 | .idea/**/dataSources.local.xml 53 | .idea/**/sqlDataSources.xml 54 | .idea/**/dynamic.xml 55 | .idea/**/uiDesigner.xml 56 | .idea/**/dbnavigator.xml 57 | 58 | # Gradle 59 | .idea/**/gradle.xml 60 | .idea/**/libraries 61 | 62 | # Gradle and Maven with auto-import 63 | # When using Gradle or Maven with auto-import, you should exclude module files, 64 | # since they will be recreated, and may cause churn. Uncomment if using 65 | # auto-import. 66 | # .idea/artifacts 67 | # .idea/compiler.xml 68 | # .idea/jarRepositories.xml 69 | # .idea/modules.xml 70 | # .idea/*.iml 71 | # .idea/modules 72 | # *.iml 73 | # *.ipr 74 | 75 | # CMake 76 | cmake-build-*/ 77 | 78 | # Mongo Explorer plugin 79 | .idea/**/mongoSettings.xml 80 | 81 | # File-based project format 82 | *.iws 83 | 84 | # IntelliJ 85 | out/ 86 | 87 | # mpeltonen/sbt-idea plugin 88 | .idea_modules/ 89 | 90 | # JIRA plugin 91 | atlassian-ide-plugin.xml 92 | 93 | # Cursive Clojure plugin 94 | .idea/replstate.xml 95 | 96 | # SonarLint plugin 97 | .idea/sonarlint/ 98 | 99 | # Crashlytics plugin (for Android Studio and IntelliJ) 100 | com_crashlytics_export_strings.xml 101 | crashlytics.properties 102 | crashlytics-build.properties 103 | fabric.properties 104 | 105 | # Editor-based Rest Client 106 | .idea/httpRequests 107 | 108 | # Android studio 3.1+ serialized cache file 109 | .idea/caches/build_file_checksums.ser 110 | 111 | ### Intellij Patch ### 112 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 113 | 114 | # *.iml 115 | # modules.xml 116 | # .idea/misc.xml 117 | # *.ipr 118 | 119 | # Sonarlint plugin 120 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 121 | .idea/**/sonarlint/ 122 | 123 | # SonarQube Plugin 124 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 125 | .idea/**/sonarIssues.xml 126 | 127 | # Markdown Navigator plugin 128 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 129 | .idea/**/markdown-navigator.xml 130 | .idea/**/markdown-navigator-enh.xml 131 | .idea/**/markdown-navigator/ 132 | 133 | # Cache file creation bug 134 | # See https://youtrack.jetbrains.com/issue/JBR-2257 135 | .idea/$CACHE_FILE$ 136 | 137 | # CodeStream plugin 138 | # https://plugins.jetbrains.com/plugin/12206-codestream 139 | .idea/codestream.xml 140 | 141 | # Azure Toolkit for IntelliJ plugin 142 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij 143 | .idea/**/azureSettings.xml 144 | 145 | ### macOS ### 146 | # General 147 | .DS_Store 148 | .AppleDouble 149 | .LSOverride 150 | 151 | # Icon must end with two \r 152 | Icon 153 | 154 | 155 | # Thumbnails 156 | ._* 157 | 158 | # Files that might appear in the root of a volume 159 | .DocumentRevisions-V100 160 | .fseventsd 161 | .Spotlight-V100 162 | .TemporaryItems 163 | .Trashes 164 | .VolumeIcon.icns 165 | .com.apple.timemachine.donotpresent 166 | 167 | # Directories potentially created on remote AFP share 168 | .AppleDB 169 | .AppleDesktop 170 | Network Trash Folder 171 | Temporary Items 172 | .apdisk 173 | 174 | ### macOS Patch ### 175 | # iCloud generated files 176 | *.icloud 177 | 178 | ### Node ### 179 | # Logs 180 | logs 181 | *.log 182 | npm-debug.log* 183 | yarn-debug.log* 184 | yarn-error.log* 185 | lerna-debug.log* 186 | .pnpm-debug.log* 187 | 188 | # Diagnostic reports (https://nodejs.org/api/report.html) 189 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 190 | 191 | # Runtime data 192 | pids 193 | *.pid 194 | *.seed 195 | *.pid.lock 196 | 197 | # Directory for instrumented libs generated by jscoverage/JSCover 198 | lib-cov 199 | 200 | # Coverage directory used by tools like istanbul 201 | coverage 202 | *.lcov 203 | 204 | # nyc test coverage 205 | .nyc_output 206 | 207 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 208 | .grunt 209 | 210 | # Bower dependency directory (https://bower.io/) 211 | bower_components 212 | 213 | # node-waf configuration 214 | .lock-wscript 215 | 216 | # Compiled binary addons (https://nodejs.org/api/addons.html) 217 | build/Release 218 | 219 | # Dependency directories 220 | node_modules/ 221 | jspm_packages/ 222 | 223 | # Snowpack dependency directory (https://snowpack.dev/) 224 | web_modules/ 225 | 226 | # TypeScript cache 227 | *.tsbuildinfo 228 | 229 | # Optional npm cache directory 230 | .npm 231 | 232 | # Optional eslint cache 233 | .eslintcache 234 | 235 | # Optional stylelint cache 236 | .stylelintcache 237 | 238 | # Microbundle cache 239 | .rpt2_cache/ 240 | .rts2_cache_cjs/ 241 | .rts2_cache_es/ 242 | .rts2_cache_umd/ 243 | 244 | # Optional REPL history 245 | .node_repl_history 246 | 247 | # Output of 'npm pack' 248 | *.tgz 249 | 250 | # Yarn Integrity file 251 | .yarn-integrity 252 | 253 | # dotenv environment variable files 254 | .env 255 | .env.development.local 256 | .env.test.local 257 | .env.production.local 258 | .env.local 259 | 260 | # parcel-bundler cache (https://parceljs.org/) 261 | .cache 262 | .parcel-cache 263 | 264 | # Next.js build output 265 | .next 266 | out 267 | 268 | # Nuxt.js build / generate output 269 | .nuxt 270 | dist 271 | 272 | # Gatsby files 273 | .cache/ 274 | # Comment in the public line in if your project uses Gatsby and not Next.js 275 | # https://nextjs.org/blog/next-9-1#public-directory-support 276 | # public 277 | 278 | # vuepress build output 279 | .vuepress/dist 280 | 281 | # vuepress v2.x temp and cache directory 282 | .temp 283 | 284 | # Docusaurus cache and generated files 285 | .docusaurus 286 | 287 | # Serverless directories 288 | .serverless/ 289 | 290 | # FuseBox cache 291 | .fusebox/ 292 | 293 | # DynamoDB Local files 294 | .dynamodb/ 295 | 296 | # TernJS port file 297 | .tern-port 298 | 299 | # Stores VSCode versions used for testing VSCode extensions 300 | .vscode-test 301 | 302 | # yarn v2 303 | .yarn/cache 304 | .yarn/unplugged 305 | .yarn/build-state.yml 306 | .yarn/install-state.gz 307 | .pnp.* 308 | 309 | ### Node Patch ### 310 | # Serverless Webpack directories 311 | .webpack/ 312 | 313 | # Optional stylelint cache 314 | 315 | # SvelteKit build / generate output 316 | .svelte-kit 317 | 318 | ### Python ### 319 | # Byte-compiled / optimized / DLL files 320 | __pycache__/ 321 | *.py[cod] 322 | *$py.class 323 | 324 | # C extensions 325 | 326 | # Distribution / packaging 327 | .Python 328 | build/ 329 | develop-eggs/ 330 | dist/ 331 | downloads/ 332 | eggs/ 333 | .eggs/ 334 | lib/ 335 | lib64/ 336 | parts/ 337 | sdist/ 338 | var/ 339 | wheels/ 340 | share/python-wheels/ 341 | *.egg-info/ 342 | .installed.cfg 343 | *.egg 344 | MANIFEST 345 | 346 | # PyInstaller 347 | # Usually these files are written by a python script from a template 348 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 349 | *.manifest 350 | *.spec 351 | 352 | # Installer logs 353 | pip-log.txt 354 | pip-delete-this-directory.txt 355 | 356 | # Unit test / coverage reports 357 | htmlcov/ 358 | .tox/ 359 | .nox/ 360 | .coverage 361 | .coverage.* 362 | nosetests.xml 363 | coverage.xml 364 | *.cover 365 | *.py,cover 366 | .hypothesis/ 367 | .pytest_cache/ 368 | cover/ 369 | 370 | # Translations 371 | *.mo 372 | *.pot 373 | 374 | # Django stuff: 375 | local_settings.py 376 | db.sqlite3 377 | db.sqlite3-journal 378 | 379 | # Flask stuff: 380 | instance/ 381 | .webassets-cache 382 | 383 | # Scrapy stuff: 384 | .scrapy 385 | 386 | # Sphinx documentation 387 | docs/_build/ 388 | 389 | # PyBuilder 390 | .pybuilder/ 391 | target/ 392 | 393 | # Jupyter Notebook 394 | .ipynb_checkpoints 395 | 396 | # IPython 397 | profile_default/ 398 | ipython_config.py 399 | 400 | # pyenv 401 | # For a library or package, you might want to ignore these files since the code is 402 | # intended to run in multiple environments; otherwise, check them in: 403 | # .python-version 404 | 405 | # pipenv 406 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 407 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 408 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 409 | # install all needed dependencies. 410 | #Pipfile.lock 411 | 412 | # poetry 413 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 414 | # This is especially recommended for binary packages to ensure reproducibility, and is more 415 | # commonly ignored for libraries. 416 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 417 | #poetry.lock 418 | 419 | # pdm 420 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 421 | #pdm.lock 422 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 423 | # in version control. 424 | # https://pdm.fming.dev/#use-with-ide 425 | .pdm.toml 426 | 427 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 428 | __pypackages__/ 429 | 430 | # Celery stuff 431 | celerybeat-schedule 432 | celerybeat.pid 433 | 434 | # SageMath parsed files 435 | *.sage.py 436 | 437 | # Environments 438 | .venv 439 | env/ 440 | venv/ 441 | ENV/ 442 | env.bak/ 443 | venv.bak/ 444 | 445 | # Spyder project settings 446 | .spyderproject 447 | .spyproject 448 | 449 | # Rope project settings 450 | .ropeproject 451 | 452 | # mkdocs documentation 453 | /site 454 | 455 | # mypy 456 | .mypy_cache/ 457 | .dmypy.json 458 | dmypy.json 459 | 460 | # Pyre type checker 461 | .pyre/ 462 | 463 | # pytype static type analyzer 464 | .pytype/ 465 | 466 | # Cython debug symbols 467 | cython_debug/ 468 | 469 | # PyCharm 470 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 471 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 472 | # and can be added to the global gitignore or merged into this file. For a more nuclear 473 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 474 | #.idea/ 475 | 476 | ### Python Patch ### 477 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 478 | poetry.toml 479 | 480 | # ruff 481 | .ruff_cache/ 482 | 483 | # LSP config files 484 | pyrightconfig.json 485 | 486 | ### VisualStudioCode ### 487 | .vscode/* 488 | !.vscode/settings.json 489 | !.vscode/tasks.json 490 | !.vscode/launch.json 491 | !.vscode/extensions.json 492 | !.vscode/*.code-snippets 493 | 494 | # Local History for Visual Studio Code 495 | .history/ 496 | 497 | # Built Visual Studio Code Extensions 498 | *.vsix 499 | 500 | ### VisualStudioCode Patch ### 501 | # Ignore all local history of files 502 | .history 503 | .ionide 504 | 505 | # End of https://www.toptal.com/developers/gitignore/api/go,node,macos,python,visualstudiocode,intellij 506 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | builds: 4 | - skip: true 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Tobias Pfandzelter, Emily Dietrich 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := "tinyFaaS" 2 | PKG := "github.com/OpenFogStack/$(PROJECT_NAME)" 3 | GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v /ext/ | grep -v _test.go) 4 | TEST_DIR := ./test 5 | 6 | SUPPORTED_ARCH=amd64 arm arm64 7 | RUNTIMES := $(shell find pkg/docker/runtimes -name Dockerfile | xargs -n1 dirname | xargs -n1 basename) 8 | 9 | OS=$(shell go env GOOS) 10 | ARCH=$(shell go env GOARCH) 11 | 12 | .PHONY: all 13 | all: build 14 | 15 | .PHONY: build 16 | build: tinyfaas-${OS}-${ARCH} 17 | 18 | .PHONY: start 19 | start: tinyfaas-${OS}-${ARCH} 20 | ./$< 21 | 22 | .PHONY: test 23 | test: build ${TEST_DIR}/test_all.py pkg/grpc/tinyfaas/tinyfaas_pb2.py pkg/grpc/tinyfaas/tinyfaas_pb2.pyi pkg/grpc/tinyfaas/tinyfaas_pb2_grpc.py 24 | @python3 ${TEST_DIR}/test_all.py 25 | 26 | .PHONY: clean 27 | clean: clean.sh 28 | @sh clean.sh 29 | 30 | 31 | define arch_build 32 | pkg/docker/runtimes-$(arch): $(foreach runtime,$(RUNTIMES),pkg/docker/runtimes-$(arch)/$(runtime)) 33 | endef 34 | $(foreach arch,$(SUPPORTED_ARCH),$(eval $(arch_build))) 35 | 36 | define runtime_build 37 | .PHONY: pkg/docker/runtimes-$(arch)/$(runtime) 38 | pkg/docker/runtimes-$(arch)/$(runtime): pkg/docker/runtimes-$(arch)/$(runtime)/Dockerfile pkg/docker/runtimes-$(arch)/$(runtime)/blob.tar.gz 39 | 40 | pkg/docker/runtimes-$(arch)/$(runtime)/blob.tar.gz: pkg/docker/runtimes/$(runtime)/build.Dockerfile 41 | mkdir -p $$(@D) 42 | cd $$( $$@ 45 | docker kill $${PROJECT_NAME}-$(runtime) 46 | 47 | pkg/docker/runtimes-$(arch)/$(runtime)/Dockerfile: pkg/docker/runtimes/$(runtime)/Dockerfile 48 | mkdir -p $$(@D) 49 | cp -r pkg/docker/runtimes/$(runtime)/Dockerfile $$@ 50 | endef 51 | $(foreach arch,$(SUPPORTED_ARCH),$(foreach runtime,$(RUNTIMES),$(eval $(runtime_build)))) 52 | 53 | # requires protoc, protoc-gen-go and protoc-gen-go-grpc 54 | # install from your package manager, e.g.: 55 | # brew install protobuf 56 | # brew install protoc-gen-go 57 | # brew install protoc-gen-go-grpc 58 | pkg/grpc/tinyfaas/tinyfaas.pb.go pkg/grpc/tinyfaas/tinyfaas_grpc.pb.go: pkg/grpc/tinyfaas/tinyfaas.proto 59 | @protoc -I $(=v1.22) to compile management service and reverse proxy 52 | - Docker (>=v24) 53 | - Make 54 | - a writable directory (tinyFaaS writes temporary files to a `./tmp` directory) 55 | 56 | Note that tinyFaaS is intended for Linux hosts (`x86_64` and `arm64`). 57 | Due to limitations of Docker Desktop for Mac, installing and running [`docker-mac-net-connect`](https://github.com/chipmk/docker-mac-net-connect) is necessary to run tinyFaaS on macOS hosts. 58 | Running tinyFaaS on Windows computers (native or through WSL) is probably possible but has not been tested and is thus not recommended. 59 | 60 | ### Getting Started 61 | 62 | Start tinyFaaS with: 63 | 64 | ```sh 65 | make start 66 | ``` 67 | 68 | The reverse proxy will be started automatically. 69 | Please note that you cannot use tinyFaaS until the reverse proxy is running. 70 | 71 | ### Managing Functions 72 | 73 | To manage functions on tinyFaaS, use the included scripts included in `./src/scripts`. 74 | 75 | To upload a function, run `upload.sh {FOLDER} {NAME} {ENV} {THREADS}`, where `{FOLDER}` is the path to your function code, `{NAME}` is the name for your function, `{ENV}` is the environment you would like to use (`python3`, `nodejs`, or `binary`), and `{THREADS}` is a number specifying the number of function handlers for your function. 76 | For example, you might call `./scripts/upload.sh "./test/fns/sieve-of-eratosthenes" "sieve" "nodejs" 1` to upload the _sieve of Eratosthenes_ example function included in this repository. 77 | This requires the `zip`, `base64`, and `curl` utilities. 78 | 79 | Alternatively, you can also upload functions from a zipped file available at some URL. 80 | Use the included script as a starting point: `uploadURL.sh {URL} {NAME} {ENV} {THREADS} {SUBFOLDER_PATH}`, where `{URL}` is the URL to a zip that has your function code, `{SUBFOLDER_PATH}` is the folder of the code within that zip (use `/` if the code is in the top-level), `{NAME}` is the name for your function, `{ENV}` is the environment, and `{THREADS}` is a number specifying the number of function handlers for your function. 81 | For example, you might call `uploadURL.sh "https://github.com/OpenFogStack/tinyFaas/archive/main.zip" "tinyFaaS-main/test/fns/sieve-of-eratosthenes" "sieve" "nodejs" 1` to upload the _sieve of Eratosthenes_ example function included in this repository. 82 | 83 | To get a list of existing functions, run `list.sh`. 84 | 85 | To delete a function, run `delete.sh {NAME}`, where `{NAME}` is the name of the function you want to remove. 86 | 87 | Additionally, we provide scripts to read logs from your function and to wipe all functions from tinyFaaS. 88 | 89 | ### Writing Functions 90 | 91 | This tinyFaaS prototype only supports functions written for NodeJS 20, Python 3.9, and binary functions. 92 | A good place to get started with writing functions is the selection of test functions in [`./test/fns`](./test/fns/). 93 | HTTP headers and GRPC Metadata are accessible in NodeJS and Python functions as key values. Check the "show-headers" test functions for more information. 94 | 95 | #### NodeJS 20 96 | 97 | Your function must be supplied as a Node module with the name `fn` that exports a single function that takes the `req` and `res` parameters for request and response, respectively. 98 | `res` supports the `send()` function that has one parameter, a string that is passed to the client as-is. 99 | 100 | #### Python 3.9 101 | 102 | Your function must be supplied as a file named `fn.py` that exposes a method `fn` that is invoked for every function invocation. 103 | This method must accept a string as an input (that can also be `None`) and must provide a string as a return value. 104 | You may also provide a `requirements.txt` file from which dependencies will be installed alongside your function. 105 | Any other data you provide will be available. 106 | 107 | #### Binary 108 | 109 | Your function must be provided as a `fn.sh` shell script that is invoked for every function call. 110 | This shell script may also call other binaries as needed. 111 | Input data is provided from `stdin`. 112 | Output responses should be provided on `stdout`. 113 | 114 | ### Calling Functions 115 | 116 | tinyFaaS supports different application layer protocols at its reverse proxy. 117 | Different protocols are useful for different use-cases: CoAP for lightweight communication, e.g., for IoT devices; HTTP to support traditional web applications; GRPC for inter-process communication. 118 | 119 | #### CoAP 120 | 121 | To call a tinyFaaS function using its CoAP endpoint, make a GET or POST request to `coap://{HOST}:{PORT}/{NAME}` where `{HOST}` is the address of the tinyFaaS host, `{PORT}` is the port for the tinyFaaS CoAP endpoint (default is `5683`), and `{NAME}` is the name of your function. 122 | You may include data in any form you want, it will be passed to your function. 123 | 124 | Unfortunately, [`curl` does not yet support CoAP](https://curl.se/mail/lib-2018-05/0017.html), but [a number](https://github.com/coapjs/coap-cli) [of other](https://aiocoap.readthedocs.io/en/latest/tools.html) [tools are available](https://fitbit.github.io/golden-gate/tools/coap_client.html). 125 | 126 | #### HTTP 127 | 128 | To call a tinyFaaS function using its HTTP endpoint, make a GET or POST request to `http://{HOST}:{PORT}/{NAME}` where `{HOST}` is the address of the tinyFaaS host, `{PORT}` is the port for the tinyFaaS HTTP endpoint (default is `80`), and `{NAME}` is the name of your function. 129 | You may include data in any form you want, it will be passed to your function. 130 | 131 | TLS is not supported (but contributions are welcome). 132 | 133 | To make an asynchronous request, pass the `X-tinyFaaS-Async` header with any value. 134 | An asynchronous request means the client will receive a `202` response code immediately and no function results will be sent back. 135 | 136 | ```sh 137 | curl --header "X-tinyFaaS-Async: true" "http://localhost:8000/sieve" 138 | ``` 139 | 140 | #### gRPC 141 | 142 | To use the gRPC endpoint, compile the `tinyfaas` protocol buffer (included in [`./pkg/grpc/tinyfaas`](./pkg/grpc/tinyfaas)) for your programming language and import it into your application. 143 | We already provide compiled versions for Go and Python in that directory. 144 | Specify the tinyFaaS host and port (default is `9000`) for the GRPC endpoint and use the `Request` function with the `functionIdentifier` being your function's name and the `data` field including data in any form you want. 145 | 146 | ### Removing tinyFaaS 147 | 148 | When you stop the management service with `SIGINT` (`Ctrl+C`), the reverse proxy and all function handlers should be stopped. 149 | You can also use: 150 | 151 | ```bash 152 | make clean 153 | ``` 154 | 155 | OR 156 | 157 | ```bash 158 | docker rm -f $$(docker ps -a -q --filter label=tinyFaaS) 159 | docker network rm $$(docker network ls -q --filter label=tinyFaaS) 160 | docker rmi $$(docker image ls -q --filter label=tinyFaaS) 161 | rm -rf ./tmp 162 | ``` 163 | 164 | ### Specifying Ports 165 | 166 | By default, tinyFaaS will use the following ports: 167 | 168 | | Port | Protocol | Description | 169 | | ---- | -------- | ------------------ | 170 | | 8080 | TCP | Management Service | 171 | | 5683 | UDP | CoAP Endpoint | 172 | | 8000 | TCP | HTTP Endpoint | 173 | | 9000 | TCP | GRPC Endpoint | 174 | 175 | To change the port of the management service, change the port binding in the `docker run` command. 176 | 177 | To change or deactivate the endpoints of tinyFaaS, you can use the `COAP_PORT`, `HTTP_PORT`, and `GRPC_PORT` environment variables, which must be passed to the management service Docker container. 178 | Specify `-1` to deactivate a specific endpoint. 179 | For example, to use `6000` as the port for the CoAP and deactivate GRPC, run the management service with this command: 180 | 181 | ```bash 182 | docker run --env COAP_PORT=6000 --env GRPC_PORT=-1 -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 --name tinyfaas-mgmt -d tinyfaas-mgmt tinyfaas-mgmt 183 | ``` 184 | 185 | ### Tests 186 | 187 | The tests in [`./test`](./test) test the end-to-end functionality of tinyFaaS and are expected to complete successfully. 188 | We use these tests during development to ensure no patches break any functionality. 189 | The tests can also serve as documentation on the expected behavior of tinyFaaS. 190 | 191 | Running the tests requires: 192 | 193 | - Python >3.10 with the `venv` module 194 | - a tinyFaaS binary built for your host 195 | 196 | Create a virtual environment for Python and install the necessary dependencies for CoAP and gRPC: 197 | 198 | ```sh 199 | python3 -m venv .venv 200 | source .venv/bin/activate 201 | python3 -m pip install -r test/requirements.txt 202 | ``` 203 | 204 | If you do not install these requirements, test runs are limited to invocations with HTTP. 205 | 206 | Run the tests with: 207 | 208 | ```sh 209 | $ make test 210 | ............. 211 | ---------------------------------------------------------------------- 212 | Ran 13 tests in 28.063s 213 | 214 | OK 215 | ``` 216 | 217 | Tests will output a `.` (dot) for successful tests and `E` or `F` for failed tests. 218 | tinyFaaS output will be written to `tf_test.out`. 219 | 220 | On macOS, [`docker-mac-net-connect`](https://github.com/chipmk/docker-mac-net-connect) is necessary to run tinyFaaS. 221 | There is a [known issue in `docker-mac-net-connect`](https://github.com/chipmk/docker-mac-net-connect/issues/36) that silently breaks the tunnel when Docker Desktop enters its resource saver mode. 222 | If you find that tinyFaaS does not start properly on your macOS host, try restarting the tunnel: 223 | 224 | ```sh 225 | sudo brew services restart docker-mac-net-connect 226 | ``` 227 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TF_TAG="tinyFaaS" 4 | TMP_DIR="tmp" 5 | 6 | # remove old containers, networks and images 7 | containers=$(docker ps -a -q --filter label=$TF_TAG) 8 | 9 | if [ -n "$containers" ]; then 10 | for container in $containers; do 11 | docker stop "$container" > /dev/null || echo "Failed to stop container $container! Please stop it manually..." 12 | docker rm "$container" > /dev/null || echo "Failed to remove container $container! Please remove it manually..." 13 | done 14 | else 15 | echo "No old containers to remove. Skipping..." 16 | fi 17 | 18 | networks=$(docker network ls -q --filter label=$TF_TAG) 19 | 20 | if [ -n "$networks" ]; then 21 | for network in $networks; do 22 | docker network rm "$network" > /dev/null || echo "Failed to remove network $network! Please remove it manually..." 23 | done 24 | else 25 | echo "No old networks to remove. Skipping..." 26 | fi 27 | 28 | images=$(docker image ls -q --filter label=$TF_TAG) 29 | 30 | if [ -n "$images" ]; then 31 | for image in $images; do 32 | docker image rm "$image" > /dev/null || echo "Failed to remove image $image! Please remove it manually..." 33 | done 34 | else 35 | echo "No old images to remove. Skipping..." 36 | fi 37 | 38 | # remove tmp directory 39 | if [ -d "$TMP_DIR" ]; then 40 | rm -rf "$TMP_DIR" > /dev/null || echo "Failed to remove directory $TMP_DIR ! Please remove it manually..." 41 | else 42 | echo "No tmp directory to remove. Skipping..." 43 | fi 44 | -------------------------------------------------------------------------------- /cmd/manager/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | _ "embed" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "path" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/OpenFogStack/tinyFaaS/pkg/docker" 19 | "github.com/OpenFogStack/tinyFaaS/pkg/manager" 20 | "github.com/google/uuid" 21 | ) 22 | 23 | const ( 24 | ConfigPort = 8080 25 | RProxyConfigPort = 8081 26 | RProxyListenAddress = "" 27 | ) 28 | 29 | type server struct { 30 | ms *manager.ManagementService 31 | } 32 | 33 | func main() { 34 | 35 | log.SetFlags(log.LstdFlags | log.Lshortfile) 36 | log.SetPrefix("manager: ") 37 | 38 | ports := map[string]int{ 39 | "coap": 5683, 40 | "http": 8000, 41 | "grpc": 9000, 42 | } 43 | 44 | for p := range ports { 45 | portstr := os.Getenv(p + "_PORT") 46 | 47 | if portstr == "" { 48 | continue 49 | } 50 | 51 | port, err := strconv.Atoi(portstr) 52 | 53 | if err != nil { 54 | log.Fatalf("invalid port for protocol %s: %s (must be an integer!)", p, err) 55 | } 56 | 57 | if port < 0 { 58 | delete(ports, p) 59 | continue 60 | } 61 | 62 | if port > 65535 { 63 | log.Fatalf("invalid port for protocol %s: %s (must be an integer lower than 65535!)", p, err) 64 | } 65 | 66 | ports[p] = port 67 | } 68 | 69 | // setting backend to docker 70 | id := uuid.New().String() 71 | 72 | // find backend 73 | backend, ok := os.LookupEnv("TF_BACKEND") 74 | 75 | if !ok { 76 | backend = "docker" 77 | log.Println("using default backend docker") 78 | } 79 | 80 | var tfBackend manager.Backend 81 | switch backend { 82 | case "docker": 83 | log.Println("using docker backend") 84 | tfBackend = docker.New(id) 85 | default: 86 | log.Fatalf("invalid backend %s", backend) 87 | } 88 | 89 | ms := manager.New( 90 | id, 91 | RProxyListenAddress, 92 | ports, 93 | RProxyConfigPort, 94 | tfBackend, 95 | ) 96 | 97 | rproxyArgs := []string{fmt.Sprintf("%s:%d", RProxyListenAddress, RProxyConfigPort)} 98 | 99 | for prot, port := range ports { 100 | rproxyArgs = append(rproxyArgs, fmt.Sprintf("%s:%s:%d", prot, RProxyListenAddress, port)) 101 | } 102 | 103 | log.Println("rproxy args:", rproxyArgs) 104 | 105 | // unpack the rproxy binary in a temporary directory 106 | rProxyDir := path.Join(os.TempDir(), id) 107 | 108 | err := os.MkdirAll(rProxyDir, 0755) 109 | 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | err = os.WriteFile(path.Join(rProxyDir, "rproxy.bin"), RProxyBin, 0755) 115 | 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | 120 | defer os.RemoveAll(rProxyDir) 121 | 122 | c := exec.Command(path.Join(rProxyDir, "rproxy.bin"), rproxyArgs...) 123 | 124 | stdout, err := c.StdoutPipe() 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | 129 | stderr, err := c.StderrPipe() 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | go func() { 135 | scanner := bufio.NewScanner(stdout) 136 | for scanner.Scan() { 137 | fmt.Println(scanner.Text()) 138 | } 139 | }() 140 | 141 | go func() { 142 | scanner := bufio.NewScanner(stderr) 143 | for scanner.Scan() { 144 | fmt.Println(scanner.Text()) 145 | } 146 | }() 147 | 148 | err = c.Start() 149 | if err != nil { 150 | log.Fatal(err) 151 | } 152 | 153 | rproxy := c.Process 154 | 155 | log.Println("started rproxy") 156 | 157 | s := &server{ 158 | ms: ms, 159 | } 160 | 161 | // create handlers 162 | r := http.NewServeMux() 163 | r.HandleFunc("/upload", s.uploadHandler) 164 | r.HandleFunc("/delete", s.deleteHandler) 165 | r.HandleFunc("/list", s.listHandler) 166 | r.HandleFunc("/wipe", s.wipeHandler) 167 | r.HandleFunc("/logs", s.logsHandler) 168 | r.HandleFunc("/uploadURL", s.urlUploadHandler) 169 | 170 | sig := make(chan os.Signal, 1) 171 | signal.Notify(sig, os.Interrupt) 172 | go func() { 173 | <-sig 174 | 175 | log.Println("received interrupt") 176 | log.Println("shutting down") 177 | 178 | // stop rproxy 179 | log.Println("stopping rproxy") 180 | err := rproxy.Kill() 181 | 182 | if err != nil { 183 | log.Println(err) 184 | } 185 | 186 | // stop handlers 187 | log.Println("stopping management service") 188 | err = ms.Stop() 189 | 190 | if err != nil { 191 | log.Println(err) 192 | } 193 | 194 | os.Exit(0) 195 | }() 196 | 197 | // start server 198 | log.Println("starting HTTP server") 199 | addr := fmt.Sprintf(":%d", ConfigPort) 200 | err = http.ListenAndServe(addr, r) 201 | if err != nil { 202 | log.Fatal(err) 203 | } 204 | } 205 | 206 | func (s *server) uploadHandler(w http.ResponseWriter, r *http.Request) { 207 | 208 | if r.Method != http.MethodPost { 209 | w.WriteHeader(http.StatusBadRequest) 210 | return 211 | } 212 | 213 | // parse request 214 | d := struct { 215 | FunctionName string `json:"name"` 216 | FunctionEnv string `json:"env"` 217 | FunctionThreads int `json:"threads"` 218 | FunctionZip string `json:"zip"` 219 | FunctionEnvs []string `json:"envs"` 220 | }{} 221 | 222 | err := json.NewDecoder(r.Body).Decode(&d) 223 | if err != nil { 224 | w.WriteHeader(http.StatusBadRequest) 225 | log.Println(err) 226 | return 227 | } 228 | 229 | log.Println("got request to upload function: Name", d.FunctionName, "Env", d.FunctionEnv, "Threads", d.FunctionThreads, "Bytes", len(d.FunctionZip), "Envs", d.FunctionEnvs) 230 | 231 | envs := make(map[string]string) 232 | for _, e := range d.FunctionEnvs { 233 | k, v, ok := strings.Cut(e, "=") 234 | 235 | if !ok { 236 | log.Println("invalid env:", e) 237 | continue 238 | } 239 | 240 | envs[k] = v 241 | } 242 | 243 | res, err := s.ms.Upload(d.FunctionName, d.FunctionEnv, d.FunctionThreads, d.FunctionZip, envs) 244 | 245 | if err != nil { 246 | w.WriteHeader(http.StatusInternalServerError) 247 | log.Println(err) 248 | return 249 | } 250 | 251 | // return success 252 | w.WriteHeader(http.StatusOK) 253 | fmt.Fprint(w, res) 254 | 255 | } 256 | 257 | func (s *server) deleteHandler(w http.ResponseWriter, r *http.Request) { 258 | if r.Method != http.MethodPost { 259 | w.WriteHeader(http.StatusBadRequest) 260 | return 261 | } 262 | 263 | // parse request 264 | d := struct { 265 | FunctionName string `json:"name"` 266 | }{} 267 | 268 | err := json.NewDecoder(r.Body).Decode(&d) 269 | if err != nil { 270 | w.WriteHeader(http.StatusBadRequest) 271 | log.Println(err) 272 | return 273 | } 274 | 275 | log.Println("got request to delete function:", d.FunctionName) 276 | 277 | // delete function 278 | err = s.ms.Delete(d.FunctionName) 279 | 280 | if err != nil { 281 | w.WriteHeader(http.StatusInternalServerError) 282 | log.Println(err) 283 | return 284 | } 285 | 286 | // return success 287 | w.WriteHeader(http.StatusOK) 288 | } 289 | 290 | func (s *server) listHandler(w http.ResponseWriter, r *http.Request) { 291 | if r.Method != http.MethodGet { 292 | w.WriteHeader(http.StatusBadRequest) 293 | return 294 | } 295 | 296 | l := s.ms.List() 297 | 298 | // return success 299 | w.WriteHeader(http.StatusOK) 300 | for _, f := range l { 301 | fmt.Fprintf(w, "%s\n", f) 302 | } 303 | } 304 | 305 | func (s *server) wipeHandler(w http.ResponseWriter, r *http.Request) { 306 | if r.Method != http.MethodPost { 307 | w.WriteHeader(http.StatusBadRequest) 308 | return 309 | } 310 | 311 | err := s.ms.Wipe() 312 | 313 | if err != nil { 314 | w.WriteHeader(http.StatusInternalServerError) 315 | log.Println(err) 316 | return 317 | } 318 | 319 | w.WriteHeader(http.StatusOK) 320 | } 321 | 322 | func (s *server) logsHandler(w http.ResponseWriter, r *http.Request) { 323 | 324 | if r.Method != http.MethodGet { 325 | w.WriteHeader(http.StatusBadRequest) 326 | return 327 | } 328 | 329 | // parse request 330 | var logs io.Reader 331 | name := r.URL.Query().Get("name") 332 | 333 | if name == "" { 334 | l, err := s.ms.Logs() 335 | if err != nil { 336 | w.WriteHeader(http.StatusInternalServerError) 337 | log.Println(err) 338 | return 339 | } 340 | logs = l 341 | } 342 | 343 | if name != "" { 344 | l, err := s.ms.LogsFunction(name) 345 | if err != nil { 346 | w.WriteHeader(http.StatusInternalServerError) 347 | log.Println(err) 348 | return 349 | } 350 | logs = l 351 | } 352 | 353 | // return success 354 | w.WriteHeader(http.StatusOK) 355 | // w.Header().Set("Content-Type", "text/plain") 356 | _, err := io.Copy(w, logs) 357 | if err != nil { 358 | w.WriteHeader(http.StatusInternalServerError) 359 | log.Println(err) 360 | return 361 | } 362 | } 363 | 364 | func (s *server) urlUploadHandler(w http.ResponseWriter, r *http.Request) { 365 | if r.Method != http.MethodPost { 366 | w.WriteHeader(http.StatusBadRequest) 367 | return 368 | } 369 | 370 | // parse request 371 | d := struct { 372 | FunctionName string `json:"name"` 373 | FunctionEnv string `json:"env"` 374 | FunctionThreads int `json:"threads"` 375 | FunctionURL string `json:"url"` 376 | FunctionEnvs []string `json:"envs"` 377 | SubFolder string `json:"subfolder_path"` 378 | }{} 379 | 380 | err := json.NewDecoder(r.Body).Decode(&d) 381 | if err != nil { 382 | w.WriteHeader(http.StatusBadRequest) 383 | log.Println(err) 384 | return 385 | } 386 | 387 | log.Println("got request to upload function:", d) 388 | 389 | envs := make(map[string]string) 390 | for _, e := range d.FunctionEnvs { 391 | k, v, ok := strings.Cut(e, "=") 392 | 393 | if !ok { 394 | log.Println("invalid env:", e) 395 | continue 396 | } 397 | 398 | envs[k] = v 399 | } 400 | 401 | res, err := s.ms.UrlUpload(d.FunctionName, d.FunctionEnv, d.FunctionThreads, d.FunctionURL, d.SubFolder, envs) 402 | 403 | if err != nil { 404 | w.WriteHeader(http.StatusInternalServerError) 405 | log.Println(err) 406 | return 407 | } 408 | 409 | // return success 410 | w.WriteHeader(http.StatusOK) 411 | fmt.Fprint(w, res) 412 | } 413 | -------------------------------------------------------------------------------- /cmd/manager/rproxy-darwin-arm64.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && arm64 2 | // +build darwin,arm64 3 | 4 | package main 5 | 6 | import _ "embed" 7 | 8 | //go:embed rproxy-darwin-arm64.bin 9 | var RProxyBin []byte 10 | -------------------------------------------------------------------------------- /cmd/manager/rproxy-linux-amd64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && amd64 2 | // +build linux,amd64 3 | 4 | package main 5 | 6 | import _ "embed" 7 | 8 | //go:embed rproxy-linux-amd64.bin 9 | var RProxyBin []byte 10 | -------------------------------------------------------------------------------- /cmd/manager/rproxy-linux-arm.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm 2 | // +build linux,arm 3 | 4 | package main 5 | 6 | import _ "embed" 7 | 8 | //go:embed rproxy-linux-arm.bin 9 | var RProxyBin []byte 10 | -------------------------------------------------------------------------------- /cmd/manager/rproxy-linux-arm64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm64 2 | // +build linux,arm64 3 | 4 | package main 5 | 6 | import _ "embed" 7 | 8 | //go:embed rproxy-linux-arm64.bin 9 | var RProxyBin []byte 10 | -------------------------------------------------------------------------------- /cmd/rproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "github.com/OpenFogStack/tinyFaaS/pkg/coap" 13 | "github.com/OpenFogStack/tinyFaaS/pkg/grpc" 14 | tfhttp "github.com/OpenFogStack/tinyFaaS/pkg/http" 15 | "github.com/OpenFogStack/tinyFaaS/pkg/rproxy" 16 | ) 17 | 18 | func main() { 19 | log.SetFlags(log.LstdFlags | log.Lshortfile) 20 | log.SetPrefix("rproxy: ") 21 | 22 | if len(os.Args) <= 3 { 23 | fmt.Println("Usage: ./rproxy [:]") 24 | os.Exit(1) 25 | } 26 | 27 | rproxyListenAddress := os.Args[1] 28 | 29 | listenAddrs := make(map[string]string) 30 | 31 | for _, arg := range os.Args[2:] { 32 | prot, listenAddr, ok := strings.Cut(arg, ":") 33 | 34 | if !ok { 35 | fmt.Println("Usage: ./rproxy :") 36 | os.Exit(1) 37 | } 38 | 39 | prot = strings.ToLower(prot) 40 | listenAddr = strings.ToLower(listenAddr) 41 | 42 | log.Printf("adding %s listener on %s", prot, listenAddr) 43 | listenAddrs[prot] = listenAddr 44 | } 45 | 46 | if len(listenAddrs) == 0 { 47 | return // nothing to do 48 | } 49 | 50 | r := rproxy.New() 51 | 52 | // CoAP 53 | if listenAddr, ok := listenAddrs["coap"]; ok { 54 | log.Printf("starting coap server on %s", listenAddr) 55 | go coap.Start(r, listenAddr) 56 | } 57 | // HTTP 58 | if listenAddr, ok := listenAddrs["http"]; ok { 59 | log.Printf("starting http server on %s", listenAddr) 60 | go tfhttp.Start(r, listenAddr) 61 | } 62 | // GRPC 63 | if listenAddr, ok := listenAddrs["grpc"]; ok { 64 | log.Printf("starting grpc server on %s", listenAddr) 65 | go grpc.Start(r, listenAddr) 66 | } 67 | 68 | server := http.NewServeMux() 69 | 70 | server.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 71 | if req.Method != "POST" { 72 | w.WriteHeader(http.StatusMethodNotAllowed) 73 | return 74 | } 75 | 76 | log.Printf("have request: %+v", req) 77 | 78 | buf := new(bytes.Buffer) 79 | buf.ReadFrom(req.Body) 80 | newStr := buf.String() 81 | 82 | log.Printf("have body: %s", newStr) 83 | 84 | var def struct { 85 | FunctionResource string `json:"name"` 86 | FunctionContainers []string `json:"ips"` 87 | } 88 | 89 | err := json.Unmarshal([]byte(newStr), &def) 90 | 91 | if err != nil { 92 | w.WriteHeader(http.StatusInternalServerError) 93 | return 94 | } 95 | 96 | log.Printf("have definition: %+v", def) 97 | 98 | if def.FunctionResource[0] == '/' { 99 | def.FunctionResource = def.FunctionResource[1:] 100 | } 101 | 102 | if len(def.FunctionContainers) > 0 { 103 | // "ips" field not empty: add function 104 | log.Printf("adding %s", def.FunctionResource) 105 | err = r.Add(def.FunctionResource, def.FunctionContainers) 106 | if err != nil { 107 | w.WriteHeader(http.StatusInternalServerError) 108 | return 109 | } 110 | w.Header().Set("Content-Type", "text/plain") 111 | w.WriteHeader(http.StatusOK) 112 | w.Write([]byte("OK")) 113 | return 114 | } else { 115 | 116 | log.Printf("deleting %s", def.FunctionResource) 117 | err = r.Del(def.FunctionResource) 118 | if err != nil { 119 | w.WriteHeader(http.StatusInternalServerError) 120 | return 121 | 122 | } 123 | } 124 | }) 125 | 126 | log.Printf("listening on %s", rproxyListenAddress) 127 | err := http.ListenAndServe(rproxyListenAddress, server) 128 | 129 | if err != nil { 130 | log.Printf("%s", err) 131 | } 132 | 133 | log.Printf("exiting") 134 | } 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenFogStack/tinyFaaS 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/docker/docker v27.0.0+incompatible 7 | github.com/google/uuid v1.6.0 8 | github.com/pfandzelter/go-coap v0.1.0 9 | google.golang.org/grpc v1.64.0 10 | google.golang.org/protobuf v1.34.2 11 | ) 12 | 13 | require ( 14 | github.com/Microsoft/go-winio v0.6.2 // indirect 15 | github.com/containerd/containerd v1.7.18 // indirect 16 | github.com/containerd/log v0.1.0 // indirect 17 | github.com/distribution/reference v0.6.0 // indirect 18 | github.com/docker/go-connections v0.5.0 // indirect 19 | github.com/docker/go-units v0.5.0 // indirect 20 | github.com/felixge/httpsnoop v1.0.4 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/klauspost/compress v1.17.9 // indirect 25 | github.com/moby/docker-image-spec v1.3.1 // indirect 26 | github.com/moby/patternmatcher v0.6.0 // indirect 27 | github.com/moby/sys/sequential v0.5.0 // indirect 28 | github.com/moby/sys/user v0.1.0 // indirect 29 | github.com/moby/term v0.5.0 // indirect 30 | github.com/morikuni/aec v1.0.0 // indirect 31 | github.com/opencontainers/go-digest v1.0.0 // indirect 32 | github.com/opencontainers/image-spec v1.1.0 // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/sirupsen/logrus v1.9.3 // indirect 35 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect 36 | go.opentelemetry.io/otel v1.27.0 // indirect 37 | go.opentelemetry.io/otel/metric v1.27.0 // indirect 38 | go.opentelemetry.io/otel/trace v1.27.0 // indirect 39 | golang.org/x/net v0.26.0 // indirect 40 | golang.org/x/sys v0.21.0 // indirect 41 | golang.org/x/text v0.16.0 // indirect 42 | golang.org/x/time v0.5.0 // indirect 43 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect 44 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect 45 | gotest.tools/v3 v3.5.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= 8 | github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 9 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 10 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 11 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 12 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 13 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 14 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 19 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 20 | github.com/docker/docker v27.0.0+incompatible h1:JRugTYuelmWlW0M3jakcIadDx2HUoUO6+Tf2C5jVfwA= 21 | github.com/docker/docker v27.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 22 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 23 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 24 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 25 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 26 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 27 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 28 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 29 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 30 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 31 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 32 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 33 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 34 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 35 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 36 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 37 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 38 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 39 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 40 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 41 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 42 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 43 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 44 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 45 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 46 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 47 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 48 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 49 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 50 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 51 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 52 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 53 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 54 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 55 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 56 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 57 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 58 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 59 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 60 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 61 | github.com/pfandzelter/go-coap v0.1.0 h1:R6RqR3aTxJlnmuFDagiCJqXhN+WuwUoUS4+OY/pPnYY= 62 | github.com/pfandzelter/go-coap v0.1.0/go.mod h1:pNQG3knnPGxs+aCrGUdTDHqy1HqE9175DnhAomuVFM0= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 68 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 72 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 73 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 74 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 75 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= 76 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= 77 | go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= 78 | go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= 79 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 80 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 81 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 82 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 83 | go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= 84 | go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= 85 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 86 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 87 | go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= 88 | go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= 89 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 90 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 93 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 94 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 95 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 100 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 101 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 110 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 111 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 112 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 113 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 114 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 115 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 116 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 117 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 118 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 119 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 120 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 121 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 122 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 123 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 124 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= 126 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= 127 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= 128 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 129 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 130 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 131 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 132 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 133 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 136 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 138 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 139 | -------------------------------------------------------------------------------- /pkg/coap/coap.go: -------------------------------------------------------------------------------- 1 | package coap 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "github.com/OpenFogStack/tinyFaaS/pkg/rproxy" 8 | "github.com/pfandzelter/go-coap" 9 | ) 10 | 11 | const async = false 12 | 13 | func Start(r *rproxy.RProxy, listenAddr string) { 14 | 15 | h := coap.FuncHandler( 16 | func(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message { 17 | 18 | log.Printf("have request: %+v", m) 19 | log.Printf("is confirmable: %v", m.IsConfirmable()) 20 | log.Printf("path: %s", m.PathString()) 21 | 22 | p := m.PathString() 23 | 24 | for p != "" && p[0] == '/' { 25 | p = p[1:] 26 | } 27 | 28 | log.Printf("have request for path: %s (async: %v)", p, async) 29 | 30 | s, res := r.Call(p, m.Payload, async, nil) 31 | 32 | mes := &coap.Message{ 33 | Type: coap.Acknowledgement, 34 | MessageID: m.MessageID, 35 | Token: m.Token, 36 | } 37 | 38 | switch s { 39 | case rproxy.StatusOK: 40 | mes.SetOption(coap.ContentFormat, coap.TextPlain) 41 | mes.Code = coap.Content 42 | mes.Payload = res 43 | case rproxy.StatusAccepted: 44 | mes.Code = coap.Created 45 | case rproxy.StatusNotFound: 46 | mes.Code = coap.NotFound 47 | case rproxy.StatusError: 48 | mes.Code = coap.InternalServerError 49 | } 50 | 51 | return mes 52 | }) 53 | 54 | log.Printf("Starting CoAP server on %s", listenAddr) 55 | 56 | coap.ListenAndServe("udp", listenAddr, h) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/docker/handler.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path" 13 | "sync" 14 | "time" 15 | 16 | "github.com/docker/docker/api/types" 17 | "github.com/docker/docker/api/types/container" 18 | "github.com/docker/docker/api/types/image" 19 | "github.com/docker/docker/api/types/network" 20 | "github.com/docker/docker/client" 21 | "github.com/docker/docker/pkg/archive" 22 | "github.com/docker/docker/pkg/stdcopy" 23 | "github.com/google/uuid" 24 | 25 | "github.com/OpenFogStack/tinyFaaS/pkg/manager" 26 | "github.com/OpenFogStack/tinyFaaS/pkg/util" 27 | ) 28 | 29 | const ( 30 | TmpDir = "./tmp" 31 | containerTimeout = 1 32 | ) 33 | 34 | type dockerHandler struct { 35 | name string 36 | env string 37 | threads int 38 | uniqueName string 39 | filePath string 40 | client *client.Client 41 | network string 42 | containers []string 43 | handlerIPs []string 44 | } 45 | 46 | type DockerBackend struct { 47 | client *client.Client 48 | tinyFaaSID string 49 | } 50 | 51 | func New(tinyFaaSID string) *DockerBackend { 52 | // create docker client 53 | client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 54 | if err != nil { 55 | log.Fatalf("error creating docker client: %s", err) 56 | return nil 57 | } 58 | 59 | return &DockerBackend{ 60 | client: client, 61 | tinyFaaSID: tinyFaaSID, 62 | } 63 | } 64 | 65 | func (db *DockerBackend) Stop() error { 66 | return nil 67 | } 68 | 69 | func (db *DockerBackend) Create(name string, env string, threads int, filedir string, envs map[string]string) (manager.Handler, error) { 70 | 71 | // make a unique function name by appending uuid string to function name 72 | uuid, err := uuid.NewRandom() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | dh := &dockerHandler{ 78 | name: name, 79 | env: env, 80 | client: db.client, 81 | threads: threads, 82 | containers: make([]string, 0, threads), 83 | handlerIPs: make([]string, 0, threads), 84 | } 85 | 86 | dh.uniqueName = name + "-" + uuid.String() 87 | log.Println("creating function", name, "with unique name", dh.uniqueName) 88 | 89 | // make a folder for the function 90 | // mkdir 91 | dh.filePath = path.Join(TmpDir, dh.uniqueName) 92 | 93 | err = os.MkdirAll(dh.filePath, 0777) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | // copy Docker stuff into folder 99 | // cp runtimes//* 100 | 101 | err = util.CopyDirFromEmbed(runtimes, path.Join(runtimesDir, dh.env), dh.filePath) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | log.Println("copied runtime files to folder", dh.filePath) 107 | 108 | // copy function into folder 109 | // into a subfolder called fn 110 | // cp /fn 111 | err = os.MkdirAll(path.Join(dh.filePath, "fn"), 0777) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | err = util.CopyAll(filedir, path.Join(dh.filePath, "fn")) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | // build image 122 | // docker build -t 123 | tar, err := archive.TarWithOptions(dh.filePath, &archive.TarOptions{}) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | r, err := db.client.ImageBuild( 129 | context.Background(), 130 | tar, 131 | types.ImageBuildOptions{ 132 | Tags: []string{dh.uniqueName}, 133 | Remove: true, 134 | Dockerfile: "Dockerfile", 135 | Labels: map[string]string{ 136 | "tinyfaas-function": dh.name, 137 | "tinyFaaS": db.tinyFaaSID, 138 | }, 139 | }, 140 | ) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | defer r.Body.Close() 146 | scanner := bufio.NewScanner(r.Body) 147 | for scanner.Scan() { 148 | log.Println(scanner.Text()) 149 | } 150 | 151 | log.Println("built image", dh.uniqueName) 152 | 153 | // create network 154 | // docker network create 155 | network, err := db.client.NetworkCreate( 156 | context.Background(), 157 | dh.uniqueName, 158 | network.CreateOptions{ 159 | Labels: map[string]string{ 160 | "tinyfaas-function": dh.name, 161 | "tinyFaaS": db.tinyFaaSID, 162 | }, 163 | }, 164 | ) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | dh.network = network.ID 170 | 171 | log.Println("created network", dh.uniqueName, "with id", network.ID) 172 | 173 | e := make([]string, 0, len(envs)) 174 | 175 | for k, v := range envs { 176 | e = append(e, fmt.Sprintf("%s=%s", k, v)) 177 | } 178 | 179 | // create containers 180 | // docker run -d --network --name 181 | for i := 0; i < dh.threads; i++ { 182 | container, err := db.client.ContainerCreate( 183 | context.Background(), 184 | &container.Config{ 185 | Image: dh.uniqueName, 186 | Labels: map[string]string{ 187 | "tinyfaas-function": dh.name, 188 | "tinyFaaS": db.tinyFaaSID, 189 | }, 190 | Env: e, 191 | }, 192 | &container.HostConfig{ 193 | NetworkMode: container.NetworkMode(dh.uniqueName), 194 | }, 195 | nil, 196 | nil, 197 | dh.uniqueName+fmt.Sprintf("-%d", i), 198 | ) 199 | 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | log.Println("created container", container.ID) 205 | 206 | dh.containers = append(dh.containers, container.ID) 207 | } 208 | 209 | // remove folder 210 | // rm -rf 211 | err = os.RemoveAll(dh.filePath) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | log.Println("removed folder", dh.filePath) 217 | 218 | return dh, nil 219 | 220 | } 221 | 222 | func (dh *dockerHandler) IPs() []string { 223 | return dh.handlerIPs 224 | } 225 | 226 | func (dh *dockerHandler) Start() error { 227 | log.Printf("dh: %+v", dh) 228 | 229 | // start containers 230 | // docker start 231 | 232 | wg := sync.WaitGroup{} 233 | for _, c := range dh.containers { 234 | wg.Add(1) 235 | go func(c string) { 236 | err := dh.client.ContainerStart( 237 | context.Background(), 238 | c, 239 | container.StartOptions{}, 240 | ) 241 | wg.Done() 242 | if err != nil { 243 | log.Printf("error starting container %s: %s", c, err) 244 | return 245 | } 246 | 247 | log.Println("started container", c) 248 | }(c) 249 | } 250 | wg.Wait() 251 | 252 | // get container IPs 253 | // docker inspect 254 | for _, container := range dh.containers { 255 | c, err := dh.client.ContainerInspect( 256 | context.Background(), 257 | container, 258 | ) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | dh.handlerIPs = append(dh.handlerIPs, c.NetworkSettings.Networks[dh.uniqueName].IPAddress) 264 | 265 | log.Println("got ip", c.NetworkSettings.Networks[dh.uniqueName].IPAddress, "for container", container) 266 | } 267 | 268 | // wait for the containers to be ready 269 | // curl http://:8000/ready 270 | for i, ip := range dh.handlerIPs { 271 | log.Println("waiting for container", ip, "to be ready") 272 | maxRetries := 10 273 | for { 274 | maxRetries-- 275 | if maxRetries == 0 { 276 | // container did not start properly! 277 | // give people some logs to look at 278 | log.Printf("container %s (ip %s) not ready after 10 retries", dh.containers[i], ip) 279 | log.Printf("getting logs for container %s", dh.containers[i]) 280 | logs, err := dh.getContainerLogs(dh.containers[i]) 281 | 282 | if err != nil { 283 | return fmt.Errorf("container %s not ready after 10 retries, error encountered when getting logs %s", ip, err) 284 | } 285 | 286 | log.Println(logs) 287 | 288 | log.Printf("end of logs for container %s", dh.containers[i]) 289 | 290 | return fmt.Errorf("container %s not ready after 10 retries", ip) 291 | } 292 | 293 | // timeout of 1 second 294 | client := http.Client{ 295 | Timeout: 3 * time.Second, 296 | } 297 | 298 | resp, err := client.Get("http://" + ip + ":8000/health") 299 | if err != nil { 300 | log.Println(err) 301 | log.Println("retrying in 1 second") 302 | time.Sleep(1 * time.Second) 303 | continue 304 | } 305 | resp.Body.Close() 306 | if resp.StatusCode == http.StatusOK { 307 | log.Println("container", ip, "is ready") 308 | break 309 | } 310 | log.Println("container", ip, "is not ready yet, retrying in 1 second") 311 | time.Sleep(1 * time.Second) 312 | } 313 | } 314 | 315 | return nil 316 | } 317 | 318 | func (dh *dockerHandler) Destroy() error { 319 | log.Println("destroying function", dh.name) 320 | log.Printf("dh: %+v", dh) 321 | 322 | wg := sync.WaitGroup{} 323 | log.Printf("stopping containers: %v", dh.containers) 324 | for _, c := range dh.containers { 325 | log.Println("removing container", c) 326 | 327 | wg.Add(1) 328 | go func(c string) { 329 | log.Println("stopping container", c) 330 | 331 | timeout := 1 // seconds 332 | 333 | err := dh.client.ContainerStop( 334 | context.Background(), 335 | c, 336 | container.StopOptions{ 337 | Timeout: &timeout, 338 | }, 339 | ) 340 | if err != nil { 341 | log.Printf("error stopping container %s: %s", c, err) 342 | } 343 | 344 | log.Println("stopped container", c) 345 | 346 | err = dh.client.ContainerRemove( 347 | context.Background(), 348 | c, 349 | container.RemoveOptions{}, 350 | ) 351 | wg.Done() 352 | if err != nil { 353 | log.Printf("error removing container %s: %s", c, err) 354 | } 355 | }(c) 356 | 357 | log.Println("removed container", c) 358 | } 359 | wg.Wait() 360 | 361 | // remove network 362 | // docker network rm 363 | err := dh.client.NetworkRemove( 364 | context.Background(), 365 | dh.network, 366 | ) 367 | if err != nil { 368 | return err 369 | } 370 | 371 | log.Println("removed network", dh.network) 372 | 373 | // remove image 374 | // docker rmi 375 | _, err = dh.client.ImageRemove( 376 | context.Background(), 377 | dh.uniqueName, 378 | image.RemoveOptions{}, 379 | ) 380 | 381 | if err != nil { 382 | return err 383 | } 384 | 385 | log.Println("removed image", dh.uniqueName) 386 | 387 | return nil 388 | } 389 | 390 | func (dh *dockerHandler) getContainerLogs(c string) (string, error) { 391 | logs := "" 392 | 393 | l, err := dh.client.ContainerLogs( 394 | context.Background(), 395 | c, 396 | container.LogsOptions{ 397 | ShowStdout: true, 398 | ShowStderr: true, 399 | Timestamps: true, 400 | }, 401 | ) 402 | if err != nil { 403 | return logs, err 404 | } 405 | 406 | var lstdout bytes.Buffer 407 | var lstderr bytes.Buffer 408 | 409 | _, err = stdcopy.StdCopy(&lstdout, &lstderr, l) 410 | 411 | l.Close() 412 | 413 | if err != nil { 414 | return logs, err 415 | } 416 | 417 | // add a prefix to each line 418 | // function= handler= 419 | scanner := bufio.NewScanner(&lstdout) 420 | 421 | for scanner.Scan() { 422 | logs += fmt.Sprintf("function=%s handler=%s %s\n", dh.name, c, scanner.Text()) 423 | } 424 | 425 | if err := scanner.Err(); err != nil { 426 | return logs, err 427 | } 428 | 429 | // same for stderr 430 | scanner = bufio.NewScanner(&lstderr) 431 | 432 | for scanner.Scan() { 433 | logs += fmt.Sprintf("function=%s handler=%s %s\n", dh.name, c, scanner.Text()) 434 | } 435 | 436 | if err := scanner.Err(); err != nil { 437 | return logs, err 438 | } 439 | 440 | return logs, nil 441 | } 442 | 443 | func (dh *dockerHandler) Logs() (io.Reader, error) { 444 | // get container logs 445 | // docker logs 446 | var logs bytes.Buffer 447 | 448 | for _, c := range dh.containers { 449 | l, err := dh.getContainerLogs(c) 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | logs.WriteString(l) 455 | } 456 | 457 | return &logs, nil 458 | } 459 | -------------------------------------------------------------------------------- /pkg/docker/runtimes-amd64.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 2 | // +build amd64 3 | 4 | package docker 5 | 6 | import "embed" 7 | 8 | //go:embed runtimes-amd64 9 | var runtimes embed.FS 10 | 11 | const runtimesDir = "runtimes-amd64" 12 | -------------------------------------------------------------------------------- /pkg/docker/runtimes-arm.go: -------------------------------------------------------------------------------- 1 | //go:build arm 2 | // +build arm 3 | 4 | package docker 5 | 6 | import "embed" 7 | 8 | //go:embed runtimes-arm 9 | var runtimes embed.FS 10 | 11 | const runtimesDir = "runtimes-arm" 12 | -------------------------------------------------------------------------------- /pkg/docker/runtimes-arm64.go: -------------------------------------------------------------------------------- 1 | //go:build arm64 2 | // +build arm64 3 | 4 | package docker 5 | 6 | import "embed" 7 | 8 | //go:embed runtimes-arm64 9 | var runtimes embed.FS 10 | 11 | const runtimesDir = "runtimes-arm64" 12 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/binary/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ADD blob.tar.gz / 4 | 5 | EXPOSE 8000 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY fn/* ./ 10 | RUN chmod +x fn.sh 11 | 12 | CMD [ "./handler.bin" ] 13 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/binary/build.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.22 2 | ARG ALPINE_VERSION=3.19 3 | 4 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder 5 | 6 | WORKDIR /usr/src/build 7 | COPY functionhandler.go . 8 | RUN GO111MODULE=off CGO_ENABLED=0 go build -o handler.bin . 9 | 10 | FROM alpine:${ALPINE_VERSION} 11 | 12 | # Create app directory 13 | WORKDIR /usr/src/app 14 | 15 | COPY --from=builder /usr/src/build/handler.bin . 16 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/binary/functionhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os/exec" 10 | ) 11 | 12 | func main() { 13 | port := ":8000" 14 | 15 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 16 | switch r.Method { 17 | case http.MethodGet: 18 | if r.URL.Path == "/health" { 19 | w.WriteHeader(http.StatusOK) 20 | fmt.Fprint(w, "OK") 21 | log.Println("reporting health: OK") 22 | return 23 | } 24 | w.WriteHeader(http.StatusNotFound) 25 | return 26 | 27 | case http.MethodPost: 28 | data, err := io.ReadAll(r.Body) 29 | if err != nil { 30 | w.WriteHeader(http.StatusInternalServerError) 31 | fmt.Fprint(w, err) 32 | return 33 | } 34 | cmd := exec.Command("./fn.sh") 35 | cmd.Stdin = bytes.NewReader(data) 36 | output, err := cmd.CombinedOutput() 37 | if err != nil { 38 | w.WriteHeader(http.StatusInternalServerError) 39 | fmt.Fprint(w, err) 40 | return 41 | } 42 | w.WriteHeader(http.StatusOK) 43 | w.Write(output) 44 | return 45 | default: 46 | w.WriteHeader(http.StatusMethodNotAllowed) 47 | return 48 | } 49 | }) 50 | 51 | log.Printf("Server listening on port %s\n", port) 52 | err := http.ListenAndServe(port, nil) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine3.19 2 | 3 | ADD blob.tar.gz / 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY fn/* ./ 8 | 9 | RUN go mod tidy 10 | RUN go mod download 11 | RUN CGO_ENABLED=0 go build -o handler . 12 | 13 | EXPOSE 8000 14 | 15 | CMD ["./handler"] -------------------------------------------------------------------------------- /pkg/docker/runtimes/go/build.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.22 2 | ARG ALPINE_VERSION=3.19 3 | 4 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} 5 | 6 | WORKDIR /usr/src/app 7 | 8 | COPY functionhandler.go ./ -------------------------------------------------------------------------------- /pkg/docker/runtimes/go/functionhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | port := os.Getenv("PORT") 13 | if port == "" { 14 | port = "8000" 15 | } 16 | 17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 18 | switch r.Method { 19 | case http.MethodGet: 20 | if r.URL.Path == "/health" { 21 | w.WriteHeader(http.StatusOK) 22 | fmt.Fprint(w, "OK") 23 | log.Println("reporting health: OK") 24 | return 25 | } 26 | w.WriteHeader(http.StatusNotFound) 27 | return 28 | 29 | case http.MethodPost: 30 | body, err := io.ReadAll(r.Body) 31 | if err != nil { 32 | w.WriteHeader(http.StatusInternalServerError) 33 | fmt.Fprint(w, err) 34 | return 35 | } 36 | 37 | headers := make(map[string]string) 38 | for k, v := range r.Header { 39 | headers[k] = v[0] 40 | } 41 | 42 | result, err := fn(string(body), headers) // returns string, err 43 | if err != nil { 44 | w.WriteHeader(http.StatusInternalServerError) 45 | fmt.Fprint(w, err) 46 | return 47 | } 48 | w.WriteHeader(http.StatusOK) 49 | _, err = w.Write([]byte(result)) 50 | if err != nil { 51 | w.WriteHeader(http.StatusInternalServerError) 52 | fmt.Fprint(w, err) 53 | } 54 | 55 | default: 56 | w.WriteHeader(http.StatusMethodNotAllowed) 57 | return 58 | } 59 | }) 60 | 61 | log.Printf("Server starting on port %s\n", port) 62 | if err := http.ListenAndServe(":"+port, nil); err != nil { 63 | log.Fatalf("Failed to start server: %v", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ADD blob.tar.gz / 4 | 5 | EXPOSE 8000 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY fn fn 10 | 11 | RUN npm install ./fn 12 | 13 | CMD [ "node", "functionhandler.js" ] 14 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/nodejs/build.Dockerfile: -------------------------------------------------------------------------------- 1 | #https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ 2 | ARG NODE_VERSION=20.14 3 | ARG ALPINE_VERSION=3.19 4 | 5 | FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} 6 | 7 | # Create app directory 8 | WORKDIR /usr/src/app 9 | 10 | COPY functionhandler.js . 11 | COPY package.json . 12 | 13 | RUN npm install express && \ 14 | npm install body-parser && \ 15 | npm cache clean --force 16 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/nodejs/functionhandler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | process.chdir("fn"); 3 | 4 | const handler = require("fn"); 5 | const express = require("express"); 6 | const bodyParser = require("body-parser") 7 | const app = express(); 8 | 9 | app.use(bodyParser.text({ 10 | type: function(req) { 11 | return 'text'; 12 | } 13 | })); 14 | 15 | app.all("/health", (req, res) => { 16 | return res.send("OK"); 17 | }); 18 | app.all("/fn", handler); 19 | app.listen(8000); 20 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functionhandler", 3 | "version": "1.0.0", 4 | "main": "functionhandler.js" 5 | } 6 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/python3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ADD blob.tar.gz / 4 | 5 | EXPOSE 8000 6 | 7 | # Create app directory 8 | WORKDIR /usr/src/app 9 | 10 | COPY fn/* ./ 11 | RUN python -m pip install -r requirements.txt --user 12 | 13 | ENV PYTHONUNBUFFERED=1 14 | CMD [ "python3", "functionhandler.py" ] 15 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/python3/build.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.11 2 | ARG ALPINE_VERSION=3.19 3 | 4 | FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} 5 | 6 | # Create app directory 7 | WORKDIR /usr/src/app 8 | 9 | COPY functionhandler.py . 10 | -------------------------------------------------------------------------------- /pkg/docker/runtimes/python3/functionhandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import typing 4 | import http.server 5 | import socketserver 6 | 7 | if __name__ == "__main__": 8 | try: 9 | import fn # type: ignore 10 | except ImportError: 11 | raise ImportError("Failed to import fn.py") 12 | 13 | # create a webserver at port 8080 and execute fn.fn for every request 14 | class tinyFaaSFNHandler(http.server.BaseHTTPRequestHandler): 15 | def do_GET(self) -> None: 16 | print(f"GET {self.path}") 17 | if self.path == "/health": 18 | self.send_response(200) 19 | self.end_headers() 20 | self.wfile.write("OK".encode("utf-8")) 21 | print("reporting health: OK") 22 | return 23 | 24 | self.send_response(404) 25 | self.end_headers() 26 | return 27 | 28 | def do_POST(self) -> None: 29 | d: typing.Optional[str] = self.rfile.read( 30 | int(self.headers["Content-Length"]) 31 | ).decode("utf-8") 32 | if d == "": 33 | d = None 34 | 35 | # Read headers into a dictionary 36 | headers: typing.Dict[str, str] = {k: v for k, v in self.headers.items()} 37 | 38 | try: 39 | res = fn.fn(d, headers) 40 | self.send_response(200) 41 | self.end_headers() 42 | if res is not None: 43 | self.wfile.write(res.encode("utf-8")) 44 | 45 | return 46 | except Exception as e: 47 | print(e) 48 | self.send_response(500) 49 | self.end_headers() 50 | self.wfile.write(str(e).encode("utf-8")) 51 | return 52 | 53 | with socketserver.ThreadingTCPServer(("", 8000), tinyFaaSFNHandler) as httpd: 54 | httpd.serve_forever() 55 | -------------------------------------------------------------------------------- /pkg/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/OpenFogStack/tinyFaaS/pkg/grpc/tinyfaas" 10 | "github.com/OpenFogStack/tinyFaaS/pkg/rproxy" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/metadata" 13 | ) 14 | 15 | // GRPCServer is the grpc endpoint for this tinyFaaS instance. 16 | type GRPCServer struct { 17 | r *rproxy.RProxy 18 | } 19 | 20 | // Request handles a request to the GRPC endpoint of the reverse-proxy of this tinyFaaS instance. 21 | func (gs *GRPCServer) Request(ctx context.Context, d *tinyfaas.Data) (*tinyfaas.Response, error) { 22 | 23 | log.Printf("have request for path: %s (async: %v)", d.FunctionIdentifier, false) 24 | 25 | // Extract metadata from the gRPC context 26 | md, ok := metadata.FromIncomingContext(ctx) 27 | headers := make(map[string]string) 28 | if ok { 29 | // Convert metadata to map[string]string 30 | for k, v := range md { 31 | if len(v) > 0 { 32 | headers[k] = v[0] 33 | } 34 | } 35 | } else { 36 | log.Print("failed to extract metadata from context, using empty headers GRPC request") 37 | } 38 | 39 | s, res := gs.r.Call(d.FunctionIdentifier, []byte(d.Data), false, headers) 40 | 41 | switch s { 42 | case rproxy.StatusOK: 43 | return &tinyfaas.Response{ 44 | Response: string(res), 45 | }, nil 46 | case rproxy.StatusAccepted: 47 | return &tinyfaas.Response{}, nil 48 | case rproxy.StatusNotFound: 49 | return nil, fmt.Errorf("function %s not found", d.FunctionIdentifier) 50 | case rproxy.StatusError: 51 | return nil, fmt.Errorf("error calling function %s", d.FunctionIdentifier) 52 | } 53 | return &tinyfaas.Response{ 54 | Response: string(res), 55 | }, nil 56 | } 57 | 58 | func Start(r *rproxy.RProxy, listenAddr string) { 59 | gs := grpc.NewServer() 60 | 61 | tinyfaas.RegisterTinyFaaSServer(gs, &GRPCServer{ 62 | r: r, 63 | }) 64 | 65 | lis, err := net.Listen("tcp", listenAddr) 66 | 67 | if err != nil { 68 | log.Fatal("Failed to listen") 69 | } 70 | 71 | log.Printf("Starting GRPC server on %s", listenAddr) 72 | defer gs.GracefulStop() 73 | gs.Serve(lis) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/requirements.txt: -------------------------------------------------------------------------------- 1 | grpcio==1.63.0 2 | grpcio-tools==1.63.0 3 | protobuf==5.26.1 4 | mypy-protobuf==3.6.0 -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v5.27.3 5 | // source: tinyfaas.proto 6 | 7 | package tinyfaas 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Data struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | FunctionIdentifier string `protobuf:"bytes,1,opt,name=functionIdentifier,proto3" json:"functionIdentifier,omitempty"` 29 | Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 30 | } 31 | 32 | func (x *Data) Reset() { 33 | *x = Data{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_tinyfaas_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *Data) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*Data) ProtoMessage() {} 46 | 47 | func (x *Data) ProtoReflect() protoreflect.Message { 48 | mi := &file_tinyfaas_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use Data.ProtoReflect.Descriptor instead. 60 | func (*Data) Descriptor() ([]byte, []int) { 61 | return file_tinyfaas_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *Data) GetFunctionIdentifier() string { 65 | if x != nil { 66 | return x.FunctionIdentifier 67 | } 68 | return "" 69 | } 70 | 71 | func (x *Data) GetData() string { 72 | if x != nil { 73 | return x.Data 74 | } 75 | return "" 76 | } 77 | 78 | type Response struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | 83 | Response string `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` 84 | } 85 | 86 | func (x *Response) Reset() { 87 | *x = Response{} 88 | if protoimpl.UnsafeEnabled { 89 | mi := &file_tinyfaas_proto_msgTypes[1] 90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 91 | ms.StoreMessageInfo(mi) 92 | } 93 | } 94 | 95 | func (x *Response) String() string { 96 | return protoimpl.X.MessageStringOf(x) 97 | } 98 | 99 | func (*Response) ProtoMessage() {} 100 | 101 | func (x *Response) ProtoReflect() protoreflect.Message { 102 | mi := &file_tinyfaas_proto_msgTypes[1] 103 | if protoimpl.UnsafeEnabled && x != nil { 104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 105 | if ms.LoadMessageInfo() == nil { 106 | ms.StoreMessageInfo(mi) 107 | } 108 | return ms 109 | } 110 | return mi.MessageOf(x) 111 | } 112 | 113 | // Deprecated: Use Response.ProtoReflect.Descriptor instead. 114 | func (*Response) Descriptor() ([]byte, []int) { 115 | return file_tinyfaas_proto_rawDescGZIP(), []int{1} 116 | } 117 | 118 | func (x *Response) GetResponse() string { 119 | if x != nil { 120 | return x.Response 121 | } 122 | return "" 123 | } 124 | 125 | var File_tinyfaas_proto protoreflect.FileDescriptor 126 | 127 | var file_tinyfaas_proto_rawDesc = []byte{ 128 | 0x0a, 0x0e, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 129 | 0x12, 0x1e, 0x6f, 0x70, 0x65, 0x6e, 0x66, 0x6f, 0x67, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x74, 130 | 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 0x2e, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 131 | 0x22, 0x4a, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x75, 0x6e, 0x63, 132 | 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 133 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 134 | 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 135 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x26, 0x0a, 0x08, 136 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 137 | 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 138 | 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x08, 0x54, 0x69, 0x6e, 0x79, 0x46, 0x61, 0x61, 0x53, 139 | 0x12, 0x59, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x2e, 0x6f, 0x70, 140 | 0x65, 0x6e, 0x66, 0x6f, 0x67, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x74, 0x69, 0x6e, 0x79, 0x66, 141 | 0x61, 0x61, 0x73, 0x2e, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 0x2e, 0x44, 0x61, 0x74, 142 | 0x61, 0x1a, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x66, 0x6f, 0x67, 0x73, 0x74, 0x61, 0x63, 0x6b, 143 | 0x2e, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 0x2e, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 144 | 0x61, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 145 | 0x3b, 0x74, 0x69, 0x6e, 0x79, 0x66, 0x61, 0x61, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 146 | 0x33, 147 | } 148 | 149 | var ( 150 | file_tinyfaas_proto_rawDescOnce sync.Once 151 | file_tinyfaas_proto_rawDescData = file_tinyfaas_proto_rawDesc 152 | ) 153 | 154 | func file_tinyfaas_proto_rawDescGZIP() []byte { 155 | file_tinyfaas_proto_rawDescOnce.Do(func() { 156 | file_tinyfaas_proto_rawDescData = protoimpl.X.CompressGZIP(file_tinyfaas_proto_rawDescData) 157 | }) 158 | return file_tinyfaas_proto_rawDescData 159 | } 160 | 161 | var file_tinyfaas_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 162 | var file_tinyfaas_proto_goTypes = []interface{}{ 163 | (*Data)(nil), // 0: openfogstack.tinyfaas.tinyfaas.Data 164 | (*Response)(nil), // 1: openfogstack.tinyfaas.tinyfaas.Response 165 | } 166 | var file_tinyfaas_proto_depIdxs = []int32{ 167 | 0, // 0: openfogstack.tinyfaas.tinyfaas.TinyFaaS.Request:input_type -> openfogstack.tinyfaas.tinyfaas.Data 168 | 1, // 1: openfogstack.tinyfaas.tinyfaas.TinyFaaS.Request:output_type -> openfogstack.tinyfaas.tinyfaas.Response 169 | 1, // [1:2] is the sub-list for method output_type 170 | 0, // [0:1] is the sub-list for method input_type 171 | 0, // [0:0] is the sub-list for extension type_name 172 | 0, // [0:0] is the sub-list for extension extendee 173 | 0, // [0:0] is the sub-list for field type_name 174 | } 175 | 176 | func init() { file_tinyfaas_proto_init() } 177 | func file_tinyfaas_proto_init() { 178 | if File_tinyfaas_proto != nil { 179 | return 180 | } 181 | if !protoimpl.UnsafeEnabled { 182 | file_tinyfaas_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 183 | switch v := v.(*Data); i { 184 | case 0: 185 | return &v.state 186 | case 1: 187 | return &v.sizeCache 188 | case 2: 189 | return &v.unknownFields 190 | default: 191 | return nil 192 | } 193 | } 194 | file_tinyfaas_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 195 | switch v := v.(*Response); i { 196 | case 0: 197 | return &v.state 198 | case 1: 199 | return &v.sizeCache 200 | case 2: 201 | return &v.unknownFields 202 | default: 203 | return nil 204 | } 205 | } 206 | } 207 | type x struct{} 208 | out := protoimpl.TypeBuilder{ 209 | File: protoimpl.DescBuilder{ 210 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 211 | RawDescriptor: file_tinyfaas_proto_rawDesc, 212 | NumEnums: 0, 213 | NumMessages: 2, 214 | NumExtensions: 0, 215 | NumServices: 1, 216 | }, 217 | GoTypes: file_tinyfaas_proto_goTypes, 218 | DependencyIndexes: file_tinyfaas_proto_depIdxs, 219 | MessageInfos: file_tinyfaas_proto_msgTypes, 220 | }.Build() 221 | File_tinyfaas_proto = out.File 222 | file_tinyfaas_proto_rawDesc = nil 223 | file_tinyfaas_proto_goTypes = nil 224 | file_tinyfaas_proto_depIdxs = nil 225 | } 226 | -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package openfogstack.tinyfaas.tinyfaas; 4 | option go_package = ".;tinyfaas"; 5 | 6 | // Represents a trigger node 7 | service TinyFaaS { rpc Request(Data) returns(Response); } 8 | 9 | message Data { 10 | string functionIdentifier = 1; 11 | string data = 2; 12 | } 13 | 14 | message Response { string response = 1; } -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v5.27.3 5 | // source: tinyfaas.proto 6 | 7 | package tinyfaas 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // TinyFaaSClient is the client API for TinyFaaS service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type TinyFaaSClient interface { 25 | Request(ctx context.Context, in *Data, opts ...grpc.CallOption) (*Response, error) 26 | } 27 | 28 | type tinyFaaSClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewTinyFaaSClient(cc grpc.ClientConnInterface) TinyFaaSClient { 33 | return &tinyFaaSClient{cc} 34 | } 35 | 36 | func (c *tinyFaaSClient) Request(ctx context.Context, in *Data, opts ...grpc.CallOption) (*Response, error) { 37 | out := new(Response) 38 | err := c.cc.Invoke(ctx, "/openfogstack.tinyfaas.tinyfaas.TinyFaaS/Request", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | // TinyFaaSServer is the server API for TinyFaaS service. 46 | // All implementations should embed UnimplementedTinyFaaSServer 47 | // for forward compatibility 48 | type TinyFaaSServer interface { 49 | Request(context.Context, *Data) (*Response, error) 50 | } 51 | 52 | // UnimplementedTinyFaaSServer should be embedded to have forward compatible implementations. 53 | type UnimplementedTinyFaaSServer struct { 54 | } 55 | 56 | func (UnimplementedTinyFaaSServer) Request(context.Context, *Data) (*Response, error) { 57 | return nil, status.Errorf(codes.Unimplemented, "method Request not implemented") 58 | } 59 | 60 | // UnsafeTinyFaaSServer may be embedded to opt out of forward compatibility for this service. 61 | // Use of this interface is not recommended, as added methods to TinyFaaSServer will 62 | // result in compilation errors. 63 | type UnsafeTinyFaaSServer interface { 64 | mustEmbedUnimplementedTinyFaaSServer() 65 | } 66 | 67 | func RegisterTinyFaaSServer(s grpc.ServiceRegistrar, srv TinyFaaSServer) { 68 | s.RegisterService(&TinyFaaS_ServiceDesc, srv) 69 | } 70 | 71 | func _TinyFaaS_Request_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 72 | in := new(Data) 73 | if err := dec(in); err != nil { 74 | return nil, err 75 | } 76 | if interceptor == nil { 77 | return srv.(TinyFaaSServer).Request(ctx, in) 78 | } 79 | info := &grpc.UnaryServerInfo{ 80 | Server: srv, 81 | FullMethod: "/openfogstack.tinyfaas.tinyfaas.TinyFaaS/Request", 82 | } 83 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 84 | return srv.(TinyFaaSServer).Request(ctx, req.(*Data)) 85 | } 86 | return interceptor(ctx, in, info, handler) 87 | } 88 | 89 | // TinyFaaS_ServiceDesc is the grpc.ServiceDesc for TinyFaaS service. 90 | // It's only intended for direct use with grpc.RegisterService, 91 | // and not to be introspected or modified (even as a copy) 92 | var TinyFaaS_ServiceDesc = grpc.ServiceDesc{ 93 | ServiceName: "openfogstack.tinyfaas.tinyfaas.TinyFaaS", 94 | HandlerType: (*TinyFaaSServer)(nil), 95 | Methods: []grpc.MethodDesc{ 96 | { 97 | MethodName: "Request", 98 | Handler: _TinyFaaS_Request_Handler, 99 | }, 100 | }, 101 | Streams: []grpc.StreamDesc{}, 102 | Metadata: "tinyfaas.proto", 103 | } 104 | -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: tinyfaas.proto 4 | # Protobuf Python Version: 5.26.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0etinyfaas.proto\x12\x1eopenfogstack.tinyfaas.tinyfaas\"0\n\x04\x44\x61ta\x12\x1a\n\x12\x66unctionIdentifier\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"\x1c\n\x08Response\x12\x10\n\x08response\x18\x01 \x01(\t2e\n\x08TinyFaaS\x12Y\n\x07Request\x12$.openfogstack.tinyfaas.tinyfaas.Data\x1a(.openfogstack.tinyfaas.tinyfaas.ResponseB\x0cZ\n.;tinyfaasb\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tinyfaas_pb2', _globals) 22 | if not _descriptor._USE_C_DESCRIPTORS: 23 | _globals['DESCRIPTOR']._loaded_options = None 24 | _globals['DESCRIPTOR']._serialized_options = b'Z\n.;tinyfaas' 25 | _globals['_DATA']._serialized_start=50 26 | _globals['_DATA']._serialized_end=98 27 | _globals['_RESPONSE']._serialized_start=100 28 | _globals['_RESPONSE']._serialized_end=128 29 | _globals['_TINYFAAS']._serialized_start=130 30 | _globals['_TINYFAAS']._serialized_end=231 31 | # @@protoc_insertion_point(module_scope) 32 | -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | 6 | import builtins 7 | import google.protobuf.descriptor 8 | import google.protobuf.message 9 | import typing 10 | 11 | DESCRIPTOR: google.protobuf.descriptor.FileDescriptor 12 | 13 | @typing.final 14 | class Data(google.protobuf.message.Message): 15 | DESCRIPTOR: google.protobuf.descriptor.Descriptor 16 | 17 | FUNCTIONIDENTIFIER_FIELD_NUMBER: builtins.int 18 | DATA_FIELD_NUMBER: builtins.int 19 | functionIdentifier: builtins.str 20 | data: builtins.str 21 | def __init__( 22 | self, 23 | *, 24 | functionIdentifier: builtins.str = ..., 25 | data: builtins.str = ..., 26 | ) -> None: ... 27 | def ClearField(self, field_name: typing.Literal["data", b"data", "functionIdentifier", b"functionIdentifier"]) -> None: ... 28 | 29 | global___Data = Data 30 | 31 | @typing.final 32 | class Response(google.protobuf.message.Message): 33 | DESCRIPTOR: google.protobuf.descriptor.Descriptor 34 | 35 | RESPONSE_FIELD_NUMBER: builtins.int 36 | response: builtins.str 37 | def __init__( 38 | self, 39 | *, 40 | response: builtins.str = ..., 41 | ) -> None: ... 42 | def ClearField(self, field_name: typing.Literal["response", b"response"]) -> None: ... 43 | 44 | global___Response = Response 45 | -------------------------------------------------------------------------------- /pkg/grpc/tinyfaas/tinyfaas_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | import warnings 5 | 6 | import tinyfaas_pb2 as tinyfaas__pb2 7 | 8 | GRPC_GENERATED_VERSION = '1.63.0' 9 | GRPC_VERSION = grpc.__version__ 10 | EXPECTED_ERROR_RELEASE = '1.65.0' 11 | SCHEDULED_RELEASE_DATE = 'June 25, 2024' 12 | _version_not_supported = False 13 | 14 | try: 15 | from grpc._utilities import first_version_is_lower 16 | _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) 17 | except ImportError: 18 | _version_not_supported = True 19 | 20 | if _version_not_supported: 21 | warnings.warn( 22 | f'The grpc package installed is at version {GRPC_VERSION},' 23 | + f' but the generated code in tinyfaas_pb2_grpc.py depends on' 24 | + f' grpcio>={GRPC_GENERATED_VERSION}.' 25 | + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' 26 | + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' 27 | + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' 28 | + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', 29 | RuntimeWarning 30 | ) 31 | 32 | 33 | class TinyFaaSStub(object): 34 | """Represents a trigger node 35 | """ 36 | 37 | def __init__(self, channel): 38 | """Constructor. 39 | 40 | Args: 41 | channel: A grpc.Channel. 42 | """ 43 | self.Request = channel.unary_unary( 44 | '/openfogstack.tinyfaas.tinyfaas.TinyFaaS/Request', 45 | request_serializer=tinyfaas__pb2.Data.SerializeToString, 46 | response_deserializer=tinyfaas__pb2.Response.FromString, 47 | _registered_method=True) 48 | 49 | 50 | class TinyFaaSServicer(object): 51 | """Represents a trigger node 52 | """ 53 | 54 | def Request(self, request, context): 55 | """Missing associated documentation comment in .proto file.""" 56 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 57 | context.set_details('Method not implemented!') 58 | raise NotImplementedError('Method not implemented!') 59 | 60 | 61 | def add_TinyFaaSServicer_to_server(servicer, server): 62 | rpc_method_handlers = { 63 | 'Request': grpc.unary_unary_rpc_method_handler( 64 | servicer.Request, 65 | request_deserializer=tinyfaas__pb2.Data.FromString, 66 | response_serializer=tinyfaas__pb2.Response.SerializeToString, 67 | ), 68 | } 69 | generic_handler = grpc.method_handlers_generic_handler( 70 | 'openfogstack.tinyfaas.tinyfaas.TinyFaaS', rpc_method_handlers) 71 | server.add_generic_rpc_handlers((generic_handler,)) 72 | 73 | 74 | # This class is part of an EXPERIMENTAL API. 75 | class TinyFaaS(object): 76 | """Represents a trigger node 77 | """ 78 | 79 | @staticmethod 80 | def Request(request, 81 | target, 82 | options=(), 83 | channel_credentials=None, 84 | call_credentials=None, 85 | insecure=False, 86 | compression=None, 87 | wait_for_ready=None, 88 | timeout=None, 89 | metadata=None): 90 | return grpc.experimental.unary_unary( 91 | request, 92 | target, 93 | '/openfogstack.tinyfaas.tinyfaas.TinyFaaS/Request', 94 | tinyfaas__pb2.Data.SerializeToString, 95 | tinyfaas__pb2.Response.FromString, 96 | options, 97 | channel_credentials, 98 | insecure, 99 | call_credentials, 100 | compression, 101 | wait_for_ready, 102 | timeout, 103 | metadata, 104 | _registered_method=True) 105 | -------------------------------------------------------------------------------- /pkg/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/OpenFogStack/tinyFaaS/pkg/rproxy" 9 | ) 10 | 11 | func Start(r *rproxy.RProxy, listenAddr string) { 12 | 13 | mux := http.NewServeMux() 14 | 15 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 16 | p := req.URL.Path 17 | 18 | for p != "" && p[0] == '/' { 19 | p = p[1:] 20 | } 21 | 22 | async := req.Header.Get("X-tinyFaaS-Async") != "" 23 | 24 | log.Printf("have request for path: %s (async: %v)", p, async) 25 | 26 | req_body, err := io.ReadAll(req.Body) 27 | 28 | if err != nil { 29 | w.WriteHeader(http.StatusInternalServerError) 30 | log.Print(err) 31 | return 32 | } 33 | 34 | headers := make(map[string]string) 35 | for k, v := range req.Header { 36 | headers[k] = v[0] 37 | } 38 | 39 | s, res := r.Call(p, req_body, async, headers) 40 | 41 | switch s { 42 | case rproxy.StatusOK: 43 | w.WriteHeader(http.StatusOK) 44 | w.Write(res) 45 | case rproxy.StatusAccepted: 46 | w.WriteHeader(http.StatusAccepted) 47 | case rproxy.StatusNotFound: 48 | w.WriteHeader(http.StatusNotFound) 49 | case rproxy.StatusError: 50 | w.WriteHeader(http.StatusInternalServerError) 51 | } 52 | }) 53 | 54 | log.Printf("Starting HTTP server on %s", listenAddr) 55 | err := http.ListenAndServe(listenAddr, mux) 56 | 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | log.Print("HTTP server stopped") 62 | 63 | } 64 | -------------------------------------------------------------------------------- /pkg/manager/manager.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "os" 13 | "path" 14 | "sync" 15 | 16 | "github.com/OpenFogStack/tinyFaaS/pkg/util" 17 | "github.com/google/uuid" 18 | ) 19 | 20 | const ( 21 | TmpDir = "./tmp" 22 | ) 23 | 24 | type ManagementService struct { 25 | id string 26 | backend Backend 27 | functionHandlers map[string]Handler 28 | functionHandlersMutex sync.Mutex 29 | rproxyListenAddress string 30 | rproxyPort map[string]int 31 | rproxyConfigPort int 32 | } 33 | 34 | type Backend interface { 35 | Create(name string, env string, threads int, filedir string, envs map[string]string) (Handler, error) 36 | Stop() error 37 | } 38 | 39 | type Handler interface { 40 | IPs() []string 41 | Start() error 42 | Destroy() error 43 | Logs() (io.Reader, error) 44 | } 45 | 46 | func New(id string, rproxyListenAddress string, rproxyPort map[string]int, rproxyConfigPort int, tfBackend Backend) *ManagementService { 47 | 48 | ms := &ManagementService{ 49 | id: id, 50 | backend: tfBackend, 51 | functionHandlers: make(map[string]Handler), 52 | rproxyListenAddress: rproxyListenAddress, 53 | rproxyPort: rproxyPort, 54 | rproxyConfigPort: rproxyConfigPort, 55 | } 56 | 57 | return ms 58 | } 59 | 60 | func (ms *ManagementService) createFunction(name string, env string, threads int, funczip []byte, subfolderPath string, envs map[string]string) (string, error) { 61 | 62 | // only allow alphanumeric characters 63 | if !util.IsAlphaNumeric(name) { 64 | return "", fmt.Errorf("function name %s contains non-alphanumeric characters", name) 65 | } 66 | 67 | // make a uuidv4 for the function 68 | uuid, err := uuid.NewRandom() 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | log.Println("creating function", name, "with uuid", uuid.String()) 74 | 75 | // create a new function handler 76 | 77 | p := path.Join(TmpDir, uuid.String()) 78 | 79 | err = os.MkdirAll(p, 0777) 80 | 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | log.Println("created folder", p) 86 | 87 | // write zip to file 88 | zipPath := path.Join(TmpDir, uuid.String()+".zip") 89 | err = os.WriteFile(zipPath, funczip, 0777) 90 | 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | err = util.Unzip(zipPath, p) 96 | 97 | if err != nil { 98 | return "", err 99 | } 100 | 101 | defer func() { 102 | // remove folder 103 | err = os.RemoveAll(p) 104 | if err != nil { 105 | log.Println("error removing folder", p, err) 106 | } 107 | 108 | err = os.Remove(zipPath) 109 | if err != nil { 110 | log.Println("error removing zip", zipPath, err) 111 | } 112 | 113 | log.Println("removed folder", p) 114 | log.Println("removed zip", zipPath) 115 | }() 116 | 117 | if subfolderPath != "" { 118 | p = path.Join(p, subfolderPath) 119 | } 120 | 121 | // if function already exists, keep it while deploying the new version 122 | var oldHandler Handler 123 | if existingHandler, ok := ms.functionHandlers[name]; ok { 124 | oldHandler = existingHandler 125 | } 126 | 127 | // create new function handler 128 | ms.functionHandlersMutex.Lock() 129 | defer ms.functionHandlersMutex.Unlock() 130 | 131 | fh, err := ms.backend.Create(name, env, threads, p, envs) 132 | 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | ms.functionHandlers[name] = fh 138 | 139 | err = ms.functionHandlers[name].Start() 140 | 141 | if err != nil { 142 | // container did not start properly... 143 | return "", err 144 | } 145 | 146 | // tell rproxy about the new function 147 | // curl -X POST http://localhost:80/add -d '{"name": "", "ips": ["", ""]}' 148 | d := struct { 149 | FunctionName string `json:"name"` 150 | FunctionIPs []string `json:"ips"` 151 | }{ 152 | FunctionName: name, 153 | FunctionIPs: fh.IPs(), 154 | } 155 | 156 | b, err := json.Marshal(d) 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | log.Println("telling rproxy about new function", name, "with ips", fh.IPs(), ":", d) 162 | 163 | resp, err := http.Post(fmt.Sprintf("http://%s:%d", ms.rproxyListenAddress, ms.rproxyConfigPort), "application/json", bytes.NewBuffer(b)) 164 | if err != nil && !errors.Is(err, io.EOF) { 165 | log.Println("error telling rproxy about new function", name, err) 166 | return "", err 167 | } 168 | 169 | if resp.StatusCode != http.StatusOK { 170 | return "", fmt.Errorf("rproxy returned status code %d", resp.StatusCode) 171 | } 172 | 173 | defer resp.Body.Close() 174 | 175 | r, err := io.ReadAll(resp.Body) 176 | if err != nil { 177 | return "", err 178 | } 179 | 180 | log.Println("rproxy response:", string(r)) 181 | 182 | // destroy the old handler if it exists 183 | if oldHandler != nil { 184 | err = oldHandler.Destroy() 185 | if err != nil { 186 | return "", err 187 | } 188 | } 189 | 190 | return name, nil 191 | } 192 | 193 | func (ms *ManagementService) Logs() (io.Reader, error) { 194 | 195 | var logs bytes.Buffer 196 | 197 | for name := range ms.functionHandlers { 198 | l, err := ms.LogsFunction(name) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | _, err = io.Copy(&logs, l) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | logs.WriteString("\n") 209 | } 210 | 211 | return &logs, nil 212 | } 213 | 214 | func (ms *ManagementService) LogsFunction(name string) (io.Reader, error) { 215 | 216 | fh, ok := ms.functionHandlers[name] 217 | if !ok { 218 | return nil, fmt.Errorf("function %s not found", name) 219 | } 220 | 221 | return fh.Logs() 222 | } 223 | 224 | func (ms *ManagementService) List() []string { 225 | list := make([]string, 0, len(ms.functionHandlers)) 226 | for name := range ms.functionHandlers { 227 | list = append(list, name) 228 | } 229 | 230 | return list 231 | } 232 | 233 | func (ms *ManagementService) Wipe() error { 234 | for name := range ms.functionHandlers { 235 | log.Println("destroying function", name) 236 | ms.Delete(name) 237 | } 238 | 239 | return nil 240 | } 241 | func (ms *ManagementService) Delete(name string) error { 242 | 243 | fh, ok := ms.functionHandlers[name] 244 | if !ok { 245 | return fmt.Errorf("function %s not found", name) 246 | } 247 | 248 | log.Println("destroying function", name) 249 | 250 | ms.functionHandlersMutex.Lock() 251 | defer ms.functionHandlersMutex.Unlock() 252 | 253 | err := fh.Destroy() 254 | if err != nil { 255 | return err 256 | } 257 | 258 | // tell rproxy about the delete function 259 | // curl -X POST http://localhost:80 -d '{"name": ""}' 260 | d := struct { 261 | FunctionName string `json:"name"` 262 | }{ 263 | FunctionName: name, 264 | } 265 | 266 | b, err := json.Marshal(d) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | log.Println("telling rproxy about deleted function", name) 272 | 273 | resp, err := http.Post(fmt.Sprintf("http://%s:%d", ms.rproxyListenAddress, ms.rproxyConfigPort), "application/json", bytes.NewBuffer(b)) 274 | 275 | if err != nil && !errors.Is(err, io.EOF) { 276 | return err 277 | } 278 | 279 | if resp.StatusCode != http.StatusOK { 280 | return fmt.Errorf("rproxy returned status code %d", resp.StatusCode) 281 | } 282 | 283 | defer resp.Body.Close() 284 | 285 | r, err := io.ReadAll(resp.Body) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | log.Println("rproxy response:", string(r)) 291 | 292 | delete(ms.functionHandlers, name) 293 | 294 | return nil 295 | } 296 | 297 | func (ms *ManagementService) Upload(name string, env string, threads int, zipped string, envs map[string]string) (string, error) { 298 | 299 | // b64 decode zip 300 | zip, err := base64.StdEncoding.DecodeString(zipped) 301 | if err != nil { 302 | // w.WriteHeader(http.StatusBadRequest) 303 | log.Println(err) 304 | return "", err 305 | } 306 | 307 | // create function handler 308 | n, err := ms.createFunction(name, env, threads, zip, "", envs) 309 | 310 | if err != nil { 311 | // w.WriteHeader(http.StatusInternalServerError) 312 | log.Println(err) 313 | return "", err 314 | } 315 | 316 | // return success 317 | // w.WriteHeader(http.StatusOK) 318 | r := "" 319 | for prot, port := range ms.rproxyPort { 320 | r += fmt.Sprintf("%s://%s:%d/%s\n", prot, ms.rproxyListenAddress, port, n) 321 | } 322 | 323 | return r, nil 324 | } 325 | 326 | func (ms *ManagementService) UrlUpload(name string, env string, threads int, funcurl string, subfolder string, envs map[string]string) (string, error) { 327 | 328 | // download url 329 | resp, err := http.Get(funcurl) 330 | if err != nil { 331 | // w.WriteHeader(http.StatusBadRequest) 332 | log.Println(err) 333 | return "", err 334 | } 335 | 336 | // reading body to memory 337 | // not the smartest thing 338 | zip, err := io.ReadAll(resp.Body) 339 | 340 | if err != nil { 341 | // w.WriteHeader(http.StatusBadRequest) 342 | log.Println(err) 343 | return "", err 344 | } 345 | 346 | // create function handler 347 | n, err := ms.createFunction(name, env, threads, zip, subfolder, envs) 348 | 349 | if err != nil { 350 | // w.WriteHeader(http.StatusInternalServerError) 351 | log.Println(err) 352 | return "", err 353 | } 354 | 355 | // return success 356 | // w.WriteHeader(http.StatusOK) 357 | r := "" 358 | for prot, port := range ms.rproxyPort { 359 | r += fmt.Sprintf("%s://%s:%d/%s\n", prot, ms.rproxyListenAddress, port, n) 360 | } 361 | 362 | return r, nil 363 | } 364 | 365 | func (ms *ManagementService) Stop() error { 366 | err := ms.Wipe() 367 | if err != nil { 368 | return err 369 | } 370 | 371 | return ms.backend.Stop() 372 | } 373 | -------------------------------------------------------------------------------- /pkg/rproxy/rproxy.go: -------------------------------------------------------------------------------- 1 | package rproxy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "regexp" 11 | "sync" 12 | ) 13 | 14 | type Status uint32 15 | 16 | const ( 17 | StatusOK Status = iota 18 | StatusAccepted 19 | StatusNotFound 20 | StatusError 21 | ) 22 | 23 | type RProxy struct { 24 | hosts map[string][]string 25 | hl sync.RWMutex 26 | } 27 | 28 | func New() *RProxy { 29 | return &RProxy{ 30 | hosts: make(map[string][]string), 31 | } 32 | } 33 | 34 | func (r *RProxy) Add(name string, ips []string) error { 35 | if len(ips) == 0 { 36 | return fmt.Errorf("no ips given") 37 | } 38 | 39 | r.hl.Lock() 40 | defer r.hl.Unlock() 41 | 42 | // if function exists, we should update! 43 | // if _, ok := r.hosts[name]; ok { 44 | // return fmt.Errorf("function already exists") 45 | // } 46 | 47 | r.hosts[name] = ips 48 | return nil 49 | } 50 | 51 | func (r *RProxy) Del(name string) error { 52 | r.hl.Lock() 53 | defer r.hl.Unlock() 54 | 55 | if _, ok := r.hosts[name]; !ok { 56 | return fmt.Errorf("function not found") 57 | } 58 | 59 | delete(r.hosts, name) 60 | return nil 61 | } 62 | 63 | func (r *RProxy) Call(name string, payload []byte, async bool, headers map[string]string) (Status, []byte) { 64 | 65 | handler, ok := r.hosts[name] 66 | 67 | if !ok { 68 | log.Printf("function not found: %s", name) 69 | return StatusNotFound, nil 70 | } 71 | 72 | log.Printf("have handlers: %s", handler) 73 | 74 | // choose random handler 75 | h := handler[rand.Intn(len(handler))] 76 | 77 | log.Printf("chosen handler: %s", h) 78 | 79 | req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:8000/fn", h), bytes.NewBuffer(payload)) 80 | if err != nil { 81 | log.Print(err) 82 | return StatusError, nil 83 | } 84 | for k, v := range headers { 85 | cleanedKey := cleanHeaderKey(k) // remove special chars from key 86 | req.Header.Set(cleanedKey, v) 87 | } 88 | 89 | // call function asynchronously 90 | if async { 91 | log.Printf("async request accepted") 92 | go func() { 93 | resp, err2 := http.DefaultClient.Do(req) 94 | if err2 != nil { 95 | return 96 | } 97 | resp.Body.Close() 98 | log.Printf("async request finished") 99 | }() 100 | return StatusAccepted, nil 101 | } 102 | 103 | // call function and return results 104 | log.Printf("sync request starting") 105 | resp, err := http.DefaultClient.Do(req) 106 | if err != nil { 107 | log.Print(err) 108 | return StatusError, nil 109 | } 110 | 111 | log.Printf("sync request finished") 112 | 113 | defer resp.Body.Close() 114 | res_body, err := io.ReadAll(resp.Body) 115 | 116 | if err != nil { 117 | log.Print(err) 118 | return StatusError, nil 119 | } 120 | 121 | // log.Printf("have response for sync request: %s", res_body) 122 | 123 | return StatusOK, res_body 124 | } 125 | func cleanHeaderKey(key string) string { 126 | // a regex pattern to match special characters 127 | re := regexp.MustCompile(`[:()<>@,;:\"/[\]?={} \t]`) 128 | // Replace special characters with an empty string 129 | return re.ReplaceAllString(key, "") 130 | } 131 | -------------------------------------------------------------------------------- /pkg/util/copy.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | /* MIT License 14 | * 15 | * Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com] 16 | * 17 | * Permission is hereby granted, free of charge, to any person obtaining a copy 18 | * of this software and associated documentation files (the "Software"), to deal 19 | * in the Software without restriction, including without limitation the rights 20 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | * copies of the Software, and to permit persons to whom the Software is 22 | * furnished to do so, subject to the following conditions: 23 | * 24 | * The above copyright notice and this permission notice shall be included in all 25 | * copies or substantial portions of the Software. 26 | * 27 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | * SOFTWARE. 34 | */ 35 | 36 | // CopyFile copies the contents of the file named src to the file named 37 | // by dst. The file will be created if it does not already exist. If the 38 | // destination file exists, all it's contents will be replaced by the contents 39 | // of the source file. The file mode will be copied from the source and 40 | // the copied data is synced/flushed to stable storage. 41 | func CopyFile(src, dst string) (err error) { 42 | in, err := os.Open(src) 43 | if err != nil { 44 | return 45 | } 46 | defer in.Close() 47 | 48 | _, err = os.Stat(dst) 49 | if err == nil || !errors.Is(err, fs.ErrNotExist) { 50 | log.Printf("Destination file %s already exists, skipping", dst) 51 | return 52 | } 53 | 54 | out, err := os.Create(dst) 55 | if err != nil { 56 | return 57 | } 58 | defer func() { 59 | if e := out.Close(); e != nil { 60 | err = e 61 | } 62 | }() 63 | 64 | _, err = io.Copy(out, in) 65 | if err != nil { 66 | return 67 | } 68 | 69 | err = out.Sync() 70 | if err != nil { 71 | return 72 | } 73 | 74 | si, err := os.Stat(src) 75 | if err != nil { 76 | return 77 | } 78 | err = os.Chmod(dst, si.Mode()) 79 | if err != nil { 80 | return 81 | } 82 | 83 | return 84 | } 85 | 86 | // CopyDir recursively copies a directory tree, attempting to preserve permissions. 87 | // Source directory must exist, destination directory must *not* exist. 88 | // Symlinks are ignored and skipped. 89 | func CopyDir(src string, dst string) (err error) { 90 | src = filepath.Clean(src) 91 | dst = filepath.Clean(dst) 92 | 93 | si, err := os.Stat(src) 94 | if err != nil { 95 | return err 96 | } 97 | if !si.IsDir() { 98 | return fmt.Errorf("source is not a directory") 99 | } 100 | 101 | _, err = os.Stat(dst) 102 | if err != nil && !os.IsNotExist(err) { 103 | return 104 | } 105 | if err == nil { 106 | return fmt.Errorf("destination already exists") 107 | } 108 | 109 | err = os.MkdirAll(dst, si.Mode()) 110 | if err != nil { 111 | return 112 | } 113 | 114 | entries, err := os.ReadDir(src) 115 | if err != nil { 116 | return 117 | } 118 | 119 | for _, entry := range entries { 120 | srcPath := filepath.Join(src, entry.Name()) 121 | dstPath := filepath.Join(dst, entry.Name()) 122 | 123 | if entry.IsDir() { 124 | err = CopyDir(srcPath, dstPath) 125 | if err != nil { 126 | return 127 | } 128 | } else { 129 | // Skip symlinks. 130 | if entry.Type()&fs.ModeSymlink != 0 { 131 | continue 132 | } 133 | 134 | err = CopyFile(srcPath, dstPath) 135 | if err != nil { 136 | return 137 | } 138 | } 139 | } 140 | 141 | return 142 | } 143 | 144 | // CopyAll recursively copies a directory tree, attempting to preserve permissions. 145 | // Source directory must exist, destination directory must exist. 146 | // Symlinks are ignored and skipped. 147 | func CopyAll(src string, dst string) (err error) { 148 | src = filepath.Clean(src) 149 | dst = filepath.Clean(dst) 150 | 151 | si, err := os.Stat(src) 152 | if err != nil { 153 | return err 154 | } 155 | if !si.IsDir() { 156 | return fmt.Errorf("source is not a directory") 157 | } 158 | 159 | _, err = os.Stat(dst) 160 | if err != nil { 161 | return err 162 | } 163 | if !si.IsDir() { 164 | return fmt.Errorf("destination is not a directory") 165 | } 166 | 167 | entries, err := os.ReadDir(src) 168 | if err != nil { 169 | return 170 | } 171 | 172 | for _, entry := range entries { 173 | srcPath := filepath.Join(src, entry.Name()) 174 | dstPath := filepath.Join(dst, entry.Name()) 175 | 176 | if entry.IsDir() { 177 | err = CopyDir(srcPath, dstPath) 178 | if err != nil { 179 | return 180 | } 181 | } else { 182 | // Skip symlinks. 183 | if entry.Type()&fs.ModeSymlink != 0 { 184 | continue 185 | } 186 | 187 | err = CopyFile(srcPath, dstPath) 188 | if err != nil { 189 | return 190 | } 191 | } 192 | } 193 | 194 | return 195 | } 196 | -------------------------------------------------------------------------------- /pkg/util/copyembed.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "io" 7 | "io/fs" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | func CopyFileFromEmbed(src embed.FS, srcPath string, dstPath string) (err error) { 14 | srcFile, err := src.Open(srcPath) 15 | if err != nil { 16 | return 17 | } 18 | defer srcFile.Close() 19 | 20 | _, err = os.Stat(dstPath) 21 | if err == nil || !errors.Is(err, fs.ErrNotExist) { 22 | log.Printf("Destination file %s already exists, skipping", dstPath) 23 | return 24 | } 25 | 26 | err = os.MkdirAll(filepath.Dir(dstPath), 0755) 27 | if err != nil { 28 | return 29 | } 30 | 31 | dstFile, err := os.Create(dstPath) 32 | 33 | if err != nil { 34 | return 35 | } 36 | 37 | defer func() { 38 | if e := dstFile.Close(); e != nil { 39 | err = e 40 | } 41 | }() 42 | 43 | _, err = io.Copy(dstFile, srcFile) 44 | 45 | if err != nil { 46 | return 47 | } 48 | 49 | err = dstFile.Sync() 50 | 51 | if err != nil { 52 | return 53 | } 54 | 55 | return 56 | 57 | } 58 | 59 | func CopyDirFromEmbed(src embed.FS, srcPath string, dstPath string) (err error) { 60 | entries, err := fs.ReadDir(src, srcPath) 61 | 62 | if err != nil { 63 | return 64 | } 65 | 66 | err = os.MkdirAll(dstPath, 0755) 67 | if err != nil { 68 | return 69 | } 70 | 71 | for _, entry := range entries { 72 | srcPath := filepath.Join(srcPath, entry.Name()) 73 | dstPath := filepath.Join(dstPath, entry.Name()) 74 | 75 | if entry.IsDir() { 76 | err = CopyDirFromEmbed(src, srcPath, dstPath) 77 | if err != nil { 78 | return 79 | } 80 | } else { 81 | // Skip symlinks. 82 | if entry.Type()&fs.ModeSymlink != 0 { 83 | continue 84 | } 85 | 86 | err = CopyFileFromEmbed(src, srcPath, dstPath) 87 | if err != nil { 88 | return 89 | } 90 | } 91 | } 92 | 93 | return 94 | 95 | } 96 | -------------------------------------------------------------------------------- /pkg/util/name.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "regexp" 4 | 5 | func IsAlphaNumeric(s string) bool { 6 | reg := regexp.MustCompile(`^[a-zA-Z0-9]+$`) 7 | 8 | return reg.MatchString(s) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/zip.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "log" 7 | "os" 8 | "path" 9 | ) 10 | 11 | func Unzip(zipPath string, p string) error { 12 | 13 | log.Printf("Unzipping %s to %s", zipPath, p) 14 | 15 | archive, err := zip.OpenReader(zipPath) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | // extract zip 21 | for _, f := range archive.File { 22 | log.Printf("Extracting %s", f.Name) 23 | 24 | if f.FileInfo().IsDir() { 25 | path := path.Join(p, f.Name) 26 | log.Printf("Creating directory %s in %s", f.Name, path) 27 | 28 | err = os.MkdirAll(path, 0777) 29 | if err != nil { 30 | return err 31 | } 32 | continue 33 | } 34 | 35 | // open file 36 | rc, err := f.Open() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // create file 42 | path := path.Join(p, f.Name) 43 | // err = os.MkdirAll(path, 0777) 44 | // if err != nil { 45 | // return err 46 | // } 47 | 48 | // write file 49 | w, err := os.Create(path) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // copy 55 | _, err = io.Copy(w, rc) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | log.Printf("Extracted %s to %s", f.Name, path) 61 | 62 | // close 63 | rc.Close() 64 | w.Close() 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /scripts/delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #delete.sh function-name 4 | 5 | set -e 6 | 7 | if ! command -v curl &> /dev/null 8 | then 9 | echo "curl could not be found but is a pre-requisite for this script" 10 | exit 11 | fi 12 | 13 | curl http://localhost:8080/delete --data "{\"name\": \"$1\"}" 14 | -------------------------------------------------------------------------------- /scripts/get_logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | for line in $(docker network ls --filter name=handler-net -q) ; do 6 | for cont in $(docker ps -a -q --filter network="$line") ; do 7 | docker logs "$cont" 8 | done 9 | done 10 | -------------------------------------------------------------------------------- /scripts/list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if ! command -v curl &> /dev/null 6 | then 7 | echo "curl could not be found but is a pre-requisite for this script" 8 | exit 9 | fi 10 | 11 | curl localhost:8080/list 12 | -------------------------------------------------------------------------------- /scripts/logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if ! command -v curl &> /dev/null 6 | then 7 | echo "curl could not be found but is a pre-requisite for this script" 8 | exit 9 | fi 10 | 11 | curl localhost:8080/logs 12 | -------------------------------------------------------------------------------- /scripts/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # upload.sh folder-name name env threads 4 | 5 | set -e 6 | 7 | if ! command -v curl &> /dev/null 8 | then 9 | echo "curl could not be found but is a pre-requisite for this script" 10 | exit 11 | fi 12 | 13 | if ! command -v zip &> /dev/null 14 | then 15 | echo "zip could not be found but is a pre-requisite for this script" 16 | exit 17 | fi 18 | 19 | if ! command -v base64 &> /dev/null 20 | then 21 | echo "base64 could not be found but is a pre-requisite for this script" 22 | exit 23 | fi 24 | 25 | pushd "$1" >/dev/null || exit 26 | curl http://localhost:8080/upload --data "{\"name\": \"$2\", \"env\": \"$3\", \"threads\": $4, \"zip\": \"$(zip -r - ./* | base64 | tr -d '\n')\"}" 27 | popd >/dev/null || exit 28 | -------------------------------------------------------------------------------- /scripts/uploadURL.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # uploadURL.sh url subfolder name env threads 4 | 5 | set -e 6 | 7 | if ! command -v curl &> /dev/null 8 | then 9 | echo "curl could not be found but is a pre-requisite for this script" 10 | exit 11 | fi 12 | 13 | curl http://localhost:8080/uploadURL --data "{\"name\": \"$3\", \"env\": \"$4\",\"threads\": $5,\"url\": \"$1\",\"subfolder_path\": \"$2\"}" 14 | -------------------------------------------------------------------------------- /scripts/wipe-functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #wipe-functions.sh 4 | 5 | set -e 6 | 7 | if ! command -v curl &> /dev/null 8 | then 9 | echo "curl could not be found but is a pre-requisite for this script" 10 | exit 11 | fi 12 | 13 | curl http://localhost:8080/wipe --data "" 14 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | tf_test.out -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # tinyFaaS Tests 2 | 3 | This directory contains tests for tinyFaaS. 4 | These tests use Python3 and the `unittest` package, which is part of the Python3 5 | standard library. 6 | 7 | Further, these tests start a local tinyFaaS instance, assuming no instance is 8 | already running. 9 | This requires `make` and Docker to be installed. 10 | 11 | Additional Python3 packages are necessary for some tests. 12 | These can be installed with `python3 -m pip install -r requirements.txt`. 13 | Alternatively, you can also use a virtual environment. 14 | If these packages are not installed, some tests may be skipped. 15 | 16 | Run these tests with: 17 | 18 | ```sh 19 | python3 test_all.py 20 | ``` 21 | -------------------------------------------------------------------------------- /test/fns/echo-binary/fn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # read from stdin into a variable 4 | INPUT=$(cat) 5 | 6 | # echo the variable to stdout 7 | echo -n "$INPUT" 8 | -------------------------------------------------------------------------------- /test/fns/echo-js/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (req, res) => { 3 | const response = req.body; 4 | const headers = req.headers; // headers from the http request or GRPC metadata 5 | 6 | console.log(response); 7 | 8 | res.send(response); 9 | } 10 | -------------------------------------------------------------------------------- /test/fns/echo-js/package.json: -------------------------------------------------------------------------------- 1 | {"name": "fn", "version": "1.0.0", "main": "index.js"} -------------------------------------------------------------------------------- /test/fns/echo/fn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import typing 4 | 5 | def fn(input: typing.Optional[str], headers: typing.Optional[typing.Dict[str, str]]) -> typing.Optional[str]: 6 | """echo the input""" 7 | return input 8 | -------------------------------------------------------------------------------- /test/fns/echo/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenFogStack/tinyFaaS/eff00d060f6f6e96bf8f35475a0e9a8e6e42de32/test/fns/echo/requirements.txt -------------------------------------------------------------------------------- /test/fns/hello-go/fn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func fn(data string, headers map[string]string) (string, error) { 6 | return "Hello, " + data + " with headers: " + fmt.Sprint(headers), nil 7 | } 8 | -------------------------------------------------------------------------------- /test/fns/hello-go/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.22 -------------------------------------------------------------------------------- /test/fns/show-headers-js/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (req, res) => { 3 | const body = req.body; 4 | const headers = req.headers; // headers from the http request or GRPC metadata 5 | 6 | console.log("Headers:", headers); 7 | 8 | res.send(headers); 9 | } 10 | -------------------------------------------------------------------------------- /test/fns/show-headers-js/package.json: -------------------------------------------------------------------------------- 1 | {"name": "fn", "version": "1.0.0", "main": "index.js"} -------------------------------------------------------------------------------- /test/fns/show-headers/fn.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | 4 | def fn(input: typing.Optional[str], headers: typing.Optional[typing.Dict[str, str]]) -> typing.Optional[str]: 5 | """echo the input""" 6 | if headers is not None: 7 | return json.dumps(headers) 8 | else: 9 | return "{}" -------------------------------------------------------------------------------- /test/fns/show-headers/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenFogStack/tinyFaaS/eff00d060f6f6e96bf8f35475a0e9a8e6e42de32/test/fns/show-headers/requirements.txt -------------------------------------------------------------------------------- /test/fns/sieve-of-eratosthenes/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (req, res) => { 3 | const max = 10000; 4 | let sieve = [], i, j, primes = []; 5 | for (i = 2; i <= max; ++i) { 6 | 7 | if (!sieve[i]) { 8 | primes.push(i); 9 | for (j = i << 1; j <= max; j += i) { 10 | sieve[j] = true; 11 | } 12 | } 13 | } 14 | 15 | let response = ("Found " + primes.length + " primes under " + max); 16 | 17 | console.log(response); 18 | 19 | res.send(response + "\n"); 20 | } 21 | -------------------------------------------------------------------------------- /test/fns/sieve-of-eratosthenes/package.json: -------------------------------------------------------------------------------- 1 | {"name": "fn", "version": "1.0.0", "main": "index.js"} -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | aiocoap==0.4.7 2 | grpcio==1.64.1 3 | protobuf==5.27.1 4 | -------------------------------------------------------------------------------- /test/test_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | import json 6 | import os 7 | import os.path as path 8 | import signal 9 | import subprocess 10 | import sys 11 | import typing 12 | import urllib.error 13 | import urllib.request 14 | 15 | connection: typing.Dict[str, typing.Union[str, int]] = { 16 | "host": "localhost", 17 | "management_port": 8080, 18 | "http_port": 8000, 19 | "grpc_port": 9000, 20 | "coap_port": 5683, 21 | } 22 | 23 | tf_process: typing.Optional[subprocess.Popen] = None # type: ignore 24 | src_path = "." 25 | fn_path = path.join(src_path, "test", "fns") 26 | script_path = path.join(src_path, "scripts") 27 | grpc_api_path = path.join(src_path, "pkg", "grpc", "tinyfaas") 28 | sys.path.append(grpc_api_path) 29 | 30 | 31 | def setUpModule() -> None: 32 | """start tinyfaas instance""" 33 | # call make clean 34 | try: 35 | subprocess.run(["make", "clean"], cwd=src_path, check=True, capture_output=True) 36 | except subprocess.CalledProcessError as e: 37 | print(f"Failed to clean up:\n{e.stderr.decode('utf-8')}") 38 | 39 | # start tinyfaas 40 | try: 41 | env = os.environ.copy() 42 | env["HTTP_PORT"] = str(connection["http_port"]) 43 | env["GRPC_PORT"] = str(connection["grpc_port"]) 44 | env["COAP_PORT"] = str(connection["coap_port"]) 45 | 46 | global tf_process 47 | 48 | # find architecture and operating system 49 | uname = os.uname() 50 | if uname.machine == "x86_64": 51 | arch = "amd64" 52 | elif uname.machine == "arm64" or uname.machine == "aarch64": 53 | arch = "arm64" 54 | else: 55 | raise Exception(f"Unsupported architecture: {uname.machine}") 56 | 57 | if uname.sysname == "Linux": 58 | os_name = "linux" 59 | elif uname.sysname == "Darwin": 60 | os_name = "darwin" 61 | else: 62 | raise Exception(f"Unsupported operating system: {uname.sysname}") 63 | 64 | tf_binary = path.join(src_path, f"tinyfaas-{os_name}-{arch}") 65 | 66 | # os.makedirs(path.join(src_path, "tmp"), exist_ok=True) 67 | with open(path.join(".", "tf_test.out"), "w") as f: 68 | tf_process = subprocess.Popen( 69 | [tf_binary], 70 | cwd=src_path, 71 | env=env, 72 | stdout=f, 73 | stderr=f, 74 | ) 75 | 76 | except subprocess.CalledProcessError as e: 77 | print(f"Failed to start:\n{e.stderr.decode('utf-8')}") 78 | 79 | # wait for tinyfaas to start 80 | while True: 81 | try: 82 | urllib.request.urlopen( 83 | f"http://{connection['host']}:{connection['management_port']}/" 84 | ) 85 | break 86 | except urllib.error.HTTPError: 87 | break 88 | except Exception: 89 | continue 90 | # wait for tinyfaas to start 91 | while True: 92 | try: 93 | urllib.request.urlopen( 94 | f"http://{connection['host']}:{connection['http_port']}/" 95 | ) 96 | break 97 | except urllib.error.HTTPError: 98 | break 99 | except Exception: 100 | continue 101 | 102 | return 103 | 104 | 105 | def tearDownModule() -> None: 106 | """stop tinyfaas instance""" 107 | 108 | # call wipe-functions.sh 109 | try: 110 | subprocess.run( 111 | ["./wipe-functions.sh"], cwd=script_path, check=True, capture_output=True 112 | ) 113 | except subprocess.CalledProcessError as e: 114 | print(f"Failed to wipe functions:\n{e.stderr.decode('utf-8')}") 115 | 116 | # stop tinyfaas 117 | # with open(path.join(src_path, "tmp", "tf_test.out"), "w") as f: 118 | # f.write(tf_process.stdout.read()) 119 | # f.write(tf_process.stderr.read()) 120 | 121 | try: 122 | tf_process.send_signal(signal.SIGINT) # type: ignore 123 | tf_process.wait(timeout=1) # type: ignore 124 | tf_process.terminate() # type: ignore 125 | except subprocess.CalledProcessError as e: 126 | print(f"Failed to stop:\n{e.stderr.decode('utf-8')}") 127 | except subprocess.TimeoutExpired: 128 | print("Failed to stop: Timeout expired") 129 | 130 | # call make clean 131 | try: 132 | subprocess.run(["make", "clean"], cwd=src_path, check=True, capture_output=True) 133 | except subprocess.CalledProcessError as e: 134 | print(f"Failed to clean up:\n{e.stderr.decode('utf-8')}") 135 | 136 | return 137 | 138 | 139 | def startFunction(folder_name: str, fn_name: str, env: str, threads: int) -> str: 140 | """starts a function, returns name""" 141 | 142 | # get full path of folder 143 | folder_name = os.path.abspath(folder_name) 144 | 145 | # use the upload.sh script 146 | try: 147 | subprocess.run( 148 | ["./upload.sh", folder_name, fn_name, env, str(threads)], 149 | cwd=script_path, 150 | check=True, 151 | capture_output=True, 152 | ) 153 | except subprocess.CalledProcessError as e: 154 | print(f"Failed to upload function {fn_name}:\n{e.stderr.decode('utf-8')}") 155 | raise e 156 | 157 | return fn_name 158 | 159 | 160 | class TinyFaaSTest(unittest.TestCase): 161 | @classmethod 162 | def setUpClass(cls) -> None: 163 | super(TinyFaaSTest, cls).setUpClass() 164 | 165 | def setUp(self) -> None: 166 | global connection 167 | self.host = connection["host"] 168 | self.http_port = connection["http_port"] 169 | self.grpc_port = connection["grpc_port"] 170 | self.coap_port = connection["coap_port"] 171 | 172 | 173 | class TestSieve(TinyFaaSTest): 174 | fn = "" 175 | 176 | @classmethod 177 | def setUpClass(cls) -> None: 178 | cls.fn = startFunction( 179 | path.join(fn_path, "sieve-of-eratosthenes"), "sieve", "nodejs", 1 180 | ) 181 | 182 | def setUp(self) -> None: 183 | super(TestSieve, self).setUp() 184 | self.fn = TestSieve.fn 185 | 186 | def test_invoke_http(self) -> None: 187 | """invoke a function""" 188 | 189 | # make a request to the function 190 | res = urllib.request.urlopen( 191 | f"http://{self.host}:{self.http_port}/{self.fn}", timeout=10 192 | ) 193 | 194 | # check the response 195 | self.assertEqual(res.status, 200) 196 | 197 | return 198 | 199 | def test_invoke_http_async(self) -> None: 200 | """invoke a function async""" 201 | 202 | # make an async request to the function 203 | req = urllib.request.Request( 204 | f"http://{self.host}:{self.http_port}/{self.fn}", 205 | headers={"X-tinyFaaS-Async": "true"}, 206 | ) 207 | 208 | res = urllib.request.urlopen(req, timeout=10) 209 | 210 | # check the response 211 | self.assertEqual(res.status, 202) 212 | 213 | return 214 | 215 | def test_invoke_coap(self) -> None: 216 | """invoke a function with CoAP""" 217 | 218 | try: 219 | import asyncio 220 | import aiocoap # type: ignore 221 | except ImportError: 222 | self.skipTest( 223 | "aiocoap is not installed -- if you want to run CoAP tests, install the dependencies in requirements.txt" 224 | ) 225 | return 226 | 227 | msg = aiocoap.Message( 228 | code=aiocoap.GET, uri=f"coap://{self.host}:{self.coap_port}/{self.fn}" 229 | ) 230 | 231 | async def main() -> aiocoap.Message: 232 | protocol = await aiocoap.Context.create_client_context() 233 | response = await protocol.request(msg).response 234 | await protocol.shutdown() 235 | return response 236 | 237 | response = asyncio.run(main()) 238 | 239 | self.assertIsNotNone(response) 240 | self.assertEqual(response.code, aiocoap.CONTENT) 241 | 242 | return 243 | 244 | def test_invoke_grpc(self) -> None: 245 | """invoke a function""" 246 | try: 247 | import grpc # type: ignore 248 | except ImportError: 249 | self.skipTest( 250 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 251 | ) 252 | 253 | import tinyfaas_pb2 254 | import tinyfaas_pb2_grpc 255 | 256 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 257 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 258 | response = stub.Request(tinyfaas_pb2.Data(functionIdentifier=self.fn)) 259 | 260 | self.assertIsNotNone(response) 261 | self.assertIsNot(response.response, "") 262 | 263 | 264 | class TestEcho(TinyFaaSTest): 265 | fn = "" 266 | 267 | @classmethod 268 | def setUpClass(cls) -> None: 269 | super(TestEcho, cls).setUpClass() 270 | cls.fn = startFunction(path.join(fn_path, "echo"), "echo", "python3", 1) 271 | 272 | def setUp(self) -> None: 273 | super(TestEcho, self).setUp() 274 | self.fn = TestEcho.fn 275 | 276 | def test_invoke_http(self) -> None: 277 | """invoke a function""" 278 | 279 | # make a request to the function with a payload 280 | payload = "Hello World!" 281 | 282 | req = urllib.request.Request( 283 | f"http://{self.host}:{self.http_port}/{self.fn}", 284 | data=payload.encode("utf-8"), 285 | ) 286 | 287 | res = urllib.request.urlopen(req, timeout=10) 288 | 289 | # check the response 290 | self.assertEqual(res.status, 200) 291 | self.assertEqual(res.read().decode("utf-8"), payload) 292 | 293 | return 294 | 295 | def test_invoke_coap(self) -> None: 296 | """invoke a function with CoAP""" 297 | 298 | try: 299 | import asyncio 300 | import aiocoap 301 | except ImportError: 302 | self.skipTest( 303 | "aiocoap is not installed -- if you want to run CoAP tests, install the dependencies in requirements.txt" 304 | ) 305 | return 306 | 307 | # make a request to the function with a payload 308 | payload = "Hello World!" 309 | 310 | msg = aiocoap.Message( 311 | code=aiocoap.GET, 312 | uri=f"coap://{self.host}:{self.coap_port}/{self.fn}", 313 | payload=payload.encode("utf-8"), 314 | ) 315 | 316 | async def main() -> aiocoap.Message: 317 | protocol = await aiocoap.Context.create_client_context() 318 | response = await protocol.request(msg).response 319 | await protocol.shutdown() 320 | return response 321 | 322 | response = asyncio.run(main()) 323 | 324 | self.assertIsNotNone(response) 325 | self.assertEqual(response.code, aiocoap.CONTENT) 326 | self.assertEqual(response.payload.decode("utf-8"), payload) 327 | 328 | return 329 | 330 | def test_invoke_grpc(self) -> None: 331 | """invoke a function""" 332 | try: 333 | import grpc 334 | except ImportError: 335 | self.skipTest( 336 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 337 | ) 338 | 339 | import tinyfaas_pb2 340 | import tinyfaas_pb2_grpc 341 | 342 | # make a request to the function with a payload 343 | payload = "Hello World!" 344 | 345 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 346 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 347 | response = stub.Request( 348 | tinyfaas_pb2.Data(functionIdentifier=self.fn, data=payload) 349 | ) 350 | 351 | self.assertIsNotNone(response) 352 | self.assertEqual(response.response, payload) 353 | 354 | 355 | class TestEchoJS(TinyFaaSTest): 356 | fn = "" 357 | 358 | @classmethod 359 | def setUpClass(cls) -> None: 360 | super(TestEchoJS, cls).setUpClass() 361 | cls.fn = startFunction(path.join(fn_path, "echo-js"), "echojs", "nodejs", 1) 362 | 363 | def setUp(self) -> None: 364 | super(TestEchoJS, self).setUp() 365 | self.fn = TestEchoJS.fn 366 | 367 | def test_invoke_http(self) -> None: 368 | """invoke a function""" 369 | 370 | # make a request to the function with a payload 371 | payload = "Hello World!" 372 | 373 | req = urllib.request.Request( 374 | f"http://{self.host}:{self.http_port}/{self.fn}", 375 | data=payload.encode("utf-8"), 376 | ) 377 | 378 | res = urllib.request.urlopen(req, timeout=10) 379 | 380 | # check the response 381 | self.assertEqual(res.status, 200) 382 | self.assertEqual(res.read().decode("utf-8"), payload) 383 | 384 | return 385 | 386 | def test_invoke_coap(self) -> None: 387 | """invoke a function with CoAP""" 388 | 389 | try: 390 | import asyncio 391 | import aiocoap 392 | except ImportError: 393 | self.skipTest( 394 | "aiocoap is not installed -- if you want to run CoAP tests, install the dependencies in requirements.txt" 395 | ) 396 | return 397 | 398 | # make a request to the function with a payload 399 | payload = "Hello World!" 400 | 401 | msg = aiocoap.Message( 402 | code=aiocoap.GET, 403 | uri=f"coap://{self.host}:{self.coap_port}/{self.fn}", 404 | payload=payload.encode("utf-8"), 405 | ) 406 | 407 | async def main() -> aiocoap.Message: 408 | protocol = await aiocoap.Context.create_client_context() 409 | response = await protocol.request(msg).response 410 | await protocol.shutdown() 411 | return response 412 | 413 | response = asyncio.run(main()) 414 | 415 | self.assertIsNotNone(response) 416 | self.assertEqual(response.code, aiocoap.CONTENT) 417 | self.assertEqual(response.payload.decode("utf-8"), payload) 418 | 419 | return 420 | 421 | def test_invoke_grpc(self) -> None: 422 | """invoke a function""" 423 | try: 424 | import grpc 425 | except ImportError: 426 | self.skipTest( 427 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 428 | ) 429 | 430 | import tinyfaas_pb2 431 | import tinyfaas_pb2_grpc 432 | 433 | # make a request to the function with a payload 434 | payload = "Hello World!" 435 | 436 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 437 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 438 | response = stub.Request( 439 | tinyfaas_pb2.Data(functionIdentifier=self.fn, data=payload) 440 | ) 441 | 442 | self.assertIsNotNone(response) 443 | self.assertEqual(response.response, payload) 444 | 445 | 446 | class TestBinary(TinyFaaSTest): 447 | fn = "" 448 | 449 | @classmethod 450 | def setUpClass(cls) -> None: 451 | super(TestBinary, cls).setUpClass() 452 | cls.fn = startFunction( 453 | path.join(fn_path, "echo-binary"), "echobinary", "binary", 1 454 | ) 455 | 456 | def setUp(self) -> None: 457 | super(TestBinary, self).setUp() 458 | self.fn = TestBinary.fn 459 | 460 | def test_invoke_http(self) -> None: 461 | """invoke a function""" 462 | 463 | # make a request to the function with a payload 464 | payload = "Hello World!" 465 | 466 | req = urllib.request.Request( 467 | f"http://{self.host}:{self.http_port}/{self.fn}", 468 | data=payload.encode("utf-8"), 469 | ) 470 | 471 | res = urllib.request.urlopen(req, timeout=10) 472 | 473 | # check the response 474 | self.assertEqual(res.status, 200) 475 | self.assertEqual(res.read().decode("utf-8"), payload) 476 | 477 | return 478 | 479 | def test_invoke_coap(self) -> None: 480 | """invoke a function with CoAP""" 481 | 482 | try: 483 | import asyncio 484 | import aiocoap 485 | except ImportError: 486 | self.skipTest( 487 | "aiocoap is not installed -- if you want to run CoAP tests, install the dependencies in requirements.txt" 488 | ) 489 | return 490 | 491 | # make a request to the function with a payload 492 | payload = "Hello World!" 493 | 494 | msg = aiocoap.Message( 495 | code=aiocoap.GET, 496 | uri=f"coap://{self.host}:{self.coap_port}/{self.fn}", 497 | payload=payload.encode("utf-8"), 498 | ) 499 | 500 | async def main() -> aiocoap.Message: 501 | protocol = await aiocoap.Context.create_client_context() 502 | response = await protocol.request(msg).response 503 | await protocol.shutdown() 504 | return response 505 | 506 | response = asyncio.run(main()) 507 | 508 | self.assertIsNotNone(response) 509 | self.assertEqual(response.code, aiocoap.CONTENT) 510 | self.assertEqual(response.payload.decode("utf-8"), payload) 511 | 512 | return 513 | 514 | def test_invoke_grpc(self) -> None: 515 | """invoke a function""" 516 | try: 517 | import grpc 518 | except ImportError: 519 | self.skipTest( 520 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 521 | ) 522 | 523 | import tinyfaas_pb2 524 | import tinyfaas_pb2_grpc 525 | 526 | # make a request to the function with a payload 527 | payload = "Hello World!" 528 | 529 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 530 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 531 | response = stub.Request( 532 | tinyfaas_pb2.Data(functionIdentifier=self.fn, data=payload) 533 | ) 534 | 535 | self.assertIsNotNone(response) 536 | self.assertEqual(response.response, payload) 537 | 538 | 539 | class TestShowHeadersJS(TinyFaaSTest): 540 | fn = "" 541 | 542 | @classmethod 543 | def setUpClass(cls) -> None: 544 | super(TestShowHeadersJS, cls).setUpClass() 545 | cls.fn = startFunction( 546 | path.join(fn_path, "show-headers-js"), "headersjs", "nodejs", 1 547 | ) 548 | 549 | def setUp(self) -> None: 550 | super(TestShowHeadersJS, self).setUp() 551 | self.fn = TestShowHeadersJS.fn 552 | 553 | def test_invoke_http(self) -> None: 554 | """invoke a function""" 555 | 556 | # make a request to the function with a custom headers 557 | req = urllib.request.Request( 558 | f"http://{self.host}:{self.http_port}/{self.fn}", 559 | headers={"lab": "scalable_software_systems_group"}, 560 | ) 561 | 562 | res = urllib.request.urlopen(req, timeout=10) 563 | 564 | # check the response 565 | self.assertEqual(res.status, 200) 566 | response_body = res.read().decode("utf-8") 567 | response_json = json.loads(response_body) 568 | self.assertIn("lab", response_json) 569 | self.assertEqual( 570 | response_json["lab"], "scalable_software_systems_group" 571 | ) # custom header 572 | self.assertIn("user-agent", response_json) 573 | self.assertIn("Python-urllib", response_json["user-agent"]) # python client 574 | 575 | return 576 | 577 | # def test_invoke_coap(self) -> None: # CoAP does not support headers 578 | 579 | def test_invoke_grpc(self) -> None: 580 | """invoke a function""" 581 | try: 582 | import grpc 583 | except ImportError: 584 | self.skipTest( 585 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 586 | ) 587 | 588 | import tinyfaas_pb2 589 | import tinyfaas_pb2_grpc 590 | 591 | # make a request to the function with a payload 592 | payload = "" 593 | metadata = (("lab", "scalable_software_systems_group"),) 594 | 595 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 596 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 597 | response = stub.Request( 598 | tinyfaas_pb2.Data(functionIdentifier=self.fn, data=payload), 599 | metadata=metadata, 600 | ) 601 | 602 | response_json = json.loads(response.response) 603 | self.assertIn("lab", response_json) 604 | self.assertEqual( 605 | response_json["lab"], "scalable_software_systems_group" 606 | ) # custom header 607 | self.assertIn("user-agent", response_json) 608 | self.assertIn("grpc-python", response_json["user-agent"]) # client header 609 | 610 | 611 | class TestShowHeaders( 612 | TinyFaaSTest 613 | ): # Note: In Python, the http.server module (and many other HTTP libraries) automatically capitalizes the first character of each word in the header keys. 614 | fn = "" 615 | 616 | @classmethod 617 | def setUpClass(cls) -> None: 618 | super(TestShowHeaders, cls).setUpClass() 619 | cls.fn = startFunction( 620 | path.join(fn_path, "show-headers"), "headers", "python3", 1 621 | ) 622 | 623 | def setUp(self) -> None: 624 | super(TestShowHeaders, self).setUp() 625 | self.fn = TestShowHeaders.fn 626 | 627 | def test_invoke_http(self) -> None: 628 | """invoke a function""" 629 | 630 | # make a request to the function with a custom headers 631 | req = urllib.request.Request( 632 | f"http://{self.host}:{self.http_port}/{self.fn}", 633 | headers={"Lab": "scalable_software_systems_group"}, 634 | ) 635 | 636 | res = urllib.request.urlopen(req, timeout=10) 637 | 638 | # check the response 639 | self.assertEqual(res.status, 200) 640 | response_body = res.read().decode("utf-8") 641 | response_json = json.loads(response_body) 642 | self.assertIn("Lab", response_json) 643 | self.assertEqual( 644 | response_json["Lab"], "scalable_software_systems_group" 645 | ) # custom header 646 | self.assertIn("User-Agent", response_json) 647 | self.assertIn("Python-urllib", response_json["User-Agent"]) # python client 648 | 649 | return 650 | 651 | # def test_invoke_coap(self) -> None: # CoAP does not support headers, instead you have 652 | 653 | def test_invoke_grpc(self) -> None: 654 | """invoke a function""" 655 | try: 656 | import grpc 657 | except ImportError: 658 | self.skipTest( 659 | "grpc is not installed -- if you want to run gRPC tests, install the dependencies in requirements.txt" 660 | ) 661 | 662 | import tinyfaas_pb2 663 | import tinyfaas_pb2_grpc 664 | 665 | # make a request to the function with a payload 666 | payload = "" 667 | metadata = (("lab", "scalable_software_systems_group"),) 668 | 669 | with grpc.insecure_channel(f"{self.host}:{self.grpc_port}") as channel: 670 | stub = tinyfaas_pb2_grpc.TinyFaaSStub(channel) 671 | response = stub.Request( 672 | tinyfaas_pb2.Data(functionIdentifier=self.fn, data=payload), 673 | metadata=metadata, 674 | ) 675 | 676 | response_json = json.loads(response.response) 677 | 678 | self.assertIn("Lab", response_json) 679 | self.assertEqual( 680 | response_json["Lab"], "scalable_software_systems_group" 681 | ) # custom header 682 | self.assertIn("User-Agent", response_json) 683 | self.assertIn("grpc-python", response_json["User-Agent"]) # client header 684 | 685 | 686 | if __name__ == "__main__": 687 | # check that make is installed 688 | try: 689 | subprocess.run(["make", "--version"], check=True, capture_output=True) 690 | except subprocess.CalledProcessError as e: 691 | print(f"Make is not installed:\n{e.stderr.decode('utf-8')}") 692 | sys.exit(1) 693 | 694 | # check that Docker is working 695 | try: 696 | subprocess.run(["docker", "ps"], check=True, capture_output=True) 697 | except subprocess.CalledProcessError as e: 698 | print(f"Docker is not installed or not working:\n{e.stderr.decode('utf-8')}") 699 | sys.exit(1) 700 | 701 | unittest.main() # run all tests 702 | --------------------------------------------------------------------------------