2020-12-09 15:31:34 +00:00
package cmd
import (
"fmt"
"os"
"os/signal"
2021-06-04 11:49:31 +00:00
"regexp"
2020-12-09 15:31:34 +00:00
"strings"
"syscall"
2021-05-25 15:34:31 +00:00
"time"
2020-12-09 15:31:34 +00:00
2021-06-11 15:10:06 +00:00
"github.com/cloudskiff/driftctl/pkg/remote/common"
2021-04-27 16:32:03 +00:00
"github.com/cloudskiff/driftctl/pkg/telemetry"
2021-06-17 12:15:45 +00:00
"github.com/fatih/color"
2021-06-13 18:28:30 +00:00
"github.com/mitchellh/go-homedir"
2021-03-03 16:20:25 +00:00
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
2020-12-09 15:31:34 +00:00
"github.com/cloudskiff/driftctl/pkg"
2020-12-16 12:02:02 +00:00
"github.com/cloudskiff/driftctl/pkg/alerter"
2021-01-27 18:12:25 +00:00
cmderrors "github.com/cloudskiff/driftctl/pkg/cmd/errors"
2020-12-09 15:31:34 +00:00
"github.com/cloudskiff/driftctl/pkg/cmd/scan/output"
"github.com/cloudskiff/driftctl/pkg/filter"
"github.com/cloudskiff/driftctl/pkg/iac/config"
"github.com/cloudskiff/driftctl/pkg/iac/supplier"
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend"
2021-03-15 17:30:18 +00:00
globaloutput "github.com/cloudskiff/driftctl/pkg/output"
2020-12-09 15:31:34 +00:00
"github.com/cloudskiff/driftctl/pkg/remote"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform"
)
func NewScanCmd ( ) * cobra . Command {
2021-06-11 15:10:06 +00:00
opts := & pkg . ScanOptions { Deep : true }
2021-03-16 15:21:28 +00:00
opts . BackendOptions = & backend . Options { }
2020-12-09 15:31:34 +00:00
cmd := & cobra . Command {
Use : "scan" ,
Short : "Scan" ,
Long : "Scan" ,
Args : cobra . NoArgs ,
PreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
2021-01-15 11:44:13 +00:00
from , _ := cmd . Flags ( ) . GetStringSlice ( "from" )
2020-12-09 15:31:34 +00:00
iacSource , err := parseFromFlag ( from )
if err != nil {
return err
}
2021-01-15 11:44:13 +00:00
opts . From = iacSource
2020-12-09 15:31:34 +00:00
to , _ := cmd . Flags ( ) . GetString ( "to" )
if ! remote . IsSupported ( to ) {
2021-02-10 13:37:59 +00:00
return errors . Errorf (
"unsupported cloud provider '%s'\nValid values are: %s" ,
to ,
strings . Join ( remote . GetSupportedRemotes ( ) , "," ) ,
2020-12-09 15:31:34 +00:00
)
}
outputFlag , _ := cmd . Flags ( ) . GetString ( "output" )
out , err := parseOutputFlag ( outputFlag )
if err != nil {
return err
}
opts . Output = * out
2021-04-20 11:50:24 +00:00
filterFlag , _ := cmd . Flags ( ) . GetStringArray ( "filter" )
if len ( filterFlag ) > 1 {
return errors . New ( "Filter flag should be specified only once" )
}
if len ( filterFlag ) == 1 && filterFlag [ 0 ] != "" {
expr , err := filter . BuildExpression ( filterFlag [ 0 ] )
2020-12-09 15:31:34 +00:00
if err != nil {
2021-02-09 18:43:39 +00:00
return errors . Wrap ( err , "unable to parse filter expression" )
2020-12-09 15:31:34 +00:00
}
opts . Filter = expr
}
2021-06-04 11:49:31 +00:00
providerVersion , _ := cmd . Flags ( ) . GetString ( "tf-provider-version" )
if err := validateTfProviderVersionString ( providerVersion ) ; err != nil {
return err
}
opts . ProviderVersion = providerVersion
2021-03-15 17:30:18 +00:00
opts . Quiet , _ = cmd . Flags ( ) . GetBool ( "quiet" )
2021-04-27 16:32:03 +00:00
opts . DisableTelemetry , _ = cmd . Flags ( ) . GetBool ( "disable-telemetry" )
2021-03-15 17:30:18 +00:00
2021-06-12 14:17:23 +00:00
opts . ConfigDir , _ = cmd . Flags ( ) . GetString ( "config-dir" )
2020-12-09 15:31:34 +00:00
return nil
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return scanRun ( opts )
} ,
}
fl := cmd . Flags ( )
2021-04-20 11:50:24 +00:00
fl . Bool (
2021-03-15 17:30:18 +00:00
"quiet" ,
false ,
"Do not display anything but scan results" ,
)
2021-04-20 11:50:24 +00:00
fl . StringArray (
2020-12-09 15:31:34 +00:00
"filter" ,
2021-04-20 11:50:24 +00:00
[ ] string { } ,
2020-12-09 15:31:34 +00:00
"JMESPath expression to filter on\n" +
"Examples : \n" +
" - Type == 'aws_s3_bucket' (will filter only s3 buckets)\n" +
" - Type =='aws_s3_bucket && Id != 'my_bucket' (excludes s3 bucket 'my_bucket')\n" +
" - Attr.Tags.Terraform == 'true' (include only resources that have Tag Terraform equal to 'true')\n" ,
)
fl . StringP (
"output" ,
"o" ,
output . Example ( output . ConsoleOutputType ) ,
"Output format, by default it will write to the console\n" +
"Accepted formats are: " + strings . Join ( output . SupportedOutputsExample ( ) , "," ) + "\n" ,
)
2021-01-15 11:44:13 +00:00
fl . StringSliceP (
2020-12-09 15:31:34 +00:00
"from" ,
"f" ,
2021-01-15 11:44:13 +00:00
[ ] string { "tfstate://terraform.tfstate" } ,
"IaC sources, by default try to find local terraform.tfstate file\n" +
2020-12-09 15:31:34 +00:00
"Accepted schemes are: " + strings . Join ( supplier . GetSupportedSchemes ( ) , "," ) + "\n" ,
)
supportedRemotes := remote . GetSupportedRemotes ( )
fl . StringVarP (
& opts . To ,
"to" ,
"t" ,
supportedRemotes [ 0 ] ,
"Cloud provider source\n" +
"Accepted values are: " + strings . Join ( supportedRemotes , "," ) + "\n" ,
)
2021-03-16 15:21:28 +00:00
fl . StringToStringVarP ( & opts . BackendOptions . Headers ,
2021-03-17 12:54:33 +00:00
"headers" ,
2021-03-16 15:21:28 +00:00
"H" ,
map [ string ] string { } ,
"Use those HTTP headers to query the provided URL.\n" +
2021-03-17 17:35:23 +00:00
"Only used with tfstate+http(s) backend for now.\n" ,
2021-03-16 15:21:28 +00:00
)
2021-05-03 10:06:59 +00:00
fl . StringVar ( & opts . BackendOptions . TFCloudToken ,
2021-04-30 14:42:05 +00:00
"tfc-token" ,
2021-04-27 12:03:29 +00:00
"" ,
"Terraform Cloud / Enterprise API token.\n" +
"Only used with tfstate+tfcloud backend.\n" ,
)
2021-06-04 11:49:31 +00:00
fl . String (
2021-06-03 09:43:15 +00:00
"tf-provider-version" ,
"" ,
"Terraform provider version to use.\n" ,
)
2021-04-09 11:15:16 +00:00
fl . BoolVar ( & opts . StrictMode ,
2021-03-29 14:00:15 +00:00
"strict" ,
false ,
2021-04-09 11:07:15 +00:00
"Includes cloud provider service-linked roles (disabled by default)" ,
2021-03-29 14:00:15 +00:00
)
2021-06-17 13:39:31 +00:00
fl . StringVar ( & opts . DriftignorePath ,
"driftignore" ,
".driftignore" ,
"Path to the driftignore file" ,
)
2021-06-13 18:28:30 +00:00
2021-06-16 09:19:14 +00:00
configDir , err := homedir . Dir ( )
if err != nil {
configDir = os . TempDir ( )
}
2021-06-12 14:17:23 +00:00
fl . String (
"config-dir" ,
2021-06-13 18:28:30 +00:00
configDir ,
2021-06-14 16:25:35 +00:00
"Directory path that driftctl uses for configuration.\n" ,
2021-06-12 14:17:23 +00:00
)
2020-12-09 15:31:34 +00:00
return cmd
}
2021-04-09 11:15:16 +00:00
func scanRun ( opts * pkg . ScanOptions ) error {
2021-03-15 17:30:18 +00:00
selectedOutput := output . GetOutput ( opts . Output , opts . Quiet )
2021-03-03 16:20:25 +00:00
2020-12-09 15:31:34 +00:00
c := make ( chan os . Signal )
signal . Notify ( c , os . Interrupt , syscall . SIGTERM )
2020-12-16 12:02:02 +00:00
alerter := alerter . NewAlerter ( )
2021-06-11 15:10:06 +00:00
2021-01-22 17:06:17 +00:00
providerLibrary := terraform . NewProviderLibrary ( )
supplierLibrary := resource . NewSupplierLibrary ( )
2021-06-11 15:10:06 +00:00
remoteLibrary := common . NewRemoteLibrary ( )
2020-12-16 12:02:02 +00:00
2021-05-03 16:41:52 +00:00
iacProgress := globaloutput . NewProgress ( "Scanning states" , "Scanned states" , true )
2021-05-14 10:11:59 +00:00
scanProgress := globaloutput . NewProgress ( "Scanning resources" , "Scanned resources" , false )
2021-03-15 17:30:18 +00:00
2021-03-26 08:44:55 +00:00
resourceSchemaRepository := resource . NewSchemaRepository ( )
2021-05-21 14:09:45 +00:00
resFactory := terraform . NewTerraformResourceFactory ( resourceSchemaRepository )
2021-06-11 15:10:06 +00:00
err := remote . Activate ( opts . To , opts . ProviderVersion , alerter , providerLibrary , supplierLibrary , remoteLibrary , scanProgress , resourceSchemaRepository , resFactory , opts . ConfigDir )
2020-12-09 15:31:34 +00:00
if err != nil {
return err
}
// Teardown
defer func ( ) {
logrus . Trace ( "Exiting scan cmd" )
2021-01-22 17:06:17 +00:00
providerLibrary . Cleanup ( )
2020-12-09 15:31:34 +00:00
logrus . Trace ( "Exited" )
} ( )
2021-06-11 15:10:06 +00:00
scanner := pkg . NewScanner ( supplierLibrary . Suppliers ( ) , remoteLibrary , alerter , pkg . ScannerOptions { Deep : opts . Deep } )
2020-12-09 15:31:34 +00:00
2021-05-21 14:09:45 +00:00
iacSupplier , err := supplier . GetIACSupplier ( opts . From , providerLibrary , opts . BackendOptions , iacProgress , resFactory )
2020-12-09 15:31:34 +00:00
if err != nil {
return err
}
2021-03-29 16:10:50 +00:00
2021-05-03 16:41:52 +00:00
ctl := pkg . NewDriftCTL ( scanner , iacSupplier , alerter , resFactory , opts , scanProgress , iacProgress , resourceSchemaRepository )
2020-12-09 15:31:34 +00:00
go func ( ) {
<- c
logrus . Warn ( "Detected interrupt, cleanup ..." )
ctl . Stop ( )
} ( )
2021-02-09 18:44:27 +00:00
analysis , err := ctl . Run ( )
if err != nil {
return err
2020-12-09 15:31:34 +00:00
}
2021-01-27 18:12:25 +00:00
2021-03-03 16:20:25 +00:00
err = selectedOutput . Write ( analysis )
2021-01-27 18:12:25 +00:00
if err != nil {
return err
}
2021-06-23 08:48:32 +00:00
globaloutput . Printf ( color . WhiteString ( "Scan duration: %s\n" , analysis . Duration . Round ( time . Second ) ) )
2021-05-25 15:34:31 +00:00
2021-04-27 16:32:03 +00:00
if ! opts . DisableTelemetry {
telemetry . SendTelemetry ( analysis )
}
2021-06-17 12:15:45 +00:00
globaloutput . Printf ( color . WhiteString ( "Provider version used to scan: %s. Use --tf-provider-version to use another version.\n" ) , resourceSchemaRepository . ProviderVersion . String ( ) )
2021-01-27 18:12:25 +00:00
if ! analysis . IsSync ( ) {
2021-05-24 12:59:18 +00:00
globaloutput . Printf ( "\nHint: use gen-driftignore command to generate a .driftignore file based on your drifts\n" )
2021-01-27 18:12:25 +00:00
return cmderrors . InfrastructureNotInSync { }
}
return nil
2020-12-09 15:31:34 +00:00
}
2021-01-15 11:44:13 +00:00
func parseFromFlag ( from [ ] string ) ( [ ] config . SupplierConfig , error ) {
2020-12-09 15:31:34 +00:00
2021-01-15 11:44:13 +00:00
configs := make ( [ ] config . SupplierConfig , 0 , len ( from ) )
2020-12-09 15:31:34 +00:00
2021-01-15 11:44:13 +00:00
for _ , flag := range from {
schemePath := strings . Split ( flag , "://" )
if len ( schemePath ) != 2 || schemePath [ 1 ] == "" || schemePath [ 0 ] == "" {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nAccepted schemes are: %s" ,
strings . Join ( supplier . GetSupportedSchemes ( ) , "," ) ,
) ,
) ,
"Unable to parse from flag '%s'" ,
2021-01-15 11:44:13 +00:00
flag ,
2021-02-09 19:03:35 +00:00
)
2021-01-15 11:44:13 +00:00
}
2020-12-09 15:31:34 +00:00
2021-01-15 11:44:13 +00:00
scheme := schemePath [ 0 ]
path := schemePath [ 1 ]
supplierBackend := strings . Split ( scheme , "+" )
if len ( supplierBackend ) > 2 {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError ( fmt . Sprintf (
"\nAccepted schemes are: %s" ,
strings . Join ( supplier . GetSupportedSchemes ( ) , "," ) ,
) ,
) ,
"Unable to parse from scheme '%s'" ,
2021-01-15 11:44:13 +00:00
scheme ,
2021-02-09 19:03:35 +00:00
)
2021-01-15 11:44:13 +00:00
}
2020-12-09 15:31:34 +00:00
2021-01-15 11:44:13 +00:00
supplierKey := supplierBackend [ 0 ]
if ! supplier . IsSupplierSupported ( supplierKey ) {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nAccepted values are: %s" ,
strings . Join ( supplier . GetSupportedSuppliers ( ) , "," ) ,
) ,
) ,
"Unsupported IaC source '%s'" ,
2021-01-15 11:44:13 +00:00
supplierKey ,
2021-02-09 19:03:35 +00:00
)
2020-12-09 15:31:34 +00:00
}
2021-01-15 11:44:13 +00:00
backendString := ""
if len ( supplierBackend ) == 2 {
backendString = supplierBackend [ 1 ]
if ! backend . IsSupported ( backendString ) {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nAccepted values are: %s" ,
strings . Join ( backend . GetSupportedBackends ( ) , "," ) ,
) ,
) ,
"Unsupported IaC backend '%s'" ,
2021-01-15 11:44:13 +00:00
backendString ,
2021-02-09 19:03:35 +00:00
)
2021-01-15 11:44:13 +00:00
}
}
configs = append ( configs , config . SupplierConfig {
Key : supplierKey ,
Backend : backendString ,
Path : path ,
} )
2020-12-09 15:31:34 +00:00
}
2021-01-15 11:44:13 +00:00
return configs , nil
2020-12-09 15:31:34 +00:00
}
func parseOutputFlag ( out string ) ( * output . OutputConfig , error ) {
schemeOpts := strings . Split ( out , "://" )
if len ( schemeOpts ) < 2 || schemeOpts [ 0 ] == "" {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nAccepted formats are: %s" ,
strings . Join ( output . SupportedOutputsExample ( ) , "," ) ,
) ,
) ,
"Unable to parse output flag '%s'" ,
2020-12-09 15:31:34 +00:00
out ,
2021-02-09 19:03:35 +00:00
)
2020-12-09 15:31:34 +00:00
}
o := schemeOpts [ 0 ]
if ! output . IsSupported ( o ) {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nValid formats are: %s" ,
strings . Join ( output . SupportedOutputsExample ( ) , "," ) ,
) ,
) ,
"Unsupported output '%s'" ,
2020-12-09 15:31:34 +00:00
o ,
2021-02-09 19:03:35 +00:00
)
2020-12-09 15:31:34 +00:00
}
opts := schemeOpts [ 1 : ]
options := map [ string ] string { }
switch o {
case output . JSONOutputType :
if len ( opts ) != 1 || opts [ 0 ] == "" {
2021-02-09 19:03:35 +00:00
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nMust be of kind: %s" ,
output . Example ( output . JSONOutputType ) ,
) ,
) ,
"Invalid json output '%s'" ,
2020-12-09 15:31:34 +00:00
out ,
2021-02-09 19:03:35 +00:00
)
2020-12-09 15:31:34 +00:00
}
options [ "path" ] = opts [ 0 ]
2021-04-21 16:51:58 +00:00
case output . HTMLOutputType :
if len ( opts ) != 1 || opts [ 0 ] == "" {
return nil , errors . Wrapf (
cmderrors . NewUsageError (
fmt . Sprintf (
"\nMust be of kind: %s" ,
output . Example ( output . HTMLOutputType ) ,
) ,
) ,
"Invalid html output '%s'" ,
out ,
)
}
options [ "path" ] = opts [ 0 ]
2020-12-09 15:31:34 +00:00
}
return & output . OutputConfig {
Key : o ,
Options : options ,
} , nil
}
2021-06-04 11:49:31 +00:00
func validateTfProviderVersionString ( version string ) error {
if version == "" {
return nil
}
if match , _ := regexp . MatchString ( "^\\d+\\.\\d+\\.\\d+$" , version ) ; ! match {
return errors . Errorf ( "Invalid version argument %s, expected a valid semver string (e.g. 2.13.4)" , version )
}
return nil
}