Skip to content

Commit 15325bf

Browse files
committed
feat: skip code upload if code hasn't changed
1 parent 0f4003e commit 15325bf

File tree

5 files changed

+80
-16
lines changed

5 files changed

+80
-16
lines changed

internal/constants/constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const (
1414
// It's used as an extra safety measure to avoid deleting resources not created by this tool.
1515
TagCreatedByScalewayMCP = "created_by=" + ProjectName
1616

17+
// TagCodeArchiveDigest is the tag key used to store the code archive digest.
18+
// It's used to avoid redeploying the same code.
19+
TagCodeArchiveDigest = "code_archive_digest="
20+
1721
// RequiredPermissionSets is the minimum permission sets required for the Scaleway API key.
1822
RequiredPermissionSets = "FunctionsFullAccess, ObservabilityFullAccess"
1923

internal/scaleway/create_deploy_function.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"time"
77

8+
"github.com/cyclimse/mcp-scaleway-functions/internal/constants"
89
"github.com/modelcontextprotocol/go-sdk/mcp"
910
function "github.com/scaleway/scaleway-sdk-go/api/function/v1beta1"
1011
"github.com/scaleway/scaleway-sdk-go/scw"
@@ -107,6 +108,8 @@ func (t *Tools) CreateAndDeployFunction(
107108
return nil, Function{}, fmt.Errorf("converting to SDK request: %w", err)
108109
}
109110

111+
// We always create the function first before zipping the code archive for
112+
// faster feedback to the user in case of errors.
110113
fun, err := t.functionsAPI.CreateFunction(createReq, scw.WithContext(ctx))
111114
if err != nil {
112115
return nil, Function{}, fmt.Errorf("creating function: %w", err)
@@ -119,6 +122,20 @@ func (t *Tools) CreateAndDeployFunction(
119122
return nil, Function{}, fmt.Errorf("creating archive: %w", err)
120123
}
121124

125+
tags := append(fun.Tags, constants.TagCodeArchiveDigest+archive.Digest)
126+
127+
// However, as a side-effect of doing creation first, we need to
128+
// update the function to add the code archive digest tag (which helps
129+
// avoid redeploying the same code in future updates).
130+
fun, err = t.functionsAPI.UpdateFunction(&function.UpdateFunctionRequest{
131+
FunctionID: fun.ID,
132+
Redeploy: scw.BoolPtr(false),
133+
Tags: scw.StringsPtr(tags),
134+
}, scw.WithContext(ctx))
135+
if err != nil {
136+
return nil, Function{}, fmt.Errorf("updating function with code archive digest tag: %w", err)
137+
}
138+
122139
presignedURLResp, err := t.functionsAPI.GetFunctionUploadURL(
123140
&function.GetFunctionUploadURLRequest{
124141
FunctionID: fun.ID,

internal/scaleway/helpers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"slices"
8+
"strings"
89
"time"
910

1011
"github.com/cyclimse/mcp-scaleway-functions/internal/constants"
@@ -95,6 +96,19 @@ func checkResourceOwnership(tags []string) error {
9596
return nil
9697
}
9798

99+
func getCodeArchiveDigestFromTags(tags []string) (string, bool) {
100+
prefix := constants.TagCodeArchiveDigest
101+
102+
for _, tag := range tags {
103+
after, found := strings.CutPrefix(tag, prefix)
104+
if found {
105+
return after, true
106+
}
107+
}
108+
109+
return "", false
110+
}
111+
98112
type WaitForFunctionCallback func(fun *function.Function)
99113

100114
// waitForFunction waits for a function to be in a terminal state (ready or error), running the

internal/scaleway/update_function.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,29 @@ func (t *Tools) UpdateFunction(
9292
return nil, Function{}, fmt.Errorf("creating archive: %w", err)
9393
}
9494

95-
presignedURLResp, err := t.functionsAPI.GetFunctionUploadURL(
96-
&function.GetFunctionUploadURLRequest{
97-
FunctionID: fun.ID,
98-
ContentLength: archive.Size,
99-
},
100-
scw.WithContext(ctx),
101-
)
102-
if err != nil {
103-
return nil, Function{}, fmt.Errorf("getting presigned URL: %w", err)
95+
shouldSkipUpload := false
96+
digest, found := getCodeArchiveDigestFromTags(fun.Tags)
97+
if found && archive.CompareDigest(digest) {
98+
shouldSkipUpload = true
10499
}
105100

106-
progress.NotifyCodeUploading(ctx, req)
101+
if !shouldSkipUpload {
102+
presignedURLResp, err := t.functionsAPI.GetFunctionUploadURL(
103+
&function.GetFunctionUploadURLRequest{
104+
FunctionID: fun.ID,
105+
ContentLength: archive.Size,
106+
},
107+
scw.WithContext(ctx),
108+
)
109+
if err != nil {
110+
return nil, Function{}, fmt.Errorf("getting presigned URL: %w", err)
111+
}
107112

108-
if err := archive.Upload(ctx, presignedURLResp.URL); err != nil {
109-
return nil, Function{}, fmt.Errorf("uploading archive: %w", err)
113+
progress.NotifyCodeUploading(ctx, req)
114+
115+
if err := archive.Upload(ctx, presignedURLResp.URL); err != nil {
116+
return nil, Function{}, fmt.Errorf("uploading archive: %w", err)
117+
}
110118
}
111119

112120
updateReq, err := in.ToSDK(fun.ID)

internal/scaleway/zip.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scaleway
33
import (
44
"archive/zip"
55
"context"
6+
"crypto/sha256"
67
"errors"
78
"fmt"
89
"io"
@@ -20,8 +21,9 @@ var (
2021
)
2122

2223
type CodeArchive struct {
23-
Path string
24-
Size uint64
24+
Path string
25+
Size uint64
26+
Digest string
2527
}
2628

2729
func NewCodeArchive(from string) (*CodeArchive, error) {
@@ -49,12 +51,22 @@ func NewCodeArchive(from string) (*CodeArchive, error) {
4951
return nil, fmt.Errorf("getting zip file stat: %w", err)
5052
}
5153

54+
digest, err := computeFileDigest(zipFile)
55+
if err != nil {
56+
return nil, fmt.Errorf("computing zip file digest: %w", err)
57+
}
58+
5259
return &CodeArchive{
53-
Path: zipFile.Name(),
54-
Size: safeConvertInt64ToUint64(stat.Size()),
60+
Path: zipFile.Name(),
61+
Size: safeConvertInt64ToUint64(stat.Size()),
62+
Digest: digest,
5563
}, nil
5664
}
5765

66+
func (f *CodeArchive) CompareDigest(otherDigest string) bool {
67+
return f.Digest == otherDigest
68+
}
69+
5870
func (f *CodeArchive) Upload(ctx context.Context, preSignedURL string) error {
5971
zipFile, err := os.Open(f.Path)
6072
if err != nil {
@@ -187,6 +199,15 @@ func zipDirectory(zipFile *os.File, pathToDir string) error {
187199
return nil
188200
}
189201

202+
func computeFileDigest(file *os.File) (string, error) {
203+
hash := sha256.New()
204+
if _, err := io.Copy(hash, file); err != nil {
205+
return "", fmt.Errorf("computing file digest: %w", err)
206+
}
207+
208+
return fmt.Sprintf("%x", hash.Sum(nil)), nil
209+
}
210+
190211
//nolint:revive,funlen
191212
func unzipDirectory(zipPath, toDir string) error {
192213
root, err := os.OpenRoot(toDir)

0 commit comments

Comments
 (0)