sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scaffolds 18 19 import ( 20 "fmt" 21 "io" 22 "os" 23 "strings" 24 25 log "github.com/sirupsen/logrus" 26 27 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 28 "sigs.k8s.io/kubebuilder/v3/pkg/plugins" 29 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates" 30 31 "sigs.k8s.io/yaml" 32 ) 33 34 var _ plugins.Scaffolder = &editScaffolder{} 35 36 const configFilePath = "grafana/custom-metrics/config.yaml" 37 38 type editScaffolder struct { 39 // fs is the filesystem that will be used by the scaffolder 40 fs machinery.Filesystem 41 } 42 43 // NewEditScaffolder returns a new Scaffolder for project edition operations 44 func NewEditScaffolder() plugins.Scaffolder { 45 return &editScaffolder{} 46 } 47 48 // InjectFS implements cmdutil.Scaffolder 49 func (s *editScaffolder) InjectFS(fs machinery.Filesystem) { 50 s.fs = fs 51 } 52 53 func fileExist(configFilePath string) bool { 54 if _, err := os.Stat(configFilePath); os.IsNotExist(err) { 55 return false 56 } 57 return true 58 } 59 60 func loadConfig(configPath string) ([]templates.CustomMetricItem, error) { 61 if !fileExist(configPath) { 62 return nil, nil 63 } 64 65 // nolint:gosec 66 f, err := os.Open(configPath) 67 if err != nil { 68 return nil, fmt.Errorf("error loading plugin config: %w", err) 69 } 70 71 items, err := configReader(f) 72 73 if err := f.Close(); err != nil { 74 return nil, fmt.Errorf("could not close config.yaml: %w", err) 75 } 76 77 return items, err 78 } 79 80 func configReader(reader io.Reader) ([]templates.CustomMetricItem, error) { 81 yamlFile, err := io.ReadAll(reader) 82 if err != nil { 83 return nil, err 84 } 85 86 config := templates.CustomMetricsConfig{} 87 88 err = yaml.Unmarshal(yamlFile, &config) 89 if err != nil { 90 return nil, err 91 } 92 93 validatedMetricItems := validateCustomMetricItems(config.CustomMetrics) 94 95 return validatedMetricItems, nil 96 } 97 98 func validateCustomMetricItems(rawItems []templates.CustomMetricItem) []templates.CustomMetricItem { 99 // 1. Filter items of missing `Metric` or `Type` 100 filterResult := []templates.CustomMetricItem{} 101 for _, item := range rawItems { 102 if hasFields(item) { 103 filterResult = append(filterResult, item) 104 } 105 } 106 107 // 2. Fill Expr and Unit if missing 108 validatedItems := make([]templates.CustomMetricItem, len(filterResult)) 109 for i, item := range filterResult { 110 item = fillMissingExpr(item) 111 validatedItems[i] = fillMissingUnit(item) 112 } 113 114 return validatedItems 115 } 116 117 func hasFields(item templates.CustomMetricItem) bool { 118 // If `Expr` exists, return true 119 if item.Expr != "" { 120 return true 121 } 122 123 // If `Metric` & valid `Type` exists, return true 124 metricType := strings.ToLower(item.Type) 125 if item.Metric != "" && (metricType == "counter" || metricType == "gauge" || metricType == "histogram") { 126 return true 127 } 128 129 return false 130 } 131 132 // TODO: Prom_ql exprs can improved to be more pratical and applicable 133 func fillMissingExpr(item templates.CustomMetricItem) templates.CustomMetricItem { 134 if item.Expr == "" { 135 switch strings.ToLower(item.Type) { 136 case "counter": 137 item.Expr = "sum(rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)` 138 case "histogram": 139 // nolint: lll 140 item.Expr = "histogram_quantile(0.90, sum by(instance, le) (rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])))` 141 default: // gauge 142 item.Expr = item.Metric 143 } 144 } 145 return item 146 } 147 148 func fillMissingUnit(item templates.CustomMetricItem) templates.CustomMetricItem { 149 if item.Unit == "" { 150 name := strings.ToLower(item.Metric) 151 item.Unit = "none" 152 if strings.Contains(name, "second") || strings.Contains(name, "duration") { 153 item.Unit = "s" 154 } else if strings.Contains(name, "byte") { 155 item.Unit = "bytes" 156 } else if strings.Contains(name, "ratio") { 157 item.Unit = "percent" 158 } 159 } 160 return item 161 } 162 163 // Scaffold implements cmdutil.Scaffolder 164 func (s *editScaffolder) Scaffold() error { 165 log.Println("Generating Grafana manifests to visualize controller status...") 166 167 // Initialize the machinery.Scaffold that will write the files to disk 168 scaffold := machinery.NewScaffold(s.fs) 169 170 configPath := string(configFilePath) 171 172 var templatesBuilder = []machinery.Builder{ 173 &templates.RuntimeManifest{}, 174 &templates.ResourcesManifest{}, 175 &templates.CustomMetricsConfigManifest{ConfigPath: configPath}, 176 } 177 178 configItems, err := loadConfig(configPath) 179 if err == nil && len(configItems) > 0 { 180 templatesBuilder = append(templatesBuilder, &templates.CustomMetricsDashManifest{Items: configItems}) 181 } else if err != nil { 182 fmt.Fprintf(os.Stderr, "Error on scaffolding manifest for custom metris:\n%v", err) 183 } 184 185 return scaffold.Execute(templatesBuilder...) 186 }