2020-12-09 15:31:34 +00:00
|
|
|
package output
|
|
|
|
|
|
|
|
import (
|
2021-05-03 15:44:14 +00:00
|
|
|
"encoding/json"
|
2020-12-09 15:31:34 +00:00
|
|
|
"fmt"
|
2021-03-03 16:20:25 +00:00
|
|
|
"os"
|
2020-12-09 15:31:34 +00:00
|
|
|
"reflect"
|
2021-06-02 11:56:18 +00:00
|
|
|
"sort"
|
2020-12-09 15:31:34 +00:00
|
|
|
"strings"
|
|
|
|
|
2021-03-03 16:20:25 +00:00
|
|
|
"github.com/aws/aws-sdk-go/aws/awsutil"
|
2020-12-09 15:31:34 +00:00
|
|
|
"github.com/fatih/color"
|
2021-05-03 15:44:14 +00:00
|
|
|
"github.com/mattn/go-isatty"
|
2020-12-09 15:31:34 +00:00
|
|
|
"github.com/r3labs/diff/v2"
|
2021-05-03 15:44:14 +00:00
|
|
|
"github.com/yudai/gojsondiff"
|
|
|
|
"github.com/yudai/gojsondiff/formatter"
|
2020-12-09 15:31:34 +00:00
|
|
|
|
2021-03-03 16:20:25 +00:00
|
|
|
"github.com/cloudskiff/driftctl/pkg/analyser"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/remote"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/resource"
|
2020-12-09 15:31:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const ConsoleOutputType = "console"
|
|
|
|
const ConsoleOutputExample = "console://"
|
|
|
|
|
|
|
|
type Console struct {
|
|
|
|
summary string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewConsole() *Console {
|
|
|
|
return &Console{
|
|
|
|
`Total coverage is {{ analysis.Coverage }}`,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Console) Write(analysis *analyser.Analysis) error {
|
|
|
|
if analysis.Summary().TotalDeleted > 0 {
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println("Found missing resources:")
|
2021-06-15 14:05:07 +00:00
|
|
|
deletedByType, keys := groupByType(analysis.Deleted())
|
|
|
|
for _, ty := range keys {
|
2020-12-09 15:31:34 +00:00
|
|
|
fmt.Printf(" %s:\n", ty)
|
2021-06-15 14:05:07 +00:00
|
|
|
for _, res := range deletedByType[ty] {
|
2021-05-11 09:56:32 +00:00
|
|
|
humanString := fmt.Sprintf(" - %s", res.TerraformId())
|
2021-06-02 11:56:18 +00:00
|
|
|
if humanAttrs := formatResourceAttributes(res); humanAttrs != "" {
|
|
|
|
humanString += fmt.Sprintf("\n %s", humanAttrs)
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println(humanString)
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if analysis.Summary().TotalUnmanaged > 0 {
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println("Found resources not covered by IaC:")
|
2021-06-15 14:05:07 +00:00
|
|
|
unmanagedByType, keys := groupByType(analysis.Unmanaged())
|
|
|
|
for _, ty := range keys {
|
2020-12-09 15:31:34 +00:00
|
|
|
fmt.Printf(" %s:\n", ty)
|
2021-06-15 14:05:07 +00:00
|
|
|
for _, res := range unmanagedByType[ty] {
|
2021-05-11 09:56:32 +00:00
|
|
|
humanString := fmt.Sprintf(" - %s", res.TerraformId())
|
2021-06-02 11:56:18 +00:00
|
|
|
if humanAttrs := formatResourceAttributes(res); humanAttrs != "" {
|
|
|
|
humanString += fmt.Sprintf("\n %s", humanAttrs)
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println(humanString)
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if analysis.Summary().TotalDrifted > 0 {
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println("Found changed resources:")
|
2020-12-09 15:31:34 +00:00
|
|
|
for _, difference := range analysis.Differences() {
|
2021-05-11 09:56:32 +00:00
|
|
|
humanString := fmt.Sprintf(" - %s (%s):", difference.Res.TerraformId(), difference.Res.TerraformType())
|
|
|
|
whiteSpace := " "
|
2021-06-02 11:56:18 +00:00
|
|
|
if humanAttrs := formatResourceAttributes(difference.Res); humanAttrs != "" {
|
|
|
|
humanString += fmt.Sprintf("\n %s", humanAttrs)
|
|
|
|
whiteSpace = " "
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println(humanString)
|
2020-12-09 15:31:34 +00:00
|
|
|
for _, change := range difference.Changelog {
|
|
|
|
path := strings.Join(change.Path, ".")
|
|
|
|
pref := fmt.Sprintf("%s %s:", color.YellowString("~"), path)
|
|
|
|
if change.Type == diff.CREATE {
|
|
|
|
pref = fmt.Sprintf("%s %s:", color.GreenString("+"), path)
|
|
|
|
} else if change.Type == diff.DELETE {
|
|
|
|
pref = fmt.Sprintf("%s %s:", color.RedString("-"), path)
|
|
|
|
}
|
|
|
|
if change.Type == diff.UPDATE {
|
2021-03-26 08:44:55 +00:00
|
|
|
if change.JsonString {
|
2021-05-11 09:56:32 +00:00
|
|
|
prefix := " "
|
|
|
|
fmt.Printf("%s%s\n%s%s\n", whiteSpace, pref, prefix, jsonDiff(change.From, change.To, prefix))
|
2020-12-09 15:31:34 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Printf("%s%s %s => %s", whiteSpace, pref, prettify(change.From), prettify(change.To))
|
2021-01-08 17:14:26 +00:00
|
|
|
if change.Computed {
|
|
|
|
fmt.Printf(" %s", color.YellowString("(computed)"))
|
2020-12-16 12:02:02 +00:00
|
|
|
}
|
|
|
|
fmt.Printf("\n")
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c.writeSummary(analysis)
|
|
|
|
|
2021-03-02 10:39:14 +00:00
|
|
|
enumerationErrorMessage := ""
|
2021-01-21 17:05:29 +00:00
|
|
|
for _, alerts := range analysis.Alerts() {
|
|
|
|
for _, alert := range alerts {
|
2021-05-11 09:56:32 +00:00
|
|
|
fmt.Println(color.YellowString(alert.Message()))
|
2021-03-02 10:39:14 +00:00
|
|
|
if alert, ok := alert.(*remote.EnumerationAccessDeniedAlert); ok && enumerationErrorMessage == "" {
|
|
|
|
enumerationErrorMessage = alert.GetProviderMessage()
|
2021-02-12 21:33:36 +00:00
|
|
|
}
|
2021-01-21 17:05:29 +00:00
|
|
|
}
|
2020-12-16 12:02:02 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 10:39:14 +00:00
|
|
|
if enumerationErrorMessage != "" {
|
2021-03-03 16:20:25 +00:00
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "\n%s\n", color.YellowString(enumerationErrorMessage))
|
2021-02-12 21:33:36 +00:00
|
|
|
}
|
|
|
|
|
2020-12-09 15:31:34 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Console) writeSummary(analysis *analyser.Analysis) {
|
|
|
|
boldWriter := color.New(color.Bold)
|
|
|
|
successWriter := color.New(color.Bold, color.FgGreen)
|
|
|
|
warningWriter := color.New(color.Bold, color.FgYellow)
|
|
|
|
errorWriter := color.New(color.Bold, color.FgRed)
|
|
|
|
total := boldWriter.Sprintf("%d", analysis.Summary().TotalResources)
|
|
|
|
|
|
|
|
fmt.Printf(
|
|
|
|
"Found %s resource(s)\n",
|
|
|
|
total,
|
|
|
|
)
|
|
|
|
fmt.Printf(
|
|
|
|
" - %s%% coverage\n",
|
|
|
|
boldWriter.Sprintf(
|
|
|
|
"%d",
|
|
|
|
analysis.Coverage(),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
if !analysis.IsSync() {
|
|
|
|
managed := successWriter.Sprintf("0")
|
|
|
|
if analysis.Summary().TotalManaged > 0 {
|
|
|
|
managed = warningWriter.Sprintf("%d", analysis.Summary().TotalManaged)
|
|
|
|
}
|
|
|
|
fmt.Printf(" - %s covered by IaC\n", managed)
|
|
|
|
|
|
|
|
unmanaged := successWriter.Sprintf("0")
|
|
|
|
if analysis.Summary().TotalUnmanaged > 0 {
|
|
|
|
unmanaged = warningWriter.Sprintf("%d", analysis.Summary().TotalUnmanaged)
|
|
|
|
}
|
|
|
|
fmt.Printf(" - %s not covered by IaC\n", unmanaged)
|
|
|
|
|
|
|
|
deleted := successWriter.Sprintf("0")
|
|
|
|
if analysis.Summary().TotalDeleted > 0 {
|
|
|
|
deleted = errorWriter.Sprintf("%d", analysis.Summary().TotalDeleted)
|
|
|
|
}
|
2021-03-26 15:39:34 +00:00
|
|
|
fmt.Printf(" - %s missing on cloud provider\n", deleted)
|
2020-12-09 15:31:34 +00:00
|
|
|
|
|
|
|
drifted := successWriter.Sprintf("0")
|
|
|
|
if analysis.Summary().TotalDrifted > 0 {
|
|
|
|
drifted = errorWriter.Sprintf("%d", analysis.Summary().TotalDrifted)
|
|
|
|
}
|
2021-03-26 15:39:34 +00:00
|
|
|
fmt.Printf(" - %s changed outside of IaC\n", boldWriter.Sprintf("%s/%d", drifted, analysis.Summary().TotalManaged))
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
if analysis.IsSync() {
|
|
|
|
fmt.Println(color.GreenString("Congrats! Your infrastructure is fully in sync."))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func prettify(resource interface{}) string {
|
|
|
|
res := reflect.ValueOf(resource)
|
|
|
|
if resource == nil || res.Kind() == reflect.Ptr && res.IsNil() {
|
|
|
|
return "<nil>"
|
|
|
|
}
|
|
|
|
|
|
|
|
return awsutil.Prettify(resource)
|
|
|
|
}
|
|
|
|
|
2021-06-15 14:05:07 +00:00
|
|
|
func groupByType(resources []resource.Resource) (map[string][]resource.Resource, []string) {
|
2020-12-09 15:31:34 +00:00
|
|
|
result := map[string][]resource.Resource{}
|
|
|
|
for _, res := range resources {
|
|
|
|
if result[res.TerraformType()] == nil {
|
|
|
|
result[res.TerraformType()] = []resource.Resource{res}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
result[res.TerraformType()] = append(result[res.TerraformType()], res)
|
|
|
|
}
|
2021-06-15 14:05:07 +00:00
|
|
|
|
|
|
|
keys := make([]string, 0, len(result))
|
|
|
|
for k := range result {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
return result, keys
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func jsonDiff(a, b interface{}, prefix string) string {
|
|
|
|
aStr := fmt.Sprintf("%s", a)
|
|
|
|
bStr := fmt.Sprintf("%s", b)
|
2021-05-03 15:44:14 +00:00
|
|
|
d := gojsondiff.New()
|
|
|
|
var aJson map[string]interface{}
|
|
|
|
_ = json.Unmarshal([]byte(aStr), &aJson)
|
|
|
|
diff, _ := d.Compare([]byte(aStr), []byte(bStr))
|
|
|
|
f := formatter.NewAsciiFormatter(aJson, formatter.AsciiFormatterConfig{
|
|
|
|
Coloring: isatty.IsTerminal(os.Stdout.Fd()),
|
|
|
|
})
|
|
|
|
// Set foreground green color for added lines and red color for deleted lines
|
|
|
|
formatter.AsciiStyles = map[string]string{
|
|
|
|
"+": "32",
|
|
|
|
"-": "31",
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
2021-05-03 15:44:14 +00:00
|
|
|
diffStr, _ := f.Format(diff)
|
|
|
|
|
|
|
|
return diffStr
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
2021-06-02 11:56:18 +00:00
|
|
|
|
|
|
|
func formatResourceAttributes(res resource.Resource) string {
|
|
|
|
if res.Schema() == nil || res.Schema().HumanReadableAttributesFunc == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
attributes := res.Schema().HumanReadableAttributesFunc(res.(*resource.AbstractResource))
|
|
|
|
if len(attributes) <= 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
// sort attributes
|
|
|
|
keys := make([]string, 0, len(attributes))
|
|
|
|
for k := range attributes {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
// retrieve stringer
|
|
|
|
attrString := ""
|
|
|
|
for _, k := range keys {
|
|
|
|
if attrString != "" {
|
|
|
|
attrString += ", "
|
|
|
|
}
|
|
|
|
attrString += fmt.Sprintf("%s: %s", k, attributes[k])
|
|
|
|
}
|
|
|
|
return attrString
|
|
|
|
}
|