mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
Signed-off-by: Marcus Söderberg <msoderb@gmail.com> Co-authored-by: Linghao Su <slh001@live.cn>
This commit is contained in:
7
assets/swagger.json
generated
7
assets/swagger.json
generated
@@ -8325,6 +8325,13 @@
|
||||
"description": "HostInfo holds metadata and resource usage metrics for a specific host in the cluster.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"labels": {
|
||||
"description": "Labels holds the labels attached to the host.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the hostname or node name in the Kubernetes cluster.",
|
||||
"type": "string"
|
||||
|
||||
@@ -774,7 +774,16 @@ func (ctrl *ApplicationController) getAppHosts(destCluster *appv1.Cluster, a *ap
|
||||
sort.Slice(resourcesInfo, func(i, j int) bool {
|
||||
return resourcesInfo[i].ResourceName < resourcesInfo[j].ResourceName
|
||||
})
|
||||
hosts = append(hosts, appv1.HostInfo{Name: nodeName, SystemInfo: node.SystemInfo, ResourcesInfo: resourcesInfo})
|
||||
|
||||
allowedNodeLabels := ctrl.settingsMgr.GetAllowedNodeLabels()
|
||||
nodeLabels := make(map[string]string)
|
||||
for _, label := range allowedNodeLabels {
|
||||
if val, ok := node.Labels[label]; ok {
|
||||
nodeLabels[label] = val
|
||||
}
|
||||
}
|
||||
|
||||
hosts = append(hosts, appv1.HostInfo{Name: nodeName, SystemInfo: node.SystemInfo, ResourcesInfo: resourcesInfo, Labels: nodeLabels})
|
||||
}
|
||||
ts.AddCheckpoint("process_app_pods_by_node_ms")
|
||||
return hosts, nil
|
||||
|
||||
@@ -2203,6 +2203,9 @@ func TestGetAppHosts(t *testing.T) {
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
configMapData: map[string]string{
|
||||
"application.allowedNodeLabels": "label1,label2",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(data, nil)
|
||||
mockStateCache := &mockstatecache.LiveStateCache{}
|
||||
@@ -2214,6 +2217,7 @@ func TestGetAppHosts(t *testing.T) {
|
||||
Name: "minikube",
|
||||
SystemInfo: corev1.NodeSystemInfo{OSImage: "debian"},
|
||||
Capacity: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("5")},
|
||||
Labels: map[string]string{"label1": "value1", "label2": "value2"},
|
||||
}})
|
||||
|
||||
// app pod
|
||||
@@ -2251,6 +2255,7 @@ func TestGetAppHosts(t *testing.T) {
|
||||
ResourceName: corev1.ResourceCPU, Capacity: 5000, RequestedByApp: 1000, RequestedByNeighbors: 2000,
|
||||
},
|
||||
},
|
||||
Labels: map[string]string{"label1": "value1", "label2": "value2"},
|
||||
}}, hosts)
|
||||
}
|
||||
|
||||
|
||||
1
controller/cache/cache.go
vendored
1
controller/cache/cache.go
vendored
@@ -169,6 +169,7 @@ type NodeInfo struct {
|
||||
Name string
|
||||
Capacity corev1.ResourceList
|
||||
SystemInfo corev1.NodeSystemInfo
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
type ResourceInfo struct {
|
||||
|
||||
1
controller/cache/info.go
vendored
1
controller/cache/info.go
vendored
@@ -472,6 +472,7 @@ func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
Name: node.Name,
|
||||
Capacity: node.Status.Capacity,
|
||||
SystemInfo: node.Status.NodeInfo,
|
||||
Labels: node.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
controller/cache/info_test.go
vendored
3
controller/cache/info_test.go
vendored
@@ -890,6 +890,8 @@ apiVersion: v1
|
||||
kind: Node
|
||||
metadata:
|
||||
name: minikube
|
||||
labels:
|
||||
foo: bar
|
||||
spec: {}
|
||||
status:
|
||||
capacity:
|
||||
@@ -907,6 +909,7 @@ status:
|
||||
Name: "minikube",
|
||||
Capacity: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("6091320Ki"), corev1.ResourceCPU: resource.MustParse("6")},
|
||||
SystemInfo: corev1.NodeSystemInfo{Architecture: "amd64", OperatingSystem: "linux", OSImage: "Ubuntu 20.04 LTS"},
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
}, info.NodeInfo)
|
||||
}
|
||||
|
||||
|
||||
BIN
docs/assets/application-pod-view-node-labels.png
Normal file
BIN
docs/assets/application-pod-view-node-labels.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -277,6 +277,9 @@ data:
|
||||
# If omitted, Argo CD injects the app name into the label: 'app.kubernetes.io/instance'
|
||||
application.instanceLabelKey: mycompany.com/appname
|
||||
|
||||
# An optional comma-separated list of node labels to propagate to the application pod view.
|
||||
application.allowedNodeLabels: topology.kubernetes.io/zone,node.kubernetes.io/instance-type
|
||||
|
||||
# You can change the resource tracking method Argo CD uses by changing the
|
||||
# setting application.resourceTrackingMethod to the desired method.
|
||||
# The following methods are available:
|
||||
|
||||
@@ -6,4 +6,15 @@ By default, the Application Details will show the `Tree` view.
|
||||
|
||||
This can be configured on an Application basis, by setting the `pref.argocd.argoproj.io/default-view` annotation, accepting one of: `tree`, `pods`, `network`, `list` as values.
|
||||
|
||||
For the Pods view, the default grouping mechanism can be configured using the `pref.argocd.argoproj.io/default-pod-sort` annotation, accepting one of: `node`, `parentResource`, `topLevelResource` as values.
|
||||
For the Pods view, the default grouping mechanism can be configured using the `pref.argocd.argoproj.io/default-pod-sort` annotation, accepting one of: `node`, `parentResource`, `topLevelResource` as values.
|
||||
|
||||
## Node Labels in Pod View
|
||||
|
||||
It's possible to propagate node labels to node information in the pod view by configuring `applications.allowedNodeLabels` in the [argocd-cm](argocd-cm-yaml.md) ConfigMap.
|
||||
|
||||
The following configuration:
|
||||
```yaml
|
||||
application.allowedNodeLabels: topology.kubernetes.io/zone,karpenter.sh/capacity-type
|
||||
```
|
||||
Would result in:
|
||||

|
||||
|
||||
1694
pkg/apis/application/v1alpha1/generated.pb.go
generated
1694
pkg/apis/application/v1alpha1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@@ -1161,6 +1161,9 @@ message HostInfo {
|
||||
|
||||
// SystemInfo contains detailed system-level information about the host, such as OS, kernel version, and architecture.
|
||||
optional .k8s.io.api.core.v1.NodeSystemInfo systemInfo = 3;
|
||||
|
||||
// Labels holds the labels attached to the host.
|
||||
map<string, string> labels = 4;
|
||||
}
|
||||
|
||||
// HostResourceInfo represents resource usage details for a specific resource type on a host.
|
||||
|
||||
@@ -1870,6 +1870,8 @@ type HostInfo struct {
|
||||
ResourcesInfo []HostResourceInfo `json:"resourcesInfo,omitempty" protobuf:"bytes,2,name=resourcesInfo"`
|
||||
// SystemInfo contains detailed system-level information about the host, such as OS, kernel version, and architecture.
|
||||
SystemInfo corev1.NodeSystemInfo `json:"systemInfo,omitempty" protobuf:"bytes,3,opt,name=systemInfo"`
|
||||
// Labels holds the labels attached to the host.
|
||||
Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,4,opt,name=labels"`
|
||||
}
|
||||
|
||||
// ApplicationTree represents the hierarchical structure of resources associated with an Argo CD application.
|
||||
|
||||
@@ -2255,6 +2255,13 @@ func (in *HostInfo) DeepCopyInto(out *HostInfo) {
|
||||
copy(*out, *in)
|
||||
}
|
||||
out.SystemInfo = in.SystemInfo
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ $pod-age-icon-clr: #ffce25;
|
||||
margin: 10px;
|
||||
margin-bottom: 14px !important;
|
||||
width: $pod-container-width + 2 * $padding;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
&--large {
|
||||
width: $pod-container-width + (2 * $padding) + ($num-stats * ($stat-width + 2 * $gutter)) + 6 * $gutter;
|
||||
@@ -52,6 +55,7 @@ $pod-age-icon-clr: #ffce25;
|
||||
&--header {
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
&--stats {
|
||||
margin-left: -2 * $gutter;
|
||||
@@ -76,11 +80,25 @@ $pod-age-icon-clr: #ffce25;
|
||||
border-radius: 3px;
|
||||
background-color: $argo-color-gray-3;
|
||||
color: $argo-color-gray-6;
|
||||
div {
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
&__item {
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
& div:last-child {
|
||||
|
||||
&__name {
|
||||
max-width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&__value {
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
max-width: 40%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface PodGroup extends Partial<ResourceNode> {
|
||||
renderMenu?: () => React.ReactNode;
|
||||
renderQuickStarts?: () => React.ReactNode;
|
||||
fullName?: string;
|
||||
hostLabels?: {[name: string]: string};
|
||||
}
|
||||
|
||||
export class PodView extends React.Component<PodViewProps> {
|
||||
@@ -162,12 +163,29 @@ export class PodView extends React.Component<PodViewProps> {
|
||||
</div>
|
||||
</div>
|
||||
{group.type === 'node' ? (
|
||||
<div className='pod-view__node__info--large'>
|
||||
{(group.info || []).map(item => (
|
||||
<div key={item.name}>
|
||||
{item.name}: <div>{item.value}</div>
|
||||
<div>
|
||||
<div className='pod-view__node__info--large'>
|
||||
{(group.info || []).map(item => (
|
||||
<Tooltip content={`${item.name}: ${item.value}`} key={item.name}>
|
||||
<div className='pod-view__node__info--large__item'>
|
||||
<div className='pod-view__node__info--large__item__name'>{item.name}:</div>
|
||||
<div className='pod-view__node__info--large__item__value'>{item.value}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
{group.hostLabels && Object.keys(group.hostLabels).length > 0 ? (
|
||||
<div className='pod-view__node__info--large'>
|
||||
{Object.keys(group.hostLabels || []).map(label => (
|
||||
<Tooltip content={`${label}: ${group.hostLabels[label]}`} key={label}>
|
||||
<div className='pod-view__node__info--large__item'>
|
||||
<div className='pod-view__node__info--large__item__name'>{label}:</div>
|
||||
<div className='pod-view__node__info--large__item__value'>{group.hostLabels[label]}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className='pod-view__node__info'>
|
||||
@@ -307,7 +325,8 @@ export class PodView extends React.Component<PodViewProps> {
|
||||
{name: 'Kernel Version', value: infraNode.systemInfo.kernelVersion},
|
||||
{name: 'OS/Arch', value: `${infraNode.systemInfo.operatingSystem}/${infraNode.systemInfo.architecture}`}
|
||||
],
|
||||
hostResourcesInfo: infraNode.resourcesInfo
|
||||
hostResourcesInfo: infraNode.resourcesInfo,
|
||||
hostLabels: infraNode.labels
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -377,7 +396,8 @@ export class PodView extends React.Component<PodViewProps> {
|
||||
{name: 'Kernel Version', value: 'N/A'},
|
||||
{name: 'OS/Arch', value: 'N/A'}
|
||||
],
|
||||
hostResourcesInfo: []
|
||||
hostResourcesInfo: [],
|
||||
hostLabels: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,6 +1005,7 @@ export interface Node {
|
||||
name: string;
|
||||
systemInfo: NodeSystemInfo;
|
||||
resourcesInfo: HostResourceInfo[];
|
||||
labels: {[name: string]: string};
|
||||
}
|
||||
|
||||
export interface NodeSystemInfo {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
informersv1 "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
v1listers "k8s.io/client-go/listers/core/v1"
|
||||
@@ -455,6 +456,8 @@ const (
|
||||
settingsApplicationInstanceLabelKey = "application.instanceLabelKey"
|
||||
// settingsResourceTrackingMethodKey is the key to configure tracking method for application resources
|
||||
settingsResourceTrackingMethodKey = "application.resourceTrackingMethod"
|
||||
// allowedNodeLabelsKey is the key to the list of allowed node labels for the application pod view
|
||||
allowedNodeLabelsKey = "application.allowedNodeLabels"
|
||||
// settingsInstallationID holds the key for the instance installation ID
|
||||
settingsInstallationID = "installationID"
|
||||
// resourcesCustomizationsKey is the key to the map of resource overrides
|
||||
@@ -2286,3 +2289,26 @@ func (mgr *SettingsManager) IsImpersonationEnabled() (bool, error) {
|
||||
}
|
||||
return cm.Data[impersonationEnabledKey] == "true", nil
|
||||
}
|
||||
|
||||
func (mgr *SettingsManager) GetAllowedNodeLabels() []string {
|
||||
labelKeys := []string{}
|
||||
argoCDCM, err := mgr.getConfigMap()
|
||||
if err != nil {
|
||||
log.Error(fmt.Errorf("failed getting allowedNodeLabels from configmap: %w", err))
|
||||
return labelKeys
|
||||
}
|
||||
value, ok := argoCDCM.Data[allowedNodeLabelsKey]
|
||||
if !ok || value == "" {
|
||||
return labelKeys
|
||||
}
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
keys := strings.SplitSeq(value, ",")
|
||||
for k := range keys {
|
||||
if errs := validation.IsQualifiedName(k); len(errs) > 0 {
|
||||
log.Warnf("Invalid node label key '%s' in configmap: %v", k, errs)
|
||||
continue
|
||||
}
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
return labelKeys
|
||||
}
|
||||
|
||||
@@ -1893,3 +1893,42 @@ func TestSettingsManager_GetHideSecretAnnotations(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsManager_GetAllowedNodeLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output []string
|
||||
}{
|
||||
{
|
||||
name: "Empty input",
|
||||
input: "",
|
||||
output: []string{},
|
||||
},
|
||||
{
|
||||
name: "Comma separated data",
|
||||
input: "example.com/label,label1,label2",
|
||||
output: []string{"example.com/label", "label1", "label2"},
|
||||
},
|
||||
{
|
||||
name: "Comma separated data with space",
|
||||
input: "example.com/label, label1, label2",
|
||||
output: []string{"example.com/label", "label1", "label2"},
|
||||
},
|
||||
{
|
||||
name: "Comma separated data with invalid label",
|
||||
input: "example.com/label,_invalid,label1,label2",
|
||||
output: []string{"example.com/label", "label1", "label2"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, settingsManager := fixtures(map[string]string{
|
||||
allowedNodeLabelsKey: tt.input,
|
||||
})
|
||||
keys := settingsManager.GetAllowedNodeLabels()
|
||||
assert.Len(t, keys, len(tt.output))
|
||||
assert.Equal(t, tt.output, keys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user