2025-08-30 13:25:28 -04:00
/ *
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 {
2025-08-26 23:19:54 -04:00
// Read the tarball data
tarballData , err := os . ReadFile ( tarballPath )
if err != nil {
os . Remove ( tarballPath )
return fmt . Errorf ( "failed to read tarball for signing: %w" , err )
}
// Sign the plugin tarball data
sig , err := plugin . SignPlugin ( tarballData , filepath . Base ( tarballPath ) , signer )
2025-08-30 13:25:28 -04:00
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 )
}