-
Notifications
You must be signed in to change notification settings - Fork 210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bugtool: Compress and copy with resume #671
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2020 Authors of Cilium | ||
|
||
package k8s | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"os" | ||
) | ||
|
||
const ( | ||
defaultReadFromByteCmd = "tail -c+%d %s" | ||
defaultMaxTries = 5 | ||
) | ||
|
||
// CopyFromPod is to copy srcFile in a given pod to local destFile with defaultMaxTries. | ||
func (c *Client) CopyFromPod(ctx context.Context, namespace, pod, container string, srcFile, destFile string) error { | ||
pipe := newPipe(&CopyOptions{ | ||
MaxTries: defaultMaxTries, | ||
ReadFunc: readFromPod(ctx, c, namespace, pod, container, srcFile), | ||
}) | ||
|
||
outFile, err := os.Create(destFile) | ||
if err != nil { | ||
return err | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is missing a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you are right, I forgot about this most of the time, and relying on linters. If I am not wrong, file is having finalizer i.e. fd will be garbage collected later, not idea in case of reuse of file descriptor. Let me make a small update on this, thanks again. |
||
|
||
if _, err = io.Copy(outFile, pipe); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func readFromPod(ctx context.Context, client *Client, namespace, pod, container, srcFile string) ReadFunc { | ||
return func(offset uint64, writer io.Writer) error { | ||
command := []string{"sh", "-c", fmt.Sprintf(defaultReadFromByteCmd, offset, srcFile)} | ||
return client.execInPodWithWriters(ctx, ExecParameters{ | ||
Namespace: namespace, | ||
Pod: pod, | ||
Container: container, | ||
Command: command, | ||
}, writer, writer) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2020 Authors of Cilium | ||
|
||
package k8s | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// CopyOptions have the data required to perform the copy operation | ||
type CopyOptions struct { | ||
// Maximum number of retries, -1 for unlimited retries. | ||
MaxTries int | ||
|
||
// ReaderFunc is the actual implementation for reading file content | ||
ReadFunc ReadFunc | ||
} | ||
|
||
// ReadFunc function is to support reading content from given offset till EOF. | ||
// The content will be written to io.Writer. | ||
type ReadFunc func(offset uint64, writer io.Writer) error | ||
|
||
// CopyPipe struct is simple implementation to support copy files with retry. | ||
type CopyPipe struct { | ||
Options *CopyOptions | ||
|
||
Reader *io.PipeReader | ||
Writer *io.PipeWriter | ||
|
||
bytesRead uint64 | ||
retries int | ||
} | ||
|
||
func newPipe(option *CopyOptions) *CopyPipe { | ||
p := new(CopyPipe) | ||
p.Options = option | ||
p.startReadFrom(0) | ||
return p | ||
} | ||
|
||
func (t *CopyPipe) startReadFrom(offset uint64) { | ||
t.Reader, t.Writer = io.Pipe() | ||
go func() { | ||
var err error | ||
defer func() { | ||
// close with error here to make sure any read operation with Pipe Reader will have return the same error | ||
// otherwise, by default, EOF will be returned. | ||
_ = t.Writer.CloseWithError(err) | ||
}() | ||
err = t.Options.ReadFunc(offset, t.Writer) | ||
}() | ||
} | ||
|
||
// Read function is to satisfy io.Reader interface. | ||
// This is simple implementation to support resuming copy in case of there is any temporary issue (e.g. networking) | ||
func (t *CopyPipe) Read(p []byte) (int, error) { | ||
n, err := t.Reader.Read(p) | ||
if err != nil { | ||
// If EOF error happens, just bubble it up, no retry is required. | ||
if errors.Is(err, io.EOF) { | ||
return n, err | ||
} | ||
|
||
// Check if the number of retries is already exhausted | ||
if t.Options.MaxTries >= 0 && t.retries >= t.Options.MaxTries { | ||
return n, fmt.Errorf("dropping out copy after %d retries: %w", t.retries, err) | ||
} | ||
|
||
// Perform retry | ||
if t.bytesRead == 0 { | ||
t.startReadFrom(t.bytesRead) | ||
} else { | ||
t.startReadFrom(t.bytesRead + 1) | ||
} | ||
t.retries++ | ||
return 0, nil | ||
} | ||
t.bytesRead += uint64(n) | ||
return n, err | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package k8s | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"testing" | ||
|
||
"gopkg.in/check.v1" | ||
) | ||
|
||
func Test(t *testing.T) { | ||
check.TestingT(t) | ||
} | ||
|
||
type CopyPipeSuites struct{} | ||
|
||
var _ = check.Suite(&CopyPipeSuites{}) | ||
|
||
type remoteFile struct { | ||
bytes []byte | ||
|
||
maxFailures int | ||
count int | ||
} | ||
|
||
func (r *remoteFile) Read(offset uint64, writer io.Writer) error { | ||
if int(offset) > len(r.bytes) { | ||
return io.EOF | ||
} | ||
_, err := writer.Write(r.bytes[offset:]) | ||
return err | ||
} | ||
|
||
func (r *remoteFile) ReadWithFailure(offset uint64, writer io.Writer) error { | ||
if int(offset) > len(r.bytes) { | ||
return io.EOF | ||
} | ||
if r.count < r.maxFailures { | ||
r.count++ | ||
return io.ErrUnexpectedEOF | ||
} | ||
|
||
_, err := writer.Write(r.bytes[offset:]) | ||
return err | ||
|
||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithoutRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.Read, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.IsNil) | ||
c.Assert(res.Bytes(), check.DeepEquals, remoteFile.bytes) | ||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
maxFailures: 2, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.ReadWithFailure, | ||
MaxTries: 3, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.IsNil) | ||
c.Assert(res.Bytes(), check.DeepEquals, remoteFile.bytes) | ||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithExhaustedRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
maxFailures: 3, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.ReadWithFailure, | ||
MaxTries: 2, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.NotNil) | ||
c.Assert(err, check.ErrorMatches, "dropping out copy after 2 retries: unexpected EOF") | ||
c.Assert(res.Bytes(), check.HasLen, 0) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The copyright year should be 2021. This also applies to other files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my bad :(