From 9b5f9480885005ccb754c2bcf17f9dc5f43aff65 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Tue, 16 Dec 2025 16:05:40 +0200 Subject: [PATCH 1/7] 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 --- commands/deploy_command.go | 126 ++++++++++- commands/deploy_command_test.go | 117 +++++++++- commands/file_uploader.go | 27 +++ secure_parameters/secure_parameters_test.go | 224 ++++++++++++++++++++ secure_parameters/secure_parametes.go | 149 +++++++++++++ 5 files changed, 634 insertions(+), 9 deletions(-) create mode 100644 secure_parameters/secure_parameters_test.go create mode 100644 secure_parameters/secure_parametes.go diff --git a/commands/deploy_command.go b/commands/deploy_command.go index ea1f91e..0658df5 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -2,7 +2,9 @@ package commands import ( "bufio" + "crypto/rand" "encoding/base64" + "encoding/json" "errors" "flag" "fmt" @@ -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" @@ -53,6 +56,7 @@ const ( applyNamespaceAsSuffix = "apply-namespace-as-suffix" maxNamespaceSize = 36 shouldBackupPreviousVersionOpt = "backup-previous-version" + requireSecureParameters = "require-secure-parameters" ) type listFlag struct { @@ -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 - | 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 + | 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", @@ -147,6 +151,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { 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(dependencyAwareStopOrderOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Stop apps in a dependency-aware order during the resume phase of a blue-green deployment", + util.GetShortOption(requireSecureParameters): "Pass secrets to the deploy service in a secure way", }, }, } @@ -172,6 +177,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++ { @@ -227,6 +233,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) { flags.String(taskExecutionTimeoutOpt, "", "") flags.Bool(shouldBackupPreviousVersionOpt, false, "") flags.Bool(dependencyAwareStopOrderOpt, false, "") + flags.Bool(requireSecureParameters, false, "") } func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { @@ -350,6 +357,48 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return Failure } + if GetBoolOpt(requireSecureParameters, flags) { + // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ + 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___, __MTA_JSON___, or __MTA_CERT___.") + 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) @@ -376,6 +425,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: diff --git a/commands/deploy_command_test.go b/commands/deploy_command_test.go index e89587a..0963785 100644 --- a/commands/deploy_command_test.go +++ b/commands/deploy_command_test.go @@ -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 @@ -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 { @@ -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.", @@ -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)) }) }) @@ -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) }) @@ -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) }) @@ -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) }) @@ -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) }) @@ -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)) + }) + }) + }) }) diff --git a/commands/file_uploader.go b/commands/file_uploader.go index 3320b20..28d7258 100644 --- a/commands/file_uploader.go +++ b/commands/file_uploader.go @@ -1,6 +1,7 @@ package commands import ( + "bytes" "fmt" "io" "os" @@ -63,6 +64,23 @@ func (r *progressBarReader) Close() error { return nil } +type namedBytesReader struct { + r *bytes.Reader + fileName string +} + +func (n *namedBytesReader) Read(p []byte) (int, error) { + return n.r.Read(p) +} + +func (n *namedBytesReader) Seek(o int64, w int) (int64, error) { + return n.r.Seek(o, w) +} + +func (n *namedBytesReader) Name() string { + return n.fileName +} + // NewFileUploader creates a new file uploader for the specified namespace func NewFileUploader(mtaClient mtaclient.MtaClientOperations, namespace string, uploadChunkSizeInMB uint64, sequentialUpload, shouldDisableProgressBar bool) *FileUploader { @@ -249,3 +267,12 @@ func (f *FileUploader) isFileAlreadyUploaded(newFilePath string, fileInfo os.Fil } return false } + +func (f *FileUploader) UploadBytes(filename string, content []byte) (string, error) { + nb := &namedBytesReader{r: bytes.NewReader(content), fileName: filename} + uploadedFile, err := f.mtaClient.UploadMtaFile(nb, int64(len(content)), &f.namespace) + if err != nil { + return "", fmt.Errorf("Could not upload in-memory file %s: %w", filename, err) + } + return uploadedFile.ID, nil +} diff --git a/secure_parameters/secure_parameters_test.go b/secure_parameters/secure_parameters_test.go new file mode 100644 index 0000000..b330620 --- /dev/null +++ b/secure_parameters/secure_parameters_test.go @@ -0,0 +1,224 @@ +package secure_parameters + +import ( + "encoding/base64" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func setEnv(t *testing.T, nameOfEnv, valueOfEnv string) { + t.Helper() + t.Setenv(nameOfEnv, valueOfEnv) +} + +func TestCollectFromEnv(t *testing.T) { + setEnv(t, "__MTA___fakePassword", "secretValue") + setEnv(t, "__MTA_JSON___fakeJson", `{"a":1,"b":"secretValueJson"}`) + testCertificate := "-----BEGIN CERTIFICATE-----\nMIBgNVBAYTAPXwBc63heW9WrP3qnDEm+UZE4V0Au7OWnOeiobq\n-----END CERTIFICATE-----\n" + setEnv(t, "__MTA_CERT___fakeCertificate", base64.StdEncoding.EncodeToString([]byte(testCertificate))) + setEnv(t, "unrelatedEnvFirst", "exampleValueFirst") + setEnv(t, "unrelatedEnvSecond", "exampleValueSecond") + + resultToTest, err := CollectFromEnv("__MTA") + if err != nil { + t.Fatalf("Collecting environment variables has failed: %s", err.Error()) + } + + parameterValue, ok := resultToTest["fakePassword"] + if !ok { + t.Fatalf("Missing key 'fakePassword' in map") + } + + if parameterValue.Type != typeString || parameterValue.StringContent != "secretValue" { + t.Fatalf("The value of 'fakePassword' key is not correct") + } + + jsonValue, ok := resultToTest["fakeJson"] + if !ok { + t.Fatalf("Missing key 'fakeJson' in map") + } + + if jsonValue.Type != typeJSON { + t.Fatalf("The value of 'fakeJson' key is not correct") + } + + if firstJsonValue, ok := jsonValue.ObjectContent["a"].(float64); !ok || firstJsonValue != 1 { + t.Fatalf("The first value of the json is not what it should be: %v", jsonValue.ObjectContent["a"]) + } + + if jsonValue.ObjectContent["b"] != "secretValueJson" { + t.Fatalf("The second value of the json is not what it should be: %v", jsonValue.ObjectContent["b"]) + } + + certificateValue, ok := resultToTest["fakeCertificate"] + + if !ok { + t.Fatalf("The value of the certificate is not present") + } + + if certificateValue.Type != typeMultiline || certificateValue.StringContent != testCertificate { + t.Fatalf("The value of the certificate is not what it should be: %v", certificateValue) + } + + if _, exists := resultToTest["other"]; exists { + t.Fatalf("Unexpected value and environment variable") + } +} + +func TestCollectFromEnvWhenWrongName(t *testing.T) { + setEnv(t, "__MTA___fake spaced parameter", "x") + + _, err := CollectFromEnv("__MTA") + if err == nil || err.Error() != `invalid secure parameter name "fake spaced parameter"` { + t.Fatalf("Expected invalid name error: %v", err) + } +} + +func TestCollectFromEnvWhenInvalidJson(t *testing.T) { + setEnv(t, "__MTA_JSON___fakeJson", `{wrongFormat - fake}`) + + _, err := CollectFromEnv("__MTA") + if err == nil || !strings.Contains(err.Error(), "invalid JSON for fakeJson") { + t.Fatalf("Expected invalid JSON error: %v", err) + } +} + +func TestCollectFromEnvWhenDuplciateNames(t *testing.T) { + setEnv(t, "__MTA_JSON___duplicate", `{"fakeValueName":"value"}`) + setEnv(t, "__MTA___duplicate", "randomValue") + + _, err := CollectFromEnv("__MTA") + if err == nil || !strings.Contains(err.Error(), `secure parameter "duplicate" defined multiple ways`) { + t.Fatalf("Expected duplication error: %v", err) + } +} + +func TestCollectFromEnvWhenInvalidCertificate(t *testing.T) { + setEnv(t, "__MTA_CERT___fakeCertificate", "%**@&@#!#¬Base64*@&$)@!") + + _, err := CollectFromEnv("__MTA") + if err == nil || !strings.Contains(err.Error(), "invalid base64 for fakeCertificate") { + t.Fatalf("Expected invalid base64 error: %v", err) + } +} + +func TestCollectFromEnvWhenDifferentPrefix(t *testing.T) { + setEnv(t, "__MTA_JSON___myJson", `{"apple":"green"}`) + + result, err := CollectFromEnv("__OTHER") + + if err != nil { + t.Fatalf("Error while trying to collect environment variables with a different prefix: %s", err.Error()) + } + + if len(result) > 0 { + t.Fatalf("There should be zero environment variables collected, but there are: %d", len(result)) + } +} + +func TestBuildSecureExtension(t *testing.T) { + parameters := map[string]ParameterValue{ + "password": {Type: typeString, StringContent: "secretValue"}, + "fakeJson": {Type: typeJSON, ObjectContent: map[string]interface{}{"secretParameterFirst": "secretValueOne", "secretParameterSecond": "secretValueTwo"}}, + "fakeCertificate": {Type: typeMultiline, StringContent: "-----BEGIN CERTIFICATE-----\nMIBgNVBAYTAPXwBc63heW9WrP3qnDEm+UZE4V0Au7OWnOeiobq\n-----END CERTIFICATE-----\n"}, + } + + yamlResult, err := BuildSecureExtension(parameters, "test-mta", "") + + if err != nil { + t.Fatalf("Error while building the secure extension descriotor: %s", err.Error()) + } + + var unmarshaledBack map[string]interface{} + + err2 := yaml.Unmarshal(yamlResult, &unmarshaledBack) + + if err2 != nil { + t.Fatalf("Error while unmarshaling extension descriptor: %s", err.Error()) + } + + if unmarshaledBack["_schema-version"] != "3.3" { + t.Fatalf("Schema version is not what it should be: %v", unmarshaledBack["_schema-version"]) + } + + if unmarshaledBack["ID"] != "__mta.secure" { + t.Fatalf("ID of the secure extension descriptor is not what it should be: %v", unmarshaledBack["ID"]) + } + + if unmarshaledBack["extends"] != "test-mta" { + t.Fatalf("Extends of secure extension descriptor is not what it should be: %v", unmarshaledBack["extends"]) + } + + parametersUnmarshaled, ok := unmarshaledBack["parameters"].(map[string]interface{}) + + if !ok { + t.Fatalf("Parameters is not a map, but rather: %T", unmarshaledBack["parameters"]) + } + + if parametersUnmarshaled["password"] != "secretValue" { + t.Fatalf("Value of password is incorrect: %v", parametersUnmarshaled["password"]) + } + + fakeJson, ok := parametersUnmarshaled["fakeJson"].(map[string]interface{}) + + if !ok { + t.Fatalf("fakeJson is not an object but: %T", parametersUnmarshaled["fakeJson"]) + } + + if fakeJson["secretParameterFirst"] != "secretValueOne" { + t.Fatalf("fakeJson.secretParameterFirst is not what it should be: %v", fakeJson["secretParameterFirst"]) + } + + if fakeJson["secretParameterSecond"] != "secretValueTwo" { + t.Fatalf("fakeJson.secretParameterSecond is not what it should be: %v", fakeJson["secretParameterSecond"]) + } + + if parametersUnmarshaled["fakeCertificate"] != "-----BEGIN CERTIFICATE-----\nMIBgNVBAYTAPXwBc63heW9WrP3qnDEm+UZE4V0Au7OWnOeiobq\n-----END CERTIFICATE-----\n" { + t.Fatalf("fakeCertificate is not what it should be: %v", parametersUnmarshaled["fakeCertificate"]) + } +} + +func TestBuildSecureExtensionWhenExplicitSchema(t *testing.T) { + parameters := map[string]ParameterValue{ + "password": {Type: typeString, StringContent: "secretValue"}, + } + + yamlResult, err := BuildSecureExtension(parameters, "test-mta", "3.1") + + if err != nil { + t.Fatalf("Error while building the secure extension descriotor: %s", err.Error()) + } + + var unmarshaledBack map[string]interface{} + + err2 := yaml.Unmarshal(yamlResult, &unmarshaledBack) + + if err2 != nil { + t.Fatalf("Error while unmarshaling extension descriptor: %s", err.Error()) + } + + if unmarshaledBack["_schema-version"] != "3.1" { + t.Fatalf("Schema version must be 3.1, but it is: %v", unmarshaledBack["_schema-version"]) + } +} + +func TestBuildSecureExtensionWhenNoParameters(t *testing.T) { + _, err := BuildSecureExtension(map[string]ParameterValue{}, "test-mta", "") + + if err == nil || err.Error() != "no secure parameters collected" { + t.Fatalf("Expected no parameters error, but rather got: %v", err) + } +} + +func TestBuildSecureExtensionWhenNoMtaId(t *testing.T) { + parameters := map[string]ParameterValue{ + "password": {Type: typeString, StringContent: "secretValue"}, + } + + _, err := BuildSecureExtension(parameters, "", "") + if err == nil || err.Error() != "mtaID is required for the extension descriptor's field 'extends'" { + t.Fatalf("Expected missing mta id error, but rather got: %v", err) + } +} diff --git a/secure_parameters/secure_parametes.go b/secure_parameters/secure_parametes.go new file mode 100644 index 0000000..8c28920 --- /dev/null +++ b/secure_parameters/secure_parametes.go @@ -0,0 +1,149 @@ +package secure_parameters + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strings" + + "gopkg.in/yaml.v3" +) + +var nameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) + +type typeOfValue int + +const ( + typeString typeOfValue = iota + typeJSON + typeMultiline +) + +type ParameterValue struct { + Type typeOfValue + StringContent string + ObjectContent map[string]interface{} +} + +func nameDuplicated(name, prefix string, result map[string]ParameterValue) error { + _, ok := result[name] + if ok { + return fmt.Errorf("secure parameter %q defined multiple ways (collision with %s)", name, prefix) + } + + return nil +} + +func CollectFromEnv(prefix string) (map[string]ParameterValue, error) { + plainValue := prefix + "___" + jsonValue := prefix + "_JSON___" + certificateValue := prefix + "_CERT___" //X509value beacuse the certiciates are of type X509 (should be renamed) + + result := map[string]ParameterValue{} + + for _, nameValuePair := range os.Environ() { + equalsIndex := strings.IndexByte(nameValuePair, '=') + if equalsIndex < 0 { + continue + } + envName := nameValuePair[:equalsIndex] + envValue := nameValuePair[equalsIndex+1:] + + var name string + + switch { + case strings.HasPrefix(envName, jsonValue): + name = strings.TrimPrefix(envName, jsonValue) + + if !nameRegex.MatchString(name) { + return nil, fmt.Errorf("invalid secure parameter name %q", name) + } + + err := nameDuplicated(name, "__MTA_JSON", result) + if err != nil { + return nil, err + } + + var jsonObject map[string]interface{} + + err2 := json.Unmarshal([]byte(envValue), &jsonObject) + if err2 != nil { + return nil, fmt.Errorf("invalid JSON for %s: %w", name, err2) + } + result[name] = ParameterValue{Type: typeJSON, ObjectContent: jsonObject} + + case strings.HasPrefix(envName, certificateValue): + name = strings.TrimPrefix(envName, certificateValue) + + if !nameRegex.MatchString(name) { + return nil, fmt.Errorf("invalid secure parameter name %q", name) + } + + err := nameDuplicated(name, "__MTA_CERT", result) + if err != nil { + return nil, err + } + + decoded, err := base64.StdEncoding.DecodeString(envValue) + if err != nil { + return nil, fmt.Errorf("invalid base64 for %s: %w", name, err) + } + result[name] = ParameterValue{Type: typeMultiline, StringContent: string(decoded)} + + case strings.HasPrefix(envName, plainValue): + name = strings.TrimPrefix(envName, plainValue) + + if !nameRegex.MatchString(name) { + return nil, fmt.Errorf("invalid secure parameter name %q", name) + } + + err := nameDuplicated(name, "__MTA", result) + if err != nil { + return nil, err + } + + result[name] = ParameterValue{Type: typeString, StringContent: envValue} + + default: + continue + } + } + + return result, nil +} + +func BuildSecureExtension(parameters map[string]ParameterValue, mtaID string, schemaVersion string) ([]byte, error) { + if len(parameters) == 0 { + return nil, errors.New("no secure parameters collected") + } + + if mtaID == "" { + return nil, errors.New("mtaID is required for the extension descriptor's field 'extends'") + } + + if schemaVersion == "" { + schemaVersion = "3.3" + } + + secureExtensionDescriptor := map[string]interface{}{ + "_schema-version": schemaVersion, + "ID": "__mta.secure", + "extends": mtaID, + "parameters": map[string]interface{}{}, + } + + parametersDescriptor := secureExtensionDescriptor["parameters"].(map[string]interface{}) + for name, currentParameterValue := range parameters { + switch currentParameterValue.Type { + case typeJSON: + parametersDescriptor[name] = currentParameterValue.ObjectContent + default: + parametersDescriptor[name] = currentParameterValue.StringContent + } + } + + return yaml.Marshal(secureExtensionDescriptor) +} From 2937061be2134f0ed78e575dbc085385d9f82516 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Mon, 26 Jan 2026 16:02:27 +0200 Subject: [PATCH 2/7] Adding fixes after review LMCROSSITXSADEPLOY-2301 --- .../cloud_foundry_operations_extended.go | 1 + .../fakes/fake_cloud_foundry_client.go | 4 + ...ient_rest_cloud_foundry_client_extended.go | 9 +- .../rest_cloud_foundry_client_extended.go | 21 +++ commands/blue_green_deploy_command.go | 2 +- commands/deploy_command.go | 111 +++++++------- secure_parameters/secure_builder.go | 35 +++++ secure_parameters/secure_parameters_test.go | 15 +- secure_parameters/secure_parametes.go | 138 +++++++++--------- 9 files changed, 206 insertions(+), 130 deletions(-) create mode 100644 secure_parameters/secure_builder.go diff --git a/clients/cfrestclient/cloud_foundry_operations_extended.go b/clients/cfrestclient/cloud_foundry_operations_extended.go index e06da53..c670032 100644 --- a/clients/cfrestclient/cloud_foundry_operations_extended.go +++ b/clients/cfrestclient/cloud_foundry_operations_extended.go @@ -10,4 +10,5 @@ type CloudFoundryOperationsExtended interface { GetApplicationRoutes(appGuid string) ([]models.ApplicationRoute, error) GetServiceInstances(mtaId, mtaNamespace, spaceGuid string) ([]models.CloudFoundryServiceInstance, error) GetServiceBindings(serviceName string) ([]models.ServiceBinding, error) + GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) } diff --git a/clients/cfrestclient/fakes/fake_cloud_foundry_client.go b/clients/cfrestclient/fakes/fake_cloud_foundry_client.go index ddabf80..9bef31b 100644 --- a/clients/cfrestclient/fakes/fake_cloud_foundry_client.go +++ b/clients/cfrestclient/fakes/fake_cloud_foundry_client.go @@ -34,3 +34,7 @@ func (f FakeCloudFoundryClient) GetServiceInstances(mtaId, mtaNamespace, spaceGu func (f FakeCloudFoundryClient) GetServiceBindings(serviceName string) ([]models.ServiceBinding, error) { return f.ServiceBindings, f.ServiceBindingsErr } + +func (f FakeCloudFoundryClient) GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) { + return f.Services[0], f.ServiceBindingsErr +} diff --git a/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go b/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go index b2523d5..072cd26 100644 --- a/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go +++ b/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go @@ -1,9 +1,10 @@ package resilient import ( + "time" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient" "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models" - "time" ) type ResilientCloudFoundryRestClient struct { @@ -46,6 +47,12 @@ func (c ResilientCloudFoundryRestClient) GetServiceBindings(serviceName string) }, c.MaxRetriesCount, c.RetryInterval) } +func (c ResilientCloudFoundryRestClient) GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) { + return retryOnError(func() (models.CloudFoundryServiceInstance, error) { + return c.CloudFoundryRestClient.GetServiceInstanceByName(serviceName, spaceGuid) + }, c.MaxRetriesCount, c.RetryInterval) +} + func retryOnError[T any](operation func() (T, error), retries int, retryInterval time.Duration) (T, error) { result, err := operation() for shouldRetry(retries, err) { diff --git a/clients/cfrestclient/rest_cloud_foundry_client_extended.go b/clients/cfrestclient/rest_cloud_foundry_client_extended.go index 6e3dd5a..53491da 100644 --- a/clients/cfrestclient/rest_cloud_foundry_client_extended.go +++ b/clients/cfrestclient/rest_cloud_foundry_client_extended.go @@ -114,6 +114,27 @@ func (c CloudFoundryRestClient) GetServiceBindings(serviceName string) ([]models return getPaginatedResourcesWithIncluded(getServiceBindingsUrl, token, c.isSslDisabled, buildServiceBinding) } +func (c CloudFoundryRestClient) GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) { + token, err := c.cliConn.AccessToken() + if err != nil { + return models.CloudFoundryServiceInstance{}, fmt.Errorf("failed to retrieve access token: %s", err) + } + apiEndpoint, _ := c.cliConn.ApiEndpoint() + + getServicesUrl := fmt.Sprintf("%s/%sservice_instances?names=%s&space_guids=%s", + apiEndpoint, cfBaseUrl, serviceName, spaceGuid) + services, err := getPaginatedResourcesWithIncluded(getServicesUrl, token, c.isSslDisabled, buildServiceInstance) + if err != nil { + return models.CloudFoundryServiceInstance{}, err + } + if len(services) == 0 { + return models.CloudFoundryServiceInstance{}, fmt.Errorf("service instance not found") + } + + resultService := services[0] + return resultService, nil +} + func getPaginatedResources[T any](url, token string, isSslDisabled bool) ([]T, error) { var result []T for url != "" { diff --git a/commands/blue_green_deploy_command.go b/commands/blue_green_deploy_command.go index 0f8fb1f..a40b0d9 100644 --- a/commands/blue_green_deploy_command.go +++ b/commands/blue_green_deploy_command.go @@ -20,7 +20,7 @@ type BlueGreenDeployCommand struct { // NewBlueGreenDeployCommand creates a new BlueGreenDeployCommand. func NewBlueGreenDeployCommand() *BlueGreenDeployCommand { baseCmd := &BaseCommand{flagsParser: deployCommandLineArgumentsParser{}, flagsValidator: deployCommandFlagsValidator{}} - deployCmd := &DeployCommand{baseCmd, blueGreenDeployProcessParametersSetter(), &blueGreenDeployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second} + deployCmd := &DeployCommand{baseCmd, blueGreenDeployProcessParametersSetter(), &blueGreenDeployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second, nil} bgDeployCmd := &BlueGreenDeployCommand{deployCmd} baseCmd.Command = bgDeployCmd return bgDeployCmd diff --git a/commands/deploy_command.go b/commands/deploy_command.go index 0658df5..a733151 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -20,6 +20,8 @@ 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" @@ -91,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{ @@ -114,7 +123,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { Perform action on an active deploy operation cf deploy -i OPERATION_ID -a ACTION [-u URL] - (EXPERIMENTAL) Deploy a multi-target app archive referenced by a remote URL + Deploy a multi-target app archive referenced by a remote URL | 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{ @@ -151,7 +160,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { 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(dependencyAwareStopOrderOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Stop apps in a dependency-aware order during the resume phase of a blue-green deployment", - util.GetShortOption(requireSecureParameters): "Pass secrets to the deploy service in a secure way", + util.GetShortOption(requireSecureParameters): "(EXPERIMENTAL) Pass secrets to the deploy service in a secure way", }, }, } @@ -280,6 +289,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 @@ -330,6 +340,41 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return Failure } + if GetBoolOpt(requireSecureParameters, flags) { + // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ + 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___, __MTA_JSON___, or __MTA_CERT___.") + 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 { @@ -358,39 +403,6 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, } if GetBoolOpt(requireSecureParameters, flags) { - // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ - 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___, __MTA_JSON___, or __MTA_CERT___.") - 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) @@ -474,26 +486,21 @@ func getRandomEncryptionKey() (string, error) { } 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) + space, errSpace := c.cliConnection.GetCurrentSpace() + if errSpace != nil { + return false, fmt.Errorf("Cannot determine the current space") } - stringTable := strings.Join(servicesOutput, "\n") - return findServiceName(stringTable, userProvidedServiceName), nil -} + spaceGuid := space.Guid -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 + _, 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 false + + return true, nil } func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) { diff --git a/secure_parameters/secure_builder.go b/secure_parameters/secure_builder.go new file mode 100644 index 0000000..d61760b --- /dev/null +++ b/secure_parameters/secure_builder.go @@ -0,0 +1,35 @@ +package secure_parameters + +import ( + "errors" + + "gopkg.in/yaml.v3" +) + +func BuildSecureExtension(parameters map[string]ParameterValue, mtaID string, schemaVersion string) ([]byte, error) { + if len(parameters) == 0 { + return nil, errors.New("no secure parameters collected") + } + + if mtaID == "" { + return nil, errors.New("mtaID is required for the secure extension descriptor's field 'extends'") + } + + if schemaVersion == "" { + return nil, errors.New("schemaVersion is required for the secure extension descriptor") + } + + secureExtensionDescriptor := map[string]interface{}{ + "_schema-version": schemaVersion, + "ID": "__mta.secure", + "extends": mtaID, + "parameters": map[string]interface{}{}, + } + + parametersDescriptor := secureExtensionDescriptor["parameters"].(map[string]interface{}) + for name, currentParameterValue := range parameters { + parametersDescriptor[name] = getValue(¤tParameterValue) + } + + return yaml.Marshal(secureExtensionDescriptor) +} diff --git a/secure_parameters/secure_parameters_test.go b/secure_parameters/secure_parameters_test.go index b330620..540ee75 100644 --- a/secure_parameters/secure_parameters_test.go +++ b/secure_parameters/secure_parameters_test.go @@ -44,12 +44,17 @@ func TestCollectFromEnv(t *testing.T) { t.Fatalf("The value of 'fakeJson' key is not correct") } - if firstJsonValue, ok := jsonValue.ObjectContent["a"].(float64); !ok || firstJsonValue != 1 { - t.Fatalf("The first value of the json is not what it should be: %v", jsonValue.ObjectContent["a"]) + castedValue, ok := jsonValue.JSONContent.(map[string]interface{}) + if !ok { + t.Fatal("fakeJson is not an Object") + } + + if firstJsonValue, ok := castedValue["a"].(float64); !ok || firstJsonValue != 1 { + t.Fatalf("The first value of the json is not what it should be: %v", castedValue["a"]) } - if jsonValue.ObjectContent["b"] != "secretValueJson" { - t.Fatalf("The second value of the json is not what it should be: %v", jsonValue.ObjectContent["b"]) + if castedValue["b"] != "secretValueJson" { + t.Fatalf("The second value of the json is not what it should be: %v", castedValue["b"]) } certificateValue, ok := resultToTest["fakeCertificate"] @@ -121,7 +126,7 @@ func TestCollectFromEnvWhenDifferentPrefix(t *testing.T) { func TestBuildSecureExtension(t *testing.T) { parameters := map[string]ParameterValue{ "password": {Type: typeString, StringContent: "secretValue"}, - "fakeJson": {Type: typeJSON, ObjectContent: map[string]interface{}{"secretParameterFirst": "secretValueOne", "secretParameterSecond": "secretValueTwo"}}, + "fakeJson": {Type: typeJSON, JSONContent: map[string]interface{}{"secretParameterFirst": "secretValueOne", "secretParameterSecond": "secretValueTwo"}}, "fakeCertificate": {Type: typeMultiline, StringContent: "-----BEGIN CERTIFICATE-----\nMIBgNVBAYTAPXwBc63heW9WrP3qnDEm+UZE4V0Au7OWnOeiobq\n-----END CERTIFICATE-----\n"}, } diff --git a/secure_parameters/secure_parametes.go b/secure_parameters/secure_parametes.go index 8c28920..6ad4838 100644 --- a/secure_parameters/secure_parametes.go +++ b/secure_parameters/secure_parametes.go @@ -3,13 +3,10 @@ package secure_parameters import ( "encoding/base64" "encoding/json" - "errors" "fmt" "os" "regexp" "strings" - - "gopkg.in/yaml.v3" ) var nameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) @@ -25,10 +22,11 @@ const ( type ParameterValue struct { Type typeOfValue StringContent string - ObjectContent map[string]interface{} + JSONContent interface{} + //ObjectContent map[string]interface{} } -func nameDuplicated(name, prefix string, result map[string]ParameterValue) error { +func validateNoDuplicatesExist(name, prefix string, result map[string]ParameterValue) error { _, ok := result[name] if ok { return fmt.Errorf("secure parameter %q defined multiple ways (collision with %s)", name, prefix) @@ -37,12 +35,22 @@ func nameDuplicated(name, prefix string, result map[string]ParameterValue) error return nil } +func getValue(parameter *ParameterValue) interface{} { + switch parameter.Type { + case typeJSON: + return parameter.JSONContent + + default: + return parameter.StringContent + } +} + func CollectFromEnv(prefix string) (map[string]ParameterValue, error) { - plainValue := prefix + "___" - jsonValue := prefix + "_JSON___" - certificateValue := prefix + "_CERT___" //X509value beacuse the certiciates are of type X509 (should be renamed) + plainPrefix := prefix + "___" + jsonPrefix := prefix + "_JSON___" + certPrefix := prefix + "_CERT___" - result := map[string]ParameterValue{} + result := make(map[string]ParameterValue) for _, nameValuePair := range os.Environ() { equalsIndex := strings.IndexByte(nameValuePair, '=') @@ -55,95 +63,83 @@ func CollectFromEnv(prefix string) (map[string]ParameterValue, error) { var name string switch { - case strings.HasPrefix(envName, jsonValue): - name = strings.TrimPrefix(envName, jsonValue) + case strings.HasPrefix(envName, jsonPrefix): + name = strings.TrimPrefix(envName, jsonPrefix) - if !nameRegex.MatchString(name) { - return nil, fmt.Errorf("invalid secure parameter name %q", name) - } - - err := nameDuplicated(name, "__MTA_JSON", result) + err := addJSONValues(name, envValue, result) if err != nil { return nil, err } + case strings.HasPrefix(envName, certPrefix): + name = strings.TrimPrefix(envName, certPrefix) - var jsonObject map[string]interface{} - - err2 := json.Unmarshal([]byte(envValue), &jsonObject) - if err2 != nil { - return nil, fmt.Errorf("invalid JSON for %s: %w", name, err2) - } - result[name] = ParameterValue{Type: typeJSON, ObjectContent: jsonObject} - - case strings.HasPrefix(envName, certificateValue): - name = strings.TrimPrefix(envName, certificateValue) - - if !nameRegex.MatchString(name) { - return nil, fmt.Errorf("invalid secure parameter name %q", name) - } - - err := nameDuplicated(name, "__MTA_CERT", result) + err := addCertificateValues(name, envValue, result) if err != nil { return nil, err } + case strings.HasPrefix(envName, plainPrefix): + name = strings.TrimPrefix(envName, plainPrefix) - decoded, err := base64.StdEncoding.DecodeString(envValue) - if err != nil { - return nil, fmt.Errorf("invalid base64 for %s: %w", name, err) - } - result[name] = ParameterValue{Type: typeMultiline, StringContent: string(decoded)} - - case strings.HasPrefix(envName, plainValue): - name = strings.TrimPrefix(envName, plainValue) - - if !nameRegex.MatchString(name) { - return nil, fmt.Errorf("invalid secure parameter name %q", name) - } - - err := nameDuplicated(name, "__MTA", result) + err := addPlainValues(name, envValue, result) if err != nil { return nil, err } - - result[name] = ParameterValue{Type: typeString, StringContent: envValue} - default: continue } } - return result, nil } -func BuildSecureExtension(parameters map[string]ParameterValue, mtaID string, schemaVersion string) ([]byte, error) { - if len(parameters) == 0 { - return nil, errors.New("no secure parameters collected") +func addJSONValues(name, raw string, result map[string]ParameterValue) error { + if !nameRegex.MatchString(name) { + return fmt.Errorf("invalid secure parameter name %q", name) } - if mtaID == "" { - return nil, errors.New("mtaID is required for the extension descriptor's field 'extends'") + errDuplicated := validateNoDuplicatesExist(name, "__MTA_JSON", result) + if errDuplicated != nil { + return errDuplicated } + var parsed interface{} - if schemaVersion == "" { - schemaVersion = "3.3" + errUnmarshal := json.Unmarshal([]byte(raw), &parsed) + if errUnmarshal != nil { + return fmt.Errorf("invalid JSON for %s: %w", name, errUnmarshal) } - secureExtensionDescriptor := map[string]interface{}{ - "_schema-version": schemaVersion, - "ID": "__mta.secure", - "extends": mtaID, - "parameters": map[string]interface{}{}, + result[name] = ParameterValue{Type: typeJSON, JSONContent: parsed} + return nil +} + +func addCertificateValues(name, raw string, result map[string]ParameterValue) error { + if !nameRegex.MatchString(name) { + return fmt.Errorf("invalid secure parameter name %q", name) } - parametersDescriptor := secureExtensionDescriptor["parameters"].(map[string]interface{}) - for name, currentParameterValue := range parameters { - switch currentParameterValue.Type { - case typeJSON: - parametersDescriptor[name] = currentParameterValue.ObjectContent - default: - parametersDescriptor[name] = currentParameterValue.StringContent - } + err := validateNoDuplicatesExist(name, "__MTA_CERT", result) + if err != nil { + return err + } + + decoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return fmt.Errorf("invalid base64 for %s: %w", name, err) } - return yaml.Marshal(secureExtensionDescriptor) + result[name] = ParameterValue{Type: typeMultiline, StringContent: string(decoded)} + return nil +} + +func addPlainValues(name, raw string, result map[string]ParameterValue) error { + if !nameRegex.MatchString(name) { + return fmt.Errorf("invalid secure parameter name %q", name) + } + + err := validateNoDuplicatesExist(name, "__MTA", result) + if err != nil { + return err + } + + result[name] = ParameterValue{Type: typeString, StringContent: raw} + return nil } From 1b0628423ca897470a3f9a0456678958525a5419 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Tue, 27 Jan 2026 09:42:58 +0200 Subject: [PATCH 3/7] Removing commented out field LMCROSSITXSADEPLOY-2301 --- secure_parameters/secure_parametes.go | 1 - 1 file changed, 1 deletion(-) diff --git a/secure_parameters/secure_parametes.go b/secure_parameters/secure_parametes.go index 6ad4838..fef8332 100644 --- a/secure_parameters/secure_parametes.go +++ b/secure_parameters/secure_parametes.go @@ -23,7 +23,6 @@ type ParameterValue struct { Type typeOfValue StringContent string JSONContent interface{} - //ObjectContent map[string]interface{} } func validateNoDuplicatesExist(name, prefix string, result map[string]ParameterValue) error { From 8922dd2753e46430b39ae28460ac83646564bb88 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Mon, 2 Feb 2026 20:38:48 +0200 Subject: [PATCH 4/7] Fixing comments and adding new disposable UPS functionality LMCROSSITXSADEPLOY-2301 --- commands/deploy_command.go | 140 ++++++++++++++------ secure_parameters/secure_parameters_test.go | 2 +- 2 files changed, 100 insertions(+), 42 deletions(-) diff --git a/commands/deploy_command.go b/commands/deploy_command.go index a733151..6dc14e6 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -34,31 +34,32 @@ import ( ) const ( - extDescriptorsOpt = "e" - timeoutOpt = "t" - versionRuleOpt = "version-rule" - noStartOpt = "no-start" - deleteServiceKeysOpt = "delete-service-keys" - keepFilesOpt = "keep-files" - skipOwnershipValidationOpt = "skip-ownership-validation" - moduleOpt = "m" - resourceOpt = "r" - allModulesOpt = "all-modules" - allResourcesOpt = "all-resources" - strategyOpt = "strategy" - skipTestingPhase = "skip-testing-phase" - skipIdleStart = "skip-idle-start" - startTimeoutOpt = "apps-start-timeout" - stageTimeoutOpt = "apps-stage-timeout" - uploadTimeoutOpt = "apps-upload-timeout" - taskExecutionTimeoutOpt = "apps-task-execution-timeout" - applyNamespaceAppNamesOpt = "apply-namespace-app-names" - applyNamespaceServiceNamesOpt = "apply-namespace-service-names" - applyNamespaceAppRoutesOpt = "apply-namespace-app-routes" - applyNamespaceAsSuffix = "apply-namespace-as-suffix" - maxNamespaceSize = 36 - shouldBackupPreviousVersionOpt = "backup-previous-version" - requireSecureParameters = "require-secure-parameters" + extDescriptorsOpt = "e" + timeoutOpt = "t" + versionRuleOpt = "version-rule" + noStartOpt = "no-start" + deleteServiceKeysOpt = "delete-service-keys" + keepFilesOpt = "keep-files" + skipOwnershipValidationOpt = "skip-ownership-validation" + moduleOpt = "m" + resourceOpt = "r" + allModulesOpt = "all-modules" + allResourcesOpt = "all-resources" + strategyOpt = "strategy" + skipTestingPhase = "skip-testing-phase" + skipIdleStart = "skip-idle-start" + startTimeoutOpt = "apps-start-timeout" + stageTimeoutOpt = "apps-stage-timeout" + uploadTimeoutOpt = "apps-upload-timeout" + taskExecutionTimeoutOpt = "apps-task-execution-timeout" + applyNamespaceAppNamesOpt = "apply-namespace-app-names" + applyNamespaceServiceNamesOpt = "apply-namespace-service-names" + applyNamespaceAppRoutesOpt = "apply-namespace-app-routes" + applyNamespaceAsSuffix = "apply-namespace-as-suffix" + maxNamespaceSize = 36 + shouldBackupPreviousVersionOpt = "backup-previous-version" + requireSecureParameters = "require-secure-parameters" + disposableUserProvidedServiceOpt = "disposable-user-provided-service" ) type listFlag struct { @@ -118,13 +119,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] [--require-secure-parameters] [--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] [--disposable-user-provided-service] [--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 - | 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, + | 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] [--disposable-user-provided-service] [--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", @@ -161,6 +162,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { 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(dependencyAwareStopOrderOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Stop apps in a dependency-aware order during the resume phase of a blue-green deployment", util.GetShortOption(requireSecureParameters): "(EXPERIMENTAL) Pass secrets to the deploy service in a secure way", + util.GetShortOption(disposableUserProvidedServiceOpt): "Deploy when --require-secure-parameters flag is active for disposable UPS to be created and then deleted at the of the operation", }, }, } @@ -187,6 +189,7 @@ func deployProcessParametersSetter() ProcessParametersSetter { processBuilder.Parameter("appsUploadTimeout", GetStringOpt(uploadTimeoutOpt, flags)) processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags)) processBuilder.Parameter("isSecurityEnabled", strconv.FormatBool(GetBoolOpt(requireSecureParameters, flags))) + processBuilder.Parameter("isDisposableUserProvidedServiceEnabled", strconv.FormatBool(GetBoolOpt(disposableUserProvidedServiceOpt, flags))) var lastSetValue string = "" for i := 0; i < len(os.Args); i++ { @@ -243,6 +246,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) { flags.Bool(shouldBackupPreviousVersionOpt, false, "") flags.Bool(dependencyAwareStopOrderOpt, false, "") flags.Bool(requireSecureParameters, false, "") + flags.Bool(disposableUserProvidedServiceOpt, false, "") } func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { @@ -277,6 +281,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, var uploadedArchivePartIds []string var uploadStatus ExecutionStatus var mtaId string + var disposableUserProvidedServiceName string // Check SLMP metadata // TODO: ensure session @@ -353,18 +358,36 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return Failure } - userProvidedServiceName := getUpsName(mtaId, namespace) + if GetBoolOpt(disposableUserProvidedServiceOpt, flags) { + disposableUserProvidedServiceName, err = getRandomisedUpsName(mtaId, namespace) + if err != nil { + ui.Failed("Failed to create disposable user-provided service name: %v", err) + return Failure + } - isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName, "v1") - if err != nil { - ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err) - return Failure - } + isDisposableUpsCreated, _, err := c.createDisposableUps(disposableUserProvidedServiceName) + if err != nil { + ui.Failed("Could not ensure disposable user-provided service %s: %v", disposableUserProvidedServiceName, err) + return Failure + } - if isUpsCreated { - ui.Say("Created user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName)) + if isDisposableUpsCreated { + ui.Say("Created disposable user-provided service %s for secure parameters. Will be automatically deleted at the end of the operation!", terminal.EntityNameColor(disposableUserProvidedServiceName)) + } } else { - ui.Say("Using existing user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName)) + userProvidedServiceName := getUpsName(mtaId, namespace) + + isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName) + 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 @@ -422,6 +445,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, processBuilder.Parameter("appArchiveId", strings.Join(uploadedArchivePartIds, ",")) processBuilder.Parameter("mtaExtDescriptorId", strings.Join(uploadedExtDescriptorIDs, ",")) processBuilder.Parameter("mtaId", mtaId) + processBuilder.Parameter("disposableUserProvidedServiceName", disposableUserProvidedServiceName) setModulesAndResourcesListParameters(modulesList, resourcesList, processBuilder, mtaElementsCalculator) c.setProcessParameters(flags, processBuilder) @@ -437,18 +461,32 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return executionMonitor.Monitor() } -func getUpsName(mtaID, namespace string) string { +func getUpsName(mtaId, namespace string) string { if strings.TrimSpace(namespace) == "" { - return "__mta-secure-" + mtaID + return "__mta-secure-" + mtaId } - return "__mta-secure-" + mtaID + "-" + namespace + return "__mta-secure-" + mtaId + "-" + namespace } -func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName, keyID string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { +func getRandomisedUpsName(mtaId, namespace string) (disposableUpsName string, err error) { + randomisedPart, err := getRandomEncryptionKey() + if err != nil { + return "", err + } + resultSuffix := randomisedPart[:7] + + if strings.TrimSpace(namespace) == "" { + return "__mta-secure-" + mtaId + "-" + resultSuffix, nil + } + return "__mta-secure-" + mtaId + "-" + namespace + "-" + resultSuffix, nil +} + +func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName 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 } @@ -460,7 +498,27 @@ func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName, upsCredentials := map[string]string{ "encryptionKey": encryptionKey, - "keyId": keyID, + } + + jsonBody, err := json.Marshal(upsCredentials) + if err != nil { + return false, "", fmt.Errorf("Error while creating JSON credentials for UPS service: %w", err) + } + + 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 (c *DeployCommand) createDisposableUps(userProvidedServiceName string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { + 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, } jsonBody, _ := json.Marshal(upsCredentials) diff --git a/secure_parameters/secure_parameters_test.go b/secure_parameters/secure_parameters_test.go index 540ee75..8e44b38 100644 --- a/secure_parameters/secure_parameters_test.go +++ b/secure_parameters/secure_parameters_test.go @@ -90,7 +90,7 @@ func TestCollectFromEnvWhenInvalidJson(t *testing.T) { } } -func TestCollectFromEnvWhenDuplciateNames(t *testing.T) { +func TestCollectFromEnvWhenDuplicateNames(t *testing.T) { setEnv(t, "__MTA_JSON___duplicate", `{"fakeValueName":"value"}`) setEnv(t, "__MTA___duplicate", "randomValue") From 81224d9e4652ff03c188fcb62efdfb8481ae0240 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Mon, 16 Feb 2026 11:41:37 +0200 Subject: [PATCH 5/7] Fixing deploy from URL and comments LMCROSSITXSADEPLOY-2301 --- .../cloud_foundry_operations_extended.go | 1 + .../fakes/fake_cloud_foundry_client.go | 4 + ...ient_rest_cloud_foundry_client_extended.go | 6 + .../rest_cloud_foundry_client_extended.go | 79 ++++++++- clients/models/cf_services_response.go | 11 +- clients/mtaclient/mta_rest_client.go | 1 + commands/deploy_command.go | 161 +++++++++++------- commands/upload_from_url_status.go | 1 + 8 files changed, 198 insertions(+), 66 deletions(-) diff --git a/clients/cfrestclient/cloud_foundry_operations_extended.go b/clients/cfrestclient/cloud_foundry_operations_extended.go index c670032..f65e9ed 100644 --- a/clients/cfrestclient/cloud_foundry_operations_extended.go +++ b/clients/cfrestclient/cloud_foundry_operations_extended.go @@ -11,4 +11,5 @@ type CloudFoundryOperationsExtended interface { GetServiceInstances(mtaId, mtaNamespace, spaceGuid string) ([]models.CloudFoundryServiceInstance, error) GetServiceBindings(serviceName string) ([]models.ServiceBinding, error) GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) + CreateUserProvidedServiceInstance(serviceName string, spaceGuid string, credentials map[string]string) (models.CloudFoundryServiceInstance, error) } diff --git a/clients/cfrestclient/fakes/fake_cloud_foundry_client.go b/clients/cfrestclient/fakes/fake_cloud_foundry_client.go index 9bef31b..783c3a1 100644 --- a/clients/cfrestclient/fakes/fake_cloud_foundry_client.go +++ b/clients/cfrestclient/fakes/fake_cloud_foundry_client.go @@ -38,3 +38,7 @@ func (f FakeCloudFoundryClient) GetServiceBindings(serviceName string) ([]models func (f FakeCloudFoundryClient) GetServiceInstanceByName(serviceName, spaceGuid string) (models.CloudFoundryServiceInstance, error) { return f.Services[0], f.ServiceBindingsErr } + +func (f FakeCloudFoundryClient) CreateUserProvidedServiceInstance(serviceName string, spaceGuid string, credentials map[string]string) (models.CloudFoundryServiceInstance, error) { + return f.Services[0], f.ServicesErr +} diff --git a/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go b/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go index 072cd26..8d5828c 100644 --- a/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go +++ b/clients/cfrestclient/resilient/resilient_rest_cloud_foundry_client_extended.go @@ -53,6 +53,12 @@ func (c ResilientCloudFoundryRestClient) GetServiceInstanceByName(serviceName, s }, c.MaxRetriesCount, c.RetryInterval) } +func (c ResilientCloudFoundryRestClient) CreateUserProvidedServiceInstance(serviceName string, spaceGuid string, credentials map[string]string) (models.CloudFoundryServiceInstance, error) { + return retryOnError(func() (models.CloudFoundryServiceInstance, error) { + return c.CloudFoundryRestClient.CreateUserProvidedServiceInstance(serviceName, spaceGuid, credentials) + }, c.MaxRetriesCount, c.RetryInterval) +} + func retryOnError[T any](operation func() (T, error), retries int, retryInterval time.Duration) (T, error) { result, err := operation() for shouldRetry(retries, err) { diff --git a/clients/cfrestclient/rest_cloud_foundry_client_extended.go b/clients/cfrestclient/rest_cloud_foundry_client_extended.go index 53491da..b86d7fe 100644 --- a/clients/cfrestclient/rest_cloud_foundry_client_extended.go +++ b/clients/cfrestclient/rest_cloud_foundry_client_extended.go @@ -1,6 +1,7 @@ package cfrestclient import ( + "bytes" "crypto/md5" "crypto/tls" "encoding/hex" @@ -60,7 +61,7 @@ func (c CloudFoundryRestClient) GetAppProcessStatistics(appGuid string) ([]model apiEndpoint, _ := c.cliConn.ApiEndpoint() getAppProcessStatsUrl := fmt.Sprintf("%s/%sapps/%s/processes/web/stats", apiEndpoint, cfBaseUrl, appGuid) - body, err := executeRequest(getAppProcessStatsUrl, token, c.isSslDisabled) + body, err := executeRequest("GET", getAppProcessStatsUrl, token, c.isSslDisabled, nil) if err != nil { return nil, err } @@ -135,10 +136,61 @@ func (c CloudFoundryRestClient) GetServiceInstanceByName(serviceName, spaceGuid return resultService, nil } +func (c CloudFoundryRestClient) CreateUserProvidedServiceInstance(serviceName string, spaceGuid string, credentials map[string]string) (models.CloudFoundryServiceInstance, error) { + token, err := c.cliConn.AccessToken() + if err != nil { + return models.CloudFoundryServiceInstance{}, fmt.Errorf("failed to retrieve access token: %s", err) + } + + apiEndpoint, _ := c.cliConn.ApiEndpoint() + + createServiceURL := fmt.Sprintf("%s/%sservice_instances", apiEndpoint, cfBaseUrl) + + payload := map[string]any{ + "type": "user-provided", + "name": serviceName, + "relationships": map[string]any{ + "space": map[string]any{ + "data": map[string]any{ + "guid": spaceGuid, + }, + }, + }, + } + + if credentials != nil { + payload["credentials"] = credentials + } + + jsonBody, err := json.Marshal(payload) + if err != nil { + return models.CloudFoundryServiceInstance{}, fmt.Errorf("failed to marshal create UPS request: %w", err) + } + + body, err := executeRequest(http.MethodPost, createServiceURL, token, c.isSslDisabled, jsonBody) + if err != nil { + return models.CloudFoundryServiceInstance{}, err + } + + response, err := parseBody[models.CloudFoundryUserProvidedServiceInstance](body) + if err != nil { + return models.CloudFoundryServiceInstance{}, err + } + + return models.CloudFoundryServiceInstance{ + Guid: response.Guid, + Name: response.Name, + Type: response.Type, + LastOperation: response.LastOperation, + SpaceGuid: response.SpaceGuid, + Metadata: response.Metadata, + }, nil +} + func getPaginatedResources[T any](url, token string, isSslDisabled bool) ([]T, error) { var result []T for url != "" { - body, err := executeRequest(url, token, isSslDisabled) + body, err := executeRequest("GET", url, token, isSslDisabled, nil) if err != nil { return nil, err } @@ -158,7 +210,7 @@ func getPaginatedResources[T any](url, token string, isSslDisabled bool) ([]T, e func getPaginatedResourcesWithIncluded[T any, Auxiliary any](url, token string, isSslDisabled bool, auxiliaryContentHandler func(T, Auxiliary) T) ([]T, error) { var result []T for url != "" { - body, err := executeRequest(url, token, isSslDisabled) + body, err := executeRequest("GET", url, token, isSslDisabled, nil) if err != nil { return nil, err } @@ -175,9 +227,22 @@ func getPaginatedResourcesWithIncluded[T any, Auxiliary any](url, token string, return result, nil } -func executeRequest(url, token string, isSslDisabled bool) ([]byte, error) { - req, _ := http.NewRequest(http.MethodGet, url, nil) - req.Header.Add("Authorization", token) +func executeRequest(methodType, url, token string, isSslDisabled bool, body []byte) ([]byte, error) { + var reader io.Reader + + if body != nil { + reader = bytes.NewReader(body) + } + request, err := http.NewRequest(methodType, url, reader) + if err != nil { + return nil, err + } + + request.Header.Add("Authorization", token) + request.Header.Set("Accept", "application/json") + if body != nil { + request.Header.Set("Content-Type", "application/json") + } // Create transport with TLS configuration httpTransport := http.DefaultTransport.(*http.Transport).Clone() @@ -187,7 +252,7 @@ func executeRequest(url, token string, isSslDisabled bool) ([]byte, error) { userAgentTransport := baseclient.NewUserAgentTransport(httpTransport) client := &http.Client{Transport: userAgentTransport} - resp, err := client.Do(req) + resp, err := client.Do(request) if err != nil { return nil, err } diff --git a/clients/models/cf_services_response.go b/clients/models/cf_services_response.go index c69378e..23b621f 100644 --- a/clients/models/cf_services_response.go +++ b/clients/models/cf_services_response.go @@ -14,6 +14,15 @@ type CloudFoundryServiceInstance struct { Offering ServiceOffering `json:"-"` } +type CloudFoundryUserProvidedServiceInstance struct { + Guid string `json:"guid"` + Name string `json:"name"` + Type string `json:"type"` + LastOperation LastOperation `json:"last_operation,omitempty"` + SpaceGuid string `jsonry:"relationships.space.data.guid"` + Metadata Metadata `json:"metadata"` +} + type LastOperation struct { Type string `json:"type"` State string `json:"state"` @@ -25,7 +34,7 @@ type LastOperation struct { type ServicePlan struct { Guid string `json:"guid"` Name string `json:"name"` - OfferingGuid string `jsonry:"relationships.service_offering.data.guid,omitempty"` + OfferingGuid string `jsonry:"rela tionships.service_offering.data.guid,omitempty"` } type ServiceOffering struct { diff --git a/clients/mtaclient/mta_rest_client.go b/clients/mtaclient/mta_rest_client.go index 35a6e45..0b5daf4 100644 --- a/clients/mtaclient/mta_rest_client.go +++ b/clients/mtaclient/mta_rest_client.go @@ -41,6 +41,7 @@ type AsyncUploadJobResult struct { Error string `json:"error,omitempty"` File *models.FileMetadata `json:"file,omitempty"` MtaId string `json:"mta_id,omitempty"` + SchemaVersion string `json:"schema_version,omitempty"` BytesProcessed int64 `json:"bytes_processed,omitempty"` ClientActions []string `json:"client_actions,omitempty"` } diff --git a/commands/deploy_command.go b/commands/deploy_command.go index 6dc14e6..29a28cb 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -4,7 +4,6 @@ import ( "bufio" "crypto/rand" "encoding/base64" - "encoding/json" "errors" "flag" "fmt" @@ -298,12 +297,14 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, if isUrl { var fileId string + var schemaVersion string asyncUploadJobResult := c.uploadFromUrl(mtaArchive, mtaClient, namespace, disableProgressBar) if asyncUploadJobResult.ExecutionStatus == Failure { return Failure } - mtaId, fileId = asyncUploadJobResult.MtaId, asyncUploadJobResult.FileId + mtaId, fileId, schemaVersion = asyncUploadJobResult.MtaId, asyncUploadJobResult.FileId, asyncUploadJobResult.SchemaVersion + ui.Say("-------------------- mtaId: %s, fileId: %s, schemaVersion: %s", mtaId, fileId, schemaVersion) // Check for an ongoing operation for this MTA ID and abort it wasAborted, err := c.CheckOngoingOperation(mtaId, namespace, dsHost, force, cfTarget) if err != nil { @@ -315,6 +316,14 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, } uploadedArchivePartIds = append(uploadedArchivePartIds, fileId) + + if GetBoolOpt(requireSecureParameters, flags) { + result := setUpSpecificsForDeploymentUsingSecrerts(flags, c, mtaId, namespace, schemaVersion, &disposableUserProvidedServiceName, &yamlBytes) + if result != Success { + return Failure + } + } + ui.Ok() } else { // Get the full path of the MTA archive @@ -346,54 +355,8 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, } if GetBoolOpt(requireSecureParameters, flags) { - // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ - 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___, __MTA_JSON___, or __MTA_CERT___.") - return Failure - } - - if GetBoolOpt(disposableUserProvidedServiceOpt, flags) { - disposableUserProvidedServiceName, err = getRandomisedUpsName(mtaId, namespace) - if err != nil { - ui.Failed("Failed to create disposable user-provided service name: %v", err) - return Failure - } - - isDisposableUpsCreated, _, err := c.createDisposableUps(disposableUserProvidedServiceName) - if err != nil { - ui.Failed("Could not ensure disposable user-provided service %s: %v", disposableUserProvidedServiceName, err) - return Failure - } - - if isDisposableUpsCreated { - ui.Say("Created disposable user-provided service %s for secure parameters. Will be automatically deleted at the end of the operation!", terminal.EntityNameColor(disposableUserProvidedServiceName)) - } - } else { - userProvidedServiceName := getUpsName(mtaId, namespace) - - isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName) - 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) + result := setUpSpecificsForDeploymentUsingSecrerts(flags, c, mtaId, namespace, descriptor.SchemaVersion, &disposableUserProvidedServiceName, &yamlBytes) + if result != Success { return Failure } } @@ -481,6 +444,67 @@ func getRandomisedUpsName(mtaId, namespace string) (disposableUpsName string, er return "__mta-secure-" + mtaId + "-" + namespace + "-" + resultSuffix, nil } +func setUpSpecificsForDeploymentUsingSecrerts(flags *flag.FlagSet, c *DeployCommand, mtaId, namespace, schemaVersion string, disposableUserProvidedServiceName *string, yamlBytes *[]byte) ExecutionStatus { + // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ + 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___, __MTA_JSON___, or __MTA_CERT___.") + return Failure + } + + if GetBoolOpt(disposableUserProvidedServiceOpt, flags) { + disposableUserProvidedServiceNameResult, err := getRandomisedUpsName(mtaId, namespace) + if err != nil { + ui.Failed("Failed to create disposable user-provided service name: %v", err) + return Failure + } + + isDisposableUpsCreated, _, err := c.createDisposableUps(disposableUserProvidedServiceNameResult) + if err != nil { + ui.Failed("Could not ensure disposable user-provided service %s: %v", disposableUserProvidedServiceName, err) + return Failure + } + + *disposableUserProvidedServiceName = disposableUserProvidedServiceNameResult + if isDisposableUpsCreated { + ui.Say("Created disposable user-provided service %s for secure parameters. Will be automatically deleted at the end of the operation!", terminal.EntityNameColor(disposableUserProvidedServiceNameResult)) + } + } else { + userProvidedServiceName := getUpsName(mtaId, namespace) + + isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName) + if err != nil { + ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err) + return Failure + } + + *disposableUserProvidedServiceName = "" + 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)) + } + } + + yamlBytesResult, err := secure_parameters.BuildSecureExtension(parameters, mtaId, schemaVersion) + if err != nil { + ui.Failed("Could not build secure extension: %s", err) + return Failure + } + if len(yamlBytesResult) == 0 { + ui.Failed("Secure extension descriptor is empty: %s", err) + return Failure + } + *yamlBytes = yamlBytesResult + + return Success +} + func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { doesUpsExist, err := c.doesUpsExist(userProvidedServiceName) if err != nil { @@ -496,18 +520,24 @@ func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err) } + space, err := c.cliConnection.GetCurrentSpace() + if err != nil { + return false, "", fmt.Errorf("Failed to get the current space: %w", err) + } + + if space.Guid == "" { + return false, "", fmt.Errorf("Failed to get the current space Guid") + } + upsCredentials := map[string]string{ "encryptionKey": encryptionKey, } - jsonBody, err := json.Marshal(upsCredentials) + _, err = c.CfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) if err != nil { - return false, "", fmt.Errorf("Error while creating JSON credentials for UPS service: %w", err) + return false, "", fmt.Errorf("Failed to create user-provided service %s: %w", userProvidedServiceName, err) } - 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 } @@ -517,14 +547,24 @@ func (c *DeployCommand) createDisposableUps(userProvidedServiceName string) (ups return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err) } + space, err := c.cliConnection.GetCurrentSpace() + if err != nil { + return false, "", fmt.Errorf("Failed to get the current space: %w", err) + } + + if space.Guid == "" { + return false, "", fmt.Errorf("Failed to get the current space Guid") + } + upsCredentials := map[string]string{ "encryptionKey": encryptionKey, } - 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) + _, err = c.CfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) + if err != nil { + return false, "", fmt.Errorf("Failed to create user-provided service %s: %w", userProvidedServiceName, err) } + return true, encryptionKey, nil } @@ -591,6 +631,7 @@ func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclie return UploadFromUrlStatus{ FileId: "", MtaId: "", + SchemaVersion: "", ClientActions: make([]string, 0), ExecutionStatus: Failure, } @@ -620,6 +661,7 @@ func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclie return UploadFromUrlStatus{ FileId: "", MtaId: "", + SchemaVersion: "", ClientActions: jobResult.ClientActions, ExecutionStatus: Failure, } @@ -630,6 +672,7 @@ func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclie return UploadFromUrlStatus{ FileId: "", MtaId: "", + SchemaVersion: "", ClientActions: jobResult.ClientActions, ExecutionStatus: Failure, } @@ -652,6 +695,7 @@ func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclie return UploadFromUrlStatus{ FileId: "", MtaId: "", + SchemaVersion: "", ClientActions: make([]string, 0), ExecutionStatus: Failure, } @@ -665,6 +709,7 @@ func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclie return UploadFromUrlStatus{ FileId: file.ID, MtaId: jobResult.MtaId, + SchemaVersion: jobResult.SchemaVersion, ClientActions: jobResult.ClientActions, ExecutionStatus: Success, } diff --git a/commands/upload_from_url_status.go b/commands/upload_from_url_status.go index b5b9319..8c40eba 100644 --- a/commands/upload_from_url_status.go +++ b/commands/upload_from_url_status.go @@ -3,6 +3,7 @@ package commands type UploadFromUrlStatus struct { FileId string MtaId string + SchemaVersion string ClientActions []string ExecutionStatus ExecutionStatus } From ad47fc6c73aec92c9d9288b263209b9c702f50af Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Wed, 18 Feb 2026 08:48:17 +0200 Subject: [PATCH 6/7] Fixing comments and cf client in tests LMCROSSITXSADEPLOY-2301 --- .../rest_cloud_foundry_client_extended.go | 6 +- clients/models/cf_services_response.go | 2 +- commands/deploy_command.go | 7 +- commands/deploy_command_test.go | 110 ++++++++++-------- secure_parameters/secure_parameters_test.go | 28 +++-- 5 files changed, 88 insertions(+), 65 deletions(-) diff --git a/clients/cfrestclient/rest_cloud_foundry_client_extended.go b/clients/cfrestclient/rest_cloud_foundry_client_extended.go index b86d7fe..e69e87a 100644 --- a/clients/cfrestclient/rest_cloud_foundry_client_extended.go +++ b/clients/cfrestclient/rest_cloud_foundry_client_extended.go @@ -61,7 +61,7 @@ func (c CloudFoundryRestClient) GetAppProcessStatistics(appGuid string) ([]model apiEndpoint, _ := c.cliConn.ApiEndpoint() getAppProcessStatsUrl := fmt.Sprintf("%s/%sapps/%s/processes/web/stats", apiEndpoint, cfBaseUrl, appGuid) - body, err := executeRequest("GET", getAppProcessStatsUrl, token, c.isSslDisabled, nil) + body, err := executeRequest(http.MethodGet, getAppProcessStatsUrl, token, c.isSslDisabled, nil) if err != nil { return nil, err } @@ -190,7 +190,7 @@ func (c CloudFoundryRestClient) CreateUserProvidedServiceInstance(serviceName st func getPaginatedResources[T any](url, token string, isSslDisabled bool) ([]T, error) { var result []T for url != "" { - body, err := executeRequest("GET", url, token, isSslDisabled, nil) + body, err := executeRequest(http.MethodGet, url, token, isSslDisabled, nil) if err != nil { return nil, err } @@ -210,7 +210,7 @@ func getPaginatedResources[T any](url, token string, isSslDisabled bool) ([]T, e func getPaginatedResourcesWithIncluded[T any, Auxiliary any](url, token string, isSslDisabled bool, auxiliaryContentHandler func(T, Auxiliary) T) ([]T, error) { var result []T for url != "" { - body, err := executeRequest("GET", url, token, isSslDisabled, nil) + body, err := executeRequest(http.MethodGet, url, token, isSslDisabled, nil) if err != nil { return nil, err } diff --git a/clients/models/cf_services_response.go b/clients/models/cf_services_response.go index 23b621f..d75210e 100644 --- a/clients/models/cf_services_response.go +++ b/clients/models/cf_services_response.go @@ -34,7 +34,7 @@ type LastOperation struct { type ServicePlan struct { Guid string `json:"guid"` Name string `json:"name"` - OfferingGuid string `jsonry:"rela tionships.service_offering.data.guid,omitempty"` + OfferingGuid string `jsonry:"relationships.service_offering.data.guid,omitempty"` } type ServiceOffering struct { diff --git a/commands/deploy_command.go b/commands/deploy_command.go index 29a28cb..c0f66db 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -304,7 +304,6 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return Failure } mtaId, fileId, schemaVersion = asyncUploadJobResult.MtaId, asyncUploadJobResult.FileId, asyncUploadJobResult.SchemaVersion - ui.Say("-------------------- mtaId: %s, fileId: %s, schemaVersion: %s", mtaId, fileId, schemaVersion) // Check for an ongoing operation for this MTA ID and abort it wasAborted, err := c.CheckOngoingOperation(mtaId, namespace, dsHost, force, cfTarget) if err != nil { @@ -318,7 +317,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, uploadedArchivePartIds = append(uploadedArchivePartIds, fileId) if GetBoolOpt(requireSecureParameters, flags) { - result := setUpSpecificsForDeploymentUsingSecrerts(flags, c, mtaId, namespace, schemaVersion, &disposableUserProvidedServiceName, &yamlBytes) + result := setUpSpecificsForDeploymentUsingSecrets(flags, c, mtaId, namespace, schemaVersion, &disposableUserProvidedServiceName, &yamlBytes) if result != Success { return Failure } @@ -355,7 +354,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, } if GetBoolOpt(requireSecureParameters, flags) { - result := setUpSpecificsForDeploymentUsingSecrerts(flags, c, mtaId, namespace, descriptor.SchemaVersion, &disposableUserProvidedServiceName, &yamlBytes) + result := setUpSpecificsForDeploymentUsingSecrets(flags, c, mtaId, namespace, descriptor.SchemaVersion, &disposableUserProvidedServiceName, &yamlBytes) if result != Success { return Failure } @@ -444,7 +443,7 @@ func getRandomisedUpsName(mtaId, namespace string) (disposableUpsName string, er return "__mta-secure-" + mtaId + "-" + namespace + "-" + resultSuffix, nil } -func setUpSpecificsForDeploymentUsingSecrerts(flags *flag.FlagSet, c *DeployCommand, mtaId, namespace, schemaVersion string, disposableUserProvidedServiceName *string, yamlBytes *[]byte) ExecutionStatus { +func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployCommand, mtaId, namespace, schemaVersion string, disposableUserProvidedServiceName *string, yamlBytes *[]byte) ExecutionStatus { // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ parameters, err := secure_parameters.CollectFromEnv("__MTA") if err != nil { diff --git a/commands/deploy_command_test.go b/commands/deploy_command_test.go index 0963785..6f12eb9 100644 --- a/commands/deploy_command_test.go +++ b/commands/deploy_command_test.go @@ -2,6 +2,7 @@ package commands_test import ( "encoding/base64" + "errors" "fmt" "io" "io/fs" @@ -12,6 +13,7 @@ import ( "time" cli_fakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/cli/fakes" + cf_client_fakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient/fakes" "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models" "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient" mtafake "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient/fakes" @@ -112,41 +114,62 @@ var _ = Describe("DeployCommand", func() { if fromUrl { mtaNameToPrint = "from url" } + lines = append(lines, - "Deploying multi-target app archive "+mtaNameToPrint+" in org "+org+" / space "+space+" as "+user+"...") - lines = append(lines, "") + "Deploying multi-target app archive "+mtaNameToPrint+" in org "+org+" / space "+space+" as "+user+"...", + "", + ) + if processAborted { lines = append(lines, "Executing action \"abort\" on operation test-process-id...", "OK", ) } - if fromUrl { - lines = append(lines, "OK") - } else { + + if !fromUrl { + 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, "Uploading 1 files...", " "+fullMtaArchivePath, - "OK") + "OK", + ) + } else { + 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, "OK") } + if extDescriptor { lines = append(lines, "Uploading 1 files...", " "+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.") + "OK", + ) } + lines = append(lines, "Test message", "Process finished.", - "Use \"cf dmol -i 1000\" to download the logs of the process.") + "Use \"cf dmol -i 1000\" to download the logs of the process.", + ) + return lines } @@ -180,6 +203,7 @@ var _ = Describe("DeployCommand", func() { BeforeEach(func() { ui.DisableTerminalOutput(true) + command = commands.NewDeployCommand() name = command.GetPluginCommand().Name cliConnection = cli_fakes.NewFakeCliConnectionBuilder(). CurrentOrg("test-org-guid", org, nil). @@ -194,8 +218,9 @@ var _ = Describe("DeployCommand", func() { jobId := "one" fileUploadJobId.Add("Location", jobId) jobResult := mtaclient.AsyncUploadJobResult{ - File: mtaArchive, - MtaId: "anatz", + File: mtaArchive, + MtaId: "anatz", + SchemaVersion: "3.1.0", } mtaClient = mtafake.NewFakeMtaClientBuilder(). GetMtaFiles([]*models.FileMetadata{&testutil.SimpleFile}, nil). @@ -209,7 +234,6 @@ var _ = Describe("DeployCommand", func() { GetMtaOperationLogContent("1000", testutil.LogID, testutil.LogContent, nil). GetMtaOperations(nil, nil, nil, []*models.Operation{&testutil.OperationResult}, nil).Build() testClientFactory = commands.NewTestClientFactory(mtaClient, nil, nil) - command = commands.NewDeployCommand() testTokenFactory := commands.NewTestTokenFactory(cliConnection) deployServiceURLCalculator := util_fakes.NewDeployServiceURLFakeCalculator("deploy-service.test.ondemand.com") command.InitializeAll(name, cliConnection, testutil.NewCustomTransport(200), testClientFactory, testTokenFactory, deployServiceURLCalculator) @@ -511,12 +535,12 @@ var _ = Describe("DeployCommand", func() { 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 + command.CfClient = &cf_client_fakes.FakeCloudFoundryClient{ + Services: []models.CloudFoundryServiceInstance{{ + Guid: "ups-guid", + Name: upsName}, + }, + ServiceBindingsErr: nil, } output, status := oc.CaptureOutputAndStatus(func() int { @@ -540,18 +564,14 @@ var _ = Describe("DeployCommand", func() { 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 + upsName := "__mta-secure-anatz" + command.CfClient = &cf_client_fakes.FakeCloudFoundryClient{ + Services: []models.CloudFoundryServiceInstance{{ + Guid: "ups-guid", + Name: upsName}, + }, + ServiceBindingsErr: errors.New("service instance not found"), + ServicesErr: nil, } output, status := oc.CaptureOutputAndStatus(func() int { @@ -575,18 +595,10 @@ var _ = Describe("DeployCommand", func() { 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 + command.CfClient = &cf_client_fakes.FakeCloudFoundryClient{ + Services: []models.CloudFoundryServiceInstance{{Name: "fakeName"}}, + ServiceBindingsErr: errors.New("error with cf api"), + ServicesErr: nil, } output, status := oc.CaptureOutputAndStatus(func() int { diff --git a/secure_parameters/secure_parameters_test.go b/secure_parameters/secure_parameters_test.go index 8e44b38..a8d6b14 100644 --- a/secure_parameters/secure_parameters_test.go +++ b/secure_parameters/secure_parameters_test.go @@ -26,7 +26,19 @@ func TestCollectFromEnv(t *testing.T) { t.Fatalf("Collecting environment variables has failed: %s", err.Error()) } - parameterValue, ok := resultToTest["fakePassword"] + testNormalVariable(t, &resultToTest) + + testJsonVariable(t, &resultToTest) + + testCertificateVariable(t, &resultToTest, testCertificate) + + if _, exists := resultToTest["other"]; exists { + t.Fatalf("Unexpected value and environment variable") + } +} + +func testNormalVariable(t *testing.T, resultToTest *map[string]ParameterValue) { + parameterValue, ok := (*resultToTest)["fakePassword"] if !ok { t.Fatalf("Missing key 'fakePassword' in map") } @@ -34,8 +46,10 @@ func TestCollectFromEnv(t *testing.T) { if parameterValue.Type != typeString || parameterValue.StringContent != "secretValue" { t.Fatalf("The value of 'fakePassword' key is not correct") } +} - jsonValue, ok := resultToTest["fakeJson"] +func testJsonVariable(t *testing.T, resultToTest *map[string]ParameterValue) { + jsonValue, ok := (*resultToTest)["fakeJson"] if !ok { t.Fatalf("Missing key 'fakeJson' in map") } @@ -56,8 +70,10 @@ func TestCollectFromEnv(t *testing.T) { if castedValue["b"] != "secretValueJson" { t.Fatalf("The second value of the json is not what it should be: %v", castedValue["b"]) } +} - certificateValue, ok := resultToTest["fakeCertificate"] +func testCertificateVariable(t *testing.T, resultToTest *map[string]ParameterValue, testCertificate string) { + certificateValue, ok := (*resultToTest)["fakeCertificate"] if !ok { t.Fatalf("The value of the certificate is not present") @@ -66,10 +82,6 @@ func TestCollectFromEnv(t *testing.T) { if certificateValue.Type != typeMultiline || certificateValue.StringContent != testCertificate { t.Fatalf("The value of the certificate is not what it should be: %v", certificateValue) } - - if _, exists := resultToTest["other"]; exists { - t.Fatalf("Unexpected value and environment variable") - } } func TestCollectFromEnvWhenWrongName(t *testing.T) { @@ -133,7 +145,7 @@ func TestBuildSecureExtension(t *testing.T) { yamlResult, err := BuildSecureExtension(parameters, "test-mta", "") if err != nil { - t.Fatalf("Error while building the secure extension descriotor: %s", err.Error()) + t.Fatalf("Error while building the secure extension descriptor: %s", err.Error()) } var unmarshaledBack map[string]interface{} From e2d70cd6c0c9fed5905a3ff518fd2e1a8cb4dacf Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Wed, 18 Feb 2026 12:33:03 +0200 Subject: [PATCH 7/7] Refactor of related ups methods LMCROSSITXSADEPLOY-2301 --- commands/deploy_command.go | 125 +------------------------- secure_parameters/secure_ups_util.go | 126 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 121 deletions(-) create mode 100644 secure_parameters/secure_ups_util.go diff --git a/commands/deploy_command.go b/commands/deploy_command.go index c0f66db..6c11ec7 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -2,7 +2,6 @@ package commands import ( "bufio" - "crypto/rand" "encoding/base64" "errors" "flag" @@ -423,26 +422,6 @@ 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 getRandomisedUpsName(mtaId, namespace string) (disposableUpsName string, err error) { - randomisedPart, err := getRandomEncryptionKey() - if err != nil { - return "", err - } - resultSuffix := randomisedPart[:7] - - if strings.TrimSpace(namespace) == "" { - return "__mta-secure-" + mtaId + "-" + resultSuffix, nil - } - return "__mta-secure-" + mtaId + "-" + namespace + "-" + resultSuffix, nil -} - func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployCommand, mtaId, namespace, schemaVersion string, disposableUserProvidedServiceName *string, yamlBytes *[]byte) ExecutionStatus { // Collect special ENVs: __MTA___, __MTA_JSON___, __MTA_CERT___ parameters, err := secure_parameters.CollectFromEnv("__MTA") @@ -457,13 +436,13 @@ func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployComma } if GetBoolOpt(disposableUserProvidedServiceOpt, flags) { - disposableUserProvidedServiceNameResult, err := getRandomisedUpsName(mtaId, namespace) + disposableUserProvidedServiceNameResult, err := secure_parameters.GetRandomisedUpsName(mtaId, namespace) if err != nil { ui.Failed("Failed to create disposable user-provided service name: %v", err) return Failure } - isDisposableUpsCreated, _, err := c.createDisposableUps(disposableUserProvidedServiceNameResult) + isDisposableUpsCreated, _, err := secure_parameters.CreateDisposableUps(disposableUserProvidedServiceNameResult, c.cliConnection, c.CfClient) if err != nil { ui.Failed("Could not ensure disposable user-provided service %s: %v", disposableUserProvidedServiceName, err) return Failure @@ -474,9 +453,9 @@ func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployComma ui.Say("Created disposable user-provided service %s for secure parameters. Will be automatically deleted at the end of the operation!", terminal.EntityNameColor(disposableUserProvidedServiceNameResult)) } } else { - userProvidedServiceName := getUpsName(mtaId, namespace) + userProvidedServiceName := secure_parameters.GetUpsName(mtaId, namespace) - isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName) + isUpsCreated, _, err := secure_parameters.ValidateUpsExistsOrElseCreateIt(userProvidedServiceName, c.cliConnection, c.CfClient) if err != nil { ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err) return Failure @@ -504,102 +483,6 @@ func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployComma return Success } -func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName 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) - } - - space, err := c.cliConnection.GetCurrentSpace() - if err != nil { - return false, "", fmt.Errorf("Failed to get the current space: %w", err) - } - - if space.Guid == "" { - return false, "", fmt.Errorf("Failed to get the current space Guid") - } - - upsCredentials := map[string]string{ - "encryptionKey": encryptionKey, - } - - _, err = c.CfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) - if err != nil { - return false, "", fmt.Errorf("Failed to create user-provided service %s: %w", userProvidedServiceName, err) - } - - return true, encryptionKey, nil -} - -func (c *DeployCommand) createDisposableUps(userProvidedServiceName string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { - encryptionKey, err := getRandomEncryptionKey() - if err != nil { - return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err) - } - - space, err := c.cliConnection.GetCurrentSpace() - if err != nil { - return false, "", fmt.Errorf("Failed to get the current space: %w", err) - } - - if space.Guid == "" { - return false, "", fmt.Errorf("Failed to get the current space Guid") - } - - upsCredentials := map[string]string{ - "encryptionKey": encryptionKey, - } - - _, err = c.CfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) - if err != nil { - return false, "", fmt.Errorf("Failed to create user-provided service %s: %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) { - 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: diff --git a/secure_parameters/secure_ups_util.go b/secure_parameters/secure_ups_util.go new file mode 100644 index 0000000..351d152 --- /dev/null +++ b/secure_parameters/secure_ups_util.go @@ -0,0 +1,126 @@ +package secure_parameters + +import ( + "crypto/rand" + "fmt" + "strings" + + "code.cloudfoundry.org/cli/v8/plugin" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient" +) + +func ValidateUpsExistsOrElseCreateIt(userProvidedServiceName string, cliConnection plugin.CliConnection, cfClient cfrestclient.CloudFoundryOperationsExtended) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { + doesUpsExist, err := doesUpsExist(userProvidedServiceName, cliConnection, cfClient) + 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) + } + + space, err := cliConnection.GetCurrentSpace() + if err != nil { + return false, "", fmt.Errorf("Failed to get the current space: %w", err) + } + + if space.Guid == "" { + return false, "", fmt.Errorf("Failed to get the current space Guid") + } + + upsCredentials := map[string]string{ + "encryptionKey": encryptionKey, + } + + _, err = cfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) + if err != nil { + return false, "", fmt.Errorf("Failed to create user-provided service %s: %w", userProvidedServiceName, err) + } + + return true, encryptionKey, nil +} + +func CreateDisposableUps(userProvidedServiceName string, cliConnection plugin.CliConnection, cfClient cfrestclient.CloudFoundryOperationsExtended) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) { + encryptionKey, err := getRandomEncryptionKey() + if err != nil { + return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err) + } + + space, err := cliConnection.GetCurrentSpace() + if err != nil { + return false, "", fmt.Errorf("Failed to get the current space: %w", err) + } + + if space.Guid == "" { + return false, "", fmt.Errorf("Failed to get the current space Guid") + } + + upsCredentials := map[string]string{ + "encryptionKey": encryptionKey, + } + + _, err = cfClient.CreateUserProvidedServiceInstance(userProvidedServiceName, space.Guid, upsCredentials) + if err != nil { + return false, "", fmt.Errorf("Failed to create user-provided service %s: %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 doesUpsExist(userProvidedServiceName string, cliConnection plugin.CliConnection, cfClient cfrestclient.CloudFoundryOperationsExtended) (bool, error) { + space, errSpace := cliConnection.GetCurrentSpace() + if errSpace != nil { + return false, fmt.Errorf("Cannot determine the current space") + } + spaceGuid := space.Guid + + _, errServiceInstance := 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 GetUpsName(mtaId, namespace string) string { + if strings.TrimSpace(namespace) == "" { + return "__mta-secure-" + mtaId + } + return "__mta-secure-" + mtaId + "-" + namespace +} + +func GetRandomisedUpsName(mtaId, namespace string) (disposableUpsName string, err error) { + randomisedPart, err := getRandomEncryptionKey() + if err != nil { + return "", err + } + resultSuffix := randomisedPart[:7] + + if strings.TrimSpace(namespace) == "" { + return "__mta-secure-" + mtaId + "-" + resultSuffix, nil + } + return "__mta-secure-" + mtaId + "-" + namespace + "-" + resultSuffix, nil +}