From 33de92e7a96e7cf68a0a4205b642e3aa4e56724c Mon Sep 17 00:00:00 2001 From: Jack Morgan Date: Tue, 26 Mar 2024 17:02:12 -0700 Subject: [PATCH 1/4] ORION-3127: initial implementation for fsoc solution delete --- cmd/solution/delete.go | 181 +++++++++++++++++++++++++++++++++++++++ cmd/solution/solution.go | 1 + 2 files changed, 182 insertions(+) create mode 100644 cmd/solution/delete.go diff --git a/cmd/solution/delete.go b/cmd/solution/delete.go new file mode 100644 index 00000000..64cb7373 --- /dev/null +++ b/cmd/solution/delete.go @@ -0,0 +1,181 @@ +// Copyright 2024 Cisco Systems, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package solution + +import ( + "fmt" + "net/url" + "reflect" + "time" + + "github.com/apex/log" + "github.com/spf13/cobra" + + "github.com/cisco-open/fsoc/config" + "github.com/cisco-open/fsoc/output" + "github.com/cisco-open/fsoc/platform/api" +) + +type SolutionDeletionData struct { + DeleteTime string `json:"deleteTime,omitempty"` + DeleteMessage string `json:"deleteMessage,omitempty"` + SolutionName string `json:"solutionName,omitempty"` + Tag string `json:"tag,omitempty"` + Status string `json:"status,omitempty"` +} + +type SolutionDeletionRecord struct { + DeletionData SolutionDeletionData `json:"data,omitempty"` +} + +type SolutionDeletionResponseBlob struct { + Items []SolutionDeletionRecord `json:"items"` +} + +var solutionDeleteCmd = &cobra.Command{ + Use: "delete ", + Args: cobra.MinimumNArgs(1), + Short: "Delete a non-stable tagged version of a solution", + Long: `This command deletes a non-stable tagged version of a solution uploaded by your tenant. + +This is for the purpose of deleting a non-stable tagged solution that you no longer want to use. +This will clean up all of objects/types defined by the solution as well as all of the solution metadata. +Please note you must terminate all active subscriptions to the solution before issuing this command. +Please also note this is an asynchronous operation and thus it may take some time for the status to reflect properly.`, + Example: `fsoc solution delete mysolution`, + Run: deleteSolution, + Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, + TraverseChildren: true, +} + +func getSolutionDeleteCommand() *cobra.Command { + + solutionDeleteCmd.Flags(). + String("tag", "", "Tag associated with the solution to delete (required)") + + _ = solutionDeleteCmd.MarkFlagRequired("tag") + + solutionDeleteCmd.Flags(). + Int("wait", 10, "Wait to terminate the command until the solution the solution deletion process is completed. Default time is 10 seconds.") + + solutionDeleteCmd.Flags(). + Bool("no-wait", false, "Don't wait for solution to be deleted after issuing delete request.") + + solutionDeleteCmd.Flags(). + Bool("yes", false, "Skip warning message and bypass confirmation step") + + solutionDeleteCmd.MarkFlagsMutuallyExclusive("wait", "no-wait") + + return solutionDeleteCmd +} + +func deleteSolution(cmd *cobra.Command, args []string) { + var confirmationAnswer string + var solutionName string + var solutionTag string + + solutionTag, _ = cmd.Flags().GetString("tag") + skipConfirmationMessage, _ := cmd.Flags().GetBool("yes") + waitForDeletionDuration, _ := cmd.Flags().GetInt("wait") + noWait, _ := cmd.Flags().GetBool("no-wait") + + solutionName = getSolutionNameFromArgs(cmd, args, "") + + headers := map[string]string{ + "tag": solutionTag, + } + + if !skipConfirmationMessage { + fmt.Printf("WARNING! This command will remove all of the objects and types that are associated with this solution and will purge all data related to those objects and types. It will also remove all solution metadata (including, but not limited to, subscriptions and other related objects).\nProceed with caution! \nPlease type the name of the solution you want to delete and hit enter confirm that you want to delete the solution with name: %s and tag: %s \n", solutionName, solutionTag) + fmt.Scanln(&confirmationAnswer) + + if confirmationAnswer != solutionName { + log.Fatal("Solution delete not confirmed, exiting command") + } + } + + solutionDeleteUrl := fmt.Sprintf(getSolutionDeleteUrl(), solutionName) + + var res any + err := api.JSONDelete(solutionDeleteUrl, &res, &api.Options{Headers: headers}) + if err != nil { + log.Fatalf("Solution delete command failed: %v", err) + } + + if !noWait { + output.PrintCmdStatus(cmd, fmt.Sprintf("Solution deletion initiated for solution with name: %s and tag: %s\n", solutionName, solutionTag)) + var deletionObjData SolutionDeletionData + waitStartTime := time.Now() + + for deletionObjData.Status == "" || deletionObjData.Status == "inProgress" || deletionObjData.IsEmpty() { + output.PrintCmdStatus(cmd, fmt.Sprintf("Waited %f seconds for solution with name: %s and tag: %s to be marked as deleted\n", time.Since(waitStartTime).Seconds(), solutionName, solutionTag)) + if time.Since(waitStartTime).Seconds() > float64(waitForDeletionDuration) { + log.Fatalf("Failed to validate solution with name %s and tag: %s was deleted: timed out", solutionName, solutionTag) + } + deletionObj := getSolutionDeletionObject(solutionTag, solutionName) + deletionObjData = deletionObj.DeletionData + time.Sleep(3 * time.Second) + } + + if deletionObjData.Status == "successful" { + output.PrintCmdStatus(cmd, fmt.Sprintf("Solution with name: %s and tag: %s deleted successfully", solutionName, solutionTag)) + } else { + output.PrintCmdStatus(cmd, fmt.Sprintf("Issue deleting solution with name: %s and tag %s. Error message: %s", solutionName, solutionTag, deletionObjData.DeleteMessage)) + } + } else { + output.PrintCmdStatus(cmd, "Solution deletion initiated, skip waiting for transaction to complete") + } +} + +func getSolutionDeleteUrl() string { + return "solution-manager/v1/solutions/%s" +} + +func getExtSolutionDeletionUrl() string { + return "knowledge-store/v1/objects/extensibility:solutionDeletion%s" +} + +func (s SolutionDeletionData) IsEmpty() bool { + return reflect.DeepEqual(s, SolutionDeletionData{}) +} + +func getSolutionDeletionObject(solutionTag string, solutionName string) SolutionDeletionRecord { + var res SolutionDeletionResponseBlob + var emptyData SolutionDeletionRecord + + cfg := config.GetCurrentContext() + layerType := "TENANT" + headers := map[string]string{ + "layer-type": layerType, + "layer-id": cfg.Tenant, + } + + filter := fmt.Sprintf(`data.solutionName eq "%s" and data.tag eq "%s"`, solutionName, solutionTag) + query := fmt.Sprintf("?order=%s&filter=%s&max=1", url.QueryEscape("desc"), url.QueryEscape(filter)) + + url := fmt.Sprintf(getExtSolutionDeletionUrl(), query) + + err := api.JSONGet(url, &res, &api.Options{Headers: headers}) + + if err != nil { + log.Fatalf("Error fetching solution deletion object %q: %v", url, err) + } + + if len(res.Items) > 0 { + return res.Items[0] + } else { + return emptyData + } +} diff --git a/cmd/solution/solution.go b/cmd/solution/solution.go index 5b6ee4d6..f85d2a91 100644 --- a/cmd/solution/solution.go +++ b/cmd/solution/solution.go @@ -66,6 +66,7 @@ func NewSubCmd() *cobra.Command { solutionCmd.AddCommand(getSolutionTestStatusCmd()) solutionCmd.AddCommand(getsolutionIsolateCmd()) solutionCmd.AddCommand(getSolutionZapCmd()) + solutionCmd.AddCommand(getSolutionDeleteCommand()) solutionListCmd.Flags().StringP("output", "o", "", "Output format (human*, json, yaml)") return solutionCmd From 8f8d9f90eb1b755f25f4a3333509f44462d6ef29 Mon Sep 17 00:00:00 2001 From: Jack Morgan Date: Tue, 26 Mar 2024 17:35:45 -0700 Subject: [PATCH 2/4] ORION-3127: handle case where solution has already been deleted once and user wants to delete it again and wait for deletion to complete --- cmd/solution/delete.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/solution/delete.go b/cmd/solution/delete.go index 64cb7373..82d8b86e 100644 --- a/cmd/solution/delete.go +++ b/cmd/solution/delete.go @@ -38,6 +38,7 @@ type SolutionDeletionData struct { type SolutionDeletionRecord struct { DeletionData SolutionDeletionData `json:"data,omitempty"` + ID string `json:"id,omitempty"` } type SolutionDeletionResponseBlob struct { @@ -85,6 +86,7 @@ func deleteSolution(cmd *cobra.Command, args []string) { var confirmationAnswer string var solutionName string var solutionTag string + var existingSolutionDeletionObjectId string solutionTag, _ = cmd.Flags().GetString("tag") skipConfirmationMessage, _ := cmd.Flags().GetBool("yes") @@ -106,6 +108,12 @@ func deleteSolution(cmd *cobra.Command, args []string) { } } + existingDeletionObj := getSolutionDeletionObject(solutionTag, solutionName) + + if !existingDeletionObj.IsEmpty() { + existingSolutionDeletionObjectId = existingDeletionObj.ID + } + solutionDeleteUrl := fmt.Sprintf(getSolutionDeleteUrl(), solutionName) var res any @@ -117,15 +125,18 @@ func deleteSolution(cmd *cobra.Command, args []string) { if !noWait { output.PrintCmdStatus(cmd, fmt.Sprintf("Solution deletion initiated for solution with name: %s and tag: %s\n", solutionName, solutionTag)) var deletionObjData SolutionDeletionData + var newDeletionObjectId string waitStartTime := time.Now() - for deletionObjData.Status == "" || deletionObjData.Status == "inProgress" || deletionObjData.IsEmpty() { + for deletionObjData.Status == "" || deletionObjData.Status == "inProgress" || deletionObjData.IsEmpty() || newDeletionObjectId == existingSolutionDeletionObjectId { output.PrintCmdStatus(cmd, fmt.Sprintf("Waited %f seconds for solution with name: %s and tag: %s to be marked as deleted\n", time.Since(waitStartTime).Seconds(), solutionName, solutionTag)) if time.Since(waitStartTime).Seconds() > float64(waitForDeletionDuration) { log.Fatalf("Failed to validate solution with name %s and tag: %s was deleted: timed out", solutionName, solutionTag) } deletionObj := getSolutionDeletionObject(solutionTag, solutionName) deletionObjData = deletionObj.DeletionData + newDeletionObjectId = deletionObj.ID + log.Infof("Got value of new deletion objectID :%s", newDeletionObjectId) time.Sleep(3 * time.Second) } @@ -151,6 +162,10 @@ func (s SolutionDeletionData) IsEmpty() bool { return reflect.DeepEqual(s, SolutionDeletionData{}) } +func (s SolutionDeletionRecord) IsEmpty() bool { + return reflect.DeepEqual(s, SolutionDeletionRecord{}) +} + func getSolutionDeletionObject(solutionTag string, solutionName string) SolutionDeletionRecord { var res SolutionDeletionResponseBlob var emptyData SolutionDeletionRecord From 886187e4cedce609d2f2e7baf74d6590206eccaf Mon Sep 17 00:00:00 2001 From: Jack Morgan Date: Tue, 26 Mar 2024 18:39:10 -0700 Subject: [PATCH 3/4] ORION-3127: address review comments + add functionality for resuming waiting if deletion is already in progress --- cmd/solution/delete.go | 31 ++++++++++++++++++------------- cmd/solution/zap.go | 3 +-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cmd/solution/delete.go b/cmd/solution/delete.go index 82d8b86e..e9250271 100644 --- a/cmd/solution/delete.go +++ b/cmd/solution/delete.go @@ -47,17 +47,17 @@ type SolutionDeletionResponseBlob struct { var solutionDeleteCmd = &cobra.Command{ Use: "delete ", - Args: cobra.MinimumNArgs(1), + Args: cobra.ExactArgs(1), Short: "Delete a non-stable tagged version of a solution", Long: `This command deletes a non-stable tagged version of a solution uploaded by your tenant. This is for the purpose of deleting a non-stable tagged solution that you no longer want to use. This will clean up all of objects/types defined by the solution as well as all of the solution metadata. Please note you must terminate all active subscriptions to the solution before issuing this command. -Please also note this is an asynchronous operation and thus it may take some time for the status to reflect properly.`, - Example: `fsoc solution delete mysolution`, +Please also note this is an asynchronous operation and thus it may take some time for the status to reflect properly. +If you issue this command while an active deletion is in progress, it will simply wait for that deletion to finish.`, + Example: ` fsoc solution delete mysolution --tag custom --wait 45 --yes`, Run: deleteSolution, - Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, TraverseChildren: true, } @@ -69,13 +69,13 @@ func getSolutionDeleteCommand() *cobra.Command { _ = solutionDeleteCmd.MarkFlagRequired("tag") solutionDeleteCmd.Flags(). - Int("wait", 10, "Wait to terminate the command until the solution the solution deletion process is completed. Default time is 10 seconds.") + Int("wait", 60, "Wait to terminate the command until the solution the solution deletion process is completed. Default time is 60 seconds.") solutionDeleteCmd.Flags(). Bool("no-wait", false, "Don't wait for solution to be deleted after issuing delete request.") solutionDeleteCmd.Flags(). - Bool("yes", false, "Skip warning message and bypass confirmation step") + BoolP("yes", "y", false, "Skip warning message and bypass confirmation step") solutionDeleteCmd.MarkFlagsMutuallyExclusive("wait", "no-wait") @@ -87,6 +87,7 @@ func deleteSolution(cmd *cobra.Command, args []string) { var solutionName string var solutionTag string var existingSolutionDeletionObjectId string + var existingSolutionDeletionInProgress bool = false solutionTag, _ = cmd.Flags().GetString("tag") skipConfirmationMessage, _ := cmd.Flags().GetBool("yes") @@ -112,23 +113,28 @@ func deleteSolution(cmd *cobra.Command, args []string) { if !existingDeletionObj.IsEmpty() { existingSolutionDeletionObjectId = existingDeletionObj.ID + if existingDeletionObj.DeletionData.Status == "inProgress" { + existingSolutionDeletionInProgress = true + } } solutionDeleteUrl := fmt.Sprintf(getSolutionDeleteUrl(), solutionName) - var res any - err := api.JSONDelete(solutionDeleteUrl, &res, &api.Options{Headers: headers}) - if err != nil { - log.Fatalf("Solution delete command failed: %v", err) + if !existingSolutionDeletionInProgress { + var res any + err := api.JSONDelete(solutionDeleteUrl, &res, &api.Options{Headers: headers}) + if err != nil { + log.Fatalf("Solution delete command failed: %v", err) + } } - if !noWait { + if !noWait && waitForDeletionDuration > 0 { output.PrintCmdStatus(cmd, fmt.Sprintf("Solution deletion initiated for solution with name: %s and tag: %s\n", solutionName, solutionTag)) var deletionObjData SolutionDeletionData var newDeletionObjectId string waitStartTime := time.Now() - for deletionObjData.Status == "" || deletionObjData.Status == "inProgress" || deletionObjData.IsEmpty() || newDeletionObjectId == existingSolutionDeletionObjectId { + for (newDeletionObjectId == existingSolutionDeletionObjectId && !existingSolutionDeletionInProgress) || deletionObjData.IsEmpty() || deletionObjData.Status == "inProgress" { output.PrintCmdStatus(cmd, fmt.Sprintf("Waited %f seconds for solution with name: %s and tag: %s to be marked as deleted\n", time.Since(waitStartTime).Seconds(), solutionName, solutionTag)) if time.Since(waitStartTime).Seconds() > float64(waitForDeletionDuration) { log.Fatalf("Failed to validate solution with name %s and tag: %s was deleted: timed out", solutionName, solutionTag) @@ -136,7 +142,6 @@ func deleteSolution(cmd *cobra.Command, args []string) { deletionObj := getSolutionDeletionObject(solutionTag, solutionName) deletionObjData = deletionObj.DeletionData newDeletionObjectId = deletionObj.ID - log.Infof("Got value of new deletion objectID :%s", newDeletionObjectId) time.Sleep(3 * time.Second) } diff --git a/cmd/solution/zap.go b/cmd/solution/zap.go index 7f98d9f9..5a2e7590 100644 --- a/cmd/solution/zap.go +++ b/cmd/solution/zap.go @@ -31,7 +31,7 @@ import ( var solutionZapCmd = &cobra.Command{ Use: "zap ", - Args: cobra.MaximumNArgs(1), + Args: cobra.ExactArgs(1), Short: "Upload an empty version of a solution to clean it up", Long: `This command creates an empty version of an existing solution and uploads it. @@ -39,7 +39,6 @@ This is for the purpose of cleaning up a solution by removing the knowledge type associated with it. Use this command with caution.`, Example: ` fsoc solution zap mysolution`, Run: zapSolution, - Annotations: map[string]string{config.AnnotationForConfigBypass: ""}, TraverseChildren: true, } From 8e624fe3d9664db43b6f568f729a4522be77e5a8 Mon Sep 17 00:00:00 2001 From: Jack Morgan Date: Tue, 26 Mar 2024 18:48:23 -0700 Subject: [PATCH 4/4] ORION-3127: address more review comments --- cmd/solution/delete.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/solution/delete.go b/cmd/solution/delete.go index e9250271..c718ca5d 100644 --- a/cmd/solution/delete.go +++ b/cmd/solution/delete.go @@ -48,10 +48,10 @@ type SolutionDeletionResponseBlob struct { var solutionDeleteCmd = &cobra.Command{ Use: "delete ", Args: cobra.ExactArgs(1), - Short: "Delete a non-stable tagged version of a solution", - Long: `This command deletes a non-stable tagged version of a solution uploaded by your tenant. + Short: "Delete a solution. Stable solutions cannot be deleted", + Long: `This command deletes a solution uploaded by your tenant. Stable solutions cannot be deleted. -This is for the purpose of deleting a non-stable tagged solution that you no longer want to use. +This is for the purpose of deleting a solution that you no longer want to use. This will clean up all of objects/types defined by the solution as well as all of the solution metadata. Please note you must terminate all active subscriptions to the solution before issuing this command. Please also note this is an asynchronous operation and thus it may take some time for the status to reflect properly. @@ -101,7 +101,7 @@ func deleteSolution(cmd *cobra.Command, args []string) { } if !skipConfirmationMessage { - fmt.Printf("WARNING! This command will remove all of the objects and types that are associated with this solution and will purge all data related to those objects and types. It will also remove all solution metadata (including, but not limited to, subscriptions and other related objects).\nProceed with caution! \nPlease type the name of the solution you want to delete and hit enter confirm that you want to delete the solution with name: %s and tag: %s \n", solutionName, solutionTag) + fmt.Printf("WARNING! This command will remove all objects and types that are associated with this solution and will purge all data related to those objects and types. It will also remove all solution metadata (including, but not limited to, subscriptions and other related objects).\nProceed with caution! \nPlease type the name of the solution you want to delete and hit enter confirm that you want to delete the solution with name: %s and tag: %s \n", solutionName, solutionTag) fmt.Scanln(&confirmationAnswer) if confirmationAnswer != solutionName { @@ -128,8 +128,9 @@ func deleteSolution(cmd *cobra.Command, args []string) { } } + output.PrintCmdStatus(cmd, fmt.Sprintf("Solution deletion initiated for solution with name: %s and tag: %s\n", solutionName, solutionTag)) + if !noWait && waitForDeletionDuration > 0 { - output.PrintCmdStatus(cmd, fmt.Sprintf("Solution deletion initiated for solution with name: %s and tag: %s\n", solutionName, solutionTag)) var deletionObjData SolutionDeletionData var newDeletionObjectId string waitStartTime := time.Now() @@ -137,7 +138,7 @@ func deleteSolution(cmd *cobra.Command, args []string) { for (newDeletionObjectId == existingSolutionDeletionObjectId && !existingSolutionDeletionInProgress) || deletionObjData.IsEmpty() || deletionObjData.Status == "inProgress" { output.PrintCmdStatus(cmd, fmt.Sprintf("Waited %f seconds for solution with name: %s and tag: %s to be marked as deleted\n", time.Since(waitStartTime).Seconds(), solutionName, solutionTag)) if time.Since(waitStartTime).Seconds() > float64(waitForDeletionDuration) { - log.Fatalf("Failed to validate solution with name %s and tag: %s was deleted: timed out", solutionName, solutionTag) + log.Fatalf("Timed out waiting for solution with name %s and tag: %s to be deleted. Deletion continues, please check status for outcome.", solutionName, solutionTag) } deletionObj := getSolutionDeletionObject(solutionTag, solutionName) deletionObjData = deletionObj.DeletionData @@ -148,10 +149,8 @@ func deleteSolution(cmd *cobra.Command, args []string) { if deletionObjData.Status == "successful" { output.PrintCmdStatus(cmd, fmt.Sprintf("Solution with name: %s and tag: %s deleted successfully", solutionName, solutionTag)) } else { - output.PrintCmdStatus(cmd, fmt.Sprintf("Issue deleting solution with name: %s and tag %s. Error message: %s", solutionName, solutionTag, deletionObjData.DeleteMessage)) + output.PrintCmdStatus(cmd, fmt.Sprintf("Failed to delete solution with name: %s and tag %s. Error message: %s", solutionName, solutionTag, deletionObjData.DeleteMessage)) } - } else { - output.PrintCmdStatus(cmd, "Solution deletion initiated, skip waiting for transaction to complete") } }