diff --git a/plugin/commands.go b/plugin/commands.go index 88db86fb..74052712 100644 --- a/plugin/commands.go +++ b/plugin/commands.go @@ -1,23 +1,6 @@ package plugin import ( - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "path" - "path/filepath" - "regexp" - "runtime" - "strings" - - "net/url" - - "github.com/mackerelio/mkr/logger" - "github.com/mholt/archiver" - "github.com/pkg/errors" "gopkg.in/urfave/cli.v1" ) @@ -30,319 +13,6 @@ var CommandPlugin = cli.Command{ check plugin by "mkr plugin install". `, Subcommands: []cli.Command{ - { - Name: "install", - Usage: "Install a plugin from github or plugin registry", - ArgsUsage: "[--prefix ] [--overwrite] ", - Action: doPluginInstall, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "prefix", - Usage: "Plugin install location. The default is /opt/mackerel-agent/plugins", - }, - cli.BoolFlag{ - Name: "overwrite", - Usage: "Overwrite a plugin command in a plugin directory, even if same name command exists", - }, - }, - Description: ` - Install a mackerel plugin and a check plugin from github or plugin registry. - To install by mkr, a plugin has to be released to Github Releases in specification format. - - is: - - /[@] - Install from specified github owner, repository, and Github Releases tag. - If you omit , the installer install from latest Github Release. - Example: mkr plugin install mackerelio/mackerel-plugin-sample@v0.0.1 - - [@ explicitly. - If you specify , the installer doesn't use Github API, - so Github API Rate Limit error doesn't occur. -`, - }, + commandPluginInstall, }, } - -// main function for mkr plugin install -func doPluginInstall(c *cli.Context) error { - argInstallTarget := c.Args().First() - if argInstallTarget == "" { - return fmt.Errorf("Specify install target") - } - - it, err := newInstallTargetFromString(argInstallTarget) - if err != nil { - return errors.Wrap(err, "Failed to install plugin while parsing install target") - } - - pluginDir, err := setupPluginDir(c.String("prefix")) - if err != nil { - return errors.Wrap(err, "Failed to install plugin while setup plugin directory") - } - - // Create a work directory for downloading and extracting an artifact - workdir, err := ioutil.TempDir(filepath.Join(pluginDir, "work"), "mkr-plugin-installer-") - if err != nil { - return errors.Wrap(err, "Failed to install plugin while creating a work directory") - } - defer os.RemoveAll(workdir) - - // Download an artifact and install by it - downloadURL, err := it.makeDownloadURL() - if err != nil { - return errors.Wrap(err, "Failed to install plugin while making a download URL") - } - artifactFile, err := downloadPluginArtifact(downloadURL, workdir) - if err != nil { - return errors.Wrap(err, "Failed to install plugin while downloading an artifact") - } - err = installByArtifact(artifactFile, filepath.Join(pluginDir, "bin"), workdir, c.Bool("overwrite")) - if err != nil { - return errors.Wrap(err, "Failed to install plugin while extracting and placing") - } - - logger.Log("", fmt.Sprintf("Successfully installed %s", argInstallTarget)) - return nil -} - -// Create a directory for plugin install -func setupPluginDir(pluginDir string) (string, error) { - if pluginDir == "" { - pluginDir = "/opt/mackerel-agent/plugins" - } - err := os.MkdirAll(filepath.Join(pluginDir, "bin"), 0755) - if err != nil { - return "", err - } - err = os.MkdirAll(filepath.Join(pluginDir, "work"), 0755) - if err != nil { - return "", err - } - return pluginDir, nil -} - -// Download plugin artifact from `u`(URL) to `workdir`, -// and returns downloaded filepath -func downloadPluginArtifact(u, workdir string) (fpath string, err error) { - logger.Log("", fmt.Sprintf("Downloading %s", u)) - - // Create request to download - resp, err := (&client{}).get(u) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // fpath is filepath where artifact will be saved - fpath = filepath.Join(workdir, path.Base(u)) - - // download artifact - file, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return "", err - } - defer file.Close() - - _, err = io.Copy(file, resp.Body) - if err != nil { - return "", err - } - - return fpath, nil -} - -// Extract artifact and install plugin -func installByArtifact(artifactFile, bindir, workdir string, overwrite bool) error { - // unzip artifact to work directory - err := archiver.Zip.Open(artifactFile, workdir) - if err != nil { - return err - } - - // Look for plugin files recursively, and place those to binPath - return filepath.Walk(workdir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - - // a plugin file should be executable, and have specified name. - name := info.Name() - isExecutable := (info.Mode() & 0111) != 0 - if isExecutable && looksLikePlugin(name) { - return placePlugin(path, filepath.Join(bindir, name), overwrite) - } - - // `path` is a file but not plugin. - return nil - }) -} - -func looksLikePlugin(name string) bool { - return strings.HasPrefix(name, "check-") || strings.HasPrefix(name, "mackerel-plugin-") -} - -func placePlugin(src, dest string, overwrite bool) error { - _, err := os.Stat(dest) - if err == nil && !overwrite { - logger.Log("", fmt.Sprintf("%s already exists. Skip installing for now", dest)) - return nil - } - logger.Log("", fmt.Sprintf("Installing %s", dest)) - return os.Rename(src, dest) -} - -type installTarget struct { - owner string - repo string - pluginName string - releaseTag string - - // fields for testing - rawGithubURL string - apiGithubURL string -} - -const ( - defaultRawGithubURL = "https://raw.githubusercontent.com" - defaultAPIGithubURL = "https://api.github.com" -) - -// the pattern of installTarget string -// (?:|/)(?:@)? -var targetReg = regexp.MustCompile(`^(?:([^@/]+)/([^@/]+)|([^@/]+))(?:@(.+))?$`) - -// Parse install target string, and construct installTarget -// example is below -// - mackerelio/mackerel-plugin-sample -// - mackerel-plugin-sample -// - mackerelio/mackerel-plugin-sample@v0.0.1 -func newInstallTargetFromString(target string) (*installTarget, error) { - matches := targetReg.FindStringSubmatch(target) - if len(matches) != 5 { - return nil, fmt.Errorf("Install target is invalid: %s", target) - } - - it := &installTarget{ - owner: matches[1], - repo: matches[2], - pluginName: matches[3], - releaseTag: matches[4], - } - return it, nil -} - -// Make artifact's download URL -func (it *installTarget) makeDownloadURL() (string, error) { - owner, repo, err := it.getOwnerAndRepo() - if err != nil { - return "", err - } - - releaseTag, err := it.getReleaseTag(owner, repo) - if err != nil { - return "", err - } - - filename := fmt.Sprintf("%s_%s_%s.zip", url.PathEscape(repo), runtime.GOOS, runtime.GOARCH) - downloadURL := fmt.Sprintf( - "https://github.com/%s/%s/releases/download/%s/%s", - url.PathEscape(owner), - url.PathEscape(repo), - url.PathEscape(releaseTag), - filename, - ) - - return downloadURL, nil -} - -func (it *installTarget) getOwnerAndRepo() (string, string, error) { - if it.owner != "" && it.repo != "" { - return it.owner, it.repo, nil - } - - // Get owner and repo from plugin registry - defURL := fmt.Sprintf( - "%s/mackerelio/plugin-registry/master/plugins/%s.json", - it.getRawGithubURL(), - url.PathEscape(it.pluginName), - ) - resp, err := (&client{}).get(defURL) - if err != nil { - return "", "", err - } - defer resp.Body.Close() - - var def registryDef - err = json.NewDecoder(resp.Body).Decode(&def) - if err != nil { - return "", "", err - } - - ownerAndRepo := strings.Split(def.Source, "/") - if len(ownerAndRepo) != 2 { - return "", "", fmt.Errorf("source definition is invalid") - } - - // Cache owner and repo - it.owner = ownerAndRepo[0] - it.repo = ownerAndRepo[1] - - return it.owner, it.repo, nil -} - -func (it *installTarget) getReleaseTag(owner, repo string) (string, error) { - if it.releaseTag != "" { - return it.releaseTag, nil - } - - // Get latest release tag from Github API - ctx := context.Background() - client := getGithubClient(ctx) - client.BaseURL = it.getAPIGithubURL() - - release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) - if err != nil { - return "", err - } - - // Cache releaseTag - it.releaseTag = release.GetTagName() - return it.releaseTag, nil -} - -func (it *installTarget) getRawGithubURL() string { - if it.rawGithubURL != "" { - return it.rawGithubURL - } - return defaultRawGithubURL -} - -// Returns URL object which Github Client.BaseURL can receive as it is -func (it *installTarget) getAPIGithubURL() *url.URL { - u := defaultAPIGithubURL - if it.apiGithubURL != "" { - u = it.apiGithubURL - } - // Ignore err because apiGithubURL is specified only internally - apiURL, _ := url.Parse(u + "/") // trailing `/` is required for BaseURL - return apiURL -} - -// registryDef represents one plugin definition in plugin-registry -// See Also: https://github.com/mackerelio/plugin-registry -type registryDef struct { - Source string `json:"source"` - Description string `json:"description"` -} diff --git a/plugin/github_test.go b/plugin/github_test.go index 6fec6abc..d7ce0b52 100644 --- a/plugin/github_test.go +++ b/plugin/github_test.go @@ -20,7 +20,7 @@ func githubTestSetup() func() { origGitConfigEnv := os.Getenv("GIT_CONFIG") os.Unsetenv("GIT_CONFIG") - return func () { + return func() { os.Setenv("GITHUB_TOKEN", origTokenEnv) os.Setenv("GIT_CONFIG", origGitConfigEnv) } diff --git a/plugin/install.go b/plugin/install.go new file mode 100644 index 00000000..e05252ab --- /dev/null +++ b/plugin/install.go @@ -0,0 +1,187 @@ +package plugin + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "github.com/mackerelio/mkr/logger" + "github.com/mholt/archiver" + "github.com/pkg/errors" + "gopkg.in/urfave/cli.v1" +) + +var commandPluginInstall = cli.Command{ + Name: "install", + Usage: "Install a plugin from github or plugin registry", + ArgsUsage: "[--prefix ] [--overwrite] ", + Action: doPluginInstall, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "prefix", + Usage: "Plugin install location. The default is /opt/mackerel-agent/plugins", + }, + cli.BoolFlag{ + Name: "overwrite", + Usage: "Overwrite a plugin command in a plugin directory, even if same name command exists", + }, + }, + Description: ` + Install a mackerel plugin and a check plugin from github or plugin registry. + To install by mkr, a plugin has to be released to Github Releases in specification format. + + is: + - /[@] + Install from specified github owner, repository, and Github Releases tag. + If you omit , the installer install from latest Github Release. + Example: mkr plugin install mackerelio/mackerel-plugin-sample@v0.0.1 + - [@ explicitly. + If you specify , the installer doesn't use Github API, + so Github API Rate Limit error doesn't occur. +`, +} + +// main function for mkr plugin install +func doPluginInstall(c *cli.Context) error { + argInstallTarget := c.Args().First() + if argInstallTarget == "" { + return fmt.Errorf("Specify install target") + } + + it, err := newInstallTargetFromString(argInstallTarget) + if err != nil { + return errors.Wrap(err, "Failed to install plugin while parsing install target") + } + + pluginDir, err := setupPluginDir(c.String("prefix")) + if err != nil { + return errors.Wrap(err, "Failed to install plugin while setup plugin directory") + } + + // Create a work directory for downloading and extracting an artifact + workdir, err := ioutil.TempDir(filepath.Join(pluginDir, "work"), "mkr-plugin-installer-") + if err != nil { + return errors.Wrap(err, "Failed to install plugin while creating a work directory") + } + defer os.RemoveAll(workdir) + + // Download an artifact and install by it + downloadURL, err := it.makeDownloadURL() + if err != nil { + return errors.Wrap(err, "Failed to install plugin while making a download URL") + } + artifactFile, err := downloadPluginArtifact(downloadURL, workdir) + if err != nil { + return errors.Wrap(err, "Failed to install plugin while downloading an artifact") + } + err = installByArtifact(artifactFile, filepath.Join(pluginDir, "bin"), workdir, c.Bool("overwrite")) + if err != nil { + return errors.Wrap(err, "Failed to install plugin while extracting and placing") + } + + logger.Log("", fmt.Sprintf("Successfully installed %s", argInstallTarget)) + return nil +} + +// Create a directory for plugin install +func setupPluginDir(pluginDir string) (string, error) { + if pluginDir == "" { + pluginDir = "/opt/mackerel-agent/plugins" + } + err := os.MkdirAll(filepath.Join(pluginDir, "bin"), 0755) + if err != nil { + return "", err + } + err = os.MkdirAll(filepath.Join(pluginDir, "work"), 0755) + if err != nil { + return "", err + } + return pluginDir, nil +} + +// Download plugin artifact from `u`(URL) to `workdir`, +// and returns downloaded filepath +func downloadPluginArtifact(u, workdir string) (fpath string, err error) { + logger.Log("", fmt.Sprintf("Downloading %s", u)) + + // Create request to download + resp, err := (&client{}).get(u) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // fpath is filepath where artifact will be saved + fpath = filepath.Join(workdir, path.Base(u)) + + // download artifact + file, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return "", err + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", err + } + + return fpath, nil +} + +// Extract artifact and install plugin +func installByArtifact(artifactFile, bindir, workdir string, overwrite bool) error { + // unzip artifact to work directory + err := archiver.Zip.Open(artifactFile, workdir) + if err != nil { + return err + } + + // Look for plugin files recursively, and place those to binPath + return filepath.Walk(workdir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + // a plugin file should be executable, and have specified name. + name := info.Name() + isExecutable := (info.Mode() & 0111) != 0 + if isExecutable && looksLikePlugin(name) { + return placePlugin(path, filepath.Join(bindir, name), overwrite) + } + + // `path` is a file but not plugin. + return nil + }) +} + +func looksLikePlugin(name string) bool { + return strings.HasPrefix(name, "check-") || strings.HasPrefix(name, "mackerel-plugin-") +} + +func placePlugin(src, dest string, overwrite bool) error { + _, err := os.Stat(dest) + if err == nil && !overwrite { + logger.Log("", fmt.Sprintf("%s already exists. Skip installing for now", dest)) + return nil + } + logger.Log("", fmt.Sprintf("Installing %s", dest)) + return os.Rename(src, dest) +} diff --git a/plugin/install_target.go b/plugin/install_target.go new file mode 100644 index 00000000..32e982fb --- /dev/null +++ b/plugin/install_target.go @@ -0,0 +1,155 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "regexp" + "runtime" + "strings" +) + +type installTarget struct { + owner string + repo string + pluginName string + releaseTag string + + // fields for testing + rawGithubURL string + apiGithubURL string +} + +const ( + defaultRawGithubURL = "https://raw.githubusercontent.com" + defaultAPIGithubURL = "https://api.github.com" +) + +// the pattern of installTarget string +// (?:|/)(?:@)? +var targetReg = regexp.MustCompile(`^(?:([^@/]+)/([^@/]+)|([^@/]+))(?:@(.+))?$`) + +// Parse install target string, and construct installTarget +// example is below +// - mackerelio/mackerel-plugin-sample +// - mackerel-plugin-sample +// - mackerelio/mackerel-plugin-sample@v0.0.1 +func newInstallTargetFromString(target string) (*installTarget, error) { + matches := targetReg.FindStringSubmatch(target) + if len(matches) != 5 { + return nil, fmt.Errorf("Install target is invalid: %s", target) + } + + it := &installTarget{ + owner: matches[1], + repo: matches[2], + pluginName: matches[3], + releaseTag: matches[4], + } + return it, nil +} + +// Make artifact's download URL +func (it *installTarget) makeDownloadURL() (string, error) { + owner, repo, err := it.getOwnerAndRepo() + if err != nil { + return "", err + } + + releaseTag, err := it.getReleaseTag(owner, repo) + if err != nil { + return "", err + } + + filename := fmt.Sprintf("%s_%s_%s.zip", url.PathEscape(repo), runtime.GOOS, runtime.GOARCH) + downloadURL := fmt.Sprintf( + "https://github.com/%s/%s/releases/download/%s/%s", + url.PathEscape(owner), + url.PathEscape(repo), + url.PathEscape(releaseTag), + filename, + ) + + return downloadURL, nil +} + +func (it *installTarget) getOwnerAndRepo() (string, string, error) { + if it.owner != "" && it.repo != "" { + return it.owner, it.repo, nil + } + + // Get owner and repo from plugin registry + defURL := fmt.Sprintf( + "%s/mackerelio/plugin-registry/master/plugins/%s.json", + it.getRawGithubURL(), + url.PathEscape(it.pluginName), + ) + resp, err := (&client{}).get(defURL) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + + var def registryDef + err = json.NewDecoder(resp.Body).Decode(&def) + if err != nil { + return "", "", err + } + + ownerAndRepo := strings.Split(def.Source, "/") + if len(ownerAndRepo) != 2 { + return "", "", fmt.Errorf("source definition is invalid") + } + + // Cache owner and repo + it.owner = ownerAndRepo[0] + it.repo = ownerAndRepo[1] + + return it.owner, it.repo, nil +} + +func (it *installTarget) getReleaseTag(owner, repo string) (string, error) { + if it.releaseTag != "" { + return it.releaseTag, nil + } + + // Get latest release tag from Github API + ctx := context.Background() + client := getGithubClient(ctx) + client.BaseURL = it.getAPIGithubURL() + + release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) + if err != nil { + return "", err + } + + // Cache releaseTag + it.releaseTag = release.GetTagName() + return it.releaseTag, nil +} + +func (it *installTarget) getRawGithubURL() string { + if it.rawGithubURL != "" { + return it.rawGithubURL + } + return defaultRawGithubURL +} + +// Returns URL object which Github Client.BaseURL can receive as it is +func (it *installTarget) getAPIGithubURL() *url.URL { + u := defaultAPIGithubURL + if it.apiGithubURL != "" { + u = it.apiGithubURL + } + // Ignore err because apiGithubURL is specified only internally + apiURL, _ := url.Parse(u + "/") // trailing `/` is required for BaseURL + return apiURL +} + +// registryDef represents one plugin definition in plugin-registry +// See Also: https://github.com/mackerelio/plugin-registry +type registryDef struct { + Source string `json:"source"` + Description string `json:"description"` +} diff --git a/plugin/commands_test.go b/plugin/install_target_test.go similarity index 64% rename from plugin/commands_test.go rename to plugin/install_target_test.go index fcdfc6ba..a1325072 100644 --- a/plugin/commands_test.go +++ b/plugin/install_target_test.go @@ -1,210 +1,16 @@ package plugin import ( + "encoding/json" "fmt" - "io/ioutil" "net/http" "net/http/httptest" - "os" - "path/filepath" "runtime" "testing" - "encoding/json" - "github.com/stretchr/testify/assert" ) -func tempd(t *testing.T) string { - tmpd, err := ioutil.TempDir("", "mkr-plugin-install") - if err != nil { - t.Fatal(err) - } - return tmpd -} - -func assertEqualFileContent(t *testing.T, aFile, bFile, message string) { - aContent, err := ioutil.ReadFile(aFile) - if err != nil { - t.Fatal(err) - } - bContent, err := ioutil.ReadFile(bFile) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, aContent, bContent, message) -} - -func TestSetupPluginDir(t *testing.T) { - { - // Creating plugin dir is successful - tmpd := tempd(t) - defer os.RemoveAll(tmpd) - - pluginDir, err := setupPluginDir(tmpd) - assert.Equal(t, tmpd, pluginDir, "returns default plugin directory") - assert.Nil(t, err, "setup finished successfully") - - fi, err := os.Stat(filepath.Join(tmpd, "bin")) - if assert.Nil(t, err) { - assert.True(t, fi.IsDir(), "plugin bin directory is created") - } - - fi, err = os.Stat(filepath.Join(tmpd, "work")) - if assert.Nil(t, err) { - assert.True(t, fi.IsDir(), "plugin work directory is created") - } - } - - { - // Creating plugin dir is failed because of directory's permission - tmpd := tempd(t) - defer os.RemoveAll(tmpd) - err := os.Chmod(tmpd, 0500) - assert.Nil(t, err, "chmod finished successfully") - - pluginDir, err := setupPluginDir(tmpd) - assert.Equal(t, "", pluginDir, "returns empty string when failed") - assert.NotNil(t, err, "error should be occured while manipulate unpermitted directory") - } -} - -func TestDownloadPluginArtifact(t *testing.T) { - ts := httptest.NewServer(http.FileServer(http.Dir("testdata"))) - defer ts.Close() - - { - // Response not found - tmpd := tempd(t) - defer os.RemoveAll(tmpd) - - fpath, err := downloadPluginArtifact(ts.URL+"/not_found.zip", tmpd) - assert.Equal(t, "", fpath, "fpath is empty") - assert.Contains(t, err.Error(), "http response not OK. code: 404,", "Returns correct err") - } - - { - // Download is finished successfully - tmpd := tempd(t) - defer os.RemoveAll(tmpd) - - fpath, err := downloadPluginArtifact(ts.URL+"/mackerel-plugin-sample_linux_amd64.zip", tmpd) - assert.Equal(t, tmpd+"/mackerel-plugin-sample_linux_amd64.zip", fpath, "Returns fpath correctly") - - _, err = os.Stat(fpath) - assert.Nil(t, err, "Downloaded file is created") - - assertEqualFileContent(t, fpath, "testdata/mackerel-plugin-sample_linux_amd64.zip", "Downloaded data is correct") - } -} - -func TestInstallByArtifact(t *testing.T) { - { - // Install by the artifact which has a single plugin - bindir := tempd(t) - defer os.RemoveAll(bindir) - workdir := tempd(t) - defer os.RemoveAll(workdir) - - err := installByArtifact("testdata/mackerel-plugin-sample_linux_amd64.zip", bindir, workdir, false) - assert.Nil(t, err, "installByArtifact finished successfully") - - installedPath := filepath.Join(bindir, "mackerel-plugin-sample") - - fi, err := os.Stat(installedPath) - assert.Nil(t, err, "A plugin file exists") - assert.True(t, fi.Mode().IsRegular() && fi.Mode().Perm() == 0755, "A plugin file has execution permission") - assertEqualFileContent( - t, - installedPath, - "testdata/mackerel-plugin-sample_linux_amd64/mackerel-plugin-sample", - "Installed plugin is valid", - ) - - // Install same name plugin, but it is skipped - workdir2 := tempd(t) - defer os.RemoveAll(workdir2) - err = installByArtifact("testdata/mackerel-plugin-sample-duplicate_linux_amd64.zip", bindir, workdir2, false) - assert.Nil(t, err, "installByArtifact finished successfully even if same name plugin exists") - - fi, err = os.Stat(filepath.Join(bindir, "mackerel-plugin-sample")) - assert.Nil(t, err, "A plugin file exists") - assertEqualFileContent( - t, - installedPath, - "testdata/mackerel-plugin-sample_linux_amd64/mackerel-plugin-sample", - "Install is skipped, so the contents is what is before", - ) - - // Install same name plugin with overwrite option - workdir3 := tempd(t) - defer os.RemoveAll(workdir3) - err = installByArtifact("testdata/mackerel-plugin-sample-duplicate_linux_amd64.zip", bindir, workdir3, true) - assert.Nil(t, err, "installByArtifact finished successfully") - assertEqualFileContent( - t, - installedPath, - "testdata/mackerel-plugin-sample-duplicate_linux_amd64/mackerel-plugin-sample", - "a plugin is installed with overwrite option, so the contents is overwritten", - ) - } - - { - // Install by the artifact which has multiple plugins - bindir := tempd(t) - defer os.RemoveAll(bindir) - workdir := tempd(t) - defer os.RemoveAll(workdir) - - installByArtifact("testdata/mackerel-plugin-sample-multi_darwin_386.zip", bindir, workdir, false) - - // check-sample, mackerel-plugin-sample-multi-1 and plugins/mackerel-plugin-sample-multi-2 - // are installed. But followings are not installed - // - mackerel-plugin-non-executable: does not have execution permission - // - not-mackerel-plugin-sample: does not has plugin file name - assertEqualFileContent(t, - filepath.Join(bindir, "check-sample"), - "testdata/mackerel-plugin-sample-multi_darwin_386/check-sample", - "check-sample is installed", - ) - assertEqualFileContent(t, - filepath.Join(bindir, "mackerel-plugin-sample-multi-1"), - "testdata/mackerel-plugin-sample-multi_darwin_386/mackerel-plugin-sample-multi-1", - "mackerel-plugin-sample-multi-1 is installed", - ) - assertEqualFileContent(t, - filepath.Join(bindir, "mackerel-plugin-sample-multi-2"), - "testdata/mackerel-plugin-sample-multi_darwin_386/plugins/mackerel-plugin-sample-multi-2", - "mackerel-plugin-sample-multi-2 is installed", - ) - - _, err := os.Stat(filepath.Join(bindir, "mackerel-plugin-not-executable")) - assert.NotNil(t, err, "mackerel-plugin-not-executable is not installed") - _, err = os.Stat(filepath.Join(bindir, "not-mackerel-plugin-sample")) - assert.NotNil(t, err, "not-mackerel-plugin-sample is not installed") - } -} - -func TestLooksLikePlugin(t *testing.T) { - testCases := []struct { - Name string - LooksLikePlugin bool - }{ - {"mackerel-plugin-sample", true}, - {"mackerel-plugin-hoge_sample1", true}, - {"check-sample", true}, - {"check-hoge-sample", true}, - {"mackerel-sample", false}, - {"hoge-mackerel-plugin-sample", false}, - {"hoge-check-sample", false}, - {"wrong-sample", false}, - } - - for _, tc := range testCases { - assert.Equal(t, tc.LooksLikePlugin, looksLikePlugin(tc.Name)) - } -} - func TestNewInstallTargetFromString(t *testing.T) { testCases := []struct { Name string diff --git a/plugin/install_test.go b/plugin/install_test.go new file mode 100644 index 00000000..d85063c4 --- /dev/null +++ b/plugin/install_test.go @@ -0,0 +1,202 @@ +package plugin + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func tempd(t *testing.T) string { + tmpd, err := ioutil.TempDir("", "mkr-plugin-install") + if err != nil { + t.Fatal(err) + } + return tmpd +} + +func assertEqualFileContent(t *testing.T, aFile, bFile, message string) { + aContent, err := ioutil.ReadFile(aFile) + if err != nil { + t.Fatal(err) + } + bContent, err := ioutil.ReadFile(bFile) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, aContent, bContent, message) +} + +func TestSetupPluginDir(t *testing.T) { + { + // Creating plugin dir is successful + tmpd := tempd(t) + defer os.RemoveAll(tmpd) + + pluginDir, err := setupPluginDir(tmpd) + assert.Equal(t, tmpd, pluginDir, "returns default plugin directory") + assert.Nil(t, err, "setup finished successfully") + + fi, err := os.Stat(filepath.Join(tmpd, "bin")) + if assert.Nil(t, err) { + assert.True(t, fi.IsDir(), "plugin bin directory is created") + } + + fi, err = os.Stat(filepath.Join(tmpd, "work")) + if assert.Nil(t, err) { + assert.True(t, fi.IsDir(), "plugin work directory is created") + } + } + + { + // Creating plugin dir is failed because of directory's permission + tmpd := tempd(t) + defer os.RemoveAll(tmpd) + err := os.Chmod(tmpd, 0500) + assert.Nil(t, err, "chmod finished successfully") + + pluginDir, err := setupPluginDir(tmpd) + assert.Equal(t, "", pluginDir, "returns empty string when failed") + assert.NotNil(t, err, "error should be occured while manipulate unpermitted directory") + } +} + +func TestDownloadPluginArtifact(t *testing.T) { + ts := httptest.NewServer(http.FileServer(http.Dir("testdata"))) + defer ts.Close() + + { + // Response not found + tmpd := tempd(t) + defer os.RemoveAll(tmpd) + + fpath, err := downloadPluginArtifact(ts.URL+"/not_found.zip", tmpd) + assert.Equal(t, "", fpath, "fpath is empty") + assert.Contains(t, err.Error(), "http response not OK. code: 404,", "Returns correct err") + } + + { + // Download is finished successfully + tmpd := tempd(t) + defer os.RemoveAll(tmpd) + + fpath, err := downloadPluginArtifact(ts.URL+"/mackerel-plugin-sample_linux_amd64.zip", tmpd) + assert.Equal(t, tmpd+"/mackerel-plugin-sample_linux_amd64.zip", fpath, "Returns fpath correctly") + + _, err = os.Stat(fpath) + assert.Nil(t, err, "Downloaded file is created") + + assertEqualFileContent(t, fpath, "testdata/mackerel-plugin-sample_linux_amd64.zip", "Downloaded data is correct") + } +} + +func TestInstallByArtifact(t *testing.T) { + { + // Install by the artifact which has a single plugin + bindir := tempd(t) + defer os.RemoveAll(bindir) + workdir := tempd(t) + defer os.RemoveAll(workdir) + + err := installByArtifact("testdata/mackerel-plugin-sample_linux_amd64.zip", bindir, workdir, false) + assert.Nil(t, err, "installByArtifact finished successfully") + + installedPath := filepath.Join(bindir, "mackerel-plugin-sample") + + fi, err := os.Stat(installedPath) + assert.Nil(t, err, "A plugin file exists") + assert.True(t, fi.Mode().IsRegular() && fi.Mode().Perm() == 0755, "A plugin file has execution permission") + assertEqualFileContent( + t, + installedPath, + "testdata/mackerel-plugin-sample_linux_amd64/mackerel-plugin-sample", + "Installed plugin is valid", + ) + + // Install same name plugin, but it is skipped + workdir2 := tempd(t) + defer os.RemoveAll(workdir2) + err = installByArtifact("testdata/mackerel-plugin-sample-duplicate_linux_amd64.zip", bindir, workdir2, false) + assert.Nil(t, err, "installByArtifact finished successfully even if same name plugin exists") + + fi, err = os.Stat(filepath.Join(bindir, "mackerel-plugin-sample")) + assert.Nil(t, err, "A plugin file exists") + assertEqualFileContent( + t, + installedPath, + "testdata/mackerel-plugin-sample_linux_amd64/mackerel-plugin-sample", + "Install is skipped, so the contents is what is before", + ) + + // Install same name plugin with overwrite option + workdir3 := tempd(t) + defer os.RemoveAll(workdir3) + err = installByArtifact("testdata/mackerel-plugin-sample-duplicate_linux_amd64.zip", bindir, workdir3, true) + assert.Nil(t, err, "installByArtifact finished successfully") + assertEqualFileContent( + t, + installedPath, + "testdata/mackerel-plugin-sample-duplicate_linux_amd64/mackerel-plugin-sample", + "a plugin is installed with overwrite option, so the contents is overwritten", + ) + } + + { + // Install by the artifact which has multiple plugins + bindir := tempd(t) + defer os.RemoveAll(bindir) + workdir := tempd(t) + defer os.RemoveAll(workdir) + + installByArtifact("testdata/mackerel-plugin-sample-multi_darwin_386.zip", bindir, workdir, false) + + // check-sample, mackerel-plugin-sample-multi-1 and plugins/mackerel-plugin-sample-multi-2 + // are installed. But followings are not installed + // - mackerel-plugin-non-executable: does not have execution permission + // - not-mackerel-plugin-sample: does not has plugin file name + assertEqualFileContent(t, + filepath.Join(bindir, "check-sample"), + "testdata/mackerel-plugin-sample-multi_darwin_386/check-sample", + "check-sample is installed", + ) + assertEqualFileContent(t, + filepath.Join(bindir, "mackerel-plugin-sample-multi-1"), + "testdata/mackerel-plugin-sample-multi_darwin_386/mackerel-plugin-sample-multi-1", + "mackerel-plugin-sample-multi-1 is installed", + ) + assertEqualFileContent(t, + filepath.Join(bindir, "mackerel-plugin-sample-multi-2"), + "testdata/mackerel-plugin-sample-multi_darwin_386/plugins/mackerel-plugin-sample-multi-2", + "mackerel-plugin-sample-multi-2 is installed", + ) + + _, err := os.Stat(filepath.Join(bindir, "mackerel-plugin-not-executable")) + assert.NotNil(t, err, "mackerel-plugin-not-executable is not installed") + _, err = os.Stat(filepath.Join(bindir, "not-mackerel-plugin-sample")) + assert.NotNil(t, err, "not-mackerel-plugin-sample is not installed") + } +} + +func TestLooksLikePlugin(t *testing.T) { + testCases := []struct { + Name string + LooksLikePlugin bool + }{ + {"mackerel-plugin-sample", true}, + {"mackerel-plugin-hoge_sample1", true}, + {"check-sample", true}, + {"check-hoge-sample", true}, + {"mackerel-sample", false}, + {"hoge-mackerel-plugin-sample", false}, + {"hoge-check-sample", false}, + {"wrong-sample", false}, + } + + for _, tc := range testCases { + assert.Equal(t, tc.LooksLikePlugin, looksLikePlugin(tc.Name)) + } +}