Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 123 additions & 3 deletions commands/deploy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package commands

import (
"bufio"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
Expand All @@ -23,6 +25,7 @@ import (
"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"
Expand Down Expand Up @@ -53,6 +56,7 @@ const (
applyNamespaceAsSuffix = "apply-namespace-as-suffix"
maxNamespaceSize = 36
shouldBackupPreviousVersionOpt = "backup-previous-version"
requireSecureParameters = "require-secure-parameters"
)

type listFlag struct {
Expand Down Expand Up @@ -105,13 +109,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,
(EXPERIMENTAL) 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] [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",
Expand Down Expand Up @@ -146,6 +150,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): "Pass secrets to the deploy service in a secure way",
},
},
}
Expand All @@ -171,6 +176,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++ {
Expand Down Expand Up @@ -225,6 +231,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 {
Expand Down Expand Up @@ -348,6 +355,48 @@ 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 := ""
yamlBytes, err := secure_parameters.BuildSecureExtension(parameters, mtaId, schemaVer)
if err != nil {
ui.Failed("Could not build secure extension: %s", err)
return Failure
}

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)
Expand All @@ -374,6 +423,77 @@ 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) {
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
}

func (c *DeployCommand) doesUpsExist(userProvidedServiceName string) (bool, error) {
servicesOutput, err := c.cliConnection.CliCommandWithoutTerminalOutput("services")
if err != nil {
return false, fmt.Errorf("Error while checking if the UPS for secure encryption exists: %w", err)
}
stringTable := strings.Join(servicesOutput, "\n")
return findServiceName(stringTable, userProvidedServiceName), nil
}

func findServiceName(servicesOutput, userProvidedServiceName string) bool {
userProvidedServiceNameToLower := strings.ToLower(userProvidedServiceName)
for _, currentLine := range strings.Split(servicesOutput, "\n") {
fields := strings.Fields(currentLine)
if len(fields) == 0 {
continue
}
if strings.ToLower(fields[0]) == userProvidedServiceNameToLower {
return true
}
}
return false
}

func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) {
switch castedMtaArchive := rawMtaArchive.(type) {
case *url.URL:
Expand Down
117 changes: 111 additions & 6 deletions commands/deploy_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ var _ = Describe("DeployCommand", func() {
const testArchive = "mtaArchive.mtar"
const mtaArchivePath = testFilesLocation + testArchive
const extDescriptorPath = testFilesLocation + "extDescriptor.mtaext"
const userProvidedServiceSecurityRelated = "__mta-secure-anatz"

var name string
var cliConnection *plugin_fakes.FakeCliConnection
Expand Down Expand Up @@ -105,7 +106,7 @@ var _ = Describe("DeployCommand", func() {
}
}

var getOutputLines = func(extDescriptor, processAborted, fromUrl bool) []string {
var getOutputLines = func(extDescriptor, processAborted, fromUrl, existentUserProvidedServiceSecurity, createdUserProvidedServiceSecurity bool) []string {
var lines []string
mtaNameToPrint := mtaArchivePath
if fromUrl {
Expand Down Expand Up @@ -134,6 +135,14 @@ var _ = Describe("DeployCommand", func() {
" "+fullExtDescriptorPath,
"OK")
}
if existentUserProvidedServiceSecurity {
lines = append(lines,
"Using existing user-provided service "+userProvidedServiceSecurityRelated+" for secure parameters.")
}
if createdUserProvidedServiceSecurity {
lines = append(lines,
"Created user-provided service "+userProvidedServiceSecurityRelated+" for secure parameters.")
}
lines = append(lines,
"Test message",
"Process finished.",
Expand Down Expand Up @@ -246,7 +255,7 @@ var _ = Describe("DeployCommand", func() {
output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{}).ToInt()
})
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true))
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, false, false))
})
})

Expand Down Expand Up @@ -348,7 +357,7 @@ var _ = Describe("DeployCommand", func() {
output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{mtaArchivePath}).ToInt()
})
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
// operation := mtaClient.StartMtaOperationArgsForCall(1)
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
})
Expand All @@ -360,7 +369,7 @@ var _ = Describe("DeployCommand", func() {
output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{mtaArchivePath, "-e", extDescriptorPath}).ToInt()
})
ex.ExpectSuccessWithOutput(status, output, getOutputLines(true, false, false))
ex.ExpectSuccessWithOutput(status, output, getOutputLines(true, false, false, false, false))
// operation := mtaClient.StartMtaOperationArgsForCall(1)
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
})
Expand All @@ -372,7 +381,7 @@ var _ = Describe("DeployCommand", func() {
output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{mtaArchivePath, "-f", "-delete-services", "-no-start", "-keep-files", "-do-not-fail-on-missing-permissions"}).ToInt()
})
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
// operation := mtaClient.StartMtaOperationArgsForCall(1)
// expectProcessParameters(getProcessParameters(true), operation.Parameters)
})
Expand Down Expand Up @@ -412,7 +421,7 @@ var _ = Describe("DeployCommand", func() {
output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{mtaArchivePath}).ToInt()
})
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false, false, false))
// operation := mtaClient.StartMtaOperationArgsForCall(1)
// expectProcessParameters(getProcessParameters(false), operation.Parameters)
})
Expand Down Expand Up @@ -494,5 +503,101 @@ var _ = Describe("DeployCommand", func() {
ex.ExpectSuccessWithOutput(status, output, getLinesForAbortingProcess())
})
})

Context("with --require-secure-parameters flag and a user-provided service instance which already exists", func() {
It("should not create a new user-provided service", func() {
os.Setenv("__MTA___fake-variable", "fakeSecret")
defer os.Unsetenv("__MTA___fake-variable")
command.FileUrlReader = newMockFileReader(correctMtaUrl)

upsName := "__mta-secure-anatz"
cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
if len(args) > 0 && args[0] == "services" {
table := fmt.Sprintf("%s user-provided fake-plan\nanother-service-instance managed fake-plan\n", upsName)
return []string{table}, nil
}
return []string{}, nil
}

output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
})

ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, true, false))
Expect(output).To(ContainElement(ContainSubstring("Using existing user-provided service")))
Expect(output).To(ContainElement(ContainSubstring(upsName)))

callCount := mtaClient.StartMtaOperationCallCount()
Expect(callCount).To(BeNumerically(">", 0))
operation := mtaClient.StartMtaOperationArgsForCall(callCount - 1)
Expect(operation.Parameters["isSecurityEnabled"]).To(Equal("true"))
})
})

Context("with --require-secure-parameters flag and a user-provided service instance missing", func() {
It("should create a new user-provided service using the appropriate cf command", func() {
os.Setenv("__MTA___fake-variable", "fakeSecret")
defer os.Unsetenv("__MTA___fake-variable")
command.FileUrlReader = newMockFileReader(correctMtaUrl)

cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
if len(args) > 0 && args[0] == "services" {
return []string{"another-service-instance managed fake-plan\n"}, nil
}
return []string{}, nil
}

cliConnection.CliCommandStub = func(args ...string) ([]string, error) {
if len(args) > 0 && args[0] == "create-user-provided-service" {
return []string{}, nil
}
return []string{}, nil
}

output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
})

ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true, false, true))
Expect(output).To(ContainElement(ContainSubstring("Created user-provided service")))
Expect(output).To(ContainElement(ContainSubstring("__mta-secure-anatz")))

callCount := mtaClient.StartMtaOperationCallCount()
Expect(callCount).To(BeNumerically(">", 0))
operation := mtaClient.StartMtaOperationArgsForCall(callCount - 1)
Expect(operation.Parameters["isSecurityEnabled"]).To(Equal("true"))
})
})

Context("with --require-secure-parameters and `cf services` fails", func() {
It("should return an error from the UPS existence check", func() {
os.Setenv("__MTA___fake-variable", "fakeSecret")
defer os.Unsetenv("__MTA___fake-variable")
command.FileUrlReader = newMockFileReader(correctMtaUrl)

cliConnection.CliCommandWithoutTerminalOutputStub = func(args ...string) ([]string, error) {
if len(args) > 0 && args[0] == "services" {
return []string{"another-service-instance managed fake-plan\n"}, nil
}
return []string{}, nil
}

cliConnection.CliCommandStub = func(args ...string) ([]string, error) {
if len(args) > 0 && args[0] == "create-user-provided-service" {
return nil, fmt.Errorf("error - could not be created")
}
return []string{}, nil
}

output, status := oc.CaptureOutputAndStatus(func() int {
return command.Execute([]string{"--require-secure-parameters"}).ToInt()
})

ex.ExpectFailure(status, output, "")
Expect(output).To(ContainElement(ContainSubstring("Could not ensure user-provided service")))
Expect(mtaClient.StartMtaOperationCallCount()).To(Equal(0))
})
})

})
})
Loading