driftctl/pkg/cmd/scan/output/console.go

209 lines
5.6 KiB
Go

package output
import (
"fmt"
"reflect"
"strings"
"github.com/cloudskiff/driftctl/pkg/analyser"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/fatih/color"
"github.com/nsf/jsondiff"
"github.com/r3labs/diff/v2"
"github.com/aws/aws-sdk-go/aws/awsutil"
)
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 {
fmt.Printf("Found deleted resources:\n")
deletedByType := groupByType(analysis.Deleted())
for ty, resources := range deletedByType {
fmt.Printf(" %s:\n", ty)
for _, res := range resources {
humanString := res.TerraformId()
if stringer, ok := res.(fmt.Stringer); ok {
humanString = stringer.String()
}
fmt.Printf(" - %s\n", humanString)
}
}
}
if analysis.Summary().TotalUnmanaged > 0 {
fmt.Printf("Found unmanaged resources:\n")
unmanagedByType := groupByType(analysis.Unmanaged())
for ty, resource := range unmanagedByType {
fmt.Printf(" %s:\n", ty)
for _, res := range resource {
humanString := res.TerraformId()
if stringer, ok := res.(fmt.Stringer); ok {
humanString = stringer.String()
}
fmt.Printf(" - %s\n", humanString)
}
}
}
if analysis.Summary().TotalDrifted > 0 {
fmt.Printf("Found drifted resources:\n")
for _, difference := range analysis.Differences() {
humanString := difference.Res.TerraformId()
if stringer, ok := difference.Res.(fmt.Stringer); ok {
humanString = stringer.String()
}
fmt.Printf(" - %s (%s):\n", humanString, difference.Res.TerraformType())
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 {
isJsonString := isFieldJsonString(difference.Res, path)
if isJsonString {
prefix := " "
fmt.Printf(" %s\n%s%s\n", pref, prefix, jsonDiff(change.From, change.To, prefix))
continue
}
}
fmt.Printf(" %s %s => %s", pref, prettify(change.From), prettify(change.To))
if change.Computed {
fmt.Printf(" %s", color.YellowString("(computed)"))
}
fmt.Printf("\n")
}
}
}
c.writeSummary(analysis)
for _, alerts := range analysis.Alerts() {
for _, alert := range alerts {
fmt.Printf("%s\n", color.YellowString(alert.Message))
}
}
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)
}
fmt.Printf(" - %s deleted on cloud provider\n", deleted)
drifted := successWriter.Sprintf("0")
if analysis.Summary().TotalDrifted > 0 {
drifted = errorWriter.Sprintf("%d", analysis.Summary().TotalDrifted)
}
fmt.Printf(" - %s drifted from IaC\n", boldWriter.Sprintf("%s/%d", drifted, analysis.Summary().TotalManaged))
}
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)
}
func groupByType(resources []resource.Resource) map[string][]resource.Resource {
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)
}
return result
}
func isFieldJsonString(res resource.Resource, fieldName string) bool {
t := reflect.TypeOf(res)
var field reflect.StructField
var ok bool
if t.Kind() == reflect.Ptr {
field, ok = t.Elem().FieldByName(fieldName)
}
if t.Kind() != reflect.Ptr {
field, ok = t.FieldByName(fieldName)
}
if !ok {
return false
}
return field.Tag.Get("jsonstring") == "true"
}
func jsonDiff(a, b interface{}, prefix string) string {
aStr := fmt.Sprintf("%s", a)
bStr := fmt.Sprintf("%s", b)
opts := jsondiff.DefaultConsoleOptions()
opts.Prefix = prefix
opts.Indent = " "
opts.Added = jsondiff.Tag{
Begin: color.GreenString("+ "),
}
opts.Removed = jsondiff.Tag{
Begin: color.RedString("- "),
}
opts.Changed = jsondiff.Tag{
Begin: color.YellowString("~ "),
}
_, str := jsondiff.Compare([]byte(aStr), []byte(bStr), &opts)
return str
}