Skip to content

Commit

Permalink
Merge pull request #55 from aris-bunnyshell/feat/secrets
Browse files Browse the repository at this point in the history
feat: implement Bunnyshell secrets
  • Loading branch information
aris-bunnyshell authored Jan 15, 2024
2 parents a7d2108 + 017a72b commit a9c7682
Show file tree
Hide file tree
Showing 14 changed files with 624 additions and 14 deletions.
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"bunnyshell.com/cli/cmd/project"
"bunnyshell.com/cli/cmd/project_variable"
"bunnyshell.com/cli/cmd/registry_integration"
"bunnyshell.com/cli/cmd/secret"
"bunnyshell.com/cli/cmd/template"
"bunnyshell.com/cli/cmd/utils"
"bunnyshell.com/cli/cmd/variable"
Expand Down Expand Up @@ -97,6 +98,7 @@ func init() {
variable.GetMainCommand(),
k8sIntegration.GetMainCommand(),
pipeline.GetMainCommand(),
secret.GetMainCommand(),
template.GetMainCommand(),
},
)
Expand Down
86 changes: 86 additions & 0 deletions cmd/secret/decrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package secret

import (
"errors"
"io"
"os"

"bunnyshell.com/cli/pkg/api/secret"
"bunnyshell.com/cli/pkg/build"
"bunnyshell.com/cli/pkg/config"
"bunnyshell.com/cli/pkg/lib"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)

var (
errMissingEncryptedExpression = errors.New("the encrypted expression must be provided")
errMultipleEncryptedExpressionInputs = errors.New("the encrypted expression must be provided either by argument or by stdin, not both")
)

var decryptCommandExample = heredoc.Docf(`
%[1]s%[2]s secret decrypt --organization dMVwZO5jGN "ENCRYPTED[F67baQQZ6XXHNxcmRz]"
%[1]scat encrypted.txt | %[2]s secret decrypt --organization dMVwZO5jGN
`, "\t", build.Name)

func init() {
settings := config.GetSettings()

decryptOptions := secret.NewDecryptOptions()

command := &cobra.Command{
Use: "decrypt <expression>",

Short: "Decrypts a secret expression of the given organization",
Example: decryptCommandExample,

Args: cobra.MaximumNArgs(1),
ArgAliases: []string{"expression"},
ValidArgsFunction: cobra.NoFileCompletions,

PreRunE: func(cmd *cobra.Command, args []string) error {
hasStdin, err := isStdinPresent()
if err != nil {
return err
}

if len(args) == 0 && !hasStdin {
return errMissingEncryptedExpression
}

if len(args) == 1 && hasStdin {
return errMultipleEncryptedExpressionInputs
}

return nil
},

RunE: func(cmd *cobra.Command, args []string) error {
if decryptOptions.Organization == "" {
decryptOptions.Organization = settings.Profile.Context.Organization
}

if len(args) == 1 {
decryptOptions.Expression = args[0]
} else {
buf, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}

decryptOptions.Expression = string(buf)
}

decryptedSecret, err := secret.Decrypt(decryptOptions)
if err != nil {
return lib.FormatCommandError(cmd, err)
}

return lib.FormatCommandData(cmd, decryptedSecret)
},
}

decryptOptions.UpdateSelfFlags(command.Flags())

mainCmd.AddCommand(command)
}
56 changes: 56 additions & 0 deletions cmd/secret/decrypt_definition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package secret

import (
"bunnyshell.com/cli/pkg/api/secret"
"bunnyshell.com/cli/pkg/build"
"bunnyshell.com/cli/pkg/lib"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)

var decryptDefinitionCommandExample = heredoc.Docf(`
%[1]s%[2]s secret decrypt-definition --organization dMVwZO5jGN --file plain.txt
%[1]scat plain.txt | %[2]s secret decrypt-definition --organization dMVwZO5jGN
`, "\t", build.Name)

var resolvedExpressions bool

func init() {
transcriptConfigurationOptions := secret.NewTranscriptConfigurationOptions()

command := &cobra.Command{
Use: "decrypt-definition",

Short: "Decrypts an environment definition for the given organization",
Example: decryptDefinitionCommandExample,

ValidArgsFunction: cobra.NoFileCompletions,

PreRunE: func(cmd *cobra.Command, args []string) error {
return validateDefinitionCommand(transcriptConfigurationOptions)
},

RunE: func(cmd *cobra.Command, args []string) error {
mode := secret.TranscriptModeExposed
if resolvedExpressions {
mode = secret.TranscriptModeResolved
}

decryptedDefinition, err := executeTranscriptConfiguration(transcriptConfigurationOptions, mode)
if err != nil {
return lib.FormatCommandError(cmd, err)
}

cmd.Println(decryptedDefinition)

return nil
},
}

flags := command.Flags()

transcriptConfigurationOptions.UpdateSelfFlags(flags)
flags.BoolVar(&resolvedExpressions, "resolved", false, "Resolve the expressions and include directly the value")

mainCmd.AddCommand(command)
}
108 changes: 108 additions & 0 deletions cmd/secret/definition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package secret

import (
"errors"
"io"
"os"

"bunnyshell.com/cli/pkg/api/secret"
"bunnyshell.com/cli/pkg/config"
"bunnyshell.com/cli/pkg/util"
)

var (
errYamlBlankValue = errors.New("the environment definition provided is blank")
errYamlMissingValue = errors.New("the environment definition must be provided")
errYamlFileNotFound = errors.New("the provided filepath does not exist")
errMultipleYamlValueInputs = errors.New("the environment definition must be provided either by argument or by stdin, not both")
)

func executeTranscriptConfiguration(options *secret.TranscriptConfigurationOptions, mode secret.TranscriptMode) (string, error) {
settings := config.GetSettings()

options.Mode = string(mode)
if options.Organization == "" {
options.Organization = settings.Profile.Context.Organization
}

err := loadDefinition(options)
if err != nil {
return "", err
}

if options.Yaml == "" {
return "", errYamlBlankValue
}

resultedDefinition, err := secret.TranscriptConfiguration(options)
if err != nil {
return "", err
}

return string(resultedDefinition.Bytes), nil
}

func validateDefinitionCommand(options *secret.TranscriptConfigurationOptions) error {
hasStdin, err := isStdinPresent()
if err != nil {
return err
}

if options.DefinitionFilePath == "" && !hasStdin {
return errYamlMissingValue
}

if options.DefinitionFilePath != "" {
if hasStdin {
return errMultipleYamlValueInputs
}

exists, err := util.FileExists(options.DefinitionFilePath)
if err != nil {
return err
}
if !exists {
return errYamlFileNotFound
}
}

return nil
}

func loadDefinition(options *secret.TranscriptConfigurationOptions) error {
if options.DefinitionFilePath != "" {
contents, err := loadDefinitionFromFile(options.DefinitionFilePath)
if err != nil {
return err
}

options.Yaml = contents
} else {
buf, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}

options.Yaml = string(buf)
}

return nil
}

func loadDefinitionFromFile(filepath string) (string, error) {
exists, err := util.FileExists(filepath)
if err != nil {
return "", err
}

if !exists {
return "", errors.New("the provided file does not exist")
}

fileContents, err := os.ReadFile(filepath)
if err != nil {
return "", err
}

return string(fileContents), nil
}
86 changes: 86 additions & 0 deletions cmd/secret/encrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package secret

import (
"errors"
"io"
"os"

"bunnyshell.com/cli/pkg/api/secret"
"bunnyshell.com/cli/pkg/build"
"bunnyshell.com/cli/pkg/config"
"bunnyshell.com/cli/pkg/lib"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)

var (
errMissingValue = errors.New("the plain value must be provided")
errMultipleValueInputs = errors.New("the value must be provided either by argument or by stdin, not both")
)

var encryptCommandExample = heredoc.Docf(`
%[1]s%[2]s secret encrypt --organization dMVwZO5jGN "my plain secret value"
%[1]scat plain.txt | %[2]s secret encrypt --organization dMVwZO5jGN
`, "\t", build.Name)

func init() {
settings := config.GetSettings()

encryptOptions := secret.NewEncryptOptions()

command := &cobra.Command{
Use: "encrypt <value>",

Short: "Encrypts a secret for the given organization",
Example: encryptCommandExample,

Args: cobra.MaximumNArgs(1),
ArgAliases: []string{"value"},
ValidArgsFunction: cobra.NoFileCompletions,

PreRunE: func(cmd *cobra.Command, args []string) error {
hasStdin, err := isStdinPresent()
if err != nil {
return err
}

if len(args) == 0 && !hasStdin {
return errMissingValue
}

if len(args) == 1 && hasStdin {
return errMultipleValueInputs
}

return nil
},

RunE: func(cmd *cobra.Command, args []string) error {
if encryptOptions.Organization == "" {
encryptOptions.Organization = settings.Profile.Context.Organization
}

if len(args) == 1 {
encryptOptions.PlainText = args[0]
} else {
buf, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}

encryptOptions.PlainText = string(buf)
}

encryptedSecret, err := secret.Encrypt(encryptOptions)
if err != nil {
return lib.FormatCommandError(cmd, err)
}

return lib.FormatCommandData(cmd, encryptedSecret)
},
}

encryptOptions.UpdateSelfFlags(command.Flags())

mainCmd.AddCommand(command)
}
Loading

0 comments on commit a9c7682

Please sign in to comment.