pkg/formatter/text_template_utils.go (270 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 formatter
import (
"bytes"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/elastic/cloud-sdk-go/pkg/api/platformapi/snaprepoapi"
)
const (
// floatFormat
floatFormat = "%.0f"
)
// rpad adds padding to the right of a string if the string is found to be
// empty "-" will be returned instead.
func rpad(s interface{}, padding int) string {
var str = "-"
template := fmt.Sprintf("%%-%ds", padding)
switch t := s.(type) {
case string:
if t != "" {
str = t
}
case *string:
if t != nil {
str = *t
}
case uint16:
str = strconv.Itoa(int(t))
case int32:
str = strconv.Itoa(int(t))
case *int32:
str = strconv.Itoa(int(*t))
case int64:
str = strconv.Itoa(int(t))
case int:
str = strconv.Itoa(t)
case bool:
str = strconv.FormatBool(t)
case *bool:
if t == nil {
str = "false"
} else {
str = strconv.FormatBool(*t)
}
case float64:
str = strconv.FormatFloat(t, 'f', -1, 64)
case *float64:
str = strconv.FormatFloat(*t, 'f', -1, 64)
default:
return fmt.Sprintf(template, s)
}
return fmt.Sprintf(template, str)
}
// formatBytes formats the capacity to human readable bytes
func formatBytes(rawCap int32, human bool) string {
const rpadSpace = 5
if human {
if rawCap == 0 {
return "-"
}
format := "%.2f"
if math.Remainder(float64(rawCap), float64(1024)) == 0 {
format = floatFormat
}
capacity := float32(rawCap) / 1024
if capacity < 1 {
format = "%d"
return fmt.Sprint(rpad(fmt.Sprintf(format, rawCap), rpadSpace), "MB")
}
if math.Mod(float64(capacity), float64(1024)) == 0 {
format = floatFormat
}
tbCapacity := capacity / 1024
if tbCapacity < 1 {
return fmt.Sprint(rpad(fmt.Sprintf(format, capacity), rpadSpace), "GB")
}
return fmt.Sprint(rpad(fmt.Sprintf(format, tbCapacity), rpadSpace), "TB")
}
return rpad(strconv.Itoa(int(rawCap)), rpadSpace)
}
// formatBytes formats the capacity to human bytes
func formatClusterBytes(rawCap int32, human bool) string {
var padding = 5
if human {
if rawCap == 0 {
return "-"
}
format := "%.2f"
if math.Remainder(float64(rawCap), float64(1024)) == 0 {
format = floatFormat
}
capacity := float32(rawCap) / 1024
if capacity < 1 {
format = "%d"
return fmt.Sprint(rpad(fmt.Sprintf(format, rawCap), padding), "MB")
}
if math.Mod(float64(capacity), float64(1024)) == 0 {
format = floatFormat
}
tbCapacity := capacity / 1024
if tbCapacity < 1 {
return fmt.Sprint(rpad(fmt.Sprintf(format, capacity), padding), "GB")
}
return fmt.Sprint(rpad(fmt.Sprintf(format, tbCapacity), padding), "TB")
}
return rpad(strconv.Itoa(int(rawCap)), 4)
}
func tab() string { return "\t" }
func substr(a, b int32) int32 { return a - b }
func computeClusterCapacity(plan *models.ElasticsearchClusterPlan) int32 {
var total = int32(0)
for _, t := range plan.ClusterTopology {
total += (t.MemoryPerNode * t.NodeCountPerZone) * t.ZoneCount
}
return total
}
func derefInt(i *int32) int32 { return *i }
func derefBool(i *bool) bool { return *i }
func displayAllocator(a *models.AllocatorInfo) bool {
return *a.Status.Connected || len(a.Instances) > 0
}
func getFailedPlanStepName(plan *models.ElasticsearchClusterPlanInfo) string {
for _, step := range plan.PlanAttemptLog {
if *step.Status == "error" && *step.StepID != "plan-completed" {
return *step.StepID
}
}
return "-"
}
func computePlanDuration(plan *models.ElasticsearchClusterPlanInfo) string {
start, err := time.Parse(time.RFC3339Nano, plan.AttemptStartTime.String())
if err != nil {
return "-"
}
end, err := time.Parse(time.RFC3339Nano, plan.AttemptEndTime.String())
if err != nil {
return "-"
}
return end.Sub(start).String()
}
func getApmFailedPlanStepName(plan *models.ApmPlanInfo) string {
for _, step := range plan.PlanAttemptLog {
if *step.Status == "error" && *step.StepID != "plan-completed" {
return *step.StepID
}
}
return "-"
}
func computeApmPlanDuration(plan *models.ApmPlanInfo) string {
start, err := time.Parse(time.RFC3339Nano, plan.AttemptStartTime.String())
if err != nil {
return "-"
}
end, err := time.Parse(time.RFC3339Nano, plan.AttemptEndTime.String())
if err != nil {
return "-"
}
return end.Sub(start).String()
}
func trimToLen(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
func rpadTrim(s string, n int) string {
var pad = n - 2
if pad < 1 {
panic("padding is too small, needs to be at least 3")
}
return rpad(trimToLen(s, pad), n)
}
// toS3TypeConfig receives an interface and returns an toS3TypeConfig
// the empty values will be transformed to "-"
func toS3TypeConfig(i interface{}) snaprepoapi.S3TypeConfig {
var buf = new(bytes.Buffer)
var typeconfig snaprepoapi.S3TypeConfig
// nolint
json.NewEncoder(buf).Encode(i)
// nolint
json.NewDecoder(buf).Decode(&typeconfig)
return typeconfig
}
func getClusterName(cluster *models.ElasticsearchClusterInfo) string {
if cluster.ClusterName == nil || *cluster.ClusterName != *cluster.ClusterID {
return trimToLen(*cluster.ClusterName, 32)
}
return "-"
}
func formatTopologyInfo(clusterInfo models.ElasticsearchClusterInfo) string {
var format = "%s\t%d"
var plan = getESCurrentOrPendingPlan(clusterInfo)
if len(plan.Plan.ClusterTopology) == 0 {
return fmt.Sprintf(format, "-", 0)
}
return fmt.Sprintf(
format,
formatClusterBytes(plan.Plan.ClusterTopology[0].MemoryPerNode, true),
plan.Plan.ClusterTopology[0].ZoneCount,
)
}
func getESCurrentOrPendingPlan(clusterInfo models.ElasticsearchClusterInfo) *models.ElasticsearchClusterPlanInfo {
if clusterInfo.PlanInfo.Pending != nil {
return clusterInfo.PlanInfo.Pending
}
// This will be returned if both (Current and Pending) are nil
// It populates the info in a best-effort manner, so data might
// be slightly off.
if clusterInfo.PlanInfo.Current == nil {
var (
zoneCount int32
zoneNames []string
memoryPerNode int32
version = "?.?.?"
nodeCountPerZone = int32(0)
)
for _, i := range clusterInfo.Topology.Instances {
if !stringInSlice(i.Zone, zoneNames) {
zoneNames = append(zoneNames, i.Zone)
zoneCount++
if strings.Split(*i.InstanceName, "-")[0] == "instance" && memoryPerNode == 0 {
if i.Memory != nil {
memoryPerNode = *i.Memory.InstanceCapacity
}
}
}
}
if len(clusterInfo.Topology.Instances) > 0 || zoneCount > 0 {
nodeCountPerZone = int32(len(clusterInfo.Topology.Instances)) / zoneCount
}
if len(clusterInfo.Topology.Instances) > 0 {
version = clusterInfo.Topology.Instances[0].ServiceVersion
}
return &models.ElasticsearchClusterPlanInfo{
Plan: &models.ElasticsearchClusterPlan{
Elasticsearch: &models.ElasticsearchConfiguration{
Version: version,
},
ClusterTopology: []*models.ElasticsearchClusterTopologyElement{
{
NodeCountPerZone: nodeCountPerZone,
MemoryPerNode: memoryPerNode,
ZoneCount: zoneCount,
},
},
},
}
}
return clusterInfo.PlanInfo.Current
}
func centiCentsToCents(i int) int {
return i / 100
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// equal checks if the passed values are equal. Currently only strings
// are supported.
func equal(x, y interface{}) bool {
// setting a to something different than "" to avoid a case where
// the passed types are not handled by the switch cases
var a = "x"
var b string
switch s := x.(type) {
case string:
a = s
case *string:
a = *s
}
switch s := y.(type) {
case string:
b = s
case *string:
b = *s
}
return a == b
}