Skip to content

Commit e9eef70

Browse files
authored
feat(function): add deploy workflow (#2950)
1 parent c70811c commit e9eef70

File tree

9 files changed

+3141
-0
lines changed

9 files changed

+3141
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Create or fetch, upload and deploy your function
4+
5+
USAGE:
6+
scw function deploy [arg=value ...]
7+
8+
ARGS:
9+
[namespace-id] Function Namespace ID to deploy to
10+
name Name of the function to deploy, will be used in namespace's name if no ID is provided
11+
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)
12+
zip-file Path of the zip file that contains your code
13+
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams | pl-waw)
14+
15+
FLAGS:
16+
-h, --help help for deploy
17+
18+
GLOBAL FLAGS:
19+
-c, --config string The path to the config file
20+
-D, --debug Enable debug mode
21+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
22+
-p, --profile string The config profile to use

cmd/scw/testdata/test-all-usage-function-usage.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ AVAILABLE COMMANDS:
1313
runtime Runtime management commands
1414
token Token management commands
1515

16+
WORKFLOW COMMANDS:
17+
deploy Deploy a function
18+
1619
FLAGS:
1720
-h, --help help for function
1821

docs/commands/function.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Functions API.
88
- [Get a cron](#get-a-cron)
99
- [List all your crons](#list-all-your-crons)
1010
- [Update an existing cron](#update-an-existing-cron)
11+
- [Deploy a function](#deploy-a-function)
1112
- [Domain management commands](#domain-management-commands)
1213
- [Create a domain name binding](#create-a-domain-name-binding)
1314
- [Delete a domain name binding](#delete-a-domain-name-binding)
@@ -151,6 +152,31 @@ scw function cron update <cron-id ...> [arg=value ...]
151152

152153

153154

155+
## Deploy a function
156+
157+
Create or fetch, upload and deploy your function
158+
159+
Create or fetch, upload and deploy your function
160+
161+
**Usage:**
162+
163+
```
164+
scw function deploy [arg=value ...]
165+
```
166+
167+
168+
**Args:**
169+
170+
| Name | | Description |
171+
|------|---|-------------|
172+
| namespace-id | | Function Namespace ID to deploy to |
173+
| name | Required | Name of the function to deploy, will be used in namespace's name if no ID is provided |
174+
| 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` | |
175+
| zip-file | Required | Path of the zip file that contains your code |
176+
| 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 |
177+
178+
179+
154180
## Domain management commands
155181

156182
Domain management commands.

internal/namespaces/function/v1beta1/custom.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ func GetCommands() *core.Commands {
1313
human.RegisterMarshalerFunc(function.FunctionStatus(""), human.EnumMarshalFunc(functionStatusMarshalSpecs))
1414
human.RegisterMarshalerFunc(function.CronStatus(""), human.EnumMarshalFunc(cronStatusMarshalSpecs))
1515

16+
cmds.Add(functionDeploy())
17+
1618
return cmds
1719
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package function
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"reflect"
9+
10+
"github.com/scaleway/scaleway-cli/v2/internal/core"
11+
"github.com/scaleway/scaleway-cli/v2/internal/tasks"
12+
function "github.com/scaleway/scaleway-sdk-go/api/function/v1beta1"
13+
"github.com/scaleway/scaleway-sdk-go/scw"
14+
)
15+
16+
type functionDeployRequest struct {
17+
NamespaceID string `json:"namespace_id"`
18+
ZipFile string `json:"zip_file"`
19+
Runtime function.FunctionRuntime `json:"runtime"`
20+
Name string `json:"name"`
21+
Region scw.Region `json:"region"`
22+
}
23+
24+
func functionDeploy() *core.Command {
25+
functionCreate := functionFunctionCreate()
26+
return &core.Command{
27+
Short: `Deploy a function`,
28+
Long: `Create or fetch, upload and deploy your function`,
29+
Namespace: "function",
30+
Resource: "deploy",
31+
Groups: []string{"workflow"},
32+
ArgsType: reflect.TypeOf(functionDeployRequest{}),
33+
ArgSpecs: []*core.ArgSpec{
34+
{
35+
Name: "namespace-id",
36+
Short: "Function Namespace ID to deploy to",
37+
},
38+
{
39+
Name: "name",
40+
Short: "Name of the function to deploy, will be used in namespace's name if no ID is provided",
41+
Required: true,
42+
},
43+
{
44+
Name: "runtime",
45+
EnumValues: functionCreate.ArgSpecs.GetByName("runtime").EnumValues,
46+
Required: true,
47+
},
48+
{
49+
Name: "zip-file",
50+
Short: "Path of the zip file that contains your code",
51+
Required: true,
52+
},
53+
core.RegionArgSpec((&function.API{}).Regions()...),
54+
},
55+
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
56+
args := argsI.(*functionDeployRequest)
57+
scwClient := core.ExtractClient(ctx)
58+
httpClient := core.ExtractHTTPClient(ctx)
59+
api := function.NewAPI(scwClient)
60+
61+
if err := validateRuntime(api, args.Region, args.Runtime); err != nil {
62+
return nil, err
63+
}
64+
65+
zipFileStat, err := os.Stat(args.ZipFile)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to stat zip-file: %w", err)
68+
}
69+
70+
if zipFileStat.Size() < 0 {
71+
return nil, fmt.Errorf("invalid zip-file, invalid size")
72+
}
73+
74+
ts := tasks.Begin()
75+
76+
if args.NamespaceID != "" {
77+
tasks.Add(ts, "Fetching namespace", DeployStepFetchNamespace(api, args.Region, args.NamespaceID))
78+
} else {
79+
tasks.Add(ts, "Creating or fetching namespace", DeployStepCreateNamespace(api, args.Region, args.Name))
80+
}
81+
tasks.Add(ts, "Creating or fetching function", DeployStepCreateFunction(api, args.Name, args.Runtime))
82+
tasks.Add(ts, "Uploading function", DeployStepFunctionUpload(httpClient, scwClient, api, args.ZipFile, zipFileStat.Size()))
83+
tasks.Add(ts, "Deploying function", DeployStepFunctionDeploy(api, args.Runtime))
84+
85+
return ts.Execute(ctx, nil)
86+
},
87+
}
88+
}
89+
90+
func validateRuntime(api *function.API, region scw.Region, runtime function.FunctionRuntime) error {
91+
runtimeName := string(runtime)
92+
93+
resp, err := api.ListFunctionRuntimes(&function.ListFunctionRuntimesRequest{
94+
Region: region,
95+
})
96+
if err != nil {
97+
return fmt.Errorf("failed to list available runtimes: %w", err)
98+
}
99+
for _, r := range resp.Runtimes {
100+
if r.Name == runtimeName {
101+
return nil
102+
}
103+
}
104+
return fmt.Errorf("invalid runtime %q", runtimeName)
105+
}
106+
107+
func DeployStepCreateNamespace(api *function.API, region scw.Region, functionName string) tasks.TaskFunc[any, *function.Namespace] {
108+
return func(t *tasks.Task, args any) (nextArgs *function.Namespace, err error) {
109+
namespaceName := functionName
110+
111+
namespaces, err := api.ListNamespaces(&function.ListNamespacesRequest{
112+
Region: region,
113+
Name: &namespaceName,
114+
})
115+
if err != nil {
116+
return nil, fmt.Errorf("failed to list namespaces: %w", err)
117+
}
118+
for _, ns := range namespaces.Namespaces {
119+
if ns.Name == namespaceName {
120+
return ns, nil
121+
}
122+
}
123+
124+
namespace, err := api.CreateNamespace(&function.CreateNamespaceRequest{
125+
Name: namespaceName,
126+
Region: region,
127+
}, scw.WithContext(t.Ctx))
128+
if err != nil {
129+
return nil, fmt.Errorf("could not create namespace: %w", err)
130+
}
131+
132+
t.AddToCleanUp(func(ctx context.Context) error {
133+
_, err := api.DeleteNamespace(&function.DeleteNamespaceRequest{
134+
Region: namespace.Region,
135+
NamespaceID: namespace.ID,
136+
})
137+
return err
138+
})
139+
140+
namespace, err = api.WaitForNamespace(&function.WaitForNamespaceRequest{
141+
NamespaceID: namespace.ID,
142+
Region: namespace.Region,
143+
})
144+
if err != nil {
145+
return nil, fmt.Errorf("could not fetch created namespace: %w", err)
146+
}
147+
148+
return namespace, nil
149+
}
150+
}
151+
152+
func DeployStepFetchNamespace(api *function.API, region scw.Region, namespaceID string) tasks.TaskFunc[any, *function.Namespace] {
153+
return func(t *tasks.Task, args any) (nextArgs *function.Namespace, err error) {
154+
namespace, err := api.WaitForNamespace(&function.WaitForNamespaceRequest{
155+
NamespaceID: namespaceID,
156+
Region: region,
157+
})
158+
if err != nil {
159+
return nil, fmt.Errorf("could not fetch namespace: %w", err)
160+
}
161+
162+
return namespace, nil
163+
}
164+
}
165+
166+
func DeployStepCreateFunction(api *function.API, functionName string, runtime function.FunctionRuntime) tasks.TaskFunc[*function.Namespace, *function.Function] {
167+
return func(t *tasks.Task, namespace *function.Namespace) (*function.Function, error) {
168+
functions, err := api.ListFunctions(&function.ListFunctionsRequest{
169+
Name: &functionName,
170+
NamespaceID: namespace.ID,
171+
Region: namespace.Region,
172+
})
173+
if err != nil {
174+
return nil, fmt.Errorf("failed to list functions: %w", err)
175+
}
176+
for _, fc := range functions.Functions {
177+
if fc.Name == functionName {
178+
return fc, err
179+
}
180+
}
181+
182+
fc, err := api.CreateFunction(&function.CreateFunctionRequest{
183+
Name: functionName,
184+
NamespaceID: namespace.ID,
185+
Runtime: runtime,
186+
Region: namespace.Region,
187+
}, scw.WithContext(t.Ctx))
188+
if err != nil {
189+
return nil, fmt.Errorf("could not create function: %w", err)
190+
}
191+
192+
t.AddToCleanUp(func(ctx context.Context) error {
193+
_, err := api.DeleteFunction(&function.DeleteFunctionRequest{
194+
FunctionID: fc.ID,
195+
Region: fc.Region,
196+
})
197+
return err
198+
})
199+
200+
return fc, nil
201+
}
202+
}
203+
204+
func DeployStepFunctionUpload(httpClient *http.Client, scwClient *scw.Client, api *function.API, zipPath string, zipSize int64) tasks.TaskFunc[*function.Function, *function.Function] {
205+
return func(t *tasks.Task, fc *function.Function) (nextArgs *function.Function, err error) {
206+
uploadURL, err := api.GetFunctionUploadURL(&function.GetFunctionUploadURLRequest{
207+
Region: fc.Region,
208+
FunctionID: fc.ID,
209+
ContentLength: uint64(zipSize),
210+
})
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
zip, err := os.Open(zipPath)
216+
if err != nil {
217+
return nil, fmt.Errorf("failed to read zip file: %w", err)
218+
}
219+
defer zip.Close()
220+
221+
req, err := http.NewRequest(http.MethodPut, uploadURL.URL, zip)
222+
if err != nil {
223+
return nil, fmt.Errorf("failed to init request: %w", err)
224+
}
225+
req = req.WithContext(t.Ctx)
226+
req.ContentLength = zipSize
227+
228+
for headerName, headerList := range uploadURL.Headers {
229+
for _, header := range *headerList {
230+
req.Header.Add(headerName, header)
231+
}
232+
}
233+
234+
secretKey, _ := scwClient.GetSecretKey()
235+
req.Header.Add("X-Auth-Token", secretKey)
236+
237+
resp, err := httpClient.Do(req)
238+
if err != nil {
239+
return nil, fmt.Errorf("failed to send upload request: %w", err)
240+
}
241+
defer resp.Body.Close()
242+
243+
if resp.StatusCode != http.StatusOK {
244+
return nil, fmt.Errorf("failed to upload function (Status: %d)", resp.StatusCode)
245+
}
246+
247+
return fc, nil
248+
}
249+
}
250+
251+
func DeployStepFunctionDeploy(api *function.API, runtime function.FunctionRuntime) tasks.TaskFunc[*function.Function, *function.Function] {
252+
return func(t *tasks.Task, fc *function.Function) (*function.Function, error) {
253+
fc, err := api.UpdateFunction(&function.UpdateFunctionRequest{
254+
Region: fc.Region,
255+
FunctionID: fc.ID,
256+
Runtime: runtime,
257+
Redeploy: scw.BoolPtr(true),
258+
})
259+
if err != nil {
260+
return nil, err
261+
}
262+
return api.WaitForFunction(&function.WaitForFunctionRequest{
263+
FunctionID: fc.ID,
264+
Region: fc.Region,
265+
})
266+
}
267+
}

0 commit comments

Comments
 (0)