mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
The troubleshoot project uses klog logging library. There is room for only one library unfortunately. Sorry logrus :)
600 lines
15 KiB
Go
600 lines
15 KiB
Go
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/handlers"
|
|
"github.com/pkg/errors"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/apimachinery/pkg/util/version"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
const (
|
|
VolumeStackPrefix = "volume-"
|
|
ControllerServiceName = "controller"
|
|
ReplicaServiceName = "replica"
|
|
|
|
HostProcPath = "/host/proc"
|
|
ReplicaDirectory = "/replicas/"
|
|
DeviceDirectory = "/dev/longhorn/"
|
|
TemporaryMountPointDirectory = "/tmp/mnt/"
|
|
|
|
DefaultKubernetesTolerationKey = "kubernetes.io"
|
|
|
|
DiskConfigFile = "longhorn-disk.cfg"
|
|
|
|
SizeAlignment = 2 * 1024 * 1024
|
|
MinimalVolumeSize = 10 * 1024 * 1024
|
|
)
|
|
|
|
var (
|
|
cmdTimeout = time.Minute // one minute by default
|
|
reservedLabels = []string{"KubernetesStatus", "ranchervm-base-image"}
|
|
|
|
APIRetryInterval = 500 * time.Millisecond
|
|
APIRetryJitterInterval = 50 * time.Millisecond
|
|
APIRetryCounts = 10
|
|
)
|
|
|
|
type MetadataConfig struct {
|
|
DriverName string
|
|
Image string
|
|
OrcImage string
|
|
DriverContainerName string
|
|
}
|
|
|
|
type DiskInfo struct {
|
|
Fsid string
|
|
Path string
|
|
Type string
|
|
FreeBlock int64
|
|
TotalBlock int64
|
|
BlockSize int64
|
|
StorageMaximum int64
|
|
StorageAvailable int64
|
|
}
|
|
|
|
func ConvertSize(size interface{}) (int64, error) {
|
|
switch size := size.(type) {
|
|
case int64:
|
|
return size, nil
|
|
case int:
|
|
return int64(size), nil
|
|
case string:
|
|
if size == "" {
|
|
return 0, nil
|
|
}
|
|
quantity, err := resource.ParseQuantity(size)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "error parsing size '%s'", size)
|
|
}
|
|
return quantity.Value(), nil
|
|
}
|
|
return 0, errors.Errorf("could not parse size '%v'", size)
|
|
}
|
|
|
|
func RoundUpSize(size int64) int64 {
|
|
if size <= 0 {
|
|
return SizeAlignment
|
|
}
|
|
r := size % SizeAlignment
|
|
if r == 0 {
|
|
return size
|
|
}
|
|
return size - r + SizeAlignment
|
|
}
|
|
|
|
func Backoff(maxDuration time.Duration, timeoutMessage string, f func() (bool, error)) error {
|
|
startTime := time.Now()
|
|
waitTime := 150 * time.Millisecond
|
|
maxWaitTime := 2 * time.Second
|
|
for {
|
|
if time.Now().Sub(startTime) > maxDuration {
|
|
return errors.New(timeoutMessage)
|
|
}
|
|
|
|
if done, err := f(); err != nil {
|
|
return err
|
|
} else if done {
|
|
return nil
|
|
}
|
|
|
|
time.Sleep(waitTime)
|
|
|
|
waitTime *= 2
|
|
if waitTime > maxWaitTime {
|
|
waitTime = maxWaitTime
|
|
}
|
|
}
|
|
}
|
|
|
|
func UUID() string {
|
|
return uuid.NewString()
|
|
}
|
|
|
|
// WaitForDevice timeout in second
|
|
func WaitForDevice(dev string, timeout int) error {
|
|
for i := 0; i < timeout; i++ {
|
|
st, err := os.Stat(dev)
|
|
if err == nil {
|
|
if st.Mode()&os.ModeDevice == 0 {
|
|
return fmt.Errorf("Invalid mode for %v: 0x%x", dev, st.Mode())
|
|
}
|
|
return nil
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
return fmt.Errorf("timeout waiting for %v", dev)
|
|
}
|
|
|
|
func RandomID() string {
|
|
return UUID()[:8]
|
|
}
|
|
|
|
func GetLocalIPs() ([]string, error) {
|
|
results := []string{}
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, addr := range addrs {
|
|
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() {
|
|
if ip.IP.To4() != nil {
|
|
results = append(results, ip.IP.String())
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// WaitForAPI timeout in second
|
|
func WaitForAPI(url string, timeout int) error {
|
|
for i := 0; i < timeout; i++ {
|
|
resp, err := http.Get(url)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
return nil
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
return fmt.Errorf("timeout waiting for %v", url)
|
|
}
|
|
|
|
func Now() string {
|
|
return time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
func ParseTime(t string) (time.Time, error) {
|
|
return time.Parse(time.RFC3339, t)
|
|
|
|
}
|
|
|
|
func Execute(envs []string, binary string, args ...string) (string, error) {
|
|
return ExecuteWithTimeout(cmdTimeout, envs, binary, args...)
|
|
}
|
|
|
|
func ExecuteWithTimeout(timeout time.Duration, envs []string, binary string, args ...string) (string, error) {
|
|
var err error
|
|
cmd := exec.Command(binary, args...)
|
|
cmd.Env = append(os.Environ(), envs...)
|
|
done := make(chan struct{})
|
|
|
|
var output, stderr bytes.Buffer
|
|
cmd.Stdout = &output
|
|
cmd.Stderr = &stderr
|
|
|
|
go func() {
|
|
err = cmd.Run()
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(timeout):
|
|
if cmd.Process != nil {
|
|
if err := cmd.Process.Kill(); err != nil {
|
|
klog.Warningf("Problem killing process pid=%v: %s", cmd.Process.Pid, err)
|
|
}
|
|
|
|
}
|
|
return "", fmt.Errorf("timeout executing: %v %v, output %s, stderr, %s, error %v",
|
|
binary, args, output.String(), stderr.String(), err)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute: %v %v, output %s, stderr, %s, error %v",
|
|
binary, args, output.String(), stderr.String(), err)
|
|
}
|
|
return output.String(), nil
|
|
}
|
|
|
|
func ExecuteWithoutTimeout(envs []string, binary string, args ...string) (string, error) {
|
|
cmd := exec.Command(binary, args...)
|
|
cmd.Env = append(os.Environ(), envs...)
|
|
|
|
var output, stderr bytes.Buffer
|
|
cmd.Stdout = &output
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return output.String(), fmt.Errorf("failed to execute: %v %v, output %s, stderr, %s, error %v",
|
|
binary, args, output.String(), stderr.String(), err)
|
|
}
|
|
return output.String(), nil
|
|
}
|
|
|
|
func TimestampAfterTimeout(ts string, timeout time.Duration) bool {
|
|
now := time.Now()
|
|
t, err := time.Parse(time.RFC3339, ts)
|
|
if err != nil {
|
|
klog.Errorf("Cannot parse time %v", ts)
|
|
return false
|
|
}
|
|
deadline := t.Add(timeout)
|
|
return now.After(deadline)
|
|
}
|
|
|
|
func TimestampWithinLimit(latest time.Time, ts string, limit time.Duration) bool {
|
|
t, err := time.Parse(time.RFC3339, ts)
|
|
if err != nil {
|
|
klog.Errorf("Cannot parse time %v", ts)
|
|
return false
|
|
}
|
|
deadline := t.Add(limit)
|
|
return deadline.After(latest)
|
|
}
|
|
|
|
func ValidateName(name string) bool {
|
|
validName := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`)
|
|
return validName.MatchString(name)
|
|
}
|
|
|
|
func GetBackupID(backupURL string) (string, error) {
|
|
u, err := url.Parse(backupURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
v := u.Query()
|
|
volumeName := v.Get("volume")
|
|
backupName := v.Get("backup")
|
|
if !ValidateName(volumeName) || !ValidateName(backupName) {
|
|
return "", fmt.Errorf("Invalid name parsed, got %v and %v", backupName, volumeName)
|
|
}
|
|
return backupName, nil
|
|
}
|
|
|
|
func GetRequiredEnv(key string) (string, error) {
|
|
env := os.Getenv(key)
|
|
if env == "" {
|
|
return "", fmt.Errorf("can't get required environment variable, env %v wasn't set", key)
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
// ParseLabels parses the provided Labels based on longhorn-engine's implementation:
|
|
// https://github.com/longhorn/longhorn-engine/blob/master/util/util.go
|
|
func ParseLabels(labels []string) (map[string]string, error) {
|
|
result := map[string]string{}
|
|
for _, label := range labels {
|
|
kv := strings.SplitN(label, "=", 2)
|
|
if len(kv) != 2 {
|
|
return nil, fmt.Errorf("invalid label not in <key>=<value> format %v", label)
|
|
}
|
|
key := kv[0]
|
|
value := kv[1]
|
|
if errList := validation.IsQualifiedName(key); len(errList) > 0 {
|
|
return nil, fmt.Errorf("invalid key %v for label: %v", key, errList[0])
|
|
}
|
|
// We don't need to validate the Label value since we're allowing for any form of data to be stored, similar
|
|
// to Kubernetes Annotations. Of course, we should make sure it isn't empty.
|
|
if value == "" {
|
|
return nil, fmt.Errorf("invalid empty value for label with key %v", key)
|
|
}
|
|
result[key] = value
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func RegisterShutdownChannel(done chan struct{}) {
|
|
sigs := make(chan os.Signal, 1)
|
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
sig := <-sigs
|
|
klog.V(2).Infof("Receive %v to exit", sig)
|
|
close(done)
|
|
}()
|
|
}
|
|
|
|
func SplitStringToMap(str, separator string) map[string]struct{} {
|
|
ret := map[string]struct{}{}
|
|
splits := strings.Split(str, separator)
|
|
for _, str := range splits {
|
|
// splits can have empty member
|
|
str = strings.TrimSpace(str)
|
|
if str == "" {
|
|
continue
|
|
}
|
|
ret[str] = struct{}{}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func GetSortedKeysFromMap(maps interface{}) []string {
|
|
v := reflect.ValueOf(maps)
|
|
if v.Kind() != reflect.Map {
|
|
return nil
|
|
}
|
|
mapKeys := v.MapKeys()
|
|
keys := make([]string, 0, len(mapKeys))
|
|
for _, k := range mapKeys {
|
|
keys = append(keys, k.String())
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// AutoCorrectName converts name to lowercase, and correct overlength name by
|
|
// replaces the name suffix with 8 char from its checksum to ensure uniquenedoss.
|
|
func AutoCorrectName(name string, maxLength int) string {
|
|
newName := strings.ToLower(name)
|
|
if len(name) > maxLength {
|
|
klog.Warningf("Name %v is too long, auto-correct to fit %v characters", name, maxLength)
|
|
checksum := GetStringChecksum(name)
|
|
newNameSuffix := "-" + checksum[:8]
|
|
newNamePrefix := strings.TrimRight(newName[:maxLength-len(newNameSuffix)], "-")
|
|
newName = newNamePrefix + newNameSuffix
|
|
}
|
|
if newName != name {
|
|
klog.Warningf("Name auto-corrected from %v to %v", name, newName)
|
|
}
|
|
return newName
|
|
}
|
|
|
|
func GetStringChecksum(data string) string {
|
|
return GetChecksumSHA512([]byte(data))
|
|
}
|
|
|
|
func GetChecksumSHA512(data []byte) string {
|
|
checksum := sha512.Sum512(data)
|
|
return hex.EncodeToString(checksum[:])
|
|
}
|
|
|
|
func GetStringHash(data string) string {
|
|
hash := fnv.New32a()
|
|
hash.Write([]byte(data))
|
|
return fmt.Sprint(strconv.FormatInt(int64(hash.Sum32()), 16))
|
|
}
|
|
|
|
func CheckBackupType(backupTarget string) (string, error) {
|
|
u, err := url.Parse(backupTarget)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return u.Scheme, nil
|
|
}
|
|
|
|
func RetryOnConflictCause(fn func() (interface{}, error)) (interface{}, error) {
|
|
return RetryOnErrorCondition(fn, apierrors.IsConflict)
|
|
}
|
|
|
|
func RetryOnNotFoundCause(fn func() (interface{}, error)) (interface{}, error) {
|
|
return RetryOnErrorCondition(fn, apierrors.IsNotFound)
|
|
}
|
|
|
|
func RetryOnErrorCondition(fn func() (interface{}, error), predicate func(error) bool) (interface{}, error) {
|
|
for i := 0; i < APIRetryCounts; i++ {
|
|
obj, err := fn()
|
|
if err == nil {
|
|
return obj, nil
|
|
}
|
|
if !predicate(err) {
|
|
return nil, err
|
|
}
|
|
time.Sleep(APIRetryInterval + APIRetryJitterInterval*time.Duration(rand.Intn(5)))
|
|
}
|
|
return nil, fmt.Errorf("cannot finish API request due to too many error retries")
|
|
}
|
|
|
|
func RunAsync(wg *sync.WaitGroup, f func()) {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
f()
|
|
}()
|
|
}
|
|
|
|
type filteredLoggingHandler struct {
|
|
filteredPaths map[string]struct{}
|
|
handler http.Handler
|
|
loggingHandler http.Handler
|
|
}
|
|
|
|
func FilteredLoggingHandler(filteredPaths map[string]struct{}, writer io.Writer, router http.Handler) http.Handler {
|
|
|
|
return filteredLoggingHandler{
|
|
filteredPaths: filteredPaths,
|
|
handler: router,
|
|
loggingHandler: handlers.CombinedLoggingHandler(writer, router),
|
|
}
|
|
}
|
|
|
|
func (h filteredLoggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
switch req.Method {
|
|
case "GET":
|
|
if _, exists := h.filteredPaths[req.URL.Path]; exists {
|
|
h.handler.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
}
|
|
h.loggingHandler.ServeHTTP(w, req)
|
|
}
|
|
|
|
func ValidateSnapshotLabels(labels map[string]string) (map[string]string, error) {
|
|
validLabels := make(map[string]string)
|
|
for key, val := range labels {
|
|
if errList := validation.IsQualifiedName(key); len(errList) > 0 {
|
|
return nil, fmt.Errorf("at least one error encountered while validating backup label with key %v: %v",
|
|
key, errList[0])
|
|
}
|
|
if val == "" {
|
|
return nil, fmt.Errorf("value for label with key %v cannot be empty", key)
|
|
}
|
|
validLabels[key] = val
|
|
}
|
|
|
|
for _, key := range reservedLabels {
|
|
if _, ok := validLabels[key]; ok {
|
|
return nil, fmt.Errorf("specified snapshot backup labels contain reserved keyword %v", key)
|
|
}
|
|
}
|
|
|
|
return validLabels, nil
|
|
}
|
|
|
|
func ValidateTags(inputTags []string) ([]string, error) {
|
|
foundTags := make(map[string]struct{})
|
|
var tags []string
|
|
for _, tag := range inputTags {
|
|
if _, ok := foundTags[tag]; ok {
|
|
continue
|
|
}
|
|
errList := validation.IsQualifiedName(tag)
|
|
if len(errList) > 0 {
|
|
return nil, fmt.Errorf("at least one error encountered while validating tags: %v", errList[0])
|
|
}
|
|
foundTags[tag] = struct{}{}
|
|
tags = append(tags, tag)
|
|
}
|
|
|
|
sort.Strings(tags)
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func IsKubernetesDefaultToleration(toleration v1.Toleration) bool {
|
|
if strings.Contains(toleration.Key, DefaultKubernetesTolerationKey) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func GetAnnotation(obj runtime.Object, annotationKey string) (string, error) {
|
|
objMeta, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot get annotation of invalid object %v: %v", obj, err)
|
|
}
|
|
|
|
annos := objMeta.GetAnnotations()
|
|
if annos == nil {
|
|
return "", nil
|
|
}
|
|
|
|
return annos[annotationKey], nil
|
|
}
|
|
|
|
func SetAnnotation(obj runtime.Object, annotationKey, annotationValue string) error {
|
|
objMeta, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot set annotation for invalid object %v: %v", obj, err)
|
|
}
|
|
|
|
annos := objMeta.GetAnnotations()
|
|
if annos == nil {
|
|
annos = map[string]string{}
|
|
}
|
|
|
|
annos[annotationKey] = annotationValue
|
|
objMeta.SetAnnotations(annos)
|
|
return nil
|
|
}
|
|
|
|
func GetDistinctTolerations(tolerationList []v1.Toleration) []v1.Toleration {
|
|
res := []v1.Toleration{}
|
|
tolerationMap := TolerationListToMap(tolerationList)
|
|
for _, t := range tolerationMap {
|
|
res = append(res, t)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func TolerationListToMap(tolerationList []v1.Toleration) map[string]v1.Toleration {
|
|
res := map[string]v1.Toleration{}
|
|
for _, t := range tolerationList {
|
|
// We use checksum of the toleration to separate 2 tolerations
|
|
// with the same t.Key but different operator/effect/value
|
|
res[GetTolerationChecksum(t)] = t
|
|
}
|
|
return res
|
|
}
|
|
|
|
func GetTolerationChecksum(t v1.Toleration) string {
|
|
return GetStringChecksum(string(t.Key) + string(t.Operator) + string(t.Value) + string(t.Effect))
|
|
}
|
|
|
|
func IsSupportedFileSystem(fsType string) bool {
|
|
if fsType == "ext4" || fsType == "ext3" || fsType == "ext2" || fsType == "xfs" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func IsKubernetesVersionAtLeast(kubeClient clientset.Interface, vers string) (bool, error) {
|
|
serverVersion, err := kubeClient.Discovery().ServerVersion()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to get Kubernetes server version")
|
|
}
|
|
currentVersion := version.MustParseSemantic(serverVersion.GitVersion)
|
|
minVersion := version.MustParseSemantic(vers)
|
|
return currentVersion.AtLeast(minVersion), nil
|
|
}
|
|
|
|
type DiskConfig struct {
|
|
DiskUUID string `json:"diskUUID"`
|
|
}
|
|
|
|
func MinInt(a, b int) int {
|
|
if a <= b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func Contains(list []string, item string) bool {
|
|
for _, i := range list {
|
|
if i == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|