Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
72c2675a2d chore(deps): update actions/create-github-app-token action to v2.2.2 2026-03-27 03:14:45 +00:00
22 changed files with 111 additions and 726 deletions

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
with:
app-id: ${{ secrets.CHERRYPICK_APP_ID }}
private-key: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}

View File

@@ -19,7 +19,7 @@
## What is Argo CD?
Argo CD is a declarative GitOps continuous delivery tool for Kubernetes.
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
![Argo CD UI](docs/assets/argocd-ui.gif)
@@ -45,7 +45,7 @@ Check live demo at https://cd.apps.argoproj.io/.
You can reach the Argo CD community and developers via the following channels:
* Q & A : [GitHub Discussions](https://github.com/argoproj/argo-cd/discussions)
* Q & A : [Github Discussions](https://github.com/argoproj/argo-cd/discussions)
* Chat : [The #argo-cd Slack channel](https://argoproj.github.io/community/join-slack)
* Contributors Office Hours: [Every Thursday](https://calendar.google.com/calendar/u/0/embed?src=argoproj@gmail.com) | [Agenda](https://docs.google.com/document/d/1xkoFkVviB70YBzSEa4bDnu-rUZ1sIFtwKKG1Uw8XsY8)
* User Community meeting: [First Wednesday of the month](https://calendar.google.com/calendar/u/0/embed?src=argoproj@gmail.com) | [Agenda](https://docs.google.com/document/d/1ttgw98MO45Dq7ZUHpIiOIEfbyeitKHNfMjbY5dLLMKQ)

View File

@@ -848,9 +848,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
log.Errorf("CompareAppState error getting server side diff dry run applier: %s", err)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionUnknownError, Message: err.Error(), LastTransitionTime: &now})
}
if cleanup != nil {
defer cleanup()
}
defer cleanup()
diffConfigBuilder.WithServerSideDryRunner(diff.NewK8sServerSideDryRunner(applier))
}

View File

@@ -1,7 +1,7 @@
# Verification of Argo CD Artifacts
## Prerequisites
- cosign `v2.0.0` or higher [installation instructions](https://docs.sigstore.dev/cosign/system_config/installation/)
- cosign `v2.0.0` or higher [installation instructions](https://docs.sigstore.dev/cosign/installation)
- slsa-verifier [installation instructions](https://github.com/slsa-framework/slsa-verifier#installation)
- crane [installation instructions](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md) (for container verification only)
@@ -154,4 +154,4 @@ slsa-verifier verify-artifact sbom.tar.gz \
> [!NOTE]
> We encourage all users to verify signatures and provenances with your admission/policy controller of choice. Doing so will verify that an image was built by us before it's deployed on your Kubernetes cluster.
Cosign signatures and SLSA provenances are compatible with several types of admission controllers. Please see the [cosign documentation](https://docs.sigstore.dev/policy-controller/overview/) and [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#verification) for supported controllers.
Cosign signatures and SLSA provenances are compatible with several types of admission controllers. Please see the [cosign documentation](https://docs.sigstore.dev/cosign/overview/#kubernetes-integrations) and [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#verification) for supported controllers.

View File

@@ -220,7 +220,7 @@ func NewClusterCache(config *rest.Config, opts ...UpdateSettingsFunc) *clusterCa
listRetryLimit: 1,
listRetryUseBackoff: false,
listRetryFunc: ListRetryFuncNever,
parentUIDToChildren: make(map[types.UID]map[kube.ResourceKey]struct{}),
parentUIDToChildren: make(map[types.UID][]kube.ResourceKey),
}
for i := range opts {
opts[i](cache)
@@ -280,11 +280,10 @@ type clusterCache struct {
respectRBAC int
// Parent-to-children index for O(1) child lookup during hierarchy traversal
// Maps any resource's UID to a set of its direct children's ResourceKeys
// Using a set eliminates O(k) duplicate checking on insertions
// Used for cross-namespace hierarchy traversal; namespaced traversal still builds a graph
parentUIDToChildren map[types.UID]map[kube.ResourceKey]struct{}
// Parent-to-children index for O(1) hierarchy traversal
// Maps any resource's UID to its direct children's ResourceKeys
// Eliminates need for O(n) graph building during hierarchy traversal
parentUIDToChildren map[types.UID][]kube.ResourceKey
}
type clusterCacheSync struct {
@@ -505,35 +504,27 @@ func (c *clusterCache) setNode(n *Resource) {
for k, v := range ns {
// update child resource owner references
if n.isInferredParentOf != nil && mightHaveInferredOwner(v) {
shouldBeParent := n.isInferredParentOf(k)
v.setOwnerRef(n.toOwnerRef(), shouldBeParent)
// Update index inline for inferred ref changes.
// Note: The removal case (shouldBeParent=false) is currently unreachable for
// StatefulSet→PVC relationships because Kubernetes makes volumeClaimTemplates
// immutable. We include it for defensive correctness and future-proofing.
if n.Ref.UID != "" {
if shouldBeParent {
c.addToParentUIDToChildren(n.Ref.UID, k)
} else {
c.removeFromParentUIDToChildren(n.Ref.UID, k)
}
}
v.setOwnerRef(n.toOwnerRef(), n.isInferredParentOf(k))
}
if mightHaveInferredOwner(n) && v.isInferredParentOf != nil {
childKey := n.ResourceKey()
shouldBeParent := v.isInferredParentOf(childKey)
n.setOwnerRef(v.toOwnerRef(), shouldBeParent)
// Update index inline for inferred ref changes.
// Note: The removal case (shouldBeParent=false) is currently unreachable for
// StatefulSet→PVC relationships because Kubernetes makes volumeClaimTemplates
// immutable. We include it for defensive correctness and future-proofing.
if v.Ref.UID != "" {
if shouldBeParent {
c.addToParentUIDToChildren(v.Ref.UID, childKey)
} else {
c.removeFromParentUIDToChildren(v.Ref.UID, childKey)
}
}
n.setOwnerRef(v.toOwnerRef(), v.isInferredParentOf(n.ResourceKey()))
}
}
}
}
// rebuildParentToChildrenIndex rebuilds the parent-to-children index after a full sync
// This is called after initial sync to ensure all parent-child relationships are tracked
func (c *clusterCache) rebuildParentToChildrenIndex() {
// Clear existing index
c.parentUIDToChildren = make(map[types.UID][]kube.ResourceKey)
// Rebuild parent-to-children index from all resources with owner refs
for _, resource := range c.resources {
key := resource.ResourceKey()
for _, ownerRef := range resource.OwnerRefs {
if ownerRef.UID != "" {
c.addToParentUIDToChildren(ownerRef.UID, key)
}
}
}
@@ -541,29 +532,31 @@ func (c *clusterCache) setNode(n *Resource) {
// addToParentUIDToChildren adds a child to the parent-to-children index
func (c *clusterCache) addToParentUIDToChildren(parentUID types.UID, childKey kube.ResourceKey) {
// Get or create the set for this parent
childrenSet := c.parentUIDToChildren[parentUID]
if childrenSet == nil {
childrenSet = make(map[kube.ResourceKey]struct{})
c.parentUIDToChildren[parentUID] = childrenSet
// Check if child is already in the list to avoid duplicates
children := c.parentUIDToChildren[parentUID]
for _, existing := range children {
if existing == childKey {
return // Already exists, no need to add
}
}
// Add child to set (O(1) operation, automatically handles duplicates)
childrenSet[childKey] = struct{}{}
c.parentUIDToChildren[parentUID] = append(children, childKey)
}
// removeFromParentUIDToChildren removes a child from the parent-to-children index
func (c *clusterCache) removeFromParentUIDToChildren(parentUID types.UID, childKey kube.ResourceKey) {
childrenSet := c.parentUIDToChildren[parentUID]
if childrenSet == nil {
return
}
children := c.parentUIDToChildren[parentUID]
for i, existing := range children {
if existing == childKey {
// Remove by swapping with last element and truncating
children[i] = children[len(children)-1]
c.parentUIDToChildren[parentUID] = children[:len(children)-1]
// Remove child from set (O(1) operation)
delete(childrenSet, childKey)
// Clean up empty sets to avoid memory leaks
if len(childrenSet) == 0 {
delete(c.parentUIDToChildren, parentUID)
// Clean up empty entries
if len(c.parentUIDToChildren[parentUID]) == 0 {
delete(c.parentUIDToChildren, parentUID)
}
return
}
}
}
@@ -1020,7 +1013,7 @@ func (c *clusterCache) sync() error {
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
c.resources = make(map[kube.ResourceKey]*Resource)
c.namespacedResources = make(map[schema.GroupKind]bool)
c.parentUIDToChildren = make(map[types.UID]map[kube.ResourceKey]struct{})
c.parentUIDToChildren = make(map[types.UID][]kube.ResourceKey)
config := c.config
version, err := c.kubectl.GetServerVersion(config)
if err != nil {
@@ -1119,6 +1112,9 @@ func (c *clusterCache) sync() error {
return fmt.Errorf("failed to sync cluster %s: %w", c.config.Host, err)
}
// Rebuild orphaned children index after all resources are loaded
c.rebuildParentToChildrenIndex()
c.log.Info("Cluster successfully synced")
return nil
}
@@ -1259,8 +1255,8 @@ func (c *clusterCache) processCrossNamespaceChildren(
}
// Use parent-to-children index for O(1) lookup of direct children
childrenSet := c.parentUIDToChildren[clusterResource.Ref.UID]
for childKey := range childrenSet {
childKeys := c.parentUIDToChildren[clusterResource.Ref.UID]
for _, childKey := range childKeys {
child := c.resources[childKey]
if child == nil {
continue
@@ -1313,8 +1309,8 @@ func (c *clusterCache) iterateChildrenUsingIndex(
action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool,
) {
// Look up direct children of this parent using the index
childrenSet := c.parentUIDToChildren[parent.Ref.UID]
for childKey := range childrenSet {
childKeys := c.parentUIDToChildren[parent.Ref.UID]
for _, childKey := range childKeys {
if actionCallState[childKey] != notCalled {
continue // action() already called or in progress
}
@@ -1634,10 +1630,6 @@ func (c *clusterCache) onNodeRemoved(key kube.ResourceKey) {
for k, v := range ns {
if mightHaveInferredOwner(v) && existing.isInferredParentOf(k) {
v.setOwnerRef(existing.toOwnerRef(), false)
// Update index inline when removing inferred ref
if existing.Ref.UID != "" {
c.removeFromParentUIDToChildren(existing.Ref.UID, k)
}
}
}
}

View File

@@ -416,128 +416,6 @@ func TestStatefulSetOwnershipInferred(t *testing.T) {
}
}
// TestStatefulSetPVC_ParentToChildrenIndex verifies that inferred StatefulSet → PVC
// relationships are correctly captured in the parentUIDToChildren index during initial sync.
//
// The index is updated inline when inferred owner refs are added in setNode()
// (see the inferred parent handling section in clusterCache.setNode).
func TestStatefulSetPVC_ParentToChildrenIndex(t *testing.T) {
stsUID := types.UID("sts-uid-123")
// StatefulSet with volumeClaimTemplate named "data"
sts := &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: kube.StatefulSetKind},
ObjectMeta: metav1.ObjectMeta{UID: stsUID, Name: "web", Namespace: "default"},
Spec: appsv1.StatefulSetSpec{
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{
ObjectMeta: metav1.ObjectMeta{Name: "data"},
}},
},
}
// PVCs that match the StatefulSet's volumeClaimTemplate pattern: <template>-<sts>-<ordinal>
// These have NO explicit owner references - the relationship is INFERRED
pvc0 := &corev1.PersistentVolumeClaim{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: kube.PersistentVolumeClaimKind},
ObjectMeta: metav1.ObjectMeta{UID: "pvc-0-uid", Name: "data-web-0", Namespace: "default"},
}
pvc1 := &corev1.PersistentVolumeClaim{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: kube.PersistentVolumeClaimKind},
ObjectMeta: metav1.ObjectMeta{UID: "pvc-1-uid", Name: "data-web-1", Namespace: "default"},
}
// Create cluster with all resources
// Must add PersistentVolumeClaim to API resources since it's not in the default set
cluster := newCluster(t, sts, pvc0, pvc1).WithAPIResources([]kube.APIResourceInfo{{
GroupKind: schema.GroupKind{Group: "", Kind: kube.PersistentVolumeClaimKind},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
Meta: metav1.APIResource{Namespaced: true},
}})
err := cluster.EnsureSynced()
require.NoError(t, err)
// Verify the parentUIDToChildren index contains the inferred relationships
cluster.lock.RLock()
defer cluster.lock.RUnlock()
pvc0Key := kube.ResourceKey{Group: "", Kind: kube.PersistentVolumeClaimKind, Namespace: "default", Name: "data-web-0"}
pvc1Key := kube.ResourceKey{Group: "", Kind: kube.PersistentVolumeClaimKind, Namespace: "default", Name: "data-web-1"}
children, ok := cluster.parentUIDToChildren[stsUID]
require.True(t, ok, "StatefulSet should have entry in parentUIDToChildren index")
require.Contains(t, children, pvc0Key, "PVC data-web-0 should be in StatefulSet's children (inferred relationship)")
require.Contains(t, children, pvc1Key, "PVC data-web-1 should be in StatefulSet's children (inferred relationship)")
// Also verify the OwnerRefs were set correctly on the PVCs
pvc0Resource := cluster.resources[pvc0Key]
require.NotNil(t, pvc0Resource)
require.Len(t, pvc0Resource.OwnerRefs, 1, "PVC0 should have inferred owner ref")
require.Equal(t, stsUID, pvc0Resource.OwnerRefs[0].UID, "PVC0 owner should be the StatefulSet")
pvc1Resource := cluster.resources[pvc1Key]
require.NotNil(t, pvc1Resource)
require.Len(t, pvc1Resource.OwnerRefs, 1, "PVC1 should have inferred owner ref")
require.Equal(t, stsUID, pvc1Resource.OwnerRefs[0].UID, "PVC1 owner should be the StatefulSet")
}
// TestStatefulSetPVC_WatchEvent_IndexUpdated verifies that when a PVC is added
// via watch event (after initial sync), both the inferred owner reference AND
// the parentUIDToChildren index are updated correctly.
//
// This tests the inline index update logic in setNode() which updates the index
// immediately when inferred owner refs are added.
func TestStatefulSetPVC_WatchEvent_IndexUpdated(t *testing.T) {
stsUID := types.UID("sts-uid-456")
// StatefulSet with volumeClaimTemplate
sts := &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: kube.StatefulSetKind},
ObjectMeta: metav1.ObjectMeta{UID: stsUID, Name: "db", Namespace: "default"},
Spec: appsv1.StatefulSetSpec{
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{
ObjectMeta: metav1.ObjectMeta{Name: "storage"},
}},
},
}
// Create cluster with ONLY the StatefulSet - PVC will be added via watch event
cluster := newCluster(t, sts).WithAPIResources([]kube.APIResourceInfo{{
GroupKind: schema.GroupKind{Group: "", Kind: kube.PersistentVolumeClaimKind},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
Meta: metav1.APIResource{Namespaced: true},
}})
err := cluster.EnsureSynced()
require.NoError(t, err)
// PVC that matches the StatefulSet's volumeClaimTemplate pattern
// Added via watch event AFTER initial sync
pvc := &corev1.PersistentVolumeClaim{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: kube.PersistentVolumeClaimKind},
ObjectMeta: metav1.ObjectMeta{UID: "pvc-watch-uid", Name: "storage-db-0", Namespace: "default"},
}
// Simulate watch event adding the PVC
cluster.lock.Lock()
cluster.setNode(cluster.newResource(mustToUnstructured(pvc)))
cluster.lock.Unlock()
cluster.lock.RLock()
defer cluster.lock.RUnlock()
pvcKey := kube.ResourceKey{Group: "", Kind: kube.PersistentVolumeClaimKind, Namespace: "default", Name: "storage-db-0"}
// Verify the OwnerRef IS correctly set
pvcResource := cluster.resources[pvcKey]
require.NotNil(t, pvcResource, "PVC should exist in cache")
require.Len(t, pvcResource.OwnerRefs, 1, "PVC should have inferred owner ref from StatefulSet")
require.Equal(t, stsUID, pvcResource.OwnerRefs[0].UID, "Owner should be the StatefulSet")
// Verify the index IS updated for inferred refs via watch events
children, indexUpdated := cluster.parentUIDToChildren[stsUID]
require.True(t, indexUpdated, "Index should be updated when inferred refs are added via watch events")
require.Contains(t, children, pvcKey, "PVC should be in StatefulSet's children (inferred relationship)")
}
func TestEnsureSyncedSingleNamespace(t *testing.T) {
obj1 := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
@@ -2420,226 +2298,3 @@ func TestIterateHierarchyV2_CircularOwnerChain_NoStackOverflow(t *testing.T) {
assert.Equal(t, 1, visitCount["resource-a"], "resource-a should be visited exactly once")
assert.Equal(t, 1, visitCount["resource-b"], "resource-b should be visited exactly once")
}
// BenchmarkSync_ParentToChildrenIndex measures the overhead of parent-to-children index
// operations during sync. This benchmark was created to investigate performance regression
// reported in https://github.com/argoproj/argo-cd/issues/26863
//
// The index is now maintained with O(1) operations (set-based) and updated inline
// in setNode() for both explicit and inferred owner refs. No rebuild is needed.
//
// This benchmark measures sync performance with resources that have owner references
// to quantify the index-building overhead at different scales.
func BenchmarkSync_ParentToChildrenIndex(b *testing.B) {
testCases := []struct {
name string
totalResources int
pctWithOwnerRefs int // Percentage of resources with owner references
}{
// Baseline: no owner refs (index operations are no-ops)
{"1000res_0pctOwnerRefs", 1000, 0},
{"5000res_0pctOwnerRefs", 5000, 0},
{"10000res_0pctOwnerRefs", 10000, 0},
// Typical case: ~80% of resources have owner refs (pods owned by RS, RS owned by Deployment)
{"1000res_80pctOwnerRefs", 1000, 80},
{"5000res_80pctOwnerRefs", 5000, 80},
{"10000res_80pctOwnerRefs", 10000, 80},
// Heavy case: all resources have owner refs
{"1000res_100pctOwnerRefs", 1000, 100},
{"5000res_100pctOwnerRefs", 5000, 100},
{"10000res_100pctOwnerRefs", 10000, 100},
// Stress test: larger scale
{"20000res_80pctOwnerRefs", 20000, 80},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
resources := make([]runtime.Object, 0, tc.totalResources)
// Create parent resources (deployments) - these won't have owner refs
numParents := tc.totalResources / 10 // 10% are parents
if numParents < 1 {
numParents = 1
}
parentUIDs := make([]types.UID, numParents)
for i := 0; i < numParents; i++ {
uid := types.UID(fmt.Sprintf("deploy-uid-%d", i))
parentUIDs[i] = uid
resources = append(resources, &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("deploy-%d", i),
Namespace: "default",
UID: uid,
},
})
}
// Create child resources (pods) - some with owner refs
numChildren := tc.totalResources - numParents
numWithOwnerRefs := (numChildren * tc.pctWithOwnerRefs) / 100
for i := 0; i < numChildren; i++ {
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("pod-%d", i),
Namespace: "default",
UID: types.UID(fmt.Sprintf("pod-uid-%d", i)),
},
}
// Add owner refs to the first numWithOwnerRefs pods
if i < numWithOwnerRefs {
parentIdx := i % numParents
pod.OwnerReferences = []metav1.OwnerReference{{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: fmt.Sprintf("deploy-%d", parentIdx),
UID: parentUIDs[parentIdx],
}}
}
resources = append(resources, pod)
}
cluster := newCluster(b, resources...)
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// sync() reinitializes resources, parentUIDToChildren, etc. at the start,
// so no manual reset is needed here.
err := cluster.sync()
if err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkUpdateParentUIDToChildren measures the cost of incremental index updates
// during setNode. This is called for EVERY resource during sync. The index uses
// set-based storage so add/remove operations are O(1) regardless of children count.
func BenchmarkUpdateParentUIDToChildren(b *testing.B) {
testCases := []struct {
name string
childrenPerParent int
}{
{"10children", 10},
{"50children", 50},
{"100children", 100},
{"500children", 500},
{"1000children", 1000},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
cluster := newCluster(b)
err := cluster.EnsureSynced()
if err != nil {
b.Fatal(err)
}
parentUID := types.UID("parent-uid")
// Pre-populate with existing children
childrenSet := make(map[kube.ResourceKey]struct{})
for i := 0; i < tc.childrenPerParent; i++ {
childKey := kube.ResourceKey{
Group: "",
Kind: "Pod",
Namespace: "default",
Name: fmt.Sprintf("existing-child-%d", i),
}
childrenSet[childKey] = struct{}{}
}
cluster.parentUIDToChildren[parentUID] = childrenSet
// Create a new child key to add
newChildKey := kube.ResourceKey{
Group: "",
Kind: "Pod",
Namespace: "default",
Name: "new-child",
}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// Simulate adding a new child - O(1) set insertion
cluster.addToParentUIDToChildren(parentUID, newChildKey)
// Remove it so we can add it again in the next iteration
cluster.removeFromParentUIDToChildren(parentUID, newChildKey)
}
})
}
}
// BenchmarkIncrementalIndexBuild measures the cost of incremental index updates
// via addToParentUIDToChildren during sync. The index uses O(1) set-based operations.
//
// This benchmark was created to investigate issue #26863 and verify the fix.
func BenchmarkIncrementalIndexBuild(b *testing.B) {
testCases := []struct {
name string
numParents int
childrenPerParent int
}{
{"100parents_10children", 100, 10},
{"100parents_50children", 100, 50},
{"100parents_100children", 100, 100},
{"1000parents_10children", 1000, 10},
{"1000parents_100children", 1000, 100},
}
for _, tc := range testCases {
// Benchmark incremental approach (what happens during setNode)
b.Run(tc.name+"_incremental", func(b *testing.B) {
cluster := newCluster(b)
err := cluster.EnsureSynced()
if err != nil {
b.Fatal(err)
}
// Prepare parent UIDs and child keys
type childInfo struct {
parentUID types.UID
childKey kube.ResourceKey
}
children := make([]childInfo, 0, tc.numParents*tc.childrenPerParent)
for p := 0; p < tc.numParents; p++ {
parentUID := types.UID(fmt.Sprintf("parent-%d", p))
for c := 0; c < tc.childrenPerParent; c++ {
children = append(children, childInfo{
parentUID: parentUID,
childKey: kube.ResourceKey{
Kind: "Pod",
Namespace: "default",
Name: fmt.Sprintf("child-%d-%d", p, c),
},
})
}
}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// Clear the index
cluster.parentUIDToChildren = make(map[types.UID]map[kube.ResourceKey]struct{})
// Simulate incremental adds (O(1) set insertions)
for _, child := range children {
cluster.addToParentUIDToChildren(child.parentUID, child.childKey)
}
}
})
}
}

2
go.mod
View File

@@ -112,7 +112,7 @@ require (
k8s.io/apimachinery v0.34.0
k8s.io/client-go v0.34.0
k8s.io/code-generator v0.34.0
k8s.io/klog/v2 v2.140.0
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/kubectl v0.34.0
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect

3
go.sum
View File

@@ -1479,9 +1479,8 @@ k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJez
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-aggregator v0.34.0 h1:XE4u+HOYkj0g44sblhTtPv+QyIIK7sJxrIlia0731kE=
k8s.io/kube-aggregator v0.34.0/go.mod h1:GIUqdChXVC448Vp2Wgxf0m6fir7Xt3A2TAZcs2JNG1Y=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=

View File

@@ -62,17 +62,6 @@ func SanitizeCluster(cluster *v1alpha1.Cluster) (*unstructured.Unstructured, err
})
}
func managedByURLFromAnnotations(annotations map[string]any) (string, bool) {
managedByURL, ok := annotations[v1alpha1.AnnotationKeyManagedByURL].(string)
if !ok {
return "", false
}
if err := settings.ValidateExternalURL(managedByURL); err != nil {
return "", false
}
return managedByURL, true
}
func CreateDeepLinksObject(resourceObj *unstructured.Unstructured, app *unstructured.Unstructured, cluster *unstructured.Unstructured, project *unstructured.Unstructured) map[string]any {
deeplinkObj := map[string]any{}
if resourceObj != nil {
@@ -83,10 +72,12 @@ func CreateDeepLinksObject(resourceObj *unstructured.Unstructured, app *unstruct
deeplinkObj[AppDeepLinkShortKey] = app.Object
// Add managed-by URL if present in annotations
if metadata, ok := app.Object["metadata"].(map[string]any); ok {
if annotations, ok := metadata["annotations"].(map[string]any); ok {
if managedByURL, ok := managedByURLFromAnnotations(annotations); ok {
deeplinkObj[ManagedByURLKey] = managedByURL
if app.Object["metadata"] != nil {
if metadata, ok := app.Object["metadata"].(map[string]any); ok {
if annotations, ok := metadata["annotations"].(map[string]any); ok {
if managedByURL, ok := annotations[v1alpha1.AnnotationKeyManagedByURL].(string); ok {
deeplinkObj[ManagedByURLKey] = managedByURL
}
}
}
}

View File

@@ -237,29 +237,6 @@ func TestManagedByURLAnnotation(t *testing.T) {
assert.Equal(t, managedByURL, deeplinksObj[ManagedByURLKey])
})
t.Run("application with invalid managed-by-url annotation is omitted", func(t *testing.T) {
// Non http(s) protocols are invalid and should not be used in deep link generation.
managedByURL := "ftp://localhost:8081"
app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Annotations: map[string]string{
v1alpha1.AnnotationKeyManagedByURL: managedByURL,
},
},
}
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app)
require.NoError(t, err)
unstructuredObj := &unstructured.Unstructured{Object: obj}
deeplinksObj := CreateDeepLinksObject(nil, unstructuredObj, nil, nil)
_, exists := deeplinksObj[ManagedByURLKey]
assert.False(t, exists)
})
t.Run("application without managed-by-url annotation", func(t *testing.T) {
// Create an application without managed-by-url annotation
app := &v1alpha1.Application{

View File

@@ -12,16 +12,13 @@ import {
createdOrNodeKey,
resourceStatusToResourceNode,
getApplicationLinkURLFromNode,
getManagedByURLFromNode,
MANAGED_BY_URL_INVALID_TEXT,
MANAGED_BY_URL_INVALID_COLOR
getManagedByURLFromNode
} from '../utils';
import {AppDetailsPreferences} from '../../../shared/services';
import {Consumer} from '../../../shared/context';
import Moment from 'react-moment';
import {format} from 'date-fns';
import {HealthPriority, ResourceNode, SyncPriority, SyncStatusCode} from '../../../shared/models';
import {isValidManagedByURL} from '../../../shared/utils';
import './application-resource-list.scss';
export interface ApplicationResourceListProps {
@@ -204,20 +201,6 @@ export const ApplicationResourceList = (props: ApplicationResourceListProps) =>
? getApplicationLinkURLFromNode(node, ctx.baseHref)
: {url: ctx.baseHref + 'applications/' + res.namespace + '/' + res.name, isExternal: false};
const managedByURL = node ? getManagedByURLFromNode(node) : null;
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
className='application-details__external_link'
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => {
e.stopPropagation();
}}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<span className='application-details__external_link'>
<a

View File

@@ -6,7 +6,6 @@ import Moment from 'react-moment';
import * as moment from 'moment';
import * as models from '../../../shared/models';
import {isValidManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_COLOR} from '../../../shared/utils';
import {EmptyState} from '../../../shared/components';
import {AppContext, Consumer} from '../../../shared/context';
@@ -497,20 +496,6 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
{ctx => {
// For nested applications, use the node's data to construct the URL
const linkInfo = getApplicationLinkURLFromNode(node, ctx.baseHref);
const managedByURL = getManagedByURLFromNode(node);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
role='link'
aria-disabled={true}
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => e.stopPropagation()}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<a
href={linkInfo.url}
@@ -519,7 +504,7 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
onClick={e => {
e.stopPropagation();
}}
title={managedByURL ? `Open application\nmanaged-by-url: ${managedByURL}` : 'Open application'}>
title={getManagedByURLFromNode(node) ? `Open application\nmanaged-by-url: ${getManagedByURLFromNode(node)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</a>
);
@@ -821,20 +806,6 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
{ctx => {
// For nested applications, use the node's data to construct the URL
const linkInfo = getApplicationLinkURLFromNode(node, ctx.baseHref);
const managedByURL = getManagedByURLFromNode(node);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
role='link'
aria-disabled={true}
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => e.stopPropagation()}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<a
href={linkInfo.url}
@@ -843,7 +814,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
onClick={e => {
e.stopPropagation();
}}
title={managedByURL ? `Open application\nmanaged-by-url: ${managedByURL}` : 'Open application'}>
title={getManagedByURLFromNode(node) ? `Open application\nmanaged-by-url: ${getManagedByURLFromNode(node)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</a>
);

View File

@@ -1,4 +1,4 @@
import {DropDownMenu, NotificationType, Tooltip} from 'argo-ui';
import {DropDownMenu, Tooltip} from 'argo-ui';
import * as React from 'react';
import Moment from 'react-moment';
import {Cluster} from '../../../shared/components';
@@ -6,8 +6,7 @@ import {ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import {ApplicationURLs} from '../application-urls';
import * as AppUtils from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {isValidManagedByURL} from '../../../shared/utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL} from '../utils';
import {ApplicationsLabels} from './applications-labels';
import {ApplicationsSource} from './applications-source';
import {services} from '../../../shared/services';
@@ -28,8 +27,6 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
const healthStatus = app.status.health.status;
const linkInfo = getApplicationLinkURL(app, ctx.baseHref);
const source = getAppDefaultSource(app);
const managedByURL = getManagedByURL(app);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -43,18 +40,6 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -107,11 +92,9 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
<span>{app.metadata.name}</span>
</Tooltip>
<button
type='button'
className={managedByURLInvalid ? 'managed-by-url-invalid' : undefined}
onClick={handleExternalLinkClick}
style={{marginLeft: '0.5em', cursor: managedByURLInvalid ? 'not-allowed' : undefined}}
title={managedByURLInvalid ? MANAGED_BY_URL_INVALID_TEXT : `Link: ${linkInfo.url}\nmanaged-by-url: ${managedByURL || 'none'}`}>
style={{marginLeft: '0.5em'}}
title={`Link: ${linkInfo.url}\nmanaged-by-url: ${getManagedByURL(app) || 'none'}`}>
<i className='fa fa-external-link-alt' />
</button>
</div>

View File

@@ -1,4 +1,4 @@
import {NotificationType, Tooltip} from 'argo-ui';
import {Tooltip} from 'argo-ui';
import * as classNames from 'classnames';
import * as React from 'react';
import {Cluster} from '../../../shared/components';
@@ -6,8 +6,7 @@ import {ContextApis, AuthSettingsCtx} from '../../../shared/context';
import * as models from '../../../shared/models';
import {ApplicationURLs} from '../application-urls';
import * as AppUtils from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {isValidManagedByURL} from '../../../shared/utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL} from '../utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
@@ -31,8 +30,6 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
const targetRevision = source ? source.targetRevision || 'HEAD' : 'Unknown';
const linkInfo = getApplicationLinkURL(app, ctx.baseHref);
const healthStatus = app.status.health.status;
const managedByURL = getManagedByURL(app);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -46,18 +43,6 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -82,20 +67,9 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
<div className={app.status.summary?.externalURLs?.length > 0 ? 'columns small-2' : 'columns small-1'}>
<div className='applications-list__external-link'>
<ApplicationURLs urls={app.status.summary?.externalURLs} />
{managedByURLInvalid ? (
<button
type='button'
className='managed-by-url-invalid'
onClick={handleExternalLinkClick}
style={{cursor: 'not-allowed'}}
title={MANAGED_BY_URL_INVALID_TEXT}>
<i className='fa fa-external-link-alt' />
</button>
) : (
<button type='button' onClick={handleExternalLinkClick} title={managedByURL ? `Managed by: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
)}
<button onClick={handleExternalLinkClick} title={getManagedByURL(app) ? `Managed by: ${getManagedByURL(app)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
<button
title={favList?.includes(app.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}
className='large-text-height'

View File

@@ -39,36 +39,28 @@
flex: 1;
min-width: 0;
}
.applications-table-source__labels {
max-width: 40%;
}
.applications-table-source__labels {
max-width: 40%;
}
}
.applications-list__external-link {
button {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
color: inherit;
&:hover {
color: $argo-color-teal-5;
}
i {
font-size: 14px;
}
}
}
.applications-list__table-row button.managed-by-url-invalid {
color: #f4c030;
.applications-list__external-link {
button {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
color: inherit;
&:hover {
color: #f4c030;
color: $argo-color-teal-5;
}
i {
font-size: 14px;
}
}
}
}

View File

@@ -70,26 +70,9 @@
&:hover {
color: $argo-color-teal-5;
}
&.managed-by-url-invalid {
color: #f4c030;
&:hover {
color: #f4c030;
}
}
i {
font-size: 14px;
}
}
}
/* Table / name column external-link (not under .applications-list__external-link) */
.applications-list__table-row button.managed-by-url-invalid {
color: #f4c030;
&:hover {
color: #f4c030;
}
}

View File

@@ -1,13 +1,12 @@
import {NotificationType, Tooltip} from 'argo-ui';
import {Tooltip} from 'argo-ui';
import * as React from 'react';
import Moment from 'react-moment';
import {ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import * as AppUtils from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus} from '../utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
import {isValidManagedByURL} from '../../../shared/utils';
export interface AppSetTableRowProps {
appSet: models.ApplicationSet;
@@ -20,8 +19,6 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
const favList = pref.appList.favoritesAppList || [];
const healthStatus = getAppSetHealthStatus(appSet);
const linkInfo = getApplicationLinkURL(appSet, ctx.baseHref);
const managedByURL = getManagedByURL(appSet);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -35,18 +32,6 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -96,11 +81,9 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
<span>{appSet.metadata.name}</span>
</Tooltip>
<button
type='button'
className={managedByURLInvalid ? 'managed-by-url-invalid' : undefined}
onClick={handleExternalLinkClick}
style={{marginLeft: '0.5em', cursor: managedByURLInvalid ? 'not-allowed' : undefined}}
title={managedByURLInvalid ? MANAGED_BY_URL_INVALID_TEXT : `Link: ${linkInfo.url}\nmanaged-by-url: ${managedByURL || 'none'}`}>
style={{marginLeft: '0.5em'}}
title={`Link: ${linkInfo.url}\nmanaged-by-url: ${getManagedByURL(appSet) || 'none'}`}>
<i className='fa fa-external-link-alt' />
</button>
</div>

View File

@@ -1,13 +1,12 @@
import {NotificationType, Tooltip} from 'argo-ui';
import {Tooltip} from 'argo-ui';
import * as React from 'react';
import {ContextApis, AuthSettingsCtx} from '../../../shared/context';
import * as models from '../../../shared/models';
import * as AppUtils from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus} from '../utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
import {ResourceIcon} from '../resource-icon';
import {isValidManagedByURL} from '../../../shared/utils';
export interface AppSetTileProps {
appSet: models.ApplicationSet;
@@ -23,8 +22,6 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
const linkInfo = getApplicationLinkURL(appSet, ctx.baseHref);
const healthStatus = getAppSetHealthStatus(appSet);
const managedByURL = getManagedByURL(appSet);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -38,18 +35,6 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -73,20 +58,9 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
</div>
<div className='columns small-1'>
<div className='applications-list__external-link'>
{managedByURLInvalid ? (
<button
type='button'
className='managed-by-url-invalid'
onClick={handleExternalLinkClick}
style={{cursor: 'not-allowed'}}
title={MANAGED_BY_URL_INVALID_TEXT}>
<i className='fa fa-external-link-alt' />
</button>
) : (
<button type='button' onClick={handleExternalLinkClick} title={managedByURL ? `Managed by: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
)}
<button onClick={handleExternalLinkClick} title={getManagedByURL(appSet) ? `Managed by: ${getManagedByURL(appSet)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
<button
title={favList?.includes(appSet.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}
className='large-text-height'

View File

@@ -8,7 +8,7 @@ import * as moment from 'moment';
import {BehaviorSubject, combineLatest, concat, from, fromEvent, Observable, Observer, Subscription} from 'rxjs';
import {debounceTime, map} from 'rxjs/operators';
import {AppContext, Context, ContextApis} from '../../shared/context';
import {isValidManagedByURL} from '../../shared/utils';
import {isValidURL} from '../../shared/utils';
import {ResourceTreeNode} from './application-resource-tree/application-resource-tree';
import {CheckboxField, COLORS, ErrorNotification, Revision} from '../../shared/components';
@@ -18,14 +18,6 @@ import {ApplicationSource} from '../../shared/models';
require('./utils.scss');
export {
MANAGED_BY_URL_INVALID_COLOR,
MANAGED_BY_URL_INVALID_TEXT,
MANAGED_BY_URL_INVALID_TOOLTIP,
managedByURLInvalidLabelStyle,
managedByURLInvalidLabelStyleCompact
} from '../../shared/utils';
export interface NodeId {
kind: string;
namespace: string;
@@ -1998,7 +1990,7 @@ export function getApplicationLinkURL(app: any, baseHref: string, node?: any): {
let url, isExternal;
if (managedByURL) {
// Validate the managed-by URL using the same validation as external links
if (!isValidManagedByURL(managedByURL)) {
if (!isValidURL(managedByURL)) {
// If URL is invalid, fall back to local URL for security
console.warn(`Invalid managed-by URL for application ${app.metadata.name}: ${managedByURL}`);
url = baseHref + 'applications/' + app.metadata.namespace + '/' + app.metadata.name;
@@ -2026,7 +2018,7 @@ export function getApplicationLinkURLFromNode(node: any, baseHref: string): {url
let url, isExternal;
if (managedByURL) {
// Validate the managed-by URL using the same validation as external links
if (!isValidManagedByURL(managedByURL)) {
if (!isValidURL(managedByURL)) {
// If URL is invalid, fall back to local URL for security
console.warn(`Invalid managed-by URL for application ${node.name}: ${managedByURL}`);
url = baseHref + 'applications/' + node.namespace + '/' + node.name;

View File

@@ -1,9 +1,4 @@
/* eslint-env jest */
declare const test: any;
declare const expect: any;
declare const describe: any;
import {concatMaps} from './utils';
import {isValidManagedByURL} from './utils';
test('map concatenation', () => {
const map1 = {
@@ -17,24 +12,3 @@ test('map concatenation', () => {
const map3 = concatMaps(map1, map2);
expect(map3).toEqual(new Map(Object.entries({a: '9', b: '2', c: '8'})));
});
describe('isValidManagedByURL', () => {
test('accepts http/https URLs', () => {
expect(isValidManagedByURL('http://example.com')).toBe(true);
expect(isValidManagedByURL('https://example.com')).toBe(true);
expect(isValidManagedByURL('https://localhost:8081')).toBe(true);
});
test('rejects non-http(s) protocols', () => {
expect(isValidManagedByURL('ftp://localhost:8081')).toBe(false);
expect(isValidManagedByURL('file:///etc/passwd')).toBe(false);
expect(isValidManagedByURL('javascript:alert(1)')).toBe(false);
expect(isValidManagedByURL('data:text/html,<script>alert(1)</script>')).toBe(false);
expect(isValidManagedByURL('vbscript:msgbox(1)')).toBe(false);
});
test('rejects invalid URL strings', () => {
expect(isValidManagedByURL('not-a-url')).toBe(false);
expect(isValidManagedByURL('')).toBe(false);
});
});

View File

@@ -1,5 +1,4 @@
import {useEffect, useState} from 'react';
import type {CSSProperties} from 'react';
import React from 'react';
import {Cluster} from './models';
export function hashCode(str: string) {
@@ -39,38 +38,6 @@ export function isValidURL(url: string): boolean {
}
}
// managed-by-url is expected to mostly if not always point to another Argo CD instance URL,
// so we only consider http/https valid for click-through behavior.
export function isValidManagedByURL(url: string): boolean {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
} catch (err) {
return false;
}
}
export const MANAGED_BY_URL_INVALID_TEXT = 'managed-by-url: invalid url provided';
export const MANAGED_BY_URL_INVALID_TOOLTIP = 'managed-by-url must be a valid http(s) URL for the managing Argo CD instance. The external link is disabled until this is fixed.';
export const MANAGED_BY_URL_INVALID_COLOR = '#f4c030';
export const managedByURLInvalidLabelStyle: CSSProperties = {
color: MANAGED_BY_URL_INVALID_COLOR,
marginLeft: '0.5em',
fontSize: '13px',
fontWeight: 500,
lineHeight: 1.35,
whiteSpace: 'nowrap'
};
export const managedByURLInvalidLabelStyleCompact: CSSProperties = {
...managedByURLInvalidLabelStyle,
marginLeft: '4px',
fontSize: '12px',
fontWeight: 600
};
export const colorSchemes = {
light: '(prefers-color-scheme: light)',
dark: '(prefers-color-scheme: dark)'
@@ -114,9 +81,9 @@ export const useSystemTheme = (cb: (theme: string) => void) => {
};
export const useTheme = (props: {theme: string}) => {
const [theme, setTheme] = useState(getTheme(props.theme));
const [theme, setTheme] = React.useState(getTheme(props.theme));
useEffect(() => {
React.useEffect(() => {
let destroyListener: (() => void) | undefined;
// change theme by system, only register listener when theme is auto

View File

@@ -207,9 +207,6 @@ func SetLogLevel(logLevel string) {
// SetGLogLevel set the glog level for the k8s go-client
func SetGLogLevel(glogLevel int) {
klog.InitFlags(nil)
// Opt into fixed stderrthreshold behavior (kubernetes/klog#212).
_ = flag.Set("legacy_stderr_threshold_behavior", "false")
_ = flag.Set("stderrthreshold", "INFO")
_ = flag.Set("logtostderr", "true")
_ = flag.Set("v", strconv.Itoa(glogLevel))
}