package kubecost import ( "fmt" "sort" "strings" "github.com/kubecost/cost-model/pkg/log" "github.com/kubecost/cost-model/pkg/prom" ) const ( AllocationNilProp string = "" AllocationClusterProp string = "cluster" AllocationNodeProp string = "node" AllocationContainerProp string = "container" AllocationControllerProp string = "controller" AllocationControllerKindProp string = "controllerKind" AllocationNamespaceProp string = "namespace" AllocationPodProp string = "pod" AllocationProviderIDProp string = "providerID" AllocationServiceProp string = "service" AllocationLabelProp string = "label" AllocationAnnotationProp string = "annotation" AllocationDeploymentProp string = "deployment" AllocationStatefulSetProp string = "statefulset" AllocationDaemonSetProp string = "daemonset" AllocationJobProp string = "job" AllocationDepartmentProp string = "department" AllocationEnvironmentProp string = "environment" AllocationOwnerProp string = "owner" AllocationProductProp string = "product" AllocationTeamProp string = "team" ) func ParseProperty(text string) (string, error) { switch strings.TrimSpace(strings.ToLower(text)) { case "cluster": return AllocationClusterProp, nil case "node": return AllocationNodeProp, nil case "container": return AllocationContainerProp, nil case "controller": return AllocationControllerProp, nil case "controllerkind": return AllocationControllerKindProp, nil case "namespace": return AllocationNamespaceProp, nil case "pod": return AllocationPodProp, nil case "providerid": return AllocationProviderIDProp, nil case "service": return AllocationServiceProp, nil case "label": return AllocationLabelProp, nil case "annotation": return AllocationAnnotationProp, nil case "deployment": return AllocationDeploymentProp, nil case "daemonset": return AllocationDaemonSetProp, nil case "statefulset": return AllocationStatefulSetProp, nil case "job": return AllocationJobProp, nil case "department": return AllocationDepartmentProp, nil case "environment": return AllocationEnvironmentProp, nil case "owner": return AllocationOwnerProp, nil case "product": return AllocationProductProp, nil case "team": return AllocationTeamProp, nil } if strings.HasPrefix(text, "label:") { label := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "label:"))) return fmt.Sprintf("label:%s", label), nil } if strings.HasPrefix(text, "annotation:") { annotation := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "annotation:"))) return fmt.Sprintf("annotation:%s", annotation), nil } return AllocationNilProp, fmt.Errorf("invalid allocation property: %s", text) } // AllocationProperties describes a set of Kubernetes objects. type AllocationProperties struct { Cluster string `json:"cluster,omitempty"` Node string `json:"node,omitempty"` Container string `json:"container,omitempty"` Controller string `json:"controller,omitempty"` ControllerKind string `json:"controllerKind,omitempty"` Namespace string `json:"namespace,omitempty"` Pod string `json:"pod,omitempty"` Services []string `json:"services,omitempty"` ProviderID string `json:"providerID,omitempty"` Labels AllocationLabels `json:"labels,omitempty"` Annotations AllocationAnnotations `json:"annotations,omitempty"` } // AllocationLabels is a schema-free mapping of key/value pairs that can be // attributed to an Allocation type AllocationLabels map[string]string // AllocationAnnotations is a schema-free mapping of key/value pairs that can be // attributed to an Allocation type AllocationAnnotations map[string]string func (p *AllocationProperties) Clone() *AllocationProperties { if p == nil { return nil } clone := &AllocationProperties{} clone.Cluster = p.Cluster clone.Node = p.Node clone.Container = p.Container clone.Controller = p.Controller clone.ControllerKind = p.ControllerKind clone.Namespace = p.Namespace clone.Pod = p.Pod clone.ProviderID = p.ProviderID var services []string services = append(services, p.Services...) clone.Services = services labels := make(map[string]string, len(p.Labels)) for k, v := range p.Labels { labels[k] = v } clone.Labels = labels annotations := make(map[string]string, len(p.Annotations)) for k, v := range p.Annotations { annotations[k] = v } clone.Annotations = annotations return clone } func (p *AllocationProperties) Equal(that *AllocationProperties) bool { if p == nil || that == nil { return false } if p.Cluster != that.Cluster { return false } if p.Node != that.Node { return false } if p.Container != that.Container { return false } if p.Controller != that.Controller { return false } if p.ControllerKind != that.ControllerKind { return false } if p.Namespace != that.Namespace { return false } if p.Pod != that.Pod { return false } if p.ProviderID != that.ProviderID { return false } pLabels := p.Labels thatLabels := that.Labels if len(pLabels) == len(thatLabels) { for k, pv := range pLabels { tv, ok := thatLabels[k] if !ok || tv != pv { return false } } } else { return false } pAnnotations := p.Annotations thatAnnotations := that.Annotations if len(pAnnotations) == len(thatAnnotations) { for k, pv := range pAnnotations { tv, ok := thatAnnotations[k] if !ok || tv != pv { return false } } } else { return false } pServices := p.Services thatServices := that.Services if len(pServices) == len(thatServices) { sort.Strings(pServices) sort.Strings(thatServices) for i, pv := range pServices { tv := thatServices[i] if tv != pv { return false } } } else { return false } return true } // GenerateKey generates a string that represents the key by which the // AllocationProperties should be aggregated, given the properties defined by // the aggregateBy parameter and the given label configuration. func (p *AllocationProperties) GenerateKey(aggregateBy []string, labelConfig *LabelConfig) string { if p == nil { return "" } if labelConfig == nil { labelConfig = NewLabelConfig() } // Names will ultimately be joined into a single name, which uniquely // identifies allocations. names := []string{} for _, agg := range aggregateBy { switch true { case agg == AllocationClusterProp: names = append(names, p.Cluster) case agg == AllocationNodeProp: names = append(names, p.Node) case agg == AllocationNamespaceProp: names = append(names, p.Namespace) case agg == AllocationControllerKindProp: controllerKind := p.ControllerKind if controllerKind == "" { // Indicate that allocation has no controller controllerKind = UnallocatedSuffix } names = append(names, controllerKind) case agg == AllocationDaemonSetProp || agg == AllocationStatefulSetProp || agg == AllocationDeploymentProp || agg == AllocationJobProp: controller := p.Controller if agg != p.ControllerKind || controller == "" { // The allocation does not have the specified controller kind controller = UnallocatedSuffix } names = append(names, controller) case agg == AllocationControllerProp: controller := p.Controller if controller == "" { // Indicate that allocation has no controller controller = UnallocatedSuffix } else if p.ControllerKind != "" { controller = fmt.Sprintf("%s:%s", p.ControllerKind, controller) } names = append(names, controller) case agg == AllocationPodProp: names = append(names, p.Pod) case agg == AllocationContainerProp: names = append(names, p.Container) case agg == AllocationServiceProp: services := p.Services if len(services) == 0 { // Indicate that allocation has no services names = append(names, UnallocatedSuffix) } else { // This just uses the first service for _, service := range services { names = append(names, service) break } } case strings.HasPrefix(agg, "label:"): labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelName := labelConfig.Sanitize(strings.TrimPrefix(agg, "label:")) if labelValue, ok := labels[labelName]; ok { names = append(names, fmt.Sprintf("%s=%s", labelName, labelValue)) } else { names = append(names, UnallocatedSuffix) } } case strings.HasPrefix(agg, "annotation:"): annotations := p.Annotations if annotations == nil { names = append(names, UnallocatedSuffix) } else { annotationName := labelConfig.Sanitize(strings.TrimPrefix(agg, "annotation:")) if annotationValue, ok := annotations[annotationName]; ok { names = append(names, fmt.Sprintf("%s=%s", annotationName, annotationValue)) } else { names = append(names, UnallocatedSuffix) } } case agg == AllocationDepartmentProp: labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelNames := strings.Split(labelConfig.DepartmentLabel, ",") for _, labelName := range labelNames { labelName = labelConfig.Sanitize(labelName) if labelValue, ok := labels[labelName]; ok { names = append(names, labelValue) } else { names = append(names, UnallocatedSuffix) } } } case agg == AllocationEnvironmentProp: labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelNames := strings.Split(labelConfig.EnvironmentLabel, ",") for _, labelName := range labelNames { labelName = labelConfig.Sanitize(labelName) if labelValue, ok := labels[labelName]; ok { names = append(names, labelValue) } else { names = append(names, UnallocatedSuffix) } } } case agg == AllocationOwnerProp: labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelNames := strings.Split(labelConfig.OwnerLabel, ",") for _, labelName := range labelNames { labelName = labelConfig.Sanitize(labelName) if labelValue, ok := labels[labelName]; ok { names = append(names, labelValue) } else { names = append(names, UnallocatedSuffix) } } } case agg == AllocationProductProp: labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelNames := strings.Split(labelConfig.ProductLabel, ",") for _, labelName := range labelNames { labelName = labelConfig.Sanitize(labelName) if labelValue, ok := labels[labelName]; ok { names = append(names, labelValue) } else { names = append(names, UnallocatedSuffix) } } } case agg == AllocationTeamProp: labels := p.Labels if labels == nil { names = append(names, UnallocatedSuffix) } else { labelNames := strings.Split(labelConfig.TeamLabel, ",") for _, labelName := range labelNames { labelName = labelConfig.Sanitize(labelName) if labelValue, ok := labels[labelName]; ok { names = append(names, labelValue) } else { names = append(names, UnallocatedSuffix) } } } default: // This case should never be reached, as input up until this point // should be checked and rejected if invalid. But if we do get a // value we don't recognize, log a warning. log.Warningf("generateKey: illegal aggregation parameter: %s", agg) } } return strings.Join(names, "/") } // Intersection returns an *AllocationProperties which contains all matching fields between the calling and parameter AllocationProperties // nillable slices and maps are left as nil func (p *AllocationProperties) Intersection(that *AllocationProperties) *AllocationProperties { if p == nil || that == nil { return nil } intersectionProps := &AllocationProperties{} if p.Cluster == that.Cluster { intersectionProps.Cluster = p.Cluster } if p.Node == that.Node { intersectionProps.Node = p.Node } if p.Container == that.Container { intersectionProps.Container = p.Container } if p.Controller == that.Controller { intersectionProps.Controller = p.Controller } if p.ControllerKind == that.ControllerKind { intersectionProps.ControllerKind = p.ControllerKind } if p.Namespace == that.Namespace { intersectionProps.Namespace = p.Namespace } if p.Pod == that.Pod { intersectionProps.Pod = p.Pod } if p.ProviderID == that.ProviderID { intersectionProps.ProviderID = p.ProviderID } return intersectionProps } func (p *AllocationProperties) String() string { if p == nil { return "" } strs := []string{} if p.Cluster != "" { strs = append(strs, "Cluster:"+p.Cluster) } if p.Node != "" { strs = append(strs, "Node:"+p.Node) } if p.Container != "" { strs = append(strs, "Container:"+p.Container) } if p.Controller != "" { strs = append(strs, "Controller:"+p.Controller) } if p.ControllerKind != "" { strs = append(strs, "ControllerKind:"+p.ControllerKind) } if p.Namespace != "" { strs = append(strs, "Namespace:"+p.Namespace) } if p.Pod != "" { strs = append(strs, "Pod:"+p.Pod) } if p.ProviderID != "" { strs = append(strs, "ProviderID:"+p.ProviderID) } if len(p.Services) > 0 { strs = append(strs, "Services:"+strings.Join(p.Services, ";")) } var labelStrs []string for k, prop := range p.Labels { labelStrs = append(labelStrs, fmt.Sprintf("%s:%s", k, prop)) } strs = append(strs, fmt.Sprintf("Labels:{%s}", strings.Join(labelStrs, ","))) var annotationStrs []string for k, prop := range p.Annotations { annotationStrs = append(annotationStrs, fmt.Sprintf("%s:%s", k, prop)) } strs = append(strs, fmt.Sprintf("Annotations:{%s}", strings.Join(annotationStrs, ","))) return fmt.Sprintf("{%s}", strings.Join(strs, "; ")) }