Skip to content
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

Proposal: Download Binary File and Download License via URI #844

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions internal/download/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
func download(url string, verifier Verifier, fetcher Fetcher) (io.ReaderAt, int64, error) {
body, err := fetcher.Get(url)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to obtain plugin archive")
return nil, 0, errors.Wrapf(err, "failed to obtain plugin")
}
defer body.Close()

Expand Down Expand Up @@ -161,6 +161,25 @@ func extractTARGZ(targetDir string, at io.ReaderAt, size int64) error {
return nil
}

// Downloads the given binary to the target directory
func downloadBinary(targetDir string, read io.ReaderAt, size int64) error {
klog.V(4).Infof("Downloading binary to %q", targetDir)

in := io.NewSectionReader(read, 0, size)
buf := make([]byte, size)

if _, err := in.Read(buf); err != nil {
return errors.Wrap(err, "failed to read binary")
}

if err := os.WriteFile(filepath.Join(targetDir, "binary"), buf, 0o755); err != nil {
return errors.Wrap(err, "failed to write binary")
}

klog.V(4).Infof("download of binary complete to %s complete", targetDir)
return nil
}

func suspiciousPath(path string) error {
if strings.Contains(path, "..") {
return errors.Errorf("refusing to unpack archive with suspicious entry %q", path)
Expand Down Expand Up @@ -191,8 +210,9 @@ func detectMIMEType(at io.ReaderAt) (string, error) {
type extractor func(targetDir string, read io.ReaderAt, size int64) error

var defaultExtractors = map[string]extractor{
"application/zip": extractZIP,
"application/x-gzip": extractTARGZ,
"application/zip": extractZIP,
"application/x-gzip": extractTARGZ,
"application/octet-stream": downloadBinary,
}

func extractArchive(dst string, at io.ReaderAt, size int64) error {
Expand Down
52 changes: 52 additions & 0 deletions internal/download/downloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,55 @@ func zipArchiveReaderForTesting(files map[string]string) (*bytes.Reader, error)
}
return bytes.NewReader(archiveBuffer.Bytes()), nil
}

func Test_downloadBinary(t *testing.T) {
type args struct {
targetDir string
read string
size string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "test fail read of binary",
args: args{
targetDir: filepath.Join(testdataPath(), "null-file"),
read: "",
size: "",
},
wantErr: true,
},
{
name: "test fail write of binary",
args: args{
targetDir: filepath.Join(testdataPath(), "test", "foo"),
read: "",
size: "",
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fd, err := os.Open(tt.args.targetDir)
if err != nil {
t.Errorf("failed to read file %s, err: %v", tt.args.targetDir, err)
return
}

st, err := fd.Stat()
if err != nil {
t.Errorf("failed to stat file %s, err: %v", tt.args.targetDir, err)
return
}

if err := downloadBinary(tt.args.targetDir, fd, st.Size()); (err != nil) != tt.wantErr {
t.Errorf("downloadBinary() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
1 change: 1 addition & 0 deletions internal/download/testdata/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a test LICENSE file
Binary file added internal/download/testdata/binary
Binary file not shown.
40 changes: 39 additions & 1 deletion internal/installation/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package installation

import (
"io"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -108,6 +109,16 @@ func install(op installOperation, opts InstallOpts) error {
return errors.Wrap(err, "failed to unpack into staging dir")
}

// Download License file from given URI and rename downloaded binary to plugin name
if op.platform.License != "" {
if err := downloadLicenseFile(downloadStagingDir, op.platform.License); err != nil {
return errors.Wrap(err, "failed downloading license file to installation directory")
}
if err := renameBinary(downloadStagingDir, op.platform.Bin); err != nil {
return errors.Wrap(err, "failed renaming binary in installation directory")
}
}

applyDefaults(&op.platform)
if err := moveToInstallDir(downloadStagingDir, op.installDir, op.platform.Files); err != nil {
return errors.Wrap(err, "failed while moving files to the installation directory")
Expand Down Expand Up @@ -137,7 +148,7 @@ func applyDefaults(platform *index.Platform) {
}

// downloadAndExtract downloads the specified archive uri (or uses the provided overrideFile, if a non-empty value)
// while validating its checksum with the provided sha256sum, and extracts its contents to extractDir that must be.
// while validating its checksum with the provided sha256sum, and extracts its contents to extractDir that must be
// created.
func downloadAndExtract(extractDir, uri, sha256sum, overrideFile string) error {
var fetcher download.Fetcher = download.HTTPFetcher{}
Expand All @@ -150,6 +161,33 @@ func downloadAndExtract(extractDir, uri, sha256sum, overrideFile string) error {
return errors.Wrap(err, "failed to unpack the plugin archive")
}

// downloadLicenseFile will download the license file from the given URI and save it to the installation directory
// of the plugin.
func downloadLicenseFile(extractDir, uri string) error {
var fetcher download.Fetcher = download.HTTPFetcher{}

body, err := fetcher.Get(uri)
if err != nil {
return errors.Wrap(err, "failed to download the license file")
}
defer body.Close()

out, err := os.Create(filepath.Join(extractDir, "LICENSE"))
if err != nil {
return errors.Wrap(err, "failed to create LICENSE file")
}
defer out.Close()

_, err = io.Copy(out, body)

return err
}

// renames the binary in the installation directory to the plugin name if the binary is given temporary "binary" name
func renameBinary(extractDir, plugin string) error {
return os.Rename(filepath.Join(extractDir, "binary"), filepath.Join(extractDir, plugin))
}

// Uninstall will uninstall a plugin.
func Uninstall(p environment.Paths, name string) error {
if name == constants.KrewPluginName {
Expand Down
70 changes: 70 additions & 0 deletions internal/installation/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,73 @@ func TestCleanupStaleKrewInstallations(t *testing.T) {
t.Fatal(diff)
}
}

func Test_downloadLicenseFile(t *testing.T) {
tmpDir := testutil.NewTempDir(t)

// start a local http server to serve the test archive from pkg/download/testdata
testdataDir := filepath.Join(testdataPath(t), "..", "..", "download", "testdata")
server := httptest.NewServer(http.FileServer(http.Dir(testdataDir)))
defer server.Close()

url := server.URL + "/LICENSE"

tests := []struct {
name string
extractDir string
uri string
wantErr bool
}{
{
name: "with valid uri",
extractDir: tmpDir.Root(),
uri: url,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := downloadLicenseFile(tt.extractDir, tt.uri); err != nil {
t.Error(err)
}
})
}
}

func Test_renameBinary(t *testing.T) {
tmpDir := testutil.NewTempDir(t)
testFile := filepath.Join(testdataPath(t), "..", "..", "download", "testdata")
tests := []struct {
name string
extractDir string
plugin string
wantErr bool
}{
{
name: "with valid plugin name",
extractDir: testFile,
plugin: "testerBin",
wantErr: false,
},
{
name: "without valid plugin name",
extractDir: tmpDir.Root(),
plugin: "not valid",
wantErr: true,
},
{
name: "without binary",
extractDir: tmpDir.Root(),
plugin: "valid",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := renameBinary(tt.extractDir, tt.plugin); (err != nil) != tt.wantErr {
t.Error(err)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/index/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Platform struct {
Selector *metav1.LabelSelector `json:"selector,omitempty"`
Files []FileOperation `json:"files"`

License string `json:"license,omitempty"`

// Bin specifies the path to the plugin executable.
// The path is relative to the root of the installation folder.
// The binary will be linked after all FileOperations are executed.
Expand Down
4 changes: 2 additions & 2 deletions site/content/docs/developer-guide/distributing-with-krew.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ without having to deal with their package managers.
Once you [develop]({{< ref "develop/plugin-development.md" >}}) a `kubectl`
plugin, follow these steps to distribute your plugin on Krew:

1. Package your plugin into an archive file (`.tar.gz` or `.zip`).
1. Make the archive file publicly available (e.g. as GitHub release files).
1. Package your plugin into an archive file or binary (`.bin`, `.tar.gz` or `.zip`).
1. Make them publicly available (e.g. as GitHub release files).
1. Write a [Krew plugin manifest]({{< ref "plugin-manifest.md" >}}) file.
1. [Submit your plugin to krew-index]({{< ref "release/../release/submitting-to-krew.md" >}}).
4 changes: 2 additions & 2 deletions site/content/docs/developer-guide/installing-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ weight: 300
---

After you have written your [plugin manifest]({{< ref "plugin-manifest.md" >}})
and archived your plugin into a `.zip` or `.tar.gz` file, you can verify that
and archived your plugin into a `.zip` or `.tar.gz` or a raw binary file, you can verify that
your plugin installs correctly with Krew by running:

```sh
Expand All @@ -15,7 +15,7 @@ your plugin installs correctly with Krew by running:
- The `--manifest` flag specifies a custom manifest rather than using
the default [krew index][index]
- `--archive` overrides the download `uri:` specified in the plugin manifest and
uses a local `.zip` or `.tar.gz` file instead.
uses a local `.bin`, `.zip` or `.tar.gz` file instead.

If the installation **fails**, run the command again with `-v=4` flag to see the
verbose logs and examine what went wrong.
Expand Down
14 changes: 12 additions & 2 deletions site/content/docs/developer-guide/plugin-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ spec:

## Specifying plugin download options

Krew plugins must be packaged as `.zip` or `.tar.gz` archives, and should be
Krew plugins must be packaged as `.zip` or `.tar.gz` archives, or as a binary, and should be
accessible to download from a user’s machine. The relevant fields are:

- `uri`: URL to the archive file (`.zip` or `.tar.gz`)
- `uri`: URL to the archive file (`.zip`, `.bin` or `.tar.gz`)
- `sha256`: sha256 sum of the archive file

```yaml
Expand All @@ -94,6 +94,16 @@ accessible to download from a user’s machine. The relevant fields are:
...
```

If you are specifying a binary, you will have the option to also specify a license that krew
will install with your binary

```yaml
platforms:
- uri: https://github.com/foo/bar/archive/v1.2.3.zip
sha256: "29C9C411AF879AB85049344B81B8E8A9FBC1D657D493694E2783A2D0DB240775"
license: https://raw.githubusercontent.com/foo/bar/v1.0.1/LICENSE
```

## Specifying platform-specific instructions

Krew makes it possible to install the same plugin on different operating systems
Expand Down
Loading