ekristen/aws-nuke

View on GitHub
pkg/commands/nuke/nuke.go

Summary

Maintainability
C
1 day
Test Coverage
package nuke

import (
    "context"
    "fmt"
    "slices"
    "strings"
    "time"

    "github.com/gotidy/ptr"
    "github.com/sirupsen/logrus"
    "github.com/urfave/cli/v2"

    "github.com/aws/aws-sdk-go/aws/endpoints"

    libconfig "github.com/ekristen/libnuke/pkg/config"
    libnuke "github.com/ekristen/libnuke/pkg/nuke"
    "github.com/ekristen/libnuke/pkg/registry"
    "github.com/ekristen/libnuke/pkg/scanner"
    "github.com/ekristen/libnuke/pkg/types"

    "github.com/ekristen/aws-nuke/v3/pkg/awsutil"
    "github.com/ekristen/aws-nuke/v3/pkg/commands/global"
    "github.com/ekristen/aws-nuke/v3/pkg/common"
    "github.com/ekristen/aws-nuke/v3/pkg/config"
    "github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

// ConfigureCreds is a helper function to configure the awsutil.Credentials object from the cli.Context
func ConfigureCreds(c *cli.Context) (creds *awsutil.Credentials) {
    creds = &awsutil.Credentials{}

    creds.Profile = c.String("profile")
    creds.AccessKeyID = c.String("access-key-id")
    creds.SecretAccessKey = c.String("secret-access-key")
    creds.SessionToken = c.String("session-token")
    creds.AssumeRoleArn = c.String("assume-role-arn")
    creds.RoleSessionName = c.String("assume-role-session-name")
    creds.ExternalID = c.String("assume-role-external-id")

    return creds
}

func execute(c *cli.Context) error { //nolint:funlen,gocyclo
    ctx, cancel := context.WithCancel(c.Context)
    defer cancel()

    defaultRegion := c.String("default-region")
    creds := ConfigureCreds(c)

    if err := creds.Validate(); err != nil {
        return err
    }

    // Create the parameters object that will be used to configure the nuke process.
    params := &libnuke.Parameters{
        Force:          c.Bool("force"),
        ForceSleep:     c.Int("force-sleep"),
        Quiet:          c.Bool("quiet"),
        NoDryRun:       c.Bool("no-dry-run"),
        Includes:       c.StringSlice("include"),
        Excludes:       c.StringSlice("exclude"),
        Alternatives:   c.StringSlice("cloud-control"),
        MaxWaitRetries: c.Int("max-wait-retries"),
    }

    if len(c.StringSlice("feature-flag")) > 0 {
        if slices.Contains(c.StringSlice("feature-flag"), "wait-on-dependencies") {
            params.WaitOnDependencies = true
        }

        if slices.Contains(c.StringSlice("feature-flag"), "filter-groups") {
            params.UseFilterGroups = true
        }
    }

    // Parse the user supplied configuration file to pass in part to configure the nuke process.
    parsedConfig, err := config.New(libconfig.Options{
        Path:         c.Path("config"),
        Deprecations: registry.GetDeprecatedResourceTypeMapping(),
        Log:          logrus.WithField("component", "config"),
    })
    if err != nil {
        logrus.Errorf("Failed to parse config file %s", c.Path("config"))
        return err
    }

    // Set the default region for the AWS SDK to use.
    if defaultRegion != "" {
        awsutil.DefaultRegionID = defaultRegion

        partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), defaultRegion)
        if !ok {
            if parsedConfig.CustomEndpoints.GetRegion(defaultRegion) == nil {
                err = fmt.Errorf(
                    "the custom region '%s' must be specified in the configuration 'endpoints'"+
                        " to determine its partition", defaultRegion)
                logrus.WithError(err).Errorf("unable to resolve partition for region: %s", defaultRegion)
                return err
            }
        }

        awsutil.DefaultAWSPartitionID = partition.ID()
    }

    // Create the AWS Account object. This will be used to get the account ID and aliases for the account.
    account, err := awsutil.NewAccount(creds, parsedConfig.CustomEndpoints)
    if err != nil {
        return err
    }

    // Get the filters for the account that is being connected to via the AWS SDK.
    filters, err := parsedConfig.Filters(account.ID())
    if err != nil {
        return err
    }

    // Instantiate libnuke
    n := libnuke.New(params, filters, parsedConfig.Settings)

    n.SetRunSleep(c.Duration("run-sleep-delay"))
    n.SetLogger(logrus.WithField("component", "libnuke"))
    n.RegisterVersion(fmt.Sprintf("> %s", common.AppVersion.String()))

    // Register our custom validate handler that validates the account and AWS nuke unique alias checks
    n.RegisterValidateHandler(func() error {
        return parsedConfig.ValidateAccount(account.ID(), account.Aliases(), c.Bool("no-alias-check"))
    })

    // Register our custom prompt handler that shows the account information
    p := &nuke.Prompt{Parameters: params, Account: account}
    n.RegisterPrompt(p.Prompt)

    // Get any specific account level configuration
    accountConfig := parsedConfig.Accounts[account.ID()]

    // Resolve the resource types to be used for the nuke process based on the parameters, global configuration, and
    // account level configuration.
    resourceTypes := types.ResolveResourceTypes(
        registry.GetNames(),
        []types.Collection{
            n.Parameters.Includes,
            parsedConfig.ResourceTypes.GetIncludes(),
            accountConfig.ResourceTypes.GetIncludes(),
        },
        []types.Collection{
            n.Parameters.Excludes,
            parsedConfig.ResourceTypes.Excludes,
            accountConfig.ResourceTypes.Excludes,
        },
        []types.Collection{
            n.Parameters.Alternatives,
            parsedConfig.ResourceTypes.GetAlternatives(),
            accountConfig.ResourceTypes.GetAlternatives(),
        },
        registry.GetAlternativeResourceTypeMapping(),
    )

    // If the user has specified the "all" region, then we need to get the enabled regions for the account
    // and use those. Otherwise, we will use the regions that are specified in the configuration.
    if slices.Contains(parsedConfig.Regions, "all") {
        parsedConfig.Regions = account.Regions()

        logrus.Info(
            `"all" detected in region list, only enabled regions and "global" will be used, all others ignored`)

        if len(parsedConfig.Regions) > 1 {
            logrus.Warnf(`additional regions defined along with "all", these will be ignored!`)
        }

        logrus.Infof("The following regions are enabled for the account (%d total):", len(parsedConfig.Regions))

        printableRegions := make([]string, 0)
        for i, region := range parsedConfig.Regions {
            printableRegions = append(printableRegions, region)
            if i%6 == 0 { // print 5 regions per line
                logrus.Infof("> %s", strings.Join(printableRegions, ", "))
                printableRegions = make([]string, 0)
            } else if i == len(parsedConfig.Regions)-1 {
                logrus.Infof("> %s", strings.Join(printableRegions, ", "))
            }
        }
    }

    // Register the scanners for each region that is defined in the configuration.
    for _, regionName := range parsedConfig.Regions {
        // Step 1 - Create the region object
        region := nuke.NewRegion(regionName, account.ResourceTypeToServiceType, account.NewSession, account.NewConfig)

        // Step 2 - Create the scannerActual object
        scannerActual := scanner.New(regionName, resourceTypes, &nuke.ListerOpts{
            Region:    region,
            AccountID: ptr.String(account.ID()),
            Logger: logrus.WithFields(logrus.Fields{
                "component": "scanner",
                "region":    regionName,
            }),
        })

        // Step 3 - Register a mutate function that will be called to modify the lister options for each resource type
        // see pkg/nuke/resource.go for the MutateOpts function. Its purpose is to create the proper session for the
        // proper region.
        regMutateErr := scannerActual.RegisterMutateOptsFunc(nuke.MutateOpts)
        if regMutateErr != nil {
            return regMutateErr
        }

        // Step 4 - Register the scannerActual with the nuke object
        regScanErr := n.RegisterScanner(nuke.Account, scannerActual)
        if regScanErr != nil {
            return regScanErr
        }
    }

    return n.Run(ctx)
}

func init() { //nolint:funlen
    flags := []cli.Flag{
        &cli.PathFlag{
            Name:    "config",
            Aliases: []string{"c"},
            Usage:   "path to config file",
            Value:   "config.yaml",
        },
        &cli.StringSliceFlag{
            Name:    "include",
            Usage:   "only run against these resource types",
            Aliases: []string{"target"},
        },
        &cli.StringSliceFlag{
            Name:    "exclude",
            Aliases: []string{"exclude-resource"},
            Usage:   "exclude these resource types",
        },
        &cli.StringSliceFlag{
            Name:  "cloud-control",
            Usage: "use these resource types with the Cloud Control API instead of the default",
        },
        &cli.BoolFlag{
            Name:    "quiet",
            Aliases: []string{"q"},
            Usage:   "hide filtered messages",
        },
        &cli.BoolFlag{
            Name:  "no-dry-run",
            Usage: "actually run the removal of the resources after discovery",
        },
        &cli.BoolFlag{
            Name:    "no-prompt",
            Usage:   "disable prompting for verification to run",
            Aliases: []string{"force"},
        },
        &cli.IntFlag{
            Name:    "prompt-delay",
            Usage:   "seconds to delay after prompt before running (minimum: 3 seconds)",
            Value:   10,
            Aliases: []string{"force-sleep"},
        },
        &cli.IntFlag{
            Name:  "max-wait-retries",
            Usage: "maximum number of retries to wait for dependencies to be removed",
        },
        &cli.DurationFlag{
            Name:    "run-sleep-delay",
            EnvVars: []string{"AWS_NUKE_RUN_SLEEP_DELAY"},
            Usage:   "time to sleep between run/loops of resource deletions, default is 5 seconds",
            Value:   5 * time.Second,
        },
        &cli.BoolFlag{
            Name:  "no-alias-check",
            Usage: "disable aws account alias check - requires entry in config as well",
        },
        &cli.StringSliceFlag{
            Name:  "feature-flag",
            Usage: "enable experimental behaviors that may not be fully tested or supported",
        },
        &cli.StringFlag{
            Name:    "default-region",
            EnvVars: []string{"AWS_DEFAULT_REGION"},
            Usage:   "the default aws region to use when setting up the aws auth session",
        },
        &cli.StringFlag{
            Name:    "access-key-id",
            EnvVars: []string{"AWS_ACCESS_KEY_ID"},
            Usage:   "the aws access key id to use when setting up the aws auth session",
        },
        &cli.StringFlag{
            Name:    "secret-access-key",
            EnvVars: []string{"AWS_SECRET_ACCESS_KEY"},
            Usage:   "the aws secret access key to use when setting up the aws auth session",
        },
        &cli.StringFlag{
            Name:    "session-token",
            EnvVars: []string{"AWS_SESSION_TOKEN"},
            Usage:   "the aws session token to use when setting up the aws auth session, typically used for temporary credentials",
        },
        &cli.StringFlag{
            Name:    "profile",
            EnvVars: []string{"AWS_PROFILE"},
            Usage:   "the aws profile to use when setting up the aws auth session, typically used for shared credentials files",
        },
        &cli.StringFlag{
            Name:    "assume-role-arn",
            EnvVars: []string{"AWS_ASSUME_ROLE_ARN"},
            Usage:   "the role arn to assume using the credentials provided in the profile or statically set",
        },
        &cli.StringFlag{
            Name:    "assume-role-session-name",
            EnvVars: []string{"AWS_ASSUME_ROLE_SESSION_NAME"},
            Usage:   "the session name to provide for the assumed role",
        },
        &cli.StringFlag{
            Name:    "assume-role-external-id",
            EnvVars: []string{"AWS_ASSUME_ROLE_EXTERNAL_ID"},
            Usage:   "the external id to provide for the assumed role",
        },
    }

    cmd := &cli.Command{
        Name:  "run",
        Usage: "run nuke against an aws account and remove everything from it",
        Aliases: []string{
            "nuke",
        },
        Flags:  append(flags, global.Flags()...),
        Before: global.Before,
        Action: execute,
    }

    common.RegisterCommand(cmd)
}