Skip to content

Commit d539701

Browse files
KevyVolionello
andauthored
Railpack CLI feature (#1301)
* Allow no physical Dockerfile and adjust for Normalization * Added testdata Compose file for railpack * Railpack logic 1: If there is a nontraditionally named Dockerfile in the tree and one specified in the compose file, use it. 2: If there is no Dockerfile in the tree, but one is specified which is named "Dockerfile" then use the pack builder. 3: If there is no Dockerfile in the tree, but one is specified which is named something else that is not "Dockerfile" then we error. 4: If there is a Dockerfile in the tree, we use it if not specified in the compose file. * Expected testcases * Handle case if there no dockerile at the desc location in compose * simplfy railpack logic * Railpack feature passing all cases * update UploadModIgnore * Revert test * Move contextAwareWriter role to ContextAwareReader * reverted testcases * Apply suggestions from code review * Collect dockerfile field modifcation to one area --------- Co-authored-by: Lio李歐 <[email protected]>
1 parent 1f26656 commit d539701

File tree

12 files changed

+265
-61
lines changed

12 files changed

+265
-61
lines changed

src/pkg/cli/compose/context.go

Lines changed: 130 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package compose
22

33
import (
44
"archive/tar"
5+
"archive/zip"
56
"bytes"
67
"compress/gzip"
78
"context"
89
"crypto/sha256"
910
"encoding/base64"
1011
"fmt"
1112
"io"
13+
"io/fs"
1214
"os"
1315
"path/filepath"
1416
"strings"
@@ -72,6 +74,94 @@ defang.exe
7274
.defang`
7375
)
7476

77+
type ArchiveType string
78+
79+
const ArchiveTypeZip ArchiveType = "application/zip"
80+
const ArchiveTypeGzip ArchiveType = "application/gzip"
81+
82+
type WriterFactory interface {
83+
CreateHeader(info fs.FileInfo, slashPath string) (io.Writer, error)
84+
Close() error
85+
}
86+
87+
type tarFactory struct {
88+
*tar.Writer
89+
gzipWriter io.WriteCloser
90+
}
91+
92+
func (tw *tarFactory) CreateHeader(info fs.FileInfo, slashPath string) (io.Writer, error) {
93+
// Convert zip header to tar header
94+
header, err := tar.FileInfoHeader(info, info.Name())
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
if !info.Mode().IsRegular() {
100+
return nil, nil
101+
}
102+
103+
// Make reproducible; WalkDir walks files in lexical order.
104+
header.ModTime = time.Unix(sourceDateEpoch, 0)
105+
header.Gid = 0
106+
header.Uid = 0
107+
header.Name = slashPath
108+
err = tw.WriteHeader(header)
109+
return tw.Writer, err
110+
}
111+
112+
func (tw *tarFactory) Close() error {
113+
// Close the tar and gzip writers before returning the buffer
114+
err := tw.Writer.Close()
115+
if err != nil {
116+
return err
117+
}
118+
119+
err = tw.gzipWriter.Close()
120+
if err != nil {
121+
return err
122+
}
123+
124+
return nil
125+
}
126+
127+
type zipFactory struct {
128+
*zip.Writer
129+
}
130+
131+
func (zw *zipFactory) CreateHeader(info fs.FileInfo, slashPath string) (io.Writer, error) {
132+
// Create a new zip file header
133+
header := &zip.FileHeader{
134+
Name: slashPath,
135+
Method: zip.Deflate,
136+
}
137+
138+
// Make reproducible
139+
header.Modified = time.Unix(sourceDateEpoch, 0)
140+
141+
if !info.Mode().IsRegular() {
142+
if info.IsDir() {
143+
header.Name = slashPath + "/" // Ensure directory paths end with slash
144+
header.Method = zip.Store // Directories are stored without compression
145+
_, err := zw.Writer.CreateHeader(header)
146+
return nil, err
147+
}
148+
return nil, nil
149+
}
150+
151+
// Create file entry in zip
152+
writer, err := zw.Writer.CreateHeader(header)
153+
return writer, err
154+
}
155+
156+
func (zw *zipFactory) Close() error {
157+
// Close the zip writer before returning the buffer
158+
err := zw.Writer.Close()
159+
if err != nil {
160+
return err
161+
}
162+
return nil
163+
}
164+
75165
func parseContextLimit(limit string, def int64) int64 {
76166
if size, err := units.RAMInBytes(limit); err == nil {
77167
return size
@@ -89,17 +179,26 @@ func getRemoteBuildContext(ctx context.Context, provider client.Provider, projec
89179
return "", fmt.Errorf("invalid build context: %w", err) // already checked in ValidateProject
90180
}
91181

182+
var archiveType ArchiveType
183+
// If we have a Railpack build, we use a zip archive
184+
if build.Dockerfile == RAILPACK {
185+
archiveType = ArchiveTypeZip
186+
// We use tar for all other builds
187+
} else {
188+
archiveType = ArchiveTypeGzip
189+
}
190+
92191
switch upload {
93192
case UploadModeIgnore:
94-
// `compose config`, ie. dry-run: don't upload the tarball, just return the path as-is
193+
// `compose config`, ie. dry-run: don't upload the archive, just return the path as-is
95194
return root, nil
96195
case UploadModeEstimate:
97196
// For estimation, we don't bother packaging the files, we just return a placeholder URL
98197
return fmt.Sprintf("s3://cd-preview/%v", time.Now().Unix()), nil
99198
}
100199

101200
term.Info("Packaging the project files for", name, "at", root)
102-
buffer, err := createTarball(ctx, build.Context, build.Dockerfile)
201+
buffer, err := createArchive(ctx, build.Context, build.Dockerfile, archiveType)
103202
if err != nil {
104203
return "", err
105204
}
@@ -121,19 +220,19 @@ func getRemoteBuildContext(ctx context.Context, provider client.Provider, projec
121220
}
122221

123222
term.Info("Uploading the project files for", name)
124-
return uploadTarball(ctx, provider, project, buffer, digest)
223+
return uploadArchive(ctx, provider, project, buffer, archiveType, digest)
125224
}
126225

127-
func uploadTarball(ctx context.Context, provider client.Provider, project string, body io.Reader, digest string) (string, error) {
128-
// Upload the tarball to the fabric controller storage;; TODO: use a streaming API
226+
func uploadArchive(ctx context.Context, provider client.Provider, project string, body io.Reader, contentType ArchiveType, digest string) (string, error) {
227+
// Upload the archive to the fabric controller storage;; TODO: use a streaming API
129228
ureq := &defangv1.UploadURLRequest{Digest: digest, Project: project}
130229
res, err := provider.CreateUploadURL(ctx, ureq)
131230
if err != nil {
132231
return "", err
133232
}
134233

135234
// Do an HTTP PUT to the generated URL
136-
resp, err := http.Put(ctx, res.Url, "application/gzip", body)
235+
resp, err := http.Put(ctx, res.Url, string(contentType), body)
137236
if err != nil {
138237
return "", err
139238
}
@@ -150,17 +249,17 @@ func uploadTarball(ctx context.Context, provider client.Provider, project string
150249
return url, nil
151250
}
152251

153-
type contextAwareWriter struct {
252+
type contextAwareReader struct {
154253
ctx context.Context
155-
io.WriteCloser
254+
io.ReadCloser
156255
}
157256

158-
func (cw contextAwareWriter) Write(p []byte) (n int, err error) {
257+
func (cr contextAwareReader) Read(p []byte) (n int, err error) {
159258
select {
160-
case <-cw.ctx.Done(): // Detect context cancelation
161-
return 0, cw.ctx.Err()
259+
case <-cr.ctx.Done(): // Detect context cancelation
260+
return 0, cr.ctx.Err()
162261
default:
163-
return cw.WriteCloser.Write(p)
262+
return cr.ReadCloser.Read(p)
164263
}
165264
}
166265

@@ -229,7 +328,6 @@ func getDockerIgnorePatterns(root, dockerfile string) ([]string, string, error)
229328
}
230329

231330
func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEntry, slashPath string) error) error {
232-
foundDockerfile := false
233331
if dockerfile == "" {
234332
dockerfile = "Dockerfile"
235333
} else {
@@ -267,7 +365,6 @@ func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEn
267365

268366
// we need the Dockerfile, even if it's in the .dockerignore file
269367
if relPath == dockerfile {
270-
foundDockerfile = true
271368
} else if relPath == dockerignore {
272369
// we need the .dockerignore file too: it might ignore itself and/or the Dockerfile, but is needed by the builder
273370
} else {
@@ -291,19 +388,23 @@ func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEn
291388
return err
292389
}
293390

294-
if !foundDockerfile {
295-
return fmt.Errorf("the specified dockerfile could not be read: %q", dockerfile)
296-
}
297-
298391
return nil
299392
}
300393

301-
func createTarball(ctx context.Context, root, dockerfile string) (*bytes.Buffer, error) {
394+
func createArchive(ctx context.Context, root string, dockerfile string, contentType ArchiveType) (*bytes.Buffer, error) {
302395
fileCount := 0
303396
// TODO: use io.Pipe and do proper streaming (instead of buffering everything in memory)
397+
304398
buf := &bytes.Buffer{}
305-
gzipWriter := &contextAwareWriter{ctx, gzip.NewWriter(buf)}
306-
tarWriter := tar.NewWriter(gzipWriter)
399+
var factory WriterFactory
400+
if contentType == ArchiveTypeZip {
401+
zipWriter := zip.NewWriter(buf)
402+
factory = &zipFactory{zipWriter}
403+
} else {
404+
gzipWriter := gzip.NewWriter(buf)
405+
tarWriter := tar.NewWriter(gzipWriter)
406+
factory = &tarFactory{tarWriter, gzipWriter}
407+
}
307408

308409
doProgress := term.StdoutCanColor() && term.IsTerminal()
309410
err := WalkContextFolder(root, dockerfile, func(path string, de os.DirEntry, slashPath string) error {
@@ -319,38 +420,27 @@ func createTarball(ctx context.Context, root, dockerfile string) (*bytes.Buffer,
319420
return err
320421
}
321422

322-
header, err := tar.FileInfoHeader(info, info.Name())
323-
if err != nil {
324-
return err
325-
}
326-
327-
// Make reproducible; WalkDir walks files in lexical order.
328-
header.ModTime = time.Unix(sourceDateEpoch, 0)
329-
header.Gid = 0
330-
header.Uid = 0
331-
header.Name = slashPath
332-
err = tarWriter.WriteHeader(header)
333-
if err != nil {
423+
writer, err := factory.CreateHeader(info, slashPath)
424+
if err != nil || writer == nil {
334425
return err
335426
}
336427

337-
if !info.Mode().IsRegular() {
338-
return nil
339-
}
340-
341428
file, err := os.Open(path)
342429
if err != nil {
343430
return err
344431
}
345432
defer file.Close()
346433

434+
// Wrap the file reader with context-aware reader
435+
contextReader := &contextAwareReader{ctx, file}
436+
347437
fileCount++
348438
if fileCount == ContextFileLimit+1 {
349439
term.Warnf("the build context contains more than %d files; use --debug or create .dockerignore to exclude caches and build artifacts", ContextFileLimit)
350440
}
351441

352442
bufLen := buf.Len()
353-
_, err = io.Copy(tarWriter, file)
443+
_, err = io.Copy(writer, contextReader)
354444
if int64(buf.Len()) > ContextSizeHardLimit {
355445
return fmt.Errorf("the build context is limited to %s; consider downloading large files in the Dockerfile or set the DEFANG_BUILD_CONTEXT_LIMIT environment variable", units.BytesSize(float64(ContextSizeHardLimit)))
356446
}
@@ -364,12 +454,8 @@ func createTarball(ctx context.Context, root, dockerfile string) (*bytes.Buffer,
364454
return nil, err
365455
}
366456

367-
// Close the tar and gzip writers before returning the buffer
368-
if err = tarWriter.Close(); err != nil {
369-
return nil, err
370-
}
371-
372-
if err = gzipWriter.Close(); err != nil {
457+
err = factory.Close() // Close the tar or zip writer
458+
if err != nil {
373459
return nil, err
374460
}
375461

src/pkg/cli/compose/context_test.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func Test_parseContextLimit(t *testing.T) {
3838
})
3939
}
4040

41-
func TestUploadTarball(t *testing.T) {
41+
func TestUploadArchive(t *testing.T) {
4242
const path = "/upload/x/"
4343
const digest = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
4444

@@ -49,28 +49,39 @@ func TestUploadTarball(t *testing.T) {
4949
if !strings.HasPrefix(r.URL.Path, path) {
5050
t.Errorf("Expected prefix %v, got %v", path, r.URL.Path)
5151
}
52-
if r.Header.Get("Content-Type") != "application/gzip" {
53-
t.Errorf("Expected Content-Type: application/gzip, got %v", r.Header.Get("Content-Type"))
52+
if !(r.Header.Get("Content-Type") == string(ArchiveTypeGzip) || r.Header.Get("Content-Type") == string(ArchiveTypeZip)) {
53+
t.Errorf("Expected Content-Type: application/gzip or application/zip, got %v", r.Header.Get("Content-Type"))
5454
}
5555
w.WriteHeader(200)
5656
}))
5757
defer server.Close()
5858

5959
t.Run("upload with digest", func(t *testing.T) {
60-
url, err := uploadTarball(context.Background(), client.MockProvider{UploadUrl: server.URL + path}, "testproj", &bytes.Buffer{}, digest)
60+
url, err := uploadArchive(context.Background(), client.MockProvider{UploadUrl: server.URL + path}, "testproj", &bytes.Buffer{}, ArchiveTypeGzip, digest)
6161
if err != nil {
62-
t.Fatalf("uploadTarball() failed: %v", err)
62+
t.Fatalf("uploadArchive() failed: %v", err)
6363
}
6464
const expectedPath = path + digest
6565
if url != server.URL+expectedPath {
6666
t.Errorf("Expected %v, got %v", server.URL+expectedPath, url)
6767
}
68+
69+
t.Run("upload with zip", func(t *testing.T) {
70+
url, err := uploadArchive(context.Background(), client.MockProvider{UploadUrl: server.URL + path}, "testproj", &bytes.Buffer{}, ArchiveTypeZip, "")
71+
if err != nil {
72+
t.Fatalf("uploadContent() failed: %v", err)
73+
}
74+
if url != server.URL+path {
75+
t.Errorf("Expected %v, got %v", server.URL+path, url)
76+
}
77+
})
78+
6879
})
6980

7081
t.Run("force upload without digest", func(t *testing.T) {
71-
url, err := uploadTarball(context.Background(), client.MockProvider{UploadUrl: server.URL + path}, "testproj", &bytes.Buffer{}, "")
82+
url, err := uploadArchive(context.Background(), client.MockProvider{UploadUrl: server.URL + path}, "testproj", &bytes.Buffer{}, ArchiveTypeGzip, "")
7283
if err != nil {
73-
t.Fatalf("uploadTarball() failed: %v", err)
84+
t.Fatalf("uploadArchive() failed: %v", err)
7485
}
7586
if url != server.URL+path {
7687
t.Errorf("Expected %v, got %v", server.URL+path, url)
@@ -134,7 +145,7 @@ func TestWalkContextFolder(t *testing.T) {
134145

135146
func TestCreateTarballReader(t *testing.T) {
136147
t.Run("Default Dockerfile", func(t *testing.T) {
137-
buffer, err := createTarball(context.Background(), "../../../testdata/testproj", "")
148+
buffer, err := createArchive(context.Background(), "../../../testdata/testproj", "", ArchiveTypeGzip)
138149
if err != nil {
139150
t.Fatalf("createTarballReader() failed: %v", err)
140151
}
@@ -171,14 +182,14 @@ func TestCreateTarballReader(t *testing.T) {
171182
})
172183

173184
t.Run("Missing Dockerfile", func(t *testing.T) {
174-
_, err := createTarball(context.Background(), "../../testdata", "Dockerfile.missing")
185+
_, err := createArchive(context.Background(), "../../testdata", "Dockerfile.missing", ArchiveTypeGzip)
175186
if err == nil {
176187
t.Fatal("createTarballReader() should have failed")
177188
}
178189
})
179190

180191
t.Run("Missing Context", func(t *testing.T) {
181-
_, err := createTarball(context.Background(), "asdfqwer", "")
192+
_, err := createArchive(context.Background(), "asdfqwer", "", ArchiveTypeGzip)
182193
if err == nil {
183194
t.Fatal("createTarballReader() should have failed")
184195
}

0 commit comments

Comments
 (0)