Skip to content

Commit b07ebdb

Browse files
authored
feat: Selectively prevent resources from being synced (#551)
Signed-off-by: jannfis <[email protected]>
1 parent b93a038 commit b07ebdb

File tree

16 files changed

+1369
-41
lines changed

16 files changed

+1369
-41
lines changed

agent/agent.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
kubeappproject "github.com/argoproj-labs/argocd-agent/internal/backend/kubernetes/appproject"
2929
kubenamespace "github.com/argoproj-labs/argocd-agent/internal/backend/kubernetes/namespace"
3030
kuberepository "github.com/argoproj-labs/argocd-agent/internal/backend/kubernetes/repository"
31+
"github.com/argoproj-labs/argocd-agent/internal/config"
3132
"github.com/argoproj-labs/argocd-agent/internal/event"
3233
"github.com/argoproj-labs/argocd-agent/internal/informer"
3334
"github.com/argoproj-labs/argocd-agent/internal/kube"
@@ -171,10 +172,10 @@ func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace stri
171172

172173
// appListFunc and watchFunc are anonymous functions for the informer
173174
appListFunc := func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
174-
return client.ApplicationsClientset.ArgoprojV1alpha1().Applications(a.namespace).List(ctx, opts)
175+
return client.ApplicationsClientset.ArgoprojV1alpha1().Applications(a.namespace).List(ctx, config.DefaultLabelSelector())
175176
}
176177
appWatchFunc := func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
177-
return client.ApplicationsClientset.ArgoprojV1alpha1().Applications(a.namespace).Watch(ctx, opts)
178+
return client.ApplicationsClientset.ArgoprojV1alpha1().Applications(a.namespace).Watch(ctx, config.DefaultLabelSelector())
178179
}
179180

180181
appInformerOptions := []informer.InformerOption[*v1alpha1.Application]{
@@ -213,11 +214,11 @@ func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace stri
213214
appManagerOpts = append(appManagerOpts, application.WithAllowUpsert(allowUpsert))
214215

215216
projListFunc := func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
216-
return client.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(a.namespace).List(ctx, opts)
217+
return client.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(a.namespace).List(ctx, config.DefaultLabelSelector())
217218
}
218219

219220
projWatchFunc := func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
220-
return client.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(a.namespace).Watch(ctx, opts)
221+
return client.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(a.namespace).Watch(ctx, config.DefaultLabelSelector())
221222
}
222223

223224
projInformerOptions := []informer.InformerOption[*v1alpha1.AppProject]{
@@ -253,10 +254,10 @@ func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace stri
253254

254255
repoInformerOptions := []informer.InformerOption[*corev1.Secret]{
255256
informer.WithListHandler[*corev1.Secret](func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
256-
return client.Clientset.CoreV1().Secrets(a.namespace).List(ctx, opts)
257+
return client.Clientset.CoreV1().Secrets(a.namespace).List(ctx, config.DefaultLabelSelector())
257258
}),
258259
informer.WithWatchHandler[*corev1.Secret](func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
259-
return client.Clientset.CoreV1().Secrets(a.namespace).Watch(ctx, opts)
260+
return client.Clientset.CoreV1().Secrets(a.namespace).Watch(ctx, config.DefaultLabelSelector())
260261
}),
261262
informer.WithAddHandler[*corev1.Secret](a.handleRepositoryCreation),
262263
informer.WithUpdateHandler[*corev1.Secret](a.handleRepositoryUpdate),

agent/agent_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,26 @@ import (
2121
"testing"
2222

2323
"github.com/sirupsen/logrus"
24+
"k8s.io/apimachinery/pkg/runtime"
2425

26+
"github.com/argoproj-labs/argocd-agent/internal/kube"
2527
"github.com/argoproj-labs/argocd-agent/pkg/client"
26-
"github.com/argoproj-labs/argocd-agent/test/fake/kube"
28+
fakekube "github.com/argoproj-labs/argocd-agent/test/fake/kube"
2729
"github.com/stretchr/testify/require"
2830
)
2931

30-
func newAgent(t *testing.T) *Agent {
32+
func newAgent(t *testing.T, apps ...runtime.Object) (*Agent, *kube.KubernetesClient) {
3133
t.Helper()
32-
kubec := kube.NewKubernetesFakeClientWithApps("argocd")
34+
kubec := fakekube.NewKubernetesFakeClientWithApps("argocd", apps...)
3335
remote, err := client.NewRemote("127.0.0.1", 8080)
3436
require.NoError(t, err)
3537
agent, err := NewAgent(context.TODO(), kubec, "argocd", WithRemote(remote))
3638
require.NoError(t, err)
37-
return agent
39+
return agent, kubec
3840
}
3941

4042
func Test_NewAgent(t *testing.T) {
41-
kubec := kube.NewKubernetesFakeClientWithApps("agent")
43+
kubec := fakekube.NewKubernetesFakeClientWithApps("agent")
4244
agent, err := NewAgent(context.TODO(), kubec, "agent", WithRemote(&client.Remote{}))
4345
require.NotNil(t, agent)
4446
require.NoError(t, err)
@@ -209,7 +211,7 @@ func Test_NewAgent(t *testing.T) {
209211
// }
210212

211213
func Test_Healthz(t *testing.T) {
212-
agent := newAgent(t)
214+
agent, _ := newAgent(t)
213215
require.NotNil(t, agent)
214216

215217
t.Run("Healthz when agent is connected", func(t *testing.T) {

agent/connection_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
)
2525

2626
func TestResyncOnStart(t *testing.T) {
27-
a := newAgent(t)
27+
a, _ := newAgent(t)
2828
a.emitter = event.NewEventSource("test")
2929
a.kubeClient.RestConfig = &rest.Config{}
3030
logCtx := log()

agent/filters.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package agent
1616

1717
import (
18+
"github.com/argoproj-labs/argocd-agent/internal/config"
1819
"github.com/argoproj-labs/argocd-agent/internal/filter"
1920
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
2021
"github.com/argoproj/argo-cd/v3/util/glob"
@@ -28,7 +29,7 @@ func (a *Agent) DefaultAppFilterChain() *filter.Chain[*v1alpha1.Application] {
2829

2930
// Admit based on namespace of the application
3031
fc.AppendAdmitFilter(func(app *v1alpha1.Application) bool {
31-
if !glob.MatchStringInList(append([]string{a.namespace}, a.options.namespaces...), app.Namespace, glob.REGEXP) {
32+
if !glob.MatchStringInList(append([]string{a.namespace}, a.allowedNamespaces...), app.Namespace, glob.REGEXP) {
3233
log().Warnf("namespace not allowed: %s", app.QualifiedName())
3334
return false
3435
}
@@ -39,6 +40,14 @@ func (a *Agent) DefaultAppFilterChain() *filter.Chain[*v1alpha1.Application] {
3940
return true
4041
})
4142

43+
// Ignore applications that have the skip sync label
44+
fc.AppendAdmitFilter(func(app *v1alpha1.Application) bool {
45+
if v, ok := app.Labels[config.SkipSyncLabel]; ok && v == "true" {
46+
return false
47+
}
48+
return true
49+
})
50+
4251
return fc
4352
}
4453

agent/filters_test.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright 2025 The argocd-agent Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package agent
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/argoproj-labs/argocd-agent/internal/config"
22+
"github.com/argoproj-labs/argocd-agent/pkg/client"
23+
fakekube "github.com/argoproj-labs/argocd-agent/test/fake/kube"
24+
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
25+
"github.com/sirupsen/logrus"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
)
30+
31+
func TestDefaultAppFilterChain_SkipSyncLabel(t *testing.T) {
32+
kubec := fakekube.NewKubernetesFakeClientWithApps("argocd")
33+
remote, err := client.NewRemote("127.0.0.1", 8080)
34+
require.NoError(t, err)
35+
agent, err := NewAgent(context.TODO(), kubec, "argocd", WithRemote(remote))
36+
require.NoError(t, err)
37+
38+
filterChain := agent.DefaultAppFilterChain()
39+
40+
tests := []struct {
41+
name string
42+
app *v1alpha1.Application
43+
expected bool
44+
}{
45+
{
46+
name: "Application without skip sync label should be admitted",
47+
app: &v1alpha1.Application{
48+
ObjectMeta: metav1.ObjectMeta{
49+
Name: "test-app",
50+
Namespace: "argocd",
51+
Labels: map[string]string{},
52+
},
53+
},
54+
expected: true,
55+
},
56+
{
57+
name: "Application with skip sync label set to false should be admitted",
58+
app: &v1alpha1.Application{
59+
ObjectMeta: metav1.ObjectMeta{
60+
Name: "test-app",
61+
Namespace: "argocd",
62+
Labels: map[string]string{
63+
config.SkipSyncLabel: "false",
64+
},
65+
},
66+
},
67+
expected: true,
68+
},
69+
{
70+
name: "Application with skip sync label set to empty string should be admitted",
71+
app: &v1alpha1.Application{
72+
ObjectMeta: metav1.ObjectMeta{
73+
Name: "test-app",
74+
Namespace: "argocd",
75+
Labels: map[string]string{
76+
config.SkipSyncLabel: "",
77+
},
78+
},
79+
},
80+
expected: true,
81+
},
82+
{
83+
name: "Application with skip sync label set to true should be rejected",
84+
app: &v1alpha1.Application{
85+
ObjectMeta: metav1.ObjectMeta{
86+
Name: "test-app",
87+
Namespace: "argocd",
88+
Labels: map[string]string{
89+
config.SkipSyncLabel: "true",
90+
},
91+
},
92+
},
93+
expected: false,
94+
},
95+
{
96+
name: "Application with skip sync label set to TRUE (case sensitive) should be admitted",
97+
app: &v1alpha1.Application{
98+
ObjectMeta: metav1.ObjectMeta{
99+
Name: "test-app",
100+
Namespace: "argocd",
101+
Labels: map[string]string{
102+
config.SkipSyncLabel: "TRUE",
103+
},
104+
},
105+
},
106+
expected: true,
107+
},
108+
{
109+
name: "Application with skip sync label and other labels should be rejected when skip sync is true",
110+
app: &v1alpha1.Application{
111+
ObjectMeta: metav1.ObjectMeta{
112+
Name: "test-app",
113+
Namespace: "argocd",
114+
Labels: map[string]string{
115+
config.SkipSyncLabel: "true",
116+
"app.kubernetes.io/name": "test-app",
117+
"app.kubernetes.io/instance": "prod",
118+
"app.kubernetes.io/managed-by": "argocd",
119+
},
120+
},
121+
},
122+
expected: false,
123+
},
124+
{
125+
name: "Application in wrong namespace with skip sync label should be rejected (namespace filter takes precedence)",
126+
app: &v1alpha1.Application{
127+
ObjectMeta: metav1.ObjectMeta{
128+
Name: "test-app",
129+
Namespace: "wrong-namespace",
130+
Labels: map[string]string{
131+
config.SkipSyncLabel: "false",
132+
},
133+
},
134+
},
135+
expected: false,
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
result := filterChain.Admit(tt.app)
142+
assert.Equal(t, tt.expected, result, "Filter result should match expected value")
143+
})
144+
}
145+
}
146+
147+
func TestDefaultAppFilterChain_NamespaceAndSkipSyncInteraction(t *testing.T) {
148+
kubec := fakekube.NewKubernetesFakeClientWithApps("argocd")
149+
remote, err := client.NewRemote("127.0.0.1", 8080)
150+
require.NoError(t, err)
151+
152+
// Create agent with multiple allowed namespaces
153+
agent, err := NewAgent(context.TODO(), kubec, "argocd",
154+
WithRemote(remote),
155+
WithAllowedNamespaces("argocd", "apps", "staging"))
156+
require.NoError(t, err)
157+
158+
filterChain := agent.DefaultAppFilterChain()
159+
160+
tests := []struct {
161+
name string
162+
app *v1alpha1.Application
163+
expected bool
164+
reason string
165+
}{
166+
{
167+
name: "App in allowed namespace without skip sync label should be admitted",
168+
app: &v1alpha1.Application{
169+
ObjectMeta: metav1.ObjectMeta{
170+
Name: "test-app",
171+
Namespace: "apps",
172+
},
173+
},
174+
expected: true,
175+
reason: "Namespace is allowed, no skip sync label",
176+
},
177+
{
178+
name: "App in allowed namespace with skip sync=true should be rejected",
179+
app: &v1alpha1.Application{
180+
ObjectMeta: metav1.ObjectMeta{
181+
Name: "test-app",
182+
Namespace: "staging",
183+
Labels: map[string]string{
184+
config.SkipSyncLabel: "true",
185+
},
186+
},
187+
},
188+
expected: false,
189+
reason: "Skip sync label takes effect even in allowed namespace",
190+
},
191+
{
192+
name: "App in disallowed namespace with skip sync=false should be rejected",
193+
app: &v1alpha1.Application{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: "test-app",
196+
Namespace: "production",
197+
Labels: map[string]string{
198+
config.SkipSyncLabel: "false",
199+
},
200+
},
201+
},
202+
expected: false,
203+
reason: "Namespace filter rejects before skip sync filter",
204+
},
205+
}
206+
207+
for _, tt := range tests {
208+
t.Run(tt.name, func(t *testing.T) {
209+
result := filterChain.Admit(tt.app)
210+
assert.Equal(t, tt.expected, result, tt.reason)
211+
})
212+
}
213+
}
214+
215+
func TestDefaultAppFilterChain_ProcessChange(t *testing.T) {
216+
kubec := fakekube.NewKubernetesFakeClientWithApps("argocd")
217+
remote, err := client.NewRemote("127.0.0.1", 8080)
218+
require.NoError(t, err)
219+
agent, err := NewAgent(context.TODO(), kubec, "argocd", WithRemote(remote))
220+
require.NoError(t, err)
221+
222+
filterChain := agent.DefaultAppFilterChain()
223+
224+
oldApp := &v1alpha1.Application{
225+
ObjectMeta: metav1.ObjectMeta{
226+
Name: "test-app",
227+
Namespace: "argocd",
228+
Labels: map[string]string{
229+
config.SkipSyncLabel: "false",
230+
},
231+
},
232+
}
233+
234+
newApp := &v1alpha1.Application{
235+
ObjectMeta: metav1.ObjectMeta{
236+
Name: "test-app",
237+
Namespace: "argocd",
238+
Labels: map[string]string{
239+
config.SkipSyncLabel: "true",
240+
},
241+
},
242+
}
243+
244+
// Test that change processing works (no change filters are currently implemented)
245+
result := filterChain.ProcessChange(oldApp, newApp)
246+
assert.True(t, result, "ProcessChange should return true when no change filters are defined")
247+
}
248+
249+
func init() {
250+
logrus.SetLevel(logrus.TraceLevel)
251+
}

0 commit comments

Comments
 (0)