diff --git a/content/spin/v1/ai-sentiment-analysis-api-tutorial.md b/content/spin/v1/ai-sentiment-analysis-api-tutorial.md index 2c311126e..948048b6e 100644 --- a/content/spin/v1/ai-sentiment-analysis-api-tutorial.md +++ b/content/spin/v1/ai-sentiment-analysis-api-tutorial.md @@ -32,6 +32,8 @@ url = "https://github.com/fermyon/developer/blob/main/content/spin/v1/ai-sentime - [Conclusion](#conclusion) - [Next Steps](#next-steps) +> This tutorial does not work with Spin `v3.0` or above as the on disk representation of the models have changed. Refer to the [V3 tutorial](/spin/v3/ai-sentiment-analysis-api-tutorial) depending on your Spin version. + Artificial Intelligence (AI) Inferencing performs well on GPUs. However, GPU infrastructure is both scarce and expensive. This tutorial will show you how to use Fermyon Serverless AI to quickly build advanced AI-enabled serverless applications that can run on Fermyon Cloud. Your applications will benefit from 50 millisecond cold start times and operate 100x faster than other on-demand AI infrastructure services. Take a quick look at the video below to learn about executing inferencing on LLMs with no extra setup. diff --git a/content/spin/v2/ai-sentiment-analysis-api-tutorial.md b/content/spin/v2/ai-sentiment-analysis-api-tutorial.md index e9ea52d71..a06a7f0a0 100644 --- a/content/spin/v2/ai-sentiment-analysis-api-tutorial.md +++ b/content/spin/v2/ai-sentiment-analysis-api-tutorial.md @@ -31,6 +31,8 @@ url = "https://github.com/fermyon/developer/blob/main/content/spin/v2/ai-sentime - [Conclusion](#conclusion) - [Next Steps](#next-steps) +> This tutorial does not work with Spin `v3.0` or above as the on disk representation of the models have changed. Refer to the [V3 tutorial](/spin/v3/ai-sentiment-analysis-api-tutorial) depending on your Spin version. + Artificial Intelligence (AI) Inferencing performs well on GPUs. However, GPU infrastructure is both scarce and expensive. This tutorial will show you how to use Fermyon Serverless AI to quickly build advanced AI-enabled serverless applications that can run on Fermyon Cloud. Your applications will benefit from 50 millisecond cold start times and operate 100x faster than other on-demand AI infrastructure services. Take a quick look at the video below to learn about executing inferencing on LLMs with no extra setup. diff --git a/content/spin/v2/deploying-to-fermyon.md b/content/spin/v2/deploying-to-fermyon.md index 5e359022f..e379fe2ef 100644 --- a/content/spin/v2/deploying-to-fermyon.md +++ b/content/spin/v2/deploying-to-fermyon.md @@ -18,7 +18,7 @@ url = "https://github.com/fermyon/developer/blob/main/content/spin/v2/deploying- [Fermyon Platform](https://www.fermyon.dev/) is a self-host platform for Spin applications. With Fermyon, you can deploy your spin applications onto a server in moments. -> Fermyon Platform does not currently support Spin 2. +> Fermyon Platform does not currently support Spin 2 or above. ### Running on Your Workstation diff --git a/content/spin/v2/variables.md b/content/spin/v2/variables.md index 8f21d9348..2dbe54270 100644 --- a/content/spin/v2/variables.md +++ b/content/spin/v2/variables.md @@ -86,7 +86,7 @@ api_version = "v1" ## Using Variables From Applications -The Spin SDK surfaces the Spin configuration interface to your language. The [interface](https://github.com/fermyon/spin/blob/main/wit/variables.wit) consists of one operation: +The Spin SDK surfaces the Spin configuration interface to your language. The [interface](https://github.com/fermyon/spin/blob/main/wit/deps/spin@2.0.0/variables.wit) consists of one operation: | Operation | Parameters | Returns | Behavior | |------------|--------------------|---------------------|----------| diff --git a/content/spin/v3/ai-sentiment-analysis-api-tutorial.md b/content/spin/v3/ai-sentiment-analysis-api-tutorial.md new file mode 100644 index 000000000..d51b31b31 --- /dev/null +++ b/content/spin/v3/ai-sentiment-analysis-api-tutorial.md @@ -0,0 +1,1101 @@ +title = "Sentiment Analysis With Serverless AI" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/ai-sentiment-analysis-api-tutorial.md" + +--- +- [Tutorial Prerequisites](#tutorial-prerequisites) + - [Spin](#spin) + - [Dependencies](#dependencies) +- [Licenses](#licenses) +- [Serverless AI Inferencing With Spin Applications](#serverless-ai-inferencing-with-spin-applications) + - [Creating a New Spin Application](#creating-a-new-spin-application) + - [Supported AI Models](#supported-ai-models) + - [Application Structure](#application-structure) + - [Application Configuration](#application-configuration) + - [Source Code](#source-code) + - [Additional Functionality](#additional-functionality) + - [Static Fileserver Component For The UI](#static-fileserver-component-for-the-ui) + - [Add the Front-End](#add-the-front-end) + - [Key Value Explorer](#key-value-explorer) + - [Application Manifest](#application-manifest) + - [Building and Deploying Your Spin Application](#building-and-deploying-your-spin-application) + - [Test Locally](#test-locally) + - [Deploy to Fermyon Cloud](#deploy-to-fermyon-cloud) + - [Testing in Fermyon Cloud](#testing-in-fermyon-cloud) + - [Visit Fermyon Cloud UI](#visit-fermyon-cloud-ui) +- [Integrating Custom Domain and Storage](#integrating-custom-domain-and-storage) +- [Conclusion](#conclusion) +- [Next Steps](#next-steps) + +Artificial Intelligence (AI) Inferencing performs well on GPUs. However, GPU infrastructure is both scarce and expensive. This tutorial will show you how to use Fermyon Serverless AI to quickly build advanced AI-enabled serverless applications that can run on Fermyon Cloud. Your applications will benefit from 50 millisecond cold start times and operate 100x faster than other on-demand AI infrastructure services. Take a quick look at the video below to learn about executing inferencing on LLMs with no extra setup. + + + +In this tutorial we will: + +* Update Spin (and dependencies) on your local machine +* Create a Serverless AI application +* Learn about the Serverless AI SDK + +## Tutorial Prerequisites + +### Spin + +You will need to [install the latest version of Spin](install#installing-spin). This tutorial requires Spin 3.0 or greater. + +If you already have Spin installed, [check what version you are on and upgrade](upgrade#are-you-on-the-latest-version) if required. + +### Dependencies + +The above installation script automatically installs the latest SDKs for Rust (which will enable us to write Serverless AI applications in Rust). However, some of the Serverless AI examples are written using TypeScript/Javascript, Python and TinyGo. To enable Serverless AI functionality via TypeScript/Javascript, Python and TinyGo, please ensure you have the latest TypeScript/JavaScript, Python or TinyGo template installed: + +**TypeScript/Javascript** + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-js-sdk --upgrade +``` + +**Python** + +Ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + +Some of the Serverless AI examples are written using Python. To enable Serverless AI functionality via Python, please ensure you have the latest Python template installed: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --upgrade +``` + +**TinyGo** + +Some of the Serverless AI examples are written using TinyGo. To enable Serverless AI functionality via TinyGo, please ensure you have the latest Spin template installed (the following command will make the `http-go` template available): + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --upgrade +``` + +## Licenses + +> This tutorial uses [Meta AI](https://ai.meta.com/)'s Llama 2, Llama Chat and Code Llama models you will need to visit [Meta's Llama webpage](https://ai.meta.com/resources/models-and-libraries/llama-downloads/) and agree to Meta's License, Acceptable Use Policy, and to Meta’s privacy policy before fetching and using Llama models. + +## Serverless AI Inferencing With Spin Applications + +Now, let's dive deep into a comprehensive tutorial and unlock your potential to use Fermyon Serverless AI. +**Note:** The full source code with other examples can be found in our [Github repo](https://github.com/fermyon/ai-examples/tree/main) + +### Creating a New Spin Application + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +The Rust code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/tree/main/sentiment-analysis-rs) + +> Note: please add `/api/...` when prompted for the path; this provides us with an API endpoint to query the sentiment analysis component. + + + +```bash +$ spin new -t http-rust +Enter a name for your new application: sentiment-analysis +Description: A sentiment analysis API that demonstrates using LLM inferencing and KV stores together +HTTP path: /api/... +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + +The TypeScript code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/tree/main/sentiment-analysis-ts) + +> Note: please add `/api/...` when prompted for the path; this provides us with an API endpoint to query the sentiment analysis component. + + +```bash +$ spin new -t http-ts +Enter a name for your new application: sentiment-analysis +Description: A sentiment analysis API that demonstrates using LLM inferencing and KV stores together +HTTP path: /api/... +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + +The Python code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/tree/main/sentiment-analysis-py) + +> Note: please add `/api/...` when prompted for the path; this provides us with an API endpoint to query the sentiment analysis component. + + +```bash +$ spin new -t http-py +Enter a name for your new application: sentiment-analysis +Description: A sentiment analysis API that demonstrates using LLM inferencing and KV stores together +HTTP path: /api/... +``` + +{{ blockEnd }} + +{{ startTab "TinyGo" }} + +> Note: please add `/api/...` when prompted for the path; this provides us with an API endpoint to query the sentiment analysis component. + + +```bash +$ spin new -t http-go +Enter a name for your new application: sentiment-analysis +Description: A sentiment analysis API that demonstrates using LLM inferencing and KV stores together +HTTP path: /api/... +``` + +{{ blockEnd }} + +{{ blockEnd }} + +### Supported AI Models + +Fermyon's Spin and Serverless AI currently support: +- Meta's open source Large Language Models (LLMs) [Llama](https://ai.meta.com/llama/), specifically the `llama2-chat` and `codellama-instruct` models (see Meta [Licenses](#licenses) section above). +- SentenceTransformers' [embeddings](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) models, specifically the `all-minilm-l6-v2` model. + +### Application Structure + +Next, we need to create the appropriate folder structure from within the application directory (alongside our `spin.toml` file). The code below demonstrates the variations in folder structure depending on which model is being used. Once the folder structure is in place, we then fetch the pre-trained AI model for our application: + +> Note: Optional, but highly recommended, is to use the [Spin Cloud GPU component](https://github.com/fermyon/spin-cloud-gpu). This offloads inferencing to Fermyon Cloud GPUs, and thus requires a free account to [Fermyon Cloud Serverless AI](https://www.fermyon.com/serverless-ai). This would replace the following steps of having to download the three models below. + +**llama2-chat example download** + +> Ensure you have read the Meta [Licenses](#licenses) section before continuing to use Llama models. + +Download the the `*.safetensors`, `config.json` and `tokenizer.json` from [huggingface](https://huggingface.co/meta-llama/Llama-2-7b-hf) and place it in the following structure below. The `.spin` directory needs to be placed in the root of the Spin project. + + + +```bash +tree .spin +.spin +└── ai-models + └── llama + └── llama2-chat + └── <*.safetensors files> + └── config.json + └── tokenizor.json +``` + +**codellama-instruct example download** + +> Ensure you have read the Meta [Licenses](#licenses) section before continuing to use Llama models. + +Download the `*.safetensors`, `config.json` and `tokenizer.json` from [huggingface](https://huggingface.co/meta-llama/CodeLlama-7b-hf/tree/main) and place it in the following structure below. + + + +```bash +tree .spin +.spin +└── ai-models + └── llama + └── codellama-instruct + └── <*.safetensors files> + └── config.json + └── tokenizor.json +``` + +**all-minilm-l6-v2 example download** + +The following section fetches a specific version of the [sentence-transformers](https://www.sbert.net/index.html#) model: + + + +```bash +$ mkdir -p .spin/ai-models/all-minilm-l6-v2 +$ cd .spin/ai-models/all-minilm-l6-v2 +$ wget https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/7dbbc90392e2f80f3d3c277d6e90027e55de9125/tokenizer.json +$ wget https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/0b6dc4ef7c29dba0d2e99a5db0c855c3102310d8/model.safetensors +``` + + + +```bash +tree .spin +.spin +└── ai-models + └── all-minilm-l6-v2 + ├── model.safetensors + └── tokenizer.json +``` + +> Note: Rather than be limited to a 1:1 relationship between a Spin applications and a downloaded model, if you would like more than just one Spin application to access a specific model (that you have already downloaded) you can create an arbitrary directory (i.e. `~/my-ai-models/`) to house your models, and then create a symbolic link to a specific Spin application (i.e. `~/application-one/.spin/ai-models`): + + + +```bash +ln -s ~/my-ai-models/ ~/application-one/.spin/ai-models +``` + +### Application Configuration + +Then, we configure the `[component.sentiment-analysis]` section of our application's manifest (the `spin.toml` file); explicitly naming our model of choice. For example, in the case of the sentiment analysis application, we specify the `llama2-chat` value for our `ai_models` configuration, and add a `default` key-value store: + +> Note: `[component.sentiment-analysis]` contains the name of the component. If you used a different name, when creating the application, this sections name would be different. + +```toml +[component.sentiment-analysis] +... +ai_models = ["llama2-chat"] +key_value_stores = ["default"] +... +``` + +### Source Code + +Now let's use the Spin SDK to access the model from our app: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +The Rust source code for this sentiment analysis example uses serde. There are a couple of ways to add the required serde dependencies: +- Run `cargo add serde -F derive` and `cargo add serde_json` from your Rust application's home directory (which will automatically update your application's `Cargo.toml` file), or +- Manually, edit your Rust application's `Cargo.toml` file by adding the following lines beneath the `Cargo.toml` file's `[dependencies]` section: + +```toml +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +``` + +Once you have added serde, as explained above, modify your `src/lib.rs` file to match the following content: + +```rust +use std::str::FromStr; + +use anyhow::Result; +use spin_sdk::{ + http::{IntoResponse, Json, Params, Request, Response, Router}, + http_component, + key_value::Store, + llm::{infer_with_options, InferencingModel::Llama2Chat}, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct SentimentAnalysisRequest { + pub sentence: String, +} + +#[derive(Serialize)] +pub struct SentimentAnalysisResponse { + pub sentiment: String, +} + +const PROMPT: &str = r#"\ +<> +You are a bot that generates sentiment analysis responses. Respond with a single positive, negative, or neutral. +<> + +Follow the pattern of the following examples: + +User: Hi, my name is Bob +Bot: neutral + +User: I am so happy today +Bot: positive + +User: I am so sad today +Bot: negative + + +User: {SENTENCE} +"#; + +/// A Spin HTTP component that internally routes requests. +#[http_component] +fn handle_route(req: Request) -> Response { + let mut router = Router::new(); + router.any("/api/*", not_found); + router.post("/api/sentiment-analysis", perform_sentiment_analysis); + router.handle(req) +} + +fn not_found(_: Request, _: Params) -> Result { + Ok(Response::new(404, "Not found")) +} + +fn perform_sentiment_analysis( + req: http::Request>, + _params: Params, +) -> Result { + // Do some basic clean up on the input + let sentence = req.body().sentence.trim(); + println!("Performing sentiment analysis on: {}", sentence); + + // Prepare the KV store + let kv = Store::open_default()?; + + // If the sentiment of the sentence is already in the KV store, return it + if kv.exists(sentence).unwrap_or(false) { + println!("Found sentence in KV store returning cached sentiment"); + let sentiment = kv.get(sentence)?; + let resp = SentimentAnalysisResponse { + sentiment: String::from_utf8(sentiment.unwrap())?, + }; + let resp_str = serde_json::to_string(&resp)?; + + return Ok(Response::new(200, resp_str)); + } + println!("Sentence not found in KV store"); + + // Otherwise, perform sentiment analysis + println!("Running inference"); + let inferencing_result = infer_with_options( + Llama2Chat, + &PROMPT.replace("{SENTENCE}", sentence), + spin_sdk::llm::InferencingParams { + max_tokens: 8, + ..Default::default() + }, + )?; + + println!("Inference result {:?}", inferencing_result); + + let sentiment = inferencing_result + .text + .lines() + .next() + .unwrap_or_default() + .strip_prefix("Bot:") + .unwrap_or_default() + .parse::(); + println!("Got sentiment: {sentiment:?}"); + + if let Ok(sentiment) = sentiment { + println!("Caching sentiment in KV store"); + let _ = kv.set(sentence, sentiment.as_str().as_bytes()); + } + + // Cache the result in the KV store + let resp = SentimentAnalysisResponse { + sentiment: sentiment + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), + }; + + let resp_str = serde_json::to_string(&resp)?; + + Ok(Response::new(200, resp_str)) +} + +#[derive(Copy, Clone, Debug)] +enum Sentiment { + Positive, + Negative, + Neutral, +} + +impl Sentiment { + fn as_str(&self) -> &str { + match self { + Self::Positive => "positive", + Self::Negative => "negative", + Self::Neutral => "neutral", + } + } +} + +impl std::fmt::Display for Sentiment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Sentiment { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + let sentiment = match s.trim() { + "positive" => Self::Positive, + "negative" => Self::Negative, + "neutral" => Self::Neutral, + _ => return Err(s.into()), + }; + Ok(sentiment) + } +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +```typescript +import { + HandleRequest, + HttpRequest, + HttpResponse, + Llm, + InferencingModels, + InferencingOptions, + Router, + Kv, +} from "@fermyon/spin-sdk"; + +interface SentimentAnalysisRequest { + sentence: string; +} + +interface SentimentAnalysisResponse { + sentiment: "negative" | "neutral" | "positive"; +} + +const decoder = new TextDecoder(); + +const PROMPT = `\ +<> +You are a bot that generates sentiment analysis responses. Respond with a single positive, negative, or neutral. +<> + +Follow the pattern of the following examples: + +Hi, my name is Bob +neutral + +I am so happy today +positive + +I am so sad today +negative + + + +`; + +async function performSentimentAnalysis(request: HttpRequest) { + // Parse sentence out of request + let data = request.json() as SentimentAnalysisRequest; + let sentence = data.sentence; + console.log("Performing sentiment analysis on: " + sentence); + + // Prepare the KV store + let kv = Kv.openDefault(); + + // If the sentiment of the sentence is already in the KV store, return it + if (kv.exists(sentence)) { + console.log("Found sentence in KV store returning cached sentiment"); + return { + status: 200, + body: JSON.stringify({ + sentiment: decoder.decode(kv.get(sentence)), + } as SentimentAnalysisResponse), + }; + } + console.log("Sentence not found in KV store"); + + // Otherwise, perform sentiment analysis + console.log("Running inference"); + let options: InferencingOptions = { maxTokens: 6 }; + let inferenceResult = Llm.infer( + InferencingModels.Llama2Chat, + PROMPT.replace("", sentence), + options + ); + console.log( + `Inference result (${inferenceResult.usage.generatedTokenCount} tokens): ${inferenceResult.text}` + ); + let sentiment = inferenceResult.text.split(/\s+/)[0]?.trim(); + + // Clean up result from inference + if ( + sentiment === undefined || + !["negative", "neutral", "positive"].includes(sentiment) + ) { + sentiment = "neutral"; + console.log("Invalid sentiment, marking it as neutral"); + } + + // Cache the result in the KV store + console.log("Caching sentiment in KV store"); + kv.set(sentence, sentiment); + + return { + status: 200, + body: JSON.stringify({ + sentiment, + } as SentimentAnalysisResponse), + }; +} + +let router = Router(); + +// Map the route to the handler +router.post("/api/sentiment-analysis", async (_, req) => { + console.log(`${new Date().toISOString()} POST /sentiment-analysis`); + return await performSentimentAnalysis(req); +}); + +// Catch all 404 handler +router.all("/api/*", async (_, req) => { + return { + status: 404, + body: "Not found", + }; +}); + +// Entry point to the Spin handler +export const handleRequest: HandleRequest = async function ( + request: HttpRequest +): Promise { + return await router.handleRequest(request, request); +}; +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +```python +from spin_http import Response +from spin_llm import llm_infer +from spin_key_value import kv_open_default +import json +import re + +PROMPT="""<> +You are a bot that generates sentiment analysis responses. Respond with a single positive, negative, or neutral. +<> +[INST] +Follow the pattern of the following examples: + +User: Hi, my name is Bob +Bot: neutral + +User: I am so happy today +Bot: positive + +User: I am so sad today +Bot: negative + +[/INST] +User: """ + +def handle_request(request): + # Extracting the sentence from the request + request_body=json.loads(request.body) + sentence=request_body["sentence"].strip() + print("Performing sentiment analysis on: " + sentence) + + # Open the default KV store + store = kv_open_default() + + # Check if the sentence is already in the KV store + if store.exists(sentence) is False: + result=llm_infer("llama2-chat", PROMPT+sentence).text + print("Raw result: " + result) + sentiment = get_sentiment_from_sentence(result) + print("Storing result in the KV store") + store.set(sentence, str.encode(sentiment)) + else: + sentiment = store.get(sentence).decode() + print("Found a cached result") + + response_body=json.dumps({"sentence": sentiment}) + + return Response(200, + {"content-type": "application/json"}, + bytes(response_body, "utf-8")) + +def get_sentiment_from_sentence(sentence) -> str: + words = sentence.lower().split() + sentiments = ["positive", "negative", "neutral"] + result = next((word for word in sentiments if word in words), None) + + if result is not None: + return result + else: + print("Inconclusive, returning 'neutral'") + return "neutral" +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + + + +```go +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/kv" + "github.com/fermyon/spin/sdk/go/v2/llm" +) + +type sentimentAnalysisRequest struct { + Sentence string +} + +type sentimentAnalysisResponse struct { + Sentiment string +} + +const prompt = `\ +You are a bot that generates sentiment analysis responses. Respond with a single positive, negative, or neutral. +Hi, my name is Bob +neutral +I am so happy today +positive +I am so sad today +negative + +` + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + router := spinhttp.NewRouter() + router.POST("/api/sentiment-analysis", performSentimentAnalysis) + router.ServeHTTP(w, r) + }) +} + +func performSentimentAnalysis(w http.ResponseWriter, r *http.Request, ps spinhttp.Params) { + var req sentimentAnalysisRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Printf("Performing sentiment analysis on: %q\n", req.Sentence) + + // Open the KV store + store, err := kv.OpenStore("default") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer store.Close() + + // If the sentiment of the sentence is already in the KV store, return it + exists, err := store.Exists(req.Sentence) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if exists { + fmt.Println("Found sentence in KV store returning cached sentiment") + value, err := store.Get(req.Sentence) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + res := sentimentAnalysisResponse{ + Sentiment: string(value), + } + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + fmt.Println("Sentence not found in KV store") + + // Otherwise, perform sentiment analysis + fmt.Println("Running inference") + params := &llm.InferencingParams{ + MaxTokens: 10, + Temperature: 0.5, + } + + result, err := llm.Infer("llama2-chat", strings.Replace(prompt, "", req.Sentence, 1), params) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Printf("Inference result (%d tokens): %s\n", result.Usage.GeneratedTokenCount, result.Text) + + var sentiment string + if fields := strings.Fields(result.Text); len(fields) > 0 { + sentiment = fields[0] + } + + // Cache the result in the KV store + fmt.Println("Caching sentiment in KV store") + store.Set(req.Sentence, []byte(sentiment)) + + res := &sentimentAnalysisResponse{ + Sentiment: sentiment, + } + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func main() {} + +``` + +{{ blockEnd }} + +{{ blockEnd }} + +### Additional Functionality + +This application also includes two more components, a key/value explorer and static-fileserver component. Let's quickly go ahead and create those (letting Spin do all of the scaffolding for us). + +### Static Fileserver Component For The UI + +We use the `spin add` command to add the new `static-fileserver` that we will name `ui`: + + + +```bash +$ spin add -t static-fileserver +Enter a name for your new component: ui +HTTP path [/static/...]: /... +Directory containing the files to serve [assets]: assets + +``` + +We create an `assets` directory where we can store files to serve statically (see the `spin.toml` file for more configuration information): + + + +```bash +$ mkdir assets +``` + +### Add the Front-End + +We can add a webpage that asks the user for some text and does the sentiment analysis on it. In your assets folder, create two files `dynamic.js` and `index.html`. + +Here's the code snippet for `index.html` + +```html + + + + Sentiment Analyzer + + + + + + + + + + + +
+
+
+ +
+
+

+ This Sentiment Analyzer is a demonstration of how you can use Fermyon + Serverless AI to easily make an AI-powered API. When you type in a + sentence it is sent to a Spin app running in the Fermyon Cloud, + inferencing is performed using the Fermyon serverless AI feature, and + the response is cached in a Fermyon key/value store. +

+

+ Note that LLM's are not perfect and the sentiment analysis performed + by this application is not guaranteed to be perfect. +

+

+ To get started type a sentence below and press + enter. +

+ +
+ +
+ +
+
+
+
+ + +``` + +Here's the code snippet for `dynamic.js` + +```javascript +// Listen for the Enter key being pressed +document.addEventListener("keydown", function (event) { + if (event.keyCode === 13) { + newCard(); + } +}); + +var globalCardCount = 0; +var runningInference = false; + +function newCard() { + if (runningInference) { + console.log("Already running inference, please wait..."); + setAlert("Already running inference, please wait..."); + return; + } + var inputElement = document.getElementById("sentence-input"); + var sentence = inputElement.value; + if (sentence === "") { + console.log("Please enter a sentence to analyze"); + setAlert("Please enter a sentence to analyze"); + return; + } + inputElement.value = ""; + + var cardIndex = globalCardCount; + globalCardCount++; + var newCard = document.createElement("div"); + newCard.id = "card-" + cardIndex; + newCard.innerHTML = ` +
+
+
${sentence}
+
+ +
+
+
+ `; + document.getElementById("sentence-input").before(newCard); + + console.log("Running inference on sentence: " + sentence); + runningInference = true; + fetch("/api/sentiment-analysis", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ sentence: sentence }), + }) + .then((response) => response.json()) + .then((data) => { + console.log(data); + updateCard(cardIndex, sentence, data.sentiment); + }) + .catch((error) => { + console.log(error); + }); +} + +function updateCard(cardIndex, sentence, sentiment) { + badge = ""; + if (sentiment === "positive") { + badge = `Positive`; + } else if (sentiment === "negative") { + badge = `Negative`; + } else if (sentiment === "neutral") { + badge = `Neutral`; + } else { + badge = `Unsure`; + } + var cardElement = document.getElementById("card-" + cardIndex); + cardElement.innerHTML = ` +
+
+
${sentence}
+
+ ${badge} +
+
+
+ `; + runningInference = false; +} + +function setAlert(msg) { + var alertElement = document.getElementById("alert"); + alertElement.innerHTML = ` +
+ + ${msg} +
+ `; + setTimeout(function () { + alertElement.innerHTML = ""; + }, 3000); +} +``` + +### Key Value Explorer + +For this, we install use a pre-made template by pointing to the templates GitHub repository: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-kv-explorer +``` + +Then, we again use `spin add` to add the new component. We will name the component `kv-explorer`): + + + +```bash +$ spin add kv-explorer -t kv-explorer +``` + +### Application Manifest + +As shown below, the Spin framework has done all of the scaffolding for us: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "sentiment-analysis-rust" +version = "0.1.0" +authors = ["Your Name "] +description = "A sentiment analysis API that demonstrates using LLM inferencing and KV stores together" + +[[trigger.http]] +route = "/api/..." +component = "sentiment-analysis-rust" + +[component.sentiment-analysis-rust] +source = "target/wasm32-wasi/release/sentiment_analysis_rust.wasm" +allow_outbound_hosts = [] +ai_models = ["llama2-chat"] +key_value_stores = ["default"] +[component.sentiment-analysis-rust.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] + +[[trigger.http]] +route = "/..." +component = "ui" + +[component.ui] +source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.1.0/spin_static_fs.wasm", digest = "sha256:96c76d9af86420b39eb6cd7be5550e3cb5d4cc4de572ce0fd1f6a29471536cb4" } +files = [{ source = "assets", destination = "/" }] + +[[trigger.http]] +route = "/internal/kv-explorer/..." +component = "kvv2" + +[component.kv-explorer] +source = { url = "https://github.com/fermyon/spin-kv-explorer/releases/download/v0.6.0/spin-kv-explorer.wasm", digest = "sha256:38110bc277a393cdfb1a885a0fd56923d47314b2086399d1e3bbcb6daa1f04ad" } +# add or remove stores you want to explore here +key_value_stores = ["default"] +``` + +### Building and Deploying Your Spin Application + +**Note:** Running inferencing on localhost (your CPU) is not as optimal as deploying to Fermyon's Serverless AI (where inferencing is performed by high-powered GPUs). You can skip this `spin build --up` step and move straight to `spin cloud deploy` if you: + +- a) are using one of the 3 supported models above, +- b) have configured your `spin.toml` file to explicitly configure the model (as shown above) + +Now, let's build and run our Spin Application locally. (**Note:** If you are following along with the TypeScript/JavaScript example, you will first need to run `npm install`. Otherwise, please continue to the following `spin` command.) + + + +```bash +$ spin build --up +``` + +### Test Locally + + + +```bash +# Create a new POST request to localhost +$ curl -vXPOST 'localhost:3000/api/sentiment-analysis' -H'Content-Type: application/json' -d "{\"sentence\": \"Well this is very nice indeed\" }" + +{"sentiment":"positive"} +``` + +### Deploy to Fermyon Cloud + +Deploying to the Fermyon Cloud is one simple command. If you have not logged into your Fermyon Cloud account already, the CLI will prompt you to login. Follow the instructions to complete the authorization process. + + + +```bash +$ spin cloud deploy +``` + +### Testing in Fermyon Cloud + + + +```bash +# Create a new POST request to your apps URL in Fermyon Cloud +$ curl -vXPOST 'https://abcxyz.fermyon.app/api/sentiment-analysis' -H'Content-Type: application/json' -d "{\"sentence\": \"Well this is very nice indeed\" }" + +{"sentiment":"positive"} +``` + +### Visit Fermyon Cloud UI + +Visiting your apps URL will produce a User Interface (UI) similar to the following. + +![sentiment analysis ui blank](/static/image/docs/sentiment-analysis-ui-blank.png) + +You can type in a sentence, and the UI will respond with the sentiment analysis. + +![sentiment analysis ui positive](/static/image/docs/sentiment-analysis-ui-positive.png) + +## Integrating Custom Domain and Storage + +The groundbreaking Fermyon Serverless AI introduces a revolutionary addition to the full-stack developer's arsenal. You can now seamlessly integrate the Fermyon [SQLite Database](https://www.fermyon.com/blog/announcing-noops-sql-db), [Key-Value Storage](https://www.fermyon.com/blog/introducing-fermyon-cloud-key-value-store), and even your [Fermyon Cloud Custom Domains](https://www.fermyon.com/blog/announcing-custom-domains) with the launch of your very own advanced AI-enabled serverless applications. + +## Conclusion + +We want to get feedback on the Serverless AI API. We are curious about what models you would like to use and what applications you are building using Serverless AI. Let us know what you need, and how Fermyon's Serverless AI could potentially help solve a problem for you. We would love to help you write your new Serverless AI application. + +## Next Steps + +- Try the numerous Serverless AI examples in our GitHub repository called [ai-examples](https://github.com/fermyon/ai-examples). +- [Contribute](/hub/contributing) your Serverless AI app to our [Spin Hub](/hub). +- Ask questions and share your thoughts in [our Discord community](https://discord.gg/AAFNfS7NGf). diff --git a/content/spin/v3/api-guides-overview.md b/content/spin/v3/api-guides-overview.md new file mode 100644 index 000000000..0ac7d02a3 --- /dev/null +++ b/content/spin/v3/api-guides-overview.md @@ -0,0 +1,26 @@ +title = "API Support Overview" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/api-guides-overview.md" + +--- + +The following table shows the status of the interfaces Spin provides to applications. + +| Host Capabilities/Interfaces | Stability | Cloud | +|----------------------------------------------|--------------|---------| +| [HTTP Trigger](./http-trigger) | Stable | Yes | +| [Redis Trigger](./redis-trigger) | Stable | No | +| [Cron Trigger](./triggers) | Experimental | No | +| [Outbound HTTP](./http-outbound) | Stable | Yes | +| [Outbound Redis](./redis-outbound) | Stable | Yes | +| [Configuration Variables](./variables) | Stable | Yes | +| [PostgreSQL](./rdbms-storage) | Experimental | Yes | +| [MySQL](./rdbms-storage) | Experimental | Yes | +| [Key-value Storage](./kv-store-api-guide) | Stabilizing | Yes | +| [Serverless AI](./serverless-ai-api-guide) | Experimental | Yes | +| [SQLite Storage](./sqlite-api-guide) | Experimental | Yes | +| [MQTT Messaging](./mqtt-outbound) | Experimental | No | + +For more information about what is possible in the programming language of your choice, please see our [Language Support Overview](./language-support-overview). diff --git a/content/spin/v3/build.md b/content/spin/v3/build.md new file mode 100644 index 000000000..021077eaa --- /dev/null +++ b/content/spin/v3/build.md @@ -0,0 +1,207 @@ +title = "Building Spin Application Code" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/build.md" + +--- + +- [Setting Up for `spin build`](#setting-up-for-spin-build) +- [Running `spin build`](#running-spin-build) +- [Running the Application After Build](#running-the-application-after-build) +- [Overriding the Working Directory](#overriding-the-working-directory) +- [Next Steps](#next-steps) + +A Spin application is made up of one or more components. Components are binary Wasm modules; _building_ refers to the process of converting your source code into those modules. + +> Even languages that don't require a compile step when used 'natively' may still require a build step to adapt them to work as Wasm modules. + +Because most compilers don't target Wasm by default, building Wasm modules often requires special command options, which you may not have at your fingertips. +What's more, when developing a multi-component application, you may need to issue such commands for several components on each iteration. +Doing this manually can be tedious and error-prone. + +To make the build process easier, the `spin build` command allows you to build all the components in one command. + +> You don't have to use `spin build` to manage your builds. If you prefer to use a Makefile or other build system, you can! `spin build` is just there to provide an 'out of the box' solution. + + +## Setting Up for `spin build` + +To use `spin build`, each component that you want to build must specify the command used to build it in `spin.toml`, as part of its `component.(name).build` table: + +```toml +[component.hello] +# This is the section you need for `spin build` +[component.hello.build] +command = "npm run build" +``` + +If you generated the component from a Fermyon-supplied template, the `build` section should be set up correctly for you. You don't need to change or add anything. + +> Different components may be built from different languages, and so each component can have its own build command. In addition, some components may be precompiled into Wasm modules, and don't need a build command at all. If a component doesn't have a build command, `spin build` just skips it. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +For Rust applications, you must have the `wasm32-wasi` target installed: + + + +```bash +$ rustup target add wasm32-wasi +``` + +The build command typically runs `cargo build` with the `wasm32-wasi` target and the `--release` option: + + + +```toml +[component.hello.build] +command = "cargo build --target wasm32-wasi --release" +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + +For JavaScript and TypeScript applications, you must have [Node.js](https://nodejs.org). + + +It's normally convenient to put the detailed build instructions in `package.json`. The build script looks like: + + + +```json +{ + "scripts": { + "build": "npx webpack --mode=production && npx mkdirp target && npx j2w -i dist.js -d combined-wit -n combined -o target/spin-http-js.wasm" + } +} +``` + +{{ details "Parts of the build script" "The build script calls out to [`webpack`](https://webpack.js.org/) and `j2w` which is a script provided by the `@fermyon/spin-sdk` package that utilizes [`ComponentizeJS`](https://github.com/bytecodealliance/ComponentizeJS)."}} + +The build command can then call the NPM script: + + + +```toml +[component.hello.build] +command = "npm run build" +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + +Ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + +For Python applications, you must have [`componentize-py`](https://pypi.org/project/componentize-py/) installed: + + + +```bash +$ pip3 install componentize-py +``` + +The build command then calls `componentize-py` on your application file: + + + +```toml +[component.hello.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +``` + +{{ blockEnd }} + +{{ startTab "TinyGo" }} + +For Go applications, you must use the TinyGo compiler, as the standard Go compiler does not yet support the WASI standard. See the [TinyGo installation guide](https://tinygo.org/getting-started/install/). + +The build command calls TinyGo with the WASI backend and appropriate options: + + + +```toml +[component.hello.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +``` + +{{ blockEnd }} + +{{ blockEnd }} + +> The output of the build command _must_ match the component's `source` path. If you change the `build` or `source` attributes, make sure to keep them in sync. + + +## Running `spin build` + +Once the build commands are set up, running `spin build` will execute, sequentially, each build command: + + + +```bash +$ spin build +Building component hello with `cargo build --target wasm32-wasi --release` + Updating crates.io index + Updating git repository `https://github.com/fermyon/spin` + + //--snip-- + + Compiling hello v0.1.0 (hello) + Finished release [optimized] target(s) in 39.05s +Finished building all Spin components +``` + +> If your build doesn't work, and your source code looks okay, you can [run `spin doctor`](./troubleshooting-application-dev.md) to check for problems with your Spin configuration and tools. + +## Running the Application After Build + +You can pass the `--up` option to `spin build` to start the application as soon as the build process completes successfully. + +This is equivalent to running `spin up` immediately after `spin build`. It accepts all the same flags and options that `up` does. See [Running Applications](running-apps) for details. + +## Overriding the Working Directory + +By default, the `command` to build a component is executed in the directory containing the `spin.toml` file. If a component's entire build source is under a subdirectory, it is often more convenient to build in that subdirectory rather than try to pass the path to the build command. You can do this by setting the `workdir` option in the `component.(id).build` table. + +For example, consider this Rust component located in subdirectory `deep`: + + + +```bash +. +├── deep +│   ├── Cargo.toml +│   └── src +│   └── lib.rs +└── spin.toml +``` + +To have the Rust build `command` run in directory `deep`, we can set the component's `workdir`: + + + +```toml +[component.deep.build] +# `command` is the normal build command for this language +command = "cargo build --target wasm32-wasi --release" +# This tells Spin to run it in the directory of the build file (in this case Cargo.toml) +workdir = "deep" +``` + +> `workdir` must be a relative path, and it is relative to the directory containing `spin.toml`. Specifying an absolute path leads to an error. + +## Next Steps + +- Try [running your application locally](running-apps) +- Try deploying a Spin application to the [Fermyon Cloud](/cloud/quickstart) diff --git a/content/spin/v3/cache.md b/content/spin/v3/cache.md new file mode 100644 index 000000000..ddee0d24f --- /dev/null +++ b/content/spin/v3/cache.md @@ -0,0 +1,95 @@ +title = "Spin Internal Data Layout" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/cache.md" + +--- + +- [Base Directories](#base-directories) +- [Plugins](#plugins) +- [Templates](#templates) +- [Application Cache](#application-cache) + - [Inside the Application Cache](#inside-the-application-cache) + +This page describes how Spin lays out its internal data on disk. + +> This document is provided as a reference for users wanting to diagnose problems or to reset Spin state. Don't modify the contents of these directories. Spin updates these directories as you issue the relevant commands on the command line. + +> No stability guarantees apply to the internal layout. It may change between Spin versions. + +## Base Directories + +Spin uses similar layouts across Linux, MacOS and Windows platforms, but the paths to various areas of the user's home directory differ across the platforms. On this page, the following terms have the following meanings: + +| Name | Linux | MacOS | Windows | +|---------------------|------------------------------------------|--------------------------------------|-------------------| +| `DATA_DIR` | `$XDG_DATA_HOME` or `$HOME/.local/share` | `$HOME/Library/Application Support`, or `$HOMEBREW_PREFIX/etc/fermyon-spin` if installed using Homebrew | `%LOCALAPPDATA%` or `%USERPROFILE%\AppData\Local` | +| `CACHE_DIR` | `$XDG_CACHE_HOME` or `$HOME/.cache` | `$HOME/Library/Caches` | `%LOCALAPPDATA%` or `%USERPROFILE%\AppData\Local` | + +These directories are based on the [XDG specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), and specifically on the cross-platform implementation in the [Rust `dirs` crate](https://docs.rs/dirs/latest/dirs/). + +> If Spin cannot resolve a base directory as listed above, it falls back to `$HOME/.spin` (`%USERPROFILE%\.spin` on Windows). + +> Spin's data directory (`DATA_DIR`) can be overridden via the `SPIN_DATA_DIR` environment variable. + +## Plugins + +Installed plugins are stored in `(DATA_DIR)/spin/plugins`. A snapshot of the plugins registry is also stored under that directory at `(DATA_DIR)/spin/plugins/.spin-plugins`; this is structured as a Git repository. + +> Note: If you [install Spin](./install) using Homebrew, the plugins are stored at `$HOMEBREW_PREFIX/fermyon-spin/plugins`. + +If you delete the plugins directory, you will no longer be able to run your plugins (until you reinstall them), but other Spin operations will be unaffected. + +## Templates + +Installed templates are stored in `(DATA_DIR)/spin/templates`. + +> Note: If you [install Spin](install) using Homebrew, the templates are stored at `$HOMEBREW_PREFIX/fermyon-spin/templates`. + +If you delete the templates directory, you will lose access to your installed templates (until you reinstall them), but other Spin operations will be unaffected. + +## Application Cache + +Downloaded application data, such as applications downloaded from registries or Wasm modules downloaded from URLs, are stored in `(CACHE_DIR)/spin/registry`. + +If you delete the application cache directory, Spin will automatically re-download the files as needed. Spin operations will be otherwise unaffected. + +### Inside the Application Cache + +> **Reminder:** This information is provided for diagnostic and entertainment purposes only, and may change across Spin versions. The only operation a user can safely undertake is to delete the entire `registry` directory. + +The application cache is divided into three subdirectories, `data`, `manifests`, and `wasm`. + +The `data` directory contains all static assets referenced from applications distributed with remote registries. The `wasm` directory contains all component sources referenced either in applications distributed with remote registries, or component sources from HTTP endpoints, directly referenced in `spin.toml`. + +> The `data` and `wasm` directories are content addressable. This means that if multiple applications reference the same static file or component source, Spin will be able to determine if it has already been pulled (on that users operating system), based on its digest. This also means that if an application has an update, Spin will only pull the changes in the component sources and static assets. + +The `manifests` directory contains the registry manifests for entire apps distributed with remote registries. They are placed in subdirectories that identify the application based on the registry, repository, and digest (or tag). + +The following `tree` command shows a typical (abbreviated) cache directory: + + + +```console +$ tree ~/Library/Caches/spin/registry/ + +├── data +│   ├── sha256:41a4649a8a8c176133792119cb45a7686767d3fa376ffd656e2ff76a6071fb07 +│   └── sha256:da3fda2db338a73483068072e22f7e7eef27afdbae3db824e130932adce703ba +├── manifests +│   └── ghcr.io +│   └── radu-matei +│   ├── hello-registries +│   │   └── latest +│   │   ├── config.json +│   │   └── manifest.json +│   └── spin-openai-demo +│   └── v1 +│   ├── config.json +│   └── manifest.json +└── wasm + ├── sha256:0b985e7d43e719f34cbb54849759a2f8e7913c0f9b17bf7cb2b3d2458d33859e + └── sha256:d5f9e1f6b61b90f7404e3800285f7860fe2cfc7d0116023efc370adbb403fe87 +``` diff --git a/content/spin/v3/contributing-docs.md b/content/spin/v3/contributing-docs.md new file mode 100644 index 000000000..7882527fd --- /dev/null +++ b/content/spin/v3/contributing-docs.md @@ -0,0 +1,583 @@ +title = "Contributing to Docs" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/contributing-docs.md" +keywords = "contribute contributing" + +--- + +- [Technical Documentation Types](#technical-documentation-types) + - [1. Tutorials](#1-tutorials) + - [2. How-To Guides](#2-how-to-guides) + - [3. Reference](#3-reference) + - [4. Explanation](#4-explanation) +- [Documents Relevant to Two or More Projects](#documents-relevant-to-two-or-more-projects) +- [Technical Documentation Procedure (Video)](#technical-documentation-procedure-video) +- [Technical Documentation Procedure (Text)](#technical-documentation-procedure-text) + - [1. Fork the Repository](#1-fork-the-repository) + - [2. Clone the Fork](#2-clone-the-fork) + - [3. Create New Branch](#3-create-new-branch) + - [4. Add Upstream](#4-add-upstream) + - [5. Code Blocks, Annotations and Table of Contents (ToC)](#5-code-blocks-annotations-and-table-of-contents-toc) + - [6.1 Checking Your Content - Using NPM](#61-checking-your-content---using-npm) + - [6.2 Indexing Your Content](#62-indexing-your-content) + - [6.3 Increasing Search Visibility For Your Content](#63-increasing-search-visibility-for-your-content) + - [6.4 The Edit On GitHub Button](#64-the-edit-on-github-button) + - [6.5 How To Properly Edit CSS Styles](#65-how-to-properly-edit-css-styles) + - [6.6 Checking Your Content - Using Bartholomew's CLI](#66-checking-your-content---using-bartholomews-cli) + - [6.7 Checking Your Content - Preview a Documentation Page on Localhost](#67-checking-your-content---preview-a-documentation-page-on-localhost) + - [6.8 Scheduling Menu Items for Timed Release](#68-scheduling-menu-items-for-timed-release) + - [7. Checking Web Pages](#7-checking-web-pages) + - [8. Add Changes](#8-add-changes) + - [9. Commit Changes](#9-commit-changes) + - [10. Push Changes](#10-push-changes) + - [11. Create a Pull Request](#11-create-a-pull-request) + +We are delighted that you are interested in making our developer documentation better. Thank you! We welcome and appreciate contributions of all types — opening issues, fixing typos, adding examples, one-liner code fixes, tests, or complete features. + +Any contribution and interaction on any Fermyon project MUST follow our [code of conduct](https://www.fermyon.com/code-of-conduct). Thank you for being part of an inclusive and open community! + +Below are a few pointers designed to help you contribute. + +## Technical Documentation Types + +The following points will help guide your contribution from a resource-type perspective; essentially we would really appreciate you creating and contributing any of the following 4 resource types. + +### 1. Tutorials + +Tutorials are oriented toward learning. Tutorials are designed to get a user started on something new (that they have not tried before). You can think of a tutorial as a lesson i.e. teaching a Spin user [how to use Redis to persist data](/cloud/data-redis). The tutorial may contain many logically ordered steps i.e. installing Spin, installing Redis, using Spin templates, configuring a Spin application and so forth. The desired outcome for a tutorial is for the user to have a working deployment or application. Think of it as a lesson in how to bake a cake. + +### 2. How-To Guides + +How-to guides are oriented towards showing a user how to solve a problem, which leads them to be able to achieve their own goal. The how-to guide will follow a series of logical steps. Think of it as providing a recipe for the user's creativity. For example, you can show a user how to [develop a Spin application](/cloud/develop) without telling them what the application must do; that is up to the user's imagination. + +### 3. Reference + +Reference resources are merely a dry description; describing the feature in its simplest form. An example of a reference resource is the [Spin application manifest reference](./manifest-reference). You will notice that the Manifest Reference page simply lists all of the manifest entries and available options. + +### 4. Explanation + +An explanation resource is written using a deep-dive approach i.e. providing a deep explanation with the view to impart a deep understanding of a particular concept, feature or product. You may find your contribution is so in-depth that it becomes a long form article like a blog post. If that is the case, please get in touch and we can discuss options around getting your content published on another platform; like the [Fermyon Blog](https://www.fermyon.com/blog/index). + +**Tying It All Together** + +You will notice that the menu system is organized in terms of "Tutorial", "How-To", "Reference" and so forth. When you write your contribution please decide which product (Cloud, Spin, Bartholomew) category it falls into and also which resource type it aligns with. Armed with that information you can go ahead and create your new file. For example, your "how-to" resource on "developing a Spin application" in Fermyon cloud would be saved to the `content/cloud/` folder; specifically, `content/cloud/develop.md` and the menu item (for the left-hand-side menu) would be added to the `templates/cloud_sidebar.hbs` file, as shown below. + +![cloud develop example](/static/image/docs/cloud-develop-example.png) + +The resulting output would be as follows. + +![cloud develop example](/static/image/docs/cloud-develop-example-2.png) + +## Documents Relevant to Two or More Projects + +If a document is relevant to two or more projects, the dynamic body feature of bartholomew is to be used. Create the document in one of the projects with the content. In the other project(s) create a file with only the frontmatter. Then add the following field to the frontmatter: + + + +```toml +. +. +body_source = "" + +[extra] + +``` + +The value for the `body_source` key, should be the path from which the content is being shared (relative to the repository's `content` folder). For example if the Spin project's `developer/content/spin/v3/contributing-docs.md` holds the sharable content (as the single source of truth), then the Cloud project can display that same content by using the following frontmatter: + + + +```toml +title = "Contributing to Docs" +template = "cloud_main" +date = "2023-11-04T00:00:01Z" +body_source = "/spin/v3/contributing-docs" + +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/cloud/contributing-docs.md" +keywords = "contribute contributing" + +--- +``` + +> Note: the `body_source = "/spin/contributing-docs"` part of the frontmatter does not need to include the `.md` file extension (as is also the case when hyperlinking via markdown anywhere in a developer documentation file's body). + +## Technical Documentation Procedure (Video) + + + +## Technical Documentation Procedure (Text) + +### 1. Fork the Repository + +The first step is to fork the [developer repository](https://github.com/fermyon/developer), from Fermyon's GitHub, to your own GitHub account. + +![Fork the repository](/static/image/fork_developer_repo.png) + +Ensure that you are forking the developer repository **to your own** GitHub account; where you have full editing privileges. + +### 2. Clone the Fork + +Copy the URL from the UI in readiness for running the `git clone` command. + +![Fork the repository](/static/image/clone_developer_repo.png) + +Go ahead and clone the new fork that you just created (the one which resides in your own GitHub account): + + + +```bash +$ cd ~ +$ git clone git@github.com:yourusername/developer.git +$ cd developer +``` + +### 3. Create New Branch + +Create a new branch that will house all of your changes for this specific contribution: + + + +```bash +$ git checkout -b my_new_branch +``` + +### 4. Add Upstream + +Create a new remote for the upstream (a pointer to the original repository to which you are contributing): + + + +```bash +$ git remote add upstream https://github.com/fermyon/developer +``` + +### 5. Code Blocks, Annotations and Table of Contents (ToC) + +It is highly recommended that you use either the `` or the `` annotation before each of your code blocks, and that each code block defines the appropriate [syntax highlighting](https://rdmd.readme.io/docs/code-blocks#language-support). The annotation can be skipped for code blocks with example code snippets i.e. non-terminal or generic output examples. + +**Selective copy** + +The selective copy annotation (``) is intended for use when communicating code and/or CLI commands for the reader to copy and paste. The selective copy annotation allows the reader to see the entire code block (both commands and results) but only copies the lines that start with `$` into the reader's clipboard (minus the `$`) when the user clicks the copy button. For example, copying the following code block will only copy `echo "hello"` into your clipboard, for pasting. + + + +```bash +$ echo "hello" +hello +``` + +> Note: If the command, that starts with `$`, is deliberately spread over two lines (by escaping the newline character), then the copy mechanism will still copy the second line which is technically still part of that single command. + +**No copy** + +The no copy annotation (``) precedes a code block where no copy and pasting of code is intended. If using the no copy attribute please still be sure to add the appropriate syntax highlighting to your code block (for display purposes). For example: + +![No Copy Source Code Example](/static/image/no-copy-source-code-example.png) + +Please find copyable snippet below, for your convenience: + +```` + + +```text +Some generic code not intended for copying/pasting +``` +```` + +The above markdown will render the following code block on the web page: + + + +```text +Some generic code not intended for copying/pasting +``` + +**Non-selective copy** - just a straight copy without any additional features. + +If you want the code in a code block to be copyable with no "smarts" to remove the `$` then you can just simply leave out the annotation altogether. A code block in markdown will be copyable without smarts just as is. + +**Multi-tab code blocks** + +Multi-tab code blocks [have recently been implemented](https://github.com/fermyon/developer/pull/239). Examples can be seen in the [Spin installer documentation](./install#installing-spin) and [Spin Key/Value documentation](./key-value-store-tutorial#the-spin-toml-file). The above examples demonstrate how tabs can either represent platforms i.e. `Windows`, `Linux` and `macOS` or represent specific programming languages i.e. `Rust`, `JavaScript` and `Golang` etc. Here is a brief example of how to implement multi-tab code blocks when writing technical documentation for this site, using markdown. + +The first step to implementing multi-tab code blocks is placing the `enable_shortcodes = true` configuration at the start of the `.md` file. Specifically, in the `.md` file's frontmatter. + +The markup to create tabs in markdown is as follows + +``` +{{ tabs "os" }} + +{{ startTab "Windows"}} + +To list files on windows use `dir` + + + +\`\`\`bash +$ dir hello_fermyon +\`\`\` +and script in windows have the extension `.bat` + + + +\`\`\`bash +hello.bat +test.bat +\`\`\` + +{{ blockEnd }} + +{{ startTab "Linux"}} + +To list files on linux use `ls` + + + +\`\`\`bash +$ ls +\`\`\` + +and script in linux have the extension `.sh` + + + +\`\`\`bash +hello.sh +test.sh +\`\`\` + +{{ blockEnd }} +{{ blockEnd }} +``` + +**Note**: Existing documentation will already be using class names for code block `tabs` and `startTab` i.e. `{{ tabs "os" }}` and `{{ startTab "Windows"}}` respectively. Please consult the following `tabs` and `startTab` class names that are already in use (before creating your own). If you need to create a new class name (because one does not already exist) please add it to the list below as part of the pull request that contains your code block contribution. + +**tabs**: +- `gh-interact` +- `os` +- `platforms` +- `sdk-type` +- `spin-version` +- `cloud-plugin-version` + +**startTab** +- `Azure AKS` +- `C#` +- `Docker Desktop` +- `Generic Kubernetes` +- `GitHub CLI` +- `GitHub UI` +- `K3d` +- `Linux` +- `macOS` +- `Python` +- `Rust` +- `TinyGo` +- `TypeScript` +- `v0.9.0` +- `v0.10.0` +- `v1.0.0` +- `v1.1.0` +- `v1.2.0` +- `v0.1.0` +- `v0.1.1` +- `Windows` + +The next section covers the highly recommended use of ToCs. + +**Implementing a Table of Contents (ToC)** + +If you create content with many headings it is highly recommended to place a ToC in your markdown file. There are excellent extensions (such as this Visual Studio Code Extension called [markdown-all-in-one](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) which will automatically generate your ToC). + +### 6.1 Checking Your Content - Using NPM + +Once you are satisfied with your contribution, you can programmatically check your content. + +If you have not done so already, please go ahead and perform the `npm ci` (npm clean install) command; to enable Node dependencies such as `markdownlint-cli2`. Simply run the following command, from the root of the developer repository: + + + +```bash +$ npm ci +``` + +On top of the Node dependencies the `timeout` executable must be installed on your system and added to the `PATH` environment variable. The `timeout` executable is included in the [gnu coreutils package](https://www.gnu.org/software/coreutils/) which should be present in most Linux distributions. + +On macOS you can install the `timeout` binary using the Homebrew package manager as shown below: + + + +```bash +$ brew install coreutils +``` + +Having all dependencies installed, you can now check for broken links (which takes several minutes) and also lint your markdown files. Simply run the following command, from the root of the developer repository: + + + +```bash +$ npm run test +``` + +**Hint:** Optionally you can run only the linter with the following command: + + + +```bash +# Example of how to lint all Markdown files in a local folder (in this case the spin folder) +npx markdownlint-cli2 content/spin/*.md \"#node_modules\" +# Example of how to lint a local Markdown file +npx markdownlint-cli2 content/spin/install.md \"#node_modules\" +``` + +**Note:** Whilst the `npm run test` command (which lints and also programmatically checks all URLs) does take extra time to complete it **must** be utilized before you [push changes](#10-push-changes); preventing the potential pushing of broken URLs to the developer documentation site. + +### 6.2 Indexing Your Content + +The developer documentation site implements in-house search. A new index is automatically generated for you when your contribution is merged into the developer documentation repository. This is done via a GitHub action. The following section explains how to alter content to increase search visibility for your content. + +### 6.3 Increasing Search Visibility For Your Content + +The built-in search functionality is based on the indexing of individual words in each markdown file, which works well most of the time. However, there are a couple of scenarios where you _may_ want to deliberately increase search visibility for your content. + +**Word Mismatch** + +Words in a documentation markdown file may not be words that are searched for by a user. For example, you may write about "different HTTP listening options" whereas a user may only ever try to find that specific content using a search phrase like "alternate port". If you are aware of typical user search phrases it is always recommended to craft your content to include any predictable user search phrases. However, in the rare case of a mismatch between words in your content and the words a user searches for, you can utilize use the `keywords` string in the `[extra]` section of your document's frontmatter to increase visibility. For example, the following code block shows frontmatter that helps a user find your documentation page (when the user searches for `port`): + +```markdown +[extra] +keywords = "port ports" +``` + +Adding a word to the `keywords` string of a page overrides the built-in search functionality by at least one order of magnitude. Adding a word to the `keywords` string may displace other content, so please use it only if necessary. + +**Homing in on specific content** + +The `keywords` string takes users to the start of a page. In some cases, this is not ideal. You may want to home in on specific content to resolve a search query. + +If a search term relates to a specific part of a page, you may use the following syntax anywhere in the body of your markdown file, and the user's search action will direct them straight to the previous heading (nearest heading above the `@searchTerm` syntax). + +```markdown + +``` + + + +When using the above `@searchTerm` feature, please note the following: +- the words must be separated by a space i.e. +- these keywords will be boosted in the search results by at least one order of magnitude; so please use them with caution, so as not to displace other valid pages containing similar content. + +Example: If you search for the word "homing", the results will point you to the previous heading in this specific section of the developer documentation. + +![homing example](/static/image/homing.png) + +### 6.4 The Edit On GitHub Button + +Each markdown file in the developer documentation requires a link to its GitHub page for the site to correctly render the "Edit on GitHub" button for that page. + +![edit on github](/static/image/edit-on-github.png) + +If you create a new markdown file and/or you notice a file without the explicit GitHub URL, please add a URL entry to the [extra] section. For example, the following `url` is required for this page that you are reading (you can check the raw markdown contents of this page to see this example): + +``` +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/contributing-docs.md" +``` + +### 6.5 How To Properly Edit CSS Styles + +> The following section (the running of the `npm run styles` command) is not necessary unless you are editing styles i.e. updating `.scss` files, in order to generate new `.css` files, as part of your contribution. + +Directly editing `.css` files is not recommended, because `.css` files are overwritten. Instead, if you would like to make and test a new/different style please go ahead and update the appropriate `.scss` file. The following command will automatically update the `.css` file that is relevant to the `.scss` file that you are editing: + + + +```bash +$ npm run styles +``` + +The above command is designed to be run in the background; enabling you to view your design changes (that are reflected in the `.css`) while you are editing the `.scss` in real-time. If you are not running this command in the background (i.e. just deliberately regenerating the `.css` files once), then the above command can be stopped by pressing `Ctrl` + `C`. + +### 6.6 Checking Your Content - Using Bartholomew's CLI + +The Bartholomew Command Line Interface (CLI) Tool is called `bart`. The `bart` CLI is a tool that simplifies working with Bartholomew projects (by now you probably already know that [Bartholomew](https://www.fermyon.com/blog/introducing-bartholomew) is our in-house WebAssembly (Wasm) content management system (CMS) that powers [our official Website](https://www.fermyon.com/)). And this (our official documentation) site. The `bart` CLI is handy to ensure quality assurance of new and existing content. Installing the CLI is a cinch, so please go ahead and use it when contributing. + +To build the Bartholomew CLI from source perform the following commands: + + + +```bash +$ cd ~ +$ git clone https://github.com/fermyon/bartholomew.git +$ cd ~/bartholomew +$ make bart +``` + +Once built, you will find the `bart` CLI executable in the `~/bartholomew/target/release` directory. However, for convenience it would be a great idea to go ahead and add the `bart` executable to your system path, for example: + + + +```bash +$ sudo mv ~/bartholomew/target/release/bart /usr/local/bin/ +``` + +Once installed, you can use the CLI's `--help` flag to learn more. For example: + + + +```bash +$ bart --help +bart 0.6.0 +The Bartholomew CLI + +USAGE: + bart + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + calendar Print the content calendar for a Bartholomew website + check Check content and identify errors or warnings + help Prints this message or the help of the given subcommand(s) + new Create a new page or website from a template +``` + +Let's take a quick look at how you can use the `bart` CLI to check any content that you are wanting to contribute. + +### 6.7 Checking Your Content - Preview a Documentation Page on Localhost + +You can host your changes to the developer documentation on your own machine (localhost) by using the following `spin` commands: + + + +```bash +$ npm ci +$ cd spin-up-hub +$ npm ci +$ cd .. +$ spin build +$ spin up -e "PREVIEW_MODE=1" +``` + +> Please note: using the `PREVIEW_MODE=1` as part of a `spin` command is safe on localhost and allows you to view the content (even if the `date` setting in the content's `.md` is set to a future date). It is often the case that you will be checking content before the publishing date via your system. The developer documentation's manifest file `spin.toml` has the `PREVIEW_MODE` set to `0` i.e. `environment = { PREVIEW_MODE = "0" }`. This `spin.toml` file is correct for a production environment and should always be `0` (so that the CMS adheres to the publishing `date` setting for content on the public site). Simply put, you can use `PREVIEW_MODE=1` safely in your command line on your localhost but you should never update the `spin.toml` file (in this regard). + +### 6.8 Scheduling Menu Items for Timed Release + +As mentioned above, all pages (`.md` files) in the documentation have a UTC date i.e. `date = "2023-07-25T17:26:00Z"`. The `date` is a page scheduling mechanism whereby each page is only displayed if the `date` has elapsed. Menu items (found in the `/developer/templates/*.hbs` files) that relate to a scheduled page can also be scheduled (so the specific menu item and its associated page appear at the same time). Simply envelope the menu item with the following `if` syntax to synchronize the appearance of the menu item with the related page: + + + +``` +{{#if (timed_publish "2023-07-25T17:26:00Z" env.PREVIEW_MODE)}} + // Scheduled menu item for timed release +{{/if}} +``` + +> In order to keep the code tidy and readable it is advised to remove the `if` logic (from the `.hbs` file) that wraps the content, once the `timed_publish` has elapsed. + +### 7. Checking Web Pages + +The `bart check` command can be used to check the content. Simply pass in the content as a parameter. The developer documentation [uses shortcodes](/bartholomew/shortcodes), so always pass `--shortcodes ./shortcodes` as shown below: + + + +```bash +$ bart check --shortcodes ./shortcodes content/spin/variables.md +shortcodes: registering alert +shortcodes: registering details +shortcodes: registering tabs +shortcodes: registering startTab +shortcodes: registering blockEnd +✅ content/spin/variables.md +``` + +> Note: `using a wildcard `*` will check a whole directory via a single command. For example, running `bart check --shortcodes ./shortcodes content/spin/*` will check all markdown files in the Spin project's documentation section. + +### 8. Add Changes + +Once your changes have been checked, go ahead and add your changes by moving to a top-level directory, under which your changes exist i.e. `cd ~/developer`. + +Add your changes by running the following command, from the root of the developer repository: + + + +```bash +$ git add +``` + +### 9. Commit Changes + +Before committing, please ensure that your GitHub installation is configured sufficiently so that you can `--signoff` as part of the `git commit` command. For example, please ensure that the `user.name` and `user.email` are configured in your terminal. You can check if these are set by typing `git config --list`. + +If you need to set these values please use the following commands: + + + +```bash +$ git config user.name "yourusername" +``` + + +```bash +$ git config user.email "youremail@somemail.com" +``` + +More information can be found at this GitHub documentation page called [signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). + +Type the following commit command to ensure that you sign off (--signoff), sign the data (-S) - recommended, and also leave a short message (-m): + + + +```bash +$ git commit -S --signoff -m "Updating documentation" +``` + +> Note: the `--signoff` option will only add a Signed-off-by trailer by the committer at the end of the commit log message. In addition to this, it is recommended that you use the `-S` option which will GPG-sign your commits. For more information about using GPG in GitHub see [this GitHub documentation](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account). + +### 10. Push Changes + +At this stage, it is a good idea to just quickly check what GitHub thinks the origin is. For example, if we type `git remote -v` we can see that the origin is our repo; which we a) forked the original repo into and b) which we then cloned to our local disk so that we could edit: + + + +```bash +$ git remote -v +``` + +The above command will return output similar to the following: + +```bash +origin git@github.com:yourusername/developer.git (fetch) +origin git@github.com:yourusername/developer.git (push) +upstream https://github.com/fermyon/developer (fetch) +upstream https://github.com/fermyon/developer (push) +``` + +Once you are satisfied go ahead and push your changes: + + + +```bash +$ git push -u origin my_new_branch +``` + +### 11. Create a Pull Request + +If you return to your GitHub repository in your browser, you will notice that a PR has automatically been generated for you. + +Clicking on the green “Compare and pull request” button will allow you to add a title and description as part of the PR. + +![Compare and pull request](/static/image/compare_and_pull_request.png) + +You can also add any information in the textbox provided below the title. For example, screen captures and/or code/console/terminal snippets of your contribution working correctly and/or tests passing etc. + +Once you have finished creating your PR, please keep an eye on the PR; answering any questions as part of the collaboration process. + +**Thank You** + +Thanks for contributing. diff --git a/content/spin/v3/contributing-spin.md b/content/spin/v3/contributing-spin.md new file mode 100644 index 000000000..0eea9aabe --- /dev/null +++ b/content/spin/v3/contributing-spin.md @@ -0,0 +1,133 @@ +title = "Contributing to Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/contributing-spin.md" + +--- +- [Making Code Contributions to Spin](#making-code-contributions-to-spin) +- [Before You Commit](#before-you-commit) +- [Committing and Pushing Your Changes](#committing-and-pushing-your-changes) + +We are delighted that you are interested in making Spin better! Thank you! + +This document will guide you through making your first contribution to the project. +We welcome and appreciate contributions of all types — opening issues, fixing +typos, adding examples, one-liner code fixes, tests, or complete features. + + +## Developer Community Calls + +
+ + Active Contributors of fermyon/spin - Last 28 days +
+ +>> _Recent contributors to Spin, past 30 days. Widget courtesy of OSSinsight.io._ + +Each Monday at 2:30pm UTC and 9:00pm UTC (alternating), we meet to discuss Spin issues, roadmap, and ideas in our Spin Project Meetings. The Spin Project follows an [open planning process](https://www.fermyon.com/blog/moving-to-a-fully-open-planning-process-for-the-spin-project) - anyone who is interested is welcome to join the discussion. [Subscribe to this Google Calendar](https://calendar.google.com/calendar/u/1?cid=c3Bpbi5tYWludGFpbmVyc0BnbWFpbC5jb20) for meeting dates. + +The [Spin Project Meeting Agenda is a public document](https://docs.google.com/document/d/1EG392gb8Eg-1ZEPDy18pgFZvMMrdAEybpCSufFXoe00/edit?usp=sharing). The document contains a rolling agenda with the date and time of each meeting, the Zoom link, and topics of discussion for the day. You will also find the meeting minutes for each meeting and the link to the recording. If you have something you would like to demo or discuss at the project meeting, we encourage you to add it to the agenda. + +## Code of Conduct + +First, any contribution and interaction on any Fermyon project MUST follow our +[code of conduct](https://www.fermyon.com/code-of-conduct). Thank you for being +part of an inclusive and open community! + +If you plan on contributing anything complex, please go through the issue and PR +queues first to make sure someone else has not started working on it. If it +doesn't exist already, please open an issue so you have a chance to get feedback +from the community and the maintainers before you start working on your feature. + +## Making Code Contributions to Spin + +The following guide is intended to make sure your contribution can get merged as +soon as possible. First, make sure you have Rust installed. + +![Rust Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffermyon%2Fspin%2Fmain%2FCargo.toml&query=$[%27workspace%27][%27package%27][%27rust-version%27]&label=Rust%20Version&logo=Rust&color=orange) + +After [installing Rust](https://www.rust-lang.org/tools/install) please ensure the `wasm32-wasi` and + `wasm32-unknown-unknown` targets are configured. For example: + +```bash +rustup target add wasm32-wasi && rustup target add wasm32-unknown-unknown +``` + +In addition, make sure you have the following prerequisites configured: + +- [`rustfmt`](https://github.com/rust-lang/rustfmt) +- [`clippy`](https://github.com/rust-lang/rust-clippy) +- `make` +- [`rust-analyzer`](https://rust-analyzer.github.io/) extension (for Visual Studio Code users) +- [GPG signature verification for your GitHub commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) and remember to use a sign-off message (`git commit -S -s`) on each of your commits + +Once you have set up the prerequisites and identified the contribution you want to make to Spin, make sure you can correctly build the project: + + + +```bash +# clone the repository +$ git clone https://github.com/fermyon/spin && cd spin +# add a new remote pointing to your fork of the project +$ git remote add fork https://github.com//spin +# create a new branch for your work +$ git checkout -b + +# build the Spin CLI +$ cargo build + +# make sure compilation is successful +$ ./target/debug/spin --help + +# run the tests and make sure they pass +$ make test +``` + +Now you should be ready to start making your contribution. To familiarize +yourself with the Spin project, please read the +[document about extending Spin](./extending-and-embedding.md). Since most of Spin is implemented in +Rust, we try to follow the common Rust coding conventions (keep an eye on the +recommendations from Clippy!). If applicable, add unit or integration tests to +ensure your contribution is correct. + +## Before You Commit + +* Format the code (`cargo fmt`) +* Run Clippy (`cargo clippy`) +* Run the `lint` task (`make lint`) +* Build the project and run the tests (`make test`) + +Spin enforces lints and tests as part of continuous integration - running them locally will save you a round-trip to your pull request! + +If everything works locally, you're ready to commit your changes. + +## Committing and Pushing Your Changes + +We require commits to be signed both with an email address and with a GPG signature. + +> Because of the way GitHub runs enforcement, the GPG signature isn't checked until _after_ all tests have run. Be sure to GPG sign up front, as it can be a bit frustrating to wait for all the tests and _then_ get blocked on the signature! + + + +```bash +$ git commit -S -s -m "" +``` + +> Some contributors like to follow the [Conventional Commits](https://github.com/conventional-commits) convention for commit messages. + +We try to only keep useful changes as separate commits — if you prefer to commit +often, please +[cleanup the commit history](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) +before opening a pull request. + +Once you are happy with your changes you can push the branch to your fork: + + + +```bash +# "fork" is the name of the git remote pointing to your fork +$ git push fork +``` + +Now you are ready to create a pull request. Thank you for your contribution! diff --git a/content/spin/v3/deploying-to-fermyon.md b/content/spin/v3/deploying-to-fermyon.md new file mode 100644 index 000000000..8cd77b6b7 --- /dev/null +++ b/content/spin/v3/deploying-to-fermyon.md @@ -0,0 +1,31 @@ +title = "Deploying Spin Applications to Fermyon" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/deploying-to-fermyon.md" + +--- +- [Fermyon Cloud](#fermyon-cloud) +- [Fermyon Platform](#fermyon-platform) + - [Running on Your Workstation](#running-on-your-workstation) + - [Running on AWS](#running-on-aws) + +## Fermyon Cloud + +[Fermyon Cloud](/cloud) is a self-service application platform for WebAssembly-based serverless functions and microservices. It enables you to run Spin applications, at scale, in the cloud, without any infrastructure setup or maintenance required. + +## Fermyon Platform + +[Fermyon Platform](https://www.fermyon.dev/) is a self-host platform for Spin applications. With Fermyon, you can deploy your spin applications onto a server in moments. + +> Fermyon Platform does not currently support Spin 2 or above. + +### Running on Your Workstation + +For instructions guiding you through running the Fermyon platform on your development workstation, +follow [this guide](https://www.fermyon.dev/quickstart-local). + +### Running on AWS + +For instructions guiding you through running the Fermyon platform on AWS, follow +[this guide](https://www.fermyon.dev/quickstart-aws). diff --git a/content/spin/v3/distributing-apps.md b/content/spin/v3/distributing-apps.md new file mode 100644 index 000000000..0fe907ed5 --- /dev/null +++ b/content/spin/v3/distributing-apps.md @@ -0,0 +1,140 @@ +title = "Publishing and Distribution" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/distributing-apps.md" + +--- +- [Logging Into a Registry](#logging-into-a-registry) + - [Logging In Using a Token](#logging-in-using-a-token) + - [Fallback Credentials](#fallback-credentials) +- [Publishing a Spin Application to a Registry](#publishing-a-spin-application-to-a-registry) +- [Running Published Applications](#running-published-applications) + - [Running Published Applications by Digest](#running-published-applications-by-digest) + - [Pulling a Published Application](#pulling-a-published-application) +- [Signing Spin Applications and Verifying Signatures](#signing-spin-applications-and-verifying-signatures) + +If you would like to publish a Spin application, so that other users can run it, you can do so using a _container registry_. + +{{ details "What's all this about containers?" "The registry protocol was originally created to publish and distribute Docker containers. Over time, registries have evolved to host other kinds of artifact - see [the OCI registry artifacts project](https://github.com/opencontainers/artifacts) for more information. However, the term remains, in services such as GitHub Container Registry or AWS Elastic Container Registry, and in the generic description _OCI (Open Container Initiative) registries_. When you use a 'container' registry to publish and distribute Spin applications, there are no actual containers involved at all!" }} + +Many cloud services offer public registries. Examples include GitHub Container Registry, Docker Hub, or Amazon Elastic Container Registry. These support both public and private distribution. You can also run your own registry using open source software. + +## Logging Into a Registry + +Before you can publish to a registry, or run applications whose registry artifacts are private, you must log in to the registry. This example shows logging into the GitHub Container Registry, `ghcr.io`: + + + +```bash +$ spin registry login ghcr.io +``` + +If you don't provide any options to `spin registry login`, it prompts you for a username and password. + +### Logging In Using a Token + +In a non-interactive environment such as GitHub Actions, you will typically log in using a token configured in the environment settings, rather than a password. To do this, use the `--password-stdin` flag, and `echo` the token value to the login command's standard input. This example shows logging into GHCR from a GitHub action: + + + +```bash +$ echo "$\{{ secrets.GITHUB_TOKEN }}" | spin registry login ghcr.io --username $\{{ github.actor }} --password-stdin +``` + +Other environments will have different ways of referring to the token and user but the pattern remains the same. + +### Fallback Credentials + +If you have logged into a registry using `docker login`, but not using `spin registry login`, Spin will fall back to your Docker credentials. + +## Publishing a Spin Application to a Registry + +To publish an application to a registry, use the `spin registry push` command. You must provide a _reference_ for the published application. This is a string whose format is defined by the registry standard, and generally consists of `//:`. (In specific circumstances you may be able to omit the username and/or the version. If you want more detail on references, see the OCI documentation.) + +> Remember you will (usually) need to be logged in to publish to a registry. + +Here is an example of pushing an application to GHCR: + + + +```bash +$ spin registry push ghcr.io/alyssa-p-hacker/hello-world:v1 +Pushed with digest sha256:06b19 +``` + +Notice that the username is part of the reference; the registry does not infer it from the login. Also notice that the version is specified explicitly; Spin does not infer it from the `spin.toml` file. + +> Whether newly uploaded artifacts are private or public depends on the registry. See your registry documentation. This will also tell you how to change the visibility if the default is not what you want. + +## Running Published Applications + +To run a published application from a registry, use `spin up -f` and pass the registry reference: + + + +```bash +$ spin up -f ghcr.io/alyssa-p-hacker/hello-world:v1 +``` + +> Remember that if the artifact is private you will need to be logged in, with permission to access it. + +> Spin optimizes downloads using a local [registry cache](./cache). When running an application from a remote registry, Spin always tries to check the registry for updates, even if the application has already been pulled. However, content files that are already pulled will not be re-downloaded. This applies even if they were downloaded as part of a different application. + +### Running Published Applications by Digest + +Registry versions are mutable; that is, the owner of an application can change which build the `:v1` label points to at any time. If you want to run a specific build of the package, you can refer to it by _digest_. This is similar to a Git commit hash: it is immutable, meaning the same digest always gets the exact same data, no matter what the package owner does. To do this, use the `@sha256:...` syntax instead of the `:v...` syntax: + + + +```bash +$ spin up -f ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 +``` + +### Pulling a Published Application + +`spin up` automatically downloads the application from the registry. If you want to manually download the application, without running it, use the `spin registry pull` command: + + + +```bash +$ spin registry pull ghcr.io/alyssa-p-hacker/hello-world:v1 +$ spin registry pull ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 +``` + +> Downloaded applications are cached. When run, or pulled again, Spin checks to see if they have changed from the cached copy, and downloads only the changes if any. + +## Signing Spin Applications and Verifying Signatures + +Because Spin uses the container registry standards to distribute applications, it can also take advantage of tooling built around those standards. Here is an example of using [Cosign and Sigstore](https://docs.sigstore.dev/) to sign and verify a Spin application: + + + +```bash +# Push your Spin application to any registry that supports the OCI registry artifacts, +# such as the GitHub Container Registry, Docker Hub, Azure ACR, or AWS ECR. +$ spin registry push ghcr.io/alyssa-p-hacker/hello-world:v1 + +# You can now sign your Spin app using Cosign (or any other tool that can sign +# OCI registry objects). +$ cosign sign ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 +Generating ephemeral keys... +Retrieving signed certificate... +tlog entry created with index: 12519542 +Pushing signature to: ghcr.io/alyssa-p-hacker/hello-world + +# Someone interested in your application can now use Cosign to verify the signature +# before running the application. +$ cosign verify ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 +Verification for ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - Existence of the claims in the transparency log was verified offline + - Any certificates were verified against the Fulcio roots. + +# The consumer of your app can now run it from the registry. +$ spin up -f ghcr.io/alyssa-p-hacker/hello-world@sha256:06b19 +``` + +> You'll need Cosign 2.0 or above to verify Spin artifacts. diff --git a/content/spin/v3/dynamic-configuration.md b/content/spin/v3/dynamic-configuration.md new file mode 100644 index 000000000..1632563ca --- /dev/null +++ b/content/spin/v3/dynamic-configuration.md @@ -0,0 +1,350 @@ +title = "Dynamic and Runtime Application Configuration" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/dynamic-configuration.md" + +--- + +- [Application Variables Runtime Configuration](#application-variables-runtime-configuration) + - [Environment Variable Provider](#environment-variable-provider) + - [Vault Application Variable Provider](#vault-application-variable-provider) + - [Vault Application Variable Provider Example](#vault-application-variable-provider-example) + - [Azure Key Vault Application Variable Provider](#azure-key-vault-application-variable-provider) + - [Azure Key Vault Application Variable Provider Example](#azure-key-vault-application-variable-provider-example) +- [Key Value Store Runtime Configuration](#key-value-store-runtime-configuration) + - [Redis Key Value Store Provider](#redis-key-value-store-provider) + - [Azure CosmosDB Key Value Store Provider](#azure-cosmosdb-key-value-store-provider) +- [SQLite Storage Runtime Configuration](#sqlite-storage-runtime-configuration) + - [LibSQL Storage Provider](#libsql-storage-provider) +- [LLM Runtime Configuration](#llm-runtime-configuration) + - [Remote Compute Provider](#remote-compute-provider) + +Configuration for Spin application features such as [application variables](./variables), +[key value storage](./kv-store-api-guide), [SQL storage](./sqlite-api-guide) +and [Serverless AI](./serverless-ai-api-guide) can be supplied dynamically, i.e. during the application runtime, +requiring no changes to the application code itself. + +This runtime configuration data is stored in the `runtime-config.toml` file and passed in via the `--runtime-config-file` flag +when invoking the `spin up` command. + +Let's look at each configuration category in-depth below. + +## Application Variables Runtime Configuration + +[Application Variables](./variables) values may be set at runtime by providers. Currently, +there are three application variable providers: the [environment-variable provider](#environment-variable-provider), +the [Vault provider](#vault-application-variable-provider) and the [Azure Key Vault provider](#azure-key-vault-application-variable-provider). + +Multiple application variable providers can be configured in Spin. Providers are +prioritized top-down in the runtime configuration file, with higher-listed providers +taking precedence. The environment variable provider always has the highest +priority. + +The provider examples below show how to use or configure each +provider. For examples on how to access these variables values within your application, see +[Using Variables from Applications](./variables#using-variables-from-applications). + +### Environment Variable Provider + +The environment variable provider gets variable values from the `spin` process's +environment (_not_ the component `environment`). Variable keys are translated +to environment variables by upper-casing and prepending with `SPIN_VARIABLE_`: + + + +```bash +$ export SPIN_VARIABLE_API_KEY="1234" # Sets the `api_key` value. +$ spin up +``` + +### Vault Application Variable Provider + +The Vault application variable provider gets secret values from [HashiCorp Vault](https://www.vaultproject.io/). +Currently, only the [KV Secrets Engine - Version 2](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2) is supported. +You can set up the v2 kv secret engine at any mount point and provide Vault information in +the [runtime configuration](#runtime-configuration) file: + + + +```toml +[[config_provider]] +type = "vault" +url = "http://127.0.0.1:8200" +token = "root" +mount = "secret" +``` + +#### Vault Application Variable Provider Example + +1. [Install Vault](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-install). +2. Start Vault: + + + +```bash +$ vault server -dev -dev-root-token-id root +``` + +3. Set a password: + + + +```bash +$ export VAULT_TOKEN=root +$ export VAULT_ADDR=http://127.0.0.1:8200 +$ vault kv put secret/secret value="test_password" +$ vault kv get secret/secret +``` + +4. Go to the [Vault variable provider example](https://github.com/fermyon/enterprise-architectures-and-patterns/tree/main/application-variable-providers/vault-provider) application. +5. Build and run the `vault-provider` app: + + + +```bash +$ spin build +$ spin up --runtime-config-file runtime_config.toml +``` + +6. Test the app: + + + +```bash +$ curl localhost:3000 --data "test_password" +{"authentication": "accepted"} +``` + + +```bash +$ curl localhost:3000 --data "wrong_password" +{"authentication": "denied"} +``` + +### Azure Key Vault Application Variable Provider + +The Azure Key Vault application variable provider gets secret values from [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault). + +Currently, only receiving the latest version of a secret is supported. + +For authenticating against Azure Key Vault, you must use the client credentials flow. To do so, create a Service Principal (SP) within your Microsoft Entra ID (previously known as Azure Active Directory) and assign the `Key Vault Secrets User` role to the SP on the scope of your Azure Key Vault instance. + +You can set up Azure Key Vault application variable provider in +the [runtime configuration](#runtime-configuration) file: + + + +```toml +[[config_provider]] +type = "azure_key_vault" +vault_url = "https://spin.vault.azure.net/" +client_id = "12345678-1234-1234-1234-123456789012" +client_secret = "some.generated.password" +tenant_id = "12345678-1234-1234-1234-123456789012" +authority_host = "AzurePublicCloud" +``` + +#### Azure Key Vault Application Variable Provider Example + +1. Deploy Azure Key Vault: + + + +```bash +# Variable Definition +$ KV_NAME=spin123 +$ LOCATION=germanywestcentral +$ RG_NAME=rg-spin-azure-key-vault + +# Create an Azure Resource Group and an Azure Key Vault +$ az group create -n $RG_NAME -l $LOCATION +$ az keyvault create -n $KV_NAME \ + -g $RG_NAME \ + -l $LOCATION \ + --enable-rbac-authorization true + +# Grab the Azure Resource Identifier of the Azure Key Vault instance +$ KV_SCOPE=$(az keyvault show -n $KV_NAME -g $RG_NAME -otsv --query "id") +``` + +2. Add a Secret to the Azure Key Vault instance: + + + +```bash +# Grab the ID of the currently signed in user in Azure CLI +$ CURRENT_USER_ID=$(az ad signed-in-user show -otsv --query "id") + +# Make the currently signed in user a "Key Vault Secrets Officer" +# on the scope of the new Azure Key Vault instance +$ az role assignment create --assignee $CURRENT_USER_ID \ + --role "Key Vault Secrets Officer" \ + --scope $KV_SCOPE + +# Create a test secret called `secret` in the Azure Key Vault instance +$ az keyvault secret set -n secret --vault-name $KV_NAME --value secret_value --onone +``` + +3. Create a Service Principal and Role Assignment for Spin: + + + +```bash +$ export SP_NAME=sp-spin + +# Create the SP +$ SP=$(az ad sp create-for-rbac -n $SP_NAME -ojson) + +# Populate local shell variables from the SP JSON +$ CLIENT_ID=$(echo $SP | jq -r '.appId') +$ CLIENT_SECRET=$(echo $SP | jq -r '.password') +$ TENANT_ID=$(echo $SP | jq -r '.tenant') + +# Assign the "Key Vault Secrets User" role to the SP +# allowing it to read secrets from the Azure Key Vault instance +$ az role assignment create --assignee $CLIENT_ID \ + --role "Key Vault Secrets User" \ + --scope $KV_SCOPE +``` + +4. Go to the [Azure Key Vault variable provider example](https://github.com/fermyon/enterprise-architectures-and-patterns/tree/main/application-variable-providers/azure-key-vault-provider) application. +5. Replace Tokens in `runtime_config.toml`. + +The `azure-key-vault-provider` application contains a `runtime_config.toml` file. Replace all tokens (e.g. `$KV_NAME$`) with the corresponding shell variables you created in the previous steps. + +6. Build and run the `azure-key-vault-provider` app: + + + +```bash +$ spin build +$ spin up --runtime-config-file runtime_config.toml +``` + +7. Test the app: + + + +```bash +$ curl localhost:3000 +Loaded Secret from Azure Key Vault: secret_value +``` + +## Key Value Store Runtime Configuration + +Spin provides built-in key-value storage. This storage is backed by an SQLite database embedded in Spin by default. However, the Spin runtime configuration file (`runtime-config.toml`) can be updated to not only modify the SQLite configuration but also choose to use a different backing store. The available store options are the embedded SQLite database, an external Redis database or Azure CosmosDB. + +### Redis Key Value Store Provider + +The following is an example of how an application's `runtime-config.toml` file can be configured to use Redis instead. Note the `type` and `url` values, which are set to `redis` and the URL of the Redis host, respectively: + +```toml +[key_value_store.default] +type = "redis" +url = "redis://localhost" +``` + +### Azure CosmosDB Key Value Store Provider + +Similarly, to implement Azure CosmosDB as a backend for Spin's key/value store, change the type to `azure_cosmos` and specify your database account details: + +```toml +[key_value_store.default] +type = "azure_cosmos" +key = "" +account = "" +database = "" +container = "" +``` + +> Note: The CosmosDB container must be created with the default partition key, `/id`. + +Whilst a single default store may be sufficient for certain application use cases, each Spin application can be configured to support multiple stores of any `type`, as shown in the `runtime-config.toml` file below: + +> **Note:** At present, when deploying an application to Fermyon Cloud only the single "default" key-value store is supported. To see more about Spin support on Fermyon Cloud, visit the [limitations documentation](/cloud/faq#spin-limitations): + +```toml +# This defines a new store named user_data +[key_value_store.user_data] +type = "spin" +path = ".spin/user_data.db" + +# This defines a new store named other_data backed by a Redis database +[key_value_store.other_data] +type = "redis" +url = "redis://localhost" +``` + +You must individually grant each component access to the stores that it needs to use. To do this, use the `component.key_value_stores` entry in the component manifest within `spin.toml`. See [Spin Key Value Store](kv-store-api-guide.md) for more details. + +## SQLite Storage Runtime Configuration + +Spin provides built-in SQLite storage. By default, this is backed by a database that Spin creates for you underneath your application directory (in the `.spin` subdirectory). However, you can use the Spin runtime configuration file (`runtime-config.toml`) to add and customize SQLite databases. + +The following example `runtime-config.toml` tells Spin to map the `default` database to an SQLite database elsewhere in the file system: + +```toml +[sqlite_database.default] +type = "spin" +path = "/planning/todo.db" +``` + +If you need more than one database, you can configure multiple databases, each with its own name: + +```toml +# This defines a new store named todo +[sqlite_database.todo] +type = "spin" +path = "/planning/todo.db" + +# This defines a new store named finance +[sqlite_database.finance] +type = "spin" +path = "/super/secret/monies.db" +``` + +Spin creates any database files that don't exist. However, it is up to you to delete them when you no longer need them. + +### LibSQL Storage Provider + +Spin can also use [libSQL](https://libsql.org/) databases accessed over HTTPS. libSQL is fully compatible with SQLite but provides additional features including remote, distributed databases. + +> Spin does not provide libSQL access to file-based databases, only databases served over HTTPS. Specifically, Spin supports [the `sqld` libSQL server](https://github.com/libsql/sqld). + +To use libSQL, set `type = "libsql"` in your `runtime-config.toml` entry. You must then provide a `url` and authentication `token` instead of a file path. For example, this entry tells Spin to map the `default` database to a libSQL service running on `libsql.example.com`: + +```toml +# This tells Spin to use the remote host as its default database +[sqlite_database.default] +type = "libsql" +url = "https://sensational-penguin-ahacker.libsql.example.com" +token = "a secret" +``` + +Spin does _not_ create libSQL databases. Use your hosting service's tools to create them (or `sqld` if you are self-hosting) . You can still set up tables and data in a libSQL database via `spin up --sqlite`. + +> You must include the scheme in the `url` field. The scheme must be `http` or `https`. Non-HTTP libSQL protocols are not supported. + +The `default` database will still be defined, even if you add other databases. + +By default, components will not have access to any of these databases (even the default one). You must grant each component access to the databases that it needs to use. To do this, use the `component.sqlite_databases` entry in the component manifest within `spin.toml`. See [SQLite Database](./sqlite-api-guide.md) for more details. + +## LLM Runtime Configuration + +Spin provides a Large Language Model interface for interacting with LLMs for inferencing and embedding. The default host implementation is to use local CPU/GPU compute. However, the Spin runtime configuration file (`runtime-config.toml`) can be updated to enable Spin to use remote compute using HTTP requests. + +### Remote Compute Provider + +The following is an example of how an application's `runtime-config.toml` file can be configured to use the remote compute option. Note the `type`, `url` and `auth_token` are set to `remote_http`, URL of the server and the auth token for the server. + +```toml +[llm_compute] +type = "remote_http" +url = "http://example.com" +auth_token = "" +``` + +Currently, the remote compute option requires an user to deploy their own LLM proxy service. Fermyon Cloud users can do this using the [`cloud-gpu` plugin](https://github.com/fermyon/spin-cloud-gpu). If you prefer to create and deploy your own proxy service, you can find a reference implementation of the proxy protocol in the [`spin-cloud-gpu plugin repository`](https://github.com/fermyon/spin-cloud-gpu/blob/main/fermyon-cloud-gpu/src/index.ts). + +By default, components will not have access to the LLM models unless granted explicit access through the `component.ai_models` entry in the component manifest within `spin.toml`. See [Serverless AI](./serverless-ai-api-guide) for more details. diff --git a/content/spin/v3/extending-and-embedding.md b/content/spin/v3/extending-and-embedding.md new file mode 100644 index 000000000..b83c466a9 --- /dev/null +++ b/content/spin/v3/extending-and-embedding.md @@ -0,0 +1,165 @@ +title = "Extending and Embedding Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/extending-and-embedding.md" + +--- +- [Extending Spin with a Custom Trigger](#extending-spin-with-a-custom-trigger) + - [Implement the Trigger World](#implement-the-trigger-world) + - [The Trigger Implements the `TriggerExecutor` Trait](#the-trigger-implements-the-triggerexecutor-trait) + - [The Trigger is an Executable](#the-trigger-is-an-executable) + - [The Trigger Detects Events...](#the-trigger-detects-events) + - [...and Invokes the Guest](#and-invokes-the-guest) +- [Other Ways to Extend and Use Spin](#other-ways-to-extend-and-use-spin) + +## Extending Spin with a Custom Trigger + +> The complete example for a custom trigger [can be found on GitHub](https://github.com/fermyon/spin/tree/main/examples/spin-timer). + +Spin currently implements triggers and application models for: + +- [HTTP applications](./http-trigger.md) that are triggered by incoming HTTP +requests, and that return an HTTP response +- [Redis applications](./redis-trigger.md) that are triggered by messages on Redis +channels + +The Spin internals and execution context (the part of Spin executing +components) are agnostic of the event source and application model. +In this document, we will explore how to extend Spin with custom event sources +(triggers) and application models built on top of the WebAssembly component +model, as well as how to embed Spin in your application. + +In this article, we will build a Spin trigger to run the applications based on a +timer, executing Spin components at a configured time interval. + +> Custom triggers are an experimental feature. The trigger APIs are not stabilized, and you may need to tweak your trigger code as you update to new Spin versions. + +Application entry points are defined using +[WebAssembly Interface (WIT)](https://component-model.bytecodealliance.org/design/wit.html). Here's the entry point for HTTP triggers: + + + +```fsharp +interface incoming-handler { + use types.{incoming-request, response-outparam} + + handle: func( + request: incoming-request, + response-out: response-outparam + ) +} +``` + +The entry point we want to execute for our timer trigger takes no input and doesn't return anything. This is purposefully chosen +to be a simple function signature. For simplicity, we allow guest code to use Spin's [application variables API](./variables.md) but not other Spin APIs. + +> Custom trigger guest code can't currently use the Spin SDK and must refer to the Spin WITs directly. + +Here is the resulting timer WIT: + + + +```fsharp +// examples/spin-timer/spin-timer.wit +package fermyon:example + +world spin-timer { + import fermyon:spin/variables@2.0.0 + export handle-timer-request: func() +} +``` + +`handle-timer-request` is the function that all components executed by the timer trigger must +implement, and which is used by the timer executor when instantiating and +invoking the component. + +The timer trigger itself is a Spin plugin whose name is `trigger-timer`. The first part must be `trigger` and the second part is the trigger type. + +You can see the full timer trigger code at the link above but here are some key features. + +### Implement the Trigger World + +The timer trigger implements the WIT world described in `spin-timer.wit`. To do that, it uses the [Bytecode Alliance `wit-bindgen` project](https://github.com/bytecodealliance/wit-bindgen) — this generates code that allows the trigger to invoke the guest's entry point, and allows the guest to invoke the Spin APIs available in the world. + + + +```rust +// examples/spin-timer/src/main.rs +wasmtime::component::bindgen!({ + path: ".", + world: "spin-timer", + async: true +}); +``` + +### The Trigger Implements the `TriggerExecutor` Trait + +Using `TriggerExecutor` allows the trigger to offload a great deal of boilerplate loader work to `spin_trigger::TriggerExecutorCommand`. + +```rust +struct TimerTrigger { + engine: TriggerAppEngine, + speedup: u64, + component_timings: HashMap, +} + +#[async_trait] +impl TriggerExecutor for TimerTrigger { + // ... +} +``` + +### The Trigger is an Executable + +A trigger is a separate program, so that it can be installed as a plugin. So it is a Rust `bin` project and has a `main` function. It can be useful to also provide a library crate, so that projects that embed Spin can load it in process if desired, but the timer sample doesn't currently show that. + +```rust +type Command = TriggerExecutorCommand; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let t = Command::parse(); + t.run().await +} +``` + +### The Trigger Detects Events... + +In this case the trigger "detects" events by running a timer. In most cases, the trigger detects events by listening on a socket, completion port, or other mechanism, or by polling a resource such as a directory or an HTTP endpoint. + +```rust +for (c, d) in &self.component_timings { + scope.spawn(async { + let duration = tokio::time::Duration::from_millis(*d * 1000 / speedup); + loop { + tokio::time::sleep(duration).await; + self.handle_timer_event(c).await.unwrap(); + } + }); +} +``` + +### ...and Invokes the Guest + +The `TriggerExecutorCommand` infrastructure equips the trigger object with a `TriggerAppEngine` specialized to the entry point described in the WIT, and already initialized with the guest Wasm. When an event occurs, the trigger invokes the guest via this engine. + +```rust +async fn handle_timer_event(&self, component_id: &str) -> anyhow::Result<()> { + // Load the guest... + let (instance, mut store) = self.engine.prepare_instance(component_id).await?; + let EitherInstance::Component(instance) = instance else { + unreachable!() + }; + let instance = SpinTimer::new(&mut store, &instance)?; + // ...and call the entry point + instance.call_handle_timer_request(&mut store).await +} +``` + +## Other Ways to Extend and Use Spin + +Besides building custom triggers, the internals of Spin could also be used independently: + +- The Spin execution context can be used entirely without a `spin.toml` application manifest — for embedding scenarios, the configuration for the +execution can be constructed without a `spin.toml`, for example by running applications only from a registry. See [Building a Host for the Spin Runtime](https://www.fermyon.com/blog/building-host-for-spin-runtime) for a simple example. diff --git a/content/spin/v3/go-components.md b/content/spin/v3/go-components.md new file mode 100644 index 000000000..88398355a --- /dev/null +++ b/content/spin/v3/go-components.md @@ -0,0 +1,391 @@ +title = "Building Spin components in Go" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/go-components.md" + +--- + +- [Versions](#versions) +- [HTTP Components](#http-components) +- [Sending Outbound HTTP Requests](#sending-outbound-http-requests) +- [Redis Components](#redis-components) +- [Storing Data in Redis From Go Components](#storing-data-in-redis-from-go-components) +- [Using Go Packages in Spin Components](#using-go-packages-in-spin-components) +- [Storing Data in the Spin Key-Value Store](#storing-data-in-the-spin-key-value-store) +- [Storing Data in SQLite](#storing-data-in-sqlite) +- [AI Inferencing From Go Components](#ai-inferencing-from-go-components) + +> This guide assumes you have Spin installed. If this is your first encounter with Spin, please see the [Quick Start](quickstart), which includes information about installing Spin with the Go templates, installing required tools, and creating Go applications. + +> This guide assumes you are familiar with the Go programming language, and that +> you have +> [configured the TinyGo toolchain locally](https://tinygo.org/getting-started/install/). +Using TinyGo to compile components for Spin is currently required, as the +[Go compiler doesn't currently have support for compiling to WASI](https://github.com/golang/go/issues/31105). + +> All examples from this page can be found in [the Spin Go SDK repository on GitHub](https://github.com/fermyon/spin-go-sdk/tree/main/examples). + +[**Want to go straight to the Spin SDK reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2) + +## Versions + +TinyGo `0.30.0` is recommended, which requires Go `v1.19+`. + +## HTTP Components + +In Spin, HTTP components are triggered by the occurrence of an HTTP request, and +must return an HTTP response at the end of their execution. Components can be +built in any language that compiles to WASI, and Go has improved support for +writing applications, through its SDK. + +Building a Spin HTTP component using the Go SDK means writing a single function, +`init` — below is a complete implementation for such a component: + + + +```go +// A Spin component written in Go that returns "Hello, Fermyon!" +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "Hello Fermyon!") + }) +} + +func main() {} +``` + +The Spin HTTP component (written in Go) can be built using the `tingygo` toolchain: + + + +```bash +$ tinygo build -o main.wasm -target=wasi main.go +``` + +Once built, we can run our Spin HTTP component using the Spin up command: + + + +```bash +$ spin up +``` + +The Spin HTTP component can now receive and process incoming requests: + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +content-type: text/plain +content-length: 15 + +Hello Fermyon! +``` + +The important things to note in the implementation above: + +- the entry point to the component is the standard `func init()` for Go programs +- handling the request is done by calling the `spinhttp.Handle` function, +which takes a `func(w http.ResponseWriter, r *http.Request)` as parameter — these +contain the HTTP request and response writer you can use to handle the request +- the HTTP objects (`*http.Request`, `http.Response`, and `http.ResponseWriter`) +are the Go objects from the standard library, so working with them should feel +familiar if you are a Go developer + +## Sending Outbound HTTP Requests + +If allowed, Spin components can send outbound requests to HTTP endpoints. Let's +see an example of a component that makes a request to +[an API that returns random animal facts](https://random-data-api.fermyon.app/animals/json) and +inserts a custom header into the response before returning: + + + +```go +// A Spin component written in Go that sends a request to an API +// with random animal facts. + +package main + +import ( + "fmt" + "net/http" + "os" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + resp, _ := spinhttp.Get("https://random-data-api.fermyon.app/animals/json") + + fmt.Fprintln(w, resp.Body) + fmt.Fprintln(w, resp.Header.Get("content-type")) + + // `spin.toml` is not configured to allow outbound HTTP requests to this host, + // so this request will fail. + if _, err := spinhttp.Get("https://fermyon.com"); err != nil { + fmt.Fprintf(os.Stderr, "Cannot send HTTP request: %v", err) + } + }) +} + +func main() {} +``` + +The Outbound HTTP Request example above can be built using the `tingygo` toolchain: + + + +```bash +$ tinygo build -o main.wasm -target=wasi main.go +``` + +Before we can execute this component, we need to add the +`random-data-api.fermyon.app` domain to the application manifest `allowed_outbound_hosts` +list containing the list of domains the component is allowed to make HTTP +requests to: + + + +```toml +# spin.toml +spin_manifest_version = 2 + +[application] +name = "spin-hello-tinygo" +version = "1.0.0" + +[[trigger.http]] +route = "/hello" +component = "tinygo-hello" + +[component.tinygo-hello] +source = "main.wasm" +allowed_outbound_hosts = [ "https://random-data-api.fermyon.app" ] +``` + +Running the application using `spin up` will start the HTTP +listener locally (by default on `localhost:3000`), and our component can +now receive requests in route `/hello`: + + + +```bash +$ curl -i localhost:3000/hello +HTTP/1.1 200 OK +content-length: 93 + +{"timestamp":1684299253331,"fact":"Reindeer grow new antlers every year"} +``` + +> Without the `allowed_outbound_hosts` field populated properly in `spin.toml`, +> the component would not be allowed to send HTTP requests, and sending the +> request would generate in a "Destination not allowed" error. + +> You can set `allowed_outbound_hosts = ["https://*:*"]` if you want to allow +> the component to make requests to any HTTP host. This is not recommended +> unless you have a specific need to contact arbitrary servers and perform your own safety checks. + +## Redis Components + +Besides the HTTP trigger, Spin has built-in support for a Redis trigger, which +will connect to a Redis instance and will execute components for new messages +on the configured channels. + +> See the [Redis trigger](./redis-trigger.md) for details about the Redis trigger. + +Writing a Redis component in Go also takes advantage of the SDK: + + + +```go +package main + +import ( + "fmt" + + "github.com/fermyon/spin/sdk/go/v2/redis" +) + +func init() { + // redis.Handle() must be called in the init() function. + redis.Handle(func(payload []byte) error { + fmt.Println("Payload::::") + fmt.Println(string(payload)) + return nil + }) +} + +// main function must be included for the compiler but is not executed. +func main() {} +``` + +The manifest for a Redis application must contain the address of the Redis instance. This is set at the application level: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "spin-redis" +trigger = { type = "redis", } +version = "0.1.0" + +[application.trigger.redis] +address = "redis://localhost:6379" + +[[trigger.redis]] +channel = "messages" +component = "echo-message" + +[component.echo-message] +source = "main.wasm" +[component.echo-message.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +``` + +The application will connect to `redis://localhost:6379`, and for every new message +on the `messages` channel, the `echo-message` component will be executed: + + + +```bash +# first, start redis-server on the default port 6379 +$ redis-server --port 6379 +# then, start the Spin application +$ spin build --up +INFO spin_redis_engine: Connecting to Redis server at redis://localhost:6379 +INFO spin_redis_engine: Subscribed component 0 (echo-message) to channel: messages +``` + +For every new message on the `messages` channel: + + + +```bash +$ redis-cli +127.0.0.1:6379> publish messages "Hello, there!" +``` + +Spin will instantiate and execute the component: + + + +```bash +INFO spin_redis_engine: Received message on channel "messages" +Payload:::: +Hello, there! +``` + +## Storing Data in Redis From Go Components + +Using the Spin's Go SDK, you can use the Redis key/value store to publish +messages to Redis channels. This can be used from both HTTP and Redis triggered +components. + +Let's see how we can use the Go SDK to connect to Redis. This HTTP component demonstrates fetching a value from Redis by key, setting a +key with a value, and publishing a message to a Redis channel: + + + +```go +package main + +import ( + "net/http" + "os" + + spin_http "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/redis" +) + +func init() { + // handler for the http trigger + spin_http.Handle(func(w http.ResponseWriter, r *http.Request) { + + // addr is the environment variable set in `spin.toml` that points to the + // address of the Redis server. + addr := os.Getenv("REDIS_ADDRESS") + + // channel is the environment variable set in `spin.toml` that specifies + // the Redis channel that the component will publish to. + channel := os.Getenv("REDIS_CHANNEL") + + // payload is the data publish to the redis channel. + payload := []byte(`Hello redis from tinygo!`) + + // create a new redis client. + rdb := redis.NewClient(addr) + + if err := rdb.Publish(channel, payload); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // set redis `mykey` = `myvalue` + if err := rdb.Set("mykey", []byte("myvalue")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // get redis payload for `mykey` + if payload, err := rdb.Get("mykey"); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + w.Write([]byte("mykey value was: ")) + w.Write(payload) + } + }) +} + +func main() {} +``` + +As with all networking APIs, you must grant access to Redis hosts via the `allowed_outbound_hosts` field in the application manifest: + + + +```toml +[component.storage-demo] +environment = { REDIS_ADDRESS = "redis://127.0.0.1:6379", REDIS_CHANNEL = "messages" } +allowed_outbound_hosts = ["redis://127.0.0.1:6379"] +``` + +This HTTP component can be paired with a Redis component, triggered on new messages on the `messages` Redis channel, to build an asynchronous messaging application, where the HTTP front-end queues work for a Redis-triggered back-end to execute as capacity is available. + +> You can find a complete example for using outbound Redis from an HTTP component +> in the [Spin repository on GitHub](https://github.com/fermyon/spin-go-sdk/tree/main/examples/redis-outbound). + +## Using Go Packages in Spin Components + +Any [package from the Go standard library](https://tinygo.org/docs/reference/lang-support/stdlib/) that can be imported in TinyGo and that compiles to +WASI can be used when implementing a Spin component. + +> Make sure to read [the page describing the HTTP trigger](./http-trigger.md) for more +> details about building HTTP applications. + +## Storing Data in the Spin Key-Value Store + +Spin has a key-value store built in. For information about using it from TinyGo, see [the key-value store tutorial](key-value-store-tutorial). + +## Storing Data in SQLite + +For more information about using SQLite from TinyGo, see [SQLite storage](sqlite-api-guide). + +## AI Inferencing From Go Components + +For more information about using Serverless AI from TinyGo, see the [Serverless AI](serverless-ai-api-guide) API guide. diff --git a/content/spin/v3/http-outbound.md b/content/spin/v3/http-outbound.md new file mode 100644 index 000000000..6e84169f8 --- /dev/null +++ b/content/spin/v3/http-outbound.md @@ -0,0 +1,239 @@ +title = "Making HTTP Requests" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/http-outbound.md" + +--- +- [Using HTTP From Applications](#using-http-from-applications) +- [Granting HTTP Permissions to Components](#granting-http-permissions-to-components) + - [Configuration-Based Permissions](#configuration-based-permissions) +- [Making HTTP Requests Within an Application](#making-http-requests-within-an-application) + - [Local Service Chaining](#local-service-chaining) + - [Intra-Application HTTP Requests by Route](#intra-application-http-requests-by-route) + +Spin provides an interface for you to make outgoing HTTP requests. + +{{ details "Why do I need a Spin interface? Why can't I just use my language's HTTP library?" "The current version of the WebAssembly System Interface (WASI) doesn't provide a sockets interface, so HTTP libraries can't be built to Wasm. The Spin interface means Wasm modules can bypass this limitation by asking Spin to perform the HTTP request on their behalf." }} + +## Using HTTP From Applications + +The outbound HTTP interface depends on your language. + +> Under the surface, Spin uses the `wasi-http` interface. If your tools support the Wasm Component Model, you can work with that directly; but for most languages the Spin SDK is more idiomatic. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/http/index.html) + +To send requests, use the [`spin_sdk::http::send`](https://docs.rs/spin-sdk/latest/spin_sdk/http/fn.send.html) function. This takes a request argument and returns a response (or error). It is `async`, so within an async inbound handler you can have multiple outbound `send`s running concurrently. + +> Support for streaming request and response bodies is **experimental**. We currently recommend that you stick with the simpler non-streaming interfaces if you don't require streaming. + +`send` is quite flexible in its request and response types. The request may be: + +* [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) - typically constructed via `http::Request::builder()` +* [`spin_sdk::http::Request`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.Request.html) - typically constructed via `spin_sdk::http::Request::get()`, `spin_sdk::http::Request::post()`, or `spin_sdk::http::Request::builder()` + * You can also use the [builder type](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.RequestBuilder.html) directly - `build` will be called automatically for you +* [`spin_sdk::http::OutgoingRequest`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.OutgoingRequest.html) - constructed via `OutgoingRequest::new()` +* Any type for which you have implemented the `TryInto` trait + +Generally, you should use `OutgoingRequest` when you need to stream the outbound request body; otherwise, the `Request` types are usually simpler. + +The response may be: + +* [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html) +* [`spin_sdk::http::Response`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.Response.html) +* [`spin_sdk::http::IncomingResponse`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.IncomingResponse.html) +* Any type for which you have implemented the [spin_sdk::http::conversions::TryFromIncomingResponse](https://docs.rs/spin-sdk/latest/spin_sdk/http/conversions/trait.TryFromIncomingResponse.html) trait + +Generally, you should use `IncomingResponse` when you need to stream the response body; otherwise, the `Response` types are usually simpler. + +Here is an example of doing outbound HTTP in a simple request-response style: + +```rust +use spin_sdk::{ + http::{IntoResponse, Request, Method, Response}, + http_component, +}; + +#[http_component] +// The trigger handler (in this case an HTTP handler) has to be async +// so we can `await` the outbound send. +async fn handle_request(_req: Request) -> anyhow::Result { + + // Create the outbound request object + let request = Request::builder() + .method(Method::Get) + .uri("https://www.fermyon.com/") + .build(); + + // Send the request and await the response + let response: Response = spin_sdk::http::send(request).await?; + + // Use the outbound response body + let response_len = response.body().len(); + + // Return the response to the inbound request + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(format!("The test page was {response_len} bytes")) + .build()) +} +``` + +For an example of receiving the response in a streaming style, [see this example in the Spin repository](https://github.com/fermyon/spin-rust-sdk/blob/main/examples/wasi-http-streaming-outgoing-body/src/lib.rs). + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +HTTP operations are available via the standard JavaScript `fetch` function. The Spin runtime maps this to the underlying Wasm interface. For example: + +```javascript +const response = await fetch("https://example.com/users"); +``` + +**Notes** + +You can find a complete example of using outbound HTTP in the JavaScript SDK repository on [GitHub](https://github.com/fermyon/spin-js-sdk/tree/main/examples/common-patterns/outbound-http) + +**Note**: `fetch` currently only works when building for the HTTP trigger. + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/http/index.html) + +HTTP functions and classes are available in the `http` module. The function name is [`send`](https://fermyon.github.io/spin-python-sdk/http/index.html#spin_sdk.http.send). The [request type](https://fermyon.github.io/spin-python-sdk/http/index.html#spin_sdk.http.Request) is `Request`, and the [response type](https://fermyon.github.io/spin-python-sdk/http/index.html#spin_sdk.http.Response) is `Response`. For example: + +```python +from spin_sdk.http import Request, Response, send +response = send(Request("GET", "https://random-data-api.fermyon.app/animals/json", {}, None)) +``` + +**Notes** + +* For compatibility with idiomatic Python, types do not necessarily match the underlying Wasm interface. For example, `method` is a string. +* Request and response bodies are `bytes`. (You can pass literal strings using the `b` prefix.) Pass `None` for no body. +* Request and response headers are dictionaries. +* Errors are signalled through exceptions. + +You can find a complete example for using outbound HTTP in the [Python SDK repository on GitHub](https://github.com/fermyon/spin-python-sdk/tree/main/examples/outgoing-request). + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/http) + +HTTP functions are available in the `github.com/fermyon/spin/sdk/go/v2/http` package. [See Go Packages for reference documentation.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2/http) The general function is named `Send`, but the Go SDK also surfaces individual functions, with request-specific parameters, for the `Get` and `Post` operations. For example: + +```go +import ( + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +res1, err1 := spinhttp.Get("https://random-data-api.fermyon.app/animals/json") +res2, err2 := spinhttp.Post("https://example.com/users", "application/json", json) + +request, err := http.NewRequest("PUT", "https://example.com/users/1", bytes.NewBufferString(user1)) +request.Header.Add("content-type", "application/json") +res3, err3 := spinhttp.Send(req) + +``` + +**Notes** + +* In the `Post` function, the body is an `io.Reader`. The Spin SDK reads this into the underlying Wasm byte array. +* The `NewRequest` function is part of the standard library. The `Send` method adapts the standard request type to the underlying Wasm interface. +* Errors are returned through the usual Go multiple return values mechanism. + +You can find a complete example for using outbound HTTP in the [Spin repository on GitHub](https://github.com/fermyon/spin-go-sdk/tree/main/examples/http-outbound). + +{{ blockEnd }} + +{{ blockEnd }} + +## Restrictions + +Spin applications cannot set the `Host` header on outbound requests. (This is a limitation of the Wasmtime runtime which underpins Spin.) + +## Granting HTTP Permissions to Components + +By default, Spin components are not allowed to make outgoing HTTP requests. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. To grant a component permission to make HTTP requests to a particular host, use the `allowed_outbound_hosts` field in the component manifest: + +```toml +[component.example] +allowed_outbound_hosts = [ "https://random-data-api.fermyon.app", "http://api.example.com:8080" ] +``` + +The Wasm module can make HTTP requests _only_ to the specified hosts. If a port is specified, the module can make requests only to that port; otherwise, the module can make requests only on the default port for the scheme. Requests to other hosts (or ports) will fail with an error. + +You can use a wildcard to allow requests to any subdomain of a domain: + +```toml +[component.example] +allowed_outbound_hosts = [ "https://*.example.com" ] +``` + +You can also pass an IPv4 CIDR address: + +```toml +[component.example] +allowed_outbound_hosts = [ "https://192.168.1.0/24" ] +``` + +For development-time convenience, you can also pass the string `"https://*:*"` in the `allowed_outbound_hosts` collection. This allows the Wasm module to make HTTP requests to _any_ host and on any port. However, once you've determined which hosts your code needs, you should remove this string and list the hosts instead. Other Spin implementations may restrict host access and disallow components that ask to connect to anything and everything! + +### Configuration-Based Permissions + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `allowed_outbound_hosts` field. However, this feature is not yet available on Fermyon Cloud. + +## Making HTTP Requests Within an Application + +If your Spin application functions as a set of microservices, you'll often want to make requests directly from one component to another within the same application. It's best not to use a full URL for this, because that's not portable across different deployment environments - the URL in the cloud is different from the one in your development environment. Instead, Spin provides two ways to make inter-component requests: + +* By component ID ("local service chaining") +* By route + +Both of these work only from HTTP components. That is, if you want to make an intra-application request from, say, a Redis trigger, you must use a full URL. + +### Local Service Chaining + +To make an HTTP request to another component in your application, use the special `.spin.internal` host name. For example, an outbound HTTP request to `authz.spin.internal` will be handled by the `authz` component. + +In this way of doing self-requests, the request is passed in memory without ever leaving the Spin host process. This is extremely fast, as the two components are wired almost directly together, but may reduce deployment flexibility depending on the nature of the microservices. Also, public components that are the target of service chaining requests may see URLs in both routed and chained forms: therefore, if they parse the URL (for example, extracting a resource identifier from the path), they must ensure both forms are correctly handled. + +> Service chaining is the only way to call [private endpoints](./http-trigger#private-endpoints). + +You must still grant permission by including the relevant `spin.internal` hosts in `allowed_outbound_hosts`: + +```toml +allowed_outbound_hosts = ["http://authz.spin.internal", "https://reporting.spin.internal"] +``` + +To allow local chaining to _any_ component in your application, use a subdomain wildcard: + +```toml +allowed_outbound_hosts = ["http://*.spin.internal"] +``` + +> Local service chaining is not currently supported on Fermyon Cloud. + +### Intra-Application HTTP Requests by Route + +To make an HTTP request to another route with your application, you can pass just the route as the URL. For example, if you make an outbound HTTP request to `/api/customers/`, Spin prepends the route with whatever host the application is running on. It also replaces the URL scheme (`http` or `https`) with the scheme of the current HTTP request. For example, if the application is running in the cloud, Spin changes `/api` to `https://.../api`. + +In this way of doing self-requests, the request undergoes normal HTTP processing once Spin has prepended the host. For example, in a cloud deployment, the request passes through the network, and potentially back in through a load balancer or other gateway. The benefit of this is that it allows load to be distributed across the environment, but it may count against your use of bandwidth. + +You must still grant permission by including `self` in `allowed_outbound_hosts`: + +```toml +allowed_outbound_hosts = ["http://self", "https://self"] +``` diff --git a/content/spin/v3/http-trigger.md b/content/spin/v3/http-trigger.md new file mode 100644 index 000000000..8487b366f --- /dev/null +++ b/content/spin/v3/http-trigger.md @@ -0,0 +1,513 @@ +title = "The Spin HTTP Trigger" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/http-trigger.md" + +--- +- [Specifying an HTTP Trigger](#specifying-an-http-trigger) +- [HTTP Trigger Routes](#http-trigger-routes) + - [Resolving Overlapping Routes](#resolving-overlapping-routes) + - [Private Endpoints](#private-endpoints) + - [Health Check Route](#health-check-route) +- [Authoring HTTP Components](#authoring-http-components) + - [The Request Handler](#the-request-handler) + - [Getting Request and Response Information](#getting-request-and-response-information) + - [Additional Request Information](#additional-request-information) + - [Inside HTTP Components](#inside-http-components) +- [HTTP With Wagi (WebAssembly Gateway Interface)](#http-with-wagi-webassembly-gateway-interface) + - [Wagi Component Requirements](#wagi-component-requirements) + - [Request Handling in Wagi](#request-handling-in-wagi) + - [Wagi HTTP Environment Variables](#wagi-http-environment-variables) +- [Exposing HTTP Triggers Using HTTPS](#exposing-http-triggers-using-https) + - [Trigger Options](#trigger-options) + - [Environment Variables](#environment-variables) + +HTTP applications are an important workload in event-driven environments, +and Spin has built-in support for creating and running HTTP +components. This page covers Spin options that are specific to HTTP. + +The HTTP trigger type in Spin is a web server. When an application has HTTP triggers, Spin listens for incoming requests and, +based on the [application manifest](./writing-apps.md), it routes them to a +component, which provides an HTTP response. + +## Specifying an HTTP Trigger + +An HTTP trigger maps an HTTP route to a component. For example: + +```toml +[[trigger.http]] +route = "/..." # the route that the trigger matches +component = "my-application" # the name of the component to handle this route +``` + +Such a trigger says that HTTP requests matching the specified _route_ should be handled by the specified _component_. The `component` field works the same way across all triggers - see [Triggers](triggers) for the details. + +> Earlier versions of Spin supported an application-wide base path; this is removed in Spin 3. + +## HTTP Trigger Routes + +An HTTP route may be _exact_ or _wildcard_. + +An _exact_ route matches only the given route. This is the default behavior. For example, `/cart` matches only `/cart`, and not `/cart/checkout`: + + + +```toml +# Run the `shopping-cart` component when the application receives a request to `/cart`... +[[trigger.http]] +route = "/cart" +component = "shopping-cart" + +# ...and the `checkout` component for `/cart/checkout` +[[trigger.http]] +route = "/cart/checkout" +component = "checkout" +``` + +You can use wildcards to match 'patterns' of routes. Spin supports two kinds of wildcards: single-segment wildcards and trailing wildcards. + +A single-segment wildcard uses the syntax `:name`, where `name` is a name that identifies the wildcard. Such a wildcard will match only a single segment of a path, and allows further matching on segments beyond it. For example, `/users/:userid/edit` matches `/users/1/edit` and `/users/alice/edit`, but does not match `/users`, `/users/1`, or `/users/1/edit/cart`. + +A trailing wildcard uses the syntax `/...` and matches the given route and any route under it. For example, `/users/...` matches `/users`, `/users/1`, `/users/1/edit`, and so on. Any of these routes will run the mapped component. + +> In particular, the route `/...` matches all routes. + +> Browser clients often `GET /favicon.ico` after a page request. If you use the `/...` route, consider handling this case in your code! + + + +```toml +[[trigger.http]] +# Run the `user-manager` component when the application receives a request to `/users` +# or any path beginning with `/users/` +route = "/users/..." +component = "user-manager" +``` + +### Resolving Overlapping Routes + +If multiple triggers could potentially handle the same request based on their +defined routes, the trigger whose route has the longest matching prefix +takes precedence. This also means that exact matches take precedence over wildcard matches. + +In the following example, requests starting with the `/users/` prefix (e.g. `/users/1`) +will be handled by `user-manager`, even though it is also matched by the `shop` route, because the `/users` prefix is longer than `/`. +But requests to `/users/admin` will be handled by the `admin` component, not `user-manager`, because that is a more exact match still: + + + +```toml +# spin.toml + +[[trigger.http]] +route = "/users/..." +component = "user-manager" + +[[trigger.http]] +route = "/users/admin" +component = "admin" + +[[trigger.http]] +route = "/..." +component = "shop" +``` + +### Private Endpoints + +Private endpoints are where an internal microservice is not exposed to the network (does not have an HTTP route) and so is accessible only from within the application. + + + +```toml +[[trigger.http]] +route = { private = true } +component = "internal" +``` + +To access a private endpoint, use [local service chaining](./http-outbound#local-service-chaining) (where the request is passed in memory without ever leaving the Spin host process). Such calls still require the internal endpoint to be included in `allowed_outbound_hosts`. + +### Health Check Route + +Every HTTP application automatically has a special route always configured at `/.well-known/spin/health`, which +returns `OK 200` when the Spin instance is healthy. + +## Authoring HTTP Components + +> Spin has two ways of running HTTP components, depending on language support for the evolving WebAssembly component standards. This section describes the default way, which is currently used by Rust, JavaScript/TypeScript, Python, and TinyGo components. For other languages, see [HTTP Components with Wagi](#http-with-wagi-webassembly-gateway-interface) below. + +By default, Spin runs components using the [WebAssembly component model](https://component-model.bytecodealliance.org/). In this model, the Wasm module exports a well-known interface that Spin calls to handle the HTTP request. + +### The Request Handler + +The exact signature of the HTTP handler, and how a function is identified to be exported as the handler, will depend on your language. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/http/index.html) + +In Rust, the handler is identified by the [`#[spin_sdk::http_component]`](https://docs.rs/spin-sdk/latest/spin_sdk/attr.http_component.html) attribute. The handler function can have one of two forms: _request-response_ or _input-output parameter_. + +**Request-Response Handlers** + +This form of handler function receives the request as an argument, and returns the response as the return value of the function. For example: + +```rust +#[http_component] +async fn handle(request: http::Request) -> anyhow::Result { ... } +``` + +In this form, nothing is sent to the client until the entire response is ready. It is convenient for many use cases, but is not suitable for streaming responses. + +> The Rust SDK includes **experimental** support for streaming request and response bodies. We currently recommend that you stick with the simpler non-streaming interfaces if you don't require streaming. + +You have some flexibility in choosing the types of the request and response. The request may be: + +* [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) +* [`spin_sdk::http::Request`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.Request.html) +* [`spin_sdk::http::IncomingRequest`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.IncomingRequest.html) +* Any type for which you have implemented the [`spin_sdk::http::conversions::TryFromIncomingRequest`](https://docs.rs/spin-sdk/latest/spin_sdk/http/conversions/trait.TryFromIncomingRequest.html) trait + +The response may be: + +* [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html) - typically constructed via `Response::builder()` +* [`spin_sdk::http::Response`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.Response.html) - typically constructed via a [`ResponseBuilder`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.ResponseBuilder.html) +* Any type for which you have implemented the [`spin_sdk::http::IntoResponse`](https://docs.rs/spin-sdk/latest/spin_sdk/http/trait.IntoResponse.html) trait +* A `Result` where the success type is one of the above and the error type is `anyhow::Error` or another error type for which you have implemented `spin_sdk::http::IntoResponse` (such as `anyhow::Result`) + +For example: + +```rust +use http::{Request, Response}; +use spin_sdk::http::IntoResponse; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +async fn handle_hello_rust(_req: Request<()>) -> anyhow::Result { + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Fermyon")?) +} +``` + +> If you're familiar with Spin 1.x, note that Spin 2 is more forgiving with the type in the `.body()` call. You don't need to convert it to bytes or wrap it in an `Option`. To return an empty body, you can pass `()` instead of `None`. + +To extract data from the request, specify a body type as the generic parameter for the `Request` type. You can use raw content types such as `Vec` and `String`, or automatically deserialize a JSON body by using the `spin_sdk::http::Json` type. + +**Input-Output Parameter Handlers** + +In this form, the handler function receives the request as an argument of type [`spin_sdk::http::IncomingRequest`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.IncomingRequest.html). It also receives an argument of type [`spin_sdk::http::ResponseOutparam`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.ResponseOutparam.html), through which is sends the response. The function does not return a value. This form is recommended for streaming responses. + +To send a response: + +1. Create a [`spin_sdk::http::OutgoingResponse`](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.OutgoingResponse.html). +2. Call `take_body()` on the `OutgoingResponse` - this gives you a [`futures::Sink`](https://docs.rs/futures/latest/futures/sink/trait.Sink.html) that you can later use to send data via the response. +3. Call `set` on the `ResponseOutparam`, passing the `OutgoingResponse`. +4. Call `send` on the `Sink` as many times as you like. Each send is carried out as you call it, so you can send the first part of the response without waiting for the whole response to be ready. + +> You will need to reference the `futures` crate in `Cargo.toml`, and `use futures::SinkExt;`, to access the `send` method. + +```rust +use futures::SinkExt; +use spin_sdk::http::{Headers, IncomingRequest, OutgoingResponse, ResponseOutparam}; +use spin_sdk::http_component; + +/// A streaming Spin HTTP component. +#[http_component] +async fn handle_hello_rust(_req: IncomingRequest, response_out: ResponseOutparam) { + // Status code and headers must be supplied before calling take_body + let response = OutgoingResponse::new( + 200, + &Headers::new(&[("content-type".to_string(), b"text/plain".to_vec())]), + ); + // Get the sink for writing the body into. This must be mutable! + let mut body = response.take_body(); + + // Connect the OutgoingResponse to the ResponseOutparam. + response_out.set(response); + + // Write to the body sink over a period of time. (In this case we simulate a + // long-running operation by manually calling `thread::sleep`.) + for i in 1..20 { + let payload = format!("Hello {i}\n"); + if let Err(e) = body.send(payload.into()).await { + eprintln!("Error sending payload: {e:#}"); + return; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} +``` + +For a full Rust SDK reference, see the [Rust Spin SDK documentation](https://docs.rs/spin-sdk/latest/spin_sdk/index.html). + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/) + +The user must a define a function named `handler` which the SDK attaches to the [`FetchEvent` listener](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent). Note that the incoming HTTP event is translated to a `FetchEvent`. + +The handler function takes in two arguments a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and a [ResponseBuilder](https://fermyon.github.io/spin-js-sdk/classes/ResponseBuilder.html) + +```ts +import { ResponseBuilder } from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + console.log(req); + res.send("hello universe"); +} +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/) + +In Python, the application must define a top-level class named IncomingHandler which inherits from [IncomingHandler](https://fermyon.github.io/spin-python-sdk/http/index.html#spin_sdk.http.IncomingHandler), overriding the `handle_request` method. + +```python +from spin_sdk import http +from spin_sdk.http import Request, Response + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + return Response( + 200, + {"content-type": "text/plain"}, + bytes("Hello from Python!", "utf-8") + ) +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/http) + +In Go, you register the handler as a callback in your program's `init` function. Call `spinhttp.Handle`, passing your handler as the sole argument. Your handler takes a `http.Request` record, from the standard `net/http` package, and a `ResponseWriter` to construct the response. + +> The do-nothing `main` function is required by TinyGo but is not used; the action happens in the `init` function and handler callback. + +```go +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "Hello Fermyon!") + }) +} + +func main() {} +``` + +> If you are moving between languages, note that in most other Spin SDKs, your handler _constructs and returns_ a response, but in Go, _Spin_ constructs a `ResponseWriter`, and you write to it; your handler does not return a value. + +{{ blockEnd }} + +{{ blockEnd }} + +### Getting Request and Response Information + +Exactly how the Spin SDK surfaces the request and response types varies from language to language; this section calls out general features. + +* In the request record, the URL contains the path and query, but not the scheme and host. For example, in a request to `https://example.com/shop/users/1/cart/items/3/edit?theme=pink`, the URL contains `/shop/users/1/cart/items/3/edit?theme=pink`. If you need the full URL, you can get it from the `spin-full-url` header - see the table below. + +### Additional Request Information + +As well as any headers passed by the client, Spin sets several headers on the request passed to your component, which you can use to access additional information about the HTTP request. + +> In the following table, the examples suppose that: +> * Spin is listening on `example.com:8080` +> * The trigger `route` is `/users/:userid/cart/...` +> * The request is to `https://example.com:8080/users/1/cart/items/3/edit?theme=pink` + +| Header Name | Value | Example | +|------------------------------|----------------------|---------| +| `spin-full-url` | The full URL of the request. This includes full host and scheme information. | `https://example.com:8080/users/1/cart/items/3/edit?theme=pink` | +| `spin-path-info` | The request path relative to the component route | `/items/3/edit` | +| `spin-path-match-n` | Where `n` is the pattern for our single-segment wildcard value (e.g. `spin-path-match-userid` will access the value in the URL that represents `:userid`) | `1` | +| `spin-matched-route` | The part of the trigger route that was matched by the route (including the wildcard indicator if present) | `/users/:userid/cart/...` | +| `spin-raw-component-route` | The component route pattern matched, including the wildcard indicator if present | `/users/:userid/cart/...` | +| `spin-component-route` | The component route pattern matched, _excluding_ any wildcard indicator | `/users/:userid/cart` | +| `spin-client-addr` | The IP address and port of the client | `127.0.0.1:53152` | + +### Inside HTTP Components + +For the most part, you'll build HTTP component modules using a language SDK (see the Language Guides section), such as a JavaScript module or a Rust crate. If you're interested in what happens inside the SDK, or want to implement HTTP components in another language, read on! + +The HTTP component interface is defined using a WebAssembly Interface (WIT) file. ([Learn more about the WIT language here.](https://component-model.bytecodealliance.org/design/wit.html)). You can find the latest WITs for Spin HTTP components at [https://github.com/fermyon/spin/tree/main/wit](https://github.com/fermyon/spin/tree/main/wit). + +The HTTP types and interfaces are defined in [https://github.com/fermyon/spin/tree/main/wit/deps/http](https://github.com/fermyon/spin/tree/main/wit/deps/http), which tracks [the `wasi-http` specification](https://github.com/WebAssembly/wasi-http). + +In particular, the entry point for Spin HTTP components is defined in [the `incoming-handler` interface](https://github.com/fermyon/spin/blob/main/wit/deps/http/handler.wit): + + + +```fsharp +// incoming-handler.wit + +interface incoming-handler { + use types.{incoming-request, response-outparam} + + handle: func( + request: incoming-request, + response-out: response-outparam + ) +} +``` + +This is the interface that all HTTP components must implement, and which is used by the Spin HTTP executor when instantiating and invoking the component. + +However, this is not necessarily the interface you, the component author, work with. It may not even be the interface of the component you build! + +In many cases, you will use a more idiomatic wrapper provided by the Spin SDK, which implements the "true" interface internally. In some cases, you will build a Wasm "core module" which implements an earlier version of the Spin HTTP interface, which Spin internally adapts to the "true" interface as it loads your module. + +But if you wish, and if your language supports it, you can implement the `incoming-handler` interface directly, using tools such as the +[Bytecode Alliance `wit-bindgen` project](https://github.com/bytecodealliance/wit-bindgen). Spin will happily load and run such a component. This is exactly how Spin SDKs, such as the [Rust](rust-components) SDK, are built; as component authoring tools roll out for Go, JavaScript, Python, and other languages, you'll be able to use those tools to build `wasi-http` handlers and therefore Spin HTTP components. + +> The WASI family of specifications, and tool support for some component model features that WASI depends on, are not yet fully stabilized. If you implement `wasi-http` directly, you may need to do some trialing to find tool versions which work together and with Spin. + +## HTTP With Wagi (WebAssembly Gateway Interface) + +A number of languages support WASI Preview 1 but not the component model. To enable developers to use these languages, Spin supports an +HTTP executor based on [Wagi](https://github.com/deislabs/wagi), or the +WebAssembly Gateway Interface, a project that implements the +[Common Gateway Interface](https://datatracker.ietf.org/doc/html/rfc3875) +specification for WebAssembly. + +Wagi allows a module built in any programming language that compiles to [WASI](https://wasi.dev/) +to handle an HTTP request by passing the HTTP request information to the module's +standard input, environment variables, and arguments, and expecting the HTTP +responses through the module's standard output. +This means that if a language has support for the WebAssembly System Interface, +it can be used to build Spin HTTP components. +The Wagi model is only used to parse the HTTP request and response. Everything +else — defining the application, running it, or [distributing](./distributing-apps.md) +is done the same way as a component that uses the Spin executor. + +### Wagi Component Requirements + +Spin uses the component model by default, and cannot detect from the Wasm module alone whether it was built with component model support. For Wagi components, therefore, you must tell Spin in the component manifest to run them using Wagi instead of 'default' Spin. To do this, use the `executor` field in the `trigger` table: + +```toml +[[trigger.http]] +route = "/" +component = "wagi-test" +executor = { type = "wagi" } +``` + +> If, for whatever reason, you want to highlight that a component uses the default Spin execution model, you can write `executor = { type = "spin" }`. But this is the default and is rarely written out. + +Wagi supports non-default entry points, and allows you to pass an arguments string that a program can receive as if it had been passed on the command line. If you need these you can specify them in the `executor` table. For details, see the [Manifest Reference](manifest-reference#the-componenttrigger-table-for-http-applications). + +### Request Handling in Wagi + +Building a Wagi component in a particular programming language that can compile +to `wasm32-wasi` does not require any special libraries — instead, +[building Wagi components](https://github.com/deislabs/wagi/tree/main/docs) can +be done by reading the HTTP request from the standard input and environment +variables, and sending the HTTP response to the module's standard output. + +In pseudo-code, this is the minimum required in a Wagi component: + +- either the `content-media` or `location` headers must be set — this is done by +printing its value to standard output +- an empty line between the headers and the body +- the response body printed to standard output: + + + +```text +print("content-type: text/html; charset=UTF-8\n\n"); +print("hello world\n"); +``` + +Here is a working example, written in [Grain](https://grain-lang.org/), +a programming language that natively targets WebAssembly and WASI but +does not yet support the component model: + +```js +import Process from "sys/process"; +import Array from "array"; + +print("content-type: text/plain\n"); + +// This will print all the Wagi env variables +print("==== Environment: ===="); +Array.forEach(print, Process.env()); + +// This will print the route path followed by each query +// param. So /foo?bar=baz will be ["/foo", "bar=baz"]. +print("==== Args: ===="); +Array.forEach(print, Process.argv()); +``` + +> You can find examples on how to build Wagi applications in +> [the DeisLabs GitHub organization](https://github.com/deislabs?q=wagi&type=public&language=&sort=). + +### Wagi HTTP Environment Variables + +Wagi passes request metadata to the program through well-known environment variables. The key path-related request variables are: + +- `X_FULL_URL` - the full URL of the request — + `http://localhost:3000/hello/abc/def?foo=bar` +- `PATH_INFO` - the path info, relative to the + component route — in our example, where the the + component route is `/hello`, this is `/abc/def`. +- `X_MATCHED_ROUTE` - the route pattern matched (including the + wildcard pattern, if applicable) — in our case `"/hello/..."`. +- `X_RAW_COMPONENT_ROUTE` - the route pattern matched (including the wildcard + pattern, if applicable) — in our case `/hello/...`. +- `X_COMPONENT_ROUTE` - the route path matched (stripped of the wildcard + pattern) — in our case `/hello` + +For details, and for a full list of all Wagi environment variables, see +[the Wagi documentation](https://github.com/deislabs/wagi/blob/main/docs/environment_variables.md). + +## Exposing HTTP Triggers Using HTTPS + +When exposing HTTP triggers using HTTPS you must provide `spin up` with a TLS certificate and a private key. This can be achieved by either using trigger options (`--tls-cert` and `--tls-key`) when running the `spin up` command, or by setting environment variables (`SPIN_TLS_CERT` and `SPIN_TLS_KEY`) before running the `spin up` command. + +### Trigger Options + +The `spin up` command's `--tls-cert` and `--tls-key` trigger options provide a way for you to specify both a TLS certificate and a private key (whilst running the `spin up` command). + +The `--tls-cert` option specifies the path to the TLS certificate to use for HTTPS, if this is not set, normal HTTP will be used. The certificate should be in PEM format. + +The `--tls-key` option specifies the path to the private key to use for HTTPS, if this is not set, normal HTTP will be used. The key should be in PKCS#8 format. + +### Environment Variables + +The `spin up` command can also automatically use the `SPIN_TLS_CERT` and `SPIN_TLS_KEY` environment variables instead of the respective flags (`--tls-cert` and `--tls-key`): + + + +```bash +SPIN_TLS_CERT= +SPIN_TLS_KEY= +``` + +Once set, `spin up` will automatically use these explicitly set environment variables. For example, if using a Linux-based system, you can go ahead and use the `export` command to set the variables in your session (before you run the `spin up` command): + + + +```bash +export SPIN_TLS_CERT= +export SPIN_TLS_KEY= +``` diff --git a/content/spin/v3/index.md b/content/spin/v3/index.md new file mode 100644 index 000000000..e11c00a51 --- /dev/null +++ b/content/spin/v3/index.md @@ -0,0 +1,31 @@ +title = "Introducing Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/index.md" + +--- + +{{suh_cards}} +{{card_element "sample" "Checklist Sample App" "A checklist app that persists data in a key value store" "https://developer.fermyon.com/hub/preview/sample_checklist" "Typescript,Http,Kv" true }} +{{card_element "template" "Zola SSG Template" "A template for using Zola framework to create a static webpage" "https://developer.fermyon.com/hub/preview/template_zola_ssg" "rust" true }} +{{card_element "sample" "AI-assisted News Summarizer" "Read an RSS newsfeed and have AI summarize it for you" "https://developer.fermyon.com/hub/preview/sample_newsreader_ai" "Typescript,Javascript,Ai" true }} +{{blockEnd}} + +Spin is a framework for building and running event-driven microservice applications with WebAssembly (Wasm) components. + +Spin uses Wasm because it is **sandboxed, portable, and fast**. Millisecond cold start times mean no need to keep applications "warm". + +Many languages have Wasm implementations, so **developers don't have to learn new languages or libraries**. + +Spin is **open source** and **built on standards**, meaning you can take your Spin applications anywhere. There are Spin implementations for local development, for self-hosted servers, for Kubernetes, and for cloud-hosted services. + +**Want to see the kinds of things people are building with Spin?** Check out what's [Built With Spin](./see-what-people-have-built-with-spin)! + +Or dive into the documentation and get started: + +- [Take Spin for a spin](quickstart) +- [Install Spin](install) +- [Learn how to write Spin applications](writing-apps) +- [Run your Spin applications in the Fermyon Cloud](/cloud/index) diff --git a/content/spin/v3/install.md b/content/spin/v3/install.md new file mode 100644 index 000000000..3bd0ca8a5 --- /dev/null +++ b/content/spin/v3/install.md @@ -0,0 +1,315 @@ +title = "Install Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/install.md" +keywords = "install" + +--- +- [Installing Spin](#installing-spin) +- [Verifying the Release Signature](#verifying-the-release-signature) +- [Building Spin From Source](#building-spin-from-source) +- [Using Cargo to Install Spin](#using-cargo-to-install-spin) +- [Installing Templates and Plugins](#installing-templates-and-plugins) + - [Templates](#templates) + - [Plugins](#plugins) +- [Next Steps](#next-steps) + +## Installing Spin + +Spin runs on Linux (amd64 and arm64), macOS (Intel and Apple Silicon), and Windows (amd64): + +{{ tabs "os" }} + +{{ startTab "Linux"}} + +**Homebrew** + +You can manage your Spin installation via [Homebrew](https://brew.sh/). Homebrew automatically installs Spin templates and Spin plugins, and on uninstall, will prompt you to delete the directory where the templates and plugins were downloaded: + +Install the Fermyon tap, which Homebrew tracks, updates, and installs Spin from: + + + +```bash +$ brew tap fermyon/tap +``` + +Install Spin: + + + +```bash +$ brew install fermyon/tap/spin +``` + +> Note: `brew install spin` will **not** install Fermyon's Spin framework. Fermyon Spin is accessed from the `fermyon` tap, as shown above. + +**Installer script** + +Another option (other than brew) is to use our installer script. The installer script installs Spin along with a starter set of language templates and plugins: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash +``` + +Once you have run the installer script, it is highly recommended to add Spin to a folder, which is on your path, e.g.: + + + +```bash +$ sudo mv spin /usr/local/bin/ +``` + +> If you have already installed Spin by building from source, and then install it via the installer, we recommend you remove the older source install by running `cargo uninstall spin-cli` Otherwise the Cargo path may take precedence over the "install from binary" path, and commands may get the "wrong" version of Spin. Use `spin --version` to confirm the version on the PATH is the one you intend, or `which spin` to confirm the path it is found on. + +To install a specific version (`v1.2.3` is just an example), you can pass arguments to the install script this way: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash -s -- -v v1.2.3 +``` + +To install the canary version of spin, you should pass the argument `-v canary`. The canary version is always the latest commit to the main branch of Spin: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash -s -- -v canary +``` + +{{ blockEnd }} + +{{ startTab "macOS"}} + +**Homebrew** + +You can manage your Spin installation via [Homebrew](https://brew.sh/). Homebrew automatically installs Spin templates and Spin plugins, and on uninstall, will prompt you to delete the directory where the templates and plugins were downloaded: + +Install the Fermyon tap, which Homebrew tracks, updates, and installs Spin from: + + + +```bash +$ brew tap fermyon/tap +``` + +Install Spin: + + + +```bash +$ brew install fermyon/tap/spin +``` + +> Note: `brew install spin` will **not** install Fermyon's Spin framework. Fermyon Spin is accessed from the `fermyon` tap, as shown above. + +**Installer script** + +Another option (other than brew) is to use our installer script. The installer script installs Spin along with a starter set of language templates and plugins: + +The installer script also installs Spin along with a starter set of language templates and plugins: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash +``` + +Once you have run the installer script, it is highly recommended to add Spin to a folder, which is on your path, e.g.: + + + +```bash +$ sudo mv spin /usr/local/bin/ +``` + +> If you have already installed Spin by building from source, and then install it via the installer, we recommend you remove the older source install by running `cargo uninstall spin-cli` Otherwise the Cargo path may take precedence over the "install from binary" path, and commands may get the "wrong" version of Spin. Use `spin --version` to confirm the version on the PATH is the one you intend, or `which spin` to confirm the path it is found on. + +To install a specific version (`v1.2.3` is just an example), you can pass arguments to the install script this way: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash -s -- -v v1.2.3 +``` + +To install the canary version of spin, you should pass the argument `-v canary`. The canary version is always the latest commit to the main branch of Spin: + + + +```bash +$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash -s -- -v canary +``` + +{{ blockEnd }} + +{{ startTab "Windows"}} + +If using Windows (PowerShell / cmd.exe), you can download the Windows binary release of Spin. + +Simply unzip the binary release and place the `spin.exe` in your system path. + +This does not install any Spin templates or plugins. For a starter list, see the [Installing Templates and Plugins section](#installing-templates-and-plugins). + +If you want to use WSL2 (Windows Subsystem for Linux 2), please follow the instructions for using Linux. + +{{ blockEnd }} +{{ blockEnd }} + +## Verifying the Release Signature + +The Spin project [signs releases](https://github.com/fermyon/spin/blob/main/docs/content/sips/012-signing-spin-releases.md) using [Sigstore](https://docs.sigstore.dev/), a project that helps with signing software and _stores signatures in a tamper-resistant public log_. Consumers of Spin releases can validate the integrity of the package they downloaded by performing a validation of the artifact against the signature present in the public log. Specifically, users get two main guarantees by verifying the signature: 1) that the author of the artifact is indeed the one expected (i.e. the build infrastructure associated with the Spin project, at a given revision that can be inspected), and 2) that the content generated by the build infrastructure has not been tampered with. + +To verify the release signature, first [configure Cosign v2.0.0+](https://docs.sigstore.dev/cosign/system_config/installation/). This is the CLI tool that we will use validate the signature. +The same directory where the installation script was run should also contain a signature of the Spin binary and the certificate used to perform the signature. The following command will perform the signature verification using the `cosign` CLI: + + + +```bash +$ cosign verify-blob \ + --signature spin.sig \ + --certificate crt.pem \ + --certificate-identity https://github.com/fermyon/spin/.github/workflows/release.yml@refs/tags/ \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + # --certificate-github-workflow-sha \ + ./spin +Verified OK +``` + +You can now move the Spin binary to the path knowing that it was indeed built by the infrastructure associated with the Spin project, and that it has not been tampered with since the build. + +## Building Spin From Source + +[Follow the contribution document](./contributing-spin.md) for a detailed guide on building Spin from source: + + + +```bash +$ git clone https://github.com/fermyon/spin +$ cd spin && make build +$ ./target/release/spin --help +``` + +> Please note: On a fresh Linux installation, you will also need the standard build toolchain (`gcc`, `make`, etc.), the SSL library headers, and on some distributions you may need `pkg-config`. For example, on Debian-like distributions, including Ubuntu, you can install the standard build toolchain with this command: + + + +```bash +$ sudo apt-get install build-essential libssl-dev pkg-config +``` + +This does not install any Spin templates or plugins. For a starter list, see the [Installing Templates and Plugins section](#installing-templates-and-plugins). + +## Using Cargo to Install Spin + +If you have [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html), you can clone the repo and install it to your path. + +> Note: The `main` branch may have unstable or breaking changes. It is advised to check out the latest Spin release tag, which can be found by navigating to https://github.com/fermyon/spin/releases/latest. + + + +```bash +$ git clone https://github.com/fermyon/spin +$ cd spin +$ # Check out the latest tagged release +$ # git checkout +$ rustup target add wasm32-wasi +$ rustup target add wasm32-unknown-unknown +$ cargo install --locked --path . +$ spin --help +``` + +> Please note: Installing Spin from source requires Rust 1.79 or newer. You can update Rust using the following command: + + + +```bash +$ rustup update +``` + +Installing Spin from source does not install any Spin templates or plugins. For a starter list, see the [Installing Templates and Plugins section](#installing-templates-and-plugins). + +## Installing Templates and Plugins + +Spin has a variety of templates and plugins to make it easier to create Spin applications in your favorite programming language. [The install script](install#installing-spin) automatically installs a starter set of templates and plugins, namely templates from the Spin repository and JavaScript and Python toolchain plugins and the Fermyon Cloud plugin. + +If you used a different installation method, we recommend you install these templates and plugins manually, as follows. + +### Templates + +Rust, Go and miscellaneous other languages: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --upgrade +``` + +Python: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --upgrade +``` + +JavaScript and TypeScript: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-js-sdk --upgrade +``` + +To list installed templates, run: + + + +```bash +$ spin templates list +``` + +For more information please read the [managing templates](./managing-templates) section of the documentation. + +### Plugins + +First update the local cache by running the `spin plugins update` command: + + + +```bash +$ spin plugins update +``` + +Then install plugins by name. + +Fermyon Cloud: + + + +```bash +$ spin plugins install cloud --yes +``` + +To list available plugins, run: + + + +```bash +$ spin plugins search +``` + +For more information, please visit the [managing plugins](./managing-plugins) section of the documentation. + +## Next Steps + +{{suh_cards}} +{{card_element "sample" "Checklist Sample App" "A checklist app that persists data in a key value store" "https://developer.fermyon.com/hub/preview/sample_checklist" "Typescript,Http,Kv" true }} +{{card_element "template" "Zola SSG Template" "A template for using Zola framework to create a static webpage" "https://developer.fermyon.com/hub/preview/template_zola_ssg" "rust" true }} +{{card_element "sample" "AI-assisted News Summarizer" "Read an RSS newsfeed and have AI summarize it for you" "https://developer.fermyon.com/hub/preview/sample_newsreader_ai" "Typescript,Javascript,Ai" true }} +{{blockEnd}} diff --git a/content/spin/v3/javascript-components.md b/content/spin/v3/javascript-components.md new file mode 100644 index 000000000..a48e95b6f --- /dev/null +++ b/content/spin/v3/javascript-components.md @@ -0,0 +1,385 @@ +title = "Building Spin Components in JavaScript" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/javascript-components.md" + +--- +- [Installing Templates](#installing-templates) +- [Structure of a JS/TS Component](#structure-of-a-jsts-component) +- [Building and Running the Template](#building-and-running-the-template) +- [HTTP Components](#http-components) +- [Sending Outbound HTTP Requests](#sending-outbound-http-requests) +- [Storing Data in Redis From JS/TS Components](#storing-data-in-redis-from-jsts-components) +- [Routing in a Component](#routing-in-a-component) +- [Storing Data in the Spin Key-Value Store](#storing-data-in-the-spin-key-value-store) +- [Storing Data in SQLite](#storing-data-in-sqlite) +- [Storing Data in MySQL and PostgreSQL Relational Databases](#storing-data-in-mysql-and-postgresql-relational-databases) +- [AI Inferencing From JS/TS Components](#ai-inferencing-from-jsts-components) +- [Node.js Compatibility](#nodejs-compatibility) +- [Using External NPM Libraries](#using-external-npm-libraries) + - [Suggested Libraries for Common Tasks](#suggested-libraries-for-common-tasks) +- [Caveats](#caveats) + +With JavaScript being a very popular language, Spin provides an SDK to support building components. The development of the JavaScript SDK is continually being worked on to improve user experience and add features. The SDK is based on [`ComponentizeJS`](https://github.com/bytecodealliance/ComponentizeJS). + +> This guide assumes you have Spin installed. If this is your first encounter with Spin, please see the [Quick Start](quickstart), which includes information about installing Spin with the JavaScript templates and creating JavaScript and TypeScript applications. + +> This guide assumes you are familiar with the JavaScript programming language, +> but if you are just getting started, be sure to check [the MDN guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide). + +> All examples from this page can be found in [the JavaScript SDK repository on GitHub](https://github.com/fermyon/spin-js-sdk/tree/main/examples). + +[**Want to go straight to the Spin SDK reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/) + +## Installing Templates + +The JavaScript/TypeScript templates can be installed from [spin-js-sdk repository](https://github.com/fermyon/spin-js-sdk/tree/main/) using the following command: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-js-sdk --update +``` + +which will install the `http-js` and `http-ts` templates: + + + +```text +Copying remote template source +Installing template http-ts... +Installing template http-js... +Installed 2 template(s) + ++-------------------------------------------------+ +| Name Description | ++=================================================+ +| http-js HTTP request handler using Javascript | +| http-ts HTTP request handler using Typescript | ++-------------------------------------------------+ +``` + +## Structure of a JS/TS Component + +A new JS/TS component can be created using the following command: + + + +```bash +$ spin new -t http-ts hello-world --accept-defaults +``` + +This creates a directory of the following structure: + + + +```text +hello-world/ +├── knitwit.json +├── package.json +├── spin.toml +├── src +│   ├── index.ts +│   └── spin.ts +├── tsconfig.json +└── webpack.config.js +``` + +The source for the component is present in `src/index.ts`. [Webpack](https://webpack.js.org) is used to bundle the component into a single `.js` file which will then be compiled to a `.wasm` module. + +{{ details "Going from JavaScript to Wasm" "The JS source is compiled to a `wasm` module using the `j2w` node executable provided by the `@fermyon/spin-sdk` which is a wrapper around `ComponentizeJS`. The `knitwit.json` is the configuration file used by [knitwit](https://github.com/fermyon/knitwit) to manage the WebAssembly dependencies of each package."}} + +## Building and Running the Template + +First, the dependencies for the template need to be installed using the following commands: + + + +```bash +$ cd hello-world +$ npm install +``` + +Next step is to use the `spin build` command to run the build script for the component. Once a Spin compatible module is created, it can be run using `spin up`: + + + +```bash +$ spin build +$ spin up +``` + +`spin build` will execute the command in the `command` key under the `[component..build]` section from `spin.toml` for each component in your application. In this case an `npm` script will be run. The command in the `package.json` will looks something like: + + + +```json +"scripts": { + "build": "npx webpack --mode=production && npx mkdirp target && npx j2w -i dist.js -d combined-wit -n combined -o target/hello-world.wasm", + "test": "echo \"Error: no test specified\" && exit 1" + } +``` + +--- + +## HTTP Components + +In Spin, HTTP components are triggered by the occurrence of an HTTP request, and +must return an HTTP response at the end of their execution. Components can be +built in any language that compiles to WASI, and Javascript/TypeScript has improved support +for writing Spin components with the Spin JS/TS SDK. + +> Make sure to read [the page describing the HTTP trigger](./http-trigger.md) for more +> details about building HTTP applications. + +Building a Spin HTTP component using the JS/TS SDK means writing a single function +that takes an HTTP request and a Response Builder which can be used to return an HTTP response as a parameter. + +Below is a complete implementation for such a component in TypeScript: + +```javascript +import { ResponseBuilder } from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + console.log(req); + res.send("hello universe"); +} +``` + +The important things to note in the implementation above: + +- The `handler` function is the entry point for the Spin component. +- The execution of the function terminates once `res.send` or `res.end` is called. + +## Sending Outbound HTTP Requests + +If allowed, Spin components can send outbound HTTP requests. +Let's see an example of a component that makes a request to [an API that returns random animal facts](https://random-data-api.fermyon.app/animals/json) + +```javascript +import { ResponseBuilder } from "@fermyon/spin-sdk"; + +interface AnimalFact { + timestamp: number; + fact: string; +} + +export async function handler(req: Request, res: ResponseBuilder) { + const animalFactResponse = await fetch("https://random-data-api.fermyon.app/animals/json") + const animalFact = await animalFactResponse.json() as AnimalFact + + const body = `Here's an animal fact: ${animalFact.fact}\n` + + res.set({"content-type": "text/plain"}) + res.send(body) +} +``` + +Before we can execute this component, we need to add the `random-data-api.fermyon.app` +domain to the application manifest `allowed_outbound_hosts` list containing the list of +domains the component is allowed to make HTTP requests to: + + + +```toml +spin_manifest_version = 2 + +[application] +authors = ["Your Name "] +description = "" +name = "hello-world" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "target/hello-world.wasm" +exclude_files = ["**/node_modules"] +allowed_outbound_hosts = ["https://random-data-api.fermyon.app"] +[component.hello-world.build] +command = "npm run build" +``` + +The component can be built using the `spin build` command. Running the application using `spin up` will start the HTTP listener locally (by default on `localhost:3000`): + + + +```text +$ curl -i localhost:3000 +HTTP/1.1 200 OK +date = "2023-11-04T00:00:01Z" +content-type: application/json; charset=utf-8 +content-length: 185 +server: spin/0.1.0 + +Here's an animal fact: Reindeer grow new antlers every year +``` + +> Without the `allowed_outbound_hosts` field populated properly in `spin.toml`, +> the component would not be allowed to send HTTP requests, and sending the +> request would result in a "Destination not allowed" error. + +> You can set `allowed_outbound_hosts = ["https://*:*"]` if you want to allow +> the component to make requests to any HTTP host. This is not recommended +> unless you have a specific need to contact arbitrary servers and perform your own safety checks. + +We just built a WebAssembly component that sends an HTTP request to another +service, manipulates that result, then responds to the original request. +This can be the basis for building components that communicate with external +databases or storage accounts, or even more specialized components like HTTP +proxies or URL shorteners. + +--- + +## Storing Data in Redis From JS/TS Components + +> You can find a complete example for using outbound Redis from an HTTP component +> in the [spin-js-sdk repository on GitHub](https://github.com/fermyon/spin-js-sdk/blob/main/examples/spin-host-apis/spin-redis). + +Using the Spin's JS SDK, you can use the Redis key/value store and to publish messages to Redis channels. + +Let's see how we can use the JS/TS SDK to connect to Redis: + +```javascript +import { ResponseBuilder, Redis } from '@fermyon/spin-sdk'; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const redisAddress = 'redis://localhost:6379/'; + +export async function handler(_req: Request, res: ResponseBuilder) { + try { + let db = Redis.open(redisAddress); + db.set('test', encoder.encode('Hello world')); + let val = db.get('test'); + + if (!val) { + res.status(404); + res.send(); + return; + } + // publish to a channel names "message" + db.publish("message", val) + res.send(val); + } catch (e: any) { + res.status(500); + res.send(`Error: ${JSON.stringify(e.payload)}`); + } +} +``` + +This HTTP component demonstrates fetching a value from Redis by key, setting a key with a value, and publishing a message to a Redis channel. + +> When using Redis databases hosted on the internet (i.e) not on localhost, the `redisAddress` must be of the format "redis://\:\@\" (e.g) `redis://myUsername:myPassword@redis-database.com` + +As with all networking APIs, you must grant access to Redis hosts via the `allowed_outbound_hosts` field in the application manifest: + + + +```toml +[component.storage-demo] +allowed_outbound_hosts = ["redis://localhost:6379"] +``` + +## Routing in a Component + +The JavaScript/TypeScript SDK provides a router that makes it easier to handle routing within a component. The router is based on [`itty-router`](https://www.npmjs.com/package/itty-router). An additional function `handleRequest` has been implemented in the router to allow passing in the Spin HTTP request directly. An example usage of the router is given below: + +```javascript +import { ResponseBuilder, Router } from '@fermyon/spin-sdk'; + +let router = Router(); + +router.get("/", (_, req, res) => { handleDefaultRoute(req, res) }) +router.get("/home/:id", (metadata, req, res) => { handleHomeRoute(req, res, metadata.params.id) }) + +async function handleDefaultRoute(_req: Request, res: ResponseBuilder) { + res.set({ "content-type": "text/plain" }); + res.send("Hello from default route"); +} + +async function handleHomeRoute(_req: Request, res: ResponseBuilder, id: string) { + res.set({ "content-type": "text/plain" }); + res.send(`Hello from home route with id: ${id}`); +} + +export async function handler(req: Request, res: ResponseBuilder) { + await router.handleRequest(req, res); +} +``` + +## Storing Data in the Spin Key-Value Store + +Spin has a key-value store built in. For information about using it from TypeScript/JavaScript, see [the key-value store tutorial](key-value-store-tutorial). + +## Storing Data in SQLite + +For more information about using SQLite from TypeScript/Javascript, see [SQLite storage](sqlite-api-guide). + +## Storing Data in MySQL and PostgreSQL Relational Databases + +For more information about using relational databases from TypeScript/JavaScript, see [Relational Databases](rdbms-storage). + +## AI Inferencing From JS/TS Components + +For more information about using Serverless AI from JS/TS, see the [Serverless AI](serverless-ai-api-guide) API guide. + +## Node.js Compatibility + +The SDK does not support the full specification of `Node.js`. A limited set of APIs can be polyfilled using the [`@fermyon/wasi-ext`](https://github.com/fermyon/js-wasi-ext) library which provides a webpack plugin. It can be used by installing the library first using: + + + +```bash +$ npm install @fermyon/wasi-ext +``` + +Once installed, the plugin provided by it can be added to the webpack config: + +```js +const WasiExtPlugin = require("wasi-ext/plugin") + +module.exports = { + ... + plugins: [ + new WasiExtPlugin() + ], + ... +}; +``` + +This library only currently supports the following polyfills: + +- `Node.js` buffers +- `process` - certain methods are no-ops and few throw exceptions. For detailed list refer to the [upstream library](https://github.com/defunctzombie/node-process/blob/master/browser.js). **Note:** `process.env` is populated only inside the handler and returns an empty object outside the handler. +- `fs` - only implements `readFileSync` and `readdirSync`. +- `os` - Implements only `EOL` and `arch`. + + +## Using External NPM Libraries + +> Not all the NPM packages are guaranteed to work with the SDK as it is not fully compatible with the browser or `Node.js`. It implements only a subset of the API. + +Some NPM packages can be installed and used in the component. If a popular library does not work, please open an issue/feature request in the [spin-js-sdk repository](https://github.com/fermyon/spin-js-sdk/issues). + + +### Suggested Libraries for Common Tasks + +These are some of the suggested libraries that have been tested and confirmed to work with the SDK for common tasks. + +{{ details "HTML parsers" "- [node-html-parser](https://www.npmjs.com/package/node-html-parser)" }} + +{{ details "Parsing formdata" "- [parse-multipart-data](https://www.npmjs.com/package/parse-multipart-data)" }} + +{{ details "Runtime schema validation" "- [zod](https://www.npmjs.com/package/zod)" }} + +{{ details "Unique ID generator" "- [nanoid](https://www.npmjs.com/package/nanoid)\n- [ulidx](https://www.npmjs.com/package/ulidx)\n- [uuid](https://www.npmjs.com/package/uuid)" }} + +## Caveats + +- All `spin-sdk` related functions and methods (like `Variables`, `Redis`, `Mysql`, `Pg`, `Kv` and `Sqlite`) can be called only inside the `handler` function. This includes `fetch`. Any attempts to use it outside the function will lead to an error. This is due to Wizer using only Wasmtime to execute the script at build time, which does not include any Spin SDK support. +- No crypto operation that involve handling private keys are supported. diff --git a/content/spin/v3/key-value-store-tutorial.md b/content/spin/v3/key-value-store-tutorial.md new file mode 100644 index 000000000..060d3658d --- /dev/null +++ b/content/spin/v3/key-value-store-tutorial.md @@ -0,0 +1,524 @@ +title = "Spin Key-Value Store" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/key-value-store-tutorial.md" + +--- +- [Key Value Store With Spin Applications](#key-value-store-with-spin-applications) +- [Tutorial Prerequisites](#tutorial-prerequisites) + - [Python](#python) +- [Creating a New Spin Application](#creating-a-new-spin-application) +- [Configuration](#configuration) + - [The Spin TOML File](#the-spin-toml-file) +- [Write Code to Save and Load Data](#write-code-to-save-and-load-data) + - [The Spin SDK Version](#the-spin-sdk-version) + - [Source Code](#source-code) +- [Building and Running Your Spin Application](#building-and-running-your-spin-application) +- [Storing and Retrieving Data From Your Default Key/Value Store](#storing-and-retrieving-data-from-your-default-keyvalue-store) +- [(Optional) Deploy Your App To Fermyon Cloud](#optional-deploy-your-app-to-fermyon-cloud) +- [Next Steps](#next-steps) + +## Key Value Store With Spin Applications + +Spin applications are best suited for event-driven, stateless workloads that have low-latency requirements. Keeping track of the application's state (storing information) is an integral part of any useful product or service. For example, users (and the business) will expect to store and load data/information at all times during an application’s execution. Spin has support for applications that need data in the form of key/value pairs and are satisfied by a Basically Available, Soft State, and Eventually Consistent (BASE) model. Workload examples include general value caching, session caching, counters, and serialized application state. In this tutorial, you will learn how to do the following: + +* Create a Spin application with `spin new` +* Use the key value store SDK to get, set, and list key value pairs +* Configure your application manifest (`spin.toml`) to use the default key value store +* Run your key value store Spin application locally with `spin up` + +## Tutorial Prerequisites + +First, follow [this guide](./install.md) to install Spin. To ensure you have the correct version, you can check with this command: + + + +```bash +$ spin --version +``` + +> Please ensure you're on Spin version 2.0 or newer. + +### Python + +If you are planning on using Python for this tutorial, please ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + +## Creating a New Spin Application + +Let's create a Spin application that will send and retrieve data from a key value store. To make things easy, we'll start from a template using the following commands ([learn more](./quickstart#creating-a-new-spin-application-from-a-template)): + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + + + +```bash +$ spin new -t http-rust spin-key-value + +# Reference: https://github.com/fermyon/spin-rust-sdk/tree/stable/examples/rust-key-value +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + + + +```bash +$ spin new -t http-ts spin-key-value + +# Reference: https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-kv +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + + + +```bash +$ spin new -t http-py spin-key-value + +# Reference: https://github.com/fermyon/spin-python-sdk/tree/main/examples/spin-kv +``` + +{{ blockEnd }} + +{{ startTab "TinyGo" }} + + + +```bash +$ spin new -t http-go spin-key-value + +# Reference: https://github.com/fermyon/spin-go-sdk/tree/stable/examples/key-value +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Configuration + +Good news - Spin will take care of setting up your Key Value store. However, in order to make sure your Spin application has permission to access the Key Value store, you must add the `key_value_stores = ["default"]` line in the `[component.]` section of the `spin.toml` file, for each component which needs access to the Key Value store. This line is necessary to communicate to Spin that a given component has access to the default Key Value store. A newly scaffolded Spin application will not have this line; you will need to add it. + +> Note: `[component.spin_key_value]` contains the name of the component. If you used a different name, when creating the application, this sections name would be different. + +```toml +[component.spin_key_value] +... +key_value_stores = ["default"] +... +``` + +>> Tip: You can choose between various store implementations by modifying [the runtime configuration](dynamic-configuration.md#key-value-store-runtime-configuration). The default implementation uses [SQLite](https://www.sqlite.org/index.html) within the Spin framework. + +Each Spin application's `key_value_stores` instances are implemented on a per-component basis across the entire Spin application. This means that within a multi-component Spin application (which has the same `key_value_stores = ["default"]` configuration line), each component will access that same data store. If one of your application's components creates a new key/value pair, another one of your application's components can update/overwrite that initial key/value after the fact. + +### The Spin TOML File + +We will give our components access to the key value store by adding the `key_value_stores = ["default"]` in the `[component.] section as shown below: + +```toml +spin_manifest_version = 2 + +[application] +name = "spin-key-value" +version = "0.1.0" +authors = ["Your Name "] +description = "A simple application that exercises key-value storage." + +[[trigger.http]] +route = "/..." +component = "spin-key-value" + +[component.spin-key-value] +... +key_value_stores = ["default"] +... +``` + + +## Write Code to Save and Load Data + +In this section, we use the Spin SDK to open and persist our application's data inside our default key/value store. This is a special store that every environment running Spin applications will make available for their application. + +### The Spin SDK Version + +If you have an existing application and would like to try out the key/value feature, please check the Spin SDK reference in your existing application's configuration. It is highly recommended to upgrade Spin and the SDK versions to the latest version available. + +### Source Code + +Now let's use the Spin SDK to: +- add new data +- check that the new data exists +- retrieve that data +- delete data +- check the data has been removed + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +```rust +use spin_sdk::{ + http::{IntoResponse, Request, Response, Method}, + http_component, + key_value::Store, +}; + +#[http_component] +fn handle_request(req: Request) -> anyhow::Result { + // Open the default key-value store + let store = Store::open_default()?; + + let (status, body) = match *req.method() { + Method::Post => { + // Add the request (URI, body) tuple to the store + store.set(req.path(), req.body())?; + println!( + "Storing value in the KV store with {:?} as the key", + req.path() + ); + (200, None) + } + Method::Get => { + // Get the value associated with the request URI, or return a 404 if it's not present + match store.get(req.path())? { + Some(value) => { + println!("Found value for the key {:?}", req.path()); + (200, Some(value)) + } + None => { + println!("No value found for the key {:?}", req.path()); + (404, None) + } + } + } + Method::Delete => { + // Delete the value associated with the request URI, if present + store.delete(req.path())?; + println!("Delete key {:?}", req.path()); + (200, None) + } + Method::Head => { + // Like GET, except do not return the value + let code = if store.exists(req.path())? { + println!("{:?} key found", req.path()); + 200 + } else { + println!("{:?} key not found", req.path()); + 404 + }; + (code, None) + } + // No other methods are currently supported + _ => (405, None), + }; + Ok(Response::new(status, body)) +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +```typescript +import { ResponseBuilder, Kv } from "@fermyon/spin-sdk"; + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +export async function handler(req: Request, res: ResponseBuilder) { + + let store = Kv.openDefault() + let status = 200 + let body + + switch (req.method) { + case "POST": + store.set(req.uri, await req.text() || (new Uint8Array()).buffer) + console.log(`Storing value in the KV store with ${req.uri} as the key`); + break; + case "GET": + let val + try { + val = store.get(req.uri) + body = decoder.decode(val) + console.log(`Found value for the key ${req.uri}`); + } catch (error) { + console.log(`Key ${req.uri} not found`); + status = 404 + } + break; + case "DELETE": + store.delete(req.uri) + console.log(`Deleted Key ${req.uri}`); + break; + case "HEAD": + if (!store.exists(req.uri)) { + console.log(`Key ${req.uri} not found`); + status = 404 + } else { + console.log(`Found Key ${req.uri}`); + } + break; + default: + } + res.status(status) + res.send(body) +} +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + +```python +from spin_sdk import http, key_value +from spin_sdk.http import Request, Response + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + with key_value.open_default() as store: + match request.method: + case "GET": + value = store.get(request.uri) + if value: + status = 200 + print(f"Found key {request.uri}") + else: + status = 404 + print(f"Key {request.uri} not found") + return Response( status, {"content-type": "text/plain"}, value) + case "POST": + store.set(request.uri, request.body) + print(f"Stored key {request.uri}") + return Response(200, {"content-type": "text/plain"}) + case "DELETE": + store.delete(request.uri) + print(f"Deleted key {request.uri}") + return Response(200, {"content-type": "text/plain"}) + case "HEAD": + if store.exists(request.uri): + print(f"Found key {request.uri}") + return Response(200, {"content-type": "text/plain"}) + print(f"Key not found {request.uri}") + return Response(404, {"content-type": "text/plain"}) + case default: + return Response(405, {"content-type": "text/plain"}) + +{{ blockEnd }} + + +{{ startTab "TinyGo" }} + +```go +package main + +import ( + "io" + "net/http" + "fmt" + + spin_http "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/kv" +) + +func init() { + // handler for the http trigger + spin_http.Handle(func(w http.ResponseWriter, r *http.Request) { + store, err := kv.OpenStore("default") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer store.Close() + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch r.Method { + case http.MethodPost: + err := store.Set(r.URL.Path, body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Println("Stored the key at:", r.URL.Path) + w.WriteHeader(http.StatusOK) + case http.MethodGet: + value, err := store.Get(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Println("Got the key:", r.URL.Path) + w.WriteHeader(http.StatusOK) + w.Write(value) + case http.MethodDelete: + err := store.Delete(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Println("Deleted the key:", r.URL.Path) + w.WriteHeader(http.StatusOK) + case http.MethodHead: + exists, err := store.Exists(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if exists { + w.WriteHeader(http.StatusOK) + fmt.Println("Found key:", r.URL.Path) + return + } + + fmt.Println("Didn't find the key:", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } + }) +} + +func main() {} + +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Building and Running Your Spin Application + +Now, let's build and run our Spin Application locally. Run the following command to build your application: + + + +```bash +$ spin build +$ spin up +``` + +> If you ever receive the error `Handler returned an error: Error::AccessDenied`, please make sure you've included a list of allowed `key_value_stores` in your `spin.toml` file (as shown above in the [configuration](#configuration) section). + +## Storing and Retrieving Data From Your Default Key/Value Store + +Once you have completed this minimal configuration and deployed your application, data will be persisted across requests. Let's begin by creating a `POST` request that stores a JSON key/value object: + + + +```bash +# Create a new POST request and set the key/value pair of foo:bar +$ curl localhost:3000/test -H 'Content-Type: application/json' -d '{"foo":"bar"}' +``` + +We can now use a `HEAD` request to confirm that our component is holding data for us. Essentially, all we want to see here is a `200 OK` response when calling our components endpoint (`/test`). Let's give it a try: + + + +```bash +$ curl -I localhost:3000/test + +HTTP/1.1 200 OK +``` + +Perfect, `200 OK`. Now, let's create a `GET` request that fetches the data from our component: + + + +```bash +# Create a GET request and fetch the key/value that we stored in the previous request +$ curl localhost:3000/test + +{"foo": "bar"} +``` + +Great! The above command successfully returned our data as intended. + +Lastly, we show how to create a `DELETE` request that removes the data for this specific component altogether: + + + +```bash +$ curl -X DELETE localhost:3000/test +``` + +Note how all of the above commands returned `200 OK` responses. In these examples, we were able to `POST`, `HEAD` (check to see if data exists), `GET` and also `DELETE` data from our component. + +Interestingly there is one more request we can re-run before wrapping up this tutorial. If no data exists in the component's endpoint of `/test` (which is technically the case now that we have sent the `DELETE` request) the `HEAD` request should correctly return `404 Not Found`. You can consider this a type of litmus test; let's try it out: + + + +```bash +$ curl -I localhost:3000/test + +HTTP/1.1 404 Not Found +``` + +As we can see above, there is currently no data found at the `/test` endpoint of our application. + +## (Optional) Deploy Your App To Fermyon Cloud + +Optionally, if you'd like to deploy your application and key value store to Fermyon Cloud here are the required steps. + +First, login to your Fermyon Cloud account. If you do not have one already, this will take you through the signup process for a free account. + + + +```bash +$ spin login + +Copy your one-time code: + +XXXXXXXX + +...and open the authorization page in your browser: + +https://cloud.fermyon.com/device-authorization + +Waiting for device authorization... +Device authorized! +``` + +Now that we have our dependencies squared away, let's deploy our application to Fermyon Cloud. From your application root folder, run the following command and opt to allow Fermyon Cloud to create a key value store on your behalf: + + + +```bash +$ spin cloud deploy +spin deploy +Uploading spin-key-value version 0.1.0-r234fe5a4 to Fermyon Cloud... +Deploying... +App "spin-key-value" accesses a key value store labeled "default" +Would you like to link an existing key value store or create a new key value store?: +> Use an existing key value store and link app to it + Create a new key value store and link the app to it +``` + +>> If you're interested in learning more about how to link your Spin app to different key value store instances on Fermyon Cloud, check out our [Key Value Links and Labels tutorial](../../cloud/linking-applications-to-resources-using-labels.md). + +Congratulations, you have a Spin application and associated Key Value store running up in Fermyon Cloud! You can visit it by clicking on the Spin application's domain name generated in the CLI output, which has the following pattern: `spin-key-value-.fermyon.app`. + +## Next Steps + +* Explore the contents of your Key Value store with the [Key Value Store Explorer template](../../hub/preview/template_kv_explorer) +* Learn about linking your applications to different [Key Value Stores on Fermyon Cloud](../../cloud/kv-cloud-tutorial.md) + diff --git a/content/spin/v3/kubernetes.md b/content/spin/v3/kubernetes.md new file mode 100644 index 000000000..1e64ec57f --- /dev/null +++ b/content/spin/v3/kubernetes.md @@ -0,0 +1,18 @@ +title = "Spin on Kubernetes" +template = "spin_main" +date = "2024-03-07T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/kubernetes.md" + +--- + +## Why Use Spin With Kubernetes? + +In addition to `spin up` Fermyon also offers Fermyon Cloud to deploy spin apps into production, so why use Spin with Kubernetes? For users that have made existing investments into Kubernetes or have requirements that their applications stay within certain clouds, not be on shared infrastructure, or run on-premise, Kubernetes provides a robust solution. + +There are a few options for running Spin on Kubernetes: + +* **[SpinKube](https://spinkube.dev)** is an open source project that provides the best Kubernetes-native experience for running Spin applications on Kubernetes. From runtime installation via `runtime-class-manager` to resource management via Spin Operator, SpinKube provides you with a complete toolkit for running spin applications on Kubernetes as a custom resource (known as SpinApps). For guidance on how to get started with SpinKube, please visit the [SpinKube documentation](https://spinkube.dev). + +* **Fermyon Platform for Kubernetes** is a managed distribution of SpinKube, currently in private beta, that can be run on your existing Kubernetes infrastructure for enhanced performance and density for your SpinApps. If this is of interest to your team, please [schedule a demo](https://www.fermyon.com/demo). diff --git a/content/spin/v3/kv-store-api-guide.md b/content/spin/v3/kv-store-api-guide.md new file mode 100644 index 000000000..6f4d47e96 --- /dev/null +++ b/content/spin/v3/kv-store-api-guide.md @@ -0,0 +1,188 @@ +title = "Key Value Store" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/kv-store-api-guide.md" + +--- +- [Using Key Value Store From Applications](#using-key-value-store-from-applications) +- [Custom Key Value Stores](#custom-key-value-stores) +- [Granting Key Value Store Permissions to Components](#granting-key-value-store-permissions-to-components) + +Spin provides an interface for you to persist data in a key value store managed by Spin. This key value store allows Spin developers to persist non-relational data across application invocations. To learn more about key value store use cases and how to enable your Spin application to use a key value store, check out our [key value tutorial](./key-value-store-tutorial.md). + +{{ details "Why do I need a Spin interface? Why can't I just use my own external store?" "You can absolutely still use your own external store either with the Redis or Postgres APIs, or outbound HTTP. However, if you're interested in quick, non-relational local storage without any infrastructure set-up then Spin's key value store is a great option." }} + +## Using Key Value Store From Applications + +The Spin SDK surfaces the Spin key value store interface to your language. The following characteristics are true of keys and values: + +* Keys as large as 256 bytes (UTF-8 encoded) +* Values as large as 1 megabyte +* Capacity for 1024 key value tuples + +The set of operations is common across all SDKs: + +| Operation | Parameters | Returns | Behavior | +|------------|------------|---------|----------| +| `open` | name | store | Open the store with the specified name. If `name` is the string "default", the default store is opened, provided that the component that was granted access in the component manifest from `spin.toml`. Otherwise, `name` must refer to a store defined and configured in a [runtime configuration file](./dynamic-configuration.md#key-value-store-runtime-configuration) supplied with the application.| +| `get` | store, key | value | Get the value associated with the specified `key` from the specified `store`. | +| `set` | store, key, value | - | Set the `value` associated with the specified `key` in the specified `store`, overwriting any existing value. | +| `delete` | store, key | - | Delete the tuple with the specified `key` from the specified `store`. `error::invalid-store` will be raised if `store` is not a valid handle to an open store. No error is raised if a tuple did not previously exist for `key`.| +| `exists` | store, key | boolean | Return whether a tuple exists for the specified `key` in the specified `store`.| +| `get-keys` | store | list | Return a list of all the keys in the specified `store`. | +| `close` | store | - | Close the specified `store`. | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/key_value/index.html) + +Key value functions are available in the `spin_sdk::key_value` module. The function names match the operations above. For example: + +```rust +use anyhow::Result; +use spin_sdk::{ + http::{IntoResponse, Request, Response}, + http_component, + key_value::{Store}, +}; +#[http_component] +fn handle_request(_req: Request) -> Result { + let store = Store::open_default()?; + store.set("mykey", b"myvalue")?; + let value = store.get("mykey")?; + let response = value.unwrap_or_else(|| "not found".into()); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(response) + .build()) +} +``` + +**General Notes** + +`set` **Operation** +- For set, the value argument can be of any type that implements `AsRef<[u8]>` + +`get` **Operation** +- For get, the return value is of type `Option>`. If the key does not exist it returns `None`. + +`open` and `close` **Operations** +- The close operation is not surfaced; it is called automatically when the store is dropped. + +`set_json` and `get_json` **Operation** +- Rust applications can [store and retrieve serializable Rust types](./rust-components#storing-data-in-the-spin-key-value-store). + +{{ blockEnd }} + +{{ startTab "Typescript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/) + +The key value functions can be accessed after opening a store using either [the `Kv.open` or the `Kv.openDefault` methods](https://fermyon.github.io/spin-js-sdk/modules/Kv.html) which returns a [handle to the store](https://fermyon.github.io/spin-js-sdk/interfaces/Kv.Store.html). For example: + +```ts +import { ResponseBuilder , Kv} from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + let store = Kv.openDefault() + store.set("mykey", "myvalue") + res.status(200) + res.set({"content-type":"text/plain"}) + res.send(store.get("mykey") ?? "Key not found") +} +``` + +**General Notes** +- The SDK doesn't surface the `close` operation. It automatically closes all stores at the end of the request; there's no way to close them early. + +[`get` **Operation**](https://fermyon.github.io/spin-js-sdk/interfaces/Kv.Store.html#get) +- The result is of the type `Uint8Array | null` +- If the key does not exist, `get` returns `null` + +[`set` **Operation**](https://fermyon.github.io/spin-js-sdk/interfaces/Kv.Store.html#set) +- The value argument is of the type `Uint8Array | string | object`. + +[`setJson`](https://fermyon.github.io/spin-js-sdk/interfaces/Kv.Store.html#setJson) and [`getJson` **Operation**](https://fermyon.github.io/spin-js-sdk/interfaces/Kv.Store.html#getJson) +- Applications can store JavaScript objects using `setJson`; these are serialized within the store as JSON. These serialized objects can be retrieved and deserialized using `getJson`. If you call `getJson` on a key that doesn't exist then it returns an empty object. + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/key_value.html) + +The key value functions are provided through the `spin_key_value` module in the Python SDK. For example: + +```python +from spin_sdk import http, key_value +from spin_sdk.http import Request, Response + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + with key_value.open_default() as store: + store.set("test", bytes("hello world!", "utf-8")) + val = store.get("test") + + return Response( + 200, + {"content-type": "text/plain"}, + val + ) + +``` + +**General Notes** +- The Python SDK doesn't surface the `close` operation. It automatically closes all stores at the end of the request; there's no way to close them early. + +[`get` **Operation**](https://fermyon.github.io/spin-python-sdk/wit/imports/key_value.html#spin_sdk.wit.imports.key_value.Store.get) +- If a key does not exist, it returns `None` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the Spin SDK reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/kv) + +Key value functions are provided by the `github.com/fermyon/spin/sdk/go/v2/kv` module. [See Go Packages for reference documentation.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2/kv) For example: + +```go +import "github.com/fermyon/spin/sdk/go/v2/kv" + +func example() error { + store, err := kv.OpenStore("default") + if err != nil { + return err + } + defer store.Close() + previous, err := store.Get("mykey") + return store.Set("mykey", []byte("myvalue")) +} + +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Custom Key Value Stores + +Spin defines a key-value store named `"default"` and provides automatic backing storage. If you need to customize Spin with additional stores, or to change the backing storage for the default store, you can do so via the `--runtime-config-file` flag and the `runtime-config.toml` file. See [Key Value Store Runtime Configuration](./dynamic-configuration#key-value-store-runtime-configuration) for details. + +## Granting Key Value Store Permissions to Components + +By default, a given component of an app will not have access to any key value store. Access must be granted specifically to each component via the component manifest: + +```toml +[component.example] +# Pass in 1 or more key value stores, based on how many you'd like your component to have access to +key_value_stores = ["", ""] +``` + +For example, a component could be given access to the default store using `key_value_stores = ["default"]`. diff --git a/content/spin/v3/language-support-overview.md b/content/spin/v3/language-support-overview.md new file mode 100644 index 000000000..073473e14 --- /dev/null +++ b/content/spin/v3/language-support-overview.md @@ -0,0 +1,110 @@ +title = "Language Support Overview" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/language-support-overview.md" + +--- + +This page contains information about language support for Spin features: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +**[📄 Visit the Rust Spin SDK reference documentation](https://docs.rs/spin-sdk/latest/spin_sdk/) to see specific modules, functions, variables and syntax relating to the following Rust features.** + +| Feature | SDK Supported? | +|-----|-----| +| **Triggers** | +| [HTTP](./http-trigger) | Supported | +| [Redis](./redis-trigger) | Supported | +| **APIs** | +| [Outbound HTTP](./rust-components.md#sending-outbound-http-requests) | Supported | +| [Configuration Variables](./variables) | Supported | +| [Key Value Storage](./kv-store-api-guide) | Supported | +| [SQLite Storage](./sqlite-api-guide) | Supported | +| [MySQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [PostgreSQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [Outbound Redis](./rust-components.md#storing-data-in-redis-from-rust-components) | Supported | +| [Serverless AI](./serverless-ai-api-guide) | Supported | +| [MQTT Messaging](./mqtt-outbound) | Supported | +| **Extensibility** | +| [Authoring Custom Triggers](./extending-and-embedding) | Supported | + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +**[📄 Visit the JS/TS Spin SDK reference documentation](https://fermyon.github.io/spin-js-sdk/) to see specific modules, functions, variables and syntax relating to the following TS/JS features.** + +| Feature | SDK Supported? | +|-----|-----| +| **Triggers** | +| [HTTP](./javascript-components#http-components) | Supported | +| Redis | Not Supported | +| **APIs** | +| [Outbound HTTP](./javascript-components#sending-outbound-http-requests) | Supported | +| [Configuration Variables](./dynamic-configuration#custom-config-variables) | Supported | +| [Key Value Storage](./kv-store-api-guide) | Supported | +| [SQLite Storage](./sqlite-api-guide) | Supported | +| [MySQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [PostgreSQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [Outbound Redis](./javascript-components#storing-data-in-redis-from-jsts-components) | Supported | +| [Serverless AI](./serverless-ai-api-guide) | Supported | +| [MQTT Messaging](./mqtt-outbound) | Supported | +| **Extensibility** | +| Authoring Custom Triggers | Not Supported | + +{{ blockEnd }} + +{{ startTab "Python"}} + +**[📄 Visit the Python Spin SDK reference documentation](https://fermyon.github.io/spin-python-sdk/v1) to see specific modules, functions, variables and syntax relating to the following Python SDK.** + +| Feature | SDK Supported? | +|-----|-----| +| **Triggers** | +| [HTTP](./python-components#a-simple-http-components-example) | Supported | +| [Redis](./redis-trigger) | Supported | +| **APIs** | +| [Outbound HTTP](./python-components#an-outbound-http-example) | Supported | +| [Configuration Variables](./dynamic-configuration#custom-config-variables) | Supported | +| [Key Value Storage](./kv-store-api-guide) | Supported | +| [SQLite Storage](./sqlite-api-guide) | Supported | +| MySQL | Supported | +| PostgreSQL | Supported | +| [Outbound Redis](./python-components#an-outbound-redis-example) | Supported | +| [Serverless AI](./serverless-ai-api-guide) | Supported | +| [MQTT Messaging](./mqtt-outbound) | Not Supported | +| **Extensibility** | +| Authoring Custom Triggers | Not Supported | + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +**[📄 Visit the TinyGo Spin SDK reference documentation](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2) to see specific modules, functions, variables and syntax relating to the following TinyGo SDK.** + +| Feature | SDK Supported? | +|-----|-----| +| **Triggers** | +| [HTTP](./go-components#http-components) | Supported | +| [Redis](./go-components#redis-components) | Supported | +| **APIs** | +| [Outbound HTTP](./go-components#sending-outbound-http-requests) | Supported | +| [Configuration Variables](./dynamic-configuration#custom-config-variables) | Supported | +| [Key Value Storage](./kv-store-api-guide) | Supported | +| [SQLite Storage](./sqlite-api-guide) | Supported | +| [MySQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [PostgreSQL](./rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | +| [Outbound Redis](./go-components#storing-data-in-redis-from-go-components) | Supported | +| [Serverless AI](./serverless-ai-api-guide) | Supported | +| [MQTT Messaging](./mqtt-outbound) | Not Supported | +| **Extensibility** | +| Authoring Custom Triggers | Not Supported | + +{{ blockEnd }} + +{{ blockEnd }} diff --git a/content/spin/v3/managing-plugins.md b/content/spin/v3/managing-plugins.md new file mode 100644 index 000000000..6c8122b2f --- /dev/null +++ b/content/spin/v3/managing-plugins.md @@ -0,0 +1,185 @@ +title = "Managing Plugins" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/managing-plugins.md" + +--- +- [Installing Plugins](#installing-plugins) + - [Installing Well Known Plugins](#installing-well-known-plugins) + - [Installing a Specific Version of a Plugin](#installing-a-specific-version-of-a-plugin) + - [Installing a Plugin From a URL](#installing-a-plugin-from-a-url) + - [Installing a Plugin From a File](#installing-a-plugin-from-a-file) +- [Running a Plugin](#running-a-plugin) +- [Viewing Available Plugins](#viewing-available-plugins) + - [Viewing Installed Plugins](#viewing-installed-plugins) +- [Uninstalling Plugins](#uninstalling-plugins) +- [Refreshing the Catalogue](#refreshing-the-catalogue) +- [Upgrading Plugins](#upgrading-plugins) +- [Downgrading Plugins](#downgrading-plugins) +- [Next Steps](#next-steps) + +Plugins are a way to extend the functionality of Spin. Spin provides commands for installing and removing them, so you don't need to use separate installation tools. When you have installed a plugin into Spin, you can call it as if it were a Spin subcommand. For example, Fermyon Cloud can be accessed with a plugin called `cloud`, and you run it via the `spin cloud` command. + +## Installing Plugins + +To install plugins, use the `spin plugins install` command. You can install plugins by name from a curated repository, or other plugins from a URL or file system. + +### Installing Well Known Plugins + +The Spin maintainers curate a catalogue of "known" plugins. You can install plugins from this catalogue by name: + + + +```bash +$ spin plugins install cloud +``` + +Spin checks that the plugin is available for your version of Spin and your operating system, and prompts you to confirm the installation. To skip the prompt, pass the `--yes` flag. + +> The curated plugins catalogue is stored in a GitHub repository. The first time you install a plugin from the catalogue, Spin clones this repository into a local cache and uses it for future install, list and upgrade commands (similar to OS package managers such as `apt`). If you want to see new catalogue entries - new plugins or new versions - you must update the local cache by running the `spin plugins update` command. + +### Installing a Specific Version of a Plugin + +To install a specific version of a plugin, pass the `--version` flag: + + + +```bash +$ spin plugins install cloud --version 0.9.1 +``` + +### Installing a Plugin From a URL + +If the plugin you want has been published on the Web but has not been added to the catalogue, you can install it from its manifest URL. The manifest is the JSON document that links to the binaries for different operating systems and processors. For example: + + + +```bash +$ spin plugins install --url https://github.com/fermyon/spin-befunge-sdk/releases/download/v1.4.0/befunge2wasm.json +``` + +### Installing a Plugin From a File + +If the plugin you want is in your local file system, you can install it from its manifest file path. The manifest is the JSON document that links to the binaries for different operating systems and processors. For example: + + + +```bash +$ spin plugins install --file ~/dev/spin-befunge-sdk/befunge2wasm.json +``` + +## Running a Plugin + +You run plugins in the same way as built-in Spin subcommands. For example: + + + +```bash +$ spin cloud --help +``` + +## Viewing Available Plugins + +To see what plugins are available in the catalogue, run `spin plugins search`: + + + +```bash +$ spin plugins search +befunge2wasm 1.4.0 [incompatible] +cloud 0.8.0 [installed] +cloud 0.9.0 +trigger-sqs 0.1.0 +``` + +The annotations by the plugins show their status and compatibility: + +| Annotation | Meaning | +|---------------------------------|---------| +| `[incompatible]` | The plugin does not run on your operating system or processor. | +| `[installed]` | You have the plugin already installed and available to run. | +| `[requires other Spin version]` | The plugin can run on your operating system and processor, but is not compatible with the version of Spin you are running. The annotation indicates which versions of Spin it is compatible with. | + +### Viewing Installed Plugins + +To see only the plugins you have installed, run `spin plugins list --installed`. + +## Uninstalling Plugins + +You can uninstall plugins using `spin plugins uninstall` with the plugin name: + + + +```bash +$ spin plugins uninstall befunge2wasm +``` + +## Refreshing the Catalogue + +The first time you install a plugin from the catalogue, Spin creates a local cache of the catalogue. It continues to use this local cache for future install, list and upgrade commands; this is similar to OS package managers such as `apt`, and avoids rate limiting on the catalogue. However, this means that in order to see new catalogue entries - new plugins or new versions - you must first update the cache. + +To update your local cache of the catalogue, run `spin plugins update`. + +## Upgrading Plugins + +To upgrade a plugin to the latest version, first run `spin plugins update` (to refresh the catalogue), then `spin plugins upgrade`. + +The `spin plugins upgrade` command has the same options as the `spin plugins install` command (according to whether the plugin comes from the catalogue, a URL, or a file). For more information, see the command help by running `spin plugins upgrade --help`. + +> The `upgrade` command uses your local cache of the catalogue. This might not include recently added plugins or versions. So always remember to run `spin plugins update` to refresh your local cache of the catalogue before performing the `spin plugins upgrade` command. + +The following example shows how to upgrade one plugin at a time (i.e. the `cloud` plugin): + + + +```bash +$ spin plugins update +$ spin plugins upgrade cloud +``` + +The following example shows how to upgrade all installed plugins at once: + + + +```bash +$ spin plugins update +$ spin plugins upgrade --all +``` + +> Note: The above example only installs plugins from the catalogue + +The following example shows additional upgrade options. Specifically, how to upgrade using the path to a remote plugin manifest and how to upgrade using the path to a local plugin manifest: + + + +```bash +$ spin plugins upgrade --url https://github.com/fermyon/spin-befunge-sdk/releases/download/v1.7.0/befunge2wasm.json +$ spin plugins upgrade --file ~/dev/spin-befunge-sdk/befunge2wasm.json +``` + +## Downgrading Plugins + +By default, Spin will only _upgrade_ plugins. Pass the `--downgrade` flag and specify the `--version` if you want Spin to roll back to an earlier version. The following abridged example (which doesn't list the full console output for simplicity) lists the versions of plugins, downgrades the `cloud` to an older version (`0.9.0`) and then lists the versions again to show the results: + + + +```bash +$ spin plugins update +$ spin plugins list +// --snip-- +cloud 0.9.0 +cloud 0.9.1 [installed] +$ spin plugins upgrade cloud --downgrade --version 0.9.0 +$ spin plugins list +// --snip-- +cloud 0.9.0 [installed] +cloud 0.9.1 +``` + +After downgrading, the `[installed]` indicator is aligned with the `0.9.0` version of `cloud`, as intended in the example. + +## Next Steps + +- [Check out the spin cloud plugin](https://github.com/fermyon/cloud-plugin) diff --git a/content/spin/v3/managing-templates.md b/content/spin/v3/managing-templates.md new file mode 100644 index 000000000..5408d3003 --- /dev/null +++ b/content/spin/v3/managing-templates.md @@ -0,0 +1,127 @@ +title = "Managing Templates" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/managing-templates.md" + +--- +- [Installing Templates](#installing-templates) + - [Installing From the Spin Git Repository](#installing-from-the-spin-git-repository) + - [Installing From a Specific Branch](#installing-from-a-specific-branch) + - [Installing From a Local Directory](#installing-from-a-local-directory) +- [Viewing Your Installed Templates](#viewing-your-installed-templates) +- [Uninstalling Templates](#uninstalling-templates) +- [Upgrading Templates](#upgrading-templates) + - [Upgrading Templates From a Local Directory](#upgrading-templates-from-a-local-directory) +- [Next Steps](#next-steps) + +Templates are a Spin tool for scaffolding new applications and components. You can use them via the `spin new` and `spin add` commands. For more information about creating applications with templates, see [Writing Spin Applications](writing-apps). + +## Installing Templates + +> This section covers general principles for installing templates. For information about installing templates for specific languages, see [Writing Spin Applications](writing-apps). + +To install templates, use the `spin templates install` command. You can install templates from a Git repository, or while [authoring templates](template-authoring) you can install them from a local directory. + +### Installing From the Spin Git Repository + +To install templates from the Spin Git repository, run `spin templates install --git`: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin +``` + +If you prefer a shorter command, you can just pass the repository id instead of the full URL: + + + +```bash +$ spin templates install --git fermyon/spin +``` + +The above command installs _all_ templates in the repository. + +> Language SDKs often ship templates in their repositories; see the relevant language guide to find out where to get its templates. + +### Installing From a Specific Branch + +By default, if you install templates from a Git repository, Spin tries to find a repo tag that matches the version of Spin, and installs from that tag. Failing this, it installs from `HEAD`. If you would like to install from a specific tag or branch, pass the `--branch` option: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --branch spin/templates/v0.8 +``` + +### Installing From a Local Directory + +To install templates from your local file system, run `spin templates install --dir`. + +> The directory you pass must be one that _contains_ a `templates` directory. Don't pass the `templates` directory itself! + + + +```bash +# Expects to find a directory ~/dev/spin-befunge-sdk/templates +$ spin templates install --dir ~/dev/spin-befunge-sdk +``` + +See [Template Authoring](template-authoring) for more details on this layout. + +## Viewing Your Installed Templates + +To see what templates you have installed, run `spin templates list`. + +You can use the `--verbose` option to see additional information such as where they were installed from. + +## Uninstalling Templates + +You can uninstall templates using `spin templates uninstall` with the template name: + + + +```bash +$ spin templates uninstall redis-befunge +``` + +> Spin doesn't currently support uninstalling a whole repo-worth of templates, only individual templates. + +## Upgrading Templates + +When you upgrade Spin, you will typically want to upgrade your templates to match. This means new applications and components will get dependencies that match the Spin version you are using. To do this, run `spin templates upgrade`: + + + +```bash +$ spin templates upgrade +Select repos to upgrade. Use Space to select/deselect and Enter to confirm selection. + [x] https://github.com/fermyon/spin-python-sdk + [ ] https://github.com/fermyon/spin (at spin/templates/v1.0) +> [x] https://github.com/fermyon/spin-js-sdk +``` + +Use the cursor keys and the space bar to select the repositories you want to upgrade, then hit Enter to upgrade the selected repositories. + +> Upgrading happens at the repo level, not the individual template level. If you've uninstalled templates, upgrading the repo they came from will bring them back. + +If you want to upgrade _all_ repositories without being prompted, run `spin templates upgrade --all`. + +As mentioned above, if you want to check which templates come from which repositories use `--verbose` i.e. `spin templates list --verbose`. + +### Upgrading Templates From a Local Directory + +`spin templates upgrade` only upgrades from Git repositories. If you want to upgrade and your templates are in a local directory, run the `spin templates install` command with the `--upgrade` flag: + + + +```bash +$ spin templates install --dir ~/dev/spin-befunge-sdk --upgrade +``` + +## Next Steps + +- [Install the templates for your language](quickstart) +- [Use your language templates to create an application](writing-apps) \ No newline at end of file diff --git a/content/spin/v3/manifest-reference-v1.md b/content/spin/v3/manifest-reference-v1.md new file mode 100644 index 000000000..9b22bf512 --- /dev/null +++ b/content/spin/v3/manifest-reference-v1.md @@ -0,0 +1,143 @@ +title = "Spin Application Manifest (Version 1) Reference" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/manifest-reference-v1.md" + +--- +- [Format](#format) +- [Manifest Fields](#manifest-fields) +- [The `trigger` Table](#the-trigger-table) + - [The `trigger` Table for HTTP Applications](#the-trigger-table-for-http-applications) + - [The `trigger` Table for Redis Applications](#the-trigger-table-for-redis-applications) +- [The `variables` Table](#the-variables-table) +- [The `component` Tables](#the-component-tables) + - [The `component.trigger` Table for HTTP Applications](#the-componenttrigger-table-for-http-applications) + - [The `component.trigger` Table for Redis Applications](#the-componenttrigger-table-for-redis-applications) + - [The `component.build` Table](#the-componentbuild-table) +- [Next Steps](#next-steps) + +This page describes version 1 of the Spin manifest file, typically called `spin.toml`. + +> There are two versions of the manifest format. The manifest format described here (version 1) is used for Spin 1.x, and is supported for backward compatibility in Spin 2.x and above. New applications targeting Spin 2 should use the [version 2 manifest format](manifest-reference.md). + +## Format + +The manifest is a TOML file, and follows standard TOML syntax. See the [TOML documentation](https://toml.io/) for information about the TOML syntax. + +## Manifest Fields + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `spin_manifest_version` | Required | String | The version of the file format that the rest of the manifest follows. Currently, this value must be `"1"`. | `"1"` | +| `name` | Required | String | The name of the application. This may be any string of alphanumeric characters, hyphens and underscores. | `"hello-world"` | +| `version` | Required | String | The version of the application. The must be a string of the form `major.minor.patch`, where each element is a number. | `"1.0.5"` | +| `description` | Optional | String | A human-readable description of the application. | `"The best app for all your world-greeting needs"` | +| `authors` | Optional | Array of strings | The authors of the applications. If present, this must ba an array, even if it has only one entry. | `["Jane Q Hacker ()"]` | +| `trigger` | Required | Table | The trigger for the application - that is, the kind of event that the application responds to. The table must contain the `type` field, and may contain others depending on the value of `type`. See [The `trigger` Table](#the-trigger-table) for details. | `{ type = "http" }` | +| `variables` | Optional | Table | Dynamic configuration variables which the user can set when they run the application. See [The `variables` Table](#the-variables-table) below. | `[variables]`
`message = { default = "hello" }` | +| `component` | Required | Table array | A manifest must contain at least one `component` table. `component` is always an array, even if there is only one component, so always use double square brackets. See [The `component` Tables](#the-component-tables) below. | `[[component]]`
`id = "hello"` | + +## The `trigger` Table + +The `trigger` table specifies the events that the application responds to. The `type` field is always required, but the other fields depend on the `type`. This section describes the built-in `http` and `redis` trigger types. + +> Because the `trigger` table usually contains only a few simple fields, you will usually see it written inline using brace notation, rather than written out using square-brackets table syntax. For example: +> +> ```toml +> trigger = { type = "http" } +> ``` + +### The `trigger` Table for HTTP Applications + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `type` | Required | String | Always `"http"` for HTTP applications. | `"http"` | + +### The `trigger` Table for Redis Applications + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `type` | Required | String | Always `"redis"` for Redis applications. | `"redis"` | +| `address` | Required | String | The address of the Redis instance the components are using the message subscriptions. Use the `redis:` URL scheme. | `"redis://localhost:6379"` | + +## The `variables` Table + +The keys of `variables` table are user-defined. The value of each key is another table with the fields shown in the following table. + +> Because each `variables` value usually contains only a few simple fields, you will usually see the table entries written inline with the values written using brace notation, rather than fully written out using square-brackets table syntax for each variable. For example: +> +> ```toml +> [variables] +> vessel = { default = "teapot" } +> token = { required = true, secret = true } +> ``` + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `default` | Optional | String | The value of the variable if no value is supplied at runtime. If specified, the value must be a string. If not specified, `required` must be `true`. | `"teapot"` | +| `required` | Optional | Boolean | Whether a value must be supplied at runtime. If not specified, `required` defaults to `false`, and `default` must be provided | `false` | +| `secret` | Optional | Boolean | If set, this variable should be treated as sensitive. | `false` | + +## The `component` Tables + +`component` is a table array, meaning each component is introduced with double-bracket syntax. Subtables are written using single-bracket syntax or inline JSON syntax. For example: + +```toml +[[component]] +id = "hello" +source = "hello.wasm" +[component.trigger] +route = "/hello" +``` + +Each table in the `component` array contains the following fields: + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `id` | Required | String | An identifier for this component, unique within the application. This may be any string of alphanumeric characters, hyphens and underscores. | `"cart-api"` | +| `description` | Optional | String | A human-readable description of the component. | `"The shopping cart API"` | +| `source` | Required | String or table | The Wasm module which should handle the component. This must be built to work with the application trigger. It can be in one of the following formats: | | +| | | String | * The path to a Wasm file (relative to the manifest file) | `dist/cart.wasm` | +| | | Table | * The URL of a Wasm file downloadable over HTTP. This must be a table containing a `url` field for the Wasm file, and a `digest` field contains a SHA256 hex digest, used to check integrity. | `{ url = "https://example.com/example.wasm", digest = "sha256:6503...2375" }` | +| `files` | Optional | Array of strings and/or tables | The [files to be made available to the Wasm module at runtime](writing-apps#including-files-with-components). This is an array, and each element of the array is either: | `[ "images/*.jpg", { source = "assets/images", destination = "/pictures" } ]` | +| | | String | * A file path or glob pattern, relative to the manifest file. The matching file or files will be available in the Wasm module at the same relative paths. | `"images/*.jpg"` | +| | | Table | * A directory to be made available to the Wasm module at a specific path. This must be a table containing a `source` field for the directory relative to the manifest file, and a `destination` field containing the absolute path at which to make it available. | `{ source = "assets/images", destination = "/pictures" }` | +| `exclude_files` | Optional | Array of strings | Any files or glob patterns that should _not_ be available to the Wasm module at runtime, even though they match a `files` entry. | `[assets/images/test/**/*.*]` | +| `allowed_http_hosts` | Optional | Array of strings | The host names or addresses to which the Wasm module is allowed to send HTTP requests. If the name includes a port, the Wasm module can send requests only to that port; otherwise, the Wasm module can send requests only to the default port for the scheme it uses. The special string `insecure:allow-all` permits the module to send HTTP requests to _any_ host, but is intended for development use only; some deployment environments may decline to honour it. | `["example.com", "localhost:8081"]` | +| `allowed_outbound_hosts` | Optional | Array of strings | The addresses to which the Wasm component is allowed to send network requests. This applies to the outbound HTTP, outbound Redis, MySQL and PostgreSQL APIs. (It does not apply to built-in storage services such as key-value and SQLite.) Each entry must contain both a scheme, a name (or IP address) and a port in `scheme://name:port` format. For known schemes, you may omit the port if it is the default for the scheme. Use `*` for wildcards. If this field is omitted, no outbound access is permitted, _except_ that, for backward compatibility, a Spin 1 component may use Redis, MySQL or PostgreSQL to _any_ host. If this field is present and empty, however, no outbound access if permitted, regardless of the component's Spin version. | `["mysql://db.example.com", "*://example.com:4567", "http://127.0.0.1:*"]` | +| `key_value_stores` | Optional | Array of strings | An array of key-value stores that the Wasm module is allowed to read or write. A store named `default` is provided by the Spin runtime, though modules must still be permitted to access it. In current versions of Spin, `"default"` is the only store allowed. | `["default"]` | +| `environment` | Optional | Table | Environment variables to be set for the Wasm module. This is a table. The table keys are user-defined; the values must be strings. | `{ DB_URL = "mysql://spin:spin@localhost/dev" }` | +| `trigger` | Required | Table | Specifies how this component is triggered. This is a table, whose contents of are trigger-specific; see below. | `[component.trigger]`
`route = "/..."` | +| `build` | Optional | Table | The command that `spin build` uses to build this component. See [The `component.build` Table](#the-componentbuild-table) below. | `[component.build]`
`command = "npm run build"` | +| `config` | Optional | Table | Dynamic configuration values to be made available to this component. The table keys are user-defined; the values must be strings, and may use template notation as described under [Dynamic Configuration](dynamic-configuration). | `[component.config]`
`api_base_url = "https://{{ api_host }}/v1"` | + +### The `component.trigger` Table for HTTP Applications + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `route` | Required | String | The route which this component handles. Requests to the route will cause the component to execute. This may be an exact route (`/example`), which matches on the given path, or a wildcard route indicated by the suffix `/...` (`/example/...`), which matches any route under this prefix. If two routes overlap, requests are directed to the matching route with the longest prefix - see [the HTTP trigger documentation](http-trigger) for details and examples. | `"/api/cart/..."` | +| `executor` | Optional | Table | How Spin should invoke the component. If present, this is a table. The `type` key is required and may have the values `"spin"` or `"wagi"`. If omitted. the default is `{ type = "spin"}`. See [the HTTP trigger documentation](http-trigger) for details. | `{ type = "wagi" }` | +| | | | If `type = "spin"` there are no other keys defined. In this case, Spin calls the component using a standard Wasm component interface. Components built using Spin SDKs or Spin interface files use this convention. | `{ type = "spin" }` | +| | | | If `type = "wagi"`, Spin calls the component's "main" (`_start`) function using [a CGI-like interface](https://github.com/deislabs/wagi). Components built using languages or toolchains that do not support Wasm interfaces will need to be called in this way. In this case, the following additional keys may be set:

* `argv` (optional): The string representation of the `argv` list that should be passed into the handler. `${SCRIPT_NAME}` will be replaced with the script name, and `${ARGS}` will be replaced with the query parameters of the request, formatted as arguments. The default is to follow the CGI specification, and pass `${SCRIPT_NAME} ${ARGS}`

* `entrypoint` (optional): The name of the function to call as the entry point to this handler. By default, it is `_start` (which in most languages translates to `main` in the source code).

See [the HTTP trigger documentation](http-trigger) for details. | `{ type = "wagi" }` | + +### The `component.trigger` Table for Redis Applications + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `channel` | Required | String | The Redis channel which this component handles. Messages on this channel will cause the component to execute. | `"purchases"` | + +### The `component.build` Table + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `command` | Required | String | The command to execute on `spin build`. | `"cargo build --target wasm32-wasi --release"` | +| `workdir` | Optional | String | The directory in which to execute `command`, relative to the manifest file. The default is the directory containing the manifest file. An example of where this is needed is a multi-component application where each component is its own source tree in its own directory. | `"my-project"` | +| `watch` | Optional | Array of strings | The files or glob patterns which `spin watch` should monitor to determine if the component Wasm file needs to be rebuilt. These are relative to `workdir`, or to the directory containing the manifest file if `workdir` is not present. | `["src/**/*.rs", "Cargo.toml"]` | + +## Next Steps + +- Learn about [writing Spin applications and their manifests](writing-apps) +- Learn about [dynamic and runtime configuration](dynamic-configuration) +- See more information about the [HTTP trigger](http-trigger) +- See more information about the [Redis trigger](redis-trigger) diff --git a/content/spin/v3/manifest-reference.md b/content/spin/v3/manifest-reference.md new file mode 100644 index 000000000..41fe09e87 --- /dev/null +++ b/content/spin/v3/manifest-reference.md @@ -0,0 +1,205 @@ +title = "Spin Application Manifest Reference" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/manifest-reference.md" + +--- +- [Manifest Format](#manifest-format) +- [Using Variables in the Manifest](#using-variables-in-the-manifest) +- [Manifest Fields](#manifest-fields) +- [The `application` Table](#the-application-table) +- [The `application.trigger` Table](#the-applicationtrigger-table) + - [The `application.trigger.redis` Table](#the-applicationtriggerredis-table) +- [The `variables` Table](#the-variables-table) +- [The `trigger` Table](#the-trigger-table) + - [Common Fields for All `trigger.(type)` Tables](#common-fields-for-all-triggertype-tables) + - [Additional Fields for `trigger.http` Tables](#additional-fields-for-triggerhttp-tables) + - [Additional Fields for `trigger.redis` Tables](#additional-fields-for-triggerredis-tables) +- [The `component` Table](#the-component-table) +- [The `component.(id).build` Table](#the-componentidbuild-table) +- [Next Steps](#next-steps) + +This page describes the contents of the Spin manifest file, typically called `spin.toml`. + +> There are two versions of the manifest format. The manifest format described here (version 2) is recommended if you're using Spin 2.0 and above. The [previous format (version 1)](manifest-reference-v1.md) is supported on Spin 2.x for backward compatibility, and is the only format supported by Spin 1.x. + +## Manifest Format + +The manifest is a TOML file, and follows standard TOML syntax. See the [TOML documentation](https://toml.io/) for information about the TOML syntax. Here is an example Spin application manifest (`spin.toml`) that was generated using `spin new -t http-rust spin-manifest-example-in-rust`: + +```toml +spin_manifest_version = 2 + +[application] +name = "spin-manifest-example-in-rust" +version = "0.1.0" +authors = ["Fermyon Engineering "] +description = "An example application to generate a Spin manifest file, in this case, via the HTTP Rust template." + +[[trigger.http]] +route = "/..." +component = "spin-manifest-example-in-rust" + +[component.spin-manifest-example-in-rust] +source = "target/wasm32-wasi/release/spin_manifest_example_in_rust.wasm" +allowed_outbound_hosts = [] +[component.spin-manifest-example-in-rust.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +## Using Variables in the Manifest + +The following fields allow you to use [expressions](./variables.md#adding-variables-to-your-applications) in their values: + +* `application.trigger.redis.address` +* `trigger.redis.address` +* `trigger.redis.channel` +* `component.*.allowed_outbound_hosts` + +Spin resolves manifest expressions at application load time. Subsequent changes to variables do not update expression-based values. + +The only variables permitted in manifest expressions are application variables. + +> Manifest expressions are not yet supported on Fermyon Cloud. + +## Manifest Fields + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `spin_manifest_version` | Required | Number | The version of the file format that the rest of the manifest follows. For manifests using this format, this value must be `2`. | `2` | +| `application` | Required | Table | Information about the application and application-global options. See [The `application` Table](#the-application-table) below. | `[application]`
`name = "greetings"`
`version = "1.0.0"` | +| `variables` | Optional | Table | Dynamic configuration variables which the user can set when they run the application. See [The `variables` Table](#the-variables-table) below. | `[variables]`
`message = { default = "hello" }` | +| `trigger` | Required | Table | Associates triggers and conditions to the components that handle them - for example, mapping a HTTP route to a handling component. See [The `trigger` Table](#the-trigger-table) below. | `[[trigger.http]]`
`component = "greeter"`
`route = "/greet"` | +| `component` | Required | Table | The WebAssembly components that make up the application, together with whatever configuration and resources they depend on, such as asset files or storage access. See [The `component` Table](#the-component-table) below. | `[component.greeter]`
`source = "greeting_manager.wasm"`
`files = ["confetti.jpg"]` | + +> If you're familiar with manifest version 1, note that the way trigger parameters map to components - for example, which component handles a particular HTTP route - is now defined on the _trigger_, not on the component. In the version 2 manifest, a `component` section specifies _only_ the Wasm module and the resources it needs. + +## The `application` Table + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `name` | Required | String | The name of the application. This may be any string of alphanumeric characters, hyphens and underscores. | `"hello-world"` | +| `version` | Optional | String | The version of the application. The must be a string of the form `major.minor.patch`, where each element is a number. | `"1.0.5"` | +| `description` | Optional | String | A human-readable description of the application. | `"The best app for all your world-greeting needs"` | +| `authors` | Optional | Array of strings | The authors of the applications. If present, this must ba an array, even if it has only one entry. | `["Jane Q Hacker ()"]` | +| `trigger` | Optional | Table | Application-global trigger settings. See [The `application.trigger` Table](#the-applicationtrigger-table) below. | `[application.trigger.redis]`
`address = "redis.example.com"` | + +## The `application.trigger` Table + +The `application.trigger` should contain only one key, the trigger type whose settings you want to override. This is usually written inline as part of the TOML table header, e.g. `[application.trigger.redis]`. + +> In many cases, your trigger will have no settings, or the default ones will suffice. In this case, you can omit the `application.trigger` table. + +### The `application.trigger.redis` Table + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `address` | Required | String | The address of the Redis instance to which the components subscribe for messages, for triggers that do not specify an address themselves. Use the `redis:` URL scheme. | `"redis://localhost:6379"` | + +## The `variables` Table + +The keys of the `variables` table are user-defined. The value of each key is another table with the fields shown in the following table. + +> Because each `variables` value usually contains only a few simple fields, you will usually see the table entries written inline with the values written using brace notation, rather than fully written out using square-brackets table syntax for each variable. For example: +> +> ```toml +> [variables] +> vessel = { default = "teapot" } +> token = { required = true, secret = true } +> ``` + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `default` | Optional | String | The value of the variable if no value is supplied at runtime. If specified, the value must be a string. If not specified, `required` must be `true`. | `"teapot"` | +| `required` | Optional | Boolean | Whether a value must be supplied at runtime. If not specified, `required` defaults to `false`, and `default` must be provided | `false` | +| `secret` | Optional | Boolean | If set, this variable should be treated as sensitive. | `false` | + +## The `trigger` Table + +The `trigger` table contains only one key, the trigger type to which your application responds. The value of this key is a table array. In practice, the `trigger` table is written using table array syntax with the trigger type inlined into each entry. For example: + +```toml +[[trigger.http]] +route = "/users" +component = "user-manager" + +[[trigger.http]] +route = "/reports" +component = "report" +``` + +Each array entry contains a mix of common fields and trigger-specific fields. + +### Common Fields for All `trigger.(type)` Tables + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-----------------|----------|-----------| +| `component` | Required | String or table | The component to run when a trigger event matching the trigger setting occurs (for example, when Spin receives an HTTP request matching the trigger's `route`). It can be in one of the following formats: | | +| | | String | * A key in the `component` table | `"user-manager"` | +| | | Table | * Specifies an unnamed component to be associated with the trigger setting. This allows simple components to be written inline instead of needing a separate section. Such a table follows [the `component` table](#the-component-table) format. | { source = "reports.wasm" }` | + +### Additional Fields for `trigger.http` Tables + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `route` | Required | String or table | The route which this component handles. Requests to the route will cause the component to execute. | | +| | | String | This may be an exact route (`/example`), which matches only the given path, or may include wildcards (`/example/:id` or `/example/...`). If two routes overlap, requests are directed to the matching route with the longest prefix. See [the HTTP trigger documentation](http-trigger) for details and examples. | `"/api/cart/..."` | +| | | Table | If the component is a private endpoint used for [local service chaining](http-outbound#local-service-chaining) then use the table value shown here. | `{ private = true }`| +| `executor` | Optional | Table | How Spin should invoke the component. If present, this is a table. The `type` key is required and may have the values `"spin"` or `"wagi"`. If omitted. the default is `{ type = "spin"}`. See [the HTTP trigger documentation](http-trigger) for details. | `{ type = "wagi" }` | +| | | | If `type = "spin"` there are no other keys defined. In this case, Spin calls the component using a standard Wasm component interface. Components built using Spin SDKs or Spin interface files use this convention. | `{ type = "spin" }` | +| | | | If `type = "wagi"`, Spin calls the component's "main" (`_start`) function using [a CGI-like interface](https://github.com/deislabs/wagi). Components built using languages or toolchains that do not support Wasm interfaces will need to be called in this way. In this case, the following additional keys may be set:

* `argv` (optional): The string representation of the `argv` list that should be passed into the handler. `${SCRIPT_NAME}` will be replaced with the script name, and `${ARGS}` will be replaced with the query parameters of the request, formatted as arguments. The default is to follow the CGI specification, and pass `${SCRIPT_NAME} ${ARGS}`

* `entrypoint` (optional): The name of the function to call as the entry point to this handler. By default, it is `_start` (which in most languages translates to `main` in the source code).

See [the HTTP trigger documentation](http-trigger) for details. | `{ type = "wagi" }` | + +### Additional Fields for `trigger.redis` Tables + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `address` | Optional | String | The address of the Redis instance to which the trigger subscribes. Use the `redis:` URL scheme. If omitted, defaults to `application.trigger.redis.address`. | `"redis://localhost:6379"` | +| `channel` | Required | String | The Redis channel which this component handles. Messages on this channel will cause the component to execute. | `"purchases"` | + +## The `component` Table + +The keys of the `component` table, usually written as part of the table syntax e.g. `[component.my-component]`, are user-defined. (In the preceding example, the key is `my-component`). Component names must be kebab-cased, i.e. the only permitted separator is a hyphen. + +The value of each key is a table with the following fields. + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `description` | Optional | String | A human-readable description of the component. | `"The shopping cart API"` | +| `source` | Required | String or table | The Wasm module which should handle the component. This must be built to work with the application trigger. It can be in one of the following formats: | | +| | | String | * The path to a Wasm file (relative to the manifest file) | `dist/cart.wasm` | +| | | Table | * The URL of a Wasm file downloadable over HTTP. This must be a table containing a `url` field for the Wasm file, and a `digest` field contains a SHA256 hex digest, used to check integrity. | `{ url = "https://example.com/example.wasm", digest = "sha256:6503...2375" }` | +| | | Table (Highly Experimental) | * The registry, package and version of a component from a registry. This experimental source configuration must be a table containing a `registry` domain field, a `package` field and a `version` field. | `{ registry = "registrytest-abcd.fermyon.app", package = "component:hello-world", version="0.0.1" }` or `{ registry = "ttl.sh", package = "user:registrytest", version="1.0.0" }` | +| `files` | Optional | Array of strings and/or tables | The [files to be made available to the Wasm module at runtime](writing-apps#including-files-with-components). This is an array, and each element of the array is either: | `[ "images/*.jpg", { source = "assets/images", destination = "/pictures" } ]` | +| | | String | * A file path or glob pattern, relative to the manifest file. The matching file or files will be available in the Wasm module at the same relative paths. | `"images/*.jpg"` | +| | | Table | * A file or directory to be made available to the Wasm module at a specific path. This must be a table containing a `source` field for the file or directory relative to the manifest file, and a `destination` field containing the absolute path at which to make it available. | `{ source = "assets/images", destination = "/pictures" }` | +| `exclude_files` | Optional | Array of strings | Any files or glob patterns that should _not_ be available to the Wasm module at runtime, even though they match a `files` entry. | `[assets/images/test/**/*.*]` | +| `allowed_http_hosts` | Optional | Array of strings | The host names or addresses to which the Wasm component is allowed to send HTTP requests. This is retained to simplify transition from the [Version 1 manifest](./manifest-reference-v1.md); new applications should use `allow_outbound_hosts` instead. | `["example.com", "localhost:8081"]` | +| `allowed_outbound_hosts` | Optional | Array of strings | The addresses to which the Wasm component is allowed to send network requests. This applies to the outbound HTTP, outbound Redis, MySQL and PostgreSQL APIs. (It does not apply to built-in storage services such as key-value and SQLite.) Each entry must contain both a scheme, a name (or IP address) and a port in `scheme://name:port` format. For known schemes, you may omit the port if it is the default for the scheme. Use `*` for wildcards. If this field is omitted or an empty list, no outbound access is permitted. | `["mysql://db.example.com", "*://example.com:4567", "http://127.0.0.1:*"]` | +| `key_value_stores` | Optional | Array of strings | An array of key-value stores that the Wasm module is allowed to read or write. A store named `default` is provided by the Spin runtime, though modules must still be permitted to access it. In current versions of Spin, `"default"` is the only store allowed. | `["default"]` | +| `environment` | Optional | Table | Environment variables to be set for the Wasm module. This is a table. The table keys are user-defined; the values must be strings. | `{ DB_URL = "mysql://spin:spin@localhost/dev" }` | +| `build` | Optional | Table | The command that `spin build` uses to build this component. See [The `component.(id).build` Table](#the-componentidbuild-table) below. | `[component.cart.build]`
`command = "npm run build"` | +| `variables` | Optional | Table | Dynamic configuration values to be made available to this component. The table keys are user-defined; the values must be strings, and may use template notation as described under [Dynamic Configuration](dynamic-configuration). | `[component.cart.variables]`
`api_base_url = "https://{{ api_host }}/v1"` | +| `dependencies_inherit_configuration` | Optional | Boolean | If true, dependencies can invoke Spin APIs with the same permissions as the main component. If false, dependencies have no permissions (e.g. network, key-value stores, SQLite databases). The default is false. | `false` | +| `dependencies` | Optional | Table | Specifies how to satisfy Wasm Component Model imports of this component. See [Using Component Dependencies](writing-apps.md#using-component-dependencies). | `[component.cart.dependencies]`
`"example:calculator/adder" = { registry = "example.com", package = "example:adding-calculator", version = "1.0.0" }` | + +> If you're familiar with manifest version 1, note that: +> * The component `id` is no longer a field within a `[[component]]`, but the key of the component in the table, written as part of the `[component.(id)]` header. +> * The trigger association is no longer a `[trigger]` sub-table but is written in the separate `trigger` table. +> * The `config` section is now named `variables`. + +## The `component.(id).build` Table + +| Name | Required? | Type | Value | Example | +|-------------------------|------------|-------------|----------|-----------| +| `command` | Required | String | The command to execute on `spin build`. | `"cargo build --target wasm32-wasi --release"` | +| `workdir` | Optional | String | The directory in which to execute `command`, relative to the manifest file. The default is the directory containing the manifest file. An example of where this is needed is a multi-component application where each component is its own source tree in its own directory. | `"my-project"` | +| `watch` | Optional | Array of strings | The files or glob patterns which `spin watch` should monitor to determine if the component Wasm file needs to be rebuilt. These are relative to `workdir`, or to the directory containing the manifest file if `workdir` is not present. | `["src/**/*.rs", "Cargo.toml"]` | + +## Next Steps + +- Learn about [writing Spin applications and their manifests](writing-apps) +- Learn about [dynamic and runtime configuration](dynamic-configuration) +- See more information about the [HTTP trigger](http-trigger) +- See more information about the [Redis trigger](redis-trigger) diff --git a/content/spin/v3/migration-v2-v3.md b/content/spin/v3/migration-v2-v3.md new file mode 100644 index 000000000..0d851c9b2 --- /dev/null +++ b/content/spin/v3/migration-v2-v3.md @@ -0,0 +1,36 @@ +title = "Migrating an Application from Spin 2.x to Spin 3.x" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/migration-v2-v3.md" + +--- + +Most applications will not require changes. The following changes may affect application authors: + +* The HTTP trigger application-level `base` option has been removed. To simulate the effect of `base`, use a common prefix for your routes. +* Spin now infers the `authority` property of an incoming HTTP request from either the request URL or the HTTP Host header. + (If the request includes both, then they must agree.) This means that when a client makes the request through a DNS name, + the `authority` will be that DNS name, rather than the Spin server's listener address. If your components use the `authority` + property then you should carefully re-test that they work correctly after this change. +* Spin now adds an HTTP Host header on `self` requests (those made using a path only). The value of the header is the listener address + of the Spin HTTP server. Consequently, the component receiving the `self` request will always see the URL `authority` as that of the + Spin HTTP server. If your application uses self-requests, and the receiving components use the `authority` + property, then you should carefully re-test that they work correctly after this change. +* The on-disk format for [Serverless AI](serverless-ai-api-guide.md) LLMs has changed. You will need to re-download local + models and re-organise them in accordance with the new format. See the [Serverless AI API guide](serverless-ai-api-guide.md#file-structure) + for details. (No application code changes are required.) +* The [environment provider for application variables](dynamic-configuration.md#environment-variable-provider) is now overridden + by application variable providers configured in the [runtime config file](dynamic-configuration.md#application-variables-runtime-configuration). + If you relied on environment variables taking precedence over other sources, you will need to stop setting `--runtime-config-file` + or tweak your workflow. +* Some modules produced by languages with older WASI support may no longer run. This specifically affects WAGI applications + compiled with a WASI-SDK version 18 or earlier. See [https://github.com/fermyon/spin/issues/2552](https://github.com/fermyon/spin/issues/2552) for details + of the issue and for possible workarounds. + +The following change will not affect most application authors, but may affect you if you work directly with the low-level WASI interfaces: + +* If an outbound HTTP request is denied, the `HTTP-request-denied` error previously occurred immediately on the + initial `wasi:http/outgoing-handler#handle` call. It now occurs when calling `get` on the `future-incoming-response` + returned from the `handle` call. diff --git a/content/spin/v3/mqtt-outbound.md b/content/spin/v3/mqtt-outbound.md new file mode 100644 index 000000000..7e7914c2e --- /dev/null +++ b/content/spin/v3/mqtt-outbound.md @@ -0,0 +1,112 @@ +title = "MQTT Messaging" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/mqtt-outbound.md" + +--- +- [Sending MQTT Messages From Applications](#sending-mqtt-messages-from-applications) +- [Granting Network Permissions to Components](#granting-network-permissions-to-components) + - [Configuration-Based Permissions](#configuration-based-permissions) +- [Known Issues](#known-issues) + +Spin provides an experimental interface for you to send messages using the MQTT protocol. + +{{ details "Why do I need a Spin interface? Why can't I just use my language's MQTT library?" "Few MQTT libraries have been updated to work over the WASI 0.2 sockets interface. The Spin interface means Wasm modules can bypass this limitation by asking Spin to make the MQTT connection on their behalf." }} + +> Want to receive MQTT messages? Use [the MQTT trigger](https://github.com/spinkube/spin-trigger-mqtt) to handle messages in your Spin application. + +## Sending MQTT Messages From Applications + +The Spin SDK surfaces the Spin MQTT interface to your language. The set of operations defined in Spin's API is as follows: + +| Operation | Parameters | Returns | Behavior | +|--------------|---------------------|---------|----------| +| `open` | address, username, password, keep-alive | connection resource | Opens a connection to the specified MQTT server. The host must be listed in `allowed_outbound_hosts`. Other operations must be called through a connection. | +| `publish` | topic, payload, QoS | - | Publishes the payload (a binary blob) as a message to the specified topic. | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/mqtt/index.html) + +MQTT functions are available in the `spin_sdk::mqtt` module. + +To access an MQTT server, use the `Connection::open` function. + +```rust +let connection = spin_sdk::mqtt::Connection::open(&address, &username, &password, keep_alive_secs)?; +``` + +You can then call the `Connection::publish` function to send MQTT messages: + +```rust +let cat_picture: Vec = request.body().to_vec(); +connection.publish("pets", &cat_picture, spin_sdk::mqtt::Qos::AtLeastOnce)?; +``` + +For full details of the MQTT API, see the [Spin SDK reference documentation](https://docs.rs/spin-sdk/latest/spin_sdk/mqtt/index.html); + +You can find a complete Rust code example for using outbound MQTT from an HTTP component in the [Spin Rust SDK repository on GitHub](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/mqtt-outbound). + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/modules/Mqtt.html) + +To access an MQTT server, use the `Mqtt.open` function. + +```ts +let connection = Mqtt.open(address, username, password, keepAliveSecs); +``` + +You can then call the `publish` method on the connection to send MQTT messages: + +```ts +let catPicture = new Uint8Array(await req.arraybuffer()); +connection.publish("pets", catPicture, QoS.AtleastOnce); +``` + +For full details of the MQTT API, see the [Spin SDK reference documentation](https://fermyon.github.io/spin-js-sdk/modules/Mqtt.html) + +You can find a complete Rust code example for using outbound MQTT from an HTTP component in the [Spin Rust SDK repository on GitHub](https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-mqtt). + +{{ blockEnd }} + +{{ startTab "Python"}} + +MQTT is not available in the current version of the Python SDK. + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +MQTT is not available in the current version of the Go SDK. + +{{ blockEnd }} + +{{ blockEnd }} + +## Granting Network Permissions to Components + +By default, Spin components are not allowed to make outgoing network requests, including MQTT. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. To grant a component permission to make network requests to a particular host, use the `allowed_outbound_hosts` field in the component manifest, specifying the host and allowed port: + +```toml +[component.uses-mqtt] +allowed_outbound_hosts = ["mqtt://messaging.example.com:1883"] +``` + +### Configuration-Based Permissions + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `allowed_outbound_hosts` field. However, this feature is not yet available on Fermyon Cloud. + +## Known Issues + +The MQTT API is experimental and subject to change. The following issues are known: + +* The MQTT sender interface in the current version of Spin is known to occasionally drop errors, especially if under load. A fix is in progress. diff --git a/content/spin/v3/observing-apps.md b/content/spin/v3/observing-apps.md new file mode 100644 index 000000000..ef8fcf493 --- /dev/null +++ b/content/spin/v3/observing-apps.md @@ -0,0 +1,112 @@ +title = "Observing Applications" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/observing-apps.md" + +--- + +- [Application Logs](#application-logs) +- [OpenTelemetry (OTel)](#opentelemetry-otel) + - [Running An Observability Stack](#running-an-observability-stack) + - [Configuring Spin](#configuring-spin) + - [Traces](#traces) + - [Metrics](#metrics) + - [Logs](#logs) + +## Application Logs + +Spin handles application logs by default, storing output and error messages from file system-run applications in the `.spin/logs` directory alongside the manifest file's location. Users have the option to direct logs to a specific folder using the `--log-dir` flag of the `spin up` command. Additionally, if users wish to prevent `stdout` and `stderr` from being written to disk, they can specify an empty string for the `--log-dir` flag, i.e. `spin up --log-dir ""` - effectively disabling log storage. See the [persistent logs](./running-apps#persistent-logs) section for more details. + +## OpenTelemetry (OTel) + +Spin now has support for the [OpenTelemetry (OTel)](https://opentelemetry.io/) observability standard. You can learn more about observability [here](https://opentelemetry.io/docs/concepts/observability-primer/). When configured, Spin will emit telemetry about your Spin App in the form of OTel [signals](https://opentelemetry.io/docs/concepts/signals/): traces, metrics, and logs. + +## Using the OTel Plugin + +We have a plugin that makes it easy to use OpenTelemetry with Spin. If you would like to examine the source code, you can visit the [GitHub repository](https://github.com/fermyon/otel-plugin). Otherwise, follow these instructions: + +- To install the plugin, run the commands below: + + ```sh + spin plugins update + spin plugins install otel + ``` +- To see the available commands, you can run `spin otel --help` + +## Configuring your own observability stack + +Follow this portion of the guide if you want to use Spin and OTel, but want to have more control than what the OTel plugin offers. + +### Configure the Docker compose stack + +In order to view the telemetry data you need to run an OTel compliant [collector](https://opentelemetry.io/docs/collector/) and the proper backends for each signal type. If you have Docker on your system you can easily start all the observability tools you need with the following commands: + +```sh +cd ~ +git clone git@github.com:fermyon/spin.git +cd spin/hack/o11y-stack +docker compose up -d +``` + +This will start the following services: + +- [OTel Collector](https://opentelemetry.io/docs/collector/): Collector to receive OTel signals from Spin and forward to the appropriate backends. +- [Jaeger](https://www.jaegertracing.io/): Backend for traces. +- [Tempo](https://grafana.com/oss/tempo/): Alternative backend for traces. +- [Loki](https://grafana.com/oss/loki/): Backend for logs. +- [Prometheus](https://prometheus.io/): Backend for metrics. +- [Grafana](https://grafana.com/oss/grafana/): Dashboard for viewing data stored in Tempo, Loki, and Prometheus. + +### Configuring Spin + +To have Spin export OTel telemetry to the collector you need to set the following environment variable: + +```sh +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 spin up +``` + +This will enable all OTel signals. If you only want to enable specific signals you can set the following environment variables individually: + +- Traces: `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +- Metrics: `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` +- Logs: `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` + +For example this would enable exporting of traces and metrics: + +```sh +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metrics spin up +``` + +Storing lots of trace data can get expensive. You may want to sample traces to reduce the amount of data stored. You can set the following environment variable to control the sampling rate: + +```sh +OTEL_TRACES_SAMPLER=traceidratio OTEL_TRACES_SAMPLER_ARG={desired_ratio} OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 spin up +``` + +Under high request loads Spin will start dropping OTel data. If keeping all of this data is important to you there are [environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-span-processor) you can set: + +```sh +OTEL_BSP_MAX_CONCURRENT_EXPORTS=4 OTEL_BSP_MAX_QUEUE_SIZE=4096 OTEL_BSP_SCHEDULE_DELAY=1000 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 spin up +``` + +Spin supports a wide array of OTel configuration options beyond what we've covered here. You can read more about them [here](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) and [here](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration). + +### Traces + +After sending some requests to your Spin app, navigate to Jaeger [http://localhost:16686](http://localhost:16686) to view the traces. + +![Traces from app](/static/image/jaeger-traces.png) + +Spin supports both inbound and outbound [trace context propagation](https://opentelemetry.io/docs/concepts/context-propagation/). This allows you to include Spin in your distributed traces that span all your services. + +### Metrics + +Navigate to [http://localhost:5050/explore](http://localhost:5050/explore) to view the metrics in Grafana. Make sure to choose the Prometheus data source from the top left dropdown menu. + +### Logs + +Navigate to [http://localhost:5050/explore](http://localhost:5050/explore) to view the logs in Grafana. Make sure to choose the Loki data source from the top left dropdown menu. + +Spin will still emit application logs as described in the [Application Logs](#application-logs) section. However, it will also send the logs to the OTel collector. diff --git a/content/spin/v3/other-languages.md b/content/spin/v3/other-languages.md new file mode 100644 index 000000000..8df1c868c --- /dev/null +++ b/content/spin/v3/other-languages.md @@ -0,0 +1,73 @@ +title = "Building Spin components in other languages" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/other-languages.md" + +--- +- [C/C++](#cc) +- [C# and .NET Languages](#c-and-net-languages) +- [Grain](#grain) +- [Ruby](#ruby) +- [Zig](#zig) + +> This document is continuously evolving as we improve language SDKs and add +> more examples on how to build Spin components in various programming languages. + +> See the document on writing [Rust](./rust-components.md) and [Go](./go-components.md) +> components for Spin for detailed guides. + +WebAssembly is becoming [a popular compilation target for programming languages](https://www.fermyon.com/wasm-languages/webassembly-language-support), and as language toolchains add support for the +[WebAssembly component model](https://github.com/WebAssembly/component-model), +building Spin components will also become supported. + +As a general rule: + +- if your language supports the +[WebAssembly component model](https://component-model.bytecodealliance.org/), +you can build Spin components either through an official Spin SDK +(such as [the Spin SDK for Rust](./rust-components.md)), or through using +bindings generators like [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) +(for languages such as C and C++) +- if your language compiles to WASI, but doesn't have support for the component +model, you can build [Spin HTTP components](./http-trigger.md) that use the +Wagi executor — for example in languages such as +[Grain](https://github.com/deislabs/hello-wagi-grain) or [Swift](https://github.com/fermyon/wagi-python). +- if your language doesn't currently compile to WASI, there is no way to +build and run Spin components in that programming language + +## C/C++ + +C and C++ are both broadly supported in the WebAssembly ecosystem. WASI/Wagi support means that both can be used to write Spin apps. + +- The [C entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/c-lang) has examples. +- The [C++ entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/cpp) has specific caveats for writing C++ (like exception handling) +- The [yo-wasm](https://github.com/deislabs/yo-wasm) project makes setting up C easier. + +## C# and .NET Languages + +.NET has experimental support for WASI, so many (if not all) .NET languages, including C# and F#, can be used to write Spin applications. + +- The [C# entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/c-sharp) has a full example. + +## Grain + +[Grain](https://grain-lang.org/), a new functional programming language, has WASI/Wagi support and can be used to write Spin apps. + +- The [Grain entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/grain) has details +- A simple [Hello World example](https://github.com/deislabs/hello-wagi-grain) shows how to use Grain +- For a production-quality example. the [Wagi Fileserver](https://github.com/deislabs/wagi-fileserver) is written in Grain + +## Ruby + +Upstream [Ruby](https://www.ruby-lang.org/en/) officially supports WebAssembly and WASI, and we here at Fermyon have successfully run Ruby apps in Spin. + +- The [Ruby entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/ruby) has the latest information +- [Ruby's 3.2.0 Preview 1 release notes](https://www.ruby-lang.org/en/news/2022/04/03/ruby-3-2-0-preview1-released/) detail WASI support + +## Zig + +Zig is a low-level systems language that has support for Wasm and WASI, and can be used to write Spin apps. + +- The [Zig entry in the Wasm Language Guide](https://www.fermyon.com/wasm-languages/zig) covers the basics +- Zig's [0.4 release notes](https://ziglang.org/download/0.4.0/release-notes.html#WebAssembly-Support) explain WebAssembly support \ No newline at end of file diff --git a/content/spin/v3/plugin-authoring.md b/content/spin/v3/plugin-authoring.md new file mode 100644 index 000000000..90c1e99f8 --- /dev/null +++ b/content/spin/v3/plugin-authoring.md @@ -0,0 +1,141 @@ +title = "Creating Spin Plugins" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/plugin-authoring.md" + +--- +- [What Are Spin Plugins?](#what-are-spin-plugins) +- [How to Find and Use a Spin Plugin](#how-to-find-and-use-a-spin-plugin) +- [Authoring a Spin Plugin](#authoring-a-spin-plugin) + - [Environment Variables Available to the Plugin Executable](#environment-variables-available-to-the-plugin-executable) + - [Packaging a Plugin](#packaging-a-plugin) + - [Creating a Spin Plugin Manifest](#creating-a-spin-plugin-manifest) + - [Installing a Local Plugin](#installing-a-local-plugin) + - [Contributing a Plugin](#contributing-a-plugin) + +Spin plugins add new functionality or subcommands to Spin without modifying the +Spin codebase. They make Spin easily extensible while keeping it lightweight. +Spin plugins can add new triggers to Spin (such as the [example timer +trigger](https://github.com/fermyon/spin/blob/main/examples/spin-timer/trigger-timer.json)), +add deployment integrations (such as +[`kube`](https://github.com/fermyon/spin-plugins/blob/main/manifests/kube/kube.json)), +and more. + +This document will cover what Spin plugins are, how to use a plugin, and how to +create a plugin. + +## What Are Spin Plugins? + +A Spin plugin is an executable that is added to Spin's plugins directory +(`$XDG_DATA_HOME/spin/plugins`) upon a `spin plugins install `. The +plugin is then ready to be used. If the plugin is an extension to the Spin CLI, +it can now be executed directly as a subcommand: `spin `. If the +plugin is a trigger plugin, it will be executed during `spin up` when an app +using that trigger is run. + +While for now plugins are assumed to be executables, in the future, support for +plugging in WebAssembly modules may be desirable. + +## How to Find and Use a Spin Plugin + +Spin maintains a centralized catalogue of available Spin plugins in the [Spin +plugins repository](https://github.com/fermyon/spin-plugins). During plugin +installation, if it does not already exist, Spin fetches the remote catalogue +and creates a local snapshot. To ensure that the local snapshot is up to date, +it is best to run `spin plugins update` before installing any plugins. + +To list available plugins, run `spin plugins search`. Now, decide which plugin to +install. For example, the `kube` plugin, which is needed in order to deploy +applications to [SpinKube](https://www.spinkube.dev/), can be installed by running: + + + +```bash +$ spin plugins install kube +``` + +With the plugin installed, you can now call `spin kube` to run it. + +To upgrade installed plugins to newer versions, run `spin plugin update` to +fetch the latest plugins to the local catalogue and `spin plugin upgrade` to perform the +upgrade on the installed plugins. + +## Authoring a Spin Plugin + +Spin plugins are implemented as a manifest that points to one or more `.tar.gz` archives which contain the plugin executables. So, to create a plugin you must: + +1. Create tar archives of the executables for the platforms you want to support +2. Compose a manifest that describes the plugin and lists the URLs for those tar archives + +### Environment Variables Available to the Plugin Executable + +Your plugin may need to know information about the instance of Spin it's running in. For example, suppose your plugin wants to call `spin build`. The trouble is that you don't know if it's on the user's system PATH. Suppose, further, that your plugin would prefer to call `spin build -c` (to build only a specific component) if it's available but can fall back to `spin build` (to build everything) if it's not. The `-c` option only exists in Spin 1.4 and above, so this optimization requires that you know which version of Spin you're running in. + +To help with this, when a user uses Spin to run your plugin, Spin sets a number of environment variables on the plugin process. Your code can use these environment variables to find out things like the path to the Spin binary and which version of Spin it is. When your plugin runs, the parent Spin process will set these to the right values for the _user's_ instance of Spin. In the example above, when your plugin wants to run `spin build`, it can consult the `SPIN_BIN_PATH` environment variable for the program path, and be confident that the `SPIN_VERSION` environment variable matches the Spin binary at that location. + +The variables Spin sets are: + +| Name | Meaning | Example | +|--------------------|-----------------------------------------------------------------------------------------------------------------------|---------| +| SPIN_BIN_PATH | The path to the Spin executable that the user is running. Use this if your plugin issues commands using the Spin CLI. | /Users/alice/.cargo/bin/spin | +| SPIN_BRANCH | The Git branch from which the Spin executable was built. | main | +| SPIN_BUILD_DATE | The date on which the Spin executable was built, in yyyy-mm-dd format. | 2023-05-15 | +| SPIN_COMMIT_DATE | The date of the Git commit from which the Spin executable was built, in yyyy-mm-dd format. | 2023-05-15 | +| SPIN_COMMIT_SHA | The SHA of the Git commit from which the Spin executable was built. | 49fb11b | +| SPIN_DEBUG | Whether the Spin executable is a debug build. | false | +| SPIN_TARGET_TRIPLE | The processor and operating system for which the Spin executable was built, in Rust target-triple format. | aarch64-apple-darwin | +| SPIN_VERSION | The version of Spin. This can be used to detect features availability, or to determine pre-stable command syntax. | 1.3.0 | +| SPIN_VERSION_MAJOR | The major version of Spin. | 1 | +| SPIN_VERSION_MINOR | The minor version of Spin. | 3 | +| SPIN_VERSION_PRE | The prerelease version string, or empty if this is a released version of Spin. | pre0 | + +> These variables aren't set if the launching Spin instance is version 1.3 or earlier. If you depend on these variables, set the `spinCompatibility` entry in the manifest to require 1.4 or above. + +### Packaging a Plugin + +After creating your plugin executable, package it along with its license as a +`tar.gz` archive. Note that the `name` field in the plugin manifest must match +both the binary and license name. See the [`spin-plugins` +repository +README](https://github.com/fermyon/spin-plugins#spin-plugin-naming-conventions) +for more details on naming conventions. + +Refer to the aptly named [`example` +plugin](https://github.com/fermyon/spin-plugins/tree/main/example) for an +example of how to build a plugin. + +### Creating a Spin Plugin Manifest + +A Spin plugin manifest is a JSON file that conforms to the [a specific JSON +schema](https://github.com/fermyon/spin-plugins/blob/main/json-schema/spin-plugin-manifest-schema-0.1.json). +A manifest defines a plugin’s name, version, license, homepage (i.e. GitHub +repo), compatible Spin version, and gives a short description of the plugin. It +also lists the URLs of the tar archives of the plugin for various operating +systems and platforms. The URL can point to the local path to the file by using +the file scheme `file://`, for example, `file:///tmp/my-plugin.tar.gz`. + +To ensure your plugin manifest is valid, follow the steps in the [`spin-plugins` +repository +README](https://github.com/fermyon/spin-plugins#validating-plugin-schemas). + +### Installing a Local Plugin + +By default, Spin will look in the plugins catalogue for a plugin. However, when +developing and testing a plugin, it is unlikely to be in the the catalogue. For +both installs and upgrades, the `--file` or `--url` flags can be used to point +to specific local or remote plugin manifests. For example, a local manifest +called `practice.json` can be installed and run as follows: + + + +```bash +$ spin plugin install --file practice.json +$ spin practice +``` + +### Contributing a Plugin + +If you think the community would benefit from your newly created plugin, create +a PR to add it to the [Spin plugins +catalogue](https://github.com/fermyon/spin-plugins/tree/main/manifests)! diff --git a/content/spin/v3/python-components.md b/content/spin/v3/python-components.md new file mode 100644 index 000000000..dc2b95f68 --- /dev/null +++ b/content/spin/v3/python-components.md @@ -0,0 +1,540 @@ +title = "Building Spin Components in Python" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/python-components.md" + +--- +- [Prerequisite](#prerequisite) +- [Componentize-Py](#componentize-py) +- [Spin's Python HTTP Request Handler Template](#spins-python-http-request-handler-template) +- [Creating a New Python Component](#creating-a-new-python-component) + - [System Housekeeping (Use a Virtual Environment)](#system-housekeeping-use-a-virtual-environment) + - [Requirements](#requirements) +- [Structure of a Python Component](#structure-of-a-python-component) +- [A Simple HTTP Components Example](#a-simple-http-components-example) + - [Building and Running the Application](#building-and-running-the-application) +- [A HTTP Request Parsing Example](#a-http-request-parsing-example) + - [Building and Running the Application](#building-and-running-the-application-1) +- [An Outbound HTTP Example](#an-outbound-http-example) + - [Configuring Outbound Requests](#configuring-outbound-requests) + - [Building and Running the Application](#building-and-running-the-application-2) +- [An Outbound Redis Example](#an-outbound-redis-example) + - [Configuring Outbound Redis](#configuring-outbound-redis) + - [Building and Running the Application](#building-and-running-the-application-3) +- [Storing Data in the Spin Key-Value Store](#storing-data-in-the-spin-key-value-store) +- [Storing Data in SQLite](#storing-data-in-sqlite) +- [AI Inferencing From Python Components](#ai-inferencing-from-python-components) +- [Troubleshooting](#troubleshooting) + +With Python being a very popular language, Spin provides support for building components with Python; [using an experimental SDK](https://github.com/fermyon/spin-python-sdk). The development of the Python SDK is continually being worked on to improve user experience and also add new features. + +> This guide assumes you have Spin installed. If this is your first encounter with Spin, please see the [Quick Start](quickstart), which includes information about installing Spin with the Python templates, installing required tools, and creating Python applications. + +> This guide assumes you are familiar with the Python programming language, but if you are just getting started, be sure to check out the official Python documentation and comprehensive language reference. + +[**Want to go straight to the Spin SDK reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk) + +## Prerequisite + +Ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + +## Componentize-Py + +The Python SDK is built using [`componentize-py`](https://github.com/bytecodealliance/componentize-py). It is a [Bytecode Alliance](https://bytecodealliance.org/) project that allows converting a Python application to a WebAssembly component. It can be installed using the following command: + + + +```bash +$ pip3 install componentize-py==0.13.3 +``` + +> **Please note:** The `hello-world` sample below installs `componentize-py` automatically via the `pip3 install -r requirements.txt` command - so feel free to skip this step if you are following the `hello-world` sample with us. + +## Spin's Python HTTP Request Handler Template + +Spin's Python HTTP Request Handler Template can be installed from [spin-python-sdk repository](https://github.com/fermyon/spin-python-sdk/tree/main/) using the following command: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --update +``` + +The above command will install the `http-py` template and produce an output similar to the following: + + + +```text +Copying remote template source +Installing template http-py... +Installed 1 template(s) + ++---------------------------------------------+ +| Name Description | ++=============================================+ +| http-py HTTP request handler using Python | ++---------------------------------------------+ +``` + +**Please note:** For more information about managing Spin templates, see the [templates guide](./managing-templates). + +## Creating a New Python Component + +A new Python component can be created using the following command: + + + +```bash +$ spin new -t http-py hello-world --accept-defaults +``` + +### System Housekeeping (Use a Virtual Environment) + +Once the component is created, we can change into the `hello-world` directory, create and activate a virtual environment and then install the component's requirements: + + + +```console +$ cd hello-world +``` + +Create a virtual environment directory (we are still inside the Spin app directory): + + + +```console +# python -m venv +$ python3 -m venv venv-dir +``` + +Activate the virtual environment (this command depends on which operating system you are using): + + + +```console +# macOS command to activate +$ source venv-dir/bin/activate +``` + +If you are using Windows, use the following commands: + +```bash +C:\Work> python3 -m venv venv +C:\Work> venv\Scripts\activate +``` + +The `(venv-dir)` will prefix your terminal prompt now: + + + +```console +(venv-dir) user@123-456-7-8 hello-world % +``` + +### Requirements + +The `requirements.txt`, by default, contains the references to the `spin-sdk` and `componentize-py` packages. These can be installed in your virtual environment using the following command: + + + +```bash +$ pip3 install -r requirements.txt +Collecting spin-sdk==3.1.0 (from -r requirements.txt (line 1)) + Using cached spin_sdk-3.1.0-py3-none-any.whl.metadata (16 kB) +Collecting componentize-py==0.13.3 (from -r requirements.txt (line 2)) + Using cached componentize_py-0.13.3-cp37-abi3-macosx_10_12_x86_64.whl.metadata (3.4 kB) +Using cached spin_sdk-3.1.0-py3-none-any.whl (94 kB) +Using cached componentize_py-0.13.3-cp37-abi3-macosx_10_12_x86_64.whl (38.8 MB) +Installing collected packages: spin-sdk, componentize-py +Successfully installed componentize-py-0.13.3 spin-sdk-3.1.0 +``` + +## Structure of a Python Component + +The `hello-world` directory structure created by the Spin `http-py` template is shown below: + + + +```text +├── app.py +├── spin.toml +└── requirements.txt +``` + +The `spin.toml` file will look similar to the following: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello-world" +version = "0.1.0" +authors = ["Your Name "] +description = "" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "app.wasm" +[component.hello-world.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +``` + +## A Simple HTTP Components Example + +In Spin, HTTP components are triggered by the occurrence of an HTTP request and must return an HTTP response at the end of their execution. Components can be built in any language that compiles to WASI. If you would like additional information about building HTTP applications you may find [the HTTP trigger page](./http-trigger.md) useful. + +Building a Spin HTTP component using the Python SDK means defining a top-level class named IncomingHandler which inherits from [`IncomingHandler`](https://fermyon.github.io/spin-python-sdk/wit/exports/index.html#spin_sdk.wit.exports.IncomingHandler), overriding the `handle_request` method. Here is an example of the default Python code which the previous `spin new` created for us; a simple example of a request/response: + + + +```python +from spin_sdk.http import IncomingHandler, Request, Response + +class IncomingHandler(IncomingHandler): + def handle_request(self, request: Request) -> Response: + return Response( + 200, + {"content-type": "text/plain"}, + bytes("Hello from Python!", "utf-8") + ) +``` + +The important things to note in the implementation above: + +- the `handle_request` method is the entry point for the Spin component. +- the component returns a `spin_sdk.http.Response`. + +### Building and Running the Application + +All you need to do is run the `spin build` command from within the project's directory; as shown below: + + + +```bash +$ spin build +``` + +Essentially, we have just created a new Spin compatible module which can now be run using the `spin up` command, as shown below: + + + +```bash +$ spin up +``` + +With Spin running our application in our terminal, we can now go ahead (grab a new terminal) and call the Spin application via an HTTP request: + + + +```bash +$ curl -i localhost:3000 + +HTTP/1.1 200 OK +content-type: text/plain +content-length: 25 + +Hello from Python! +``` + +## A HTTP Request Parsing Example + +The following snippet shows how you can access parts of the request e.g. the `request.method` and the `request.body`: + + + +```python +import json +from spin_sdk import http +from spin_sdk.http import Request, Response + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + # Access the request.method + if request.method == 'POST': + # Read the request.body as a string + json_str = request.body.decode('utf-8') + # Create a JSON object representation of the request.body + json_object = json.loads(json_str) + # Access a value in the JSON object + name = json_object['name'] + # Print the variable to console logs + print(name) + # Print the type of the variable to console logs + print(type(name)) + # Print the available methods of the variable to the console logs + print(dir(name)) + return Response(200, + {"content-type": "text/plain"}, + bytes(f"Practicing reading the request object", "utf-8")) +``` + +### Building and Running the Application + +All you need to do is run the `spin build --up` command from within the project's directory; as shown below: + + + +```bash +$ spin build --up +``` + +With Spin running our application in our terminal, we can now go ahead (grab a new terminal) and call the Spin application via an HTTP request: + + + +```bash +$ curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"name":"Python"}' \ + http://localhost:3000/ + +HTTP/1.1 200 OK +content-type: text/plain +content-length: 37 +date: Mon, 15 Apr 2024 04:26:00 GMT + +Practicing reading the request object +``` + +The response "Practicing reading the request object" is returned as expected. In addition, if we check the terminal where Spin is running, we will see that the console logs printed the following: + +The value of the variable called `name`: + + + +```bash +Python +``` + +The `name` variable type (in this case a Python string): + + + +```bash + +``` + +The methods available to that type: + + + +```bash +['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', +... abbreviated ... +'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] +``` + +> **Please note:** All examples from this documentation page can be found in [the Python SDK repository on GitHub](https://github.com/fermyon/spin-python-sdk/tree/main/examples). If you are following along with these examples and don't get the desired result perhaps compare your own code with our previously built examples (mentioned above). Also please feel free to reach out on [Discord](https://discord.gg/AAFNfS7NGf) if you have any questions or need any additional support. + +## An Outbound HTTP Example + +This next example will create an outbound request, to obtain a random fact about animals, which will be returned to the calling code. If you would like to try this out, you can go ahead and update your existing `app.py` file from the previous step; using the following source code: + + + +```python +from spin_sdk import http +from spin_sdk.http import Request, Response, send + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + resp = send(Request("GET", "https://random-data-api.fermyon.app/animals/json", {}, None)) + + return Response(200, + {"content-type": "text/plain"}, + bytes(f"Here is an animal fact: {str(resp.body, 'utf-8')}", "utf-8")) + +``` + +### Configuring Outbound Requests + +The Spin framework protects your code from making outbound requests to just any URL. For example, if we try to run the above code **without any additional configuration**, we will correctly get the following error `AssertionError: HttpError::DestinationNotAllowed`. To allow our component to request the `random-data-api.fermyon.app` domain, all we have to do is add that domain to the specific component of the application that is making the request. Here is an example of an updated `spin.toml` file where we have added `allowed_outbound_hosts`: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello-world" +version = "0.1.0" +authors = ["Your Name "] +description = "" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "app.wasm" +allowed_outbound_hosts = ["https://random-data-api.fermyon.app"] +[component.hello-world.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +watch = ["*.py", "requirements.txt"] +``` + +### Building and Running the Application + +Run the `spin build --up` command from within the project's directory; as shown below: + + + +```bash +$ spin build --up +``` + +With Spin running our application in our terminal, we can now go ahead (grab a new terminal) and call the Spin application via an HTTP request: + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +content-type: text/plain +content-length: 99 +date: Mon, 15 Apr 2024 04:52:45 GMT + +Here is an animal fact: {"timestamp":1713156765221,"fact":"Bats are the only mammals that can fly"} +``` + +## An Outbound Redis Example + +In this final example, we talk to an existing Redis instance. You can find the official [instructions on how to install Redis here](https://redis.io/docs/getting-started/installation/). We also gave a quick run-through on setting up Redis with Spin in our previous article called [Persistent Storage in Webassembly Applications](https://www.fermyon.com/blog/persistent-storage-in-webassembly-applications), so please take a look at that blog if you need a hand. + +### Configuring Outbound Redis + +After installing Redis on localhost, we add two entries to the `spin.toml` file: + +* `variables = { redis_address = "redis://127.0.0.1:6379" }` externalizes the URL of the server to access +* `allowed_outbound_hosts = ["redis://127.0.0.1:6379"]` enables network access to the host and port where Redis is running + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello-world" +version = "0.1.0" +authors = ["Your Name "] +description = "" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +id = "hello-world" +source = "app.wasm" +variables = { redis_address = "redis://127.0.0.1:6379" } +allowed_outbound_hosts = ["redis://127.0.0.1:6379"] +[component.hello-world.build] +command = "spin py2wasm app -o app.wasm" +``` + +If you are still following along, please go ahead and update your `app.py` file one more time, as follows: + + + +```python +from spin_sdk import http, redis, variables +from spin_sdk.http import Request, Response + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + with redis.open(variables.get("redis_address")) as db: + db.set("foo", b"bar") + value = db.get("foo") + db.incr("testIncr") + db.sadd("testSets", ["hello", "world"]) + content = db.smembers("testSets") + db.srem("testSets", ["hello"]) + assert value == b"bar", f"expected \"bar\", got \"{str(value, 'utf-8')}\"" + + return Response(200, + {"content-type": "text/plain"}, + bytes(f"Executed outbound Redis commands: {request.uri}", "utf-8")) +``` + +### Building and Running the Application + +Run the `spin build --up` command from within the project's directory; as shown below: + + + +```bash +$ spin build --up +``` + +In a new terminal, make the request via the curl command, as shown below: + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +content-type: text/plain +content-length: 35 +date: Mon, 15 Apr 2024 05:53:17 GMT + +Executed outbound Redis commands: / +``` + +If we go into our Redis CLI on localhost we can see that the value `foo` which was set in the Python source code ( `redis_set(redis_address, "foo", b"bar")` ) is now correctly set to the value of `bar`: + + + +```bash +redis-cli +127.0.0.1:6379> get foo +"bar" +``` + +## Storing Data in the Spin Key-Value Store + +Spin has a key-value store built in. For information about using it from Python, see [the key-value store API guide](kv-store-api-guide). + +## Storing Data in SQLite + +For more information about using SQLite from Python, see [SQLite storage](sqlite-api-guide). + +## AI Inferencing From Python Components + +For more information about using Serverless AI from Python, see the [Serverless AI](serverless-ai-api-guide) API guide. + +## Troubleshooting + +If you bump into issues when installing the requirements.txt. For example: + + + +```console +error: externally-managed-environment +× This environment is externally managed +``` + +Please note, this error is specific to Homebrew-installed Python installations and occurs because installing a **non-brew-packaged** Python package requires you to either: +- create a virtual environment using `python3 -m venv path/to/venv`, or +- use the `--break-system-packages` option in your `pip3 install` command i.e. `pip3 install -r requirements.txt --break-system-packages` + +We recommend installing a virtual environment using `venv`, as shown in the [system housekeeping section](#system-housekeeping-use-a-virtual-environment) above. + +For all Python examples, please ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). diff --git a/content/spin/v3/quickstart.md b/content/spin/v3/quickstart.md new file mode 100644 index 000000000..5e2daf28d --- /dev/null +++ b/content/spin/v3/quickstart.md @@ -0,0 +1,960 @@ +title = "Taking Spin for a spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/quickstart.md" +keywords = "quickstart" + +--- +- [Install Spin](#install-spin) +- [Install the Prerequisites](#install-the-prerequisites) + - [Install a Template](#install-a-template) + - [Install the Tools](#install-the-tools) +- [Create Your First Application](#create-your-first-application) +- [Structure of a Python Component](#structure-of-a-python-component) +- [Build Your Application](#build-your-application) +- [Run Your Application](#run-your-application) +- [Deploy Your Application to Fermyon Cloud](#deploy-your-application-to-fermyon-cloud) + - [Log in to Fermyon Cloud](#log-in-to-fermyon-cloud) + - [Deploy the Application](#deploy-the-application) +- [Next Steps](#next-steps) + +Let's get Spin and take it from nothing to a "hello world" application! + + + +## Install Spin + +{{ tabs "os" }} + +{{ startTab "Linux"}} + +Download the `spin` binary along with a starter set of templates and plugins using the `install.sh` script hosted on this site: + + + +
$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
+
+ +Then move the `spin` binary somewhere in your path, so you can run it from anywhere. For example: + + + +```bash +$ sudo mv ./spin /usr/local/bin/spin +``` + +{{ blockEnd }} + +{{ startTab "macOS"}} + +Download the `spin` binary along with a starter set of templates and plugins using the `install.sh` script hosted on this site: + + + +
$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
+
+ +Then move the `spin` binary somewhere in your path, so you can run it from anywhere. For example: + + + +```bash +$ sudo mv ./spin /usr/local/bin/spin +``` + +{{ blockEnd }} + +{{ startTab "Windows"}} + +Download the Windows binary release of Spin from GitHub. + +Unzip the binary release and place the `spin.exe` in your system path. + +{{ blockEnd }} +{{ blockEnd }} + +[See more options for installing Spin.](install) + +## Install the Prerequisites + +### Install a Template + +> If you used the installer script above, the templates are already installed, and you can skip this section! + +The quickest and most convenient way to start a new application is to install and use a Spin template for your preferred language. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --update +Copying remote template source +Installing template redis-rust... +Installing template http-rust... +... other templates omitted ... ++------------------------------------------------------------------------+ +| Name Description | ++========================================================================+ +| ... other templates omitted ... | +| http-rust HTTP request handler using Rust | +| redis-rust Redis message handler using Rust | +| ... other templates omitted ... | ++------------------------------------------------------------------------+ +``` + +Note: The Rust templates are in a repo that contains several other languages; they will all be installed together. + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-js-sdk --update +Copying remote template source +Installing template http-js... +Installing template http-ts... ++------------------------------------------------------------------------+ +| Name Description | ++========================================================================+ +| http-js HTTP request handler using Javascript | +| http-ts HTTP request handler using Typescript | ++------------------------------------------------------------------------+ +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --update +Copying remote template source +Installing template http-py... ++---------------------------------------------+ +| Name Description | ++=============================================+ +| http-py HTTP request handler using Python | ++---------------------------------------------+ +``` + +{{ blockEnd }} + +{{ startTab "TinyGo" }} + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --update +Copying remote template source +Installing template redis-go... +Installing template http-go... ++------------------------------------------------------------------------+ +| Name Description | ++========================================================================+ +| ... other templates omitted ... | +| http-go HTTP request handler using (Tiny)Go | +| redis-go Redis message handler using (Tiny)Go | +| ... other templates omitted ... | ++------------------------------------------------------------------------+ +``` + +Note: The Go templates are in a repo that contains several other languages; they will all be installed together. + +{{ blockEnd }} + +{{ blockEnd }} + +### Install the Tools + +Some languages require additional tool support for Wasm: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +You'll need the `wasm32-wasi` target for Rust: + + + +```bash +$ rustup target add wasm32-wasi +``` + +[Learn more in the language guide.](rust-components) + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + +You will need `npm` installed and on the path. `npm` will install any additional build tools as part of building the application. + +[Learn more in the language guide.](javascript-components) + +{{ blockEnd }} + +{{ startTab "Python" }} + +Ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + +You'll install all the required Python tools as part of building the application. We'll cover that in the Build Your Application section below. For now, there's nothing to do here! + +[Learn more in the language guide.](python-components) + +{{ blockEnd }} + +{{ startTab "TinyGo" }} + +You'll need the TinyGo compiler, as the standard Go compiler does not yet support WASI exports. See the [TinyGo installation guide](https://tinygo.org/getting-started/install/). + +[Learn more in the language guide.](go-components) + +{{ blockEnd }} + +{{ blockEnd }} + +## Create Your First Application + +{{suh_cards}} +{{card_element "sample" "Checklist Sample App" "A checklist app that persists data in a key value store" "https://developer.fermyon.com/hub/preview/sample_checklist" "Typescript,Http,Kv" true }} +{{card_element "sample" "AI-assisted News Summarizer" "Read an RSS newsfeed and have AI summarize it for you" "https://developer.fermyon.com/hub/preview/sample_newsreader_ai" "Typescript,Javascript,Ai" true }} +{{card_element "template" "Zola SSG Template" "A template for using Zola framework to create a static webpage" "https://developer.fermyon.com/hub/preview/template_zola_ssg" "rust" true }} +{{blockEnd}} + +Now you are ready to create your first Spin application: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +Use the `spin new` command and the `http-rust` template to scaffold a new Spin application: + + + +```bash +$ spin new +Pick a template to start your application with: + http-c (HTTP request handler using C and the Zig toolchain) + http-csharp (HTTP request handler using C# (EXPERIMENTAL)) + http-go (HTTP request handler using (Tiny)Go) + http-grain (HTTP request handler using Grain) +> http-rust (HTTP request handler using Rust) + http-swift (HTTP request handler using SwiftWasm) + http-zig (HTTP request handler using Zig) + redis-go (Redis message handler using (Tiny)Go) + redis-rust (Redis message handler using Rust) + +Enter a name for your new application: hello_rust +Project description: My first Rust Spin application +HTTP path: /... +``` + +This command created a directory with the necessary files needed to build and run a Rust Spin application. Change to that directory, and look at the files. It looks very much like a normal Rust library project: + + + +```bash +$ cd hello_rust +$ tree +. +├── .gitignore +├── Cargo.toml +├── spin.toml +└── src + └── lib.rs +``` + +The additional `spin.toml` file is the manifest file, which tells Spin what events should trigger what components. In this case our trigger is HTTP, for a Web application, and we have only one component, at the route `/...`. This is a wildcard that matches any route: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello_rust" +version = "0.1.0" +authors = ["Your Name "] +description = "My first Rust Spin application" + +[[trigger.http]] +route = "/..." +component = "hello-rust" + +[component.hello-rust] +source = "target/wasm32-wasi/release/hello_rust.wasm" +allowed_outbound_hosts = [] +[component.hello-rust.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +This represents a simple Spin HTTP application (triggered by an HTTP request). It has: + +* A single HTTP trigger, for the `/...` route, associated with the `hello-rust` component. `/...` is a wildcard, meaning it will match any route. When the application gets an HTTP request that matches this route - that is, any HTTP request at all! - Spin will run the `hello-rust` component. +* A single component called `hello-rust`, whose implementation is in the associated `hello_rust.wasm` WebAssembly component. When, in response to the HTTP trigger, Spin runs this component, it will execute the HTTP handler in `hello_rust.wasm`. (We're about to see the source code for that.) + +[Learn more about the manifest here.](./writing-apps) + +Now let's have a look at the code. Below is the complete source +code for a Spin HTTP component written in Rust — a regular Rust function that +takes an HTTP request as a parameter and returns an HTTP response, and it is +annotated with the `http_component` macro which identifies it as the entry point +for HTTP requests: + +```rust +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +fn handle_hello_rust(req: Request) -> anyhow::Result { + println!("Handling request to {:?}", req.header("spin-full-url")); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Fermyon") + .build()) +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +Use the `spin new` command and the `http-ts` template to scaffold a new Spin application. (If you prefer JavaScript to TypeScript, the `http-js` template is very similar.): + + + +```bash +$ spin new +Pick a template to start your application with: + http-js (HTTP request handler using Javascript) +> http-ts (HTTP request handler using Typescript) +Enter a name for your new application: hello_typescript +Project description: My first TypeScript Spin application +HTTP path: /... +``` + +This command created a directory with the necessary files needed to build and run a TypeScript Spin application. Change to that directory, and look at the files. It looks similar to a lot of NPM projects: + + + +```bash +$ cd hello_typescript +$ tree +. +├── package.json +├── README.md +├── spin.toml +├── src +│   └── index.ts +├── tsconfig.json +└── webpack.config.js +``` + +The additional `spin.toml` file is the manifest file, which tells Spin what events should trigger what components. In this case our trigger is HTTP, for a Web application, and we have only one component, at the route `/...`. This is a wildcard that matches any route: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello_typescript" +version = "0.1.0" +authors = ["Your Name "] +description = "My first TypeScript Spin application" + +[[trigger.http]] +route = "/..." +component = "hello-typescript" + +[component.hello-typescript] +source = "target/hello-typescript.wasm" +exclude_files = ["**/node_modules"] +[component.hello-typescript.build] +command = "npm run build" +``` + +This represents a simple Spin HTTP application (triggered by an HTTP request). It has: + +* A single HTTP trigger, for the `/...` route, associated with the `hello-typescript` component. `/...` is a wildcard, meaning it will match any route. When the application gets an HTTP request that matches this route - that is, any HTTP request at all! - Spin will run the `hello-typescript` component. +* A single component called `hello-typescript`, whose implementation is in the associated `hello-typescript.wasm` WebAssembly component. When, in response to the HTTP trigger, Spin runs this component, it will execute the HTTP handler in `hello-typescript.wasm`. (We're about to see the source code for that.) + +[Learn more about the manifest here.](./writing-apps) + +Now let's have a look at the code. Below is the complete source +code for a Spin HTTP component written in TypeScript — a regular function named `handleRequest` that +takes an HTTP request as a parameter and returns an HTTP response. (The +JavaScript version looks slightly different, but is still a function with +the same signature.) The Spin integration looks for the `handler` function +by name when building your application into a Wasm module: + +```javascript +import { ResponseBuilder } from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + console.log(req); + res.send("hello universe"); +} +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +You can install the Spin template for Python HTTP handlers from the [spin-python-sdk repository](https://github.com/fermyon/spin-python-sdk) using the following command: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --update +``` + +The above command will install the `http-py` template and produce an output similar to the following: + + + +```text +Copying remote template source +Installing template http-py... +Installed 1 template(s) + ++---------------------------------------------+ +| Name Description | ++=============================================+ +| http-py HTTP request handler using Python | ++---------------------------------------------+ +``` + +**Please note:** For more information about managing Spin templates, see the [templates guide](./managing-templates). + +This command created a directory with the necessary files needed to build and run a Python Spin application. Change to that directory, and look at the files. It contains a minimal Python application: + + + +```bash +$ spin new -t http-py hello-python --accept-defaults +``` + +Once the component is created, we can change into the `hello-python` directory, create and activate a virtual environment and then install the component's requirements: + + + +```console +$ cd hello-python +``` + +Create a virtual environment directory (we are still inside the Spin app directory): + + + +```console +# python -m venv +$ python3 -m venv venv-dir +``` + +Activate the virtual environment (this command depends on which operating system you are using): + + + +```console +# macOS & Linux command to activate +$ source venv-dir/bin/activate +``` + +If you are using Windows, use the following commands: + +```bash +C:\Work> python3 -m venv venv +C:\Work> venv\Scripts\activate +``` + +The `(venv-dir)` will prefix your terminal prompt now: + + + +```console +(venv-dir) user@123-456-7-8 hello-python % +``` + +The `requirements.txt`, by default, contains the references to the `spin-sdk` and `componentize-py` packages. These can be installed in your virtual environment using the following command: + + + +```bash +$ pip3 install -r requirements.txt +Collecting spin-sdk==3.1.0 (from -r requirements.txt (line 1)) + Using cached spin_sdk-3.1.0-py3-none-any.whl.metadata (16 kB) +Collecting componentize-py==0.13.3 (from -r requirements.txt (line 2)) + Using cached componentize_py-0.13.3-cp37-abi3-macosx_10_12_x86_64.whl.metadata (3.4 kB) +Using cached spin_sdk-3.1.0-py3-none-any.whl (94 kB) +Using cached componentize_py-0.13.3-cp37-abi3-macosx_10_12_x86_64.whl (38.8 MB) +Installing collected packages: spin-sdk, componentize-py +Successfully installed componentize-py-0.13.3 spin-sdk-3.1.0 +``` + +## Structure of a Python Component + +The `hello-python` directory structure created by the Spin `http-py` template is shown below: + + + +```text +├── app.py +├── spin.toml +└── requirements.txt +``` + +The additional `spin.toml` file is the manifest file, which tells Spin what events should trigger what components. In this case our trigger is HTTP, for a Web application, and we have only one component, at the route `/...`. This is a wildcard that matches any route. + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello-python" +version = "0.1.0" +authors = ["Your Name "] +description = "My first Python Spin application" + +[[trigger.http]] +route = "/..." +component = "hello-python" + +[component.hello-python] +source = "app.wasm" +[component.hello-python.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +``` +This represents a simple Spin HTTP application (triggered by an HTTP request). It has: + +* A single HTTP trigger, for the `/...` route, associated with the `hello-python` component. `/...` is a wildcard, meaning it will match any route. When the application gets an HTTP request that matches this route - that is, any HTTP request at all! - Spin will run the `hello-python` component. +* A single component called `hello-python`, whose implementation is in the associated `app.wasm` WebAssembly component. When, in response to the HTTP trigger, Spin runs this component, it will execute the HTTP handler in `app.wasm`. (We're about to see the source code for that.) + +[Learn more about the manifest here.](./writing-apps) + +Now let's have a look at the code. Below is the complete source +code for a Spin HTTP component written in Python — a regular function named `handle_request` that +takes an HTTP request as a parameter and returns an HTTP response. + + + +```python +from spin_sdk.http import IncomingHandler, Request, Response + +class IncomingHandler(IncomingHandler): + def handle_request(self, request: Request) -> Response: + return Response( + 200, + {"content-type": "text/plain"}, + bytes("Hello from the Python SDK!", "utf-8") + ) +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +Use the `spin new` command and the `http-go` template to scaffold a new Spin application. + + + +```bash +$ spin new +Pick a template to start your application with: + http-c (HTTP request handler using C and the Zig toolchain) + http-empty (HTTP application with no components) +> http-go (HTTP request handler using (Tiny)Go) + http-grain (HTTP request handler using Grain) + http-php (HTTP request handler using PHP) + http-rust (HTTP request handler using Rust) +Enter a name for your new application: hello_go +Description: My first Go Spin application +HTTP path: /... +``` + +This command created a directory with the necessary files needed to build and run a Go Spin application using the TinyGo compiler. Change to that directory, and look at the files. It looks very much like a normal Go project: + + + +```bash +$ cd hello_go +$ tree +. +├── go.mod +├── go.sum +├── main.go +└── spin.toml +``` + +The additional `spin.toml` file is the manifest file, which tells Spin what events should trigger what components. In this case our trigger is HTTP, for a Web application, and we have only one component, at the route `/...`. This is a wildcard that matches any route. + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello_go" +version = "0.1.0" +authors = ["Your Name "] +description = "My first Go Spin application" + +[[trigger.http]] +route = "/..." +component = "hello-go" + +[component.hello-go] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.hello-go.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +``` + +This represents a simple Spin HTTP application (triggered by an HTTP request). It has: + +* A single HTTP trigger, for the `/...` route, associated with the `hello-go` component. `/...` is a wildcard, meaning it will match any route. When the application gets an HTTP request that matches this route - that is, any HTTP request at all! - Spin will run the `hello-go` component. +* A single component called `hello-go`, whose implementation is in the associated `main.wasm` WebAssembly component. When, in response to the HTTP trigger, Spin runs this component, it will execute the HTTP handler in `main.wasm`. (We're about to see the source code for that.) + +[Learn more about the manifest here.](./writing-apps) + +Now let's have a look at the code. Below is the complete source +code for a Spin HTTP component written in Go. Notice where the work is done. The +`main` function is empty (and Spin never calls it). Instead, the `init` function +sets up a callback, and passes that callback to `spinhttp.Handle` to register it as +the handler for HTTP requests. You can learn more about this structure +in the [Go language guide](go-components). + +```go +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "Hello Fermyon!") + }) +} + +func main() {} +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Build Your Application + +The Spin template creates starter source code. Now you need to turn that into a Wasm module. The template puts build instructions for each component into the manifest. Use the `spin build` command to run them: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +```bash +$ spin build +Executing the build command for component hello-rust: cargo build --target wasm32-wasi --release + Updating crates.io index + Updating git repository `https://github.com/fermyon/spin` + Updating git repository `https://github.com/bytecodealliance/wit-bindgen` + Compiling anyhow v1.0.69 + Compiling version_check v0.9.4 + # ... + Compiling spin-sdk v0.10.0 + Compiling hello-rust v0.1.0 (/home/ivan/testing/start/hello_rust) + Finished release [optimized] target(s) in 11.94s +Finished building all Spin components +``` + +If the build fails, check: + +* Are you in the `hello_rust` directory? +* Did you successfully [install the `wasm32-wasi` target](#install-the-tools)? +* Is your version of Rust up to date (`cargo --version`)? The Spin SDK needs Rust 1.64 or above. + +If you would like to know what build command Spin runs for a component, you can find it in the manifest, in the `component.(id).build` section: + +```toml +[component.hello-rust.build] +command = "cargo build --target wasm32-wasi --release" +``` + +You can always run this command manually; `spin build` is a shortcut to save you having to remember it. + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +As normal for NPM projects, before you build for the first time, you must run `npm install`: + + + +```bash +$ npm install + +added 141 packages, and audited 142 packages in 13s + +20 packages are looking for funding + run `npm fund` for details + +found 0 vulnerabilities +``` + +Then run `spin build`: + + + +```bash +$ spin build +Executing the build command for component hello-typescript: npm run build + +> hello-typescript@1.0.0 build +> npx webpack --mode=production && npx mkdirp target && npx j2w -i dist.js -d combined-wit -n combined -o target/hello-typescript.wasm + +asset spin.js 4.57 KiB [emitted] (name: main) +runtime modules 670 bytes 3 modules +./src/index.ts 2.85 KiB [built] [code generated] +webpack 5.75.0 compiled successfully in 1026 ms + +Starting to build Spin compatible module +Preinitiating using Wizer +Optimizing wasm binary using wasm-opt +Spin compatible module built successfully +Finished building all Spin components +``` + +If the build fails, check: + +* Are you in the `hello_typescript` directory? +* Did you run `npm install` before building`? + +If you would like to know what build command Spin runs for a component, you can find it in the manifest, in the `component.(id).build` section: + +```toml +[component.hello-typescript.build] +command = "npm run build" +``` + +You can always run this command manually; `spin build` is a shortcut. + +{{ blockEnd }} + +{{ startTab "Python"}} + +As a standard practice for Python, create and activate a virtual env: + +If you are on a Mac/linux based operating system use the following commands: + +```bash +$ python3 -m venv venv +$ source venv/bin/activate +``` + +If you are using Windows, use the following commands: + +```bash +C:\Work> python3 -m venv venv +C:\Work> venv\Scripts\activate +``` + +Install `componentize-py` and `spin-sdk` packages + + + +```bash +$ pip3 install -r requirements.txt +``` + +Then run: + + + +```bash +$ spin build +Executing the build command for component hello-python: "componentize-py -w spin-http componentize app -o app.wasm" +Finished building all Spin components +``` + +If the build fails, check: + +* Are you in the `hello-python` directory? +* Did you install the requirements? +* Is your virtual environment still activated? + +If you would like to know what build command Spin runs for a component, you can find it in the manifest, in the `component.(id).build` section: + +```toml +[component.hello-python.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +``` + +You can always run this command manually; `spin build` is a shortcut. + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + + + +```bash +$ spin build +Executing the build command for component hello-go: tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go +go: downloading github.com/fermyon/spin/sdk/go v0.10.0 +Finished building all Spin components +``` + +If the build fails, check: + +* Are you in the `hello_go` directory? +* Did you successfully [install TinyGo](#install-the-tools)? +* Are your versions of Go and TinyGo up to date? The Spin SDK needs TinyGo 0.27 or above. +* Set Environment Variable `CGO_ENABLED=1`. (Since the Go SDK is built using CGO, it requires the CGO_ENABLED=1 environment variable to be set.) + +If you would like to know what build command Spin runs for a component, you can find it in the manifest, in the `component.(id).build` section: + +```toml +[component.hello-go.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +``` + +You can always run this command manually; `spin build` is a shortcut to save you having to remember it. + +{{ blockEnd }} + +{{ blockEnd }} + +> `spin build` can be used to build all components defined in the Spin manifest +> file at the same time, and also has a flag that starts the application after +> finishing the compilation, `spin build --up`. +> +> For more details, see the [page about building Spin applications](./build.md). + +## Run Your Application + +Now that you have created the application and built the component, you can _spin up_ +the application (pun intended): + + + +```bash +$ spin up +Serving http://127.0.0.1:3000 +Available Routes: + hello-typescript: http://127.0.0.1:3000 (wildcard) +``` + +If you would like to see what Spin is doing under the hood, set the RUST_LOG environment variable for detailed logs, before running `spin up`: + + + +```bash +$ export RUST_LOG=spin=trace +``` + +> The variable is `RUST_LOG` no matter what language your application is written in, because this is setting the log level for Spin itself. + +Spin instantiates all components from the application manifest, and +creates the router configuration for the HTTP trigger according to the routes in the manifest. The +component can now be invoked by making requests to `http://localhost:3000/` +(or any path under that, since it's a wildcard): + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +content-type: text/plain +content-length: 14 +date = "2023-11-04T00:00:01Z" + +Hello, Fermyon +``` + +> The `curl` output may vary based on which language SDK you use. + +Congratulations! You just created, built and ran your first Spin application! + +## Deploy Your Application to Fermyon Cloud + +`spin up` runs your application locally. Now it's time to put it on the Web via [Fermyon Cloud](https://cloud.fermyon.com). + +> Fermyon Cloud's Starter tier is free, and doesn't require you to enter any kind of payment instrument. You only need a [GitHub account](https://docs.github.com/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/remembering-your-github-username-or-email). + +### Log in to Fermyon Cloud + +Before deploying your application to Fermyon Cloud, you have to log in, using the `spin login` command. This generates a code to authorize your current device against the Fermyon Cloud, and prints a link that will take you to where you enter the code. (You will need to be logged into your GitHub account; if you're not, it will prompt you to log in.) Follow the instructions in the prompt to complete the authorization process. + +`spin login` prints a confirmation message when authorization completes: + + + +```bash +$ spin login + +Copy your one-time code: + +XXXXXXXX + +...and open the authorization page in your browser: + +https://cloud.fermyon.com/device-authorization + +Waiting for device authorization... +Device authorized! +``` + +### Deploy the Application + +Now let's deploy the application: + + + +```bash +$ spin deploy +``` + +The deployment process prints progress information as your application uploads and is rolled out to the cloud: + + + +```console +Uploading hello_typescript version 0.1.0+XXXXXXXX to Fermyon Cloud... +Deploying... +Waiting for application to become ready... ready +Available Routes: + hello-typescript: https://hello-typescript-XXXXXXXX.fermyon.app (wildcard) +``` + +You can Ctrl+Click on the link in the terminal to visit the web application you just deployed. + +> In the example output above, `hello_typescript` is a placeholder for your application name - you'll see whatever you entered as the application name when you ran `spin new` earlier. The `XXXXXXXX` fragment is randomly generated to make a unique URL. + +Congratulations again - you've now deployed your first Spin application to [Fermyon Cloud](https://cloud.fermyon.com)! + +## Next Steps + +- Learn more about [writing Spin components and manifests](writing-apps) +- Learn how to [build your Spin application code](build) +- Learn more about [Fermyon Cloud](/cloud/index) diff --git a/content/spin/v3/rdbms-storage.md b/content/spin/v3/rdbms-storage.md new file mode 100644 index 000000000..833ac0e61 --- /dev/null +++ b/content/spin/v3/rdbms-storage.md @@ -0,0 +1,209 @@ +title = "Relational Database Storage" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/rdbms-storage.md" + +--- +- [Using MySQL and PostgreSQL From Applications](#using-mysql-and-postgresql-from-applications) +- [Granting Network Permissions to Components](#granting-network-permissions-to-components) + +Spin provides two interfaces for relational (SQL) databases: + +* A built-in [SQLite Database](./sqlite-api-guide), which is always available and requires no management on your part. +* "Bring your own database" support for MySQL and PostgreSQL, where you host and manage the database outside of Spin. + +This page covers the "bring your own database" scenario. See [SQLite Storage](./sqlite-api-guide) for the built-in service. + +{{ details "Why do I need a Spin interface? Why can't I just use my language's database libraries?" "The current version of the WebAssembly System Interface (WASI) doesn't provide a sockets interface, so database libraries that depend on sockets can't be built to Wasm. The Spin interface means Wasm modules can bypass this limitation by asking Spin to make the database connection on their behalf." }} + +## Using MySQL and PostgreSQL From Applications + +The Spin SDK surfaces the Spin MySQL and PostgreSQL interfaces to your language. The set of operations is the same across both databases: + +| Operation | Parameters | Returns | Behavior | +|------------|----------------------------|---------------------|----------| +| `open` | address | connection resource | Opens a connection to the specified database. The host must be listed in `allowed_outbound_hosts`. Other operations must be called through a connection. | +| `query` | statement, SQL parameters | database records | Runs the specified statement against the database, returning the query results as a set of rows. | +| `execute` | statement, SQL parameters | integer (not MySQL) | Runs the specified statement against the database, returning the number of rows modified by the statement. (MySQL does not return the modified row count.) | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/index.html) + +MySQL functions are available in the `spin_sdk::mysql` module, and PostgreSQL functions in the `spin_sdk::pg` module. The function names match the operations above. This example shows MySQL: + +```rust +use spin_sdk::mysql::{self, Connection, Decode, ParameterValue}; + +let connection = Connection::open(&address)?; + +let params = vec![ParameterValue::Int32(id)]; +let rowset = connection.query("SELECT id, name FROM pets WHERE id = ?", ¶ms)?; + +match rowset.rows.first() { + None => /* no rows matched query */, + Some(row) => { + let name = String::decode(&row[1])?; + } +} +``` + +**Notes** + +* Parameters are instances of the `ParameterValue` enum; you must wrap raw values in this type. +* A row is a vector of the `DbValue` enum. Use the `Decode` trait to access conversions to common types. +* Using PostgreSQL works in the same way, except that you `use` the `spin_sdk::pg` module instead of `spin_sdk::mysql`. +* Modified row counts are returned as `u64`. (MySQL `execute` does not return the modified row count.) +* All functions wrap the return in `anyhow::Result`. + +You can find complete examples for using relational databases in the Spin repository on GitHub ([MySQL](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/mysql), [PostgreSQL](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/postgres)). + +For full information about the MySQL and PostgreSQL APIs, see [the Spin SDK reference documentation](https://docs.rs/spin-sdk/latest/spin_sdk/index.html). + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/) + +The code below is an [Outbound MySQL example](https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-mysql). There is also an outbound [PostgreSQL example](https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-postgres) available. + +```ts +import { ResponseBuilder, Mysql } from '@fermyon/spin-sdk'; + +// Connects as the root user without a password +const DB_URL = "mysql://root:@127.0.0.1/spin_dev" + +/* + Run the following commands to setup the instance: + create database spin_dev; + use spin_dev; + create table test(id int, val int); + insert into test values (4,4); +*/ + +export async function handler(_req: Request, res: ResponseBuilder) { + let conn = Mysql.open(DB_URL); + conn.execute('delete from test where id=?', [4]); + conn.execute('insert into test values (4,5)', []); + let ret = conn.query('select * from test', []); + // return a object that looks like + // { "columns": [{name: "id", dataType: "int32"}], "rows": [{ "id": 4, "val": 5 }] } + res.send(JSON.stringify(ret, null, 2)); +} +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/) + +The code below is an [Outbound MySQL example](https://github.com/fermyon/spin-python-sdk/tree/main/examples/spin-mysql). There is also an outbound [PostgreSQL example](https://github.com/fermyon/spin-python-sdk/tree/main/examples/spin-postgres) available. + +```python +from spin_sdk import http +from spin_sdk.http import Request, Response +from spin_sdk import mysql + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + with mysql.open("mysql://root:@127.0.0.1/spin_dev") as db: + print(db.query("select * from test", [])) + + return Response( + 200, + {"content-type": "text/plain"}, + bytes("Hello from Python!", "utf-8") + ) +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2) + +MySQL functions are available in the `github.com/fermyon/spin/sdk/go/v2/mysql` package, and PostgreSQL in `github.com/fermyon/spin/sdk/go/v2/pg`. [See Go Packages for reference documentation.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2) + +The package follows the usual Go database API. Use `Open` to return a connection to the database of type `*sql.DB` - see the [Go standard library documentation](https://pkg.go.dev/database/sql#DB) for usage information. For example: + +```go +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/pg" +) + +type Pet struct { + ID int64 + Name string + Prey *string // nullable field must be a pointer + IsFinicky bool +} + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + + // addr is the environment variable set in `spin.toml` that points to the + // address of the Mysql server. + addr := os.Getenv("DB_URL") + + db := pg.Open(addr) + defer db.Close() + + _, err := db.Query("INSERT INTO pets VALUES ($1, 'Maya', $2, $3);", int32(4), "bananas", true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + rows, err := db.Query("SELECT * FROM pets") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var pets []*Pet + for rows.Next() { + var pet Pet + if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil { + fmt.Println(err) + } + pets = append(pets, &pet) + } + json.NewEncoder(w).Encode(pets) + }) +} + +func main() {} +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Granting Network Permissions to Components + +By default, Spin components are not allowed to make outgoing network requests, including MySQL or PostgreSQL. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. To grant a component permission to make network requests to a particular host, use the `allowed_outbound_hosts` field in the component manifest, specifying the host and allowed port: + +```toml +[component.uses-db] +allowed_outbound_hosts = ["postgres://postgres.example.com:5432"] +``` + +### Configuration-Based Permissions + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `allowed_outbound_hosts` field. However, this feature is not yet available on Fermyon Cloud. diff --git a/content/spin/v3/redis-outbound.md b/content/spin/v3/redis-outbound.md new file mode 100644 index 000000000..d73d42319 --- /dev/null +++ b/content/spin/v3/redis-outbound.md @@ -0,0 +1,184 @@ +title = "Redis Storage" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/redis-outbound.md" + +--- +- [Using Redis From Applications](#using-redis-from-applications) +- [Granting Network Permissions to Components](#granting-network-permissions-to-components) + - [Configuration-Based Permissions](#configuration-based-permissions) + +Spin provides an interface for you to read and write the Redis key/value store, and to publish Redis pub-sub messages. + +{{ details "Why do I need a Spin interface? Why can't I just use my language's Redis library?" "The current version of the WebAssembly System Interface (WASI) doesn't provide a sockets interface, so Redis libraries that depend on sockets can't be built to Wasm. The Spin interface means Wasm modules can bypass this limitation by asking Spin to make the Redis connection on their behalf." }} + +## Using Redis From Applications + +The Spin SDK surfaces the Spin Redis interface to your language. The set of operations is common across all SDKs: + +| Operation | Parameters | Returns | Behavior | +|--------------|---------------------|---------|----------| +| `open` | address | connection resource | Opens a connection to the specified Redis instance. The host must be listed in `allowed_outbound_hosts`. Other operations must be called through a connection. (Exception: for JavaScript, do not call `open`, and pass the address to each data operation - see the language guides below.) | +| Single value operations | +| `get` | key | bytes | Returns the value of `key`. If the key does not exist, returns a zero-length array. | +| `set` | key, bytes | - | Sets the value of `key`, overwriting any existing value. | +| `incr` | key | integer | Increments the value at `key` by 1. If the key does not exist, its value is set to 0 first (and immediately incremented to 1). If the current value isn't an integer, or a string that represents an integer, it errors and the value is not changed. | +| `del` | list of keys | - | Removes the specified keys. Keys that don't exist are ignored (they do _not_ cause an error). | +| Set value operations | +| `sadd` | key, list of strings | integer | Adds the strings to the set of values of `key`, and returns the number of newly added values. If the key does not exist, its value is set to the set of values | +| `smembers` | key | list of strings | Returns the set of values of `key`. if the key does not exist, this is an empty set. | +| `srem` | key, list of strings | integer | Removes the strings from the set of values of `key`, and returns the number of newly removed values. If the key does not exist, this does nothing. | +| Pub-sub operations | +| `publish` | channel, bytes | - | Publishes a message to the specified channel, with the specified payload bytes. | +| General operations | +| `execute` | command, list of argument values | list of results | Executes the specified command with the specified arguments. This is a general-purpose 'escape hatch' if you need a command that isn't covered by the built-in operations. | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/redis/index.html) + +Redis functions are available in the `spin_sdk::redis` module. + +To access a Redis instance, use the `Connection::open` function. + +```rust +let connection = spin_sdk::redis::Connection::open(&address)?; +``` + +You can then call functions on the `Connection` to work with the Redis instance: + +```rust +connection.set("my-key", &"my-value".into()); +let data = connection.get("my-key")?; +``` + +For full details of the Redis API, see the [Spin SDK reference documentation](https://docs.rs/spin-sdk/latest/spin_sdk/redis/index.html); + +**General Notes** + +* Keys are of type `&str`. +* Bytes parameters are of type `&[u8]` and return values are `Vec`. +* Numeric return values are of type `i64`. +* All functions wrap the return in `anyhow::Result`. + +**`get` Operation** + +* This returns a `Result>>`. If the key is not found, the return value is `Ok(None)`. + +**`del` Operation** + +* The list of keys is passed as `&[String]`. + +**Set Operations** + +* List arguments are passed as `&[String]` and returned as `Vec`. + +**`execute` Operation** + +* The arguments and results are enums, representing integers, binary payloads, and (for results) status and nil values. + +You can find a complete Rust code example for using outbound Redis from an HTTP component in the [Spin repository on GitHub](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/redis-outbound). Please also see this, related, [outbound Redis (using Rust) section](./rust-components#storing-data-in-redis-from-rust-components). + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/) + +Redis functions are available on [the `Redis` module](https://fermyon.github.io/spin-js-sdk/modules/Redis.html). The function names match the operations above. For example: + +```javascript +import { Redis } from "@fermyon/spin-sdk" + +let db = Redis.open("redis://localhost:6379") +let value = db.get(key); +``` + +**General Notes** +* Key parameters are strings. +* Bytes parameters and return values are buffers (TypeScript `Uint8Array`). +* Lists are passed and returned as arrays. + +**`execute` Operation** + +* The arguments and results can be either numbers or buffers. (In TypeScript they are union types, e.g. `BigInt | Uint8Array`.) + +You can find a complete TypeScript example for using outbound Redis from an HTTP component in the [JavaScript SDK repository on GitHub](https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-redis). Please also see this, related, [outbound Redis (using TypeScript) section](./javascript-components#storing-data-in-redis-from-jsts-components). + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/redis.html) + +Redis functions are available in [the `redis` module](https://fermyon.github.io/spin-python-sdk/redis.html). The function names are prefixed `redis_`. You must pass the Redis instance address to _each_ operation as its first parameter. For example: + +```python +from spin_sdk import redis +with redis.open("redis://localhost:6379") as db: + val = db.get("test") +``` + +**General Notes** + +* Address and key parameters are strings (`str`). +* Bytes parameters and return values are `bytes`. (You can pass literal strings using the `b` prefix, e.g. `redis_set(address, key, b"hello")`.) +* Numeric return values are of type `int64`. +* Lists are passed and returned as Python lists. +* Errors are signalled through exceptions. + +You can find a complete Python code example for using outbound Redis from an HTTP component in the [Python SDK repository on GitHub](https://github.com/fermyon/spin-python-sdk/tree/main/examples/spin-redis). Please also see this, related, [outbound Redis (using Python) section](./python-components#an-outbound-redis-example). + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/redis) + +Redis functions are available in the `github.com/fermyon/spin/sdk/go/v2/redis` package. [See Go Packages for reference documentation.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2/redis) The function names are TitleCased. For example: + +```go +import ( + "github.com/fermyon/spin/sdk/go/v2/redis" +) + +rdb := redis.NewClient(addr) +payload, err := rdb.Get(key) +``` + +**General Notes** + +* Key parameters are strings. +* Bytes parameters are byte slices (`[]byte`). +* Lists are passed as slices. For example, `redis.Del` takes the keys to delete as a `[]string`. +* Errors are return through the usual Go multiple return values mechanism. + +**`execute` Operation** + +* The arguments are passed as `[]redis.RedisParameter`. You can construct `RedisParameter` instances around an `interface{}` but must provide a `Kind`. For example, `hello := redis.RedisParameter{Kind: redis.RedisParameterKindBinary, Val: []byte("hello")}`. +* The results are returned as `[]redis.Result`. You can use the `Kind` member of `redis.Result` to interpret the `Val`. + +You can find a complete TinyGo example for using outbound Redis from an HTTP component in the [Spin repository on GitHub](https://github.com/fermyon/spin-go-sdk/tree/main/examples/redis-outbound). Please also see this, related, [outbound Redis (using TinyGo) section](./go-components#storing-data-in-redis-from-go-components). + +{{ blockEnd }} + +{{ blockEnd }} + +## Granting Network Permissions to Components + +By default, Spin components are not allowed to make outgoing network requests, including Redis. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. To grant a component permission to make network requests to a particular host, use the `allowed_outbound_hosts` field in the component manifest, specifying the host and allowed port: + +```toml +[component.uses-redis] +allowed_outbound_hosts = ["redis://redis.example.com:6379"] +``` + +### Configuration-Based Permissions + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `allowed_outbound_hosts` field. However, this feature is not yet available on Fermyon Cloud. diff --git a/content/spin/v3/redis-trigger.md b/content/spin/v3/redis-trigger.md new file mode 100644 index 000000000..c00ed0b2b --- /dev/null +++ b/content/spin/v3/redis-trigger.md @@ -0,0 +1,169 @@ +title = "The Spin Redis trigger" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/redis-trigger.md" + +--- +- [Specifying a Redis Trigger](#specifying-a-redis-trigger) + - [Setting a Default Server](#setting-a-default-server) +- [Redis Trigger Authentication](#redis-trigger-authentication) +- [Redis Components](#redis-components) + - [The Message Handler](#the-message-handler) +- [Inside Redis Components](#inside-redis-components) + +Pub-sub (publish-subscribe) messaging is a popular architecture for asynchronous message processing. Spin has built-in support to creating and running applications in response to messages on [pub-sub Redis channels](https://redis.io/topics/pubsub). + +The Redis trigger in Spin subscribes to messages from a given Redis instance, and dispatches those messages to components for handling. + +> This page deals with the Redis trigger for subscribing to pub-sub messages. For information about reading and writing the Redis key-value store, or for publishing messages, see the Language Guides. + +> The Redis trigger is not yet available in Fermyon Cloud. + +## Specifying a Redis Trigger + +A Redis trigger maps a Redis channel to a component. For example: + +```toml +[[trigger.redis]] +address = "redis://notifications.example.com:6379" # the Redis instance that the trigger subscribes to (optional - see below) +channel = "messages" # the channel that the trigger subscribes to +component = "my-application" # the name of the component to handle this route +``` + +Such a trigger says that Redis messages on the specified _channel_ should be handled by the specified _component_. The `component` field works the same way across all triggers - see [Triggers](triggers) for the details. + +> Spin subscribes only to the channels that are mapped to components. Other channels are ignored. If multiple components subscribe to the same channel, a message on that channel will activate all of the components. + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `address` and `channel` fields. + +### Setting a Default Server + +In many applications, all components listen to the same Redis server (on different channels, of course). For this case, it is more convenient to specify the server at the application level instead of on each component. This is done via the `[application.trigger.redis]` section of manifest: + +```toml +[application.trigger.redis] +address = "redis://notifications.example.com:6379" +``` + +> If you create an application from a Redis template, the trigger will be already set up for you. + +You can use [application variables](./variables.md#adding-variables-to-your-applications) in the `address` field. + +## Redis Trigger Authentication + +By default, Spin does not authenticate to Redis. You can work around this by providing a password in the `redis://` URL. For example: `address = "redis://:p4ssw0rd@localhost:6379"` + +> Do not use passwords in code committed to version control systems. + +> We plan to offer secrets-based authentication in future versions of Spin. + +As mentioned above, you can use [application variables](./variables.md#adding-variables-to-your-applications) in Redis `address` fields. This can be particularly useful for credentials, allowing you to pass credentials in via [variables providers](./dynamic-configuration.md#application-variables-runtime-configuration) rather than including them in `spin.toml`. + +## Redis Components + +Spin runs Redis components using the [WebAssembly component model](https://component-model.bytecodealliance.org/). In this model, the Wasm module exports a well-known interface that Spin calls to handle the Redis message. + +### The Message Handler + +The exact signature of the Redis handler, and how a function is identified to be exported as the handler, will depend on your language. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/index.html) + +In Rust, the handler is identified by the `#[spin_sdk::redis_component]` attribute. It takes a `bytes::Bytes`, representing the raw payload of the Redis message, and returns an `anyhow::Result` indicating success or an error with details. This example just logs the payload as a string: + +```rust +use anyhow::Result; +use bytes::Bytes; +use spin_sdk::redis_component; +use std::str::from_utf8; + +/// A simple Spin Redis component. +#[redis_component] +fn on_message(message: Bytes) -> Result<()> { + println!("{}", from_utf8(&message)?); + Ok(()) +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +The JavaScript/TypeScript SDK doesn't currently support Redis components. Please [let us know](https://github.com/fermyon/spin-js-sdk/issues) if this is important to you. + +{{ blockEnd }} + +{{ startTab "Python"}} + +In Python, the handler needs to implement the [`InboundRedis`](https://fermyon.github.io/spin-python-sdk/wit/exports/index.html#spin_sdk.wit.exports.InboundRedis) class, and override the `handle_message` method: + +```python +from spin_sdk.wit import exports +class InboundRedis(exports.InboundRedis): + def handle_message(self, message: bytes): + print(message) +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/redis) + +In Go, you register the handler as a callback in your program's `init` function. Call `redis.Handle` (from the Spin SDK `redis` package), passing your handler as the sole argument. Your handler takes a single byte slice (`[]byte`) argument, and may return an error or `nil`. + +> The do-nothing `main` function is required by TinyGo but is not used; the action happens in the `init` function and handler callback. + +This example just logs the payload as a string: + +```go +package main + +import ( + "fmt" + + "github.com/fermyon/spin/sdk/go/v2/redis" +) + +func init() { + redis.Handle(func(payload []byte) error { + fmt.Println(string(payload)) + return nil + }) +} + +func main() {} +``` + +{{ blockEnd }} + +{{ blockEnd }} + +## Inside Redis Components + +For the most part, you'll build Redis component modules using a language SDK (see the Language Guides section), such as a Rust crate or Go package. If you're interested in what happens inside the SDK, or want to implement Redis components in another language, read on! + +The Redis component interface is defined using a WebAssembly Interface (WIT) file. ([Learn more about the WIT language here.](https://component-model.bytecodealliance.org/design/wit.html)). You can find the latest WITs for Spin Redis components at [https://github.com/fermyon/spin/tree/main/wit](https://github.com/fermyon/spin/tree/main/wit). + +In particular, the entry point for Spin Redis components is defined in [the `inbound-redis` interface](https://github.com/fermyon/spin/blob/main/wit/deps/spin%40unversioned/inbound-redis.wit): + + + +```fsharp +interface inbound-redis { + use redis-types.{payload, error} + + // The entrypoint for a Redis handler. + handle-message: func(message: payload) -> result<_, error> +} +``` + +This is the interface that all Redis components must implement, and +which is used by Spin when instantiating and invoking the component. +However, it is implemented internally by the Spin SDK - you don't need to implement it directly. diff --git a/content/spin/v3/registry-tutorial.md b/content/spin/v3/registry-tutorial.md new file mode 100644 index 000000000..266f89212 --- /dev/null +++ b/content/spin/v3/registry-tutorial.md @@ -0,0 +1,132 @@ +title = "Spin Apps in Registries" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/registry-tutorial.md" + +--- +- [Spin Registry Support](#spin-registry-support) +- [Prerequisites](#prerequisites) +- [Publishing and Running Spin Applications Using Registries (Video)](#publishing-and-running-spin-applications-using-registries-video) +- [Set Up Your GHCR Instance](#set-up-your-ghcr-instance) +- [Push a Spin App to GHCR](#push-a-spin-app-to-ghcr) +- [Pull a Spin App From GHCR](#pull-a-spin-app-from-ghcr) +- [Run a Spin App From GHCR](#run-a-spin-app-from-ghcr) +- [Deploy a Spin App from GHCR (or any registry)](#deploy-a-spin-app-from-ghcr-or-any-registry) +- [Conclusion](#conclusion) +- [Next Steps](#next-steps) + +## Spin Registry Support + +With Spin's registry support, you can package and save your Spin application as an artifact in a registry like [GitHub Container Registry (GHCR)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) or [DockerHub](https://hub.docker.com/) and then run your Spin app from these registries. + +## Prerequisites + +First, follow [this guide](./install) to ensure you have the latest version of Spin installed (this tutorial refers to Spin 1.0 and above). You can check the Spin version using the following command: + + + +```bash +$ spin --version +``` + +## Publishing and Running Spin Applications Using Registries (Video) + +The following video shows you how to push a Spin app to GHCR, and then run that artifact with Spin or with Docker. The video also contains additional information about signing and verifying your GHCR artifacts. + + + +The rest of this page shows you how to use GHCR artifacts locally with Spin. + +## Set Up Your GHCR Instance + +To use a GHCR instance, you need to set up authentication. Follow [these steps](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic) to generate a personal access token and then use it to sign into your GHCR. You should see the following message to confirm your login attempt to GHCR was successful: + + + +```bash +> Login Succeeded +``` + +## Push a Spin App to GHCR + +Let's use this full-stack [TypeScript and ReactJS Spin app](https://github.com/radu-matei/spin-react-fullstack) to walk through this tutorial. If you have a Spin app already feel free to navigate to that directory and skip the step below. + +Fork and clone the app [GitHub repository](https://github.com/radu-matei/spin-react-fullstack.git): + + + + ```bash +$ git clone https://github.com/USERNAME/spin-react-fullstack.git +``` + +Now, switch to that directory and rebuild the application: + + + + ```bash +$ cd spin-react-fullstack +$ spin build +``` + +Now we're ready to push the application. Run the `spin registry push` command to push your application to the registry: + + + + ```bash +$ spin registry push ghcr.io/USERNAME/spin-react-fullstack:v1 +``` + +> **Note:** You can find more information on `spin registry` options and subcommands by using the `--help` option. + +You now have a Spin application stored in your registry. You can see the artifact under packages in the [GitHub UI](https://docs.github.com/en/packages/learn-github-packages/viewing-packages#viewing-a-repositorys-packages). + +## Pull a Spin App From GHCR + +Now that we've successfully pushed a Spin app, let's see if we can pull it. To do so, run the following command: + + + + ```bash +$ spin registry pull ghcr.io/USERNAME/spin-react-fullstack:v1 +``` + +## Run a Spin App From GHCR + +Lastly, let's run this Spin application: + + + + ```bash +$ spin up -f ghcr.io/USERNAME/spin-react-fullstack:v1 +``` + +## Deploy a Spin App from GHCR (or any registry) + +You can deploy a Spin app from any registry such as GHCR or DockerHub using the CLI command `spin deploy -f ` + +> **Note:** You need a Fermyon Cloud account to deploy to. The remote-reference can be private, as long as you are authenticated locally to the registry in question, since the CLI will pull it down prior to publishing it to Cloud's internal registry. + + +## Conclusion + +Congratulations on completing this tutorial! You have now successfully built, pushed, pulled, and run a Spin app using GHCR. Behind the scenes, Spin uses [OCI artifacts](https://github.com/opencontainers/artifacts) project to distribute Spin apps across container registries. To learn more about how this feature works, take a look at [our proposal](https://github.com/fermyon/spin/blob/main/docs/content/sips/008-using-oci-registries.md) and [the implementation](https://github.com/fermyon/spin/pull/1014). + +## Next Steps + +- If you're interested in shaping how registry support will look in the next version, please share your thoughts in [our Discord community](https://discord.gg/AAFNfS7NGf) + + diff --git a/content/spin/v3/running-apps.md b/content/spin/v3/running-apps.md new file mode 100644 index 000000000..cc54f5aa5 --- /dev/null +++ b/content/spin/v3/running-apps.md @@ -0,0 +1,202 @@ +title = "Running Spin Applications" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/running-apps.md" + +--- +- [Specifying the Application to Run](#specifying-the-application-to-run) +- [Specifying the Wasm File to Run](#specifying-the-wasm-file-to-run) + - [Testing HTTP Applications](#testing-http-applications) +- [Application Output](#application-output) +- [Persistent Logs](#persistent-logs) +- [Trigger-Specific Options](#trigger-specific-options) +- [Monitoring Applications for Changes](#monitoring-applications-for-changes) +- [Splitting an Application Across Environments](#splitting-an-application-across-environments) +- [The Always Build Option](#the-always-build-option) +- [Next Steps](#next-steps) + +Once you have created and built your application, it's ready to run. To run an application, use the `spin up` command. + +## Specifying the Application to Run + +By default, `spin up` looks for a file named `spin.toml` in the current directory. + +If your manifest is named something different, or isn't in your current directory, use the `-f` (`--from`) flag. You also use `-f` to run remote applications. + +| `-f` Value | `spin up` Behavior | +|---------------------------------|---------------------| +| File name: `-f demo.toml` | Runs the specified manifest file | +| Directory name: `-f demo/` | Looks for a `spin.toml` file in that directory and runs that | +| Registry reference: `-f ghcr.io/fermyon/example:v1` | Pulls the application from the registry and runs that | + +> If Spin misunderstands a registry reference as a file name, or vice versa, you can use `--from-file` or `--from-registry` instead of `-f` to disambiguate. + +> If you see the error `failed to resolve content at "example.wasm"` (where `example.wasm` is the module file of a component), check that the application has been built. + +> If your application doesn't run, you can [run `spin doctor`](./troubleshooting-application-dev.md) to check for problems with your Spin configuration and tools. + +## Specifying the Wasm File to Run + +The `spin up --from` (`spin up -f`) option can point to a pre-existing Wasm binary executable instead of an application's manifest: + + + +```bash +$ spin up --from mymodule.wasm +``` + +Please note that the uses for performing `spin up` using just a Wasm file are very limited outside of basic testing of the Wasm file. This is because Wasm files run in this way have no access to application-level configuration that allows storage, outbound HTTP and so on. Only incoming HTTP handlers are supported. + +### Testing HTTP Applications + +By default, HTTP applications listen on `localhost:3000`. You can override this with the `--listen` option. Spin prints links to the application components to make it easy to open them in the browser or copy them to `curl` commands for testing. + +## Application Output + +By default, Spin prints application output, and any of its own error messages, to the console. + +To hide application output, pass the `--quiet` flag: + + + +```bash +$ spin up --quiet +``` + +To limit application output to specific components, pass the `--follow` flag: + + + +```bash +$ spin up --follow cart --follow cart-api +``` + +## Persistent Logs + +By default: + +* If you run an application from the file system (a TOML file), Spin saves a copy of the application output and error messages. This is saved in the `.spin/logs` directory, under the directory containing the manifest file. +* If you run an application from a registry reference, Spin does not save a copy of the application output and error messages; they are only printed to the console. + +To control logging, pass the `--log-dir` flag. The logs will be saved to the specified directory (no matter whether the application is local or remote): + + + +```bash +$ spin up --log-dir ~/dev/bugbash +``` + +If you prefer **not** to have the `stdout` and `stderr` of your application's components written to disk (as in the example above), you can pass the `--log-dir` flag with an empty string, like this: + + + +```bash +$ spin up --log-dir "" +``` + +## Trigger-Specific Options + +Some trigger types support additional `spin up` flags. For example, HTTP applications can have a `--listen` flag to specify an address and port to listen on. See the [HTTP trigger](http-trigger) and [Redis trigger](redis-trigger) pages for more details. + +## Monitoring Applications for Changes + +Spin's `watch` command rebuilds and restarts Spin applications whenever files change. You can use the `spin watch` command in place of the `spin build` and `spin up` commands, to build, run and then keep your Spin application running without manual intervention while staying on the latest code and files. + +> The `watch` command accepts valid `spin up` options and passes them through to `spin up` for you when running/rerunning the Spin application. +E.g. `spin watch --listen 127.0.0.1:3001` + +By default, Spin watch monitors: + +* The application manifest (`spin.toml` file) +* Any files specified in the `component.(id).build.watch` sections of the `spin.toml` file +* Any files specified in the `component.(id).files` sections of the `spin.toml` file +* The files specified in the `component.(id).source` sections of the `spin.toml` file + +If any of these change, Spin will rebuild the application if necessary, then restart the application with the new files. + +> Spin watch does not consider changes to a file's metadata (file permissions or when it was last modified) as a change. + +The following `spin.toml` configuration (belonging to a Spin `http-rust` application) is configured to ensure that the application is both **rebuilt** (via `cargo build --target wasm32-wasi --release`) and **rerun** whenever changes occur in any Rust source (`.rs`) files, the `Cargo.toml` file or the `spin.toml` file, itself. When changes occur in either the Wasm binary file (`target/wasm32-wasi/release/test.wasm`) or the text file (`my-files/changing-file.txt`) the application is only **rerun** using the initial `spin up` command: + + + + ```toml +[component.test] +// -- snip +files = ["my-files/changing-file.txt"] +source = "target/wasm32-wasi/release/test.wasm" +[component.test.build] +command = "cargo build --target wasm32-wasi --release" +# Example watch configuration for a Rust application +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +If the `build` section specifies a `workdir`, then `watch` patterns are relative to that directory. Otherwise, `watch` patterns are relative to the directory containing the `spin.toml` file. + +If you would prefer Spin watch to only rerun the application (without a rebuild) when changes occur, you can use the `--skip-build` option when running the `spin watch` command. In this case, Spin will ignore the `component.(id).build.watch` section, and monitor only the `spin.toml`, `component.source` and `component.files`. + +The table below outlines exactly which files `spin watch` will monitor for changes depending on how you run the command. `spin watch` uses the configuration found on every component in your application. + +| Files | `spin watch` monitors for changes | `spin watch --skip-build` monitors for changes | +| ----------------------- | ---------------------------------------------- | ---------------------------------------------- | +| Application manifest `spin.toml` | Yes | Yes | +| Component `build.watch` | Yes | No | +| Component `files` | Yes | Yes | +| Component `source` | No (Yes if the component has no build command) | Yes + +Spin watch waits up to 100 milliseconds before responding to filesystem events, then processes all events that occurred in that interval together. This is so that if you make several changes close together (for example, using a Save All command), you get them all processed in one rebuild/reload cycle, rather than going through a cycle for each one. You can override the interval by passing in the `--debounce` option; e.g. `spin watch --debounce 1000` will make Spin watch respond to filesystem events at most once per second. + +> Note: If the build step (`spin build`) fails, `spin up` will not be re-run, and the previous version of the app will remain. + +Passing the `--clear` flag clears the screen anytime a rebuild or rerun occurs. Spin watch does not clear the screen between rebuild and rerun as this provides you with an opportunity to see any warnings. + +For additional information about Spin's `watch` feature, please see the [Spin watch - live reload for Wasm app development](https://www.fermyon.com/blog/spin-watch-live-reloading) blog article. + +> The application manifests shown in the blog post are the version 1 manifest, but the content applies equally to the version 2 format. + +## Splitting an Application Across Environments + +You can run a subset of the components in an application by using the `--component-id` (`-c`) flag. Set the flag multiple times to select multiple components. + +> This is an experimental feature. Unlike stable features, it may change even between minor versions. + +This supports _selective deployment_ - that is, splitting an application across environments while still distributing it as a single unit. For example, suppose your application contains the following layers: + +* A front end which handles browser and API requests, which you want to run in edge data centres close to users +* A database backend which must be run in a single central location +* An AI service which requires an LLM model to be loaded onto the hardware + +You could deploy the application across the environments by using `--component-id`. For example: + + + +```console +# on the US data centre edge nodes +$ spin up -f registry.example.com/app:1.1.0 -c ui -c api -c assets + +# on the European data centre edge nodes +$ spin up -f registry.example.com/app:1.1.0 -c ui -c api -c assets + +# on the European data centre core nodes +$ spin up -f registry.example.com/app:1.1.0 -c database + +# in the LLM environment +$ spin up -f registry.example.com/app:1.1.0 -c chat-engine -c sentiment-analyzer +``` + +> In practice you'd set these commands up in a scheduler or orchestrator rather than typing them interactively - or, more likely, use a selection-aware scheduler such as [SpinKube](https://www.spinkube.dev/) rather than running `spin up` directly. + +If you run a subset which includes a component that uses [local service chaining](./http-outbound#local-service-chaining), then you must also include all chaining targets in the subset - Spin checks this at load time. [Self-requests](./http-outbound#making-http-requests-within-an-application) will work only if the target route maps to a component in the subset, but this is not checked at load time - instead, self-requests to unselected components will fail at request time with 404 Not Found. + +## The Always Build Option + +Some people find it frustrating having to remember to build their applications before running `spin up`. If you want to _always_ build your projects when you run them, set the `SPIN_ALWAYS_BUILD` environment variable in your profile or session. If this is set, `spin up` runs [`spin build`](build) before starting your applications. + +## Next Steps + +- Learn how to [create and update a Spin application](writing-apps) +- Learn about how to [configure your application at runtime](dynamic-configuration) +- See how to [package and distribute your application](registry-tutorial) +- Try deploying your application to run in the [Fermyon Cloud](/cloud/quickstart) diff --git a/content/spin/v3/rust-components.md b/content/spin/v3/rust-components.md new file mode 100644 index 000000000..bc2274016 --- /dev/null +++ b/content/spin/v3/rust-components.md @@ -0,0 +1,598 @@ +title = "Building Spin components in Rust" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/rust-components.md" + +--- +- [Prerequisites](#prerequisites) + - [Install the Templates](#install-the-templates) + - [Install the Tools](#install-the-tools) +- [HTTP Components](#http-components) +- [Redis Components](#redis-components) +- [Sending Outbound HTTP Requests](#sending-outbound-http-requests) +- [Routing in a Component](#routing-in-a-component) +- [Storing Data in Redis From Rust Components](#storing-data-in-redis-from-rust-components) +- [Storing Data in the Spin Key-Value Store](#storing-data-in-the-spin-key-value-store) + - [Serializing Objects to the Key-Value Store](#serializing-objects-to-the-key-value-store) +- [Storing Data in SQLite](#storing-data-in-sqlite) +- [Storing Data in Relational Databases](#storing-data-in-relational-databases) +- [Using External Crates in Rust Components](#using-external-crates-in-rust-components) + - [Using the `http` crate](#using-the-http-crate) +- [AI Inferencing From Rust Components](#ai-inferencing-from-rust-components) +- [Troubleshooting](#troubleshooting) +- [Manually Creating New Projects With Cargo](#manually-creating-new-projects-with-cargo) +- [Read the Rust Spin SDK Documentation](#read-the-rust-spin-sdk-documentation) + +Spin aims to have best-in-class support for building components in Rust, and +writing such components should be familiar for Rust developers. + +> This guide assumes you have Spin installed. If this is your first encounter with Spin, please see the [Quick Start](quickstart), which includes information about installing Spin with the Rust templates, installing required tools, and creating Rust applications. + +> This guide assumes you are familiar with the Rust programming language, +> but if you are just getting started, be sure to check [the +official resources for learning Rust](https://www.rust-lang.org/learn). + +> All examples from this page can be found in [the Spin Rust SDK repository on GitHub](https://github.com/fermyon/spin-rust-sdk/tree/main/examples). + +[**Want to go straight to the Spin SDK reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/index.html) + +## Prerequisites + +### Install the Templates + +You don't need the Spin Rust templates to work on Rust components, but they speed up creating new applications and components. You can install them as follows: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin --update +Copying remote template source +Installing template redis-rust... +Installing template http-rust... +... other templates omitted ... ++------------------------------------------------------------------------+ +| Name Description | ++========================================================================+ +| ... other templates omitted ... | +| http-rust HTTP request handler using Rust | +| redis-rust Redis message handler using Rust | +| ... other templates omitted ... | ++------------------------------------------------------------------------+ +``` + +Note: The Rust templates are in a repo that contains several other languages; they will all be installed together. + +### Install the Tools + +To build Spin components, you'll need the `wasm32-wasi` target for Rust. + + + +```bash +$ rustup target add wasm32-wasi +``` + +> If you get a lot of strange errors when you try to build your first Rust component, check that you have this target installed by running `rustup target list --installed`. This is the most common source of problems when starting out with Rust in Spin! + +## HTTP Components + +In Spin, HTTP components are triggered by the occurrence of an HTTP request, and +must return an HTTP response at the end of their execution. Components can be +built in any language that compiles to WASI, but Rust has improved support +for writing Spin components with the Spin Rust SDK. + +> Make sure to read [the page describing the HTTP trigger](./http-trigger.md) for more +> details about building HTTP applications. + +Building a Spin HTTP component using the Rust SDK means writing a single function decorated with the [`#[http_component]`](https://docs.rs/spin-sdk/latest/spin_sdk/attr.http_component.html) attribute. The function can have one of two forms: + +* takes an HTTP request as a parameter, and returns an HTTP response — shown below +* taken as parameters _both_ the HTTP request and an object through which to write a response - see [the HTTP trigger page](./http-trigger#authoring-http-components) for an example. + +```rust +use spin_sdk::http::{Request, Response, IntoResponse}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +async fn handle_hello_rust(_req: Request) -> anyhow::Result { + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Fermyon") + .build()) +} +``` + +The important things to note in the implementation above: + +- the [`spin_sdk::http_component`](https://docs.rs/spin-sdk/latest/spin_sdk/attr.http_component.html) macro marks the function as the entry point for the Spin component +- the function signature — `fn hello_world(req: Request) -> Result` — + the Spin HTTP component allows for a flexible set of response types via the [`IntoResponse`](https://docs.rs/spin-sdk/latest/spin_sdk/http/trait.IntoResponse.html) trait, including the SDK's `Response` type and the `Response` type from the Rust [`http` crate](https://crates.io/crates/http). See the section on [using the `http` crate](#using-the-http-crate) for more information. + +> If you're familiar with Spin 1.x, you will see some changes when upgrading to the Spin 2 SDK. Mostly these provide more flexibility, but you will likely need to change some details such as module paths. If you don't want to modify your code, you can continue using the 1.x SDK - your components will still run. + +## Redis Components + +Besides the HTTP trigger, Spin has built-in support for a Redis trigger — +which will connect to a Redis instance and will execute Spin components for +new messages on the configured channels. + +> See the [Redis trigger](./redis-trigger.md) for details about the Redis trigger. + +Writing a Redis component in Rust also takes advantage of the SDK: + +```rust +use anyhow::Result; +use bytes::Bytes; +use spin_sdk::redis_component; + +/// A simple Spin Redis component. +#[redis_component] +fn on_message(message: Bytes) -> Result<()> { + println!("{}", std::str::from_utf8(&message)?); + Ok(()) +} +``` + +- the `spin_sdk::redis_component` macro marks the function as the + entry point for the Spin component +- in the function signature — `fn on_message(msg: Bytes) -> anyhow::Result<()>` — +`msg` contains the payload from the Redis channel +- the component returns a Rust `anyhow::Result`, so if there is an error +processing the request, it returns an `anyhow::Error`. + +The component can be built with Cargo by executing: + + + +```bash +$ cargo build --target wasm32-wasi --release +``` + +The manifest for a Redis application must contain the address of the Redis +instance the trigger must connect to: + + + +```toml +spin_manifest_version = 2 +name = "spin-redis" +version = "0.1.0" + +[application.trigger.redis] +address = "redis://localhost:6379" + +[[trigger.redis]] +channel = "messages" +component = { source = "target/wasm32-wasi/release/spinredis.wasm" } +``` + +This application will connect to `redis://localhost:6379`, and for every new +message on the `messages` channel, the `echo-message` component will be executed: + + + +```bash +# first, start redis-server on the default port 6379 +$ redis-server --port 6379 +# then, start the Spin application +$ spin up --file spin.toml +# the application log file will output the following +INFO spin_redis_engine: Connecting to Redis server at redis://localhost:6379 +INFO spin_redis_engine: Subscribed component 0 (echo-message) to channel: messages +``` + +For every new message on the `messages` channel: + + + +```bash +$ redis-cli +127.0.0.1:6379> publish messages "Hello, there!" +``` + +Spin will instantiate and execute the component we just built, which will emit the `println!` message to the application log file: + +``` +INFO spin_redis_engine: Received message on channel "messages" +Hello, there! +``` + +> You can find a complete example for a Redis triggered component in the +> [Spin repository on GitHub](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/redis). + +## Sending Outbound HTTP Requests + +If allowed, Spin components can send outbound HTTP requests. +Let's see an example of a component that makes a request to +[an API that returns random animal facts](https://random-data-api.fermyon.app/animals/json) and +inserts a custom header into the response before returning: + +```rust +use anyhow::Result; +use spin_sdk::{ + http::{IntoResponse, Request, Method, Response}, + http_component, +}; + +#[http_component] +async fn send_outbound(_req: Request) -> Result { + // Create the outbound request object + let req = Request::builder() + .method(Method::Get) + .uri("https://random-data-api.fermyon.app/animals/json") + .build(); + + // Send the request and await the response + let res: Response = spin_sdk::http::send(req).await?; + + println!("{:?}", res); // log the response + Ok(res) +} +``` + +> The `http::Request::builder()` method is provided by the Rust `http` crate. The `http` crate is already added to projects using the Spin `http-rust` template. If you create a project without using this template, you'll need to add the `http` crate yourself via `cargo add http`. + +Before we can execute this component, we need to add the `random-data-api.fermyon.app` +domain to the component's `allowed_outbound_hosts` list in the application manifest. This contains the list of +domains the component is allowed to make network requests to: + + + +```toml +# spin.toml +spin_manifest_version = 2 + +[application] +name = "animal-facts" +version = "1.0.0" + +[[trigger.http]] +route = "/..." +component = "get-animal-fact" + +[component.get-animal-fact] +source = "get-animal-fact/target/wasm32-wasi/release/get_animal_fact.wasm" +allowed_outbound_hosts = ["https://random-data-api.fermyon.app"] +``` + +Running the application using `spin up` will start the HTTP +listener locally (by default on `localhost:3000`), and our component can +now receive requests in route `/outbound`: + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +date: Fri, 27 Oct 2023 03:54:36 GMT +content-type: application/json; charset=utf-8 +content-length: 185 +spin-component: get-animal-fact + +{"timestamp":1684299253331,"fact":"Reindeer grow new antlers every year"} +``` + +> Without the `allowed_outbound_hosts` field populated properly in `spin.toml`, +> the component would not be allowed to send HTTP requests, and sending the +> request would result in a "Destination not allowed" error. + +> You can set `allowed_outbound_hosts = ["https://*:*"]` if you want to allow +> the component to make requests to any HTTP host. This is not recommended +> unless you have a specific need to contact arbitrary servers and perform your own safety checks. + +We just built a WebAssembly component that sends an HTTP request to another +service, manipulates that result, then responds to the original request. +This can be the basis for building components that communicate with external +databases or storage accounts, or even more specialized components like HTTP +proxies or URL shorteners. + +> The Spin SDK for Rust provides more flexibility than we show here, including allowing streaming uploads or downloads. See the [Outbound HTTP API Guide](./http-outbound.md) for more information. + +## Routing in a Component + + + +The Rust SDK [provides a router](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/http-router) that makes it easier to handle routing within a component: + +```rust +use anyhow::Result; +use spin_sdk::{ + http::{IntoResponse, Params, Request, Response, Router}, + http_component, +}; + +/// A Spin HTTP component that internally routes requests. +#[http_component] +fn handle_route(req: Request) -> Response { + let mut router = Router::new(); + router.get("/goodbye/:planet", api::goodbye_planet); + router.any_async("/*", api::echo_wildcard); + router.handle(req) +} + +mod api { + use super::*; + + // /goodbye/:planet + pub fn goodbye_planet(_req: Request, params: Params) -> Result { + let planet = params.get("planet").expect("PLANET"); + Ok(Response::new(200, planet.to_string())) + } + + // /* + pub async fn echo_wildcard(_req: Request, params: Params) -> Result { + let capture = params.wildcard().unwrap_or_default(); + Ok(Response::new(200, capture.to_string())) + } +} +``` + +Handlers within a `Router` can be sync or async. Use `Router`'s "plain" methods (e.g. `get`, `post`) to assign synchronous handlers, and its "async" methods (e.g. `get_async`, `post_async`) for asynchronous handlers. You can mix sync and async handlers in the same `Router`, and can use `handle` or `handle_async` to invoke `Router` processing, regardless of whether invididual handlers are sync or async. + +> For further reference, see the [Spin SDK HTTP router](https://docs.rs/spin-sdk/latest/spin_sdk/http/struct.Router.html). + +## Storing Data in Redis From Rust Components + +Using the Spin's Rust SDK, you can use the Redis key/value store and to publish +messages to Redis channels. This can be used from both HTTP and Redis triggered +components. + +Let's see how we can use the Rust SDK to connect to Redis: + +```rust +use anyhow::Context; +use spin_sdk::{ + http::{responses::internal_server_error, IntoResponse, Request, Response}, + http_component, + redis::Connection, +}; + +// The environment variable set in `spin.toml` that points to the +// address of the Redis server that the component will publish +// a message to. +const REDIS_ADDRESS_ENV: &str = "REDIS_ADDRESS"; + +// The environment variable set in `spin.toml` that specifies +// the Redis channel that the component will publish to. +const REDIS_CHANNEL_ENV: &str = "REDIS_CHANNEL"; + +/// This HTTP component demonstrates fetching a value from Redis +/// by key, setting a key with a value, and publishing a message +/// to a Redis channel. The component is triggered by an HTTP +/// request served on the route configured in the `spin.toml`. +#[http_component] +fn publish(_req: Request) -> anyhow::Result { + let address = std::env::var(REDIS_ADDRESS_ENV)?; + let channel = std::env::var(REDIS_CHANNEL_ENV)?; + + // Establish a connection to Redis + let conn = Connection::open(&address)?; + + // Get the message to publish from the Redis key "mykey" + let payload = conn + .get("mykey") + .context("Error querying Redis")? + .context("'mykey' was unexpectedly empty")?; + + // Set the Redis key "spin-example" to value "Eureka!" + conn.set("spin-example", &"Eureka!".to_owned().into_bytes()) + .context("Error executing Redis set command")?; + + // Set the Redis key "int-key" to value 0 + conn.set("int-key", &format!("{:x}", 0).into_bytes()) + .context("Error executing Redis set command")?; + let int_value = conn + .incr("int-key") + .context("Error executing Redis incr command")?; + assert_eq!(int_value, 1); + + // Publish to Redis + match conn.publish(&channel, &payload) { + Ok(()) => Ok(Response::builder().status(200).build()), + Err(_e) => Ok(internal_server_error()) + } +} +``` + +As with all networking APIs, you must grant access to Redis hosts via the `allowed_outbound_hosts` field in the application manifest: + + + +```toml +[component.redis-test] +environment = { REDIS_ADDRESS = "redis://127.0.0.1:6379", REDIS_CHANNEL = "messages" } +# Note this contains only the host and port - do not include the URL! +allowed_outbound_hosts = ["redis://127.0.0.1:6379"] +``` + +This HTTP component can be paired with a Redis component, triggered on new +messages on the `messages` Redis channel. + +> You can find a complete example for using outbound Redis from an HTTP component +> in the [Spin repository on GitHub](https://github.com/fermyon/spin-rust-sdk/tree/main/examples/redis-outbound). + +## Storing Data in the Spin Key-Value Store + +Spin has a key-value store built in. For information about using it from Rust, see [the key-value store tutorial](key-value-store-tutorial). + +### Serializing Objects to the Key-Value Store + +The Spin key-value API stores and retrieves only lists of bytes. The Rust SDK provides helper functions that allow you to store and retrieve [Serde](https://docs.rs/serde/latest/serde) serializable values in a typed way. The underlying storage format is JSON (and is accessed via the `get_json` and `set_json` helpers). + +To make your objects serializable, you will also need a reference to `serde`. The relevant `Cargo.toml` entries look like this: + +``` +[dependencies] +// --snip -- +serde = { version = "1.0.163", features = ["derive"] } +// --snip -- +``` + +We configure our application to provision the default `key_value_stores` by adding the following line to our application's manifest (the `spin.toml` file), at the component level: + +``` +[component.redis-test] +key_value_stores = ["default"] +``` + +The Rust code below shows how to store and retrieve serializable objects from the key-value store (note how the example below implements Serde's `derive` feature): + +```rust +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use spin_sdk::{ + http::{Request, Response}, + http_component, + key_value::Store, +}; + +// Define a serializable User type +#[derive(Serialize, Deserialize)] +struct User { + fingerprint: String, + location: String, +} + +#[http_component] +fn handle_request(_req: Request) -> anyhow::Result { + // Open the default key-value store + let store = Store::open_default()?; + + // Create an instance of a User object and populate the values + let user = User { + fingerprint: "0x1234".to_owned(), + location: "Brisbane".to_owned(), + }; + // Store the User object using the "my_json" key + store.set_json("my_json", &user)?; + // Retrieve the user object from the key-value store, using the "my_json" key + let retrieved_user: User = store.get_json("my_json")?.context("user not found")?; + // Return the user's fingerprint as the response body + Ok(Response::builder() + .status(200) + .body(retrieved_user.fingerprint) + .build()) +} +``` + +> If you are familiar with Spin 1.x, you will be used to `get` and `get_json` returning a `Result<...>`, with "key not found" being one of the error cases. In Spin 2, `get` and `get_json` return `Result>`, with "key not found" represented by `Ok(None)`. + +Once built and running (using `spin build` and `spin up`) you can test the above example in your browser (by visiting localhost:3000) or via curl, as shown below: + + + +```bash +$ curl localhost:3000 +HTTP/1.1 200 OK + +0x1234 +``` + +For more information on the Rust key-value API see [the Spin SDK documentation](https://docs.rs/spin-sdk/latest/spin_sdk/key_value/index.html). + +## Storing Data in SQLite + +For more information about using SQLite from Rust, see [SQLite storage](sqlite-api-guide). + +## Storing Data in Relational Databases + +Spin provides clients for MySQL and PostgreSQL. For information about using them from Rust, see [Relational Databases](rdbms-storage). + +## Using External Crates in Rust Components + +In Rust, Spin components are regular libraries that contain a function +annotated using the `http_component` macro, compiled to the `wasm32-wasi` target. +This means that any [crate](https://crates.io) that compiles to `wasm32-wasi` can +be used when implementing the component. + +### Using the `http` crate + +If you're already familiar with the popular [`http` crate](https://crates.io/crates/http), you may wish to use that instead of using the HTTP types included in the Spin SDK. Generally, the `http` crate's types can be used anywhere the Spin SDK HTTP types can be used. For example, the first basic HTTP component can be rewritten to use the `http` crate like so: + +```rust +use http::{Request, Response}; +use spin_sdk::http::IntoResponse; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +async fn handle_hello_rust(_req: Request<()>) -> anyhow::Result { + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Fermyon")?) +} +``` + +Of course, you'll need to remember to add the http crate to your `Cargo.toml`: + +``` +[dependencies] +// --snip -- +http = "0.2.9" +// --snip -- +``` + +## AI Inferencing From Rust Components + +For more information about using Serverless AI from Rust, see the [Serverless AI](serverless-ai-api-guide) API guide. + +## Troubleshooting + +If you bump into issues building and running your Rust component, here are some common causes of problems: + +- Make sure `cargo` is present in your path. +- Make sure the [Rust](https://www.rust-lang.org/) version is recent. + +![Rust Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffermyon%2Fspin%2Fmain%2FCargo.toml&query=$[%27workspace%27][%27package%27][%27rust-version%27]&label=Rust%20Version&logo=Rust&color=orange) + + - To check: run `cargo --version`. + - To update: run `rustup update`. +- Make sure the `wasm32-wasi` compiler target is installed. + - To check: run `rustup target list --installed` and check that `wasm32-wasi` is on the list. + - To install: run `rustup target add wasm32-wasi`. +- Make sure you are building in `release` mode. Spin manifests refer to your Wasm file by a path, and the default path corresponds to `release` builds. + - To build manually: run `cargo build --release --target wasm32-wasi`. + - If you're using `spin build` and the templates, this should be set up correctly for you. +- Make sure that the `source` field in the component manifest match the path and name of the Wasm file in `target/wasm32-wasi/release`. These could get out of sync if you renamed the Rust package in its `Cargo.toml`. + +## Manually Creating New Projects With Cargo + +The recommended way of creating new Spin projects is by starting from a template. +This section shows how to manually create a new project with Cargo. + +When creating a new Spin project with Cargo, you should use the `--lib` flag: + + + +```bash +$ cargo init --lib +``` + +A `Cargo.toml` with standard Spin dependencies looks like this: + + + +```toml +[package] +name = "your-app" +version = "0.1.0" +edition = "2021" + +[lib] +# Required to have a `cdylib` (dynamic library) to produce a Wasm module. +crate-type = [ "cdylib" ] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# The Spin SDK. +spin-sdk = { git = "https://github.com/fermyon/spin" } +``` + +## Read the Rust Spin SDK Documentation + +Although you learned a lot by following the concepts and samples shown here, you can dive even deeper and read the [Rust Spin SDK documentation](https://docs.rs/spin-sdk/latest/spin_sdk/index.html). diff --git a/content/spin/v3/see-what-people-have-built-with-spin.md b/content/spin/v3/see-what-people-have-built-with-spin.md new file mode 100644 index 000000000..de4a26301 --- /dev/null +++ b/content/spin/v3/see-what-people-have-built-with-spin.md @@ -0,0 +1,82 @@ +title = "Built With Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/see-what-people-have-built-with-spin.md" + +--- +- [AI-powered bookmarking app with Python and WebAssembly](#ai-powered-bookmarking-app-with-python-and-webassembly) +- [Like Button](#like-button) +- [Social App](#social-app) +- [Static Content Server](#static-content-server) +- [Bots](#bots) +- [URL Shortener and QR Code Generator](#url-shortener-and-qr-code-generator) +- [Content Management System (CMS)](#content-management-system-cms) +- [The Finicky Whiskers Game](#the-finicky-whiskers-game) +- [Accessing External APIs](#accessing-external-apis) +- [SQLite Storage Using Javascript](#sqlite-storage-using-javascript) +- [Next Steps](#next-steps) + +Spin can be used to build many different types of applications. The following blog articles show how different applications are built with Spin. + +## AI-powered bookmarking app with Python and WebAssembly + +[How to build an AI-powered bookmarking app with Python and WebAssembly](https://dev.to/fermyon/part-1-how-to-build-an-ai-powered-bookmarking-app-with-python-and-webassembly-2c5b). + +## Like Button + +[How I Built a Like Button for My Blog with Spin](https://www.fermyon.com/blog/how-i-built-a-like-button-for-my-blog-with-spin) + +## Social App + +The 'Building a Social App with Spin' series covers the process and decision-making of building an authenticated, dynamic, database-backed application and API, right the way from downloading Spin and setting up the project to deploying the finished application to the cloud. + +* [Part 1: Project Setup and first API](https://www.fermyon.com/blog/building-a-social-app-with-spin-1) +* [Part 2: Vue.js App and Token Verification](https://www.fermyon.com/blog/building-a-social-app-with-spin-2) +* [Part 3: Post API and Spin SDK Bindings](https://www.fermyon.com/blog/building-a-social-app-with-spin-3) +* [Part 3.5: Go Postgres Usage](https://www.fermyon.com/blog/building-a-social-app-with-spin-3-5) +* [Part 4: Key-Value Storage and Fermyon Cloud](https://www.fermyon.com/blog/building-a-social-app-with-spin-4) + +## Static Content Server + +[Serving Static Content via WebAssembly](https://www.fermyon.com/blog/serving-static-content-via-webassembly) + +## Bots + +[Bots With Spin and Fermyon Cloud](https://www.fermyon.com/blog/bots-with-spin-and-fermyon-cloud) + +## URL Shortener and QR Code Generator + +[Shortlink and QR Code Generator](https://www.fermyon.com/blog/component-reuse) + +## Content Management System (CMS) + +[Build Your Own Content Management System (CMS) From a Template](https://www.fermyon.com/blog/build-you-own-cms-from-a-template) + +## The Finicky Whiskers Game + +The 'Finicky Whiskers' series covers the journey of creating a game with an architecture designed to [re-think microservices](https://www.fermyon.com/blog/rethinking-microservices) and showcase how WebAssembly modules can be started, executed, and shut down in the blink of an eye. + +> Finicky Whiskers was built to be the world's most adorable manual load generator, so it's not necessarily an model of how to use Spin most efficiently! But the series may be useful for decomposing an application into Spin microservices, and as a look at how Spin and containers can work together. As a bonus, if you are interested, you can play the game [here](https://finickywhiskers.com/index.html). + +* [Part 1: The World's Most Adorable Manual Load Generator](https://www.fermyon.com/blog/finicky-whiskers-part-1-intro) +* [Part 2: Serving the HTML, CSS, and Static Assets](https://www.fermyon.com/blog/finicky-whiskers-part-2-fileserver) +* [Part 3: The Microservices](https://www.fermyon.com/blog/finicky-whiskers-part-3-microservices) +* [Part 4: Spin, Containers, Nomad, and Infrastructure](https://www.fermyon.com/blog/finicky-whiskers-part-4-infrastructure) + +## Accessing External APIs + +Chances are whatever you are building will want to talk to other endpoints on the web. If so, Spin's HTTP library can help. The following article explains how to access external APIs from within Spin applications. + +- [Accessing external APIs](https://www.fermyon.com/blog/spin-rest-apis) + +## SQLite Storage Using Javascript + +Check out our [NoOps and Serverless Are the Perfect Pair](https://www.fermyon.com/blog/noops-and-serverless-are-the-perfect-pair) blog article that provides an SQLite database example (using Javascript). + +## Next Steps + +- Try [running your application locally](running-apps) +- Learn about how to [configure your application at runtime](dynamic-configuration) +- Look up details in the [application manifest reference](manifest-reference) +- Try deploying a Spin application to the [Fermyon Cloud](/cloud/quickstart) diff --git a/content/spin/v3/serverless-ai-api-guide.md b/content/spin/v3/serverless-ai-api-guide.md new file mode 100644 index 000000000..62a9f56b5 --- /dev/null +++ b/content/spin/v3/serverless-ai-api-guide.md @@ -0,0 +1,253 @@ +title = "Serverless AI API" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/serverless-ai-api-guide.md" + +--- +- [Using Serverless AI From Applications](#using-serverless-ai-from-applications) + - [Configuration](#configuration) + - [File Structure](#file-structure) +- [Serverless AI Interface](#serverless-ai-interface) +- [Troubleshooting](#troubleshooting) + - [Error "Local LLM operations are not supported in this version of Spin"](#error-local-llm-operations-are-not-supported-in-this-version-of-spin) + +The nature of AI and LLM workloads on already trained models lends itself very naturally to a serverless-style architecture. As a framework for building and deploying serverless applications, Spin provides an interface for you to perform AI inference within Spin applications. + +## Using Serverless AI From Applications + +### Configuration + +By default, a given component of a Spin application will not have access to any Serverless AI models. Access must be provided explicitly via the Spin application's manifest (the `spin.toml` file). For example, an individual component in a Spin application could be given access to the llama2-chat model by adding the following `ai_models` configuration inside the specific `[component.(name)]` section: + + + +```toml +// -- snip -- + +[component.please-send-the-codes] +ai_models = ["codellama-instruct"] + +// -- snip -- +``` + +> Spin supports the models of the Llama architecture for inferencing and "all-minilm-l6-v2" for generating embeddings. + +### File Structure + +By default, the Spin framework will expect any already trained model files (which are configured as per the previous section) to be downloaded by the user and made available inside a `.spin/ai-models/` file path of a given application. +Within the `.spin/ai-models` directory, models of the same architecture (e.g. `llama`) must be grouped under a directory with the same name as the architecture. +Within an architecture directory, each individual model (e.g. `llama2-chat`, `codellama-instruct`) must be placed under a folder with the same name as the model. So for any given model, that files for the model are placed in the directory `.spin/ai-models//`. For example: + +```bash +code-generator-rs/.spin/ai-models/llama/codellama-instruct/safetensors +code-generator-rs/.spin/ai-models/llama/codellama-instruct/config.json +code-generator-rs/.spin/ai-models/llama/codellama-instruct/tokenizer.json +``` + +See the [serverless AI Tutorial](./ai-sentiment-analysis-api-tutorial) documentation for more concrete examples of implementing the Fermyon Serverless AI API, in your favorite language. + +> For embeddings models, it is expected that both a `tokenizer.json` and a `model.safetensors` are located in the directory named after the model. For example, for the `foo-bar-baz` model, Spin will look in the `.spin/ai-models/foo-bar-baz` directory for `tokenizer.json` and a `model.safetensors`. + +## Serverless AI Interface + +The Spin SDK surfaces the Serverless AI interface to a variety of different languages. See the [Language Support Overview](./language-support-overview) to see if your specific language is supported. + +The set of operations is common across all supporting language SDKs: + +| Operation | Parameters | Returns | Behavior | +|:-----|:----------------|:-------|:----------------| +| `infer` | model`string`
prompt`string` | `string` | The `infer` is performed on a specific model.

The name of the model is the first parameter provided (i.e. `llama2-chat`, `codellama-instruct`, or other; passed in as a `string`).

The second parameter is a prompt; passed in as a `string`.
| +| `infer_with_options` | model`string`
prompt`string`
params`list` | `string` | The `infer_with_options` is performed on a specific model.

The name of the model is the first parameter provided (i.e. `llama2-chat`, `codellama-instruct`, or other; passed in as a `string`).

The second parameter is a prompt; passed in as a `string`.

The third parameter is a mix of float and unsigned integers relating to inferencing parameters in this order:

- `max-tokens` (unsigned 32 integer) Note: the backing implementation may return less tokens.
Default is 100

- `repeat-penalty` (float 32) The amount the model should avoid repeating tokens.
Default is 1.1

- `repeat-penalty-last-n-token-count` (unsigned 32 integer) The number of tokens the model should apply the repeat penalty to.
Default is 64

- `temperature` (float 32) The randomness with which the next token is selected.
Default is 0.8

- `top-k` (unsigned 32 integer) The number of possible next tokens the model will choose from.
Default is 40

- `top-p` (float 32) The probability total of next tokens the model will choose from.
Default is 0.9

The result from `infer_with_options` is a `string` | +| `generate-embeddings` | model`string`
prompt`list` | `string` | The `generate-embeddings` is performed on a specific model.

The name of the model is the first parameter provided (i.e. `all-minilm-l6-v2`, passed in as a `string`).

The second parameter is a prompt; passed in as a `list` of `string`s.

The result from `generate-embeddings` is a two-dimension array containing float32 type values only | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/llm/index.html) + +To use Serverless AI functions, the `llm` module from the Spin SDK provides the methods. The following snippet is from the [Rust code generation example](https://github.com/fermyon/ai-examples/tree/main/code-generator-rs): + + + +```rust +use spin_sdk::{ + http::{IntoResponse, Request, Response}, + llm, +}; + +// -- snip -- + +fn handle_code(req: Request) -> anyhow::Result { + // -- snip -- + + let result = llm::infer_with_options( + llm::InferencingModel::CodellamaInstruct, + &prompt, + llm::InferencingParams { + max_tokens: 400, + repeat_penalty: 1.1, + repeat_penalty_last_n_token_count: 64, + temperature: 0.8, + top_k: 40, + top_p: 0.9, + }, + )?; + + // -- snip -- +} + +``` + +**General Notes** + +The `infer_with_options` examples, operation: + +- The above example takes the model name `llm::InferencingModel::CodellamaInstruct` as input. From an interface point of view, the model name is technically an alias for a string (to maximize future compatibility as users want to support more and different types of models). +- The second parameter is a prompt (string) from whoever/whatever is making the request to the `handle_code()` function. +- A third, optional, parameter which is an interface allows you to specify parameters such as `max_tokens`, `repeat_penalty`, `repeat_penalty_last_n_token_count`, `temperature`, `top_k` and `top_p`. +- The return value (the `inferencing-result` record) contains a text field of type `string`. Ideally, this would be a `stream` that would allow streaming inferencing results back to the user, but alas streaming support is not yet ready for use so we leave that as a possible future backward incompatible change. + +{{ blockEnd }} + +{{ startTab "Typescript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/modules/Llm.html) + +To use Serverless AI functions, [the `Llm` module](https://fermyon.github.io/spin-js-sdk/modules/Llm.html) from the Spin SDK provides two methods: `infer` and `generateEmbeddings`. For example: + +```javascript +import { ResponseBuilder, Llm} from "@fermyon/spin-sdk" + +export async function handler(req: Request, res: ResponseBuilder) { + let embeddings = Llm.generateEmbeddings(Llm.EmbeddingModels.AllMiniLmL6V2, ["someString"]) + console.log(embeddings.embeddings) + let result = Llm.infer(Llm.InferencingModels.Llama2Chat, prompt) + + res.set({"content-type":"text/plain"}) + res.send(result.text) +} +``` + +**General Notes** + +`infer` operation: + +- It takes in the following arguments - model name, prompt and a optional third parameter for inferencing options. +- The model name is a string. There are enums for the inbuilt models (llama2-chat and codellama) in [`InferencingModels`](https://fermyon.github.io/spin-js-sdk/enums/Llm.InferencingModels.html). +- The optional third parameter which is an [InferencingOptions](https://fermyon.github.io/spin-js-sdk/interfaces/Llm.InferencingOptions.html) interface allows you to specify parameters such as `maxTokens`, `repeatPenalty`, `repeatPenaltyLastNTokenCount`, `temperature`, `topK`, `topP`. +- The return value is an [`InferenceResult`](https://fermyon.github.io/spin-js-sdk/interfaces/Llm.EmbeddingResult.html). + +`generateEmbeddings` operation: + +- It takes two arguments - model name and list of strings to generate the embeddings for. +- The model name is a string. There are enums for the inbuilt models (AllMiniLmL6V2) in [`EmbeddingModels`](https://fermyon.github.io/spin-js-sdk/enums/Llm.EmbeddingModels.html). +- The return value is an [`EmbeddingResult`](https://fermyon.github.io/spin-js-sdk/interfaces/Llm.EmbeddingResult.html) + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/llm.html) + +```python +from spin_sdk import http +from spin_sdk.http import Request, Response +from spin_sdk import llm + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + prompt="You are a stand up comedy writer. Tell me a joke." + result = llm.infer("llama2-chat", prompt) + return Response(200, + {"content-type": "application/json"}, + bytes(result.text, "utf-8")) +``` + +**General Notes** + +[`infer` operation](https://fermyon.github.io/spin-python-sdk/llm.html#spin_sdk.llm.infer): + +- The model name is passed in as a string (as shown above; `"llama2-chat"`). +[`infer_with_options` operation](https://fermyon.github.io/spin-python-sdk/llm.html#spin_sdk.llm.infer_with_options): + +- It takes in a model name, prompt text, and optionally a [parameter object](https://fermyon.github.io/spin-python-sdk/llm.html#spin_sdk.llm.InferencingParams) to control the inferencing. + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/llm) + +Serverless AI functions are available in the `github.com/fermyon/spin/sdk/go/v2/llm` package. See [Go Packages](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2/llm) for reference documentation. For example: + +```go +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/llm" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + result, err := llm.Infer("llama2-chat", "What is a good prompt?", nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Printf("Prompt tokens: %d\n", result.Usage.PromptTokenCount) + fmt.Printf("Generated tokens: %d\n", result.Usage.GeneratedTokenCount) + fmt.Fprintf(w, "%s\n", result.Text) + + embeddings, err := llm.GenerateEmbeddings("all-minilm-l6-v2", []string{"Hello world"}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Printf("Prompt Tokens: %d\n", embeddings.Usage.PromptTokenCount) + fmt.Printf("%v\n", embeddings.Embeddings) + }) +} +``` + +**General Notes** + +`infer` operation: + +- It takes in the following arguments - model name, prompt and an optional third parameter for inferencing options (pass `nil` if you don't want to specify it). +- The model name is a string. +- The params allows you to specify `MaxTokens`, `RepeatPenalty`, `RepeatPenaltyLastNTokenCount`, `Temperature`, `TopK`, `TopP`. +- It returns a result struct with a `Text` field that contains the answer and a `Usage` field that contains metadata about the operation. + +`generateEmbeddings` operation: + +- It takes two arguments - model name and list of strings to generate the embeddings for. +- The model name is a string: `all-minilm-l6-v2` +- It returns a result struct with an `Embeddings` field that contains the `[][]float32` embeddings and a `Usage` field that contains metadata about the operation. + +{{ blockEnd }} + +{{ blockEnd }} + +## Troubleshooting + +### Error "Local LLM operations are not supported in this version of Spin" + +If you see "Local LLM operations are not supported in this version of Spin", then your copy of Spin has been built without local LLM support. + +> The term "version" in the error message refers to how the software you are using built the Spin runtime, not to the numeric version of the runtime itself. + +Most Spin builds support local LLMs as described above. However, the models built into Spin do not build on some combinations of platforms (for example, there are known problems with the aarch64/musl combination). This may cause some environments that embed Spin to disable the local LLM feature altogether. (For examples, some versions of the `containerd-spin-shim` did this.) In such cases, you will see the error above. + +In such cases, you can: + +* See if there is another Spin build available for your platform. All current builds from the [Spin GitHub repository](https://github.com/fermyon/spin) or [Fermyon Spin installer support](./install.md) support local LLMs. +* Use the [`cloud-gpu` plugin and runtime config option](./serverless-ai-hello-world.md#building-and-deploying-your-spin-application) to have LLM inferencing serviced in Fermyon Cloud instead of locally. diff --git a/content/spin/v3/serverless-ai-hello-world.md b/content/spin/v3/serverless-ai-hello-world.md new file mode 100644 index 000000000..b32560611 --- /dev/null +++ b/content/spin/v3/serverless-ai-hello-world.md @@ -0,0 +1,451 @@ +date = "2023-11-04T00:00:01Z" +title = "Build your first AI app using Serverless AI Inferencing" +description = "Getting Started with building your AI app in Python, Rust or TypeScript" +template = "spin_main" +tags = ["ai", "serverless", "getting started"] +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/serverless-ai-hello-world.md" + +--- + +- [Tutorial Prerequisites](#tutorial-prerequisites) + - [Spin](#spin) + - [Dependencies](#dependencies) +- [Licenses](#licenses) +- [Serverless AI Inferencing With Spin](#serverless-ai-inferencing-with-spin) + - [Creating a New Spin Application](#creating-a-new-spin-application) + - [Configuring Your Application](#configuring-your-application) + - [Source Code](#source-code) + - [Building and Deploying Your Spin Application](#building-and-deploying-your-spin-application) +- [Next Steps](#next-steps) + +This tutorial will show you how to use Fermyon Serverless AI to quickly build your first AI-enabled serverless application that can run on Fermyon Cloud. +In this tutorial we will: + +* Install Spin (and dependencies) on your local machine +* Create a ‘Hello World’ Serverless AI application +* Learn about the Serverless AI SDK (in Rust, TypeScript and Python) + +Here's a video walkthrough of this tutorial + + + +## Tutorial Prerequisites + +### Spin + +You will need to [install the latest version of Spin](install#installing-spin). Serverless AI is supported on Spin v1.5 and above +If you already have Spin installed, [check what version you are on and upgrade](upgrade#are-you-on-the-latest-version) if required. + +### Dependencies + +**Rust** +The above installation script automatically installs the latest SDKs for Rust, which enables Serverless AI functionality. + +**TypeScript/Javascript** + +To enable Serverless AI functionality via TypeScript/Javascript, please ensure you have the latest TypeScript/JavaScript template installed: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-js-sdk --upgrade +``` + +**Python** + +Ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running: + +```bash +python3 --version +``` + +If you do not have Python 3.10 or later, you can install it by following the instructions [here](https://www.python.org/downloads/). + + +To enable Serverless AI functionality via Python, please ensure you have the latest Python template installed: + + + +```bash +$ spin templates install --git https://github.com/fermyon/spin-python-sdk --update +``` + +As a standard practice for Python, create and activate a virtual env: + +If you are on a Mac/linux based operating system use the following commands: + +```bash +$ python3 -m venv venv +$ source venv/bin/activate +``` + +If you are using Windows, use the following commands: + +```bash +C:\Work> python3 -m venv venv +C:\Work> venv\Scripts\activate +``` + +## Licenses + +> This tutorial uses Meta AI's Llama 2, Llama Chat and Code Llama models you will need to visit Meta's Llama webpage and agree to Meta's License, Acceptable Use Policy, and to Meta’s privacy policy before fetching and using Llama models. + +## Serverless AI Inferencing With Spin + +Now, let's write your first Serverless AI application with Spin. + +### Creating a New Spin Application + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +The Rust code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/). + + + +```bash +$ spin new -t http-rust +Enter a name for your new application: hello-world +Description: My first Serverless AI app +HTTP path: /... +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + +The Python code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/). + + + +```bash +# Create new app +$ spin new -t http-py hello-world --accept-defaults +# Change into app directory +$ cd hello-world +Enter a name for your new application: hello-world +Description: My first Serverless AI app +HTTP path: /... +``` + +Create a virtual environment directory (we are still inside the Spin app directory): + + + +```console +# python -m venv +$ python3 -m venv venv-dir +``` + +Activate the virtual environment (this command depends on which operating system you are using): + + + +```console +# macOS command to activate +$ source venv-dir/bin/activate +``` + +The `(venv-dir)` will prefix your terminal prompt now: + + + +```console +(venv-dir) user@123-456-7-8 hello-world % +``` + +The `requirements.txt`, by default, contains the references to the `spin-sdk` and `componentize-py` packages. These can be installed in your virtual environment using the following command: + + + +```bash +$ pip3 install -r requirements.txt +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + +The TypeScript code snippets below are taken from the [Fermyon Serverless AI Examples](https://github.com/fermyon/ai-examples/). + + + +```bash +$ spin new -t http-ts +Enter a name for your new application: hello-world +Description: My first Serverless AI app +HTTP path: /... +``` + +{{ blockEnd }} +{{ blockEnd }} + +### Configuring Your Application + +The `spin.toml` file is the manifest file which tells Spin what events should trigger what components. Configure the `[component.hello-world]` section of our application's manifest explicitly naming our model of choice. For this example, we specify the `llama2-chat` value for our `ai_models` configuration: + +```toml +ai_models = ["llama2-chat"] +``` + +This is what your `spin.toml` file should look like, based on whether you’re using Rust, TypeScript or Python: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + + + +```toml +spin_manifest_version = 2 + +[application] +name = "hello-world" +version = "0.1.0" +authors = ["Your Name "] +description = "My first Serverless AI app" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "target/wasm32-wasi/release/hello_world.wasm" +allowed_outbound_hosts = [] +ai_models = ["llama2-chat"] +[component.hello-world.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + + + +```toml +spin_manifest_version = 2 + +[application] +authors = ["Your Name "] +description = "My first Serverless AI app" +name = "hello-world" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "target/hello-world.wasm" +exclude_files = ["**/node_modules"] +ai_models = ["llama2-chat"] +[component.hello-world.build] +command = "npm run build" +``` + +{{ blockEnd }} + +{{ startTab "Python" }} + + + +```toml +spin_manifest_version = 2 + +[application] +authors = ["Your Name "] +description = "" +name = "hello-world" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "app.wasm" +ai_models = ["llama2-chat"] +[component.hello-world.build] +command = "componentize-py -w spin-http componentize app -o app.wasm" +watch = ["*.py", "requirements.txt"] +``` + +{{ blockEnd }} +{{ blockEnd }} + +### Source Code + +Now let's use the Spin SDK to access the model from our app. Executing inference from a LLM is a single line of code. Add the `Llm` and the `InferencingModels` to your app and use the `Llm.infer` to execute an inference. Here’s how the code looks: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +```rust +use spin_sdk::{http::{IntoResponse, Request, Response}, http_component, llm}; + +#[http_component] +fn hello_world(_req: Request) -> anyhow::Result { + let model = llm::InferencingModel::Llama2Chat; + let inference = llm::infer(model, "Can you tell me a joke about cats"); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(format!("{:?}", inference)) + .build()) +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +```tsx +import { Llm, InferencingModels, HandleRequest, HttpRequest, HttpResponse } from "@fermyon/spin-sdk" +const model = InferencingModels.Llama2Chat +export const handleRequest: HandleRequest = async function (request: HttpRequest): Promise { +const prompt = "Can you tell me a joke about cats" +const out = Llm.infer(model, prompt) +return { + status: 200, + body: out.text + } +} +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +```python +from spin_sdk import http +from spin_sdk.http import Request, Response +from spin_sdk import llm + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + res = llm.infer_with_options("llama2-chat", "Can you tell me a joke about cats?", llm.LLMInferencingParams(temperature=0.5)) + return Response( + 200, + {"content-type": "text/plain"}, + bytes(res.text, "utf-8") + ) + + +``` + +{{ blockEnd }} + +{{ blockEnd }} + +### Building and Deploying Your Spin Application + +Now that you have written your first Serverless AI app, it’s time to build and deploy it. To build your app run the following commands from inside your app’s folder (where the `spin.toml` file is located): + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + + + +```bash +$ spin build +``` + +{{ blockEnd }} + +{{ startTab "TypeScript" }} + + + +```bash + +$ npm install +$ spin build + +``` + +{{ blockEnd }} +{{ startTab "Python" }} + + + +```bash +$ spin build +``` + +{{ blockEnd }} + +Now that your app is built, there are three ways to test your Serverless AI app. One way to test the app is to run inferencing locally. This means running a LLM on your CPU. This is not as optimal compared to deploying to Fermyon’s Serverless AI, which uses high-powered GPUs in the cloud. To know more about this method, including downloading LLMs to your local machine, check out [the in-depth tutorial](ai-sentiment-analysis-api-tutorial) on Building a Sentiment Analysis API using Serverless AI. + +Here are the two other methods for testing your app: + +**Deploy the app to the Fermyon Cloud** + +You can deploy the app to the cloud by using the `spin deploy` command. In case you have not logged into your account before deploying your application, you need to grant access via a one-time token. Follow the instructions in the prompt to complete the auth process. + +Once you have logged in and the app is deployed, you will see a URL, upon successful deployment. The app is now deployed and can be accessed by anyone with the URL: + +```bash +$ spin deploy + +>Uploading hello-world version 0.1.0+ra01f74e2... +Deploying... +Waiting for application to become ready...... ready +Available Routes: +hello-world: https://hello-world-XXXXXX.fermyon.app (wildcard) +``` + +The app’s manifest file reads the line `ai_models = ["llama2-chat"]` and uses that model in the cloud. For any changes to take effect in the app, it needs to be re-deployed to the cloud. + +**Using the Cloud-GPU plugin to test locally** + +To avoid having to deploy the app for every change, you can use the [Cloud-GPU plugin](https://github.com/fermyon/spin-cloud-gpu) to deploy locally, with the LLM running in the cloud. While the app is hosted locally (running on `localhost`), every inferencing request is sent to the LLM that is running in the cloud. Follow the steps to use the `cloud-gpu` plugin. + +**Note**: This plugin works only with spin v1.5.1 and above. + +First, install the plugin using the command: + +```bash +$ spin plugins install -u https://github.com/fermyon/spin-cloud-gpu/releases/download/canary/cloud-gpu.json -y +``` + +Let’s initialize the plugin. This command essentially deploys the Spin app to a Cloud GPU proxy and generates a runtime-config: + +```bash +$ spin cloud-gpu init + +[llm_compute] +type = "remote_http" +url = "https://fermyon-cloud-gpu-.fermyon.app" +auth_token = "" +``` + +In the root of your Spin app directory, create a file named `runtime-config.toml` and paste the runtime-config generated in the previous step. + +Now you are ready to test the Serverless AI app locally, using a GPU that is running in the cloud. To deploy the app locally you can use `spin up` (or `spin watch`) but with the following flag: + +```bash +$ spin up --runtime-config-file + +Logging component stdio to ".spin/logs/" +Serving http://127.0.0.1:3000 +Available Routes: +hello-world: http://127.0.0.1:3000 (wildcard) +``` + +## Next Steps + +This was just a small example of what Serverless AI Inferencing can do. To check out more detailed code samples: + +- Read [our in-depth tutorial](ai-sentiment-analysis-api-tutorial) on building a Sentiment Analysis API with Serverless AI +- Look at the [Serverless AI API Guide](serverless-ai-api-guide) +- Try the numerous Serverless AI examples in our GitHub repository called [ai-examples](https://github.com/fermyon/ai-examples). +- [Contribute](../../hub/contributing) your Serverless AI app to our [Spin Hub](../../hub/index). +- Ask questions and share your thoughts in [our Discord community](https://discord.gg/AAFNfS7NGf). +--- diff --git a/content/spin/v3/spin-application-structure.md b/content/spin/v3/spin-application-structure.md new file mode 100644 index 000000000..02fda5614 --- /dev/null +++ b/content/spin/v3/spin-application-structure.md @@ -0,0 +1,218 @@ +title = "Application Structure" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/spin-application-structure.md" +keywords = "structure" + +--- + +A Spin application can contain multiple components. If more than one component is built from source, you should consider how to organise the application project. + +> If you have multiple components, but all except one is downloaded from a remote source such as a URL, you don't need to worry about the additional structure recommended on this page here. For example, an application with one custom component and two fileserver components doesn't need to start from the `empty` template. + +The discussion on this page assumes that you know from the start that you are going to need to build multiple components. If you've already started a project, you may need to move some existing code around when you add your second built-from-source component. For information about this, see [this Spin Application Structure blog post](https://www.fermyon.com/blog/spin-application-structure) or [this video](https://www.youtube.com/watch?v=QQD-qodabSc). + +> The blog post and video show manifest changes in the Spin 1 format. In the Spin 2 manifest format, the changes are similar, affecting the component `source` and `build` sections. + +## Recommended Application Structure + +If we start with a blank canvas and use the `http-empty` template we will get a new Spin application: + + + +```console +$ spin new -t http-empty +Enter a name for your new application: myapp +Description: My application +``` + +The above command will provide an empty structure, as shown below: + + + +```console +└── myapp + └── spin.toml +``` + +To add new components to the application, we simply move into the `myapp` directory and begin to add components using the `spin add` subcommand: + + + +```console +$ spin add -t http-rust +Enter a name for your new component: first-http-rust-component +Description: The first of many new components +HTTP path: /first/... +$ spin add -t http-rust +Enter a name for your new component: second-http-rust-component +Description: The second of many new components +HTTP path: /second/... +``` + +After adding two new components, we can see the visual representation of our application. Notice the symmetry; there is no hierarchy or nesting of components: + + + +```console +. +├── first-http-rust-component +│ ├── Cargo.toml +│ └── src +│ └── lib.rs +├── second-http-rust-component +│ ├── Cargo.toml +│ └── src +│ └── lib.rs +└── spin.toml +``` + +To customize each of the two components, we can modify the `lib.rs` (Rust source code) of each component: + +```rust +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +fn handle_first_http_rust_component(req: Request) -> anyhow::Result { + println!("Handling request to {:?}", req.header("spin-full-url")); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, First Component") + .build()) +} +``` + +```rust +#[http_component] +fn handle_second_http_rust_component(req: Request) -> anyhow::Result { + println!("Handling request to {:?}", req.header("spin-full-url")); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Second Component") + .build()) +} +``` + +As an additional example of adding more components, let's add a new static file server component: + + + +```console +$ spin add -t static-fileserver +Enter a name for your new component: assets +HTTP path: /static/... +Directory containing the files to serve: assets +``` + +After the static file server component is added, we create the `assets` directory (our local directory containing the files to serve) and then add some static content into the `assets` directory to be served: + + + +```console +$ mkdir assets +$ cp ~/Desktop/my-static-image.jpg assets +$ cp ~/Desktop/old.txt assets +$ cp ~/Desktop/new.txt assets +``` + +> The above commands are just an example that assumes you have the image (`my-static-image.jpg`) and two text files (`old.txt` & `new.txt`) on your Desktop. + +Why stop there? We can add even more functionality to our application. Let's now add a `redirect` component to redirect requests made to `/static/old.txt` and forward those through to `/static/new.txt`: + + + +```console +$ spin add -t redirect +Enter a name for your new component: additional-component-redirect +Redirect from: /static/old.txt +Redirect to: /static/new.txt +``` + +We now have 4 separate components scaffolded for us by Spin. Note the application manifest (the `spin.toml` file) is correctly configured based on our `spin add` commands: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "myapp" +version = "0.1.0" + +[[trigger.http]] +route = "/first/..." +component = "first-http-rust-component" + +[component.first-http-rust-component] +source = "first-http-rust-component/target/wasm32-wasi/release/first_http_rust_component.wasm" +allowed_outbound_hosts = [] +[component.first-http-rust-component.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "first-http-rust-component" +watch = ["src/**/*.rs", "Cargo.toml"] + +[[trigger.http]] +route = "/second/..." +component = "second-http-rust-component" + +[component.second-http-rust-component] +source = "second-http-rust-component/target/wasm32-wasi/release/second_http_rust_component.wasm" +allowed_outbound_hosts = [] +[component.second-http-rust-component.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "second-http-rust-component" +watch = ["src/**/*.rs", "Cargo.toml"] + +[[trigger.http]] +route = "/static/..." +component = "assets" + +[component.assets] +source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.1.0/spin_static_fs.wasm", digest = "sha256:96c76d9af86420b39eb6cd7be5550e3cb5d4cc4de572ce0fd1f6a29471536cb4" } +files = [ { source = "assets", destination = "/" } ] + +[[trigger.http]] +component = "additional-component-redirect" +route = "/static/old.txt" + +[component.additional-component-redirect] +source = { url = "https://github.com/fermyon/spin-redirect/releases/download/v0.1.0/redirect.wasm", digest = "sha256:8bee959843f28fef2a02164f5840477db81d350877e1c22cb524f41363468e52" } +environment = { DESTINATION = "/static/new.txt" } +``` + +Also, note that the application's folder structure, scaffolded for us by Spin via the spin add commands, is symmetrical and shows no nesting of components: + + + +```console +├── assets +│ ├── my-static-image.jpg +│ ├── new.txt +│ └── old.txt +├── first-http-rust-component +│ ├── Cargo.toml +│ └── src +│ └── lib.rs +├── second-http-rust-component +│ ├── Cargo.toml +│ └── src +│ └── lib.rs +└── spin.toml +``` + +This is the recommended Spin application structure. + +## Next Steps + +- Learn about how to [build your Spin application code](build) +- Try [running your application locally](running-apps) +- Discover how Spin application authors [design and organise applications](see-what-people-have-built-with-spin) +- Learn about how to [configure your application at runtime](dynamic-configuration) +- Look up details in the [application manifest reference](manifest-reference) +- Try deploying a Spin application to the [Fermyon Cloud](/cloud/quickstart) \ No newline at end of file diff --git a/content/spin/v3/sqlite-api-guide.md b/content/spin/v3/sqlite-api-guide.md new file mode 100644 index 000000000..3641edee4 --- /dev/null +++ b/content/spin/v3/sqlite-api-guide.md @@ -0,0 +1,290 @@ +title = "SQLite Storage" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/sqlite-api-guide.md" + +--- +- [Granting SQLite Database Permissions to Components](#granting-sqlite-database-permissions-to-components) +- [Using SQLite Storage From Applications](#using-sqlite-storage-from-applications) +- [Preparing an SQLite Database](#preparing-an-sqlite-database) +- [Custom SQLite Databases](#custom-sqlite-databases) + - [Granting Access to Custom SQLite Databases](#granting-access-to-custom-sqlite-databases) + +Spin provides an interface for you to persist data in an SQLite Database managed by Spin. This database allows Spin developers to persist relational data across application invocations. + +{{ details "Why do I need a Spin interface? Why can't I just use my own external database?" "You can absolutely still use your own external database either with the [MySQL or Postgres APIs](./rdbms-storage). However, if you're interested in quick, local relational storage without any infrastructure set-up then Spin's SQLite Database is a great option." }} + +## Granting SQLite Database Permissions to Components + +By default, a given component of an app will not have access to any SQLite Databases. Access must be granted specifically to each component via the component manifest. For example, a component could be given access to the default store using: + +```toml +[component.example] +sqlite_databases = ["default"] +``` + +> Note: To deploy your Database application to Fermyon Cloud using `spin cloud deploy`, see the [SQLite Database](/cloud/noops-sql-db#accessing-private-beta) section in the documentation. It covers signing up for the private beta and setting up your Cloud database tables and initial data. + +## Using SQLite Storage From Applications + +The Spin SDK surfaces the Spin SQLite Database interface to your language. + +The set of operations is common across all SDKs: + +| Operation | Parameters | Returns | Behavior | +|------------|------------|---------|----------| +| `open` | name | connection | Open the database with the specified name. If `name` is the string "default", the default database is opened, provided that the component that was granted access in the component manifest from `spin.toml`. Otherwise, `name` must refer to a store defined and configured in a [runtime configuration file](./dynamic-configuration.md#sqlite-storage-runtime-configuration) supplied with the application.| +| `execute` | connection, sql, parameters | database records | Executes the SQL statement and returns the results of the statement. SELECT statements typically return records or scalars. INSERT, UPDATE, and DELETE statements typically return empty result sets, but may return values in some cases. The `execute` operation recognizes the [SQLite dialect of SQL](https://www.sqlite.org/lang.html). | +| `close` | connection | - | Close the specified `connection`. | + +The exact detail of calling these operations from your application depends on your language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +Please note, we use `serde` in this Rust example, so please add `serde` as a dependency in your application's `Cargo.toml` file: + +```toml +[dependencies] +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +``` + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/sqlite/index.html) + +SQLite functions are available in the `spin_sdk::sqlite` module. The function names match the operations above. For example: + +```rust +use anyhow::Result; +use serde::Serialize; +use spin_sdk::{ + http::{Request, Response, IntoResponse}, + http_component, + sqlite::{Connection, Value}, +}; + +#[http_component] +fn handle_request(req: Request) -> Result { + let connection = Connection::open_default()?; + + let execute_params = [ + Value::Text("Try out Spin SQLite".to_owned()), + Value::Text("Friday".to_owned()), + ]; + connection.execute( + "INSERT INTO todos (description, due) VALUES (?, ?)", + execute_params.as_slice(), + )?; + + let rowset = connection.execute( + "SELECT id, description, due FROM todos", + &[] + )?; + + let todos: Vec<_> = rowset.rows().map(|row| + ToDo { + id: row.get::("id").unwrap(), + description: row.get::<&str>("description").unwrap().to_owned(), + due: row.get::<&str>("due").unwrap().to_owned(), + } + ).collect(); + + let body = serde_json::to_vec(&todos)?; + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(body) + .build()) +} + +// Helper for returning the query results as JSON +#[derive(Serialize)] +struct ToDo { + id: u32, + description: String, + due: String, +} +``` + +**General Notes** +* All functions are on the `spin_sdk::sqlite::Connection` type. +* Parameters are instances of the `Value` enum; you must wrap raw values in this type. +* The `execute` function returns a `QueryResult`. To iterate over the rows use the `rows()` function. This returns an iterator; use `collect()` if you want to load it all into a collection. +* The values in rows are instances of the `Value` enum. However, you can use `row.get(column_name)` to extract a specific column from a row. `get` casts the database value to the target Rust type. If the compiler can't infer the target type, write `row.get::<&str>(column_name)` (or whatever the desired type is). +* All functions wrap the return in `Result`, with the error type being `spin_sdk::sqlite::Error`. + +{{ blockEnd }} + +{{ startTab "Typescript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/modules/Sqlite.html) + +To use SQLite functions, use [the `Sqlite.open` or `Sqlite.openDefault` function](https://fermyon.github.io/spin-js-sdk/modules/Sqlite.html) to obtain [a `SqliteConnection` object](https://fermyon.github.io/spin-js-sdk/interfaces/Sqlite.SqliteConnection.html). `SqliteConnection` provides the `execute` method as described above. For example: + +```javascript +import { ResponseBuilder, Sqlite } from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + let conn = Sqlite.openDefault(); + let result = conn.execute("SELECT * FROM todos WHERE id > (?);", [1]); + + res.send(JSON.stringify(result)); +} +``` + +**General Notes** +* The `execute` function returns an object with `rows` and `columns` properties. `columns` is an array of strings representing column names. `rows` is an array of rows, each of which is an object containing Javascript values keyed using the column names. +* The `SqliteConnection` object doesn't surface the `close` function. + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/sqlite.html) + +To use SQLite functions, use the `sqlite` module in the Python SDK. The [`sqlite_open`](https://fermyon.github.io/spin-python-sdk/sqlite.html#spin_sdk.sqlite.open) and [`sqlite_open_default`](https://fermyon.github.io/spin-python-sdk/sqlite.html#spin_sdk.sqlite.open_default) functions return a [connection object](https://fermyon.github.io/spin-python-sdk/wit/imports/sqlite.html#spin_sdk.wit.imports.sqlite.Connection). The connection object provides the [`execute` method](https://fermyon.github.io/spin-python-sdk/wit/imports/sqlite.html#spin_sdk.wit.imports.sqlite.Connection.execute) as described above. For example: + +```python +from spin_sdk import http, sqlite +from spin_sdk.http import Request, Response +from spin_sdk.sqlite import ValueInteger + +class IncomingHandler(http.IncomingHandler): + def handle_request(self, request: Request) -> Response: + with sqlite.open_default() as db: + result = db.execute("SELECT * FROM todos WHERE id > (?);", [ValueInteger(1)]) + rows = result.rows + + return Response( + 200, + {"content-type": "text/plain"}, + bytes(str(rows), "utf-8") + ) +``` + +**General Notes** +* The `execute` method returns [a `QueryResult` object](https://fermyon.github.io/spin-python-sdk/wit/imports/sqlite.html#spin_sdk.wit.imports.sqlite.QueryResult) with `rows` and `columns` methods. `columns` returns a list of strings representing column names. `rows` is an array of rows, each of which is an array of [`RowResult`](https://fermyon.github.io/spin-python-sdk/wit/imports/sqlite.html#spin_sdk.wit.imports.sqlite.RowResult) in the same order as `columns`. +* The connection object doesn't surface the `close` function. +* Errors are surfaced as exceptions. + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/sqlite) + +The Go SDK is implemented as a driver for the standard library's [database/sql](https://pkg.go.dev/database/sql) interface. + +```go +package main + +import ( + "encoding/json" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/sqlite" +) + +type Todo struct { + ID string + Description string + Due string +} + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + db := sqlite.Open("default") + defer db.Close() + + _, err := db.Exec("INSERT INTO todos (description, due) VALUES (?, ?)", "Try out Spin SQLite", "Friday") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + rows, err := db.Query("SELECT id, description, due FROM todos") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var todos []*Todo + for rows.Next() { + var todo Todo + if err := rows.Scan(&todo.ID, &todo.Description, &todo.Due); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + todos = append(todos, &todo) + } + json.NewEncoder(w).Encode(todos) + }) +} + +func main() {} +``` + +**General Notes** + +A convenience function `sqlite.Open()` is provided to create a database connection. Because the `http.Handle` function is inside the `init()` function the Spin SQLite driver cannot be initialized the same way as other drivers using [sql.Open](https://pkg.go.dev/database/sql#Open). + +{{ blockEnd }} + +{{ blockEnd }} + +## Preparing an SQLite Database + +Although Spin provides SQLite as a built-in database, SQLite still needs you to create its tables. In most cases, the most convenient way to do this is to use the `spin up --sqlite` option to run whatever SQL statements you need before your application starts. This is typically used to create or alter tables, but can be used for whatever other maintenance or troubleshooting tasks you need. + +You can run a SQL script from a file using the `@filename` syntax: + + + +```bash +spin up --sqlite @migration.sql +``` + +Or you can pass SQL statements directly on the command line as a (quoted) string: + + + +```bash +spin up --sqlite "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, due TEXT NOT NULL)" +``` + +As with runtime operations, this flag uses the [SQLite dialect of SQL](https://www.sqlite.org/lang.html). + +You can provide the `--sqlite` flag more than once; Spin runs the statements (or files) in the order you provide them, and waits for each to complete before running the next. + +> It's also possible to create tables from your Wasm components using the usual `execute` function. That can end up mingling your "hot path" application logic with database maintenance code; decide which approach is best based on your application's needs. + +## Custom SQLite Databases + +Spin defines a database named `"default"` and provides automatic backing storage. If you need to customize Spin with additional databases, or to change the backing storage for the default database, you can do so via the `--runtime-config-file` flag and the `runtime-config.toml` file. See [SQLite Database Runtime Configuration](./dynamic-configuration#sqlite-storage-runtime-configuration) for details. + +### Granting Access to Custom SQLite Databases + +As mentioned above, by default, a given component of an app will not have access to any SQLite Databases. Access must be granted specifically to each component via the component manifest, using the `component.sqlite_databases` field in the manifest. + +Components can be given access to different databases, and may be granted access to more than one database. For example: + +```toml +# c1 has no access to any databases +[component.example] +name = "c1" + +# c2 can use the default database, but no custom databases +[component.example] +name = "c2" +sqlite_databases = ["default"] + +# c3 can use the custom databases "marketing" and "sales", which must be +# defined in the runtime config file, but cannot use the default database +[component.example] +name = "c3" +sqlite_databases = ["marketing", "sales"] +``` diff --git a/content/spin/v3/template-authoring.md b/content/spin/v3/template-authoring.md new file mode 100644 index 000000000..a8fc15a8a --- /dev/null +++ b/content/spin/v3/template-authoring.md @@ -0,0 +1,149 @@ +title = "Creating Spin templates" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/template-authoring.md" + +--- +- [Authoring the Content](#authoring-the-content) + - [Expression Syntax](#expression-syntax) +- [Authoring the Manifest](#authoring-the-manifest) +- [Supporting `spin add`](#supporting-spin-add) +- [Hosting Templates in Git](#hosting-templates-in-git) + +Spin templates allow a Spin developer to quickly create the skeleton of an +application or component, ready for the application logic to be filled in. + +A template consists of two directories, `content` and `metadata`. + +* The `content` directory contains all the files you'd like to be copied into + the Spin application directory, such as source code, the `spin.toml` file, + standard assets, precompiled modules, etc. These files can contain placeholders + so the user of the template can customize the end result. +* The `metadata` directory contains the files that control how the template is + instantiated. In this version of Spin, the only file in this directory + should be the _template manifest_. + +For examples of the directory contents, see the `templates` directory in the +[Spin GitHub repository](https://github.com/fermyon/spin). + +Templates must always be shared in a `templates` directory. This allows the +installer to locate them in repos that contain other content. + +## Authoring the Content + +Copy all the files that you want to be copied as part of the template into +the `content` directory. If you do nothing more, they will be copied +verbatim. Often, though, you'll want to allow the user to put their own +values in - for example, a project name, or an HTTP route. + +To do this, replace the text you want the user to be able to substitute +with an expression of the form `{{parameter-name}}`, where `parameter-name` +is an identifier of your choice. **You will need to add an entry to +the manifest matching this name** - see below. + +You can reuse a parameter in more than one place - it will be prompted only once and will get the same value in each place. + +You can also transform the user value by specifying a filter after a bar: +`{{parameter-name | filter-name}}`. This is particularly useful when you +want to conform to specific language conventions. The following filters +are supported: + +| Name | Effect | +|---------------|--------| +| `kebab_case` | Transforms input into kebab case, e.g. `My Application` to `my-application` | +| `snake_case` | Transforms input into snake case, e.g. `My Application` to `my_application` | +| `pascal_case` | Transforms input into Pascal case, e.g. `my application` to `MyApplication` | + +### Expression Syntax + +Content uses [the Liquid template language](https://shopify.github.io/liquid/). See the Liquid documentation for the available syntax and control tags. + +A common pitfall occurs because some entries in `spin.toml`, such as [component variable templates](variables), use the same double-brace syntax as Liquid does. If you want to generate a line such as `my-secret = "{{ secret }}"`, you must escape the double braces, for example using the Liquid [`raw` tag](https://shopify.github.io/liquid/tags/template/). If you don't do this, Liquid will look for a template parameter called `secret` instead! + +## Authoring the Manifest + +The template manifest is a TOML file. It must be named `spin-template.toml`: + + + +```toml +manifest_version = "1" +id = "my-application" +description = "An application" +tags = ["my-tag"] + +[parameters] +# Example parameter +project-name = { type = "string", prompt = "Project name" } +``` + +* `manifest_version` specifies the format this manifest follows. It must be `"1"`. +* `id` is however you want users to refer to your template in `spin new`. + It may contain letters, digits, hypens and underscores. +* `description` is optional. It is shown when displaying the template. +* `tags` is optional. These are used to enable discoverability via the Spin CLI. + For example, `spin new --tag my-tag` will prompt selection for a template containing `"my-tag"`. + +The `parameters` table is where you list the placeholders that you edited +into your content for the user to substitute. You should include an entry +for each parameter. The key is the parameter name, and the value a JSON +document that contains at minimum a `type` and `prompt`. `type` must +currently be `string`. `prompt` is displayed when prompting the user +for the value to substitute. + +The document may also have a `default`, which will be displayed to the user +and can be accepted by pressing Enter. It may also specify constraints +on what the user is allowed to enter. The following constraints are +supported: + +| Key | Value and usage | +|---------------|-----------------| +| `pattern` | A regular expression. The user input must match the regular expression to be accepted. | + +## Supporting `spin add` + +The `spin add` command lets users add your template as a new component in +an existing application. If you'd like to support this, you'll need to +add a few items to your metadata. + +* In the `metadata` directory, create a folder named `snippets`. In that + folder, create a file containing the (templated) manifest _just_ for the + component to be added. + * Don't include any application-level entries, just the component section. + * If your template contains component files, remember they will be copied + into a subdirectory, and make sure any paths reflect that. +* In the `spin-template.toml` file, add a table called `add_component`, with + the following entries: + +| Key | Value and usage | +|-----------------|-----------------| +| `snippets` | A subtable with an entry named `component`, whose value is the name of the file containing the component manifest template. (Don't include the `snippets` directory prefix - Spin knows to look in the `snippets` directory.) | +| `skip_files` | Optional array of content files that should _not_ be copied when running in "add component" mode. For example, if your template contains a `spin.toml` file, you should use this setting to exclude that, because you want to add a new entry to the existing file, not overwrite it. | +| `skip_parameters` | Optional array of parameters that Spin should _not_ prompt for when running in "add component" mode. | + +Here is an example `add_component` table from a HTTP template: + + + +```toml +[add_component] +skip_files = ["spin.toml"] +skip_parameters = ["shared-prefix"] +[add_component.snippets] +component = "component.txt" +``` + +> For examples from the Spin project, see `http-rust` and `static-fileserver`. + +## Hosting Templates in Git + +You can publish templates in a Git repo. The templates must be in the `/templates` +directory, with a subdirectory per template. + +When a user installs templates from your repo, by default Spin looks for a tag +to identify a compatible version of the templates. This tag is of the +form `spin/templates/vX.Y`, where X is the major version, and Y the minor +version, of the user's copy of Spin. For example, if the user is on +Spin 0.3.1, templates will be installed from `spin/templates/v0.3`. If this +tag does not exist, Spin installs templates from `HEAD`. diff --git a/content/spin/v3/testing-apps.md b/content/spin/v3/testing-apps.md new file mode 100644 index 000000000..59060e934 --- /dev/null +++ b/content/spin/v3/testing-apps.md @@ -0,0 +1,329 @@ +title = "Testing Applications" +template = "spin_main" +date = "2024-05-05T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/testing-apps.md" + +--- + +The [spin test plugin](https://github.com/fermyon/spin-test) allows you to run tests, written in WebAssembly, against a Spin application (where all Spin and WASI APIs are configurable mocks). + +To use `spin test` you write test scenarios for your app in any language with WebAssembly component support, and mock out all interactions your app has with the outside world without requiring any code changes to the app itself. That means the code you test in development is the same code that runs in production. + +> Note: `spin test` is still under active development and so the details here may have changed since this post was first published. Check [the spin-test repo](https://github.com/fermyon/spin-test) for the latest information on installation and usage. + +## Prerequisites + +The example below uses the [Rust programming language](https://www.rust-lang.org/) and [cargo components](https://github.com/bytecodealliance/cargo-component)(a cargo subcommand for building WebAssembly components). + +## Installing the Plugin + +To run `spin test` , you’ll first need to install the canary release of the plugin. As `spin test` matures, we’ll be making stable releases: + +```bash +spin plugin install -u https://github.com/fermyon/spin-test/releases/download/canary/spin-test.json +``` + +This will install the plugin which can be invoked with `spin test`. + +## Creating App and Component + +First, create an empty Spin app, change into that app folder and then add a component inside it: + + + +```bash +$ spin new -t http-empty my-app --accept-defaults +$ cd my-app/ +$ spin add -t http-rust my-component --accept-defaults +``` + +The above commands will scaffold out the application in the following format: + + + +```bash +my-app/ +├── my-component +│   ├── Cargo.toml +│   └── src +│   └── lib.rs +└── spin.toml +``` + +## Creating a Test Suite + +There is currently first-class support for writing tests in Rust, but any language with support for writing WebAssembly components can be used as long as the `fermyon:spin-test/test` world is targeted. You can find the definition of this world [here](https://github.com/fermyon/spin-test/blob/4dcaf79c10fc29a8da2750bdaa383b5869db1715/host-wit/world.wit#L13-L16). For this example, we'll use Rust. + +We use `cargo` to create a test suite, and then change into our newly created `tests` directory: + + + +```bash +$ cargo new tests --lib +$ cd tests +``` + +After running the cargo new command we will have the following application structure: + + + +```bash +my-app/ +├── my-component +│   ├── Cargo.toml +│   └── src +│   └── lib.rs +├── spin.toml +└── tests + ├── Cargo.toml + └── src + └── lib.rs +``` + +From within that test suite (from inside the `tests` directory), we then add the spin-test SDK reference: + + + +```bash +$ cargo add spin-test-sdk --git https://github.com/fermyon/spin-test +``` + +Then, we open the `Cargo.toml` file from within in the `tests` directory and edit to add the `crate-type` of `cdylib`: + +```toml +[lib] +crate-type = ["cdylib"] +``` + +The `my-app/tests/Cargo.toml` file will look like this after editing: + +```toml +[package] +name = "tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +spin-test-sdk = { git = "https://github.com/fermyon/spin-test", version = "0.1.0" } + +[lib] +crate-type = ["cdylib"] +``` + +## Writing a Test + +Next, create a test that `spin test` can run as a compiled WebAssembly component. + +In this example, we will write some tests appropriate to a JSON API service for information about service users. Here are two such tests written in Rust using the [Spin Test Rust SDK](https://github.com/fermyon/spin-test/tree/main/crates/spin-test-sdk). The first test ensures that the Spin app responds properly to an HTTP request. The second test ensures that the Spin app responds properly when the user data is present in the key-value store - for testing purposes, simulated by inserting it into a "virtual" store. + +Open the `my-app/tests/src/lib.rs` file and fill it with the following content: + +```rust +use spin_test_sdk::{ + bindings::{fermyon::spin_test_virt, wasi, wasi::http}, + spin_test, +}; + +#[spin_test] +fn send_get_request_without_key() { + // Perform the request + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_path_with_query(Some("/")).unwrap(); + let response = spin_test_sdk::perform_request(request); + + // Assert response status and body is 404 + assert_eq!(response.status(), 404); +} + +#[spin_test] +fn send_get_request_with_invalid_key() { + // Perform the request + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_path_with_query(Some("/x?123")).unwrap(); + let response = spin_test_sdk::perform_request(request); + + // Assert response status and body is 404 + assert_eq!(response.status(), 404); +} + +#[spin_test] +fn send_get_request_with_invalid_key_id() { + // Perform the request + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_path_with_query(Some("/user?0")).unwrap(); + let response = spin_test_sdk::perform_request(request); + + // Assert response status and body is 404 + assert_eq!(response.status(), 404); +} + +#[spin_test] +fn cache_hit() { + let user_json = r#"{"id":123,"name":"Ryan"}"#; + + // Configure the app's virtualised 'default' key-value store ready for the test + let key_value = spin_test_virt::key_value::Store::open("default"); + // Set a specific key with a specific value + key_value.set("123", user_json.as_bytes()); + + // Make the request against the Spin app + let request = wasi::http::types::OutgoingRequest::new(wasi::http::types::Headers::new()); + request.set_path_with_query(Some("/user?123")).unwrap(); + let response = spin_test_sdk::perform_request(request); + + // Assert the response status and body + assert_eq!(response.status(), 200); + let body = response.body_as_string().unwrap(); + assert_eq!(body, user_json); + + // Assert the key-value store was queried + assert_eq!( + key_value.calls(), + vec![spin_test_virt::key_value::Call::Get("123".to_owned())] + ); +} +``` + +The following points are intended to unpack the above example for your understanding: + +- Each function marked with `#[spin_test]` will be run as a separate test. +- Each test can perform any setup to their environment by using the APIs available in `spin_test_sdk::bindings::fermyon::spin_test_virt` +- After requests are made, you can use the APIs in `spin_test_sdk::bindings::fermyon::spin_test_virt` to make assertions that confirm that the request has been responded to (e.g. response status equals 200) or that expected Spin API calls (e.g. `get` to the key/value API) have been made. + +The tests above run inside of WebAssembly. Calls, such as Key Value storage and retrieval, never actually leave the WebAssembly sandbox. This means your tests are quick and reproducible as you don’t need to rely on running an actual web server, and you don’t need to ensure any of your app’s dependencies are running. Everything your app interacts with is mocked for you. The isolation benefits from this mocking mean that your application's actual data is never touched. There is also the added benefit of reproducibility whereby you never have to worry about leftover data from previous tests. + + +## Configure `spin-test` + +Before you can run the test, you'll need to tell `spin-test` where your test lives and how to build it. You do this from inside our app’s manifest (the `spin.toml` file). We change back up into our application's root directory: + + + +```bash +$ cd .. +``` + +Then we edit the `my-app` application's manifest (the `spin.toml` file) by adding the following block: + +```toml +[component.my-component.tool.spin-test] +source = "tests/target/wasm32-wasi/release/tests.wasm" +build = "cargo component build --release" +dir = "tests" +``` + +## Updating the App to Pass the Tests + +This section provides configuration and business logic at the application level. + +### Configure App Storage + +We are using Key Value storage in the application and therefore need to configure the list of allowed `key_value_stores` in our `spin.toml` file (in this case we are just using the `default`). Go ahead and paste the `key_value_stores` configuration directly inside the `[component.my-component]` section, as shown below: + + + +```toml +[component.my-component] +... +key_value_stores = ["default"] +... +``` + +> If you would like to learn more about Key Value storage, see [this tutorial](./key-value-store-tutorial.md). + +After editing, the whole `my-app/spin.toml` file will look like the following: + +```toml +spin_manifest_version = 2 + +[application] +name = "my-app" +version = "0.1.0" +authors = ["Fermyon Engineering "] +description = "" + +[[trigger.http]] +route = "/..." +component = "my-component" + +[component.my-component] +source = "my-component/target/wasm32-wasi/release/my_component.wasm" +allowed_outbound_hosts = [] +key_value_stores = ["default"] +[component.my-component.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "my-component" +watch = ["src/**/*.rs", "Cargo.toml"] + +[component.my-component.tool.spin-test] +source = "tests/target/wasm32-wasi/release/tests.wasm" +build = "cargo component build --release" +dir = "tests" +``` + +### Adding Business Logic + +The goal of tests is to ensure that the business logic in our application works as intended. We haven't yet updated our "business logic" frrom the "Hello, Fermyon" app. To make our new tests pass, copy and paste the following code into the application's source file (located at `my-app/my-component/src/lib.rs`): + +```rust +use spin_sdk::{ + http::{IntoResponse, Request, Response, Method}, + http_component, + key_value::Store, +}; + +#[http_component] +fn handle_request(req: Request) -> anyhow::Result { + // Open the default key-value store + let store = Store::open_default()?; + + let (status, body) = match *req.method() { + Method::Get => { + // Get the value associated with the request URI, or return a 404 if it's not present + match store.get(req.query())? { + Some(value) => { + println!("Found value for the key {:?}", req.query()); + (200, Some(value)) + } + None => { + println!("No value found for the key {:?}", req.query()); + (404, None) + } + } + } + // No other methods are currently supported + _ => (405, None), + }; + Ok(Response::new(status, body)) +} +``` + +## Building and Running the Test + +With the application's configuration and business logic done, we're ready for our test to be run. We can do this by invoking `spin-test` from the application's root directory (`my-app`): + + + +```bash +$ spin build +$ spin test + +running 4 tests +No value found for the key "123" +test request-with-invalid-key ... ok +Found value for the key "123" +test cache-hit ... ok +No value found for the key "0" +test request-with-invalid-key-id ... ok +No value found for the key "" +test request-without-key ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.08s +``` + +## Next Steps + +`spin-test` is still in the early days of development, so you’re likely to run into things that don’t quite work yet. We’d love to hear about your experience so we can prioritize which features and bugs to fix first. We’re excited about the future potential of using WebAssembly components for testing, and we look forward to hearing about your experiences as we continue the development of `spin-test`. + +You might also like to learn about `spin doctor` which is a command-line tool that detects and helps fix issues preventing applications from building and running. For more information see the [troubleshooting applications page](./troubleshooting-application-dev.md). \ No newline at end of file diff --git a/content/spin/v3/triggers.md b/content/spin/v3/triggers.md new file mode 100644 index 000000000..85b3ce438 --- /dev/null +++ b/content/spin/v3/triggers.md @@ -0,0 +1,264 @@ +title = "Triggers" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/triggers.md" + +--- +- [Triggers and Components](#triggers-and-components) + - [Mapping a Trigger to a Named Component](#mapping-a-trigger-to-a-named-component) + - [Writing the Component Inside the Trigger](#writing-the-component-inside-the-trigger) + - [Choosing Between the Approaches](#choosing-between-the-approaches) + - [Setting Up Multiple Trigger Types](#setting-up-multiple-trigger-types) +- [Spin Cron Trigger](#spin-cron-trigger) + - [Cron Trigger Expressions](#cron-trigger-expressions) + - [Installing the Cron Trigger Plugin](#installing-the-cron-trigger-plugin) + - [Installing the Cron Trigger Template](#installing-the-cron-trigger-template) + - [Creating the Web Application](#creating-the-web-application) + - [Inspecting the Source Code](#inspecting-the-source-code) + - [Building and Running the Application](#building-and-running-the-application) + +A Spin _trigger_ maps an event, such as an HTTP request or a Redis pub-sub message, to a component that handles that event. + +An application can contain multiple triggers. + +In Spin 2.2 and earlier, all triggers must be of the same type. For example, an application can contain triggers for multiple HTTP routes, or for multiple Redis pub-sub channels, but not both. + +In Spin 2.3 and later, an application can contain triggers of different types. For example, a single application can serve HTTP on one or more routes, and at the same time subscribe to one or more Redis pub-sub channels. + +> If you're familiar with [Spin 1.x](/spin/manifest-reference-v1#the-trigger-table), note that [Spin 2 uses the term "trigger"](/spin/manifest-reference#the-trigger-table) to refer to each individual route or channel, rather than the trigger type. It's closer to the `[component.trigger]` usage than to the application trigger. + +## Triggers and Components + +How events are specified depends on the type of trigger involved. For example, an [HTTP trigger](./http-trigger.md) is specified by the route it handles. A [Cron Trigger](#spin-cron-trigger) is specified by the schedule on which it runs. A [Redis trigger](./redis-trigger.md) is specified by the channel it monitors. A trigger always, however, has a `component` field, specifying the component that handles matching events. The `component` can be specified in two ways. + +### Mapping a Trigger to a Named Component + +An application manifest can define _named_ components in the `component` section. Each component is a WebAssembly component file (or reference) plus the supporting resources it needs, and metadata such as build information. The component name is written as part of the TOML `component` declaration. For example: + +```toml +[component.checkout] # The component's name is "checkout" +source = "target/wasm32-wasi/release/checkout.wasm" +allowed_outbound_hosts = ["https://payment-processing.example.com"] +key_value_stores = ["default"] +[component.checkout.build] +command = "cargo build --target wasm32-wasi --release" +``` + +To map a trigger to a named component, specify its name in the trigger's `component` field: + +```toml +[[trigger.http]] +route = "/cart/checkout" +component = "checkout" +``` + +### Writing the Component Inside the Trigger + +Instead of writing the component in a separate section and referencing it by name, you can write it the same fields _inline_ in the trigger `component` field. For example: + +```toml +# Using inline table syntax +[[trigger.http]] +route = "/cart/..." +component = { source = "dist/cart.wasm" } + +# Nested table syntax +[[trigger.http]] +route = "/cart/..." +[trigger.http.component] +source = "target/wasm32-wasi/release/checkout.wasm" +allowed_outbound_hosts = ["payment-processing.example.com"] +``` + +These behave in the same way: the inline table syntax is more compact for short declarations, but the nested table syntax is easier to read when there are many fields or the values are long. + +### Choosing Between the Approaches + +These ways of writing components achieve the same thing, so which should you choose? + +Named components have the following advantages: + +* Reuse. If you want two triggers to behave in the same way, they can refer to the same named component. Remember this means they are not just handled by the same Wasm file, but with the same settings. +* Named. If an error occurs, Spin can tell you the name of the component where the error happened. With inline components, Spin has to synthesize a name. This isn't a big deal in single-component apps, but makes diagnostics harder in larger apps. + +Inline components have the following advantages: + +* Compact, especially when using inline table syntax. +* One place to look. Both the trigger event and the handling details are always in the same piece of TOML. + +If you are not sure, or are not experienced, we recommend using named components at first, and adopting inline components as and when you find cases where you prefer them. + +### Setting Up Multiple Trigger Types + +In this section, we build an application that contains multiple triggers. + +Here is an example of creating an application with both HTTP and Redis triggers: + + + +```bash +# Start with an empty application +$ spin new -t http-empty multiple-trigger-example +Description: An application that handles both HTTP requests and Redis messages +# Change into to the application directory +$ cd multiple-trigger-example +# Add an HTTP trigger application +$ spin add -t http-rust rust-http-trigger-example +Description: A Rust HTTP example +HTTP path: /... +# Add a Redis trigger application +$ spin add -t redis-rust rust-redis-trigger-example +Description: A Rust redis example +Redis address: redis://localhost:6379 +Redis channel: one +``` + +The above `spin new` and `spin add` commands will scaffold a Spin manifest (`spin.toml` file) with the following triggers: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "multiple-trigger-example" +version = "0.1.0" +authors = ["Your Name "] +description = "An application that handles both HTTP requests and Redis messages" + +[[trigger.http]] +route = "/..." +component = "rust-http-trigger-example" + +[component.rust-http-trigger-example] +source = "rust-http-trigger-example/target/wasm32-wasi/release/rust_http_trigger_example.wasm" +allowed_outbound_hosts = [] +[component.rust-http-trigger-example.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "rust-http-trigger-example" +watch = ["src/**/*.rs", "Cargo.toml"] + +[application.trigger.redis] +address = "redis://localhost:6379" + +[[trigger.redis]] +channel = "one" +component = "rust-redis-trigger-example" + +[component.rust-redis-trigger-example] +source = "rust-redis-trigger-example/target/wasm32-wasi/release/rust_redis_trigger_example.wasm" +allowed_outbound_hosts = [] +[component.rust-redis-trigger-example.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "rust-redis-trigger-example" +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +## Cron Trigger + +Spin has experimental support for creating and running components on a schedule. Please note that there are only working Cron Trigger app samples written in [Rust](https://github.com/fermyon/spin-trigger-cron/tree/main/guest-rust) and [Python](https://github.com/fermyon/spin-trigger-cron/tree/main/guest-python) at present. + +> Please note: You can not `spin deploy` an application to Fermyon Cloud if it uses `cron` because non-HTTP triggers are not supported in Fermyon Cloud. + +Let's look at how the [experimental Cron trigger for Spin](https://github.com/fermyon/spin-trigger-cron) allows you to deploy an application that runs on a schedule. A Cron trigger maps a cron expression (a schedule) to a specific component. For example: + + + +```toml +[[trigger.cron]] +component = "hello-cron" +cron_expression = "1/2 * * * * *" +``` + +> Note: The 7th field (year) for the `cron_expression` is optional. + +### Cron Trigger Expressions + +The expression is based on the crontab (cron table) syntax whereby each line is made up of 7 fields that represent the time to execute. + +```bash +# ┌──────────── sec (0–59) +# | ┌───────────── min (0–59) +# | │ ┌───────────── hour (0–23) +# | │ │ ┌───────────── day of month (1–31) +# | │ │ │ ┌───────────── month (1–12) +# | │ │ │ │ ┌───────────── day of week (0–6) +# | │ │ │ │ | ┌─────────────- year +# | │ │ │ │ | │ +# | │ │ │ │ | │ + 1/30 * * * * * * +``` + +> For more information about setting the schedule, please see the [Spin Cron Trigger repository](https://github.com/fermyon/spin-trigger-cron?tab=readme-ov-file#trigger-configuration). + +Let's look at a time-based workload inside a Rust application. + +### Installing the Cron Trigger Plugin + +First, we install the plugin: + +```bash +spin plugins install --url https://github.com/fermyon/spin-trigger-cron/releases/download/canary/trigger-cron.json +``` + +### Installing the Cron Trigger Template + +Then, we install the template: + +```bash +spin templates install --git https://github.com/fermyon/spin-trigger-cron +``` + +### Creating the Application + +With the plugin and template installed, we create a new application: + +```bash +spin new -t cron-rust hello_cron --accept-defaults +``` + +### Inspecting the Source Code + +The Rust source code for this application is as follows: + +```Rust +use spin_cron_sdk::{cron_component, Error, Metadata}; +use spin_sdk::variables; + +#[cron_component] +async fn handle_cron_event(metadata: Metadata) -> Result<(), Error> { + let key = variables::get("something").unwrap_or_default(); + println!( + "[{}] Hello this is me running every {}", + metadata.timestamp, key + ); + Ok(()) +} +``` + +### Building and Running the Application + +We can immediately run this pre-written (template) application and observe the time-driven execution: + +```bash +cd hello_cron +spin build --up + +Building component hello-cron with `cargo build --target wasm32-wasi --release` + +... + +Finished building all Spin components +[1715640447] Hello from a cron component +[1715640449] Hello from a cron component +[1715640451] Hello from a cron component +[1715640453] Hello from a cron component +[1715640455] Hello from a cron component +[1715640457] Hello from a cron component +``` + +As we can see from the above output, our application is now running and executing the function every two seconds without the need for any incoming requests or any intervention from users or other machines. + +If you would like to learn more about using the Spin Cron Trigger, please check out [the Spin Cron Trigger blog post](https://www.fermyon.com/blog/spin-cron-trigger) and the [Spin Cron Trigger GitHub repository](https://github.com/fermyon/spin-trigger-cron). diff --git a/content/spin/v3/troubleshooting-application-dev.md b/content/spin/v3/troubleshooting-application-dev.md new file mode 100644 index 000000000..41b9b7275 --- /dev/null +++ b/content/spin/v3/troubleshooting-application-dev.md @@ -0,0 +1,46 @@ +title = "Troubleshooting Application Development" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/troubleshooting-application-dev.md" + +--- + +## Spin Doctor + +The `spin doctor` command detects problems that could stop your application building and running, and can help to fix them. These include problems like invalid manifests, missing Wasm files, and missing tools. + +To troubleshoot using `spin doctor`, run the command: + + + +```bash +$ spin doctor +``` + +> If you're not in the application directory, use the `-f` flag to tell the doctor which application to check + +Spin performs a series of checks on your application. If it finds a problem, it prints a description and, if possible, offers to fix it. Here's an example where a stray keystroke has messed up the version field in the application manifest: + + + +```bash +$ spin doctor +📟 The Spin Doctor is in. +🩺 Checking spin.toml... + +❗ Diagnosis: Manifest 'spin_manifest_version' must be "1", not "11" +🩹 The Spin Doctor can help! Would you like to: +> Set manifest version to "1" + Do nothing + See more details about the recommended changes +``` + +If `spin doctor` detects a problem it can fix, you can choose to accept the fix, skip it to fix manually later, or see more details before choosing. If `spin doctor` can't fix the problem, it displays the problem so you can make your own decision about how to fix it. + +> `spin doctor` is in an early stage of development, and there are many potential problems it doesn't yet check for. Please [raise an issue](https://github.com/fermyon/spin/issues/new?template=suggestion.md) if you have a problem you think `spin doctor` should check for. + +## Spin Test + +The [spin test plugin](https://github.com/fermyon/spin-test) allows you to write test scenarios for your application's business logic. For more information, see [testing applications](./testing-apps.md). \ No newline at end of file diff --git a/content/spin/v3/upgrade.md b/content/spin/v3/upgrade.md new file mode 100644 index 000000000..dd086d2a8 --- /dev/null +++ b/content/spin/v3/upgrade.md @@ -0,0 +1,134 @@ +title = "Upgrade Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/upgrade.md" + +--- +- [Are You on the Latest Version?](#are-you-on-the-latest-version) +- [Upgrade Spin](#upgrade-spin) + - [Installer](#installer) + - [Homebrew](#homebrew) + - [Cargo](#cargo) + - [Source](#source) +- [Troubleshooting](#troubleshooting) + - [Not Seeing the Latest Version?](#not-seeing-the-latest-version) + +## Are You on the Latest Version? + +The best way to know if you're on the latest version of Spin is to run `spin --version`: + + + +```bash +$ spin --version +``` + +You can compare the output from the above command with the [latest release release](https://github.com/fermyon/spin/releases/latest) listed in the Spin GitHub repository (which is also shown in the image below): + +![spin version image](https://img.shields.io/github/v/release/fermyon/spin) + +## Upgrade Spin + +Spin can be installed in many ways, and therefore the upgrade procedure can differ between users. Here are a few suggested ways to upgrade Spin to the latest version. If you're not sure how or where you installed your current version of Spin try using the `which` command on [Linux and macOS](https://linux.die.net/man/1/which) and the `where` command on [Windows](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/where), as shown below: + +{{ tabs "os" }} + +{{ startTab "Linux"}} + + + +```bash +$ which spin +``` + +{{ blockEnd }} + +{{ startTab "macOS"}} + + + +```bash +$ which spin +``` + +{{ blockEnd }} + +{{ startTab "Windows"}} + + + +```bash +$ where spin +``` + +{{ blockEnd }} + +{{ blockEnd }} + +### Installer + +If you originally followed the documentation's [installer script method](./install#installing-spin), please revisit to reinstall. + +> Hint: Your Spin executable will likely be in the following location: + + + +```bash +/usr/local/bin/spin +``` + +### Homebrew + +If you installed Spin using [Homebrew](https://brew.sh/) please use the following commands to upgrade Spin. + + + +```bash +$ brew update +$ brew upgrade fermyon/tap/spin +``` + +### Cargo + +If you originally followed the documentation's [Cargo install method](./install#using-cargo-to-install-spin), please revisit to reinstall. + +> Hint: Your Spin executable will likely be in the following location: + + + +```bash +~/.cargo/bin/spin +``` + +### Source + +If you followed the documentation's [install from source method](./install#building-spin-from-source) please revisit to reinstall. + +> Hint: Your Spin executable will likely be in the following location (where you originally cloned the Spin repository): + +```bash +~/spin/target/release/spin +``` + +## Troubleshooting + +If you have upgraded Spin and don't see the newer version, please consider the following. + +### Not Seeing the Latest Version? + +It may be possible that you have installed Spin **using more than one** of the above methods. In this case, the Spin executable that runs is the one that is listed first in your `PATH` system variable. + +If you have upgraded Spin yet still see the old version using `spin --version` this can be due to the order of precedence in your `PATH`. Try echoing your path to the screen and checking to see whether the location of your intended Spin executable is listed before or after other pre-existing installation paths: + +```bash +echo $PATH +/Users/my_user/.cargo/bin:/usr/local/bin +``` + +> Paths are separated by the `:` (colon) + +In the above case, the [Cargo install method](./install#using-cargo-to-install-spin)'s installation will take precedence over the [installer script method](./install#installing-spin)'s installation. + +In this case, you can either remove the Cargo installation of Spin using `cargo uninstall spin-cli` or update your system path to prioritize the Spin binary path that you prefer. diff --git a/content/spin/v3/url-shortener-tutorial.md b/content/spin/v3/url-shortener-tutorial.md new file mode 100644 index 000000000..cf0ada6df --- /dev/null +++ b/content/spin/v3/url-shortener-tutorial.md @@ -0,0 +1,169 @@ +title = "Building a URL Shortener With Spin" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/url-shortener-tutorial.md" + +--- +- [A Simple URL Shortener Built With Spin](#a-simple-url-shortener-built-with-spin) +- [Here Is One We Prepared Earlier](#here-is-one-we-prepared-earlier) +- [Conclusion](#conclusion) + +## A Simple URL Shortener Built With Spin + +This tutorial will walk you through building a Spin component that +redirects short URLs to their configured destinations. +In essence, this is a simple HTTP component that returns a response that contains +redirect information based on the user-defined routes. + +This is an evolving tutorial. As Spin allows building more complex components +(through supporting access to services like databases), this tutorial will be +updated to reflect that. + +## Here Is One We Prepared Earlier + +The complete implementation for this tutorial [can be found on GitHub](https://github.com/fermyon/url-shortener). + +First, our URL shortener allows users to configure their own final URLs — +currently, that is done through a configuration file that contains multiple +`[[route]]` entries, each containing the shortened path as `source`, and +the `destination` URL: + +```toml +[[route]] +source = "/spin" +destination = "https://github.com/fermyon/spin" + +[[route]] +source = "/hype" +destination = "https://www.fermyon.com/blog/how-to-think-about-wasm" +``` + +Whenever a request for `https:///spin` is sent, our component will +redirect to `https://github.com/fermyon/spin`. Now that we have a basic +understanding of how the component should behave, let's see how to implement it +using Spin. + +First, we start with [a new Spin component written in Rust](./rust-components.md): + +```rust +/// A Spin HTTP component that redirects requests +/// based on the router configuration. +#[http_component] +fn redirect(req: Request) -> Result { + let router = Router::default()?; + router.redirect(req) +} +``` + +All the component does is create a new router based on the default configuration, +then use it to redirect the request. Let's see how the router is defined: + +```rust +#[derive(Debug, Deserialize, Serialize)] +pub struct Route { + pub source: String, + pub destination: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Router { + #[serde(rename = "route")] + pub routes: Vec, +} +``` + +The `Router` structure is a Rust representation of the TOML configuration above. + +```rust +pub fn redirect(self, req: Request) -> Result { + // read the request path from the `spin-path-info` header + let path_info = req + .headers() + .get("spin-path-info") + .expect("cannot get path info from request headers"); + // if the path is not present in the router configuration, + // return 404 Not Found. + let route = match self.path(path_info.to_str()?) { + Some(r) => r, + None => return not_found(), + }; + // otherwise, return the redirect to the destination + let res = http::Response::builder() + .status(http::StatusCode::PERMANENT_REDIRECT) + .header(http::header::LOCATION, route.destination) + .body(None)?; + Ok(res) +} +``` + +The `redirect` function is straightforward — it reads the request path from the +`spin-path-info` header (make sure to read the [document about the HTTP trigger](./http-trigger.md) +for an overview of the HTTP headers present in Spin components), selects the +corresponding destination from the router configuration, then sends the +HTTP redirect to the new location. + +At this point, we can build and run the module with Spin: + +```bash +$ spin build +$ spin up +``` + +And the component can now handle incoming requests: + + + +```bash +# based on the configuration file, a request +# to /spin should be redirected +$ curl -i localhost:3000/spin +HTTP/1.1 308 Permanent Redirect +location: https://github.com/fermyon/spin +content-length: 0 +# based on the configuration file, a request +# to /hype should be redirected +$ curl -i localhost:3000/hype +HTTP/1.1 308 Permanent Redirect +location: https://www.fermyon.com/blog/how-to-think-about-wasm +content-length: 0 +# /abc is not present in the router configuration, +# so this returns a 404. +$ curl -i localhost:3000/abc +HTTP/1.1 404 Not Found +content-length: 9 + +Not Found +``` + +> Notice that you can use the `--listen` option for `spin up` to start the +> web server on a specific host and port, which you can then bind to a domain. + +We can now [publish the application to an OCI registry](./distributing-apps.md) (together +with router configuration file): + + + +```bash +$ spin registry push ghcr.io/alyssa-p-hacker/url-shortener:v1 +Pushed with digest sha256:3c408a1b29b3098286c7ea5ab22f47248ccfadcc63ad5596ca0d85e3f522c43d +``` + +And now we can run the application directly from the registry: + + + +```bash +$ spin up -f ghcr.io/alyssa-p-hacker/url-shortener:v1 + +Serving http://127.0.0.1:3000 +Available Routes: + shortener: http://127.0.0.1:3000 (wildcard) +``` + +## Conclusion + +In this tutorial we built a simple URL shortener as a Spin component. +In the future we will expand this tutorial by storing the router configuration +in a database supported by Spin, and potentially create another component that +can be used to add new routes to the configuration. diff --git a/content/spin/v3/variables.md b/content/spin/v3/variables.md new file mode 100644 index 000000000..3d64dbd68 --- /dev/null +++ b/content/spin/v3/variables.md @@ -0,0 +1,268 @@ +title = "Application Variables" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/variables.md" + +--- +- [Adding Variables to Your Applications](#adding-variables-to-your-applications) +- [Using Variables From Applications](#using-variables-from-applications) + +Spin supports dynamic application variables. Instead of being static, their values can be updated without modifying the application, creating a simpler experience for rotating secrets, updating API endpoints, and more. + +These variables are defined in a Spin application manifest (in the `[variables]` section), and their values can be set or overridden at runtime by an [application variables provider](./dynamic-configuration.md#application-variables-runtime-configuration). When running Spin locally, the variables provider can be [Hashicorp Vault](./dynamic-configuration.md#vault-application-variable-provider) for secrets, [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault), or host environment variables. + +For information about configuring application variables providers, refer to the [dynamic configuration documentation](./dynamic-configuration.md#application-variables-runtime-configuration). + +## Adding Variables to Your Applications + +Variables are added to an application under the top-level `[variables]` section of an application manifest (`spin.toml`). Each entry must either have a default value or be marked as `required = true`. “Required” entries must be [provided](./dynamic-configuration#application-variables-runtime-configuration) with a value. + +For example, consider an application which makes an outbound API call using a bearer token. To configure this using variables, you would: +* Add a `[variables]` section to the application manifest +* Add a `token` entry for the bearer token. Since there is no reasonable default value for this secret, set the variable as required with `required = true`. +* Add an `api_uri` variable. The URL _is_ known, but is useful to override, for example for A/B testing. So you can give this variable a default value with `default = "http://my-api.com"`. +The resulting application manifest looks like this: + + + +```toml +[variables] +api_token = { required = true } +api_uri = { default = "http://my-api.com" } +``` + +Variables are surfaced to a specific component by adding a `[component.(name).variables]` section to the component and referencing them within it. The `[component.(name).variables]` section contains a mapping of component variables and values. Entries can be static (like `api_version` below) or reference an updatable application variable (like `token` below) using [mustache](https://mustache.github.io/)-inspired string templates. Only components that explicitly use variables in their configuration section will get access to them. This enables only exposing variables (such as secrets) to the desired components of an application. + + + +```toml +[component.(name).variables] +token = "\{{ api_token }}" +api_uri = "\{{ api_uri }}" +api_version = "v1" +``` + +When a component variable references an application variable, its value will dynamically update as the application variable changes. For example, if the `api_token` variable is provided using the [Spin Vault provider](./dynamic-configuration.md#vault-application-variable-provider), it can be updated by changing the value in HashiCorp Vault. The next time the component gets the value of `token`, the latest value of `api_token` will be returned by the provider. See the [next section](#using-variables-from-applications) to learn how to use Spin's configuration SDKs to get configuration variables within applications. + +Variables can also be used in other sections of the application manifest that benefit from dynamic configuration. In these cases, the variables are substituted at application load time rather than dynamically updated while the application is running. For example, the `allowed_outbound_hosts` can be dynamically configured using variables as follows: + + + +```toml +[component.(name)] +allowed_outbound_hosts = [ "\{{ api_uri }}" ] +``` + + +All in all, an application manifest with `api_token` and `api_uri` variables and a component that uses them would look similar to the following: + + + +```toml +spin_manifest_version = 2 + +[application] +name = "api-consumer" +version = "0.1.0" +description = "A Spin app that makes API requests" + +[variables] +api_token = { required = true } +api_uri = { default = "http://my-api.com" } + +[[trigger.http]] +route = "/..." +component = "api-consumer" + +[component.api-consumer] +source = "app.wasm" +[component.api-consumer.variables] +token = "\{{ api_token }}" +api_uri = "\{{ api_uri }} +api_version = "v1" +``` + +## Using Variables From Applications + +The Spin SDK surfaces the Spin configuration interface to your language. The [interface](https://github.com/fermyon/spin/blob/main/wit/deps/spin@2.0.0/variables.wit) consists of one operation: + +| Operation | Parameters | Returns | Behavior | +|------------|--------------------|---------------------|----------| +| `get` | Variable name | Variable value | Gets the value of the variable from the configured provider | + +To illustrate the variables API, each of the following examples makes a request to some API with a bearer token. The API URI, version, and token are all passed as application variables. The application manifest associated with the examples would look similar to the one described [in the previous section](#adding-variables-to-your-applications). + +The exact details of calling the config SDK from a Spin application depends on the language: + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://docs.rs/spin-sdk/latest/spin_sdk/variables/index.html) + +The interface is available in the `spin_sdk::variables` module and is named `get`. + +```rust +use spin_sdk::{ + http::{IntoResponse, Method, Request, Response}, + http_component, variables, +}; + +#[http_component] +async fn handle_api_call_with_token(_req: Request) -> anyhow::Result { + let token = variables::get("token")?; + let api_uri = variables::get("api_uri")?; + let version = variables::get("version")?; + let versioned_api_uri = format!("{}/{}", api_uri, version); + let request = Request::builder() + .method(Method::Get) + .uri(versioned_api_uri) + .header("Authorization", format!("Bearer {}", token)) + .build(); + let response: Response = spin_sdk::http::send(request).await?; + // Do something with the response ... + Ok(Response::builder() + .status(200) + .build()) +} +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-js-sdk/modules/Variables.html) + +```ts +import { ResponseBuilder, Variables } from "@fermyon/spin-sdk"; + +export async function handler(req: Request, res: ResponseBuilder) { + let token = Variables.get("token") + let apiUri = Variables.get("api_uri") + let version = Variables.get("version") + let versionedAPIUri = `${apiUri}/${version}` + let response = await fetch( + versionedAPIUri, + { + headers: { + 'Authorization': 'Bearer ' + token + } + } + ); + // Do something with the response ... + res.set({ "Content-Type": "text/plain" }) + res.send("Used an API") +} +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://fermyon.github.io/spin-python-sdk/variables.html) + +The `variables` module has a function called `get`(https://fermyon.github.io/spin-python-sdk/variables.html#spin_sdk.variables.get). + +```py +from spin_sdk.http import IncomingHandler, Request, Response, send +from spin_sdk import variables + +class IncomingHandler(IncomingHandler): + def handle_request(self, request: Request) -> Response: + token = variables.get("token") + api_uri = variables.get("api_uri") + version = variables.get("version") + versioned_api_uri = f"{api_uri}/{version}" + headers = { + "Authorization": f"Bearer {token}" + } + response = send(Request("GET", versioned_api_uri, headers, None)) + # Do something with the response ... + return Response( + 200, + {"content-type": "text/plain"}, + bytes("Used an API", "utf-8") + ) +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +> [**Want to go straight to the reference documentation?** Find it here.](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2@v2.0.0/variables) + +The function is available in the `github.com/fermyon/spin/sdk/go/v2/variables` package and is named `Get`. See [Go package](https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2/variables) for reference documentation. + +```go +import ( + "bytes" + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/variables" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + token, err := variables.Get("token") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + apiUri, err := variables.Get("api_uri") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + version, err := variables.Get("version") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + versionedApiUri := fmt.Sprintf("%s/%s", apiUri, version) + + request, err := http.NewRequest("GET", versionedApiUri, bytes.NewBuffer([]byte(""))) + request.Header.Add("Authorization", "Bearer "+token) + response, err := spinhttp.Send(request) + // Do something with the response ... + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "Used an API") + }) +} + +func main() {} +``` + +{{ blockEnd }} + +{{ blockEnd }} + +To build and run the application, we issue the following commands: + + + +```bash +# Build the application +$ spin build +# Run the application, setting the values of the API token and URI via the environment variable provider +# using the `SPIN_VARIABLE` prefix (upper case is necessary as shown here) +$ SPIN_VARIABLE_API_TOKEN="your-token-here" SPIN_VARIABLE_API_URI="http://my-best-api.com" spin up +``` + +Assuming you have configured you application to use an API, to test the application, simply query +the app endpoint: + + + +```bash +$ curl -i localhost:3000 +HTTP/1.1 200 OK +content-type: text/plain +content-length: 11 +date: Wed, 31 Jul 2024 22:03:35 GMT + +Used an API +``` diff --git a/content/spin/v3/writing-apps.md b/content/spin/v3/writing-apps.md new file mode 100644 index 000000000..11e0485f5 --- /dev/null +++ b/content/spin/v3/writing-apps.md @@ -0,0 +1,530 @@ +title = "Writing Spin Applications" +template = "spin_main" +date = "2023-11-04T00:00:01Z" +enable_shortcodes = true +[extra] +url = "https://github.com/fermyon/developer/blob/main/content/spin/v3/writing-apps.md" + +--- +- [Writing an Application Manifest](#writing-an-application-manifest) + - [The `trigger` Fields](#the-trigger-fields) + - [The Component Name](#the-component-name) + - [The Component `source`](#the-component-source) +- [Writing a Component Wasm Module](#writing-a-component-wasm-module) +- [Creating an Application From a Template](#creating-an-application-from-a-template) +- [Adding a New Component to an Application](#adding-a-new-component-to-an-application) +- [Including Files with Components](#including-files-with-components) +- [Adding Environment Variables to Components](#adding-environment-variables-to-components) +- [Granting Networking Permissions to Components](#granting-networking-permissions-to-components) +- [Granting Storage Permissions to Components](#granting-storage-permissions-to-components) +- [Example Manifests](#example-manifests) + - [Including a Directory of Files](#including-a-directory-of-files) + - [Using Public Components](#using-public-components) + - [Customizing the Executor](#customizing-the-executor) + - [Setting the Redis Channel to Monitor](#setting-the-redis-channel-to-monitor) +- [Using Component Dependencies](#using-component-dependencies) + - [Declaring Component Dependencies](#declaring-component-dependencies) + - [Specifying Dependencies](#specifying-dependencies) + - [Dependencies from a Registry](#dependencies-from-a-registry) + - [Dependencies from a Local Component](#dependencies-from-a-local-component) + - [Dependencies from a URL](#dependencies-from-a-url) + - [Mapping All Imports from a Package](#mapping-all-imports-from-a-package) + - [Dependency Permissions](#dependency-permissions) +- [Next Steps](#next-steps) + +A Spin application consists of a set of WebAssembly (Wasm) _components_, and a _manifest_ that lists those components with some data about when and how to run them. This page describes how to write components, manifests, and hence applications. + +## Writing an Application Manifest + +> You won't normally create a manifest by hand, but will instead [create](#creating-an-application-from-a-template) or [update](#adding-a-new-component-to-an-application) one from a template. Skip to those sections if that's what you want to do. But it's important to know what's inside a manifest for when you need to update component capabilities or troubleshoot a problem. + +A Spin _application manifest_ is a [TOML](https://toml.io/) file that contains: + +* Identification information about the application +* What events the application should run in response to (the _triggers_) +* The Wasm modules of the application and their associated settings (the _components_) +* Optionally, configuration variables that the user can set when running the application + +By convention, the Spin manifest file is usually called `spin.toml`. + +This example is the manifest for a simple HTTP application with a single trigger executed when the `/hello` endpoint is accessed: + + + +```toml +spin_manifest_version = 2 + +# General identification information +[application] +name = "spin-hello-world" +version = "1.0.0" +description = "A simple application that returns hello world." + +# The application's sole trigger. This application responds to HTTP requests +# on the path "/hello", and handles them using the "hello" component. +[[trigger.http]] +route = "/hello" +component = "hello" + +# The "hello" component +[component.hello] +description = "A simple component that returns hello world." +# The Wasm module to run for the component +source = "target/wasm32-wasi/release/helloworld.wasm" +# How to build the Wasm module from source +[component.hello.build] +command = "cargo build --target wasm32-wasi --release" +``` + +You can look up the various fields in the [Manifest Reference](manifest-reference), but let's look at the key fields in more detail. + +### The `trigger` Fields + +An application contains one or more _triggers_. Each trigger specifies a type, an event of that type that the application responds to, and a component to handle that event. + +> In current versions of Spin, all triggers in the application must be of the same type. You can't listen for Redis messages and HTTP requests in the same application. + +The description above might sound a bit abstract. Let's clarify it with a concrete example. The most common trigger type is `http`, for which events are distinguished by the route. So an HTTP trigger might look like this: + +```toml +# double square brackets because there could be multiple 'trigger.http' tables +[[trigger.http]] # the type is part of the TOML "section" +route = "/hello" # the route (event) that this trigger responds to +component = "greeter" # the component to handle the trigger +``` + +Put the type (`http`), event (`route = "/hello"`), and component (`greeter`) together, and this is saying "when the application gets an HTTP request to `/hello`, respond to it using the `greeter` component. + +For more details about `http` triggers, see the [HTTP trigger](http-trigger) documentation. + +Another trigger type is `redis`, which is triggered by Redis pub-sub messages. For the `redis` type, the trigger event is specified by a pub-sub channel, e.g. `channel = "alerts"`. See the [Redis trigger](redis-trigger) documentation for more information. + +Multiple triggers may refer to the same component. For example, you could have another trigger on `/dober-dan` which also invokes the `greeter` component. + +Some triggers have additional application-level configuration options. For example, the Redis trigger allows you to provide an `address` field, which tells Spin the default server for components that do not specify servers. See the [Redis trigger](redis-trigger) documentation for more details. + +### The Component Name + +Component names are identifier strings. While not significant in themselves, they must be unique within the application. Triggers use names to refer to components, and Spin uses the name in logging and error messages to tell you which component a message applies to. + +Each component is a TOML table, and the name is written as part of the table header. For example: + + + +```toml +[component.greeter] +``` + +### The Component `source` + +Each component has a `source` field which tells Spin where to load the Wasm module from. + +For components that you are working on locally, set it to the file path to the compiled Wasm module file. This must be a relative path from the directory containing `spin.toml`. + + + +```toml +[component.shopping-cart] +source = "dist/cart.wasm" +``` + +For components that are published on the Web, provide a `url` field containing the URL of the Wasm module, and a `digest` field indicating the expected SHA256 hash of the module contents. The digest must be prefixed with the string `sha256:`. Spin uses the digest to check that the module is what you were expecting, and won't load the module if the content doesn't match. + + + +```toml +[component.asset-server] +source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.1/spin_static_fs.wasm", digest = "sha256:650376c33a0756b1a52cad7ca670f1126391b79050df0321407da9c741d32375" } +``` + +Multiple components can have the same source. An example is a document archive, where one component might serve user interface assets (CSS, images, etc.) on one route, while another serves the documents themselves on another route - both using the same file server module, but with different settings. + +## Writing a Component Wasm Module + +> You won't normally create a component by hand, but will instead [create](#creating-an-application-from-a-template) or [add](#adding-a-new-component-to-an-application) one from a template. Skip to those sections if that's what you want to do. But it's important to know what a component looks like so you can understand where to write your own code. + +At the Wasm level, a Spin component is a Wasm component or module that exports a handler for the application trigger. At the developer level, a Spin component is a library or program that implements your event handling logic, and uses Spin interfaces, libraries, or tools to associate that with the events handled by Spin. + +See the Language Guides section for how to do this in your preferred language. As an example, this is a component written in the Rust language. The `hello_world` function uses an attribute `#[http_component]` to identify the function as handling a Spin HTTP event. The function takes a `Request` and returns a `anyhow::Result`. + +```rust +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +fn hello_world(req: Request) -> anyhow::Result { + println!("Handling request to {:?}", req.header("spin-full-url")); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello, Fermyon") + .build()) +}​​ +``` + +## Creating an Application From a Template + +If you've installed the Spin templates for your preferred language, you can use them to get started without having to write a manifest or the boilerplate code yourself. To do this, run the `spin new` command, and choose the template that matches the type of application you want to create, and the language you want to use. + +{{ tabs "sdk-type" }} + +{{ startTab "Rust"}} + +Choose the `http-rust` template to create a new HTTP application, or `redis-rust` to create a new Redis application. + + + +```bash +$ spin new +Pick a template to start your application with: + http-c (HTTP request handler using C and the Zig toolchain) + http-csharp (HTTP request handler using C# (EXPERIMENTAL)) + http-go (HTTP request handler using (Tiny)Go) + http-grain (HTTP request handler using Grain) +> http-rust (HTTP request handler using Rust) + http-swift (HTTP request handler using SwiftWasm) + http-zig (HTTP request handler using Zig) + redis-go (Redis message handler using (Tiny)Go) + redis-rust (Redis message handler using Rust) + +Enter a name for your new application: hello_rust +Project description: My first Rust Spin application +HTTP path: /... +``` + +{{ blockEnd }} + +{{ startTab "TypeScript"}} + +Choose the `http-ts` or `http-js` template to create a new HTTP application, according to whether you want to use TypeScript or JavaScript. + +> The JavaScript development kit doesn't yet support Redis applications. + + + +```bash +$ spin new +Pick a template to start your application with: + http-js (HTTP request handler using Javascript) +> http-ts (HTTP request handler using Typescript) +Enter a name for your new application: hello_typescript +Project description: My first TypeScript Spin application +HTTP path: /... +``` + +{{ blockEnd }} + +{{ startTab "Python"}} + +Choose the `http-py` template to create a new HTTP application. + +> The Python development kit doesn't yet support Redis applications. + + + +```bash +$ spin new +Pick a template to start your application with: +> http-py (HTTP request handler using Python) +Enter a name for your new application: hello_python +Description: My first Python Spin application +HTTP path: /... +``` + +{{ blockEnd }} + +{{ startTab "TinyGo"}} + +Choose the `http-go` template to create a HTTP application, or `redis-go` to create a Redis application. + + + +```bash +$ spin new +Pick a template to start your application with: + http-c (HTTP request handler using C and the Zig toolchain) + http-empty (HTTP application with no components) +> http-go (HTTP request handler using (Tiny)Go) + http-grain (HTTP request handler using Grain) + http-php (HTTP request handler using PHP) + http-rust (HTTP request handler using Rust) +Enter a name for your new application: hello_go +Description: My first Go Spin application +HTTP path: /... +``` + +{{ blockEnd }} + +{{ blockEnd }} + +All of these templates create a manifest containing a single component, and the source code for a minimal "hello world" component. + +## Adding a New Component to an Application + +To add a new component to an existing application using a template, run the `spin add` command. This works in a very similar way to `spin new`, except that it expects the `spin.toml` file to already exist, and adds the details for the new component to that `spin.toml`. + +> Please take a look at the [Spin application structure](spin-application-structure) documentation, which explains how to achieve the recommended application structure (through the use of Spin templates via the `spin new` and `spin add` commands). + +## Including Files with Components + +You can include files with a component. This means that: + +1. The Wasm module will be able to read those files at runtime. +1. When you distribute or deploy the application, those files will be bundled with it so that the component can still access them. + +To do this, use the `files` field in the component manifest: + + + +```toml +[component.asset-server] +files = [ "images/**/*.jpg", { source = "styles/dist", destination = "/styles" } ] +``` + +The `files` field is an array listing the files, patterns and directories you want to include. Each element of the array can be: + +* A glob pattern, such as `images/**/*.jpg`, or single file path. In this case, the file or files that match the pattern are available to the Wasm code, at the same paths as they are in your file system. For example, if the glob pattern matches `images/photos/lake.jpg`, the Wasm module can access it using the path `images/photos/lake.jpg`. Glob patterns are relative to the directory containing `spin.toml`, and must be within that directory. +* A mapping from a `source` file or directory to a `destination` location, such as `{ source = "styles/dist", destination = "/styles" }`. In this case, the file, or the entire contents of the source directory, are available to the Wasm code at the destination location. In this example, if you have a file named `styles/dist/list/exciting.css`, the Wasm module can access it using the path `/styles/list/exciting.css`. Source locations are relative to the directory containing `spin.toml`; destination locations must be absolute. + +If your files list would match some files or directories that you _don't_ want included, you can use the `exclude_files` field to omit them. + +> By default, Spin takes a snapshot of your included files, and components access that snapshot. This ensures that when you test your application, you are checking it with the set of files it will be deployed with. However, it also means that your component does not pick up changes to the files while it is running. When testing a Web site, you might want to be able to edit a HTML or CSS file, and see the changes reflected without restarting Spin. You can tell Spin to give components access to the original, "live" files by passing the `--direct-mounts` flag to `spin up`. + +> Each component can access _only_ the files included via its `files` section. It does not have access to files included by other components, to your source code, or to the compiled `.wasm` file (unless you add those to the `files` section). + +## Adding Environment Variables to Components + +Environment variables can be provided to components via the Spin application manifest. + +To do this, use the `environment` field in the component manifest: + + + +```toml +[component.pet-info] +environment = { PET = "CAT", FOOD = "WATERMELON" } +``` + +The field accepts a map of environment variable key/value pairs. They are mapped inside the component at runtime. + +The environment variables can then be accessed inside the component. For example, in Rust: + +```rust +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +#[http_component] +fn handle_hello_rust(req: Request) -> anyhow::Result { + let response = format!("My {} likes to eat {}", std::env::var("PET")?, std::env::var("FOOD")?); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(response) + .build()) +} +``` + +## Granting Networking Permissions to Components + +By default, Spin components are not allowed to make outgoing network requests. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. + +To grant a component permission to make outbound requests to a particular address, use the `allowed_outbound_hosts` field in the component manifest. Each entry must comprise a host name or IP address _and_ a port. For example: + + + +```toml +[component.talkative] +allowed_outbound_hosts = ["redis://redis.example.com:6379", "https://api.example.com:8080"] +``` + +If a port is specified, the component can make requests only to that port. If no port is specified, the component can make requests only to the default port for the scheme (e.g. port 443 for the `https` scheme, port 5432 for the `postgres` scheme). If you need to allow requests to _any_ port, use the wildcard `*` (e.g. `mysql://db.example.com:*`). + +> If you're familiar with Spin 1, `allowed_outbound_hosts` replaces `allowed_http_hosts`. Spin 2 still accepts `allowed_http_hosts` but will recommend migrating to `allowed_outbound_hosts`. Additionally, if your application uses the version 1 manifest format, _and_ the manifest does not specify `allowed_outbound_hosts`, then version 1 components are allowed to use Redis, MySQL and PostgreSQL without restriction. + +The Wasm module can send network traffic _only_ to the hosts specified in these two fields. Requests to other hosts (or other ports) will fail with an error. + +{{ details "This feels like extra work! Why do I have to list the hosts?" "This comes from the Wasm principle of deny by default: the user of a component, rather than the component itself, should decide what resource it's allowed to access. But this isn't just an abstract principle: it's critical to being able to trust third party components. For example, suppose you add `bad-boy-brians-totally-legitimate-file-server.wasm` to your application. Unless you unwisely grant it network permissions, you can be _provably certain_ that it doesn't access your Postgres database or send information to evildoers." }} + +For development-time convenience, you can also pass the string `"*://*:*"` in the `allowed_outbound_hosts` collection. This allows the component to make network requests to _any_ host and on any port. However, once you've determined which hosts your code needs, you should remove this string, and list the hosts instead. Other Spin implementations may restrict host access, and may disallow components that ask to connect to anything and everything! + +## Granting Storage Permissions to Components + +By default, Spin components are not allowed to access Spin's storage services. This follows the general Wasm rule that modules must be explicitly granted capabilities, which is important to sandboxing. To grant a component permission to use a Spin-provided store, use the `key_value_stores` field in the component manifest: + + + +```toml +[component.squirrel] +key_value_stores = [ "default" ] +``` + +See [Persisting Data with Spin](key-value-store-tutorial) for more information. + +## Example Manifests + +This section shows some examples of Spin component manifests. + +### Including a Directory of Files + +This example shows a Spin HTTP component that includes all the files in `static/` under the application directory, made available to the Wasm module at the `/` path. + + + +```toml +[[trigger.http]] +route = "/static/..." +component = "fileserver" + +[component.fileserver] +source = "modules/spin_static_fs.wasm" +files = [ { source = "static/", destination = "/" } ] +``` + +### Using Public Components + +This is similar to the file server component above, but gets the Wasm module from a public release instead of a local copy. Notice that the files are still drawn from a local path. This is a way in which you can use off-the-shelf component logic with your own application data. + + + +```toml +[[trigger.http]] +route = "/static/..." +component = "fileserver" + +[component.fileserver] +source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.1/spin_static_fs.wasm", digest = "sha256:650376c33a0756b1a52cad7ca670f1126391b79050df0321407da9c741d32375" } +files = [ { source = "static/", destination = "/" } ] +``` + +### Customizing the Executor + +This example shows an HTTP component whose Wasm module, instead of using the default Spin HTTP interface, uses the CGI-like WAGI (WebAssembly Gateway Interface) protocol. Spin can't work out the application model from the component, so the manifest needs to tell Spin to use WAGI instead of its default mode. It does this via the `executor` field. This is specific to the HTTP trigger so it goes under `trigger.http`. + +In addition, this module does not provide its WAGI functionality via the default `_start` entry point, but via a custom entry point named `serve-wagi`. The `executor` table needs to tell Spin this via the `entrypoint` field. Finally, this component needs to run the WAGI entry point with a specific set of command line arguments, which are expressed in the WAGI executor's `argv` field. + + + +```toml +[[trigger.http]] +route = "/..." +component = "env" +executor = { type = "wagi", argv = "test ${SCRIPT_NAME} ${ARGS} done", entrypoint = "serve-wagi" } + +[component.env] +source = "modules/env_wagi.wasm" +files = [ "content/**/*" , "templates/*", "scripts/*", "config/*"] +``` + +### Setting the Redis Channel to Monitor + +The example shows a Redis component. The manifest sets the `channel` field to say which channel the component should monitor. In this case, the component is invoked for new messages on the `messages` channel. + + + +```toml +[[trigger.redis]] +channel = "messages" +component = "echo-message" + +[component.echo-message] +source = "spinredis.wasm" +``` + +## Using Component Dependencies + +Few of us write applications without relying on libraries. Traditionally, those libraries have had to come from the language ecosystem - e.g. `npm` for JavaScript, `pip` for Python, etc. - and you can still work this way in Spin. However, the WebAssembly Component Model means that you can also depend on other WebAssembly components. The process of combining your application component with the Wasm components it depends on is called _composition_, and Spin supports this natively. + +> Spin's composition is limited to "plug" style scenarios where each of your component's imports is satisfied independently, and where the dependency component does not need to be further composed with any other components. The analogy is plugging each of your imports into a socket provided by a dependency. If you need to construct a more complex composition, you must use a dedicated tool such as [`wac`](https://github.com/bytecodealliance/wac) as part of your build. See the [Component Model book](https://component-model.bytecodealliance.org/creating-and-consuming/composing.html) for details and examples. + +To use composition through Spin, your component must import a [WIT (Wasm Interface Type) interface](https://component-model.bytecodealliance.org/design/wit.html), and the dependency must export the same WIT interface. The details of working with WIT interfaces is language-specific, and is beyond the scope of the Spin documentation. You can learn more from the [language guides in the Component Model book](https://component-model.bytecodealliance.org/language-support.html). This section focuses on describing the dependency composition support in Spin. + +### Declaring Component Dependencies + +To declare a component dependency, create a `[component.(name).dependencies]` table in your Spin manifest, and list all the WIT interfaces you import (other than the ones that Spin itself satisfies), together with the packages that you would like to use to satisfy those imports. + +For example, suppose your component imports a WIT interface named `security:http/malice` for detecting malicious HTTP requests. This interface might be defined by a vendor or standards body, and might have multiple implementations. Suppose Bargain Security, Inc. provides an HTTP inspection package which includes an implementation of this interface, and that they publish this in the `registry.example.com` registry as `bargains:inspection@2.0.0`. You can then set up your dependency as follows: + +```toml +[component.my-app.dependencies] +"security:http/malice" = { package = "bargains:inspection", version = "2.0.0", registry = "packages.example.com" } +``` + +During loading, Spin will download the package from the registry, locate its `security:http/malice` export, and wire up your imports to that export so that when your component calls a function in the WIT interface, that call is dispatched to the Bargain Security package. + +> Your Wasm component depends _only_ on the WIT interface. If, inexplicably, you become dissatisfied with Bargain Security, Inc., then you can switch to a different vendor by changing the package reference in the dependency mapping (and, of course, re-testing with the new implementation). + +### Specifying Dependencies + +Spin supports three sources for dependencies. + +#### Dependencies from a Registry + +To use a dependency from a registry, specify the following fields: + +| Field | Required? | Description | Example | +|------------|-------------|----------------------------------------------------------------------------------------------|---------| +| `version` | Required | A [semantic versioning constraint](https://semver.org/) for the package version to use. | `">= 1.1.0"` | +| `package` | Optional | The name of the package to use. If omitted, this defaults to the package name of the imported interface. | `"bargains:inspection"` | +| `registry` | Optional | The registry that hosts the package. If omitted, this defaults to your system default registry. | `"registry.example.com"` | +| `export` | Optional | The name of the export in the package. If omitted, this defaults to the name of the import. | `"more-secure:checking-it-out/web"` | + +If you don't need any of the optional fields, you can provide the version constraint as a plain string instead of writing out the table: + +```toml +# Use the `security:http` package in the default registry +"security:http/malice" = "2.0.0" +``` + +#### Dependencies from a Local Component + +To use a dependency from a component on your file system, specify the following fields: + +| Field | Required? | Description | Example | +|------------|-------------|----------------------------------------------------------------------------------------------|---------| +| `path` | Required | The path to the Wasm file containing the component. | `"../validation/request-checker.wasm"` | +| `export` | Optional | The name of the export in the package. If omitted, this defaults to the name of the import. | `"more-secure:checking-it-out/web"` | + +#### Dependencies from a URL + +To use a dependency from an HTTP URL, such as a GitHub release, specify the following fields: + +| Field | Required? | Description | Example | +|------------|-------------|----------------------------------------------------------------------------------------------|---------| +| `url` | Required | The URL of the Wasm file containing the component. | `"https://example.com/downloads/request-checker.wasm"` | +| `digest` | Required | The SHA256 digest of the Wasm file. This is required for integrity checking. | `"sha256:650376c33a0756b1a52cad7ca670f1126391b79050df0321407da9c741d32375"` | +| `export` | Optional | The name of the export in the package. If omitted, this defaults to the name of the import. | `"more-secure:checking-it-out/web"` | + +### Mapping All Imports from a Package + +If you are importing several interfaces from the same WIT package, and want them all satisfied by the same Wasm package, you can omit the interface from the dependency name. For example, suppose you import the `malice`, `tomfoolery`, and `shenanigans` WIT interfaces from the `security:http` package, and that your Bargain Security package exports all three of them. You can write: + +```toml +[component.my-app.dependencies] +"security:http" = { package = "bargains:inspection", version = "2.0.0", registry = "packages.example.com" } +``` + +and Spin will map all of your `security:http` imports to the matching exports from the package. + +### Dependency Permissions + +By default, dependencies do not have access to Spin resources that require permission to be given in the manifest - network hosts, key-value stores, SQLite databases, variables, etc. + +If you depend on a component which requires such access, and you trust all the components that you depend on, you can grant them access to the same resources that the 'main' component has by setting the `dependencies_inherit_configuration` flag on the main component: + +```toml +[component.my-app] +dependencies_inherit_configuration = true +``` + +> Spin does not currently support inheritance on a component-by-component basis. If the flag is set, _all_ the dependencies will act with the permissions of the main component. + +## Next Steps + +- Learn about [Spin application structure](spin-application-structure) +- Learn about how to [build your Spin application code](build) +- Try [running your application locally](running-apps) +- Discover how Spin application authors [design and organise applications](see-what-people-have-built-with-spin) +- Learn about how to [configure your application at runtime](dynamic-configuration) +- Look up details in the [application manifest reference](manifest-reference) +- Try deploying a Spin application to the [Fermyon Cloud](/cloud/quickstart) \ No newline at end of file diff --git a/content/wasm-languages/standards.md b/content/wasm-languages/standards.md index 72251ea0e..d4c94c860 100644 --- a/content/wasm-languages/standards.md +++ b/content/wasm-languages/standards.md @@ -28,7 +28,7 @@ WebAssembly is standardized by W3C, the same group that standardizes CSS, HTML, - The [WASI site](https://wasi.dev) is the main page - The [WASI repo](https://github.com/WebAssembly/WASI) - The working group [charter](https://github.com/WebAssembly/WASI/blob/main/Charter.md) -- Most of the WASI proposed standards are [in the WebAssembly GitHub](https://github.com/search?q=org%3AWebAssembly+wasi) +- Most of the WASI proposed standards are [in the WebAssembly GitHub](https://github.com/orgs/WebAssembly/repositories?q=wasi) - The [WASI libc library](https://github.com/WebAssembly/wasi-libc) is the C library for core WASI ### The Component Model diff --git a/spin.toml b/spin.toml index c51dbc1f0..636c4deb6 100644 --- a/spin.toml +++ b/spin.toml @@ -154,6 +154,11 @@ id = "trigger-bartholomew-spin-v2" component = "bartholomew-spin-v2" route = "/spin/v2/..." +[[trigger.http]] +id = "trigger-bartholomew-spin-v3" +component = "bartholomew-spin-v3" +route = "/spin/v3/..." + [[trigger.http]] id = "trigger-redirect-wasm-langs-root" component = "redirect-wasm-langs-root" @@ -334,7 +339,7 @@ environment = { DESTINATION = "/wasm-languages/webassembly-language-support" } # Component to give us clean versioned URLs for spin from the root [component.spin-version-proxy] source = "modules/spin-redirecter.wasm" -variables = { latest_spin_version = "v2" } +variables = { latest_spin_version = "v3" } files = ["spin-redirect.json"] exclude_files = ["**/node_modules"] allowed_outbound_hosts = ["http://self"] @@ -362,3 +367,9 @@ files = ["content/**/*", "templates/*", "scripts/*", "config/*", "shortcodes/*"] [component.bartholomew-spin-v2.build] command = "npm run build-index && npm run build-hub-index" watch = ["content/**/*", "templates/*"] + +[component.bartholomew-spin-v3] +# Using build from bartholomew main +source = "modules/bartholomew.wasm" +environment = { PREVIEW_MODE = "0" } +files = ["content/**/*", "templates/*", "scripts/*", "config/*", "shortcodes/*"] diff --git a/templates/page_lang.hbs b/templates/page_lang.hbs index e2b1f7c55..2ee747a8c 100644 --- a/templates/page_lang.hbs +++ b/templates/page_lang.hbs @@ -14,11 +14,15 @@ + {{#if (active_project request.spin-full-url "/spin/v3/" )}} + {{> spin_sidebar_v3 }} + {{else}} {{#if (active_project request.spin-full-url "/spin/v2/" )}} {{> spin_sidebar_v2 }} {{else}} {{> spin_sidebar }} {{/if}} + {{/if}} diff --git a/templates/spin_main.hbs b/templates/spin_main.hbs index b338f3c1d..474ad8271 100644 --- a/templates/spin_main.hbs +++ b/templates/spin_main.hbs @@ -10,15 +10,21 @@ {{! This adds the version selector - uncomment after v2 launch }} + {{#if (active_project request.spin-full-url "/spin/v3/" )}} + {{> spin_sidebar_v3 }} + {{else}} {{#if (active_project request.spin-full-url "/spin/v2/" )}} {{> spin_sidebar_v2 }} {{else}} {{> spin_sidebar }} {{/if}} + {{/if}} diff --git a/templates/spin_sidebar_v3.hbs b/templates/spin_sidebar_v3.hbs new file mode 100644 index 000000000..e5401ae8f --- /dev/null +++ b/templates/spin_sidebar_v3.hbs @@ -0,0 +1,324 @@ +
+ + + +
+ + + +
+
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+
+ + + +
+
+ +