Skip to content

Commit

Permalink
cli: add --json option
Browse files Browse the repository at this point in the history
Allow outputing json in conjunction with --auto-approve opion.
Deletion logic has been moved to a specific class "Destroyer"

closes #3

Signed-off-by: Jérôme Jutteau <[email protected]>
  • Loading branch information
jerome-jutteau authored and outscale-mdr committed Mar 18, 2022
1 parent 1bdbd7d commit c8a7d9d
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 100 deletions.
19 changes: 19 additions & 0 deletions cmd/frieza/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"fmt"
"io/ioutil"
"log"

. "github.com/outscale-dev/frieza/internal/common"
"github.com/teris-io/cli"
Expand Down Expand Up @@ -29,6 +31,19 @@ func cliDebug() cli.Option {
return cli.NewOption("debug", "enable verbose output for debuging purpose").WithType(cli.TypeBool)
}

func cliJson() cli.Option {
return cli.NewOption("json", "output in json format (with --plan option only)").WithType(cli.TypeBool)
}

func cliFatalf(json bool, format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
if json {
log.Fatalf("{\"error\": \"%s\"}", msg)
} else {
log.Fatalf(msg)
}
}

func cliRoot() cli.App {
return cli.New(ShortDescription()).
WithCommand(cliProfile()).
Expand All @@ -53,5 +68,9 @@ func ShortDescription() string {
" Frieza can remove all resources from a cloud account or resources which are not part of a \"snapshot\".\n" +
" Snapshots are only a listing of cloud resources.\n" +
" Start by adding a new cloud profile with `profile new` sub-command.\n"
}

func disableLogs() {
log.SetFlags(0)
log.SetOutput(ioutil.Discard)
}
44 changes: 18 additions & 26 deletions cmd/frieza/cli_clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,72 +11,64 @@ import (
func cliClean() cli.Command {
return cli.NewCommand("clean", "delete created resources since a specific snapshot").
WithOption(cli.NewOption("plan", "Only show what resource would be deleted").WithType(cli.TypeBool)).
WithOption(cliJson()).
WithOption(cli.NewOption("auto-approve", "Approve resource deletion without confirmation").WithType(cli.TypeBool)).
WithArg(cli.NewArg("snapshot_name", "snapshot")).
WithOption(cliConfigPath()).
WithOption(cliDebug()).
WithAction(func(args []string, options map[string]string) int {
setupDebug(options)
clean(options["config"], &args[0], options["plan"] == "true", options["auto-approve"] == "true")
clean(options["config"], &args[0], options["plan"] == "true", options["auto-approve"] == "true", options["json"] == "true")
return 0
})
}

func clean(customConfigPath string, snapshotName *string, plan bool, autoApprove bool) {
func clean(customConfigPath string, snapshotName *string, plan bool, autoApprove bool, jsonOutput bool) {
var configPath *string
if jsonOutput && !autoApprove {
cliFatalf(true, "Cannot use --json option without --auto-approve")
}
if len(customConfigPath) > 0 {
configPath = &customConfigPath
}
config, err := ConfigLoadWithDefault(configPath)
if err != nil {
log.Fatalf("Cannot load configuration: %s", err.Error())
cliFatalf(jsonOutput, "Cannot load configuration: %s", err.Error())
}
snapshot, err := SnapshotLoad(*snapshotName, config)
if err != nil {
log.Fatalf("Error load snapshot %s: %s", *snapshotName, err.Error())
cliFatalf(jsonOutput, "Error load snapshot %s: %s", *snapshotName, err.Error())
}

var providers []Provider
var objectsToDelete []Objects
destroyer := NewDestroyer()
objectsCount := 0

for _, data := range snapshot.Data {
profile, err := config.GetProfile(data.Profile)
if err != nil {
log.Fatalf("Error while getting profile %s: %s", data.Profile, err.Error())
cliFatalf(jsonOutput, "Error while getting profile %s: %s", data.Profile, err.Error())
}
provider, err := ProviderNew(*profile)
if err != nil {
log.Fatalf("Error intializing profile %s: %s", data.Profile, err.Error())
cliFatalf(jsonOutput, "Error intializing profile %s: %s", data.Profile, err.Error())
}
objects := ReadObjects(&provider)
diff := NewDiff()
diff.Build(&data.Objects, &objects)
count := ObjectsCount(&diff.Created)
objectsCount += count
if count > 0 {
fmt.Printf("Newly created object to delete in profile %s (%s):\n", profile.Name, provider.Name())
fmt.Printf(ObjectsPrint(&provider, &diff.Created))
} else {
fmt.Printf("No new object to delete in profile %s (%s)\n", profile.Name, provider.Name())
}
providers = append(providers, provider)
objectsToDelete = append(objectsToDelete, *&diff.Created)
objectsCount += ObjectsCount(&diff.Created)
destroyer.add(profile, &provider, &diff.Created)
}

if objectsCount == 0 {
fmt.Println("Nothing to delete, exiting")
destroyer.print(jsonOutput)
if plan || objectsCount == 0 {
return
}

if plan {
return
if jsonOutput {
disableLogs()
}

message := fmt.Sprintf("Do you really want to delete newly created resources?\n" +
" Frieza will delete all resources shown above.")
if !confirmAction(&message, autoApprove) {
log.Fatal("Clean canceled")
}
loopDelete(providers, objectsToDelete)
destroyer.run()
}
92 changes: 18 additions & 74 deletions cmd/frieza/cli_nuke.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package main

import (
"bufio"
"fmt"
"log"
"os"
"strings"

. "github.com/outscale-dev/frieza/internal/common"
"github.com/teris-io/cli"
Expand All @@ -14,25 +11,29 @@ import (
func cliNuke() cli.Command {
return cli.NewCommand("nuke", "delete ALL resources of specified profiles").
WithOption(cli.NewOption("plan", "Only show what resource would be deleted").WithType(cli.TypeBool)).
WithOption(cliJson()).
WithOption(cli.NewOption("auto-approve", "Approve resource deletion without confirmation").WithType(cli.TypeBool)).
WithOption(cliConfigPath()).
WithOption(cliDebug()).
WithArg(cli.NewArg("profile", "one or more profile").AsOptional()).
WithAction(func(args []string, options map[string]string) int {
setupDebug(options)
nuke(options["config"], args, options["plan"] == "true", options["auto-approve"] == "true")
nuke(options["config"], args, options["plan"] == "true", options["auto-approve"] == "true", options["json"] == "true")
return 0
})
}

func nuke(customConfigPath string, profiles []string, plan bool, autoApprove bool) {
func nuke(customConfigPath string, profiles []string, plan bool, autoApprove bool, jsonOutput bool) {
if jsonOutput && !autoApprove {
cliFatalf(true, "Cannot use --json option without --auto-approve")
}
var configPath *string
if len(customConfigPath) > 0 {
configPath = &customConfigPath
}

if len(profiles) == 0 {
log.Fatalln("No profile provided, use --help for more details.")
cliFatalf(jsonOutput, "No profile provided, use --help for more details.")
}

uniqueProfiles := make(map[string]bool)
Expand All @@ -42,91 +43,34 @@ func nuke(customConfigPath string, profiles []string, plan bool, autoApprove boo

config, err := ConfigLoadWithDefault(configPath)
if err != nil {
log.Fatalf("Cannot load configuration: %s", err.Error())
cliFatalf(jsonOutput, "Cannot load configuration: %s", err.Error())
}

var providers []Provider
var objectsToDelete []Objects
objectsCount := 0

destroyer := NewDestroyer()
for profileName := range uniqueProfiles {
profile, err := config.GetProfile(profileName)
if err != nil {
log.Fatalf("Error while getting profile %s: %s", profileName, err.Error())
cliFatalf(jsonOutput, "Error while getting profile %s: %s", profileName, err.Error())
}
provider, err := ProviderNew(*profile)
if err != nil {
log.Fatalf("Error intializing profile %s: %s", profileName, err.Error())
}
toDelete := ReadObjects(&provider)
objectsCount += ObjectsCount(&toDelete)
fmt.Printf("Profile %s (%s):\n", profile.Name, provider.Name())
if objectsCount > 0 {
fmt.Print(ObjectsPrint(&provider, &toDelete))
} else {
fmt.Println("* no object *")
cliFatalf(jsonOutput, "Error intializing profile %s: %s", profileName, err.Error())
}
providers = append(providers, provider)
objectsToDelete = append(objectsToDelete, toDelete)
}

if objectsCount == 0 {
fmt.Println("\nNothing to delete, exiting")
return
objectsToDelete := ReadObjects(&provider)
destroyer.add(profile, &provider, &objectsToDelete)
}

destroyer.print(jsonOutput)
if plan {
return
}

if jsonOutput {
disableLogs()
}
message := fmt.Sprintf("Do you really want to delete ALL resources?\n" +
" Frieza will delete all resources shown above.")
if !confirmAction(&message, autoApprove) {
log.Fatal("Nuke canceled")
}
loopDelete(providers, objectsToDelete)
}

func loopDelete(providers []Provider, objects []Objects) {
for {
var objectsCount []int
var totalObjectCount int
for i := range objects {
count := ObjectsCount(&objects[i])
totalObjectCount += count
objectsCount = append(objectsCount, count)
}
if totalObjectCount == 0 {
return
}
for i, provider := range providers {
if objectsCount[i] == 0 {
continue
}
DeleteObjects(&provider, objects[i])
}
for i, provider := range providers {
diff := NewDiff()
remaining := ReadNonEmptyObjects(&provider, objects[i])
diff.Build(&remaining, &objects[i])
objects[i] = diff.Retained
}
}
}

func confirmAction(message *string, autoApprove bool) bool {
if autoApprove {
return true
}
fmt.Printf("\n%s\n", *message)
fmt.Printf(" There is no undo. Only 'yes' will be accepted to confirm.\n\n")
fmt.Printf(" Enter a value: ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.Replace(response, "\n", "", -1)
response = strings.Replace(response, "\r", "", -1)
if response != "yes" {
return false
}
return true
destroyer.run()
}
Loading

0 comments on commit c8a7d9d

Please sign in to comment.