From 367b79eaf06a17347fcbaab8230473d6cfbbcfca Mon Sep 17 00:00:00 2001 From: Rohan Singh Date: Mon, 31 Jan 2022 17:40:05 -0500 Subject: [PATCH] Add documentation for secrets (#138) Also some reformatting/editing of the authoring guide. --- docs/authoring_apps.md | 74 ++++++++++++++++++++++++++++++++++++------ docs/modules.md | 20 +++++++++++- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/docs/authoring_apps.md b/docs/authoring_apps.md index 9f8a14a0c1..3228f96235 100644 --- a/docs/authoring_apps.md +++ b/docs/authoring_apps.md @@ -2,25 +2,38 @@ If you haven't already, check out the [tutorial](tutorial.md) for how to get started writing apps. This guide picks up where the tutorial leaves off and provides practices and philosophy on how to build apps using pixlet. ## Architecture -The way pixlet works is heavily influenced by the way Tidbyt works. The Tidbyt displays really tiny 64x32 images and keeps a local cache of images for the installations on the device. On a regular interval, the Tidbyt sends heartbeats to the Tidbyt backend letting the backend know what installations it has inside its cache. The backend then determines what apps need rendered, executes the appropriate Starlark script, and encodes the result as a [WebP](https://developers.google.com/speed/webp) image to send back to the device. +Pixlet is heavily influenced by the way Tidbyt devices work. The Tidbyt displays tiny 64x32 animations and images. It keeps a local cache of the apps that are installed on it. + +Each Tidbyt regularly sends heartbeats to the Tidbyt cloud, announcing what it has cached. The Tidbyt cloud decides which app needs to be rendered next, and executes the appropriate Starlark script. It encodes the result as a [WebP](https://developers.google.com/speed/webp) image, and sends it back to the device. To mimic how we host apps internally, `pixlet render` executes the Starlark script and `pixlet push` pushes the resulting WebP to your Tidbyt. -## Cache -As part of pixlet, we offer the `cache` module to cache results from API requests or other data that's needed between renders. Inside of `pixlet`, it's not super useful given the `pixlet` binary has a short lived execution. When publishing apps using the Tidbyt [Community](https://github.com/tidbyt/community) repo, it can cut down on API requests and add some reliability to your app. +## Config +When running an app, Pixlet passes a `config` object to the app's `main()`: + +```starlark +def main(config): + who = config.get("who") + print("Hello, %s" % who) +``` -The main thing to keep in mind when working with `cache` is that it's scoped per app. This means that two installations by two different users will have the same cache. Anything that's cached should not be specific to an installation and instead specific to the parameters being requested. Make sure you create a cache key that is unique for the type of information you are caching. +The `config` object contains values that are useful for your app. You can set the actual values by: -## Config -Config is used to configure an app. It's a key/value pair of config values and is passed into `main`. It's exposed through query parameters in the url of `pixlet serve` or through command line args through `pixlet render`. When publishing apps to the Tidbyt [Community](https://github.com/tidbyt/community) repo, the Tidbyt backend will populate the config values from values provided in the mobile app. +1. Passing URL query parameters when using `pixlet serve`. +2. Setting command-line arguments via `pixlet render`. -The important thing to remember here is that your app should always be able to render even if a config value isn't provided. Providing default values for every config value or checking it for `None` will ensure the app behaves as expected even if config was not provided. For example, the following ensures there will always be a value for `foo`: +When apps that are published to the [Tidbyt Community repo][3], users can install and configure them with the Tidbyt smartphone app. [Define a schema for your app][4] to enable this. + +Your app should always be able to render, even if a config value isn't provided. Provide defaults for every config value, or check if the value is `None`. This will ensure the app behaves as expected even if config was not provided. + +For example, the following ensures there will always be a value for `who`: ```starlark -DEFAULT_FOO = "bar" +DEFAULT_WHO = "world" def main(config): - foo = config.get("foo", DEFAULT_FOO) + who = config.get("who") or DEFAULT_WHO + print("Hello, %s" % who) ``` The `config` object also has helpers to convert config values into specific types: @@ -30,6 +43,47 @@ config.str("foo") # returns a string, or None if not found config.bool("foo") # returns a boolean (True or False), or None if not found ``` +## Cache +Use the `cache` module to cache results from API requests or other data that's needed between renders. We require sensible caching for apps in the [Tidbyt Community repo](https://github.com/tidbyt/community). Caching cuts down on API requests, and can make your app more reliable. + +**Make sure to create cache keys that are unique for the type of information you are caching.** Each app has its own cache, but that cache is shared among every user. Two installations of the same app by two different users will share the same cache. + +A good strategy is to create cache keys based on the config parameters or information being requested. + +## Secrets + +Many apps need secret values like API keys. When publishing your app to the [Tidbyt community repo][3], encrypt sensitive values so that only the Tidbyt cloud servers can decrypt them. + +To encrypt values, use the `pixlet encrypt` command. For exmaple: + +``` +$ pixlet encrypt my-app-name top_secret_api_key_123456 +"AV6+...." # encrypted value +``` + +Use the `secret.decrypt()` function in your app to decrypt this value: + +```starlark +load("secret.star", "secret") + +def main(config): + api_key = secret.decrypt("AV6+...") or config.get("dev_api_key") +``` + +When you run `pixlet` locally, `secret.decrypt` will always return `None`. When your app runs in the Tidbyt cloud, `secret.decrypt` will return the string that you passed to `pixlet encrypt`. + ## Fail -Using a `fail()` inside of your app should be used incredibly sparingly. It kills the entire execution in an unrecoverable fashion. If your app depends on an external API, it cannot cache the response, and has no means of recovering with a failed response - then a `fail()` is appropriate. Otherwise, using a `print()` and handling the error appropriately is a better approach. \ No newline at end of file +The [`fail()`][1] function will immediately end the execution of your app and return an error. It should be used incredibly sparingly, and only in cases that are _permanent_ failures. + +For example, if your app receives an error from an external API, try these options before `fail()`: + +1. Return a cached response. +2. Display a useful message or fallback data. +3. [`print()`][2] an error message. +3. Handle the error in a way that makes sense for your app. + +[1]: https://github.com/bazelbuild/starlark/blob/master/spec.md#fail +[2]: https://github.com/bazelbuild/starlark/blob/master/spec.md#print +[3]: https://github.com/tidbyt/community +[4]: schema.md \ No newline at end of file diff --git a/docs/modules.md b/docs/modules.md index 370cfe2d96..012f869f94 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -17,7 +17,7 @@ this example, the `render` module is made available to the script as the symbol `r` instead of `render`: ```starlark -load("render.star", r="render") +load("render.star", r = "render") ``` ## Starlib modules @@ -117,6 +117,24 @@ Example: See [examples/schema_hello_world.star](../examples/schema_hello_world.star) for an example. +## Pixlet module: Secret + +The secret module can decrypt values that were encrypted with `pixlet encrypt`. + +| Function | Description | +| --- | --- | +| `decrypt(value)` | Decrypts and returns the value when running in Tidbyt cloud. Returns `None` when running locally. Decryption will fail if the name of the app doesn't match the name that was passed to `pixlet encrypt`. | + +Example: +```starlark +load("secret.star", "secret") + +ENCRYPTED_API_KEY = "AV6+..." . # from `pixlet encyrpt` + +def main(config): + api_key = secret.decrypt(ENCRYPTED_API_KEY) or config.get("dev_api_key") +``` + ## Pixlet module: Sunrise The `sunrise` module calculates sunrise and sunset times for a given set of GPS coordinates and timestamp.