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

Supporting Private GitHub repos and other artifact stores #816

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
62 changes: 62 additions & 0 deletions internal/download/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"io"
"net/http"
"os"
"os/exec"
"strings"

"github.com/pkg/errors"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -56,3 +58,63 @@ func (f fileFetcher) Get(_ string) (io.ReadCloser, error) {

// NewFileFetcher returns a local file reader.
func NewFileFetcher(path string) Fetcher { return fileFetcher{f: path} }

var _ Fetcher = CommandFetcher{}

// CommandFetcher is used to run a command to receive a file as stdout
type CommandFetcher struct{}

func (CommandFetcher) Get(cmd string) (io.ReadCloser, error) {
var stream io.ReadCloser

// Create tempFile for loading the plugin artifact
tempFile, err := os.CreateTemp("", "")
if err != nil {
return stream, err
}
defer klog.V(2).Infof("Removed temp file at %q", tempFile.Name())
defer os.Remove(tempFile.Name())

klog.V(2).Infof("Created temp file for command output at %q", tempFile.Name())

// Intentionally not closing the stream object in this function!
// The open file to tempFile will remain readable until the last process
// releases it. Another function is responsible for closing the
// io.ReaderCloser.
stream, err = os.Open(tempFile.Name())
if err != nil {
return stream, err
}

// HACK REMOVEME TESTING ONLY
// This is a workaround to accept command within the `uri` key of the krew
// plugin spec. An alternative solution, which is likely more optimal, is
// adding a new field to the spec for `downloadCommand` or similarly named
// key which would contain a command to run. This hack was put in place to
// test the rough implementation of loading a plugin from the stdout of a
// command.
cmd = strings.Replace(cmd, "cmd://", "", 1)

// TODO Improve splitting, this implementation has issues with newlines and
// qoutes in the cmd string.
c := strings.Split(cmd, " ")
runner := exec.Command(c[0], c[1:]...)

// NOTE Attempted to pass runner.Stdout() as an io.ReaderCloser but the io
// closes as soon as the application finishes running, which cannot extend
// reading beyond this function. Instead opted to push this into a tempFile
// on the local filesystem.
// Send stdout to tempFile for later ingestion
runner.Stdout = tempFile

klog.V(2).Infof("Running command %q", cmd)
if err := runner.Run(); err != nil {
// TODO It would be helpful to have more diagnostic information like the
// stdout and stderr of the command if Run() fails to exit 0. Right now the
// Command err just says things like `exited code 1` or similarly vague
// output. Could use klog.V(N)... to log output with verbosity.
return stream, errors.Wrapf(err, "failed to run command: %s", cmd)
}

return stream, err
}
29 changes: 28 additions & 1 deletion internal/installation/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func applyDefaults(platform *index.Platform) {
// 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{}
var fetcher download.Fetcher = uriFetcherSelector(uri)
if overrideFile != "" {
fetcher = download.NewFileFetcher(overrideFile)
}
Expand All @@ -150,6 +150,33 @@ func downloadAndExtract(extractDir, uri, sha256sum, overrideFile string) error {
return errors.Wrap(err, "failed to unpack the plugin archive")
}

// uriFetcherSelector returns a Fetcher based on uri scheme
// HACK Do not ship, strictly for testing. This was a dummy implementation to
// select the "Fetcher" interface based on the contents of `uri` spec key. An
// alternative approach would be to have another key like `downloadCommand` or
// even a more deep spec which could include things like environment vars, etc.
// Alternatively, if this is a desirable pattern, you could add real schemes
// like gs:// and s3:// which could invoke supported upstream cloud provider
// SDKs.
func uriFetcherSelector(uri string) (f download.Fetcher) {
switch {
case strings.HasPrefix(uri, "http://"):
f = download.HTTPFetcher{}
case strings.HasPrefix(uri, "https://"):
f = download.HTTPFetcher{}
case strings.HasPrefix(uri, "cmd://"):
f = download.CommandFetcher{}
case strings.HasPrefix(uri, "file://"):
// HACK This is just an example of natively supporting a local file
// artifact in the uri field.
f = download.NewFileFetcher(strings.Replace(uri, "file://", "", 1))
default:
// Alternatives could be to send a user error here
f = download.HTTPFetcher{}
}
return f
}

// Uninstall will uninstall a plugin.
func Uninstall(p environment.Paths, name string) error {
if name == constants.KrewPluginName {
Expand Down