-
Notifications
You must be signed in to change notification settings - Fork 42
Adding a new flag to the deploy command and the related new functionality in order to support the collecting of secrets and sending them to the backend #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ package commands | |
|
|
||
| import ( | ||
| "bufio" | ||
| "crypto/rand" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
|
|
@@ -18,11 +20,14 @@ import ( | |
| "code.cloudfoundry.org/cli/v8/cf/terminal" | ||
| "code.cloudfoundry.org/cli/v8/plugin" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient/resilient" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands/retrier" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/configuration" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/log" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/secure_parameters" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui" | ||
| "github.com/cloudfoundry-incubator/multiapps-cli-plugin/util" | ||
| "gopkg.in/cheggaaa/pb.v1" | ||
|
|
@@ -53,6 +58,7 @@ const ( | |
| applyNamespaceAsSuffix = "apply-namespace-as-suffix" | ||
| maxNamespaceSize = 36 | ||
| shouldBackupPreviousVersionOpt = "backup-previous-version" | ||
| requireSecureParameters = "require-secure-parameters" | ||
| ) | ||
|
|
||
| type listFlag struct { | ||
|
|
@@ -87,16 +93,23 @@ type DeployCommand struct { | |
|
|
||
| FileUrlReader fs.File | ||
| FileUrlReadTimeout time.Duration | ||
| CfClient cfrestclient.CloudFoundryOperationsExtended | ||
| } | ||
|
|
||
| // NewDeployCommand creates a new deploy command. | ||
| func NewDeployCommand() *DeployCommand { | ||
| baseCmd := &BaseCommand{flagsParser: deployCommandLineArgumentsParser{}, flagsValidator: deployCommandFlagsValidator{}} | ||
| deployCmd := &DeployCommand{baseCmd, deployProcessParametersSetter(), &deployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second} | ||
| deployCmd := &DeployCommand{baseCmd, deployProcessParametersSetter(), &deployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second, nil} | ||
| baseCmd.Command = deployCmd | ||
| return deployCmd | ||
| } | ||
|
|
||
| func (c *DeployCommand) Initialize(name string, cliConnection plugin.CliConnection) { | ||
| c.BaseCommand.Initialize(name, cliConnection) | ||
| delegate := cfrestclient.NewCloudFoundryRestClient(cliConnection) | ||
| c.CfClient = resilient.NewResilientCloudFoundryClient(delegate, maxRetriesCount, retryIntervalInSeconds) | ||
| } | ||
|
|
||
| // GetPluginCommand returns the plugin command details | ||
| func (c *DeployCommand) GetPluginCommand() plugin.Command { | ||
| return plugin.Command{ | ||
|
|
@@ -105,13 +118,13 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { | |
| UsageDetails: plugin.Usage{ | ||
| Usage: `Deploy a multi-target app archive | ||
|
|
||
| cf deploy MTA [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u URL] [-f] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT] | ||
| cf deploy MTA [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u URL] [-f] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--require-secure-parameters] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT] | ||
|
|
||
| Perform action on an active deploy operation | ||
| cf deploy -i OPERATION_ID -a ACTION [-u URL] | ||
|
|
||
| Deploy a multi-target app archive referenced by a remote URL | ||
| <write MTA archive URL to STDOUT> | cf deploy [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u MTA_CONTROLLER_URL] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]` + util.UploadEnvHelpText, | ||
| <write MTA archive URL to STDOUT> | cf deploy [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u MTA_CONTROLLER_URL] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [require-secure-parameters] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]` + util.UploadEnvHelpText, | ||
|
|
||
| Options: map[string]string{ | ||
| extDescriptorsOpt: "Extension descriptors", | ||
|
|
@@ -146,6 +159,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { | |
| util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", | ||
| util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", | ||
| util.GetShortOption(shouldBackupPreviousVersionOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Backup previous version of applications, use new cli command \"rollback-mta\" to rollback to the previous version", | ||
| util.GetShortOption(requireSecureParameters): "(EXPERIMENTAL) Pass secrets to the deploy service in a secure way", | ||
| }, | ||
| }, | ||
| } | ||
|
|
@@ -171,6 +185,7 @@ func deployProcessParametersSetter() ProcessParametersSetter { | |
| processBuilder.Parameter("appsStageTimeout", GetStringOpt(stageTimeoutOpt, flags)) | ||
| processBuilder.Parameter("appsUploadTimeout", GetStringOpt(uploadTimeoutOpt, flags)) | ||
| processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags)) | ||
| processBuilder.Parameter("isSecurityEnabled", strconv.FormatBool(GetBoolOpt(requireSecureParameters, flags))) | ||
|
|
||
| var lastSetValue string = "" | ||
| for i := 0; i < len(os.Args); i++ { | ||
|
|
@@ -225,6 +240,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) { | |
| flags.String(uploadTimeoutOpt, "", "") | ||
| flags.String(taskExecutionTimeoutOpt, "", "") | ||
| flags.Bool(shouldBackupPreviousVersionOpt, false, "") | ||
| flags.Bool(requireSecureParameters, false, "") | ||
| } | ||
|
|
||
| func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { | ||
|
|
@@ -271,6 +287,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, | |
| sequentialUpload := conf.GetUploadChunksSequentially() | ||
| disableProgressBar := conf.GetDisableUploadProgressBar() | ||
| fileUploader := NewFileUploader(mtaClient, namespace, uploadChunkSize, sequentialUpload, disableProgressBar) | ||
| var yamlBytes []byte | ||
|
|
||
| if isUrl { | ||
| var fileId string | ||
|
|
@@ -321,6 +338,41 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, | |
| return Failure | ||
| } | ||
|
|
||
| if GetBoolOpt(requireSecureParameters, flags) { | ||
| // Collect special ENVs: __MTA___<name>, __MTA_JSON___<name>, __MTA_CERT___<name> | ||
| parameters, err := secure_parameters.CollectFromEnv("__MTA") | ||
| if err != nil { | ||
| ui.Failed("Secure parameters error: %s", err) | ||
| return Failure | ||
| } | ||
|
|
||
| if len(parameters) == 0 { | ||
| ui.Failed("No secure parameters found in environment. Set variables like __MTA___<name>, __MTA_JSON___<name>, or __MTA_CERT___<name>.") | ||
| return Failure | ||
| } | ||
|
|
||
| userProvidedServiceName := getUpsName(mtaId, namespace) | ||
|
|
||
| isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName, "v1") | ||
| if err != nil { | ||
| ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err) | ||
| return Failure | ||
| } | ||
|
|
||
| if isUpsCreated { | ||
| ui.Say("Created user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName)) | ||
| } else { | ||
| ui.Say("Using existing user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName)) | ||
| } | ||
|
|
||
| schemaVer := descriptor.SchemaVersion | ||
| yamlBytes, err = secure_parameters.BuildSecureExtension(parameters, mtaId, schemaVer) | ||
| if err != nil { | ||
| ui.Failed("Could not build secure extension: %s", err) | ||
| return Failure | ||
| } | ||
| } | ||
|
|
||
| // Upload the MTA archive file | ||
| uploadedArchivePartIds, uploadStatus = c.uploadFiles([]string{mtaArchivePath}, fileUploader) | ||
| if uploadStatus == Failure { | ||
|
|
@@ -348,6 +400,15 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, | |
| return Failure | ||
| } | ||
|
|
||
| if GetBoolOpt(requireSecureParameters, flags) { | ||
| secureFileID, err := fileUploader.UploadBytes("__mta.secure.mtaext", yamlBytes) | ||
| if err != nil { | ||
| ui.Failed("Could not upload secure extension: %s", err) | ||
| return Failure | ||
| } | ||
| uploadedExtDescriptorIDs = append(uploadedExtDescriptorIDs, secureFileID) | ||
| } | ||
|
|
||
| // Build the process instance | ||
| processBuilder := NewDeploymentStrategy(flags, c.processTypeProvider).CreateProcessBuilder() | ||
| processBuilder.Namespace(namespace) | ||
|
|
@@ -374,6 +435,72 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, | |
| return executionMonitor.Monitor() | ||
| } | ||
|
|
||
| func getUpsName(mtaID, namespace string) string { | ||
| if strings.TrimSpace(namespace) == "" { | ||
| return "__mta-secure-" + mtaID | ||
| } | ||
| return "__mta-secure-" + mtaID + "-" + namespace | ||
| } | ||
|
|
||
| func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName, keyID string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { | ||
| doesUpsExist, err := c.doesUpsExist(userProvidedServiceName) | ||
| if err != nil { | ||
| return false, "", fmt.Errorf("Check if the UPS exists: %w", err) | ||
| } | ||
| if doesUpsExist { | ||
| return false, "", nil | ||
| } | ||
|
|
||
| encryptionKey, err := getRandomEncryptionKey() | ||
| if err != nil { | ||
| return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err) | ||
| } | ||
|
|
||
| upsCredentials := map[string]string{ | ||
| "encryptionKey": encryptionKey, | ||
| "keyId": keyID, | ||
| } | ||
| jsonBody, _ := json.Marshal(upsCredentials) | ||
|
|
||
| if _, err := c.cliConnection.CliCommand("create-user-provided-service", userProvidedServiceName, "-p", string(jsonBody)); err != nil { | ||
| return false, "", fmt.Errorf("Command cf cups %s has failed: %w", userProvidedServiceName, err) | ||
| } | ||
| return true, encryptionKey, nil | ||
| } | ||
|
|
||
| func getRandomEncryptionKey() (string, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there some library that can do this for us?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could not manage to find anything in particular - saw that many use the base64 encoding which I tried but since for every 3 bytes it spits out 4 characters - I was getting 43 chars out of my 32 bytes. Also since I wanted the constraint of having only alphanumerical values and not any / or + which base64 brings, I decided the use the explicit "alphabet" of allowed symbols and then just randomly choose each character from it. |
||
| const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" | ||
|
|
||
| encryptionKeyBytes := make([]byte, 32) | ||
| if _, err := rand.Read(encryptionKeyBytes); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| for i := range encryptionKeyBytes { | ||
| encryptionKeyBytes[i] = alphabet[int(encryptionKeyBytes[i]&63)] | ||
| } | ||
|
|
||
| return string(encryptionKeyBytes), nil | ||
|
Comment on lines
+479
to
+483
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this really creates a proper 256 bits keys? Why not something like: func generateAES256Key() ([]byte, error) { Why is alphabet needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it does. I tried doing it like that myself in the beginning, but the problem was that the argument in the Sprintf method determines the encoding used, thus giving different lenghts as a result. Using the fmt.Sprinf("%x", key) with the "x" argument, Go does a base16 encoding which actually converts the 32 generated bytes into 64 characters. The base16 encoding doubles the size of the byte input when it is converted to a string output, since base16 uses only the letters from 'a' to 'f' and 1 through 9, 16 symbols altogether, and since one byte can be between 0-255 - in order to represent all the 256 different values we need to have for each of the 16 unique symbols, 16 more - meaning 16x16 = 16^2 = 256 |
||
| } | ||
|
|
||
| func (c *DeployCommand) doesUpsExist(userProvidedServiceName string) (bool, error) { | ||
| space, errSpace := c.cliConnection.GetCurrentSpace() | ||
| if errSpace != nil { | ||
| return false, fmt.Errorf("Cannot determine the current space") | ||
| } | ||
| spaceGuid := space.Guid | ||
|
|
||
| _, errServiceInstance := c.CfClient.GetServiceInstanceByName(userProvidedServiceName, spaceGuid) | ||
| if errServiceInstance != nil { | ||
| if errServiceInstance.Error() == "service instance not found" { | ||
| return false, nil | ||
| } | ||
| return false, fmt.Errorf("Error while checking if the UPS for secure encryption exists: %w", errServiceInstance) | ||
| } | ||
|
|
||
| return true, nil | ||
| } | ||
|
|
||
| func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) { | ||
| switch castedMtaArchive := rawMtaArchive.(type) { | ||
| case *url.URL: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the error from
json.Marshalignored here?