Skip to content
This repository was archived by the owner on Oct 13, 2023. It is now read-only.

Commit ef3c7dc

Browse files
goksuthaJeztah
authored andcommitted
Introduce .zip import for docker context
Adds capabilities to import a .zip file with importZip. Detects the content type of source by checking bytes & DetectContentType. Adds LimitedReader reader, a fork of io.LimitedReader, was needed for better error messaging instead of just getting back EOF. We are using limited reader to avoid very big files causing memory issues. Adds a new file size limit for context imports, this limit is used for the main file for .zip & .tar and individual compressed files for .zip. Added TestImportZip that will check the import content type Then will assert no err on Importing .zip file Signed-off-by: Goksu Toprak <goksu.toprak@docker.com> (cherry picked from commit 291e86289be4ab78840154933b6e744dd6a5fc8e) Signed-off-by: Sebastiaan van Stijn <github@gone.nl> Upstream-commit: 90f256aeab1be23193d5323deb56cf8dd04e1951 Component: cli
1 parent e3f2d77 commit ef3c7dc

5 files changed

Lines changed: 252 additions & 17 deletions

File tree

components/cli/cli/command/context/import.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func newImportCommand(dockerCli command.Cli) *cobra.Command {
1515
cmd := &cobra.Command{
1616
Use: "import CONTEXT FILE|-",
17-
Short: "Import a context from a tar file",
17+
Short: "Import a context from a tar or zip file",
1818
Args: cli.ExactArgs(2),
1919
RunE: func(cmd *cobra.Command, args []string) error {
2020
return RunImport(dockerCli, args[0], args[1])
@@ -28,6 +28,7 @@ func RunImport(dockerCli command.Cli, name string, source string) error {
2828
if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil {
2929
return err
3030
}
31+
3132
var reader io.Reader
3233
if source == "-" {
3334
reader = dockerCli.In()
@@ -43,6 +44,7 @@ func RunImport(dockerCli command.Cli, name string, source string) error {
4344
if err := store.Import(name, dockerCli.ContextStore(), reader); err != nil {
4445
return err
4546
}
47+
4648
fmt.Fprintln(dockerCli.Out(), name)
4749
fmt.Fprintf(dockerCli.Err(), "Successfully imported context %q\n", name)
4850
return nil
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package store
2+
3+
import (
4+
"errors"
5+
"io"
6+
)
7+
8+
// LimitedReader is a fork of io.LimitedReader to override Read.
9+
type LimitedReader struct {
10+
R io.Reader
11+
N int64 // max bytes remaining
12+
}
13+
14+
// Read is a fork of io.LimitedReader.Read that returns an error when limit exceeded.
15+
func (l *LimitedReader) Read(p []byte) (n int, err error) {
16+
if l.N < 0 {
17+
return 0, errors.New("read exceeds the defined limit")
18+
}
19+
if l.N == 0 {
20+
return 0, io.EOF
21+
}
22+
// have to cap N + 1 otherwise we won't hit limit err
23+
if int64(len(p)) > l.N+1 {
24+
p = p[0 : l.N+1]
25+
}
26+
n, err = l.R.Read(p)
27+
l.N -= int64(n)
28+
return n, err
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package store
2+
3+
import (
4+
"io/ioutil"
5+
"strings"
6+
"testing"
7+
8+
"gotest.tools/assert"
9+
)
10+
11+
func TestLimitReaderReadAll(t *testing.T) {
12+
r := strings.NewReader("Reader")
13+
14+
_, err := ioutil.ReadAll(r)
15+
assert.NilError(t, err)
16+
17+
r = strings.NewReader("Test")
18+
_, err = ioutil.ReadAll(&LimitedReader{R: r, N: 4})
19+
assert.NilError(t, err)
20+
21+
r = strings.NewReader("Test")
22+
_, err = ioutil.ReadAll(&LimitedReader{R: r, N: 2})
23+
assert.Error(t, err, "read exceeds the defined limit")
24+
}

components/cli/cli/context/store/store.go

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package store
22

33
import (
44
"archive/tar"
5+
"archive/zip"
6+
"bufio"
7+
"bytes"
58
_ "crypto/sha256" // ensure ids can be computed
69
"encoding/json"
710
"errors"
811
"fmt"
912
"io"
1013
"io/ioutil"
14+
"net/http"
1115
"path"
1216
"path/filepath"
1317
"strings"
@@ -259,12 +263,44 @@ func Export(name string, s Reader) io.ReadCloser {
259263
return reader
260264
}
261265

266+
const (
267+
maxAllowedFileSizeToImport int64 = 10 << 20
268+
zipType string = "application/zip"
269+
)
270+
271+
func getImportContentType(r *bufio.Reader) (string, error) {
272+
head, err := r.Peek(512)
273+
if err != nil && err != io.EOF {
274+
return "", err
275+
}
276+
277+
return http.DetectContentType(head), nil
278+
}
279+
262280
// Import imports an exported context into a store
263281
func Import(name string, s Writer, reader io.Reader) error {
264-
tr := tar.NewReader(reader)
282+
// Buffered reader will not advance the buffer, needed to determine content type
283+
r := bufio.NewReader(reader)
284+
285+
importContentType, err := getImportContentType(r)
286+
if err != nil {
287+
return err
288+
}
289+
switch importContentType {
290+
case zipType:
291+
return importZip(name, s, r)
292+
default:
293+
// Assume it's a TAR (TAR does not have a "magic number")
294+
return importTar(name, s, r)
295+
}
296+
}
297+
298+
func importTar(name string, s Writer, reader io.Reader) error {
299+
tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
265300
tlsData := ContextTLSData{
266301
Endpoints: map[string]EndpointTLSData{},
267302
}
303+
268304
for {
269305
hdr, err := tr.Next()
270306
if err == io.EOF {
@@ -282,37 +318,112 @@ func Import(name string, s Writer, reader io.Reader) error {
282318
if err != nil {
283319
return err
284320
}
285-
var meta Metadata
286-
if err := json.Unmarshal(data, &meta); err != nil {
321+
meta, err := parseMetadata(data, name)
322+
if err != nil {
287323
return err
288324
}
289-
meta.Name = name
290325
if err := s.CreateOrUpdate(meta); err != nil {
291326
return err
292327
}
293328
} else if strings.HasPrefix(hdr.Name, "tls/") {
294-
relative := strings.TrimPrefix(hdr.Name, "tls/")
295-
parts := strings.SplitN(relative, "/", 2)
296-
if len(parts) != 2 {
297-
return errors.New("archive format is invalid")
298-
}
299-
endpointName := parts[0]
300-
fileName := parts[1]
301329
data, err := ioutil.ReadAll(tr)
302330
if err != nil {
303331
return err
304332
}
305-
if _, ok := tlsData.Endpoints[endpointName]; !ok {
306-
tlsData.Endpoints[endpointName] = EndpointTLSData{
307-
Files: map[string][]byte{},
308-
}
333+
if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil {
334+
return err
335+
}
336+
}
337+
}
338+
339+
return s.ResetTLSMaterial(name, &tlsData)
340+
}
341+
342+
func importZip(name string, s Writer, reader io.Reader) error {
343+
body, err := ioutil.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
344+
if err != nil {
345+
return err
346+
}
347+
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
348+
if err != nil {
349+
return err
350+
}
351+
tlsData := ContextTLSData{
352+
Endpoints: map[string]EndpointTLSData{},
353+
}
354+
355+
for _, zf := range zr.File {
356+
fi := zf.FileInfo()
357+
if fi.IsDir() {
358+
// skip this entry, only taking files into account
359+
continue
360+
}
361+
if zf.Name == metaFile {
362+
f, err := zf.Open()
363+
if err != nil {
364+
return err
365+
}
366+
367+
data, err := ioutil.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
368+
defer f.Close()
369+
if err != nil {
370+
return err
371+
}
372+
meta, err := parseMetadata(data, name)
373+
if err != nil {
374+
return err
375+
}
376+
if err := s.CreateOrUpdate(meta); err != nil {
377+
return err
378+
}
379+
} else if strings.HasPrefix(zf.Name, "tls/") {
380+
f, err := zf.Open()
381+
if err != nil {
382+
return err
383+
}
384+
data, err := ioutil.ReadAll(f)
385+
defer f.Close()
386+
if err != nil {
387+
return err
388+
}
389+
err = importEndpointTLS(&tlsData, zf.Name, data)
390+
if err != nil {
391+
return err
309392
}
310-
tlsData.Endpoints[endpointName].Files[fileName] = data
311393
}
312394
}
395+
313396
return s.ResetTLSMaterial(name, &tlsData)
314397
}
315398

399+
func parseMetadata(data []byte, name string) (Metadata, error) {
400+
var meta Metadata
401+
if err := json.Unmarshal(data, &meta); err != nil {
402+
return meta, err
403+
}
404+
meta.Name = name
405+
return meta, nil
406+
}
407+
408+
func importEndpointTLS(tlsData *ContextTLSData, path string, data []byte) error {
409+
parts := strings.SplitN(strings.TrimPrefix(path, "tls/"), "/", 2)
410+
if len(parts) != 2 {
411+
// TLS endpoints require archived file directory with 2 layers
412+
// i.e. tls/{endpointName}/{fileName}
413+
return errors.New("archive format is invalid")
414+
}
415+
416+
epName := parts[0]
417+
fileName := parts[1]
418+
if _, ok := tlsData.Endpoints[epName]; !ok {
419+
tlsData.Endpoints[epName] = EndpointTLSData{
420+
Files: map[string][]byte{},
421+
}
422+
}
423+
tlsData.Endpoints[epName].Files[fileName] = data
424+
return nil
425+
}
426+
316427
type setContextName interface {
317428
setContext(name string)
318429
}

components/cli/cli/context/store/store_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package store
22

33
import (
4+
"archive/zip"
5+
"bufio"
6+
"bytes"
47
"crypto/rand"
8+
"encoding/json"
9+
"io"
510
"io/ioutil"
611
"os"
12+
"path"
713
"testing"
814

915
"gotest.tools/assert"
@@ -125,3 +131,66 @@ func TestErrHasCorrectContext(t *testing.T) {
125131
assert.ErrorContains(t, err, "no-exists")
126132
assert.Check(t, IsErrContextDoesNotExist(err))
127133
}
134+
135+
func TestDetectImportContentType(t *testing.T) {
136+
testDir, err := ioutil.TempDir("", t.Name())
137+
assert.NilError(t, err)
138+
defer os.RemoveAll(testDir)
139+
140+
buf := new(bytes.Buffer)
141+
r := bufio.NewReader(buf)
142+
ct, err := getImportContentType(r)
143+
assert.NilError(t, err)
144+
assert.Assert(t, zipType != ct)
145+
}
146+
147+
func TestImportZip(t *testing.T) {
148+
testDir, err := ioutil.TempDir("", t.Name())
149+
assert.NilError(t, err)
150+
defer os.RemoveAll(testDir)
151+
152+
zf := path.Join(testDir, "test.zip")
153+
154+
f, err := os.Create(zf)
155+
defer f.Close()
156+
assert.NilError(t, err)
157+
w := zip.NewWriter(f)
158+
159+
meta, err := json.Marshal(Metadata{
160+
Endpoints: map[string]interface{}{
161+
"ep1": endpoint{Foo: "bar"},
162+
},
163+
Metadata: context{Bar: "baz"},
164+
Name: "source",
165+
})
166+
assert.NilError(t, err)
167+
var files = []struct {
168+
Name, Body string
169+
}{
170+
{"meta.json", string(meta)},
171+
{path.Join("tls", "docker", "ca.pem"), string([]byte("ca.pem"))},
172+
}
173+
174+
for _, file := range files {
175+
f, err := w.Create(file.Name)
176+
assert.NilError(t, err)
177+
_, err = f.Write([]byte(file.Body))
178+
assert.NilError(t, err)
179+
}
180+
181+
err = w.Close()
182+
assert.NilError(t, err)
183+
184+
source, err := os.Open(zf)
185+
assert.NilError(t, err)
186+
ct, err := getImportContentType(bufio.NewReader(source))
187+
assert.NilError(t, err)
188+
assert.Equal(t, zipType, ct)
189+
190+
source, _ = os.Open(zf)
191+
defer source.Close()
192+
var r io.Reader = source
193+
s := New(testDir, testCfg)
194+
err = Import("zipTest", s, r)
195+
assert.NilError(t, err)
196+
}

0 commit comments

Comments
 (0)