package costmodel import ( "errors" "fmt" "regexp" "strings" "github.com/opencost/opencost/core/pkg/clustercache" "github.com/opencost/opencost/core/pkg/log" "github.com/opencost/opencost/core/pkg/source" ) var ( // Static KeyTuple Errors ErrNewKeyTuple = errors.New("new-key-tuple: key not containing exactly 3 components") // Static Errors for ContainerMetric creation ErrInvalidKey error = errors.New("not a valid key") ErrNoContainer error = errors.New("vector does not have container name") ErrNoContainerName error = errors.New("vector does not have string container name") ErrNoPod error = errors.New("vector does not have pod name") ErrNoPodName error = errors.New("vector does not have string pod name") ErrNoNamespace error = errors.New("vector does not have namespace") ErrNoNamespaceName error = errors.New("vector does not have string namespace") ErrNoNodeName error = errors.New("vector does not have string node") ErrNoClusterID error = errors.New("vector does not have string cluster id") ) const invalidNodeNameErrFmt = `invalid node name: %s was likely set from "instance" label. cAdvisor scrape configs require the following: relabel_configs: - action: labelmap regex: __meta_kubernetes_node_name replacement: node` //-------------------------------------------------------------------------- // KeyTuple //-------------------------------------------------------------------------- // KeyTuple contains is a utility which parses Namespace, Key, and ClusterID from a // comma delimitted string. type KeyTuple struct { key string kIndex int cIndex int } // Namespace returns the the namespace from the string key. func (kt *KeyTuple) Namespace() string { return kt.key[0 : kt.kIndex-1] } // Key returns the identifier from the string key. func (kt *KeyTuple) Key() string { return kt.key[kt.kIndex : kt.cIndex-1] } // ClusterID returns the cluster identifier from the string key. func (kt *KeyTuple) ClusterID() string { return kt.key[kt.cIndex:] } // NewKeyTuple creates a new KeyTuple instance by determining the exact indices of each tuple // entry. When each component is requested, a string slice is returned using the boundaries. func NewKeyTuple(key string) (*KeyTuple, error) { kIndex := strings.IndexRune(key, ',') if kIndex < 0 { return nil, ErrNewKeyTuple } kIndex += 1 subIndex := strings.IndexRune(key[kIndex:], ',') if subIndex < 0 { return nil, ErrNewKeyTuple } cIndex := kIndex + subIndex + 1 if strings.ContainsRune(key[cIndex:], ',') { return nil, ErrNewKeyTuple } return &KeyTuple{ key: key, kIndex: kIndex, cIndex: cIndex, }, nil } //-------------------------------------------------------------------------- // ContainerMetric //-------------------------------------------------------------------------- // ContainerMetric contains a set of identifiers specific to a kubernetes container including // a unique string key type ContainerMetric struct { Namespace string PodName string ContainerName string NodeName string ClusterID string key string } // Key returns a unique string key that can be used in map[string]interface{} func (c *ContainerMetric) Key() string { return c.key } // containerMetricKey creates a unique string key, a comma delimitted list of the provided // parameters. func containerMetricKey(ns, podName, containerName, nodeName, clusterID string) string { return ns + "," + podName + "," + containerName + "," + nodeName + "," + clusterID } // NewContainerMetricFromKey creates a new ContainerMetric instance using a provided comma delimitted // string key. func NewContainerMetricFromKey(key string) (*ContainerMetric, error) { s := strings.Split(key, ",") if len(s) == 5 { return &ContainerMetric{ Namespace: s[0], PodName: s[1], ContainerName: s[2], NodeName: s[3], ClusterID: s[4], key: key, }, nil } return nil, ErrInvalidKey } // NewContainerMetricFromValues creates a new ContainerMetric instance using the provided string parameters. func NewContainerMetricFromValues(ns, podName, containerName, nodeName, clusterId string) *ContainerMetric { return &ContainerMetric{ Namespace: ns, PodName: podName, ContainerName: containerName, NodeName: nodeName, ClusterID: clusterId, key: containerMetricKey(ns, podName, containerName, nodeName, clusterId), } } // NewContainerMetricsFromPod creates a slice of ContainerMetric instances for each container in the // provided Pod. func NewContainerMetricsFromPod(pod *clustercache.Pod, clusterID string) ([]*ContainerMetric, error) { podName := pod.Name ns := pod.Namespace node := pod.Spec.NodeName var cs []*ContainerMetric for _, container := range pod.Spec.Containers { containerName := container.Name cs = append(cs, &ContainerMetric{ Namespace: ns, PodName: podName, ContainerName: containerName, NodeName: node, ClusterID: clusterID, key: containerMetricKey(ns, podName, containerName, node, clusterID), }) } return cs, nil } // NewContainerMetricFromResult accepts the metrics map from a QueryResult and returns a new ContainerMetric // instance func NewContainerMetricFromResult(result *source.QueryResult, defaultClusterID string) (*ContainerMetric, error) { containerName, err := result.GetContainer() if err != nil { return nil, ErrNoContainer } podName, err := result.GetPod() if err != nil { return nil, ErrNoPodName } namespace, err := result.GetNamespace() if err != nil { return nil, ErrNoNamespaceName } nodeName, err := result.GetNode() if err != nil { log.Debugf("Prometheus vector does not have node name") nodeName = "" } clusterID, err := result.GetCluster() if err != nil { log.Debugf("Prometheus vector does not have cluster id") clusterID = defaultClusterID } return &ContainerMetric{ ContainerName: containerName, PodName: podName, Namespace: namespace, NodeName: nodeName, ClusterID: clusterID, key: containerMetricKey(namespace, podName, containerName, nodeName, clusterID), }, nil } func NewContainerMetricFrom(result *source.ContainerMetricResult, defaultClusterID string) (*ContainerMetric, error) { containerName := result.Container if containerName == "" { return nil, ErrNoContainer } podName := result.Pod if podName == "" { return nil, ErrNoPodName } namespace := result.Namespace if namespace == "" { return nil, ErrNoNamespaceName } nodeName := result.Node if !isValidNodeName(nodeName) { return nil, fmt.Errorf(invalidNodeNameErrFmt, nodeName) } if nodeName == "" { log.Debugf("metric vector does not have node name") nodeName = "" } clusterID := result.Cluster if clusterID == "" { clusterID = defaultClusterID } return &ContainerMetric{ ContainerName: containerName, PodName: podName, Namespace: namespace, NodeName: nodeName, ClusterID: clusterID, key: containerMetricKey(namespace, podName, containerName, nodeName, clusterID), }, nil } /* - contain at least 1 character - contain no more than 253 characters - contain only lowercase alphanumeric characters, '-' or '.' - start with an alphanumeric character - end with an alphanumeric character */ var nodeNameRegex = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9\-\.]*[a-z0-9])?$`) // isValidNodeName determines if the nodeName provided is valid according to DNS subdomain // specifications: RFC 1123 func isValidNodeName(nodeName string) bool { if len(nodeName) > 253 { return false } return nodeNameRegex.Match([]byte(nodeName)) }