github.com/hashicorp/packer@v1.14.3/command/plugins_install.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package command 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/json" 11 "flag" 12 "fmt" 13 "io" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "runtime" 18 "strings" 19 20 "github.com/hashicorp/packer/packer/plugin-getter/release" 21 22 "github.com/hashicorp/go-version" 23 "github.com/hashicorp/hcl/v2" 24 "github.com/hashicorp/packer-plugin-sdk/plugin" 25 pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" 26 "github.com/hashicorp/packer/hcl2template/addrs" 27 "github.com/hashicorp/packer/packer" 28 plugingetter "github.com/hashicorp/packer/packer/plugin-getter" 29 "github.com/hashicorp/packer/packer/plugin-getter/github" 30 pkrversion "github.com/hashicorp/packer/version" 31 ) 32 33 type PluginsInstallCommand struct { 34 Meta 35 } 36 37 func (c *PluginsInstallCommand) Synopsis() string { 38 return "Install latest Packer plugin [matching version constraint]" 39 } 40 41 func (c *PluginsInstallCommand) Help() string { 42 helpText := ` 43 Usage: packer plugins install [OPTIONS...] <plugin> [<version constraint>] 44 45 This command will install the most recent compatible Packer plugin matching 46 version constraint. 47 When the version constraint is omitted, the most recent version will be 48 installed. 49 50 Ex: packer plugins install github.com/hashicorp/happycloud v1.2.3 51 packer plugins install --path ./packer-plugin-happycloud "github.com/hashicorp/happycloud" 52 53 Options: 54 -path <path> Install the plugin from a locally-sourced plugin binary. 55 This installs the plugin where a normal invocation would, but will 56 not try to download it from a remote location, and instead 57 install the binary in the Packer plugins path. This option cannot 58 be specified with a version constraint. 59 -force Forces reinstallation of plugins, even if already installed. 60 ` 61 62 return strings.TrimSpace(helpText) 63 } 64 65 func (c *PluginsInstallCommand) Run(args []string) int { 66 ctx, cleanup := handleTermInterrupt(c.Ui) 67 defer cleanup() 68 69 cmdArgs, ret := c.ParseArgs(args) 70 if ret != 0 { 71 return ret 72 } 73 74 return c.RunContext(ctx, cmdArgs) 75 } 76 77 type PluginsInstallArgs struct { 78 MetaArgs 79 PluginIdentifier string 80 PluginPath string 81 Version string 82 Force bool 83 } 84 85 func (pa *PluginsInstallArgs) AddFlagSets(flags *flag.FlagSet) { 86 flags.StringVar(&pa.PluginPath, "path", "", "install the binary specified by path as a Packer plugin.") 87 flags.BoolVar(&pa.Force, "force", false, "force installation of the specified plugin, even if already installed.") 88 pa.MetaArgs.AddFlagSets(flags) 89 } 90 91 func (c *PluginsInstallCommand) ParseArgs(args []string) (*PluginsInstallArgs, int) { 92 pa := &PluginsInstallArgs{} 93 94 flags := c.Meta.FlagSet("plugins install") 95 flags.Usage = func() { c.Ui.Say(c.Help()) } 96 pa.AddFlagSets(flags) 97 err := flags.Parse(args) 98 if err != nil { 99 c.Ui.Error(fmt.Sprintf("Failed to parse options: %s", err)) 100 return pa, 1 101 } 102 103 args = flags.Args() 104 if len(args) < 1 || len(args) > 2 { 105 c.Ui.Error(fmt.Sprintf("Invalid arguments, expected either 1 or 2 positional arguments, got %d", len(args))) 106 flags.Usage() 107 return pa, 1 108 } 109 110 if len(args) == 2 { 111 pa.Version = args[1] 112 } 113 114 if pa.Path != "" && pa.Version != "" { 115 c.Ui.Error("Invalid arguments: a version cannot be specified when using --path to install a local plugin binary") 116 flags.Usage() 117 return pa, 1 118 } 119 120 pa.PluginIdentifier = args[0] 121 return pa, 0 122 } 123 124 func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *PluginsInstallArgs) int { 125 opts := plugingetter.ListInstallationsOptions{ 126 PluginDirectory: c.Meta.CoreConfig.Components.PluginConfig.PluginDirectory, 127 BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{ 128 OS: runtime.GOOS, 129 ARCH: runtime.GOARCH, 130 APIVersionMajor: pluginsdk.APIVersionMajor, 131 APIVersionMinor: pluginsdk.APIVersionMinor, 132 Checksummers: []plugingetter.Checksummer{ 133 {Type: "sha256", Hash: sha256.New()}, 134 }, 135 ReleasesOnly: true, 136 }, 137 } 138 if runtime.GOOS == "windows" { 139 opts.BinaryInstallationOptions.Ext = ".exe" 140 } 141 142 plugin, err := addrs.ParsePluginSourceString(args.PluginIdentifier) 143 if err != nil { 144 c.Ui.Errorf("Invalid source string %q: %s", args.PluginIdentifier, err) 145 return 1 146 } 147 148 // If we did specify a binary to install the plugin from, we ignore 149 // the Github-based getter in favour of installing it directly. 150 if args.PluginPath != "" { 151 return c.InstallFromBinary(opts, plugin, args) 152 } 153 154 // a plugin requirement that matches them all 155 pluginRequirement := plugingetter.Requirement{ 156 Identifier: plugin, 157 } 158 159 if args.Version != "" { 160 constraints, err := version.NewConstraint(args.Version) 161 if err != nil { 162 c.Ui.Error(err.Error()) 163 return 1 164 } 165 166 hasPrerelease := false 167 for _, con := range constraints { 168 if con.Prerelease() { 169 hasPrerelease = true 170 } 171 } 172 if hasPrerelease { 173 c.Ui.Errorf("Unsupported prerelease for constraint %q", args.Version) 174 return 1 175 } 176 177 pluginRequirement.VersionConstraints = constraints 178 } 179 180 getters := []plugingetter.Getter{ 181 &release.Getter{ 182 Name: "releases.hashicorp.com", 183 }, 184 &github.Getter{ 185 // In the past some terraform plugins downloads were blocked from a 186 // specific aws region by s3. Changing the user agent unblocked the 187 // downloads so having one user agent per version will help mitigate 188 // that a little more. Especially in the case someone forks this 189 // code to make it more aggressive or something. 190 // TODO: allow to set this from the config file or an environment 191 // variable. 192 UserAgent: "packer-getter-github-" + pkrversion.String(), 193 Name: "github.com", 194 }, 195 } 196 197 newInstall, err := pluginRequirement.InstallLatest(plugingetter.InstallOptions{ 198 PluginDirectory: opts.PluginDirectory, 199 BinaryInstallationOptions: opts.BinaryInstallationOptions, 200 Getters: getters, 201 Force: args.Force, 202 }) 203 204 if err != nil { 205 c.Ui.Error(err.Error()) 206 return 1 207 } 208 209 if newInstall != nil { 210 msg := fmt.Sprintf("Installed plugin %s %s in %q", pluginRequirement.Identifier, newInstall.Version, newInstall.BinaryPath) 211 ui := &packer.ColoredUi{ 212 Color: packer.UiColorCyan, 213 Ui: c.Ui, 214 } 215 ui.Say(msg) 216 return 0 217 } 218 219 return 0 220 } 221 222 func (c *PluginsInstallCommand) InstallFromBinary(opts plugingetter.ListInstallationsOptions, pluginIdentifier *addrs.Plugin, args *PluginsInstallArgs) int { 223 pluginDir := opts.PluginDirectory 224 225 var err error 226 227 args.PluginPath, err = filepath.Abs(args.PluginPath) 228 if err != nil { 229 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 230 Severity: hcl.DiagError, 231 Summary: "Failed to transform path", 232 Detail: fmt.Sprintf("Failed to transform the given path to an absolute one: %s", err), 233 }}) 234 } 235 236 s, err := os.Stat(args.PluginPath) 237 if err != nil { 238 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 239 Severity: hcl.DiagError, 240 Summary: "Unable to find plugin to promote", 241 Detail: fmt.Sprintf("The plugin %q failed to be opened because of an error: %s", args.PluginIdentifier, err), 242 }}) 243 } 244 245 if s.IsDir() { 246 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 247 Severity: hcl.DiagError, 248 Summary: "Plugin to promote cannot be a directory", 249 Detail: "The packer plugin promote command can only install binaries, not directories", 250 }}) 251 } 252 253 describeCmd, err := exec.Command(args.PluginPath, "describe").Output() 254 if err != nil { 255 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 256 Severity: hcl.DiagError, 257 Summary: "Failed to describe the plugin", 258 Detail: fmt.Sprintf("Packer failed to run %s describe: %s", args.PluginPath, err), 259 }}) 260 } 261 262 var desc plugin.SetDescription 263 if err := json.Unmarshal(describeCmd, &desc); err != nil { 264 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 265 Severity: hcl.DiagError, 266 Summary: "Failed to decode plugin describe info", 267 Detail: fmt.Sprintf("'%s describe' produced information that Packer couldn't decode: %s", args.PluginPath, err), 268 }}) 269 } 270 271 semver, err := version.NewSemver(desc.Version) 272 if err != nil { 273 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 274 Severity: hcl.DiagError, 275 Summary: "Invalid version", 276 Detail: fmt.Sprintf("Plugin's reported version (%q) is not semver-compatible: %s", desc.Version, err), 277 }}) 278 } 279 if semver.Prerelease() != "" && semver.Prerelease() != "dev" { 280 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 281 Severity: hcl.DiagError, 282 Summary: "Invalid version", 283 Detail: fmt.Sprintf("Packer can only install plugin releases with this command (ex: 1.0.0) or development pre-releases (ex: 1.0.0-dev), the binary's reported version is %q", desc.Version), 284 }}) 285 } 286 287 pluginBinary, err := os.Open(args.PluginPath) 288 if err != nil { 289 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 290 Severity: hcl.DiagError, 291 Summary: "Failed to open plugin binary", 292 Detail: fmt.Sprintf("Failed to open plugin binary from %q: %s", args.PluginPath, err), 293 }}) 294 } 295 296 pluginContents := bytes.Buffer{} 297 _, err = io.Copy(&pluginContents, pluginBinary) 298 if err != nil { 299 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 300 Severity: hcl.DiagError, 301 Summary: "Failed to read plugin binary's contents", 302 Detail: fmt.Sprintf("Failed to read plugin binary from %q: %s", args.PluginPath, err), 303 }}) 304 } 305 _ = pluginBinary.Close() 306 307 // At this point, we know the provided binary behaves correctly with 308 // describe, so it's very likely to be a plugin, let's install it. 309 installDir := filepath.Join( 310 pluginDir, 311 filepath.Join(pluginIdentifier.Parts()...), 312 ) 313 err = os.MkdirAll(installDir, 0755) 314 if err != nil { 315 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 316 Severity: hcl.DiagError, 317 Summary: "Failed to create output directory", 318 Detail: fmt.Sprintf("The installation directory %q failed to be created because of an error: %s", installDir, err), 319 }}) 320 } 321 322 // Remove metadata from plugin path 323 noMetaVersion := semver.Core().String() 324 if semver.Prerelease() != "" { 325 noMetaVersion = fmt.Sprintf("%s-%s", noMetaVersion, semver.Prerelease()) 326 } 327 328 outputPrefix := fmt.Sprintf( 329 "packer-plugin-%s_v%s_%s", 330 pluginIdentifier.Name(), 331 noMetaVersion, 332 desc.APIVersion, 333 ) 334 binaryPath := filepath.Join( 335 installDir, 336 outputPrefix+opts.BinaryInstallationOptions.FilenameSuffix(), 337 ) 338 339 outputPlugin, err := os.OpenFile(binaryPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0755) 340 if err != nil { 341 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 342 Severity: hcl.DiagError, 343 Summary: "Failed to create plugin binary", 344 Detail: fmt.Sprintf("Failed to create plugin binary at %q: %s", binaryPath, err), 345 }}) 346 } 347 defer outputPlugin.Close() 348 349 _, err = outputPlugin.Write(pluginContents.Bytes()) 350 if err != nil { 351 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 352 Severity: hcl.DiagError, 353 Summary: "Failed to copy plugin binary's contents", 354 Detail: fmt.Sprintf("Failed to copy plugin binary from %q to %q: %s", args.PluginPath, binaryPath, err), 355 }}) 356 } 357 358 // We'll install the SHA256SUM file alongside the plugin, based on the 359 // contents of the plugin being passed. 360 shasum := sha256.New() 361 _, _ = shasum.Write(pluginContents.Bytes()) 362 363 shasumPath := fmt.Sprintf("%s_SHA256SUM", binaryPath) 364 shaFile, err := os.OpenFile(shasumPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) 365 if err != nil { 366 return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ 367 Severity: hcl.DiagError, 368 Summary: "Failed to create plugin SHA256SUM file", 369 Detail: fmt.Sprintf("Failed to create SHA256SUM file at %q: %s", shasumPath, err), 370 }}) 371 } 372 defer shaFile.Close() 373 374 fmt.Fprintf(shaFile, "%x", shasum.Sum([]byte{})) 375 c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginIdentifier, args.PluginPath, binaryPath)) 376 377 return 0 378 }