Skip to content

Commit 6f70789

Browse files
authored
Merge pull request #158 from kaytu-io/feat-adds-terraform-automation
feat: Adds terraform automation
2 parents 4f205b7 + 2cafcb1 commit 6f70789

File tree

9 files changed

+678
-120
lines changed

9 files changed

+678
-120
lines changed

Diff for: cmd/root.go

+17
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,27 @@ func init() {
4343
rootCmd.AddCommand(predef.LoginCmd)
4444
rootCmd.AddCommand(predef.LogoutCmd)
4545
rootCmd.AddCommand(optimizeCmd)
46+
rootCmd.AddCommand(terraformCmd)
4647

4748
optimizeCmd.PersistentFlags().String("preferences", "", "Path to preferences file (yaml)")
4849
optimizeCmd.PersistentFlags().String("output", "interactive", "Show optimization results in selected output (possible values: interactive, table, csv, json. default value: interactive)")
4950
optimizeCmd.PersistentFlags().Bool("plugin-debug-mode", false, "Enable plugin debug mode (manager wont start plugin)")
51+
52+
terraformCmd.Flags().String("preferences", "", "Path to preferences file (yaml)")
53+
terraformCmd.Flags().String("github-owner", "", "Github owner")
54+
terraformCmd.Flags().String("github-repo", "", "Github repo")
55+
terraformCmd.Flags().String("github-username", "", "Github username")
56+
terraformCmd.Flags().String("github-token", "", "Github token")
57+
terraformCmd.Flags().String("github-base-branch", "", "Github base branch")
58+
terraformCmd.Flags().String("terraform-file-path", "", "Terraform file path (relative to your git repository)")
59+
terraformCmd.Flags().Int64("ignore-younger-than", 1, "Ignoring resources which are younger than X hours")
60+
terraformCmd.MarkFlagRequired("github-owner")
61+
terraformCmd.MarkFlagRequired("github-repo")
62+
terraformCmd.MarkFlagRequired("github-username")
63+
terraformCmd.MarkFlagRequired("github-token")
64+
terraformCmd.MarkFlagRequired("github-base-branch")
65+
terraformCmd.MarkFlagRequired("terraform-file-path")
66+
5067
}
5168

5269
func Execute() {

Diff for: cmd/terraform.go

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/hashicorp/hcl/v2/hclwrite"
8+
"github.com/kaytu-io/kaytu/pkg/github"
9+
plugin2 "github.com/kaytu-io/kaytu/pkg/plugin"
10+
"github.com/kaytu-io/kaytu/pkg/plugin/proto/src/golang"
11+
"github.com/kaytu-io/kaytu/pkg/server"
12+
"github.com/kaytu-io/kaytu/pkg/utils"
13+
"github.com/kaytu-io/kaytu/preferences"
14+
"github.com/spf13/cobra"
15+
"github.com/zclconf/go-cty/cty"
16+
"regexp"
17+
"strconv"
18+
"strings"
19+
"time"
20+
)
21+
22+
var terraformCmd = &cobra.Command{
23+
Use: "terraform",
24+
Short: "Create pull request for right sizing opportunities on your terraform git",
25+
Long: "Create pull request for right sizing opportunities on your terraform git",
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
ignoreYoungerThan := utils.ReadIntFlag(cmd, "ignore-younger-than")
28+
contentBytes, err := github.GetFile(
29+
utils.ReadStringFlag(cmd, "github-owner"),
30+
utils.ReadStringFlag(cmd, "github-repo"),
31+
utils.ReadStringFlag(cmd, "terraform-file-path"),
32+
utils.ReadStringFlag(cmd, "github-username"),
33+
utils.ReadStringFlag(cmd, "github-token"),
34+
)
35+
if err != nil {
36+
return err
37+
}
38+
39+
manager := plugin2.New()
40+
manager.SetNonInteractiveView()
41+
err = manager.StartServer()
42+
if err != nil {
43+
return err
44+
}
45+
err = manager.StartPlugin("rds-instance")
46+
if err != nil {
47+
return err
48+
}
49+
for i := 0; i < 100; i++ {
50+
runningPlg := manager.GetPlugin("kaytu-io/plugin-aws")
51+
if runningPlg != nil {
52+
break
53+
}
54+
time.Sleep(100 * time.Millisecond)
55+
}
56+
runningPlg := manager.GetPlugin("kaytu-io/plugin-aws")
57+
if runningPlg == nil {
58+
return fmt.Errorf("running plugin not found")
59+
}
60+
cfg, err := server.GetConfig()
61+
if err != nil {
62+
return err
63+
}
64+
65+
for _, rcmd := range runningPlg.Plugin.Config.Commands {
66+
if rcmd.Name == "rds-instance" {
67+
preferences.Update(rcmd.DefaultPreferences)
68+
69+
if rcmd.LoginRequired && cfg.AccessToken == "" {
70+
// login
71+
return fmt.Errorf("please login")
72+
}
73+
break
74+
}
75+
}
76+
err = runningPlg.Stream.Send(&golang.ServerMessage{
77+
ServerMessage: &golang.ServerMessage_Start{
78+
Start: &golang.StartProcess{
79+
Command: "rds-instance",
80+
Flags: nil,
81+
KaytuAccessToken: cfg.AccessToken,
82+
},
83+
},
84+
})
85+
if err != nil {
86+
return err
87+
}
88+
jsonOutput, err := manager.NonInteractiveView.WaitAndReturnResults("json")
89+
if err != nil {
90+
return err
91+
}
92+
93+
var jsonObj struct {
94+
Items []*golang.OptimizationItem
95+
}
96+
err = json.Unmarshal([]byte(jsonOutput), &jsonObj)
97+
if err != nil {
98+
return err
99+
}
100+
101+
recommendation := map[string]string{}
102+
rightSizingDescription := map[string]string{}
103+
for _, item := range jsonObj.Items {
104+
var recommendedInstanceSize string
105+
maxRuntimeHours := int64(1) // since default for ignoreYoungerThan is 1
106+
for _, device := range item.Devices {
107+
for _, property := range device.Properties {
108+
if property.Key == "RuntimeHours" {
109+
i, _ := strconv.ParseInt(property.Current, 10, 64)
110+
maxRuntimeHours = max(maxRuntimeHours, i)
111+
}
112+
if property.Key == "Instance Size" && property.Current != property.Recommended {
113+
recommendedInstanceSize = property.Recommended
114+
}
115+
}
116+
}
117+
118+
if maxRuntimeHours < ignoreYoungerThan {
119+
continue
120+
}
121+
if recommendedInstanceSize == "" {
122+
continue
123+
}
124+
recommendation[item.Id] = recommendedInstanceSize
125+
rightSizingDescription[item.Id] = item.Description
126+
}
127+
128+
file, diags := hclwrite.ParseConfig(contentBytes, "filename.tf", hcl.InitialPos)
129+
if diags.HasErrors() {
130+
return fmt.Errorf("%s", diags.Error())
131+
}
132+
133+
body := file.Body()
134+
localVars := map[string]string{}
135+
countRightSized := 0
136+
var rightSizedIds []string
137+
for _, block := range body.Blocks() {
138+
if block.Type() == "locals" {
139+
for k, v := range block.Body().Attributes() {
140+
value := strings.TrimSpace(string(v.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))
141+
142+
localVars[k] = value
143+
}
144+
}
145+
if block.Type() == "module" {
146+
identifier := block.Body().GetAttribute("identifier")
147+
if identifier == nil {
148+
continue
149+
}
150+
151+
value := strings.TrimSpace(string(identifier.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))
152+
value = resolveValue(localVars, value)
153+
154+
var instanceUseIdentifierPrefixBool bool
155+
instanceUseIdentifierPrefix := block.Body().GetAttribute("instance_use_identifier_prefix")
156+
if instanceUseIdentifierPrefix != nil {
157+
boolValue := strings.TrimSpace(string(instanceUseIdentifierPrefix.Expr().BuildTokens(hclwrite.Tokens{}).Bytes()))
158+
instanceUseIdentifierPrefixBool = boolValue == "true"
159+
}
160+
161+
if instanceUseIdentifierPrefixBool {
162+
for k, v := range recommendation {
163+
if strings.HasPrefix(k, value) {
164+
dbNameAttr := block.Body().GetAttribute("db_name")
165+
if dbNameAttr != nil {
166+
block.Body().SetAttributeValue("instance_class", cty.StringVal(v))
167+
countRightSized++
168+
rightSizedIds = append(rightSizedIds, k)
169+
}
170+
}
171+
}
172+
} else {
173+
if _, ok := recommendation[value]; ok {
174+
dbNameAttr := block.Body().GetAttribute("db_name")
175+
if dbNameAttr != nil {
176+
block.Body().SetAttributeValue("instance_class", cty.StringVal(recommendation[value]))
177+
countRightSized++
178+
rightSizedIds = append(rightSizedIds, value)
179+
}
180+
}
181+
}
182+
}
183+
}
184+
185+
description := ""
186+
for _, id := range rightSizedIds {
187+
description += fmt.Sprintf("Changing instance class of %s to %s\n", id, recommendation[id])
188+
description += rightSizingDescription[id] + "\n\n"
189+
}
190+
191+
if countRightSized == 0 {
192+
return nil
193+
}
194+
return github.ApplyChanges(
195+
utils.ReadStringFlag(cmd, "github-owner"),
196+
utils.ReadStringFlag(cmd, "github-repo"),
197+
utils.ReadStringFlag(cmd, "github-username"),
198+
utils.ReadStringFlag(cmd, "github-token"),
199+
utils.ReadStringFlag(cmd, "github-base-branch"),
200+
fmt.Sprintf("SRE Bot right sizing %d resources", countRightSized),
201+
utils.ReadStringFlag(cmd, "terraform-file-path"),
202+
string(file.Bytes()),
203+
fmt.Sprintf("SRE Bot right sizing %d resources", countRightSized),
204+
description,
205+
)
206+
},
207+
}
208+
209+
func resolveValue(vars map[string]string, value string) string {
210+
varRegEx, err := regexp.Compile("local\\.(\\w+)")
211+
if err != nil {
212+
panic(err)
213+
}
214+
215+
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
216+
value = strings.TrimPrefix(value, "\"")
217+
value = strings.TrimSuffix(value, "\"")
218+
219+
exprRegEx, err := regexp.Compile("\\$\\{([\\w.]+)}")
220+
if err != nil {
221+
panic(err)
222+
}
223+
224+
items := exprRegEx.FindAllString(value, 100)
225+
for _, item := range items {
226+
resolvedItem := resolveValue(vars, item)
227+
value = strings.ReplaceAll(value, item, resolvedItem)
228+
}
229+
return value
230+
} else {
231+
if varRegEx.MatchString(value) {
232+
subMatch := varRegEx.FindStringSubmatch(value)
233+
value = vars[subMatch[1]]
234+
return resolveValue(vars, value)
235+
} else {
236+
return value
237+
}
238+
}
239+
}

Diff for: go.mod

+20
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,68 @@ require (
1010
github.com/fatih/color v1.16.0
1111
github.com/golang-jwt/jwt/v4 v4.5.0
1212
github.com/golang/protobuf v1.5.4
13+
github.com/google/go-github v17.0.0+incompatible
1314
github.com/google/go-github/v62 v62.0.0
15+
github.com/hashicorp/hcl/v2 v2.20.1
1416
github.com/jedib0t/go-pretty/v6 v6.5.9
1517
github.com/muesli/reflow v0.3.0
1618
github.com/rogpeppe/go-internal v1.11.0
1719
github.com/schollz/progressbar/v3 v3.14.2
1820
github.com/spf13/cobra v1.8.0
1921
github.com/stretchr/testify v1.8.4
22+
github.com/zclconf/go-cty v1.13.0
2023
golang.org/x/oauth2 v0.17.0
2124
google.golang.org/grpc v1.63.2
2225
google.golang.org/protobuf v1.34.0
26+
gopkg.in/src-d/go-git.v4 v4.13.1
2327
gopkg.in/yaml.v2 v2.4.0
2428
)
2529

2630
require (
31+
github.com/agext/levenshtein v1.2.1 // indirect
32+
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
33+
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
2734
github.com/atotto/clipboard v0.1.4 // indirect
2835
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2936
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
3037
github.com/davecgh/go-spew v1.1.1 // indirect
38+
github.com/emirpasic/gods v1.12.0 // indirect
39+
github.com/google/go-cmp v0.6.0 // indirect
3140
github.com/google/go-querystring v1.1.0 // indirect
3241
github.com/inconshreveable/mousetrap v1.1.0 // indirect
42+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
43+
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
3344
github.com/kr/pretty v0.3.1 // indirect
3445
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
3546
github.com/mattn/go-colorable v0.1.13 // indirect
3647
github.com/mattn/go-isatty v0.0.20 // indirect
3748
github.com/mattn/go-localereader v0.0.1 // indirect
3849
github.com/mattn/go-runewidth v0.0.15 // indirect
3950
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
51+
github.com/mitchellh/go-homedir v1.1.0 // indirect
52+
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
4053
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
4154
github.com/muesli/cancelreader v0.2.2 // indirect
4255
github.com/muesli/termenv v0.15.2 // indirect
4356
github.com/pmezard/go-difflib v1.0.0 // indirect
4457
github.com/rivo/uniseg v0.4.7 // indirect
58+
github.com/sergi/go-diff v1.0.0 // indirect
4559
github.com/spf13/pflag v1.0.5 // indirect
60+
github.com/src-d/gcfg v1.4.0 // indirect
61+
github.com/xanzy/ssh-agent v0.2.1 // indirect
62+
golang.org/x/crypto v0.22.0 // indirect
4663
golang.org/x/mod v0.9.0 // indirect
4764
golang.org/x/net v0.24.0 // indirect
4865
golang.org/x/sync v0.6.0 // indirect
4966
golang.org/x/sys v0.20.0 // indirect
5067
golang.org/x/term v0.20.0 // indirect
5168
golang.org/x/text v0.14.0 // indirect
69+
golang.org/x/tools v0.6.0 // indirect
5270
google.golang.org/appengine v1.6.8 // indirect
5371
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
5472
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
73+
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
74+
gopkg.in/warnings.v0 v0.1.2 // indirect
5575
gopkg.in/yaml.v3 v3.0.1 // indirect
5676
)
5777

0 commit comments

Comments
 (0)