mirror of https://github.com/pulumi/pulumi.git
292 lines
9.5 KiB
Go
292 lines
9.5 KiB
Go
// Copyright 2016-2018, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/pulumi/pulumi/pkg/v3/util"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func newPluginInstallCmd() *cobra.Command {
|
|
var picmd pluginInstallCmd
|
|
cmd := &cobra.Command{
|
|
Use: "install [KIND NAME [VERSION]]",
|
|
Args: cmdutil.MaximumNArgs(3),
|
|
Short: "Install one or more plugins",
|
|
Long: "Install one or more plugins.\n" +
|
|
"\n" +
|
|
"This command is used to manually install plugins required by your program. It\n" +
|
|
"may be run with a specific KIND, NAME, and optionally, VERSION, or by omitting\n" +
|
|
"these arguments and letting Pulumi compute the set of plugins required by the\n" +
|
|
"current project. When Pulumi computes the download set automatically, it may\n" +
|
|
"download more plugins than are strictly necessary.\n" +
|
|
"\n" +
|
|
"If VERSION is specified, it cannot be a range; it must be a specific number.\n" +
|
|
"If VERSION is unspecified, Pulumi will attempt to look up the latest version of\n" +
|
|
"the plugin, though the result is not guaranteed.",
|
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
|
ctx := commandContext()
|
|
return picmd.Run(ctx, args)
|
|
}),
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVar(&picmd.serverURL,
|
|
"server", "", "A URL to download plugins from")
|
|
cmd.PersistentFlags().BoolVar(&picmd.exact,
|
|
"exact", false, "Force installation of an exact version match (usually >= is accepted)")
|
|
cmd.PersistentFlags().StringVarP(&picmd.file,
|
|
"file", "f", "", "Install a plugin from a binary, folder or tarball, instead of downloading it")
|
|
cmd.PersistentFlags().BoolVar(&picmd.reinstall,
|
|
"reinstall", false, "Reinstall a plugin even if it already exists")
|
|
cmd.PersistentFlags().StringVar(&picmd.checksum,
|
|
"checksum", "", "The expected SHA256 checksum for the plugin archive")
|
|
|
|
return cmd
|
|
}
|
|
|
|
type pluginInstallCmd struct {
|
|
serverURL string
|
|
exact bool
|
|
file string
|
|
reinstall bool
|
|
checksum string
|
|
|
|
diag diag.Sink
|
|
env env.Env
|
|
color colors.Colorization
|
|
|
|
pluginGetLatestVersion func(
|
|
workspace.PluginSpec,
|
|
) (*semver.Version, error) // == workspace.PluginSpec.GetLatestVersion
|
|
}
|
|
|
|
func (cmd *pluginInstallCmd) Run(ctx context.Context, args []string) error {
|
|
if cmd.env == nil {
|
|
cmd.env = env.Global()
|
|
}
|
|
if cmd.diag == nil {
|
|
cmd.diag = cmdutil.Diag()
|
|
}
|
|
if cmd.color == "" {
|
|
cmd.color = cmdutil.GetGlobalColorization()
|
|
}
|
|
if cmd.pluginGetLatestVersion == nil {
|
|
cmd.pluginGetLatestVersion = (workspace.PluginSpec).GetLatestVersion
|
|
}
|
|
|
|
// Parse the kind, name, and version, if specified.
|
|
var installs []workspace.PluginSpec
|
|
if len(args) > 0 {
|
|
if !workspace.IsPluginKind(args[0]) {
|
|
return fmt.Errorf("unrecognized plugin kind: %s", args[0])
|
|
} else if len(args) < 2 {
|
|
return errors.New("missing plugin name argument")
|
|
}
|
|
|
|
var version *semver.Version
|
|
if len(args) == 3 {
|
|
parsedVersion, err := semver.ParseTolerant(args[2])
|
|
version = &parsedVersion
|
|
if err != nil {
|
|
return fmt.Errorf("invalid plugin semver: %w", err)
|
|
}
|
|
}
|
|
if len(args) < 3 && cmd.file != "" {
|
|
return errors.New("missing plugin version argument, this is required if installing from a file")
|
|
}
|
|
|
|
var checksums map[string][]byte
|
|
if cmd.checksum != "" {
|
|
checksumBytes, err := hex.DecodeString(cmd.checksum)
|
|
if err != nil {
|
|
return fmt.Errorf("--checksum was not a valid hex string: %w", err)
|
|
}
|
|
checksums = map[string][]byte{
|
|
runtime.GOOS + "-" + runtime.GOARCH: checksumBytes,
|
|
}
|
|
}
|
|
|
|
pluginSpec := workspace.PluginSpec{
|
|
Kind: workspace.PluginKind(args[0]),
|
|
Name: args[1],
|
|
Version: version,
|
|
PluginDownloadURL: cmd.serverURL, // If empty, will use default plugin source.
|
|
Checksums: checksums,
|
|
}
|
|
|
|
// Bundled plugins are generally not installable with this command. They are expected to be
|
|
// distributed with Pulumi itself. But we turn this check off if PULUMI_DEV is set so we can
|
|
// test installing plugins that are being moved to their own distribution (such as when we move
|
|
// pulumi-nodejs).
|
|
if !cmd.env.GetBool(env.Dev) && workspace.IsPluginBundled(pluginSpec.Kind, pluginSpec.Name) {
|
|
return fmt.Errorf(
|
|
"the %v %v plugin is bundled with Pulumi, and cannot be directly installed"+
|
|
" with this command. If you need to reinstall this plugin, reinstall"+
|
|
" Pulumi via your package manager or install script.",
|
|
pluginSpec.Name,
|
|
pluginSpec.Kind,
|
|
)
|
|
}
|
|
|
|
// Try and set known plugin download URLs
|
|
if urlSet := util.SetKnownPluginDownloadURL(&pluginSpec); urlSet {
|
|
cmd.diag.Infof(
|
|
diag.Message("", "Plugin download URL set to %s"), pluginSpec.PluginDownloadURL)
|
|
}
|
|
|
|
// If we don't have a version try to look one up
|
|
if version == nil {
|
|
latestVersion, err := cmd.pluginGetLatestVersion(pluginSpec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pluginSpec.Version = latestVersion
|
|
}
|
|
|
|
installs = append(installs, pluginSpec)
|
|
} else {
|
|
if cmd.file != "" {
|
|
return errors.New("--file (-f) is only valid if a specific package is being installed")
|
|
}
|
|
if cmd.checksum != "" {
|
|
return errors.New("--checksum is only valid if a specific package is being installed")
|
|
}
|
|
|
|
// If a specific plugin wasn't given, compute the set of plugins the current project needs.
|
|
plugins, err := getProjectPlugins()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, plugin := range plugins {
|
|
// Skip language plugins; by definition, we already have one installed.
|
|
// TODO[pulumi/pulumi#956]: eventually we will want to honor and install these in the usual way.
|
|
if plugin.Kind != workspace.LanguagePlugin {
|
|
installs = append(installs, plugin)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now for each kind, name, version pair, download it from the release website, and install it.
|
|
for _, install := range installs {
|
|
label := fmt.Sprintf("[%s plugin %s]", install.Kind, install)
|
|
|
|
// If the plugin already exists, don't download it unless --reinstall was passed. Note that
|
|
// by default we accept plugins with >= constraints, unless --exact was passed which requires ==.
|
|
if !cmd.reinstall {
|
|
if cmd.exact {
|
|
if workspace.HasPlugin(install) {
|
|
logging.V(1).Infof("%s skipping install (existing == match)", label)
|
|
continue
|
|
}
|
|
} else {
|
|
if has, _ := workspace.HasPluginGTE(install); has {
|
|
logging.V(1).Infof("%s skipping install (existing >= match)", label)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
cmdutil.Diag().Infoerrf(
|
|
diag.Message("", "%s installing"), label)
|
|
|
|
// If we got here, actually try to do the download.
|
|
var source string
|
|
var payload workspace.PluginContent
|
|
var err error
|
|
if cmd.file == "" {
|
|
withProgress := func(stream io.ReadCloser, size int64) io.ReadCloser {
|
|
return workspace.ReadCloserProgressBar(stream, size, "Downloading plugin", cmd.color)
|
|
}
|
|
retry := func(err error, attempt int, limit int, delay time.Duration) {
|
|
cmd.diag.Warningf(
|
|
diag.Message("", "Error downloading plugin: %s\nWill retry in %v [%d/%d]"), err, delay, attempt, limit)
|
|
}
|
|
|
|
r, err := workspace.DownloadToFile(install, withProgress, retry)
|
|
if err != nil {
|
|
return fmt.Errorf("%s downloading from %s: %w", label, install.PluginDownloadURL, err)
|
|
}
|
|
defer func() { contract.IgnoreError(os.Remove(r.Name())) }()
|
|
|
|
payload = workspace.TarPlugin(r)
|
|
} else {
|
|
source = cmd.file
|
|
logging.V(1).Infof("%s opening tarball from %s", label, cmd.file)
|
|
payload, err = getFilePayload(cmd.file, install)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
logging.V(1).Infof("%s installing tarball ...", label)
|
|
if err = install.InstallWithContext(ctx, payload, cmd.reinstall); err != nil {
|
|
return fmt.Errorf("installing %s from %s: %w", label, source, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getFilePayload(file string, spec workspace.PluginSpec) (workspace.PluginContent, error) {
|
|
source := file
|
|
stat, err := os.Stat(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat on file %s: %w", source, err)
|
|
}
|
|
|
|
if stat.IsDir() {
|
|
return workspace.DirPlugin(file), nil
|
|
}
|
|
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening file %s: %w", source, err)
|
|
}
|
|
compressHeader := make([]byte, 5)
|
|
_, err = f.Read(compressHeader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading file %s: %w", source, err)
|
|
}
|
|
_, err = f.Seek(0, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("seeking back in file %s: %w", source, err)
|
|
}
|
|
if !encoding.IsCompressed(compressHeader) {
|
|
// Windows doesn't have executable bits to check
|
|
if runtime.GOOS != "windows" && (stat.Mode()&0o100) == 0 {
|
|
return nil, fmt.Errorf("%s is not executable", source)
|
|
}
|
|
return workspace.SingleFilePlugin(f, spec), nil
|
|
}
|
|
return workspace.TarPlugin(f), nil
|
|
}
|