mirror of https://github.com/helm/helm.git
				
				
				
			
		
			
	
	
		
			210 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			210 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
|  | /* | ||
|  | Copyright The Helm Authors. | ||
|  | 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 cmd | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"bytes" | ||
|  | 	"errors" | ||
|  | 	"fmt" | ||
|  | 	"io" | ||
|  | 	"os" | ||
|  | 	"path/filepath" | ||
|  | 	"syscall" | ||
|  | 
 | ||
|  | 	"github.com/spf13/cobra" | ||
|  | 	"golang.org/x/term" | ||
|  | 
 | ||
|  | 	"helm.sh/helm/v4/internal/plugin" | ||
|  | 	"helm.sh/helm/v4/pkg/cmd/require" | ||
|  | 	"helm.sh/helm/v4/pkg/provenance" | ||
|  | ) | ||
|  | 
 | ||
|  | const pluginPackageDesc = ` | ||
|  | This command packages a Helm plugin directory into a tarball. | ||
|  | 
 | ||
|  | By default, the command will generate a provenance file signed with a PGP key. | ||
|  | This ensures the plugin can be verified after installation. | ||
|  | 
 | ||
|  | Use --sign=false to skip signing (not recommended for distribution). | ||
|  | ` | ||
|  | 
 | ||
|  | type pluginPackageOptions struct { | ||
|  | 	sign           bool | ||
|  | 	keyring        string | ||
|  | 	key            string | ||
|  | 	passphraseFile string | ||
|  | 	pluginPath     string | ||
|  | 	destination    string | ||
|  | } | ||
|  | 
 | ||
|  | func newPluginPackageCmd(out io.Writer) *cobra.Command { | ||
|  | 	o := &pluginPackageOptions{} | ||
|  | 
 | ||
|  | 	cmd := &cobra.Command{ | ||
|  | 		Use:   "package [PATH]", | ||
|  | 		Short: "package a plugin directory into a plugin archive", | ||
|  | 		Long:  pluginPackageDesc, | ||
|  | 		Args:  require.ExactArgs(1), | ||
|  | 		RunE: func(_ *cobra.Command, args []string) error { | ||
|  | 			o.pluginPath = args[0] | ||
|  | 			return o.run(out) | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f := cmd.Flags() | ||
|  | 	f.BoolVar(&o.sign, "sign", true, "use a PGP private key to sign this plugin") | ||
|  | 	f.StringVar(&o.key, "key", "", "name of the key to use when signing. Used if --sign is true") | ||
|  | 	f.StringVar(&o.keyring, "keyring", defaultKeyring(), "location of a public keyring") | ||
|  | 	f.StringVar(&o.passphraseFile, "passphrase-file", "", "location of a file which contains the passphrase for the signing key. Use \"-\" to read from stdin.") | ||
|  | 	f.StringVarP(&o.destination, "destination", "d", ".", "location to write the plugin tarball.") | ||
|  | 
 | ||
|  | 	return cmd | ||
|  | } | ||
|  | 
 | ||
|  | func (o *pluginPackageOptions) run(out io.Writer) error { | ||
|  | 	// Check if the plugin path exists and is a directory
 | ||
|  | 	fi, err := os.Stat(o.pluginPath) | ||
|  | 	if err != nil { | ||
|  | 		return err | ||
|  | 	} | ||
|  | 	if !fi.IsDir() { | ||
|  | 		return fmt.Errorf("plugin package only supports directories, not tarballs") | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// Load and validate plugin metadata
 | ||
|  | 	pluginMeta, err := plugin.LoadDir(o.pluginPath) | ||
|  | 	if err != nil { | ||
|  | 		return fmt.Errorf("invalid plugin directory: %w", err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// Create destination directory if needed
 | ||
|  | 	if err := os.MkdirAll(o.destination, 0755); err != nil { | ||
|  | 		return err | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// If signing is requested, prepare the signer first
 | ||
|  | 	var signer *provenance.Signatory | ||
|  | 	if o.sign { | ||
|  | 		// Load the signing key
 | ||
|  | 		signer, err = provenance.NewFromKeyring(o.keyring, o.key) | ||
|  | 		if err != nil { | ||
|  | 			return fmt.Errorf("error reading from keyring: %w", err) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// Get passphrase
 | ||
|  | 		passphraseFetcher := o.promptUser | ||
|  | 		if o.passphraseFile != "" { | ||
|  | 			passphraseFetcher, err = o.passphraseFileFetcher() | ||
|  | 			if err != nil { | ||
|  | 				return err | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// Decrypt the key
 | ||
|  | 		if err := signer.DecryptKey(passphraseFetcher); err != nil { | ||
|  | 			return err | ||
|  | 		} | ||
|  | 	} else { | ||
|  | 		// User explicitly disabled signing
 | ||
|  | 		fmt.Fprintf(out, "WARNING: Skipping plugin signing. This is not recommended for plugins intended for distribution.\n") | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// Now create the tarball (only after signing prerequisites are met)
 | ||
|  | 	// Use plugin metadata for filename: PLUGIN_NAME-SEMVER.tgz
 | ||
|  | 	metadata := pluginMeta.Metadata() | ||
|  | 	filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version) | ||
|  | 	tarballPath := filepath.Join(o.destination, filename) | ||
|  | 
 | ||
|  | 	tarFile, err := os.Create(tarballPath) | ||
|  | 	if err != nil { | ||
|  | 		return fmt.Errorf("failed to create tarball: %w", err) | ||
|  | 	} | ||
|  | 	defer tarFile.Close() | ||
|  | 
 | ||
|  | 	if err := plugin.CreatePluginTarball(o.pluginPath, metadata.Name, tarFile); err != nil { | ||
|  | 		os.Remove(tarballPath) | ||
|  | 		return fmt.Errorf("failed to create plugin tarball: %w", err) | ||
|  | 	} | ||
|  | 	tarFile.Close() // Ensure file is closed before signing
 | ||
|  | 
 | ||
|  | 	// If signing was requested, sign the tarball
 | ||
|  | 	if o.sign { | ||
|  | 		// Sign the plugin tarball (not the source directory)
 | ||
|  | 		sig, err := plugin.SignPlugin(tarballPath, signer) | ||
|  | 		if err != nil { | ||
|  | 			os.Remove(tarballPath) | ||
|  | 			return fmt.Errorf("failed to sign plugin: %w", err) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// Write the signature
 | ||
|  | 		provFile := tarballPath + ".prov" | ||
|  | 		if err := os.WriteFile(provFile, []byte(sig), 0644); err != nil { | ||
|  | 			os.Remove(tarballPath) | ||
|  | 			return err | ||
|  | 		} | ||
|  | 
 | ||
|  | 		fmt.Fprintf(out, "Successfully signed. Signature written to: %s\n", provFile) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	fmt.Fprintf(out, "Successfully packaged plugin and saved it to: %s\n", tarballPath) | ||
|  | 
 | ||
|  | 	return nil | ||
|  | } | ||
|  | 
 | ||
|  | func (o *pluginPackageOptions) promptUser(name string) ([]byte, error) { | ||
|  | 	fmt.Printf("Password for key %q >  ", name) | ||
|  | 	pw, err := term.ReadPassword(int(syscall.Stdin)) | ||
|  | 	fmt.Println() | ||
|  | 	return pw, err | ||
|  | } | ||
|  | 
 | ||
|  | func (o *pluginPackageOptions) passphraseFileFetcher() (provenance.PassphraseFetcher, error) { | ||
|  | 	file, err := openPassphraseFile(o.passphraseFile, os.Stdin) | ||
|  | 	if err != nil { | ||
|  | 		return nil, err | ||
|  | 	} | ||
|  | 	defer file.Close() | ||
|  | 
 | ||
|  | 	// Read the entire passphrase
 | ||
|  | 	passphrase, err := io.ReadAll(file) | ||
|  | 	if err != nil { | ||
|  | 		return nil, err | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// Trim any trailing newline characters (both \n and \r\n)
 | ||
|  | 	passphrase = bytes.TrimRight(passphrase, "\r\n") | ||
|  | 
 | ||
|  | 	return func(_ string) ([]byte, error) { | ||
|  | 		return passphrase, nil | ||
|  | 	}, nil | ||
|  | } | ||
|  | 
 | ||
|  | // copied from action.openPassphraseFile
 | ||
|  | // TODO: should we move this to pkg/action so we can reuse the func from there?
 | ||
|  | func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) { | ||
|  | 	if passphraseFile == "-" { | ||
|  | 		stat, err := stdin.Stat() | ||
|  | 		if err != nil { | ||
|  | 			return nil, err | ||
|  | 		} | ||
|  | 		if (stat.Mode() & os.ModeNamedPipe) == 0 { | ||
|  | 			return nil, errors.New("specified reading passphrase from stdin, without input on stdin") | ||
|  | 		} | ||
|  | 		return stdin, nil | ||
|  | 	} | ||
|  | 	return os.Open(passphraseFile) | ||
|  | } |