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  }