Skip to content

Commit

Permalink
progress with publish
Browse files Browse the repository at this point in the history
  • Loading branch information
cyrildiagne committed Jan 11, 2020
1 parent 9dc487d commit ecf7079
Show file tree
Hide file tree
Showing 26 changed files with 735 additions and 421 deletions.
41 changes: 19 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,36 @@
[![](https://circleci.com/gh/cyrildiagne/kuda/tree/master.svg?style=shield&circle-token=b14f5838ae2acabe21a8255070507f7e36ba510b)](https://circleci.com/gh/cyrildiagne/kuda)
[![](https://img.shields.io/github/v/release/cyrildiagne/kuda?include_prereleases)](https://github.com/cyrildiagne/kuda/releases)

🧪 **Status:** experimental
**Status:** 🧪Experimental

## Develop and deploy APIs on remote GPUs
## Turn any model into a serverless API

Easily turn any model into a serverless API that will consume cloud GPUs only
when it's being called.

Kuda deploys your API as a docker container, so you can use any language, any
framework, and there is no library to import in your code.

Kuda deploys APIs as serverless containers on remote GPUs using [Knative](https://knative.dev).
So you can use any language, any framework, and there is no library to import in your code.
All you need is a Dockerfile.

## Easy to use

- `kuda init` Initializes your local & remote configurations.
- `kuda dev` Deploy the API on remote GPUs in dev mode (with file sync & live reload).
- `kuda deploy` Deploy the API in production mode.
It will be automatically scaled down to zero when there is no traffic,
and back up when there are new requests.
- `kuda init` Initializes your local & remote configurations
- `kuda dev` Deploys the API in dev mode (with file sync & live reload)
- `kuda deploy` Deploys the API in production mode
- `kuda publish` Publishes the API template to the registry

## Features

- Provision GPUs & scale based on traffic (from zero to N)
- Interactive development on remote GPUs from any workstation
- Interactive development on cloud GPUs from any workstation
- Protect & control access to your APIs using API Keys
- HTTPS with TLS termination & automatic certificate management

## Use the frameworks you know

Here's a simple example that prints the result of `nvidia-smi` using [Flask](http://flask.palletsprojects.com):
Here's a minimal example that just prints the result of `nvidia-smi` using
[Flask](http://flask.palletsprojects.com):

- `main.py`

Expand All @@ -40,25 +44,18 @@ app = flask.Flask(__name__)

@app.route('/')
def hello():
return 'Hello GPU!\n\n' + os.popen('nvidia-smi').read()
return 'Hello GPU:\n' + os.popen('nvidia-smi').read()
```

- `Dockerfile`

```Dockerfile
FROM nvidia/cuda:10.1-base

RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& \
apt-get clean && \
apt-get autoremove && \
rm -rf /var/lib/apt/lists/*
RUN apt-get install -y python3 python3-pip

RUN pip3 install setuptools Flask gunicorn

WORKDIR /app

COPY main.py ./main.py

CMD exec gunicorn --bind :80 --workers 1 --threads 8 main:app
Expand All @@ -68,7 +65,6 @@ CMD exec gunicorn --bind :80 --workers 1 --threads 8 main:app

```yaml
name: hello-gpu

deploy:
dockerfile: ./Dockerfile
```
Expand Down Expand Up @@ -100,7 +96,8 @@ Hello GPU!
```

Checkout the full example with annotations in [examples/hello-gpu-flask](examples/hello-gpu-flask).
Checkout the full example with annotations in
[examples/hello-gpu-flask](examples/hello-gpu-flask).

## Get Started

Expand Down
66 changes: 27 additions & 39 deletions cli/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,21 @@ var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy the API remotely in production mode.",
Run: func(cmd *cobra.Command, args []string) {

// Check if dry run
dryRun, err := cmd.Flags().GetBool("dry-run")
if err != nil {
panic(err)
}

published, _ := cmd.Flags().GetString("from")
if published != "" {
deployFromPublished(published, dryRun)
deployFromPublished(published)
} else {
deployFromLocal(dryRun)
deployFromLocal()
}
},
}

func init() {
RootCmd.AddCommand(deployCmd)
deployCmd.Flags().StringP("from", "f", "", "Fully qualified name of a published API image.")
deployCmd.Flags().Bool("dry-run", false, "Generate the config files but skip execution.")
}

func deployFromPublished(published string, dryRun bool) error {
func deployFromPublished(published string) error {
fmt.Println("Deploy from published API image", published)

fmt.Println("Sending to deployer:", cfg.Deployer.Remote.DeployerURL)
Expand All @@ -58,7 +50,7 @@ func deployFromPublished(published string, dryRun bool) error {
// Close writer
writer.Close()

url := cfg.Deployer.Remote.DeployerURL
url := cfg.Deployer.Remote.DeployerURL + "/deploy"
req, err := http.NewRequest("POST", url, body)
if err != nil {
return err
Expand All @@ -71,7 +63,7 @@ func deployFromPublished(published string, dryRun bool) error {
return nil
}

func deployFromLocal(dryRun bool) {
func deployFromLocal() {
// Load the manifest
manifestFile := "./kuda.yaml"
manifest, err := utils.LoadManifest(manifestFile)
Expand All @@ -81,17 +73,17 @@ func deployFromLocal(dryRun bool) {
}

if cfg.Deployer.Remote != nil {
if err := deployWithRemote(manifest, dryRun); err != nil {
if err := deploy(manifest); err != nil {
fmt.Println("ERROR:", err)
}
} else if cfg.Deployer.Skaffold != nil {
if err := deployWithSkaffold(manifest, dryRun); err != nil {
if err := deployWithSkaffold(manifest); err != nil {
fmt.Println("ERROR:", err)
}
}
}

func deployWithRemote(manifest *latest.Manifest, dryRun bool) error {
func addContextFilesToRequest(source string, writer *multipart.Writer) error {
// Create destination tar file
output, err := ioutil.TempFile("", "*.tar")
fmt.Println("Building context tar:", output.Name())
Expand All @@ -107,22 +99,11 @@ func deployWithRemote(manifest *latest.Manifest, dryRun bool) error {
defer dockerignore.Close()

// Tar context folder.
source := "./"
utils.Tar(source, output.Name(), output, dockerignore)

// Stop here if dry run.
if dryRun {
fmt.Println("Dry run: Skipping remote deployment.")
return nil
}

// Defer the deletion of the temp tar file.
defer os.Remove(output.Name())

// Create request
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)

// Add tar file to request
file, err := os.Open(output.Name())
defer file.Close()
Expand All @@ -135,14 +116,28 @@ func deployWithRemote(manifest *latest.Manifest, dryRun bool) error {
}
io.Copy(part, file)

return nil
}

func deploy(manifest *latest.Manifest) error {
// Create request body
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add context
if err := addContextFilesToRequest("./", writer); err != nil {
return err
}
// Add namespace
writer.WriteField("namespace", cfg.Namespace)

// Close writer
writer.Close()

url := cfg.Deployer.Remote.DeployerURL
// Create request.
url := cfg.Deployer.Remote.DeployerURL + "/deploy"
req, err := http.NewRequest("POST", url, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())

// Send to remote deployer.
Expand Down Expand Up @@ -186,7 +181,7 @@ func sendToRemoteDeployer(req *http.Request) error {
return nil
}

func deployWithSkaffold(manifest *latest.Manifest, dryRun bool) error {
func deployWithSkaffold(manifest *latest.Manifest) error {

folder := cfg.Deployer.Skaffold.ConfigFolder
registry := cfg.Deployer.Skaffold.DockerRegistry
Expand All @@ -197,20 +192,13 @@ func deployWithSkaffold(manifest *latest.Manifest, dryRun bool) error {
DockerArtifact: registry + "/" + manifest.Name,
}

skaffoldFile, err := utils.GenerateSkaffoldConfigFiles(service, manifest.Deploy, folder)
if err != nil {
if err := utils.GenerateSkaffoldConfigFiles(service, manifest.Deploy, folder); err != nil {
return err
}
fmt.Println("Config files have been written in:", folder)

// Stop here if dry run.
if dryRun {
fmt.Println("Dry run: Skipping execution.")
return nil
}

// Run command.
args := []string{"run", "-f", skaffoldFile}
args := []string{"run", "-f", folder + "/skaffold.yaml"}
cmd := exec.Command("skaffold", args...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
Expand Down
22 changes: 4 additions & 18 deletions cli/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,11 @@ var devCmd = &cobra.Command{
panic(err)
}

// Check if dry run
dryRun, err := cmd.Flags().GetBool("dry-run")
if err != nil {
panic(err)
}

if cfg.Deployer.Remote != nil {
panic("dev is not yet supported on remote deployers")
} else if cfg.Deployer.Skaffold != nil {
// Start dev with Skaffold.
if err := devWithSkaffold(*manifest, dryRun); err != nil {
if err := devWithSkaffold(*manifest); err != nil {
fmt.Println("ERROR:", err)
}
}
Expand All @@ -43,10 +37,9 @@ var devCmd = &cobra.Command{

func init() {
RootCmd.AddCommand(devCmd)
devCmd.Flags().Bool("dry-run", false, "Generate the config files but skip execution.")
}

func devWithSkaffold(manifest latest.Manifest, dryRun bool) error {
func devWithSkaffold(manifest latest.Manifest) error {

folder := cfg.Deployer.Skaffold.ConfigFolder
registry := cfg.Deployer.Skaffold.DockerRegistry
Expand All @@ -57,20 +50,13 @@ func devWithSkaffold(manifest latest.Manifest, dryRun bool) error {
DockerArtifact: registry + "/" + manifest.Name,
}

skaffoldFile, err := utils.GenerateSkaffoldConfigFiles(service, manifest.Dev, folder)
if err != nil {
if err := utils.GenerateSkaffoldConfigFiles(service, manifest.Dev, folder); err != nil {
return err
}
fmt.Println("Config files have been written in:", folder)

// Stop here if dry run.
if dryRun {
fmt.Println("Dry run: Skipping execution.")
return nil
}

// Run command.
args := []string{"dev", "-f", skaffoldFile}
args := []string{"dev", "-f", folder + "/skaffold.yaml"}
cmd := exec.Command("skaffold", args...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
Expand Down
28 changes: 13 additions & 15 deletions cli/cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package cmd

import (
"bytes"
"errors"
"fmt"
"mime/multipart"
"net/http"

"github.com/cyrildiagne/kuda/pkg/manifest/latest"
"github.com/cyrildiagne/kuda/pkg/utils"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
// "github.com/go-openapi/loads/fmts"
)

// publishCmd represents the `kuda publish` command.
Expand All @@ -23,9 +22,13 @@ var publishCmd = &cobra.Command{
manifestFile := "./kuda.yaml"
manifest, err := utils.LoadManifest(manifestFile)
if err != nil {
fmt.Println("Could not load manifest", manifestFile)
fmt.Println("Could not load ./kuda.yaml", manifestFile)
panic(err)
}

// TODO: Ensure there is an OpenAPI spec.

// Publish
if err := publish(manifest); err != nil {
panic(err)
}
Expand All @@ -34,35 +37,30 @@ var publishCmd = &cobra.Command{

func init() {
RootCmd.AddCommand(publishCmd)
publishCmd.Flags().Bool("dry-run", false, "Check the manifest for publication but skip execution.")
}

func publish(manifest *latest.Manifest) error {
// Make sure a version is set.
if manifest.Version == "" {
return errors.New("version missing in manifest file")
}
// Create request
// Create request body
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add namespace
writer.WriteField("namespace", cfg.Namespace)
// Add manifest
manifestYAML, err := yaml.Marshal(manifest)
if err != nil {
// Add context
if err := addContextFilesToRequest("./", writer); err != nil {
return err
}
writer.WriteField("manifest", string(manifestYAML))
// Add namespace
writer.WriteField("namespace", cfg.Namespace)
// Close writer
writer.Close()

// Create request.
url := cfg.Deployer.Remote.DeployerURL + "/publish"
req, err := http.NewRequest("POST", url, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())

// Send to remote deployer.
if err := sendToRemoteDeployer(req); err != nil {
return err
}
Expand Down
Loading

0 comments on commit ecf7079

Please sign in to comment.