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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions clients/cfrestclient/cloud_foundry_operations_extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions clients/cfrestclient/fakes/fake_cloud_foundry_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions clients/cfrestclient/rest_cloud_foundry_client_extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
2 changes: 1 addition & 1 deletion commands/blue_green_deploy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
133 changes: 130 additions & 3 deletions commands/deploy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package commands

import (
"bufio"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
Expand All @@ -18,11 +20,14 @@ import (
"code.cloudfoundry.org/cli/v8/cf/terminal"
"code.cloudfoundry.org/cli/v8/plugin"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/cfrestclient/resilient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands/retrier"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/configuration"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/log"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/secure_parameters"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/util"
"gopkg.in/cheggaaa/pb.v1"
Expand Down Expand Up @@ -53,6 +58,7 @@ const (
applyNamespaceAsSuffix = "apply-namespace-as-suffix"
maxNamespaceSize = 36
shouldBackupPreviousVersionOpt = "backup-previous-version"
requireSecureParameters = "require-secure-parameters"
)

type listFlag struct {
Expand Down Expand Up @@ -87,16 +93,23 @@ type DeployCommand struct {

FileUrlReader fs.File
FileUrlReadTimeout time.Duration
CfClient cfrestclient.CloudFoundryOperationsExtended
}

// NewDeployCommand creates a new deploy command.
func NewDeployCommand() *DeployCommand {
baseCmd := &BaseCommand{flagsParser: deployCommandLineArgumentsParser{}, flagsValidator: deployCommandFlagsValidator{}}
deployCmd := &DeployCommand{baseCmd, deployProcessParametersSetter(), &deployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second}
deployCmd := &DeployCommand{baseCmd, deployProcessParametersSetter(), &deployCommandProcessTypeProvider{}, os.Stdin, 30 * time.Second, nil}
baseCmd.Command = deployCmd
return deployCmd
}

func (c *DeployCommand) Initialize(name string, cliConnection plugin.CliConnection) {
c.BaseCommand.Initialize(name, cliConnection)
delegate := cfrestclient.NewCloudFoundryRestClient(cliConnection)
c.CfClient = resilient.NewResilientCloudFoundryClient(delegate, maxRetriesCount, retryIntervalInSeconds)
}

// GetPluginCommand returns the plugin command details
func (c *DeployCommand) GetPluginCommand() plugin.Command {
return plugin.Command{
Expand All @@ -105,13 +118,13 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command {
UsageDetails: plugin.Usage{
Usage: `Deploy a multi-target app archive

cf deploy MTA [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u URL] [-f] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]
cf deploy MTA [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u URL] [-f] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--require-secure-parameters] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]

Perform action on an active deploy operation
cf deploy -i OPERATION_ID -a ACTION [-u URL]

Deploy a multi-target app archive referenced by a remote URL
<write MTA archive URL to STDOUT> | cf deploy [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u MTA_CONTROLLER_URL] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]` + util.UploadEnvHelpText,
<write MTA archive URL to STDOUT> | cf deploy [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u MTA_CONTROLLER_URL] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [require-secure-parameters] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]` + util.UploadEnvHelpText,

Options: map[string]string{
extDescriptorsOpt: "Extension descriptors",
Expand Down Expand Up @@ -146,6 +159,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command {
util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds",
util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds",
util.GetShortOption(shouldBackupPreviousVersionOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Backup previous version of applications, use new cli command \"rollback-mta\" to rollback to the previous version",
util.GetShortOption(requireSecureParameters): "(EXPERIMENTAL) Pass secrets to the deploy service in a secure way",
},
},
}
Expand All @@ -171,6 +185,7 @@ func deployProcessParametersSetter() ProcessParametersSetter {
processBuilder.Parameter("appsStageTimeout", GetStringOpt(stageTimeoutOpt, flags))
processBuilder.Parameter("appsUploadTimeout", GetStringOpt(uploadTimeoutOpt, flags))
processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags))
processBuilder.Parameter("isSecurityEnabled", strconv.FormatBool(GetBoolOpt(requireSecureParameters, flags)))

var lastSetValue string = ""
for i := 0; i < len(os.Args); i++ {
Expand Down Expand Up @@ -225,6 +240,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) {
flags.String(uploadTimeoutOpt, "", "")
flags.String(taskExecutionTimeoutOpt, "", "")
flags.Bool(shouldBackupPreviousVersionOpt, false, "")
flags.Bool(requireSecureParameters, false, "")
}

func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus {
Expand Down Expand Up @@ -271,6 +287,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
sequentialUpload := conf.GetUploadChunksSequentially()
disableProgressBar := conf.GetDisableUploadProgressBar()
fileUploader := NewFileUploader(mtaClient, namespace, uploadChunkSize, sequentialUpload, disableProgressBar)
var yamlBytes []byte

if isUrl {
var fileId string
Expand Down Expand Up @@ -321,6 +338,41 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
return Failure
}

if GetBoolOpt(requireSecureParameters, flags) {
// Collect special ENVs: __MTA___<name>, __MTA_JSON___<name>, __MTA_CERT___<name>
parameters, err := secure_parameters.CollectFromEnv("__MTA")
if err != nil {
ui.Failed("Secure parameters error: %s", err)
return Failure
}

if len(parameters) == 0 {
ui.Failed("No secure parameters found in environment. Set variables like __MTA___<name>, __MTA_JSON___<name>, or __MTA_CERT___<name>.")
return Failure
}

userProvidedServiceName := getUpsName(mtaId, namespace)

isUpsCreated, _, err := c.validateUpsExistsOrElseCreateIt(userProvidedServiceName, "v1")
if err != nil {
ui.Failed("Could not ensure user-provided service %s: %v", userProvidedServiceName, err)
return Failure
}

if isUpsCreated {
ui.Say("Created user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName))
} else {
ui.Say("Using existing user-provided service %s for secure parameters.", terminal.EntityNameColor(userProvidedServiceName))
}

schemaVer := descriptor.SchemaVersion
yamlBytes, err = secure_parameters.BuildSecureExtension(parameters, mtaId, schemaVer)
if err != nil {
ui.Failed("Could not build secure extension: %s", err)
return Failure
}
}

// Upload the MTA archive file
uploadedArchivePartIds, uploadStatus = c.uploadFiles([]string{mtaArchivePath}, fileUploader)
if uploadStatus == Failure {
Expand Down Expand Up @@ -348,6 +400,15 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
return Failure
}

if GetBoolOpt(requireSecureParameters, flags) {
secureFileID, err := fileUploader.UploadBytes("__mta.secure.mtaext", yamlBytes)
if err != nil {
ui.Failed("Could not upload secure extension: %s", err)
return Failure
}
uploadedExtDescriptorIDs = append(uploadedExtDescriptorIDs, secureFileID)
}

// Build the process instance
processBuilder := NewDeploymentStrategy(flags, c.processTypeProvider).CreateProcessBuilder()
processBuilder.Namespace(namespace)
Expand All @@ -374,6 +435,72 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,
return executionMonitor.Monitor()
}

func getUpsName(mtaID, namespace string) string {
if strings.TrimSpace(namespace) == "" {
return "__mta-secure-" + mtaID
}
return "__mta-secure-" + mtaID + "-" + namespace
}

func (c *DeployCommand) validateUpsExistsOrElseCreateIt(userProvidedServiceName, keyID string) (upsCreatedByTheCli bool, encryptionKeyResult string, err error) {
doesUpsExist, err := c.doesUpsExist(userProvidedServiceName)
if err != nil {
return false, "", fmt.Errorf("Check if the UPS exists: %w", err)
}
if doesUpsExist {
return false, "", nil
}

encryptionKey, err := getRandomEncryptionKey()
if err != nil {
return false, "", fmt.Errorf("Error while generating AES-256 encryption key: %w", err)
}

upsCredentials := map[string]string{
"encryptionKey": encryptionKey,
"keyId": keyID,
}
jsonBody, _ := json.Marshal(upsCredentials)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the error from json.Marshal ignored here?


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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there some library that can do this for us?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not manage to find anything in particular - saw that many use the base64 encoding which I tried but since for every 3 bytes it spits out 4 characters - I was getting 43 chars out of my 32 bytes. Also since I wanted the constraint of having only alphanumerical values and not any / or + which base64 brings, I decided the use the explicit "alphabet" of allowed symbols and then just randomly choose each character from it.

const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"

encryptionKeyBytes := make([]byte, 32)
if _, err := rand.Read(encryptionKeyBytes); err != nil {
return "", err
}

for i := range encryptionKeyBytes {
encryptionKeyBytes[i] = alphabet[int(encryptionKeyBytes[i]&63)]
}

return string(encryptionKeyBytes), nil
Comment on lines +479 to +483
Copy link
Contributor

@IvanBorislavovDimitrov IvanBorislavovDimitrov Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really creates a proper 256 bits keys? Why not something like:

func generateAES256Key() ([]byte, error) {
key := make([]byte, 32) // 256 bits
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return string([]byte(fmt.Sprintf("%x", key))), nil
}

Why is alphabet needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does. I tried doing it like that myself in the beginning, but the problem was that the argument in the Sprintf method determines the encoding used, thus giving different lenghts as a result. Using the fmt.Sprinf("%x", key) with the "x" argument, Go does a base16 encoding which actually converts the 32 generated bytes into 64 characters. The base16 encoding doubles the size of the byte input when it is converted to a string output, since base16 uses only the letters from 'a' to 'f' and 1 through 9, 16 symbols altogether, and since one byte can be between 0-255 - in order to represent all the 256 different values we need to have for each of the 16 unique symbols, 16 more - meaning 16x16 = 16^2 = 256

}

func (c *DeployCommand) doesUpsExist(userProvidedServiceName string) (bool, error) {
space, errSpace := c.cliConnection.GetCurrentSpace()
if errSpace != nil {
return false, fmt.Errorf("Cannot determine the current space")
}
spaceGuid := space.Guid

_, errServiceInstance := c.CfClient.GetServiceInstanceByName(userProvidedServiceName, spaceGuid)
if errServiceInstance != nil {
if errServiceInstance.Error() == "service instance not found" {
return false, nil
}
return false, fmt.Errorf("Error while checking if the UPS for secure encryption exists: %w", errServiceInstance)
}

return true, nil
}

func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) {
switch castedMtaArchive := rawMtaArchive.(type) {
case *url.URL:
Expand Down
Loading
Loading