From f288609da7d3f4572edb0e4de5ef67e8d8b57278 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Fri, 22 Jun 2018 14:21:34 -0700 Subject: [PATCH] Hello world samples for Go, Node, and CSharp (#11) * Add some hierachy * Clone helloworld samples from serving * add ignore files * remove mac hidden files * Finish readme * Tweak formatting * Update instructions * Updating go * Update yaml example * Fix typo in service name. * Update readme and yaml * Update app yaml * Ignore .vscode folder * Fix missing newline at EOF * Update sample to read env variable --- .gitignore | 1 + serving/samples/helloworld-csharp/.gitignore | 3 + serving/samples/helloworld-csharp/Dockerfile | 14 + serving/samples/helloworld-csharp/Program.cs | 24 ++ .../Properties/launchSettings.json | 27 ++ serving/samples/helloworld-csharp/README.md | 121 ++++++ serving/samples/helloworld-csharp/Startup.cs | 35 ++ serving/samples/helloworld-csharp/app.yaml | 15 + .../helloworld-csharp.csproj | 15 + serving/samples/helloworld-go/Dockerfile | 17 + serving/samples/helloworld-go/README.md | 135 +++++++ serving/samples/helloworld-go/app.yaml | 15 + serving/samples/helloworld-go/helloworld.go | 42 ++ serving/samples/helloworld-go/sample.yaml | 48 +++ .../helloworld-go/updated_configuration.yaml | 38 ++ serving/samples/helloworld-nodejs/.gitignore | 1 + serving/samples/helloworld-nodejs/Dockerfile | 19 + serving/samples/helloworld-nodejs/README.md | 161 ++++++++ serving/samples/helloworld-nodejs/app.js | 30 ++ serving/samples/helloworld-nodejs/app.yaml | 15 + .../helloworld-nodejs/package-lock.json | 373 ++++++++++++++++++ .../samples/helloworld-nodejs/package.json | 23 ++ 22 files changed, 1172 insertions(+) create mode 100644 .gitignore create mode 100644 serving/samples/helloworld-csharp/.gitignore create mode 100644 serving/samples/helloworld-csharp/Dockerfile create mode 100644 serving/samples/helloworld-csharp/Program.cs create mode 100644 serving/samples/helloworld-csharp/Properties/launchSettings.json create mode 100644 serving/samples/helloworld-csharp/README.md create mode 100644 serving/samples/helloworld-csharp/Startup.cs create mode 100644 serving/samples/helloworld-csharp/app.yaml create mode 100644 serving/samples/helloworld-csharp/helloworld-csharp.csproj create mode 100644 serving/samples/helloworld-go/Dockerfile create mode 100644 serving/samples/helloworld-go/README.md create mode 100644 serving/samples/helloworld-go/app.yaml create mode 100644 serving/samples/helloworld-go/helloworld.go create mode 100644 serving/samples/helloworld-go/sample.yaml create mode 100644 serving/samples/helloworld-go/updated_configuration.yaml create mode 100644 serving/samples/helloworld-nodejs/.gitignore create mode 100644 serving/samples/helloworld-nodejs/Dockerfile create mode 100644 serving/samples/helloworld-nodejs/README.md create mode 100644 serving/samples/helloworld-nodejs/app.js create mode 100644 serving/samples/helloworld-nodejs/app.yaml create mode 100644 serving/samples/helloworld-nodejs/package-lock.json create mode 100644 serving/samples/helloworld-nodejs/package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e43b0f988 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/serving/samples/helloworld-csharp/.gitignore b/serving/samples/helloworld-csharp/.gitignore new file mode 100644 index 000000000..9192ecc0d --- /dev/null +++ b/serving/samples/helloworld-csharp/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.vscode/ diff --git a/serving/samples/helloworld-csharp/Dockerfile b/serving/samples/helloworld-csharp/Dockerfile new file mode 100644 index 000000000..31dd35163 --- /dev/null +++ b/serving/samples/helloworld-csharp/Dockerfile @@ -0,0 +1,14 @@ +FROM microsoft/dotnet:2.1-sdk +WORKDIR /app + +# copy csproj and restore as distinct layers +COPY *.csproj ./ +RUN dotnet restore + +# copy and build everything else +COPY . ./ +RUN dotnet publish -c Release -o out + +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "out/helloworld-csharp.dll"] diff --git a/serving/samples/helloworld-csharp/Program.cs b/serving/samples/helloworld-csharp/Program.cs new file mode 100644 index 000000000..40261f5d2 --- /dev/null +++ b/serving/samples/helloworld-csharp/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace helloworld_csharp +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup().UseUrls("http://0.0.0.0:8080"); + } +} diff --git a/serving/samples/helloworld-csharp/Properties/launchSettings.json b/serving/samples/helloworld-csharp/Properties/launchSettings.json new file mode 100644 index 000000000..55433266e --- /dev/null +++ b/serving/samples/helloworld-csharp/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39053", + "sslPort": 44372 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "helloworld_csharp": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/serving/samples/helloworld-csharp/README.md b/serving/samples/helloworld-csharp/README.md new file mode 100644 index 000000000..003a8cc3a --- /dev/null +++ b/serving/samples/helloworld-csharp/README.md @@ -0,0 +1,121 @@ +# Hello World - .NET Core sample + +This sample application shows how to create a hello world application in C# using .NET Core 2.1. +It reads in an env variable 'TARGET' and prints "Hello World: ${TARGET}!" if +TARGET is not specified, it will use "NOT SPECIFIED" as the TARGET. + +## Prerequisites + +* You have a Kubernetes cluster with Knative installed. Follow the [installation instructions](https://github.com/knative/install/) if you need to do this. +* You have installed and initalized [Google Cloud SDK](https://cloud.google.com/sdk/docs/) and have created a project in Google Cloud. +* You have `kubectl` configured to connect to the Kubernetes cluster running Knative. +* You have installed [.NET Core SDK 2.1](https://www.microsoft.com/net/core). + +## Steps to recreate the sample code + +While you can clone all of the code from this directory, hello world apps are generally more useful if you build them step-by-step. The following instructions recreate the source files from this folder. + +1. From the console, create a new empty web project using the dotnet command: + + ```shell + dotnet new web -o helloworld-csharp + ``` + +1. Update the `CreateWebHostBuilder` definition in `Program.cs` by adding `.UseUrls("http://0.0.0.0:8080")` to define the serving port: + + ```csharp + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup().UseUrls("http://0.0.0.0:8080"); + ``` + +1. Update the `app.Run(...)` statement in `Startup.cs` to read and return the TARGET environment variable: + + ```csharp + app.Run(async (context) => + { + var target = Environment.GetEnvironmentVariable("TARGET") ?? "NOT SPECIFIED"; + await context.Response.WriteAsync($"Hello World: {target}"); + }); + ``` + +1. In your project directory, create a file named `Dockerfile` and copy the code block below into it. For detailed instructions on dockerizing a .NET core app, see [dockerizing a .NET core app](https://docs.microsoft.com/en-us/dotnet/core/docker/docker-basics-dotnet-core#dockerize-the-net-core-application). + + ```docker + FROM microsoft/dotnet:2.1-sdk + WORKDIR /app + + # copy csproj and restore as distinct layers + COPY *.csproj ./ + RUN dotnet restore + + # copy and build everything else + COPY . ./ + RUN dotnet publish -c Release -o out + ENTRYPOINT ["dotnet", "out/helloworld-csharp.dll"] + ``` + +1. Create a new file, `app.yaml` and copy the following service definition into the file. Make sure to replace `{PROJECT_ID}` with the ID of your Google Cloud project. If you are using docker or another container registry instead, replace the entire image path. + + ```yaml + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + metadata: + name: helloworld-python + namespace: default + spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-csharp + env: + - name: TARGET + value: "C# Sample v1" + ``` + +## Build and deploy this sample + +Once you have recreated the sample code files (or used the files in the sample folder) you're ready to build and deploy the sample app. + +1. For this example, we'll use Google Cloud Container Builder to build the sample into a container. To use container builder, execute the following gcloud command: + + ```shell + gcloud container builds submit --tag gcr.io/${PROJECT_ID}/helloworld-csharp + ``` + +1. After the build has completed, you can deploy the app into your cluster. Ensure that the container image value in `app.yaml` matches the container you build in the previous step. Apply the configuration using kubectl: + + ```shell + kubectl apply -f app.yaml + ``` + +1. Now that your service is created, Knative will perform the following steps: + * Create a new immutable revision for this version of the app. + * Network programming to create a route, ingress, service, and load balance for your app. + * Automatically scale your pods up and down (including to zero active pods). + +1. To find the URL and IP address for your service, use kubectl to list the ingress points in the cluster: + + ```shell + kubectl get ing + + NAME HOSTS ADDRESS PORTS AGE + helloworld-csharp-ingress helloworld-csharp.default.demo-domain.com 35.232.134.1 80 1m + ``` + +1. Now you can make a request to your app to see the result. Replace `{IP_ADDRESS}` with the address you see returned in the previous step. + + ```shell + curl -H "Host: helloworld-csharp.default.demo-domain.com" http://{IP_ADDRESS} + Hello World! + ``` + +## Remove the sample app deployment + +To remove the sample app from your cluster, delete the service record: + +```shell +kubectl delete -f app.yaml +``` diff --git a/serving/samples/helloworld-csharp/Startup.cs b/serving/samples/helloworld-csharp/Startup.cs new file mode 100644 index 000000000..4099a1c6b --- /dev/null +++ b/serving/samples/helloworld-csharp/Startup.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace helloworld_csharp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.Run(async (context) => + { + var target = Environment.GetEnvironmentVariable("TARGET") ?? "NOT SPECIFIED"; + await context.Response.WriteAsync($"Hello World: {target}"); + }); + } + } +} diff --git a/serving/samples/helloworld-csharp/app.yaml b/serving/samples/helloworld-csharp/app.yaml new file mode 100644 index 000000000..a6836aa0e --- /dev/null +++ b/serving/samples/helloworld-csharp/app.yaml @@ -0,0 +1,15 @@ +apiVersion: serving.knative.dev/v1alpha1 +kind: Service +metadata: + name: helloworld-csharp + namespace: default +spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-csharp + env: + - name: TARGET + value: "C# Sample v1" diff --git a/serving/samples/helloworld-csharp/helloworld-csharp.csproj b/serving/samples/helloworld-csharp/helloworld-csharp.csproj new file mode 100644 index 000000000..d62ba8bec --- /dev/null +++ b/serving/samples/helloworld-csharp/helloworld-csharp.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.1 + + + + + + + + + + + diff --git a/serving/samples/helloworld-go/Dockerfile b/serving/samples/helloworld-go/Dockerfile new file mode 100644 index 000000000..035aff66f --- /dev/null +++ b/serving/samples/helloworld-go/Dockerfile @@ -0,0 +1,17 @@ +# Start from a Debian image with the latest version of Go installed +# and a workspace (GOPATH) configured at /go. +FROM golang + +# Copy the local package files to the container's workspace. +ADD . /go/src/github.com/knative/docs/helloworld + +# Build the outyet command inside the container. +# (You may fetch or manage dependencies here, +# either manually or with a tool like "godep".) +RUN go install github.com/knative/docs/helloworld + +# Run the outyet command by default when the container starts. +ENTRYPOINT /go/bin/helloworld + +# Document that the service listens on port 8080. +EXPOSE 8080 diff --git a/serving/samples/helloworld-go/README.md b/serving/samples/helloworld-go/README.md new file mode 100644 index 000000000..c6feaa5fd --- /dev/null +++ b/serving/samples/helloworld-go/README.md @@ -0,0 +1,135 @@ +# Hello World - Go + +A simple web app written in Go that you can use for testing. +It reads in an env variable 'TARGET' and prints "Hello World: ${TARGET}!" if +TARGET is not specified, it will use "NOT SPECIFIED" as the TARGET. + +## Prerequisites + +* You have a Kubernetes cluster with Knative installed. Follow the [installation instructions](https://github.com/knative/install/) if you need to do this. +* You have installed and initalized [Google Cloud SDK](https://cloud.google.com/sdk/docs/) and have created a project in Google Cloud. +* You have `kubectl` configured to connect to the Kubernetes cluster running Knative. + +## Steps to recreate the sample code + +While you can clone all of the code from this directory, hello world +apps are generally more useful if you build them step-by-step. The +following instructions recreate the source files from this folder. + +1. Create a new file named `helloworld.go` and paste the following code. This code creates a basic web server which listens on port 8080: + + ```go + package main + + import ( + "flag" + "fmt" + "log" + "net/http" + "os" + ) + + func handler(w http.ResponseWriter, r *http.Request) { + log.Print("Hello world received a request.") + target := os.Getenv("TARGET") + if target == "" { + target = "NOT SPECIFIED" + } + fmt.Fprintf(w, "Hello World: %s!\n", target) + } + + func main() { + flag.Parse() + log.Print("Hello world sample started.") + + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) + } + ``` + +1. In your project directory, create a file named `Dockerfile` and copy the code block below into it. For detailed instructions on dockerizing a Go app, see [Deploying Go servers with Docker](https://blog.golang.org/docker). + + ```docker + # Start from a Debian image with the latest version of Go installed + # and a workspace (GOPATH) configured at /go. + FROM golang + + # Copy the local package files to the container's workspace. + ADD . /go/src/github.com/knative/docs/helloworld + + # Build the helloworld command inside the container. + # (You may fetch or manage dependencies here, + # either manually or with a tool like "godep".) + RUN go install github.com/knative/docs/helloworld + + # Run the helloworld command by default when the container starts. + ENTRYPOINT /go/bin/helloworld + + # Document that the service listens on port 8080. + EXPOSE 8080 + ``` + +1. Create a new file, `app.yaml` and copy the following service definition into the file. Make sure to replace `{PROJECT_ID}` with the ID of your Google Cloud project. If you are using docker or another container registry instead, replace the entire image path. + + ```yaml + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + metadata: + name: helloworld-go + namespace: default + spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-go + env: + - name: TARGET + value: "Go Sample v1" + ``` + +## Build and deploy this sample + +Once you have recreated the sample code files (or used the files in the sample folder) you're ready to build and deploy the sample app. + +1. For this example, we'll use Google Cloud Container Builder to build the sample into a container. To use container builder, execute the following gcloud command: + + ```shell + gcloud container builds submit --tag gcr.io/${PROJECT_ID}/helloworld-go + ``` + +1. After the build has completed, you can deploy the app into your cluster. Ensure that the container image value in `app.yaml` matches the container you build in the previous step. Apply the configuration using kubectl: + + ```shell + kubectl apply -f app.yaml + ``` + +1. Now that your service is created, Knative will perform the following steps: + * Create a new immutable revision for this version of the app. + * Network programming to create a route, ingress, service, and load balance for your app. + * Automatically scale your pods up and down (including to zero active pods). + +1. To find the URL and IP address for your service, use kubectl to list the ingress points in the cluster. You may need to wait a few seconds for the ingress point to be created, if you don't see it right away. + + ```shell + kubectl get ing --watch + + NAME HOSTS ADDRESS PORTS AGE + helloworld-go-ingress helloworld-go.default.demo-domain.com,*.helloworld-go.default.demo-domain.com 35.232.134.1 80 1m + ``` + +1. Now you can make a request to your app to see the result. Replace `{IP_ADDRESS}` with the address you see returned in the previous step. + + ```shell + curl -H "Host: helloworld-go.default.demo-domain.com" http://{IP_ADDRESS} + Hello World: NOT SPECIFIED + ``` + +## Remove the sample app deployment + +To remove the sample app from your cluster, delete the service record: + +```shell +kubectl delete -f app.yaml +``` diff --git a/serving/samples/helloworld-go/app.yaml b/serving/samples/helloworld-go/app.yaml new file mode 100644 index 000000000..204f7c0fa --- /dev/null +++ b/serving/samples/helloworld-go/app.yaml @@ -0,0 +1,15 @@ +apiVersion: serving.knative.dev/v1alpha1 +kind: Service +metadata: + name: helloworld-go + namespace: default +spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-go + env: + - name: TARGET + value: "Go Sample v1" diff --git a/serving/samples/helloworld-go/helloworld.go b/serving/samples/helloworld-go/helloworld.go new file mode 100644 index 000000000..e3948aeb3 --- /dev/null +++ b/serving/samples/helloworld-go/helloworld.go @@ -0,0 +1,42 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" +) + +func handler(w http.ResponseWriter, r *http.Request) { + log.Print("Hello world received a request.") + target := os.Getenv("TARGET") + if target == "" { + target = "NOT SPECIFIED" + } + fmt.Fprintf(w, "Hello World: %s!\n", target) +} + +func main() { + flag.Parse() + log.Print("Hello world sample started.") + + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} diff --git a/serving/samples/helloworld-go/sample.yaml b/serving/samples/helloworld-go/sample.yaml new file mode 100644 index 000000000..10ff7376a --- /dev/null +++ b/serving/samples/helloworld-go/sample.yaml @@ -0,0 +1,48 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: serving.knative.dev/v1alpha1 +kind: Configuration +metadata: + name: configuration-example + namespace: default +spec: + revisionTemplate: + metadata: + labels: + knative.dev/type: app + spec: + container: + # This is the Go import path for the binary to containerize + # and substitute here. + image: github.com/knative/serving/sample/helloworld + env: + - name: TARGET + value: shiniestnewestversion + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: serving.knative.dev/v1alpha1 +kind: Route +metadata: + name: route-example + namespace: default +spec: + traffic: + - configurationName: configuration-example + percent: 100 diff --git a/serving/samples/helloworld-go/updated_configuration.yaml b/serving/samples/helloworld-go/updated_configuration.yaml new file mode 100644 index 000000000..2ffe49ba0 --- /dev/null +++ b/serving/samples/helloworld-go/updated_configuration.yaml @@ -0,0 +1,38 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: serving.knative.dev/v1alpha1 +kind: Configuration +metadata: + name: configuration-example + namespace: default +spec: + revisionTemplate: + metadata: + labels: + knative.dev/type: app + spec: + container: + # This is the Go import path for the binary to containerize + # and substitute here. + image: github.com/knative/serving/sample/helloworld + env: + - name: TARGET + value: updatedversion + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 diff --git a/serving/samples/helloworld-nodejs/.gitignore b/serving/samples/helloworld-nodejs/.gitignore new file mode 100644 index 000000000..9aa7a4f99 --- /dev/null +++ b/serving/samples/helloworld-nodejs/.gitignore @@ -0,0 +1 @@ +node_packages/ diff --git a/serving/samples/helloworld-nodejs/Dockerfile b/serving/samples/helloworld-nodejs/Dockerfile new file mode 100644 index 000000000..5685f4761 --- /dev/null +++ b/serving/samples/helloworld-nodejs/Dockerfile @@ -0,0 +1,19 @@ +FROM node:8 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install --only=production +# If you are building your code for production +# RUN npm install --only=production + +# Bundle app source +COPY . . + +EXPOSE 8080 +CMD [ "npm", "start" ] diff --git a/serving/samples/helloworld-nodejs/README.md b/serving/samples/helloworld-nodejs/README.md new file mode 100644 index 000000000..f76c28a64 --- /dev/null +++ b/serving/samples/helloworld-nodejs/README.md @@ -0,0 +1,161 @@ +# Hello World - Node.js + +A simple web app in Node that you can use for testing. +It reads in an env variable 'TARGET' and prints "Hello World: ${TARGET}!" if +TARGET is not specified, it will use "NOT SPECIFIED" as the TARGET. + +## Prerequisites + +* You have a Kubernetes cluster with Knative installed. Follow the [installation instructions](https://github.com/knative/install/) if you need to do this. +* You have installed and initalized [Google Cloud SDK](https://cloud.google.com/sdk/docs/) and have created a project in Google Cloud. +* You have `kubectl` configured to connect to the Kubernetes cluster running Knative. +* [Node.js](https://nodejs.org/en/) installed and configured. + +## Steps to recreate the sample code + +1. Create a new directory and initalize npm. You can accept the defaults, but change the entry point to app.js to be consistent with the sample code here. + + ```shell + npm init + + package name: (helloworld-nodejs) + version: (1.0.0) + description: + entry point: (index.js) app.js + test command: + git repository: + keywords: + author: + license: (ISC) Apache-2.0 + ``` + +1. Install the `express` dependency: + + ```shell + npm install express --save + ``` + +1. Create a new file named 'app.js' and paste the following code: + + ```js + const express = require('express'); + const app = express(); + + app.get('/', function (req, res) { + console.log('Hello world received a request.'); + + var target = process.env.TARGET || 'NOT SPECIFIED'; + res.send('Hello world: ' + target); + }); + + var port = 8080; + app.listen(port, function () { + console.log('Hello world listening on port', port); + }); + ``` + +1. Modify the package.json file to add a start command to the scripts section: + + ```json + { + "name": "knative-serving-helloworld", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "start": "node app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Apache-2.0" + } + ``` + +1. In your project directory, create a file named `Dockerfile` and paste the following code. For detailed instructions on dockerizing a Node.js app, see [Dockerizing a Node.js web app](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/). + + ```docker + FROM node:8 + + # Create app directory + WORKDIR /usr/src/app + + # Install app dependencies + # A wildcard is used to ensure both package.json AND package-lock.json are copied + # where available (npm@5+) + COPY package*.json ./ + + RUN npm install + # If you are building your code for production + # RUN npm install --only=production + + # Bundle app source + COPY . . + + EXPOSE 8080 + CMD [ "npm", "start" ] + ``` + +1. Create a new file, `app.yaml` and copy the following service definition into the file. Make sure to replace `{PROJECT_ID}` with the ID of your Google Cloud project. If you are using docker or another container registry instead, replace the entire image path. + + ```yaml + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + metadata: + name: helloworld-nodejs + namespace: default + spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-nodejs + env: + - name: TARGET + value: "Node.js Sample v1" + ``` + +## Build and deploy this sample + +Once you have recreated the sample code files (or used the files in the sample folder) you're ready to build and deploy the sample app. + +1. For this example, we'll use Google Cloud Container Builder to build the sample into a container. To use container builder, execute the following gcloud command: + + ```shell + gcloud container builds submit --tag gcr.io/${PROJECT_ID}/helloworld-nodejs + ``` + +1. After the build has completed, you can deploy the app into your cluster. Ensure that the container image value in `app.yaml` matches the container you build in the previous step. Apply the configuration using kubectl: + + ```shell + kubectl apply -f app.yaml + ``` + +1. Now that your service is created, Knative will perform the following steps: + * Create a new immutable revision for this version of the app. + * Network programming to create a route, ingress, service, and load balance for your app. + * Automatically scale your pods up and down (including to zero active pods). + +1. To find the URL and IP address for your service, use kubectl to list the ingress points in the cluster. You may need to wait a few seconds for the ingress point to be created, if you don't see it right away. + + ```shell + kubectl get ing --watch + + NAME HOSTS ADDRESS PORTS AGE + helloworld-nodejs-ingress helloworld-nodejs.default.demo-domain.com 35.232.134.1 80 1m + ``` + +1. Now you can make a request to your app to see the result. Replace `{IP_ADDRESS}` with the address you see returned in the previous step. + + ```shell + curl -H "Host: helloworld-nodejs.default.demo-domain.com" http://{IP_ADDRESS} + Hello World: NOT SPECIFIED + ``` + +## Remove the sample app deployment + +To remove the sample app from your cluster, delete the service record: + +```shell +kubectl delete -f app.yaml +``` \ No newline at end of file diff --git a/serving/samples/helloworld-nodejs/app.js b/serving/samples/helloworld-nodejs/app.js new file mode 100644 index 000000000..b07efa0c9 --- /dev/null +++ b/serving/samples/helloworld-nodejs/app.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const express = require('express'); +const app = express(); + +app.get('/', function (req, res) { + console.log('Hello world received a request.'); + + var target = process.env.TARGET || 'NOT SPECIFIED'; + res.send('Hello world: ' + target); +}); + +var port = 8080; +app.listen(port, function () { + console.log('Hello world listening on port', port); +}); diff --git a/serving/samples/helloworld-nodejs/app.yaml b/serving/samples/helloworld-nodejs/app.yaml new file mode 100644 index 000000000..a516e179c --- /dev/null +++ b/serving/samples/helloworld-nodejs/app.yaml @@ -0,0 +1,15 @@ +apiVersion: serving.knative.dev/v1alpha1 +kind: Service +metadata: + name: helloworld-nodejs + namespace: default +spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + image: gcr.io/{PROJECT_ID}/helloworld-nodejs + env: + - name: TARGET + value: "Node.js Sample v1" diff --git a/serving/samples/helloworld-nodejs/package-lock.json b/serving/samples/helloworld-nodejs/package-lock.json new file mode 100644 index 000000000..d8ed9a76d --- /dev/null +++ b/serving/samples/helloworld-nodejs/package-lock.json @@ -0,0 +1,373 @@ +{ + "name": "knative-serving-helloworld", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.3", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "1.4.0", + "type-is": "1.6.16", + "utils-merge": "1.0.1", + "vary": "1.1.2" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "unpipe": "1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.4.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.3", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/serving/samples/helloworld-nodejs/package.json b/serving/samples/helloworld-nodejs/package.json new file mode 100644 index 000000000..7a0f72e70 --- /dev/null +++ b/serving/samples/helloworld-nodejs/package.json @@ -0,0 +1,23 @@ +{ + "name": "knative-serving-helloworld", + "version": "1.0.0", + "description": "Simple hello world sample in Node", + "main": "app.js", + "scripts": { + "start": "node app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/knative/serving.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/knative/serving/issues" + }, + "homepage": "https://github.com/knative/serving#readme", + "dependencies": { + "express": "^4.16.3" + } +}