cost-optimization/gke-shift-left-cost/main.go (181 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/fernandorubbo/k8s-cost-estimator/api"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
)
const version = "v0.0.1"
var (
k8sPath = flag.String("k8s", "", "Required. Path to k8s manifests folder")
k8sPrevPath = flag.String("k8s-prev", "", "Optional. Path to the previous K8s manifests folder. Useful to compare prices.")
outputFile = flag.String("output", "", "Optional. Output file path. If not provided, console is used")
environ = flag.String("environ", "LOCAL", "Optional. Where your code is running at. Used to know determine the output file format: GITHUB | GITLAB | LOCAL")
authKey = flag.String("auth-key", "", "Optional. The GCP service account JSON key filepath. If not provided, default service account is used (Run 'gcloud auth application-default login' to set your user as the default service account)")
configFile = flag.String("config", "", "Optional. The defaults configuration YAML filepath to set: machine family, region and compute resources not provided in k8s manifests")
verbosity = flag.String("v", "panic", "Optional. Verbosity: panic|fatal|error|warn|info|debug|trace. Default panic")
)
func init() {
flag.Parse()
level, err := log.ParseLevel(*verbosity)
exitOnError("Invalid 'verbosity' parameter", err)
if *environ == "GITLAB" {
log.SetFormatter(&log.JSONFormatter{
DisableTimestamp: true,
FieldMap: log.FieldMap{
log.FieldKeyLevel: "severity",
},
})
}
log.SetOutput(os.Stdout)
log.SetLevel(level)
// required flags
validateK8sPath(*k8sPath, "k8s")
}
func main() {
log.Infof("Starting cost estimation (version %s)...", version)
config := readConfigFromFile()
priceCatalog := newGCPPriceCatalog(config)
currentCost := estimateCost(*k8sPath, config, priceCatalog)
if isPreviousPathProvided() {
log.Infof("Comparing current cost against previous version. Paths: '%s' vs '%s'", *k8sPath, *k8sPrevPath)
previousCosts := estimateCost(*k8sPrevPath, config, priceCatalog)
diffCost := currentCost.Subtract(previousCosts)
outputDiff(diffCost)
} else {
output(currentCost.ToMarkdown())
}
log.Info("Finished cost estimation!")
}
func readConfigFromFile() api.CostimatorConfig {
conf := api.ConfigDefaults()
if *configFile != "" {
data, err := ioutil.ReadFile(*configFile)
exitOnError("Unable to read 'config' file", err)
err = yaml.Unmarshal(data, &conf)
exitOnError("Unable to umarshal 'config' file", err)
} else {
log.Debugf("Parameter 'config' not provided. Using default config.")
}
return conf
}
func newGCPPriceCatalog(config api.CostimatorConfig) api.GCPPriceCatalog {
log.Debug("Retriving Price Catalog from GCP...")
credentials := readAuthKeyFromFile()
priceCatalog, err := api.NewGCPPriceCatalog(credentials, config)
exitOnError("Unable to read Pricing Catalog from GCP", err)
return priceCatalog
}
func readAuthKeyFromFile() []byte {
var credentials []byte
if *authKey != "" {
var err error
credentials, err = ioutil.ReadFile(*authKey)
exitOnError("Unable to read auth-key file", err)
} else {
log.Info("auth-key not provided. Using default service account.")
}
return credentials
}
func validateK8sPath(k8sPath string, flag string) {
if !isK8sPathProvided(k8sPath, flag) {
exit(fmt.Sprintf("%s is required", flag))
}
}
func isPreviousPathProvided() bool {
return isK8sPathProvided(*k8sPrevPath, "k8s-prev")
}
func isK8sPathProvided(k8sPath string, flag string) bool {
if k8sPath == "" {
return false
}
f, err := os.Stat(k8sPath)
if os.IsNotExist(err) {
exit(fmt.Sprintf("%s provided does not exists", flag))
}
if !(f.IsDir() || strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml")) {
exit(fmt.Sprintf("%s provided must be a folder or a yaml file", flag))
}
return true
}
func estimateCost(path string, conf api.CostimatorConfig, pc api.GCPPriceCatalog) api.Cost {
log.Infof("Estimating monthly cost for k8s objects in path '%s'...", path)
manifests := api.Manifests{}
err := manifests.LoadObjectsFromPath(path, conf)
if err != nil {
exitOnError(fmt.Sprintf("Unable estimate cost for %s", path), err)
}
return manifests.EstimateCost(pc)
}
func outputDiff(diffCost api.DiffCost) {
output(diffCost.ToMarkdown())
if *outputFile == "" {
return
}
saveDiffFile(diffCost)
}
func output(markdown string) {
fmt.Printf("\n%s\n", markdown)
if *outputFile == "" {
return
}
switch strings.ToUpper(*environ) {
case "GITHUB":
log.Debugf("Saving Github file at '%s'", *outputFile)
saveGithubFile(markdown)
case "GITLAB":
log.Debugf("Saving Gitlab file at '%s'", *outputFile)
saveGithubFile(markdown)
default:
log.Debugf("Saving Markdown file at '%s'", *outputFile)
saveMarkdownFile(markdown)
}
}
func saveDiffFile(diffCost api.DiffCost) {
ext := path.Ext(*outputFile)
diffOutputFile := (*outputFile)[0:len(*outputFile)-len(ext)] + ".diff"
log.Debugf("Saving Diff file at '%s'", diffOutputFile)
f, err := os.Create(diffOutputFile)
exitOnError(fmt.Sprintf("Creating Diff file %s", diffOutputFile), err)
defer f.Close()
pd := diffCost.MonthlyDiffRange.ToPriceDiff()
err = json.NewEncoder(f).Encode(pd)
exitOnError(fmt.Sprintf("Writting Diff file %s", diffOutputFile), err)
}
func saveGithubFile(markdown string) {
type github struct {
Body string `json:"body"`
}
gh := &github{
Body: markdown,
}
f, err := os.Create(*outputFile)
exitOnError(fmt.Sprintf("Creating output file %s", *outputFile), err)
defer f.Close()
err = json.NewEncoder(f).Encode(gh)
exitOnError(fmt.Sprintf("Writting output file %s", *outputFile), err)
}
func saveMarkdownFile(markdown string) {
err := ioutil.WriteFile(*outputFile, []byte(markdown), 0644)
exitOnError(fmt.Sprintf("Writing output file %s", *outputFile), err)
}
func exitOnError(message string, err error) {
if err != nil {
exitWithError(message, err)
}
}
func exitWithError(message string, err error) {
fmt.Printf("\nError: %s\nCause: %+v\n\nSee parameters options below:\n", err, message)
flag.PrintDefaults()
os.Exit(-1)
}
func exit(message string) {
fmt.Printf("\nError: %s\n\nSee parameters options below:\n", message)
flag.PrintDefaults()
os.Exit(-1)
}