Skip to content

Commit

Permalink
feat(function): add deploy workflow (#2950)
Browse files Browse the repository at this point in the history
  • Loading branch information
Codelax committed Apr 12, 2023
1 parent c70811c commit e9eef70
Show file tree
Hide file tree
Showing 9 changed files with 3,141 additions and 0 deletions.
22 changes: 22 additions & 0 deletions cmd/scw/testdata/test-all-usage-function-deploy-usage.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
Create or fetch, upload and deploy your function

USAGE:
scw function deploy [arg=value ...]

ARGS:
[namespace-id] Function Namespace ID to deploy to
name Name of the function to deploy, will be used in namespace's name if no ID is provided
runtime (unknown_runtime | golang | python | python3 | node8 | node10 | node14 | node16 | node17 | python37 | python38 | python39 | python310 | go113 | go117 | go118 | node18 | rust165 | go119 | python311 | php82 | node19 | go120)
zip-file Path of the zip file that contains your code
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams | pl-waw)

FLAGS:
-h, --help help for deploy

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
3 changes: 3 additions & 0 deletions cmd/scw/testdata/test-all-usage-function-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ AVAILABLE COMMANDS:
runtime Runtime management commands
token Token management commands

WORKFLOW COMMANDS:
deploy Deploy a function

FLAGS:
-h, --help help for function

Expand Down
26 changes: 26 additions & 0 deletions docs/commands/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Functions API.
- [Get a cron](#get-a-cron)
- [List all your crons](#list-all-your-crons)
- [Update an existing cron](#update-an-existing-cron)
- [Deploy a function](#deploy-a-function)
- [Domain management commands](#domain-management-commands)
- [Create a domain name binding](#create-a-domain-name-binding)
- [Delete a domain name binding](#delete-a-domain-name-binding)
Expand Down Expand Up @@ -151,6 +152,31 @@ scw function cron update <cron-id ...> [arg=value ...]



## Deploy a function

Create or fetch, upload and deploy your function

Create or fetch, upload and deploy your function

**Usage:**

```
scw function deploy [arg=value ...]
```


**Args:**

| Name | | Description |
|------|---|-------------|
| namespace-id | | Function Namespace ID to deploy to |
| name | Required | Name of the function to deploy, will be used in namespace's name if no ID is provided |
| runtime | Required<br />One of: `unknown_runtime`, `golang`, `python`, `python3`, `node8`, `node10`, `node14`, `node16`, `node17`, `python37`, `python38`, `python39`, `python310`, `go113`, `go117`, `go118`, `node18`, `rust165`, `go119`, `python311`, `php82`, `node19`, `go120` | |
| zip-file | Required | Path of the zip file that contains your code |
| region | Default: `fr-par`<br />One of: `fr-par`, `nl-ams`, `pl-waw` | Region to target. If none is passed will use default region from the config |



## Domain management commands

Domain management commands.
Expand Down
2 changes: 2 additions & 0 deletions internal/namespaces/function/v1beta1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ func GetCommands() *core.Commands {
human.RegisterMarshalerFunc(function.FunctionStatus(""), human.EnumMarshalFunc(functionStatusMarshalSpecs))
human.RegisterMarshalerFunc(function.CronStatus(""), human.EnumMarshalFunc(cronStatusMarshalSpecs))

cmds.Add(functionDeploy())

return cmds
}
267 changes: 267 additions & 0 deletions internal/namespaces/function/v1beta1/custom_deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package function

import (
"context"
"fmt"
"net/http"
"os"
"reflect"

"github.com/scaleway/scaleway-cli/v2/internal/core"
"github.com/scaleway/scaleway-cli/v2/internal/tasks"
function "github.com/scaleway/scaleway-sdk-go/api/function/v1beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type functionDeployRequest struct {
NamespaceID string `json:"namespace_id"`
ZipFile string `json:"zip_file"`
Runtime function.FunctionRuntime `json:"runtime"`
Name string `json:"name"`
Region scw.Region `json:"region"`
}

func functionDeploy() *core.Command {
functionCreate := functionFunctionCreate()
return &core.Command{
Short: `Deploy a function`,
Long: `Create or fetch, upload and deploy your function`,
Namespace: "function",
Resource: "deploy",
Groups: []string{"workflow"},
ArgsType: reflect.TypeOf(functionDeployRequest{}),
ArgSpecs: []*core.ArgSpec{
{
Name: "namespace-id",
Short: "Function Namespace ID to deploy to",
},
{
Name: "name",
Short: "Name of the function to deploy, will be used in namespace's name if no ID is provided",
Required: true,
},
{
Name: "runtime",
EnumValues: functionCreate.ArgSpecs.GetByName("runtime").EnumValues,
Required: true,
},
{
Name: "zip-file",
Short: "Path of the zip file that contains your code",
Required: true,
},
core.RegionArgSpec((&function.API{}).Regions()...),
},
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
args := argsI.(*functionDeployRequest)
scwClient := core.ExtractClient(ctx)
httpClient := core.ExtractHTTPClient(ctx)
api := function.NewAPI(scwClient)

if err := validateRuntime(api, args.Region, args.Runtime); err != nil {
return nil, err
}

zipFileStat, err := os.Stat(args.ZipFile)
if err != nil {
return nil, fmt.Errorf("failed to stat zip-file: %w", err)
}

if zipFileStat.Size() < 0 {
return nil, fmt.Errorf("invalid zip-file, invalid size")
}

ts := tasks.Begin()

if args.NamespaceID != "" {
tasks.Add(ts, "Fetching namespace", DeployStepFetchNamespace(api, args.Region, args.NamespaceID))
} else {
tasks.Add(ts, "Creating or fetching namespace", DeployStepCreateNamespace(api, args.Region, args.Name))
}
tasks.Add(ts, "Creating or fetching function", DeployStepCreateFunction(api, args.Name, args.Runtime))
tasks.Add(ts, "Uploading function", DeployStepFunctionUpload(httpClient, scwClient, api, args.ZipFile, zipFileStat.Size()))
tasks.Add(ts, "Deploying function", DeployStepFunctionDeploy(api, args.Runtime))

return ts.Execute(ctx, nil)
},
}
}

func validateRuntime(api *function.API, region scw.Region, runtime function.FunctionRuntime) error {
runtimeName := string(runtime)

resp, err := api.ListFunctionRuntimes(&function.ListFunctionRuntimesRequest{
Region: region,
})
if err != nil {
return fmt.Errorf("failed to list available runtimes: %w", err)
}
for _, r := range resp.Runtimes {
if r.Name == runtimeName {
return nil
}
}
return fmt.Errorf("invalid runtime %q", runtimeName)
}

func DeployStepCreateNamespace(api *function.API, region scw.Region, functionName string) tasks.TaskFunc[any, *function.Namespace] {
return func(t *tasks.Task, args any) (nextArgs *function.Namespace, err error) {
namespaceName := functionName

namespaces, err := api.ListNamespaces(&function.ListNamespacesRequest{
Region: region,
Name: &namespaceName,
})
if err != nil {
return nil, fmt.Errorf("failed to list namespaces: %w", err)
}
for _, ns := range namespaces.Namespaces {
if ns.Name == namespaceName {
return ns, nil
}
}

namespace, err := api.CreateNamespace(&function.CreateNamespaceRequest{
Name: namespaceName,
Region: region,
}, scw.WithContext(t.Ctx))
if err != nil {
return nil, fmt.Errorf("could not create namespace: %w", err)
}

t.AddToCleanUp(func(ctx context.Context) error {
_, err := api.DeleteNamespace(&function.DeleteNamespaceRequest{
Region: namespace.Region,
NamespaceID: namespace.ID,
})
return err
})

namespace, err = api.WaitForNamespace(&function.WaitForNamespaceRequest{
NamespaceID: namespace.ID,
Region: namespace.Region,
})
if err != nil {
return nil, fmt.Errorf("could not fetch created namespace: %w", err)
}

return namespace, nil
}
}

func DeployStepFetchNamespace(api *function.API, region scw.Region, namespaceID string) tasks.TaskFunc[any, *function.Namespace] {
return func(t *tasks.Task, args any) (nextArgs *function.Namespace, err error) {
namespace, err := api.WaitForNamespace(&function.WaitForNamespaceRequest{
NamespaceID: namespaceID,
Region: region,
})
if err != nil {
return nil, fmt.Errorf("could not fetch namespace: %w", err)
}

return namespace, nil
}
}

func DeployStepCreateFunction(api *function.API, functionName string, runtime function.FunctionRuntime) tasks.TaskFunc[*function.Namespace, *function.Function] {
return func(t *tasks.Task, namespace *function.Namespace) (*function.Function, error) {
functions, err := api.ListFunctions(&function.ListFunctionsRequest{
Name: &functionName,
NamespaceID: namespace.ID,
Region: namespace.Region,
})
if err != nil {
return nil, fmt.Errorf("failed to list functions: %w", err)
}
for _, fc := range functions.Functions {
if fc.Name == functionName {
return fc, err
}
}

fc, err := api.CreateFunction(&function.CreateFunctionRequest{
Name: functionName,
NamespaceID: namespace.ID,
Runtime: runtime,
Region: namespace.Region,
}, scw.WithContext(t.Ctx))
if err != nil {
return nil, fmt.Errorf("could not create function: %w", err)
}

t.AddToCleanUp(func(ctx context.Context) error {
_, err := api.DeleteFunction(&function.DeleteFunctionRequest{
FunctionID: fc.ID,
Region: fc.Region,
})
return err
})

return fc, nil
}
}

func DeployStepFunctionUpload(httpClient *http.Client, scwClient *scw.Client, api *function.API, zipPath string, zipSize int64) tasks.TaskFunc[*function.Function, *function.Function] {
return func(t *tasks.Task, fc *function.Function) (nextArgs *function.Function, err error) {
uploadURL, err := api.GetFunctionUploadURL(&function.GetFunctionUploadURLRequest{
Region: fc.Region,
FunctionID: fc.ID,
ContentLength: uint64(zipSize),
})
if err != nil {
return nil, err
}

zip, err := os.Open(zipPath)
if err != nil {
return nil, fmt.Errorf("failed to read zip file: %w", err)
}
defer zip.Close()

req, err := http.NewRequest(http.MethodPut, uploadURL.URL, zip)
if err != nil {
return nil, fmt.Errorf("failed to init request: %w", err)
}
req = req.WithContext(t.Ctx)
req.ContentLength = zipSize

for headerName, headerList := range uploadURL.Headers {
for _, header := range *headerList {
req.Header.Add(headerName, header)
}
}

secretKey, _ := scwClient.GetSecretKey()
req.Header.Add("X-Auth-Token", secretKey)

resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send upload request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to upload function (Status: %d)", resp.StatusCode)
}

return fc, nil
}
}

func DeployStepFunctionDeploy(api *function.API, runtime function.FunctionRuntime) tasks.TaskFunc[*function.Function, *function.Function] {
return func(t *tasks.Task, fc *function.Function) (*function.Function, error) {
fc, err := api.UpdateFunction(&function.UpdateFunctionRequest{
Region: fc.Region,
FunctionID: fc.ID,
Runtime: runtime,
Redeploy: scw.BoolPtr(true),
})
if err != nil {
return nil, err
}
return api.WaitForFunction(&function.WaitForFunctionRequest{
FunctionID: fc.ID,
Region: fc.Region,
})
}
}

0 comments on commit e9eef70

Please sign in to comment.