Skip to content

Commit f326e34

Browse files
committed
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
LMCROSSITXSADEPLOY-2301
1 parent 48392da commit f326e34

5 files changed

Lines changed: 634 additions & 9 deletions

File tree

commands/deploy_command.go

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package commands
22

33
import (
44
"bufio"
5+
"crypto/rand"
56
"encoding/base64"
7+
"encoding/json"
68
"errors"
79
"flag"
810
"fmt"
@@ -23,6 +25,7 @@ import (
2325
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands/retrier"
2426
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/configuration"
2527
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/log"
28+
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/secure_parameters"
2629
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui"
2730
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/util"
2831
"gopkg.in/cheggaaa/pb.v1"
@@ -53,6 +56,7 @@ const (
5356
applyNamespaceAsSuffix = "apply-namespace-as-suffix"
5457
maxNamespaceSize = 36
5558
shouldBackupPreviousVersionOpt = "backup-previous-version"
59+
requireSecureParameters = "require-secure-parameters"
5660
)
5761

5862
type listFlag struct {
@@ -105,13 +109,13 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command {
105109
UsageDetails: plugin.Usage{
106110
Usage: `Deploy a multi-target app archive
107111
108-
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]
112+
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]
109113
110114
Perform action on an active deploy operation
111115
cf deploy -i OPERATION_ID -a ACTION [-u URL]
112116
113-
Deploy a multi-target app archive referenced by a remote URL
114-
<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,
117+
(EXPERIMENTAL) Deploy a multi-target app archive referenced by a remote URL
118+
<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,
115119

116120
Options: map[string]string{
117121
extDescriptorsOpt: "Extension descriptors",
@@ -146,6 +150,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command {
146150
util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds",
147151
util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds",
148152
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",
153+
util.GetShortOption(requireSecureParameters): "Pass secrets to the deploy service in a secure way",
149154
},
150155
},
151156
}
@@ -171,6 +176,7 @@ func deployProcessParametersSetter() ProcessParametersSetter {
171176
processBuilder.Parameter("appsStageTimeout", GetStringOpt(stageTimeoutOpt, flags))
172177
processBuilder.Parameter("appsUploadTimeout", GetStringOpt(uploadTimeoutOpt, flags))
173178
processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags))
179+
processBuilder.Parameter("isSecurityEnabled", strconv.FormatBool(GetBoolOpt(requireSecureParameters, flags)))
174180

175181
var lastSetValue string = ""
176182
for i := 0; i < len(os.Args); i++ {
@@ -225,6 +231,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) {
225231
flags.String(uploadTimeoutOpt, "", "")
226232
flags.String(taskExecutionTimeoutOpt, "", "")
227233
flags.Bool(shouldBackupPreviousVersionOpt, false, "")
234+
flags.Bool(requireSecureParameters, false, "")
228235
}
229236

230237
func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus {
@@ -348,6 +355,48 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
348355
return Failure
349356
}
350357

358+
if GetBoolOpt(requireSecureParameters, flags) {
359+
// Collect special ENVs: __MTA___<name>, __MTA_JSON___<name>, __MTA_CERT___<name>
360+
parameters, err := secure_parameters.CollectFromEnv("__MTA")
361+
if err != nil {
362+
ui.Failed("Secure parameters error: %s", err)
363+
return Failure
364+
}
365+
366+
if len(parameters) == 0 {
367+
ui.Failed("No secure parameters found in environment. Set variables like __MTA___<name>, __MTA_JSON___<name>, or __MTA_CERT___<name>.")
368+
return Failure
369+
}
370+
371+
userProvidedServiceName := getUpsName(mtaId, namespace)
372+
373+
isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName, "v1")
374+
if err != nil {
375+
ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err)
376+
return Failure
377+
}
378+
379+
if isUpsCreated {
380+
ui.Say("Created user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName))
381+
} else {
382+
ui.Say("Using existing user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName))
383+
}
384+
385+
schemaVer := ""
386+
yamlBytes, err := secure_parameters.BuildSecureExtension(parameters, mtaId, schemaVer)
387+
if err != nil {
388+
ui.Failed("Could not build secure extension: %s", err)
389+
return Failure
390+
}
391+
392+
secureFileID, err := fileUploader.UploadBytes("__mta.secure.mtaext", yamlBytes)
393+
if err != nil {
394+
ui.Failed("Could not upload secure extension: %s", err)
395+
return Failure
396+
}
397+
uploadedExtDescriptorIDs = append(uploadedExtDescriptorIDs, secureFileID)
398+
}
399+
351400
// Build the process instance
352401
processBuilder := NewDeploymentStrategy(flags, c.processTypeProvider).CreateProcessBuilder()
353402
processBuilder.Namespace(namespace)
@@ -374,6 +423,77 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
374423
return executionMonitor.Monitor()
375424
}
376425

426+
func getUpsName(mtaID, namespace string) string {
427+
if strings.TrimSpace(namespace) == "" {
428+
return "__mta-secure-" + mtaID
429+
}
430+
return "__mta-secure-" + mtaID + "-" + namespace
431+
}
432+
433+
func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName, keyID string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) {
434+
doesUpsExist, err := c.doesUpsExist(userProvidedServiceName)
435+
if err != nil {
436+
return false, "", fmt.Errorf("Check if the UPS exists: %w", err)
437+
}
438+
if doesUpsExist {
439+
return false, "", nil
440+
}
441+
442+
encryptionKey, err := getRandomEncryptionKey()
443+
if err != nil {
444+
return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err)
445+
}
446+
447+
upsCredentials := map[string]string{
448+
"encryptionKey": encryptionKey,
449+
"keyId": keyID,
450+
}
451+
jsonBody, _ := json.Marshal(upsCredentials)
452+
453+
if _, err := c.cliConnection.CliCommand("create-user-provided-service", userProvidedServiceName, "-p", string(jsonBody)); err != nil {
454+
return false, "", fmt.Errorf("Command cf cups %s has failed: %w", userProvidedServiceName, err)
455+
}
456+
return true, encryptionKey, nil
457+
}
458+
459+
func getRandomEncryptionKey() (string, error) {
460+
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
461+
462+
encryptionKeyBytes := make([]byte, 32)
463+
if _, err := rand.Read(encryptionKeyBytes); err != nil {
464+
return "", err
465+
}
466+
467+
for i := range encryptionKeyBytes {
468+
encryptionKeyBytes[i] = alphabet[int(encryptionKeyBytes[i]&63)]
469+
}
470+
471+
return string(encryptionKeyBytes), nil
472+
}
473+
474+
func (c *DeployCommand) doesUpsExist(userProvidedServiceName string) (bool, error) {
475+
servicesOutput, err := c.cliConnection.CliCommandWithoutTerminalOutput("services")
476+
if err != nil {
477+
return false, fmt.Errorf("Error while checking if the UPS for secure encryption exists: %w", err)
478+
}
479+
stringTable := strings.Join(servicesOutput, "\n")
480+
return findServiceName(stringTable, userProvidedServiceName), nil
481+
}
482+
483+
func findServiceName(servicesOutput, userProvidedServiceName string) bool {
484+
userProvidedServiceNameToLower := strings.ToLower(userProvidedServiceName)
485+
for _, currentLine := range strings.Split(servicesOutput, "\n") {
486+
fields := strings.Fields(currentLine)
487+
if len(fields) == 0 {
488+
continue
489+
}
490+
if strings.ToLower(fields[0]) == userProvidedServiceNameToLower {
491+
return true
492+
}
493+
}
494+
return false
495+
}
496+
377497
func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) {
378498
switch castedMtaArchive := rawMtaArchive.(type) {
379499
case *url.URL:

commands/deploy_command_test.go

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ var _ = Describe("DeployCommand", func() {
7878
const testArchive = "mtaArchive.mtar"
7979
const mtaArchivePath = testFilesLocation + testArchive
8080
const extDescriptorPath = testFilesLocation + "extDescriptor.mtaext"
81+
const userProvidedServiceSecurityRelated = "__mta-secure-anatz"
8182

8283
var name string
8384
var cliConnection *plugin_fakes.FakeCliConnection
@@ -105,7 +106,7 @@ var _ = Describe("DeployCommand", func() {
105106
}
106107
}
107108

108-
var getOutputLines = func(extDescriptor, processAborted, fromUrl bool) []string {
109+
var getOutputLines = func(extDescriptor, processAborted, fromUrl, existentUserProvidedServiceSecurity, createdUserProvidedServiceSecurity bool) []string {
109110
var lines []string
110111
mtaNameToPrint := mtaArchivePath
111112
if fromUrl {
@@ -134,6 +135,14 @@ var _ = Describe("DeployCommand", func() {
134135
" "+fullExtDescriptorPath,
135136
"OK")
136137
}
138+
if existentUserProvidedServiceSecurity {
139+
lines = append(lines,
140+
"Using existing user-provided service "+userProvidedServiceSecurityRelated+" for secure parameters.")
141+
}
142+
if createdUserProvidedServiceSecurity {
143+
lines = append(lines,
144+
"Created user-provided service "+userProvidedServiceSecurityRelated+" for secure parameters.")
145+
}
137146
lines = append(lines,
138147
"Test message",
139148
"Process finished.",
@@ -246,7 +255,7 @@ var _ = Describe("DeployCommand", func() {
246255
output, status := oc.CaptureOutputAndStatus(func() int {
247256
return command.Execute([]string{}).ToInt()
248257
})
249-
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true))
258+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, false, false))
250259
})
251260
})
252261

@@ -348,7 +357,7 @@ var _ = Describe("DeployCommand", func() {
348357
output, status := oc.CaptureOutputAndStatus(func() int {
349358
return command.Execute([]string{mtaArchivePath}).ToInt()
350359
})
351-
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
360+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
352361
// operation := mtaClient.StartMtaOperationArgsForCall(1)
353362
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
354363
})
@@ -360,7 +369,7 @@ var _ = Describe("DeployCommand", func() {
360369
output, status := oc.CaptureOutputAndStatus(func() int {
361370
return command.Execute([]string{mtaArchivePath, "-e", extDescriptorPath}).ToInt()
362371
})
363-
ex.ExpectSuccessWithOutput(status, output, getOutputLines(true, false, false))
372+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(true, false, false, false, false))
364373
// operation := mtaClient.StartMtaOperationArgsForCall(1)
365374
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
366375
})
@@ -372,7 +381,7 @@ var _ = Describe("DeployCommand", func() {
372381
output, status := oc.CaptureOutputAndStatus(func() int {
373382
return command.Execute([]string{mtaArchivePath, "-f", "-delete-services", "-no-start", "-keep-files", "-do-not-fail-on-missing-permissions"}).ToInt()
374383
})
375-
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
384+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
376385
// operation := mtaClient.StartMtaOperationArgsForCall(1)
377386
// expectProcessParameters(getProcessParameters(true), operation.Parameters)
378387
})
@@ -412,7 +421,7 @@ var _ = Describe("DeployCommand", func() {
412421
output, status := oc.CaptureOutputAndStatus(func() int {
413422
return command.Execute([]string{mtaArchivePath}).ToInt()
414423
})
415-
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
424+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
416425
// operation := mtaClient.StartMtaOperationArgsForCall(1)
417426
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
418427
})
@@ -494,5 +503,101 @@ var _ = Describe("DeployCommand", func() {
494503
ex.ExpectSuccessWithOutput(status, output, getLinesForAbortingProcess())
495504
})
496505
})
506+
507+
Context("with --require-secure-parameters flag and a user-provided service instance which already exists", func() {
508+
It("should not create a new user-provided service", func() {
509+
os.Setenv("__MTA___fake-variable", "fakeSecret")
510+
defer os.Unsetenv("__MTA___fake-variable")
511+
command.FileUrlReader = newMockFileReader(correctMtaUrl)
512+
513+
upsName := "__mta-secure-anatz"
514+
cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
515+
if len(args) > 0 && args[0] == "services" {
516+
table := fmt.Sprintf("%s user-provided fake-plan\nanother-service-instance managed fake-plan\n", upsName)
517+
return []string{table}, nil
518+
}
519+
return []string{}, nil
520+
}
521+
522+
output, status := oc.CaptureOutputAndStatus(func() int {
523+
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
524+
})
525+
526+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, true, false))
527+
Expect(output).To(ContainElement(ContainSubstring("Using existing user-provided service")))
528+
Expect(output).To(ContainElement(ContainSubstring(upsName)))
529+
530+
callCount := mtaClient.StartMtaOperationCallCount()
531+
Expect(callCount).To(BeNumerically(">", 0))
532+
operation := mtaClient.StartMtaOperationArgsForCall(callCount - 1)
533+
Expect(operation.Parameters["isSecurityEnabled"]).To(Equal("true"))
534+
})
535+
})
536+
537+
Context("with --require-secure-parameters flag and a user-provided service instance missing", func() {
538+
It("should create a new user-provided service using the appropriate cf command", func() {
539+
os.Setenv("__MTA___fake-variable", "fakeSecret")
540+
defer os.Unsetenv("__MTA___fake-variable")
541+
command.FileUrlReader = newMockFileReader(correctMtaUrl)
542+
543+
cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
544+
if len(args) > 0 && args[0] == "services" {
545+
return []string{"another-service-instance managed fake-plan\n"}, nil
546+
}
547+
return []string{}, nil
548+
}
549+
550+
cliConnection.CliCommandStub = func(args ...string) ([]string, error) {
551+
if len(args) > 0 && args[0] == "create-user-provided-service" {
552+
return []string{}, nil
553+
}
554+
return []string{}, nil
555+
}
556+
557+
output, status := oc.CaptureOutputAndStatus(func() int {
558+
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
559+
})
560+
561+
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, false, true))
562+
Expect(output).To(ContainElement(ContainSubstring("Created user-provided service")))
563+
Expect(output).To(ContainElement(ContainSubstring("__mta-secure-anatz")))
564+
565+
callCount := mtaClient.StartMtaOperationCallCount()
566+
Expect(callCount).To(BeNumerically(">", 0))
567+
operation := mtaClient.StartMtaOperationArgsForCall(callCount - 1)
568+
Expect(operation.Parameters["isSecurityEnabled"]).To(Equal("true"))
569+
})
570+
})
571+
572+
Context("with --require-secure-parameters and `cf services` fails", func() {
573+
It("should return an error from the UPS existence check", func() {
574+
os.Setenv("__MTA___fake-variable", "fakeSecret")
575+
defer os.Unsetenv("__MTA___fake-variable")
576+
command.FileUrlReader = newMockFileReader(correctMtaUrl)
577+
578+
cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
579+
if len(args) > 0 && args[0] == "services" {
580+
return []string{"another-service-instance managed fake-plan\n"}, nil
581+
}
582+
return []string{}, nil
583+
}
584+
585+
cliConnection.CliCommandStub = func(args ...string) ([]string, error) {
586+
if len(args) > 0 && args[0] == "create-user-provided-service" {
587+
return nil, fmt.Errorf("error - could not be created")
588+
}
589+
return []string{}, nil
590+
}
591+
592+
output, status := oc.CaptureOutputAndStatus(func() int {
593+
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
594+
})
595+
596+
ex.ExpectFailure(status, output, "")
597+
Expect(output).To(ContainElement(ContainSubstring("Could not ensure user-provided service")))
598+
Expect(mtaClient.StartMtaOperationCallCount()).To(Equal(0))
599+
})
600+
})
601+
497602
})
498603
})

0 commit comments

Comments
 (0)