From 8e2c8bce06476d86a2492b4800444ad80fe2970c Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:18:35 +0000 Subject: [PATCH 1/3] feat: Add galasactl secrets delete command Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 20 ++ README.md | 12 + docs/generated/errors-list.md | 7 + docs/generated/galasactl.md | 1 + docs/generated/galasactl_secrets.md | 27 ++ docs/generated/galasactl_secrets_delete.md | 31 +++ pkg/cmd/commandCollection.go | 26 ++ pkg/cmd/secrets.go | 95 +++++++ pkg/cmd/secretsDelete.go | 140 +++++++++++ pkg/cmd/secretsDelete_test.go | 58 +++++ pkg/cmd/secrets_test.go | 59 +++++ pkg/errors/errorMessage.go | 12 +- pkg/errors/galasaAPIError.go | 52 ++++ pkg/runs/runsDelete.go | 54 +--- pkg/secrets/secrets.go | 23 ++ pkg/secrets/secretsDelete.go | 77 ++++++ pkg/secrets/secretsDelete_test.go | 274 +++++++++++++++++++++ 17 files changed, 917 insertions(+), 51 deletions(-) create mode 100644 docs/generated/galasactl_secrets.md create mode 100644 docs/generated/galasactl_secrets_delete.md create mode 100644 pkg/cmd/secrets.go create mode 100644 pkg/cmd/secretsDelete.go create mode 100644 pkg/cmd/secretsDelete_test.go create mode 100644 pkg/cmd/secrets_test.go create mode 100644 pkg/secrets/secrets.go create mode 100644 pkg/secrets/secretsDelete.go create mode 100644 pkg/secrets/secretsDelete_test.go diff --git a/.secrets.baseline b/.secrets.baseline index c469e8b5..99a6d52b 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -76,6 +76,16 @@ } ], "results": { + "dockerfiles/certs/ibminter.pem": [ + { + "hashed_secret": "b3723414cb4a90ac8c2bc504ea01923fe5fccc8a", + "is_secret": false, + "is_verified": false, + "line_number": 28, + "type": "Artifactory Credentials", + "verified_result": null + } + ], "gherkin-docs.md": [ { "hashed_secret": "c61db10457a740b07845146f2d1b391c133a6ebf", @@ -125,6 +135,16 @@ "type": "Hex High Entropy String", "verified_result": null } + ], + "pkg/secrets/secretsDelete_test.go": [ + { + "hashed_secret": "2dfbe3ec00a96d6f711d9a70f78be17f6fd574ca", + "is_secret": false, + "is_verified": false, + "line_number": 83, + "type": "Secret Keyword", + "verified_result": null + } ] }, "version": "0.13.1+ibm.62.dss", diff --git a/README.md b/README.md index 7bf1641a..8ff0f92c 100644 --- a/README.md +++ b/README.md @@ -632,7 +632,19 @@ galasactl resources delete -f my_resources.yaml For a complete list of supported parameters see [here](./docs/generated/galasactl_resources_delete.md). +## secrets delete +This command deletes a secret with the given name from the Galasa Ecosystem's credentials store. The name of the secret to be deleted must be provided using the `--name` flag. + +### Examples + +To delete a secret named `SYSTEM1`, run the following command: + +``` +galasactl secrets delete --name SYSTEM1 +``` + +For a complete list of supported parameters see [here](./docs/generated/galasactl_secrets_delete.md). ## Reference Material diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index d3ccc523..3d2ca908 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -162,6 +162,13 @@ The `galasactl` tool can generate the following errors: - GAL1164E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - GAL1165E: '{}' is not supported as a valid login ID. Login ID should not contain spaces. - GAL1166E: The loginId provided by the --user field cannot be an empty string. +- GAL1167E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. +- GAL1168E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1169E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1170E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' +- GAL1171E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces. +- GAL1173E: An attempt to delete a secret named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/docs/generated/galasactl.md b/docs/generated/galasactl.md index fab02d62..9729626d 100644 --- a/docs/generated/galasactl.md +++ b/docs/generated/galasactl.md @@ -22,5 +22,6 @@ A tool for controlling Galasa resources using the command-line. * [galasactl properties](galasactl_properties.md) - Manages properties in an ecosystem * [galasactl resources](galasactl_resources.md) - Manages resources in an ecosystem * [galasactl runs](galasactl_runs.md) - Manage test runs in the ecosystem +* [galasactl secrets](galasactl_secrets.md) - Manage secrets stored in the Galasa service's credentials store * [galasactl users](galasactl_users.md) - Manages users in an ecosystem diff --git a/docs/generated/galasactl_secrets.md b/docs/generated/galasactl_secrets.md new file mode 100644 index 00000000..48ac5e1b --- /dev/null +++ b/docs/generated/galasactl_secrets.md @@ -0,0 +1,27 @@ +## galasactl secrets + +Manage secrets stored in the Galasa service's credentials store + +### Synopsis + +The parent command for operations to manipulate secrets in the Galasa service's credentials store + +### Options + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + -h, --help Displays the options for the 'secrets' command. +``` + +### Options inherited from parent commands + +``` + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. +``` + +### SEE ALSO + +* [galasactl](galasactl.md) - CLI for Galasa +* [galasactl secrets delete](galasactl_secrets_delete.md) - Deletes a secret from the credentials store + diff --git a/docs/generated/galasactl_secrets_delete.md b/docs/generated/galasactl_secrets_delete.md new file mode 100644 index 00000000..a48273aa --- /dev/null +++ b/docs/generated/galasactl_secrets_delete.md @@ -0,0 +1,31 @@ +## galasactl secrets delete + +Deletes a secret from the credentials store + +### Synopsis + +Deletes a secret from the credentials store + +``` +galasactl secrets delete [flags] +``` + +### Options + +``` + -h, --help Displays the options for the 'secrets delete' command. + --name string A mandatory flag that identifies the secret to be created or manipulated. +``` + +### Options inherited from parent commands + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. +``` + +### SEE ALSO + +* [galasactl secrets](galasactl_secrets.md) - Manage secrets stored in the Galasa service's credentials store + diff --git a/pkg/cmd/commandCollection.go b/pkg/cmd/commandCollection.go index dec89295..8ae99d8f 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -59,6 +59,8 @@ const ( COMMAND_NAME_RESOURCES_CREATE = "resources create" COMMAND_NAME_RESOURCES_UPDATE = "resources update" COMMAND_NAME_RESOURCES_DELETE = "resources delete" + COMMAND_NAME_SECRETS = "secrets" + COMMAND_NAME_SECRETS_DELETE = "secrets delete" COMMAND_NAME_USERS = "users" COMMAND_NAME_USERS_GET = "users get" ) @@ -142,6 +144,10 @@ func (commands *commandCollectionImpl) init(factory spi.Factory) error { err = commands.addResourcesCommands(factory, rootCommand) } + if err == nil { + err = commands.addSecretsCommands(factory, rootCommand) + } + if err == nil { err = commands.addUsersCommands(factory, rootCommand) } @@ -375,6 +381,26 @@ func (commands *commandCollectionImpl) addResourcesCommands(factory spi.Factory, return err } +func (commands *commandCollectionImpl) addSecretsCommands(factory spi.Factory, rootCommand spi.GalasaCommand) error { + + var err error + var secretsCommand spi.GalasaCommand + var secretsDeleteCommand spi.GalasaCommand + + secretsCommand, err = NewSecretsCmd(rootCommand) + + if err == nil { + secretsDeleteCommand, err = NewSecretsDeleteCommand(factory, secretsCommand, rootCommand) + } + + if err == nil { + commands.commandMap[secretsCommand.Name()] = secretsCommand + commands.commandMap[secretsDeleteCommand.Name()] = secretsDeleteCommand + } + + return err +} + func (commands *commandCollectionImpl) addUsersCommands(factory spi.Factory, rootCommand spi.GalasaCommand) error { var err error diff --git a/pkg/cmd/secrets.go b/pkg/cmd/secrets.go new file mode 100644 index 00000000..54683e98 --- /dev/null +++ b/pkg/cmd/secrets.go @@ -0,0 +1,95 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "github.com/galasa-dev/cli/pkg/spi" + "github.com/spf13/cobra" +) + +type SecretsCmdValues struct { + bootstrap string + name string +} + +type SecretsCommand struct { + cobraCommand *cobra.Command + values *SecretsCmdValues +} + +// ------------------------------------------------------------------------------------------------ +// Constructors +// ------------------------------------------------------------------------------------------------ + +func NewSecretsCmd(rootCommand spi.GalasaCommand) (spi.GalasaCommand, error) { + cmd := new(SecretsCommand) + err := cmd.init(rootCommand) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public functions +// ------------------------------------------------------------------------------------------------ + +func (cmd *SecretsCommand) Name() string { + return COMMAND_NAME_SECRETS +} + +func (cmd *SecretsCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *SecretsCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private functions +// ------------------------------------------------------------------------------------------------ + +func (cmd *SecretsCommand) init(rootCmd spi.GalasaCommand) error { + + var err error + + cmd.values = &SecretsCmdValues{} + cmd.cobraCommand, err = cmd.createCobraCommand(rootCmd) + + return err +} + +func (cmd *SecretsCommand) createCobraCommand(rootCommand spi.GalasaCommand) (*cobra.Command, error) { + + var err error + + secretsCobraCmd := &cobra.Command{ + Use: "secrets", + Short: "Manage secrets stored in the Galasa service's credentials store", + Long: "The parent command for operations to manipulate secrets in the Galasa service's credentials store", + } + + addBootstrapFlag(secretsCobraCmd, &cmd.values.bootstrap) + + rootCommand.CobraCommand().AddCommand(secretsCobraCmd) + + return secretsCobraCmd, err +} + +func addSecretNameFlag(cmd *cobra.Command, isMandatory bool, secretsCmdValues *SecretsCmdValues) { + + flagName := "name" + var description string + if isMandatory { + description = "A mandatory flag that identifies the secret to be created or manipulated." + } else { + description = "An optional flag that identifies the secret to be retrieved." + } + + cmd.Flags().StringVar(&secretsCmdValues.name, flagName, "", description) + + if isMandatory { + cmd.MarkFlagRequired(flagName) + } +} diff --git a/pkg/cmd/secretsDelete.go b/pkg/cmd/secretsDelete.go new file mode 100644 index 00000000..bcc361bb --- /dev/null +++ b/pkg/cmd/secretsDelete.go @@ -0,0 +1,140 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "log" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/secrets" + "github.com/galasa-dev/cli/pkg/spi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +type SecretsDeleteCommand struct { + cobraCommand *cobra.Command +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewSecretsDeleteCommand( + factory spi.Factory, + secretsDeleteCommand spi.GalasaCommand, + rootCmd spi.GalasaCommand, +) (spi.GalasaCommand, error) { + + cmd := new(SecretsDeleteCommand) + + err := cmd.init(factory, secretsDeleteCommand, rootCmd) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *SecretsDeleteCommand) Name() string { + return COMMAND_NAME_SECRETS_DELETE +} + +func (cmd *SecretsDeleteCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *SecretsDeleteCommand) Values() interface{} { + return nil +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ +func (cmd *SecretsDeleteCommand) init(factory spi.Factory, secretsCommand spi.GalasaCommand, rootCmd spi.GalasaCommand) error { + var err error + + cmd.cobraCommand, err = cmd.createCobraCmd(factory, secretsCommand, rootCmd.Values().(*RootCmdValues)) + + return err +} + +func (cmd *SecretsDeleteCommand) createCobraCmd( + factory spi.Factory, + secretsCommand spi.GalasaCommand, + rootCommandValues *RootCmdValues, +) (*cobra.Command, error) { + + var err error + + secretsCommandValues := secretsCommand.Values().(*SecretsCmdValues) + secretsDeleteCobraCmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a secret from the credentials store", + Long: "Deletes a secret from the credentials store", + Aliases: []string{COMMAND_NAME_SECRETS_DELETE}, + RunE: func(cobraCommand *cobra.Command, args []string) error { + return cmd.executeSecretsDelete(factory, secretsCommand.Values().(*SecretsCmdValues), rootCommandValues) + }, + } + + addSecretNameFlag(secretsDeleteCobraCmd, true, secretsCommandValues) + + secretsCommand.CobraCommand().AddCommand(secretsDeleteCobraCmd) + + return secretsDeleteCobraCmd, err +} + +func (cmd *SecretsDeleteCommand) executeSecretsDelete( + factory spi.Factory, + secretsCmdValues *SecretsCmdValues, + rootCmdValues *RootCmdValues, +) error { + + var err error + // Operations on the file system will all be relative to the current folder. + fileSystem := factory.GetFileSystem() + + err = utils.CaptureLog(fileSystem, rootCmdValues.logFileName) + + if err == nil { + rootCmdValues.isCapturingLogs = true + + log.Println("Galasa CLI - Delete a secret from the credentials store") + + env := factory.GetEnvironment() + + var galasaHome spi.GalasaHome + galasaHome, err = utils.NewGalasaHome(fileSystem, env, rootCmdValues.CmdParamGalasaHomePath) + if err == nil { + + var urlService *api.RealUrlResolutionService = new(api.RealUrlResolutionService) + var bootstrapData *api.BootstrapData + bootstrapData, err = api.LoadBootstrap(galasaHome, fileSystem, env, secretsCmdValues.bootstrap, urlService) + if err == nil { + + var console = factory.GetStdOutConsole() + + apiServerUrl := bootstrapData.ApiServerURL + log.Printf("The API server is at '%s'\n", apiServerUrl) + + authenticator := factory.GetAuthenticator( + apiServerUrl, + galasaHome, + ) + + var apiClient *galasaapi.APIClient + apiClient, err = authenticator.GetAuthenticatedAPIClient() + + byteReader := factory.GetByteReader() + + if err == nil { + err = secrets.DeleteSecret(secretsCmdValues.name, console, apiClient, byteReader) + } + } + } + } + return err +} diff --git a/pkg/cmd/secretsDelete_test.go b/pkg/cmd/secretsDelete_test.go new file mode 100644 index 00000000..1c8fe7d7 --- /dev/null +++ b/pkg/cmd/secretsDelete_test.go @@ -0,0 +1,58 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "testing" + + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestCommandListContainsSecretsDeleteCommand(t *testing.T) { + /// Given... + factory := utils.NewMockFactory() + commands, _ := NewCommandCollection(factory) + + // When... + secretsCommand, err := commands.GetCommand(COMMAND_NAME_SECRETS_DELETE) + assert.Nil(t, err) + + // Then... + assert.NotNil(t, secretsCommand) + assert.Equal(t, COMMAND_NAME_SECRETS_DELETE, secretsCommand.Name()) + assert.Nil(t, secretsCommand.Values()) +} + +func TestSecretsDeleteHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + + var args []string = []string{"secrets", "delete", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + // Check what the user saw is reasonable. + checkOutput("Deletes a secret from the credentials store", "", factory, t) + + assert.Nil(t, err) +} + +func TestSecretsDeleteNoNameFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "delete"} + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: required flag(s) \"name\" not set", factory, t) +} diff --git a/pkg/cmd/secrets_test.go b/pkg/cmd/secrets_test.go new file mode 100644 index 00000000..98921163 --- /dev/null +++ b/pkg/cmd/secrets_test.go @@ -0,0 +1,59 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "testing" + + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestCommandListContainsSecretsCommand(t *testing.T) { + /// Given... + factory := utils.NewMockFactory() + commands, _ := NewCommandCollection(factory) + + // When... + secretsCommand, err := commands.GetCommand(COMMAND_NAME_SECRETS) + assert.Nil(t, err) + + // Then... + assert.NotNil(t, secretsCommand) + assert.Equal(t, COMMAND_NAME_SECRETS, secretsCommand.Name()) + assert.NotNil(t, secretsCommand.Values()) + assert.IsType(t, &SecretsCmdValues{}, secretsCommand.Values()) +} + +func TestSecretsHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + + var args []string = []string{"secrets", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + // Check what the user saw is reasonable. + checkOutput("The parent command for operations to manipulate secrets in the Galasa service's credentials store", "", factory, t) + + assert.Nil(t, err) +} + +func TestSecretsNoCommandsProducesUsageReport(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets"} + + // When... + err := Execute(factory, args) + + // Then... + assert.Nil(t, err) + + checkOutput("Usage:\n galasactl secrets [command]", "", factory, t) +} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 598f58c9..f4ecb3e2 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -260,8 +260,16 @@ var ( GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT = NewMessageType("GAL1161E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1161, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR = NewMessageType("GAL1162E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1162, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON = NewMessageType("GAL1164E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1164, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON = NewMessageType("GAL1164E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1164, STACK_TRACE_NOT_WANTED) + + GALASA_ERROR_DELETE_SECRET_NO_RESPONSE_CONTENT = NewMessageType("GAL1167E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server.", 1167, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_SECRET_RESPONSE_BODY_UNREADABLE = NewMessageType("GAL1168E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1168, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_SECRET_UNPARSEABLE_CONTENT = NewMessageType("GAL1169E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1169, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_SECRET_SERVER_REPORTED_ERROR = NewMessageType("GAL1170E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1170, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_SECRET_EXPLANATION_NOT_JSON = NewMessageType("GAL1171E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1171, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces.", 1172, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_SECRET_REQUEST_FAILED = NewMessageType("GAL1173E: An attempt to delete a secret named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1173, STACK_TRACE_NOT_WANTED) // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/errors/galasaAPIError.go b/pkg/errors/galasaAPIError.go index eb04bef8..bce7a074 100644 --- a/pkg/errors/galasaAPIError.go +++ b/pkg/errors/galasaAPIError.go @@ -9,6 +9,9 @@ package errors import ( "encoding/json" "log" + "net/http" + + "github.com/galasa-dev/cli/pkg/spi" ) type GalasaAPIError struct { @@ -41,3 +44,52 @@ func GetApiErrorFromResponseBytes(body []byte, marshallingErrorLambda func(marsh } return apiError, err } + +func HttpResponseToGalasaError( + response *http.Response, + identifier string, + byteReader spi.ByteReader, + errorMsgUnexpectedStatusCodeNoResponseBody *MessageType, + errorMsgUnableToReadResponseBody *MessageType, + errorMsgResponsePayloadInWrongFormat *MessageType, + errorMsgReceivedFromApiServer *MessageType, + errorMsgResponseContentTypeNotJson *MessageType, +) error { + defer response.Body.Close() + var err error + var responseBodyBytes []byte + statusCode := response.StatusCode + + if response.ContentLength == 0 { + log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) + err = NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) + } else { + + contentType := response.Header.Get("Content-Type") + if contentType != "application/json" { + err = NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) + } else { + responseBodyBytes, err = byteReader.ReadAll(response.Body) + if err != nil { + err = NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) + } else { + + var errorFromServer *GalasaAPIError + errorFromServer, err = GetApiErrorFromResponseBytes( + responseBodyBytes, + func (marshallingError error) error { + log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) + return NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode, marshallingError) + }, + ) + + if err == nil { + // server returned galasa api error structure we understand. + log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) + err = NewGalasaError(errorMsgReceivedFromApiServer, identifier, statusCode, errorFromServer.Message) + } + } + } + } + return err +} diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 7aae4f46..1e25a312 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -84,6 +84,10 @@ func deleteRuns( apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) httpResponse, err = apicall.Execute() + + if httpResponse != nil { + defer httpResponse.Body.Close() + } // 200-299 http status codes manifest in an error. if err != nil { @@ -91,7 +95,7 @@ func deleteRuns( // We never got a response, error sending it or something ? err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) } else { - err = httpResponseToGalasaError( + err = galasaErrors.HttpResponseToGalasaError( httpResponse, runName, byteReader, @@ -115,51 +119,3 @@ func deleteRuns( return err } -func httpResponseToGalasaError( - response *http.Response, - identifier string, - byteReader spi.ByteReader, - errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, - errorMsgUnableToReadResponseBody *galasaErrors.MessageType, - errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, - errorMsgReceivedFromApiServer *galasaErrors.MessageType, - errorMsgResponseContentTypeNotJson *galasaErrors.MessageType, -) error { - defer response.Body.Close() - var err error - var responseBodyBytes []byte - statusCode := response.StatusCode - - if response.ContentLength == 0 { - log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) - err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) - } else { - - contentType := response.Header.Get("Content-Type") - if contentType != "application/json" { - err = galasaErrors.NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) - } else { - responseBodyBytes, err = byteReader.ReadAll(response.Body) - if err != nil { - err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) - } else { - - var errorFromServer *galasaErrors.GalasaAPIError - errorFromServer, err = galasaErrors.GetApiErrorFromResponseBytes( - responseBodyBytes, - func (marshallingError error) error { - log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) - return galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode, marshallingError) - }, - ) - - if err == nil { - // server returned galasa api error structure we understand. - log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) - err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, statusCode, errorFromServer.Message) - } - } - } - } - return err -} diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go new file mode 100644 index 00000000..f6ef23d8 --- /dev/null +++ b/pkg/secrets/secrets.go @@ -0,0 +1,23 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package secrets + +import ( + "strings" + + galasaErrors "github.com/galasa-dev/cli/pkg/errors" +) + +func validateSecretName(secretName string) error { + var err error + secretName = strings.TrimSpace(secretName) + + if secretName == "" || strings.ContainsAny(secretName, " \n\t") { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_NAME) + } + return err +} diff --git a/pkg/secrets/secretsDelete.go b/pkg/secrets/secretsDelete.go new file mode 100644 index 00000000..42af9e01 --- /dev/null +++ b/pkg/secrets/secretsDelete.go @@ -0,0 +1,77 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package secrets + +import ( + "context" + "log" + "net/http" + + "github.com/galasa-dev/cli/pkg/embedded" + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/spi" +) + +func DeleteSecret( + secretName string, + console spi.Console, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + var err error + + err = validateSecretName(secretName) + if err == nil { + log.Printf("Secret name validated OK") + err = sendDeleteSecretRequest(secretName, apiClient, byteReader) + } + log.Printf("SecretsDelete exiting. err is %v\n", err) + return err +} + +func sendDeleteSecretRequest( + secretName string, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + var err error + var httpResponse *http.Response + var context context.Context = context.Background() + var restApiVersion string + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + + if err == nil { + httpResponse, err = apiClient.SecretsAPIApi.DeleteSecret(context, secretName). + ClientApiVersion(restApiVersion). + Execute() + + if httpResponse != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + if httpResponse == nil { + // We never got a response, error sending it or something? + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_DELETE_SECRET_REQUEST_FAILED, err.Error()) + } else { + err = galasaErrors.HttpResponseToGalasaError( + httpResponse, + secretName, + byteReader, + galasaErrors.GALASA_ERROR_DELETE_SECRET_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_SECRET_RESPONSE_BODY_UNREADABLE, + galasaErrors.GALASA_ERROR_DELETE_SECRET_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_SECRET_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_DELETE_SECRET_EXPLANATION_NOT_JSON, + ) + } + } + } + return err +} diff --git a/pkg/secrets/secretsDelete_test.go b/pkg/secrets/secretsDelete_test.go new file mode 100644 index 00000000..fa7067fd --- /dev/null +++ b/pkg/secrets/secretsDelete_test.go @@ -0,0 +1,274 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package secrets + +import ( + "net/http" + "strconv" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestCanDeleteASecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + deleteSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "DeleteSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") +} + +func TestDeleteASecretWithBlankNameDisplaysError(t *testing.T) { + // Given... + secretName := " " + + // The client-side validation should fail, so no HTTP interactions will be performed + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "DeleteSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1172E") + assert.Contains(t, errorMsg, " Invalid secret name provided") +} + +func TestDeleteNonExistantSecretDisplaysError(t *testing.T) { + // Given... + nonExistantSecret := "secretDoesNotExist123" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + nonExistantSecret, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusNotFound) + writer.Write([]byte(`{ "error_message": "No such secret exists" }`)) + } + + + interactions := []utils.HttpInteraction{ deleteSecretInteraction } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + nonExistantSecret, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsDelete did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, nonExistantSecret) + assert.Contains(t, errorMsg, "GAL1170E") + assert.Contains(t, errorMsg, "Error details from the server are: 'No such secret exists'") +} + +func TestSecretsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + } + + interactions := []utils.HttpInteraction{ + deleteSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsDelete did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText , secretName) + assert.Contains(t, consoleText , "GAL1167E") +} + +func TestSecretsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + + interactions := []utils.HttpInteraction{ + deleteSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsDelete did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1171E") + assert.Contains(t, consoleText, "Error details from the server are not in the json format") +} + +func TestSecretsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) + } + + interactions := []utils.HttpInteraction{ + deleteSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsDelete did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1169E") + assert.Contains(t, consoleText, "Error details from the server are not in a valid json format") + assert.Contains(t, consoleText, "Cause: 'unexpected end of JSON input'") +} + +func TestSecretsDeleteFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + + // Create the expected HTTP interactions with the API server + deleteSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodDelete) + deleteSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + deleteSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := DeleteSecret( + secretName, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsDelete returned an unexpected error") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1168E") + assert.Contains(t, consoleText, "Error details from the server could not be read") +} From 8c9f31d5a492e33f7158351ed3fde5046ef02da7 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:44:59 +0000 Subject: [PATCH 2/3] feat: Add validation to secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- docs/generated/errors-list.md | 2 +- pkg/errors/errorMessage.go | 2 +- pkg/secrets/secrets.go | 19 ++++++++++++++++--- pkg/secrets/secretsDelete.go | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 3d2ca908..f535bcc8 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -167,7 +167,7 @@ The `galasactl` tool can generate the following errors: - GAL1169E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' - GAL1170E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1171E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. -- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces. +- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set. - GAL1173E: An attempt to delete a secret named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index f4ecb3e2..decf13f1 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -268,7 +268,7 @@ var ( GALASA_ERROR_DELETE_SECRET_UNPARSEABLE_CONTENT = NewMessageType("GAL1169E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1169, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_SERVER_REPORTED_ERROR = NewMessageType("GAL1170E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1170, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_EXPLANATION_NOT_JSON = NewMessageType("GAL1171E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1171, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces.", 1172, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set.", 1172, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_REQUEST_FAILED = NewMessageType("GAL1173E: An attempt to delete a secret named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1173, STACK_TRACE_NOT_WANTED) // Warnings... diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index f6ef23d8..e73c5ebe 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -12,12 +12,25 @@ import ( galasaErrors "github.com/galasa-dev/cli/pkg/errors" ) -func validateSecretName(secretName string) error { +func validateSecretName(secretName string) (string, error) { var err error secretName = strings.TrimSpace(secretName) - if secretName == "" || strings.ContainsAny(secretName, " \n\t") { + if secretName == "" || strings.ContainsAny(secretName, " \n\t") || !isLatin1(secretName) { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_NAME) } - return err + return secretName, err +} + +// Checks if a given string contains only characters in the Latin-1 character set (codepoints 0-255), +// returning true if so, and false otherwise +func isLatin1(str string) bool { + isValidLatin1 := true + for _, character := range str { + if character > 255 { + isValidLatin1 = false + break + } + } + return isValidLatin1 } diff --git a/pkg/secrets/secretsDelete.go b/pkg/secrets/secretsDelete.go index 42af9e01..bf192ba4 100644 --- a/pkg/secrets/secretsDelete.go +++ b/pkg/secrets/secretsDelete.go @@ -25,7 +25,7 @@ func DeleteSecret( ) error { var err error - err = validateSecretName(secretName) + secretName, err = validateSecretName(secretName) if err == nil { log.Printf("Secret name validated OK") err = sendDeleteSecretRequest(secretName, apiClient, byteReader) From 10e2a5fb27c419bfaed5ed8ba935fc312d9a1c4b Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:20:56 +0000 Subject: [PATCH 3/3] Empty commit to kick off build Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>