forked from Praqma/helmsman
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.go
331 lines (289 loc) · 10.2 KB
/
utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v2"
"github.com/BurntSushi/toml"
"github.com/Praqma/helmsman/aws"
"github.com/Praqma/helmsman/gcs"
"github.com/logrusorgru/aurora"
)
// printMap prints to the console any map of string keys and values.
func printMap(m map[string]string) {
for key, value := range m {
fmt.Println(key, " : ", value)
}
}
// printObjectMap prints to the console any map of string keys and object values.
func printNamespacesMap(m map[string]namespace) {
for key, value := range m {
fmt.Println(key, " : protected = ", value)
}
}
// fromTOML reads a toml file and decodes it to a state type.
// It uses the BurntSuchi TOML parser which throws an error if the TOML file is not valid.
func fromTOML(file string, s *state) (bool, string) {
if _, err := toml.DecodeFile(file, s); err != nil {
return false, err.Error()
}
return true, "INFO: Parsed TOML [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps."
}
// toTOML encodes a state type into a TOML file.
// It uses the BurntSuchi TOML parser.
func toTOML(file string, s *state) {
log.Println("printing generated toml ... ")
var buff bytes.Buffer
var (
newFile *os.File
err error
)
if err := toml.NewEncoder(&buff).Encode(s); err != nil {
logError(err.Error())
os.Exit(1)
}
newFile, err = os.Create(file)
if err != nil {
logError(err.Error())
}
bytesWritten, err := newFile.Write(buff.Bytes())
if err != nil {
logError(err.Error())
}
log.Printf("Wrote %d bytes.\n", bytesWritten)
newFile.Close()
}
// fromYAML reads a yaml file and decodes it to a state type.
// parser which throws an error if the YAML file is not valid.
func fromYAML(file string, s *state) (bool, string) {
yamlFile, err := ioutil.ReadFile(file)
if err != nil {
return false, err.Error()
}
if err = yaml.Unmarshal(yamlFile, s); err != nil {
return false, err.Error()
}
return true, "INFO: Parsed YAML [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps."
}
// toYaml encodes a state type into a YAML file
func toYAML(file string, s *state) {
log.Println("printing generated yaml ... ")
var buff bytes.Buffer
var (
newFile *os.File
err error
)
if err := yaml.NewEncoder(&buff).Encode(s); err != nil {
logError(err.Error())
os.Exit(1)
}
newFile, err = os.Create(file)
if err != nil {
logError(err.Error())
}
bytesWritten, err := newFile.Write(buff.Bytes())
if err != nil {
logError(err.Error())
}
log.Printf("Wrote %d bytes.\n", bytesWritten)
newFile.Close()
}
// invokes either yaml or toml parser considering file extension
func fromFile(file string, s *state) (bool, string) {
if isOfType(file, ".toml") {
return fromTOML(file, s)
} else if isOfType(file, ".yaml") {
return fromYAML(file, s)
} else {
return false, "State file does not have toml/yaml extension."
}
}
func toFile(file string, s *state) {
if isOfType(file, ".toml") {
toTOML(file, s)
} else if isOfType(file, ".yaml") {
fromYAML(file, s)
} else {
logError("ERROR: State file does not have toml/yaml extension.")
}
}
// isOfType checks if the file extension of a filename/path is the same as "filetype".
// isisOfType is case insensitive. filetype should contain the "." e.g. ".yaml"
func isOfType(filename string, filetype string) bool {
return filepath.Ext(strings.ToLower(filename)) == strings.ToLower(filetype)
}
// readFile returns the content of a file as a string.
// takes a file path as input. It throws an error and breaks the program execution if it fails to read the file.
func readFile(filepath string) string {
data, err := ioutil.ReadFile(filepath)
if err != nil {
logError("ERROR: failed to read [ " + filepath + " ] file content: " + err.Error())
}
return string(data)
}
// printHelp prints Helmsman commands
func printHelp() {
fmt.Println("Helmsman version: " + appVersion)
fmt.Println("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.")
fmt.Println("Usage: helmsman [options]")
fmt.Println()
fmt.Println("Options:")
fmt.Println("-f desired state file name(s), may be supplied more than once to merge state files.")
fmt.Println("-e file(s) to load environment variables from (default .env), may be supplied more than once")
fmt.Println("--debug prints basic logs during execution.")
fmt.Println("--dry-run apply the dry-run option for helm commands.")
fmt.Println("--apply generates and applies an action plan.")
fmt.Println("--verbose prints more verbose logs during execution.")
fmt.Println("--ns-override overrides defined namespaces with a provided one.")
fmt.Println("--skip-validation generates and applies an action plan.")
fmt.Println("--apply-labels applies Helmsman labels to Helm state for all defined apps.")
fmt.Println("--keep-untracked-releases keep releases that are managed by Helmsman and are no longer tracked in your desired state.")
fmt.Println("--help prints Helmsman help.")
fmt.Println("--no-banner don't show the banner")
fmt.Println("-v prints Helmsman version.")
}
// logVersions prints the versions of kubectl and helm to the logs
func logVersions() {
log.Println("VERBOSE: kubectl client version: " + kubectlVersion)
log.Println("VERBOSE: Helm client version: " + helmVersion)
}
// substituteEnv checks if a string has an env variable (contains '$'), then it returns its value
// if the env variable is empty or unset, an empty string is returned
// if the string does not contain '$', it is returned as is.
func substituteEnv(name string) string {
if strings.Contains(name, "$") {
return os.ExpandEnv(name)
}
return name
}
// sliceContains checks if a string slice contains a given string
func sliceContains(slice []string, s string) bool {
for _, a := range slice {
if strings.TrimSpace(a) == s {
return true
}
}
return false
}
// downloadFile downloads a file from GCS or AWS buckets and name it with a given outfile
// if downloaded, returns the outfile name. If the file path is local file system path, it is returned as is.
func downloadFile(path string, outfile string) string {
if strings.HasPrefix(path, "s3") {
tmp := getBucketElements(path)
aws.ReadFile(tmp["bucketName"], tmp["filePath"], outfile)
} else if strings.HasPrefix(path, "gs") {
tmp := getBucketElements(path)
gcs.ReadFile(tmp["bucketName"], tmp["filePath"], outfile)
} else {
log.Println("INFO: " + outfile + " will be used from local file system.")
copyFile(path, outfile)
}
return outfile
}
// copyFile copies a file from source to destination
func copyFile(source string, destination string) {
from, err := os.Open(source)
if err != nil {
logError("ERROR: while copying " + source + " to " + destination + " : " + err.Error())
}
defer from.Close()
to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
logError("ERROR: while copying " + source + " to " + destination + " : " + err.Error())
}
defer to.Close()
_, err = io.Copy(to, from)
if err != nil {
logError("ERROR: while copying " + source + " to " + destination + " : " + err.Error())
}
}
// deleteFile deletes a file
func deleteFile(path string) {
log.Println("INFO: cleaning up ... deleting " + path)
if err := os.Remove(path); err != nil {
logError("ERROR: could not delete file: " + path)
}
}
// notifySlack sends a JSON formatted message to Slack over a webhook url
// It takes the content of the message (what changes helmsman is going to do or have done separated by \n)
// and the webhook URL as well as a flag specifying if this is a failure message or not
// It returns true if the sending of the message is successful, otherwise returns false
func notifySlack(content string, url string, failure bool, executing bool) bool {
log.Println("INFO: posting notifications to slack ... ")
color := "#36a64f" // green
if failure {
color = "#FF0000" // red
}
var pretext string
if content == "" {
pretext = "No actions to perform!"
} else if failure {
pretext = "Failed to generate/execute a plan: "
} else if executing && !failure {
pretext = "Here is what I have done: "
} else {
pretext = "Here is what I am going to do:"
}
t := time.Now().UTC()
var jsonStr = []byte(`{
"attachments": [
{
"fallback": "Helmsman results.",
"color": "` + color + `" ,
"pretext": "` + pretext + `",
"title": "` + content + `",
"footer": "Helmsman ` + appVersion + `",
"ts": ` + strconv.FormatInt(t.Unix(), 10) + `
}
]
}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logError("ERROR: while sending notifications to slack" + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return true
}
return false
}
// logError sends a notification on slack if a webhook URL is provided and logs the error before terminating.
func logError(msg string) {
if _, err := url.ParseRequestURI(s.Settings.SlackWebhook); err == nil {
notifySlack(msg, s.Settings.SlackWebhook, true, apply)
}
log.Fatal(aurora.Bold(aurora.Red(msg)))
}
// getBucketElements returns a map containing the bucket name and the file path inside the bucket
// this func works for S3 and GCS bucket links of the format:
// s3 or gs://bucketname/dir.../file.ext
func getBucketElements(link string) map[string]string {
tmp := strings.SplitAfterN(link, "//", 2)[1]
m := make(map[string]string)
m["bucketName"] = strings.SplitN(tmp, "/", 2)[0]
m["filePath"] = strings.SplitN(tmp, "/", 2)[1]
return m
}
// replaceStringInFile takes a map of keys and values and replaces the keys with values within a given file.
// It saves the modified content in a new file
func replaceStringInFile(input []byte, outfile string, replacements map[string]string) {
output := input
for k, v := range replacements {
output = bytes.Replace(output, []byte(k), []byte(v), -1)
}
if err := ioutil.WriteFile(outfile, output, 0666); err != nil {
logError(err.Error())
}
}