Skip to content

Commit

Permalink
Merge pull request #158 from kaytu-io/feat-adds-terraform-automation
Browse files Browse the repository at this point in the history
feat: Adds terraform automation
  • Loading branch information
salehkhazaei authored May 28, 2024
2 parents 4f205b7 + 2cafcb1 commit 6f70789
Show file tree
Hide file tree
Showing 9 changed files with 678 additions and 120 deletions.
17 changes: 17 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,27 @@ func init() {
rootCmd.AddCommand(predef.LoginCmd)
rootCmd.AddCommand(predef.LogoutCmd)
rootCmd.AddCommand(optimizeCmd)
rootCmd.AddCommand(terraformCmd)

optimizeCmd.PersistentFlags().String("preferences", "", "Path to preferences file (yaml)")
optimizeCmd.PersistentFlags().String("output", "interactive", "Show optimization results in selected output (possible values: interactive, table, csv, json. default value: interactive)")
optimizeCmd.PersistentFlags().Bool("plugin-debug-mode", false, "Enable plugin debug mode (manager wont start plugin)")

terraformCmd.Flags().String("preferences", "", "Path to preferences file (yaml)")
terraformCmd.Flags().String("github-owner", "", "Github owner")
terraformCmd.Flags().String("github-repo", "", "Github repo")
terraformCmd.Flags().String("github-username", "", "Github username")
terraformCmd.Flags().String("github-token", "", "Github token")
terraformCmd.Flags().String("github-base-branch", "", "Github base branch")
terraformCmd.Flags().String("terraform-file-path", "", "Terraform file path (relative to your git repository)")
terraformCmd.Flags().Int64("ignore-younger-than", 1, "Ignoring resources which are younger than X hours")
terraformCmd.MarkFlagRequired("github-owner")
terraformCmd.MarkFlagRequired("github-repo")
terraformCmd.MarkFlagRequired("github-username")
terraformCmd.MarkFlagRequired("github-token")
terraformCmd.MarkFlagRequired("github-base-branch")
terraformCmd.MarkFlagRequired("terraform-file-path")

}

func Execute() {
Expand Down
239 changes: 239 additions & 0 deletions cmd/terraform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package cmd

import (
"encoding/json"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/kaytu-io/kaytu/pkg/github"
plugin2 "github.com/kaytu-io/kaytu/pkg/plugin"
"github.com/kaytu-io/kaytu/pkg/plugin/proto/src/golang"
"github.com/kaytu-io/kaytu/pkg/server"
"github.com/kaytu-io/kaytu/pkg/utils"
"github.com/kaytu-io/kaytu/preferences"
"github.com/spf13/cobra"
"github.com/zclconf/go-cty/cty"
"regexp"
"strconv"
"strings"
"time"
)

var terraformCmd = &cobra.Command{
Use: "terraform",
Short: "Create pull request for right sizing opportunities on your terraform git",
Long: "Create pull request for right sizing opportunities on your terraform git",
RunE: func(cmd *cobra.Command, args []string) error {
ignoreYoungerThan := utils.ReadIntFlag(cmd, "ignore-younger-than")
contentBytes, err := github.GetFile(
utils.ReadStringFlag(cmd, "github-owner"),
utils.ReadStringFlag(cmd, "github-repo"),
utils.ReadStringFlag(cmd, "terraform-file-path"),
utils.ReadStringFlag(cmd, "github-username"),
utils.ReadStringFlag(cmd, "github-token"),
)
if err != nil {
return err
}

manager := plugin2.New()
manager.SetNonInteractiveView()
err = manager.StartServer()
if err != nil {
return err
}
err = manager.StartPlugin("rds-instance")
if err != nil {
return err
}
for i := 0; i < 100; i++ {
runningPlg := manager.GetPlugin("kaytu-io/plugin-aws")
if runningPlg != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
runningPlg := manager.GetPlugin("kaytu-io/plugin-aws")
if runningPlg == nil {
return fmt.Errorf("running plugin not found")
}
cfg, err := server.GetConfig()
if err != nil {
return err
}

for _, rcmd := range runningPlg.Plugin.Config.Commands {
if rcmd.Name == "rds-instance" {
preferences.Update(rcmd.DefaultPreferences)

if rcmd.LoginRequired && cfg.AccessToken == "" {
// login
return fmt.Errorf("please login")
}
break
}
}
err = runningPlg.Stream.Send(&golang.ServerMessage{
ServerMessage: &golang.ServerMessage_Start{
Start: &golang.StartProcess{
Command: "rds-instance",
Flags: nil,
KaytuAccessToken: cfg.AccessToken,
},
},
})
if err != nil {
return err
}
jsonOutput, err := manager.NonInteractiveView.WaitAndReturnResults("json")
if err != nil {
return err
}

var jsonObj struct {
Items []*golang.OptimizationItem
}
err = json.Unmarshal([]byte(jsonOutput), &jsonObj)
if err != nil {
return err
}

recommendation := map[string]string{}
rightSizingDescription := map[string]string{}
for _, item := range jsonObj.Items {
var recommendedInstanceSize string
maxRuntimeHours := int64(1) // since default for ignoreYoungerThan is 1
for _, device := range item.Devices {
for _, property := range device.Properties {
if property.Key == "RuntimeHours" {
i, _ := strconv.ParseInt(property.Current, 10, 64)
maxRuntimeHours = max(maxRuntimeHours, i)
}
if property.Key == "Instance Size" && property.Current != property.Recommended {
recommendedInstanceSize = property.Recommended
}
}
}

if maxRuntimeHours < ignoreYoungerThan {
continue
}
if recommendedInstanceSize == "" {
continue
}
recommendation[item.Id] = recommendedInstanceSize
rightSizingDescription[item.Id] = item.Description
}

file, diags := hclwrite.ParseConfig(contentBytes, "filename.tf", hcl.InitialPos)
if diags.HasErrors() {
return fmt.Errorf("%s", diags.Error())
}

body := file.Body()
localVars := map[string]string{}
countRightSized := 0
var rightSizedIds []string
for _, block := range body.Blocks() {
if block.Type() == "locals" {
for k, v := range block.Body().Attributes() {
value := strings.TrimSpace(string(v.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))

localVars[k] = value
}
}
if block.Type() == "module" {
identifier := block.Body().GetAttribute("identifier")
if identifier == nil {
continue
}

value := strings.TrimSpace(string(identifier.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))
value = resolveValue(localVars, value)

var instanceUseIdentifierPrefixBool bool
instanceUseIdentifierPrefix := block.Body().GetAttribute("instance_use_identifier_prefix")
if instanceUseIdentifierPrefix != nil {
boolValue := strings.TrimSpace(string(instanceUseIdentifierPrefix.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))
instanceUseIdentifierPrefixBool = boolValue == "true"
}

if instanceUseIdentifierPrefixBool {
for k, v := range recommendation {
if strings.HasPrefix(k, value) {
dbNameAttr := block.Body().GetAttribute("db_name")
if dbNameAttr != nil {
block.Body().SetAttributeValue("instance_class", cty.StringVal(v))
countRightSized++
rightSizedIds = append(rightSizedIds, k)
}
}
}
} else {
if _, ok := recommendation[value]; ok {
dbNameAttr := block.Body().GetAttribute("db_name")
if dbNameAttr != nil {
block.Body().SetAttributeValue("instance_class", cty.StringVal(recommendation[value]))
countRightSized++
rightSizedIds = append(rightSizedIds, value)
}
}
}
}
}

description := ""
for _, id := range rightSizedIds {
description += fmt.Sprintf("Changing instance class of %s to %s\n", id, recommendation[id])
description += rightSizingDescription[id] + "\n\n"
}

if countRightSized == 0 {
return nil
}
return github.ApplyChanges(
utils.ReadStringFlag(cmd, "github-owner"),
utils.ReadStringFlag(cmd, "github-repo"),
utils.ReadStringFlag(cmd, "github-username"),
utils.ReadStringFlag(cmd, "github-token"),
utils.ReadStringFlag(cmd, "github-base-branch"),
fmt.Sprintf("SRE Bot right sizing %d resources", countRightSized),
utils.ReadStringFlag(cmd, "terraform-file-path"),
string(file.Bytes()),
fmt.Sprintf("SRE Bot right sizing %d resources", countRightSized),
description,
)
},
}

func resolveValue(vars map[string]string, value string) string {
varRegEx, err := regexp.Compile("local\\.(\\w+)")
if err != nil {
panic(err)
}

if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = strings.TrimPrefix(value, "\"")
value = strings.TrimSuffix(value, "\"")

exprRegEx, err := regexp.Compile("\\$\\{([\\w.]+)}")
if err != nil {
panic(err)
}

items := exprRegEx.FindAllString(value, 100)
for _, item := range items {
resolvedItem := resolveValue(vars, item)
value = strings.ReplaceAll(value, item, resolvedItem)
}
return value
} else {
if varRegEx.MatchString(value) {
subMatch := varRegEx.FindStringSubmatch(value)
value = vars[subMatch[1]]
return resolveValue(vars, value)
} else {
return value
}
}
}
20 changes: 20 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,68 @@ require (
github.com/fatih/color v1.16.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/protobuf v1.5.4
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-github/v62 v62.0.0
github.com/hashicorp/hcl/v2 v2.20.1
github.com/jedib0t/go-pretty/v6 v6.5.9
github.com/muesli/reflow v0.3.0
github.com/rogpeppe/go-internal v1.11.0
github.com/schollz/progressbar/v3 v3.14.2
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/zclconf/go-cty v1.13.0
golang.org/x/oauth2 v0.17.0
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.34.0
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/src-d/gcfg v1.4.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
Loading

0 comments on commit 6f70789

Please sign in to comment.