Skip to content

Commit c5d8972

Browse files
authored
fix: Better transformation of AppProjects from autonomous agents (#500)
Signed-off-by: jannfis <[email protected]>
1 parent 3b299ee commit c5d8972

File tree

9 files changed

+161
-15
lines changed

9 files changed

+161
-15
lines changed

docs/user-guide/appprojects.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ spec:
4343
- agent-*
4444
destinations:
4545
- name: agent-*
46-
namespace: "*"
46+
namespace: "guestbook"
4747
server: "*"
4848
sourceRepos:
4949
- "*"
@@ -55,13 +55,13 @@ When this AppProject is created on the principal, it will be automatically distr
5555

5656
When an AppProject is sent to an agent, it undergoes transformation to make it agent-specific:
5757

58-
1. **Destinations**: Only destinations matching the agent are kept, and they're transformed to point to the local cluster:
58+
1. **Destinations**: Only destinations matching the agent are kept (using glob pattern matching), and they're transformed to point to the local cluster:
5959

6060
```yaml
6161
destinations:
6262
- name: "in-cluster"
6363
server: "https://kubernetes.default.svc"
64-
namespace: "*"
64+
namespace: "guestbook" # Preserves original namespace restrictions
6565
```
6666

6767
2. **Source Namespaces**: Limited to only the agent's namespace:
@@ -99,24 +99,90 @@ spec:
9999
- name: "in-cluster"
100100
namespace: "*"
101101
server: "https://kubernetes.default.svc"
102+
# Can also have multiple destinations - all will be transformed
103+
- name: "another-destination"
104+
namespace: "specific-ns"
105+
server: "https://kubernetes.default.svc"
102106
sourceNamespaces:
103107
- argocd
104108
sourceRepos:
105109
- "*"
106110
```
107111

112+
When this AppProject is created on an autonomous agent named `agent-production`, it will be transformed and appear on the principal as:
113+
114+
```yaml
115+
apiVersion: argoproj.io/v1alpha1
116+
kind: AppProject
117+
metadata:
118+
name: agent-production-my-project # Prefixed with agent name
119+
namespace: argocd # Placed in Argo CD namespace on principal
120+
spec:
121+
destinations:
122+
- name: agent-production # All destinations point to agent
123+
namespace: "*"
124+
server: "*"
125+
- name: agent-production
126+
namespace: "specific-ns" # Original namespace restrictions preserved
127+
server: "*"
128+
sourceNamespaces:
129+
- agent-production # Agent's namespace on principal
130+
sourceRepos:
131+
- "*"
132+
```
133+
108134
### Principal-Side Transformation
109135

110136
When an AppProject is received from an autonomous agent, the principal applies transformations:
111137

138+
!!! note
139+
AppProjects from autonomous agents on the control plane are not used for reconciliation (there is no app controller running on the principal for these projects). Instead, they allow the Argo CD API (and UI, CLI) to determine which operations are valid to be performed on Applications that reference these AppProjects.
140+
141+
!!! warning "Important"
142+
AppProjects that are synced from autonomous agents should not be used by other Applications outside of that agent, as they may change unpredictably when the autonomous agent modifies its local AppProject configuration.
143+
112144
1. **Name Prefixing**: The project name is prefixed with the agent name to avoid conflicts:
113145

114146
```
115147
Original: my-project
116148
On Principal: agent-production-my-project
117149
```
118150

119-
2. **Namespace Mapping**: The project is placed in the agent's corresponding namespace on the principal
151+
2. **Source Namespaces**: Transformed to allow Applications from the agent's namespace on the principal:
152+
153+
```yaml
154+
sourceNamespaces:
155+
- agent-production # The agent's namespace on the principal
156+
```
157+
158+
3. **Destinations**: All destinations are transformed to point to the agent cluster:
159+
160+
```yaml
161+
destinations:
162+
- name: agent-production # The agent name
163+
server: "*"
164+
namespace: "*" # Preserves original namespace restrictions
165+
```
166+
167+
4. **Namespace Mapping**: The project is placed in the Argo CD namespace on the principal (same as where other AppProjects reside)
168+
169+
## Key Transformation Differences
170+
171+
The transformation logic differs significantly between managed and autonomous agents:
172+
173+
### Managed Agents (Principal → Agent)
174+
- **Direction**: AppProject flows from principal to agent
175+
- **Selection**: Uses glob pattern matching on `sourceNamespaces` and `destinations` to determine which agents receive the project
176+
- **Destinations**: Filtered to only include destinations matching the agent, then transformed to `in-cluster`
177+
- **Source Namespaces**: Replaced with the single agent namespace
178+
- **Name**: Remains unchanged
179+
180+
### Autonomous Agents (Agent → Principal)
181+
- **Direction**: AppProject flows from agent to principal
182+
- **Selection**: All AppProjects created on autonomous agents are synchronized
183+
- **Destinations**: All destinations are transformed to point to the agent cluster (name = agent name, server = "*")
184+
- **Source Namespaces**: Replaced with the agent's namespace on the principal
185+
- **Name**: Prefixed with agent name to avoid conflicts
120186

121187
### Lifecycle Management
122188

hack/dev-env/apps/autonomous-guestbook.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ metadata:
44
name: guestbook
55
namespace: argocd
66
spec:
7-
project: default
7+
project: test-project
88
source:
99
repoURL: https://github.com/argoproj/argocd-example-apps
1010
targetRevision: HEAD
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: argoproj.io/v1alpha1
2+
kind: AppProject
3+
metadata:
4+
name: test-project
5+
namespace: argocd
6+
spec:
7+
clusterResourceWhitelist:
8+
- group: ''
9+
kind: Namespace
10+
destinations:
11+
- namespace: 'guestbook'
12+
name: 'in-cluster'
13+
server: 'https://kubernetes.default.svc'
14+
sourceRepos:
15+
- '*'
16+

principal/event.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/argoproj-labs/argocd-agent/internal/event"
2525
"github.com/argoproj-labs/argocd-agent/internal/kube"
2626
"github.com/argoproj-labs/argocd-agent/internal/manager"
27-
"github.com/argoproj-labs/argocd-agent/internal/manager/appproject"
2827
"github.com/argoproj-labs/argocd-agent/internal/metrics"
2928
"github.com/argoproj-labs/argocd-agent/internal/namedlock"
3029
"github.com/argoproj-labs/argocd-agent/internal/resync"
@@ -150,11 +149,9 @@ func (s *Server) processApplicationEvent(ctx context.Context, agentName string,
150149
}
151150

152151
// AppProjects from the autonomous agents are prefixed with the agent name
153-
if incoming.Spec.Project != appproject.DefaultAppProjectName {
154-
incoming.Spec.Project, err = agentPrefixedProjectName(incoming.Spec.Project, agentName)
155-
if err != nil {
156-
return fmt.Errorf("could not prefix project name: %w", err)
157-
}
152+
incoming.Spec.Project, err = agentPrefixedProjectName(incoming.Spec.Project, agentName)
153+
if err != nil {
154+
return fmt.Errorf("could not prefix project name: %w", err)
158155
}
159156

160157
// Set the destination name to the cluster mapping for the agent
@@ -293,11 +290,18 @@ func (s *Server) processAppProjectEvent(ctx context.Context, agentName string, e
293290

294291
// AppProjects coming from different autonomous agents could have the same name,
295292
// so we prefix the project name with the agent name
296-
if agentMode.IsAutonomous() && incoming.Name != appproject.DefaultAppProjectName {
293+
if agentMode.IsAutonomous() {
297294
incoming.Name, err = agentPrefixedProjectName(incoming.Name, agentName)
298295
if err != nil {
299296
return fmt.Errorf("could not prefix project name: %w", err)
300297
}
298+
// Set the source namespaces to allow the agent's namespace on the principal
299+
incoming.Spec.SourceNamespaces = []string{agentName}
300+
// Set all destinations to point to the agent cluster
301+
for i := range incoming.Spec.Destinations {
302+
incoming.Spec.Destinations[i].Name = agentName
303+
incoming.Spec.Destinations[i].Server = "*"
304+
}
301305
}
302306

303307
switch ev.Type() {

principal/event_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func Test_UpdateEvents(t *testing.T) {
312312
assert.Equal(t, "foo", napp.Spec.Destination.Name)
313313
assert.Equal(t, "", napp.Spec.Destination.Server)
314314
assert.Nil(t, napp.Operation)
315-
assert.Equal(t, "default", napp.Spec.Project)
315+
assert.Equal(t, "foo-default", napp.Spec.Project)
316316
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, napp.Status.Sync.Status)
317317
})
318318

@@ -507,6 +507,16 @@ func Test_processAppProjectEvent(t *testing.T) {
507507
},
508508
Spec: v1alpha1.AppProjectSpec{
509509
SourceRepos: []string{"foo"},
510+
Destinations: []v1alpha1.ApplicationDestination{
511+
{
512+
Name: "original-cluster",
513+
Server: "https://original.server.com",
514+
},
515+
{
516+
Name: "another-cluster",
517+
Server: "https://another.server.com",
518+
},
519+
},
510520
},
511521
}
512522

@@ -534,6 +544,16 @@ func Test_processAppProjectEvent(t *testing.T) {
534544
createdProject, err := fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects("argocd").Get(context.TODO(), projName, v1.GetOptions{})
535545
assert.NoError(t, err)
536546
assert.Equal(t, projName, createdProject.Name)
547+
548+
// Check that SourceNamespaces is set to the agent name
549+
assert.Equal(t, []string{"foo"}, createdProject.Spec.SourceNamespaces)
550+
551+
// Check that all destinations are updated to point to the agent cluster
552+
assert.Len(t, createdProject.Spec.Destinations, 2)
553+
for _, dest := range createdProject.Spec.Destinations {
554+
assert.Equal(t, "foo", dest.Name)
555+
assert.Equal(t, "*", dest.Server)
556+
}
537557
})
538558

539559
t.Run("Update appProject in autonomous mode", func(t *testing.T) {
@@ -544,6 +564,12 @@ func Test_processAppProjectEvent(t *testing.T) {
544564
},
545565
Spec: v1alpha1.AppProjectSpec{
546566
SourceRepos: []string{"foo"},
567+
Destinations: []v1alpha1.ApplicationDestination{
568+
{
569+
Name: "original-cluster",
570+
Server: "https://original.server.com",
571+
},
572+
},
547573
},
548574
}
549575

@@ -554,6 +580,7 @@ func Test_processAppProjectEvent(t *testing.T) {
554580

555581
updatedProject := project.DeepCopy()
556582
updatedProject.Spec.Description = "updated"
583+
updatedProject.Name = "test" // Use original name (will be prefixed)
557584
ev.SetData(cloudevents.ApplicationJSON, updatedProject)
558585

559586
wq := wqmock.NewTypedRateLimitingInterface[*cloudevents.Event](t)
@@ -567,13 +594,21 @@ func Test_processAppProjectEvent(t *testing.T) {
567594
_, err = s.processRecvQueue(context.Background(), "foo", wq)
568595
assert.NoError(t, err)
569596

570-
projName, err := agentPrefixedProjectName(project.Name, "foo")
597+
projName, err := agentPrefixedProjectName(updatedProject.Name, "foo")
571598
assert.Nil(t, err)
572599

573-
// Check that the AppProject was created with the prefixed name
600+
// Check that the AppProject was updated with the correct changes
574601
got, err := fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects("argocd").Get(context.TODO(), projName, v1.GetOptions{})
575602
assert.NoError(t, err)
576603
assert.Equal(t, updatedProject.Spec.Description, got.Spec.Description)
604+
605+
// Check that SourceNamespaces is set to the agent name
606+
assert.Equal(t, []string{"foo"}, got.Spec.SourceNamespaces)
607+
608+
// Check that all destinations are updated to point to the agent cluster
609+
assert.Len(t, got.Spec.Destinations, 1)
610+
assert.Equal(t, "foo", got.Spec.Destinations[0].Name)
611+
assert.Equal(t, "*", got.Spec.Destinations[0].Server)
577612
})
578613

579614
t.Run("Delete AppProject in managed mode", func(t *testing.T) {

test/e2e2/basic_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ func (suite *BasicTestSuite) Test_AgentAutonomous() {
169169
requires.NoError(err)
170170
app.Spec.Destination.Name = "agent-autonomous"
171171
app.Spec.Destination.Server = ""
172+
app.Spec.Project = "agent-autonomous-default"
172173
requires.Equal(&app.Spec, &papp.Spec)
173174

174175
// Modify the application on the autonomous-agent and ensure the change is

test/e2e2/fixture/fixture.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,33 @@ func (suite *BaseSuite) SetupSuite() {
6262
requires.Nil(err)
6363
suite.AutonomousAgentClient, err = NewKubeClient(config)
6464
requires.Nil(err)
65+
6566
}
6667

6768
func (suite *BaseSuite) SetupTest() {
6869
err := CleanUp(suite.Ctx, suite.PrincipalClient, suite.ManagedAgentClient, suite.AutonomousAgentClient)
6970
suite.Require().Nil(err)
71+
72+
// Ensure that the autonomous agent's default AppProject exists on the principal
73+
project := &argoapp.AppProject{}
74+
key := types.NamespacedName{Name: "default", Namespace: "argocd"}
75+
err = suite.AutonomousAgentClient.Get(suite.Ctx, key, project, metav1.GetOptions{})
76+
suite.Require().Nil(err)
77+
now := time.Now().Format(time.RFC3339)
78+
project.Annotations = map[string]string{"created": now}
79+
err = suite.AutonomousAgentClient.Update(suite.Ctx, project, metav1.UpdateOptions{})
80+
suite.Require().Nil(err)
81+
82+
suite.Require().Eventually(func() bool {
83+
project := &argoapp.AppProject{}
84+
key := types.NamespacedName{Name: "agent-autonomous-default", Namespace: "argocd"}
85+
err := suite.PrincipalClient.Get(suite.Ctx, key, project, metav1.GetOptions{})
86+
if err != nil {
87+
suite.T().Log(err)
88+
}
89+
return err == nil && len(project.Annotations) > 0 && project.Annotations["created"] == now
90+
}, 10*time.Second, 1*time.Second)
91+
7092
suite.T().Logf("Test begun at: %v", time.Now())
7193
}
7294

test/e2e2/resync_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ func (suite *ResyncTestSuite) createAutonomousApp() *argoapp.Application {
504504
requires.NoError(err)
505505
app.Spec.Destination.Name = "agent-autonomous"
506506
app.Spec.Destination.Server = ""
507+
app.Spec.Project = "agent-autonomous-default"
507508
requires.Equal(&app.Spec, &papp.Spec)
508509

509510
return &app

test/e2e2/sync_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ func (suite *SyncTestSuite) Test_SyncAutonomous() {
245245
requires.NoError(err)
246246
app.Spec.Destination.Name = "agent-autonomous"
247247
app.Spec.Destination.Server = ""
248+
app.Spec.Project = "agent-autonomous-default"
248249
requires.Equal(&app.Spec, &papp.Spec)
249250

250251
// Modify the application on the autonomous-agent and ensure the change is

0 commit comments

Comments
 (0)