diff --git a/agent/inbound.go b/agent/inbound.go index 2aaf72b2..e6c7109b 100644 --- a/agent/inbound.go +++ b/agent/inbound.go @@ -105,6 +105,9 @@ func (a *Agent) processIncomingApplication(ev *event.Event) error { return err } + // Applications must exist in the same namespace as the agent + incomingApp.SetNamespace(a.namespace) + var exists, sourceUIDMatch bool if a.mode == types.AgentModeManaged { @@ -196,6 +199,9 @@ func (a *Agent) processIncomingAppProject(ev *event.Event) error { return err } + // AppProjects must exist in the same namespace as the agent + incomingAppProject.SetNamespace(a.namespace) + exists, sourceUIDMatch, err := a.projectManager.CompareSourceUID(a.context, incomingAppProject) if err != nil { return fmt.Errorf("failed to validate source UID of appProject: %w", err) @@ -277,6 +283,9 @@ func (a *Agent) processIncomingRepository(ev *event.Event) error { return err } + // Repository secrets must exist in the same namespace as the agent + incomingRepo.SetNamespace(a.namespace) + var exists, sourceUIDMatch bool // Source UID annotation is not present for repos on the autonomous agent since it is the source of truth. @@ -422,7 +431,9 @@ func (a *Agent) processIncomingResourceResyncEvent(ev *event.Event) error { // createApplication creates an Application upon an event in the agent's work // queue. func (a *Agent) createApplication(incoming *v1alpha1.Application) (*v1alpha1.Application, error) { + // Applications must exist in the same namespace as the agent incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "CreateApplication", "app": incoming.QualifiedName(), @@ -472,7 +483,9 @@ func (a *Agent) createApplication(incoming *v1alpha1.Application) (*v1alpha1.App } func (a *Agent) updateApplication(incoming *v1alpha1.Application) (*v1alpha1.Application, error) { + // Applications must exist in the same namespace as the agent incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "UpdateApplication", "app": incoming.QualifiedName(), @@ -513,7 +526,9 @@ func (a *Agent) updateApplication(incoming *v1alpha1.Application) (*v1alpha1.App } func (a *Agent) deleteApplication(app *v1alpha1.Application) error { + // Applications must exist in the same namespace as the agent app.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "DeleteApplication", "app": app.QualifiedName(), @@ -556,6 +571,9 @@ func (a *Agent) deleteApplication(app *v1alpha1.Application) error { // createAppProject creates an AppProject upon an event in the agent's work // queue. func (a *Agent) createAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppProject, error) { + // AppProjects must exist in the same namespace as the agent + incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "CreateAppProject", "appProject": incoming.Name, @@ -591,6 +609,9 @@ func (a *Agent) createAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppPr } func (a *Agent) updateAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppProject, error) { + // AppProjects must exist in the same namespace as the agent + incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "UpdateAppProject", "appProject": incoming.Name, @@ -617,6 +638,9 @@ func (a *Agent) updateAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppPr } func (a *Agent) deleteAppProject(project *v1alpha1.AppProject) error { + // AppProjects must exist in the same namespace as the agent + project.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "DeleteAppProject", "appProject": project.Name, @@ -650,6 +674,9 @@ func (a *Agent) deleteAppProject(project *v1alpha1.AppProject) error { // createRepository creates a Repository upon an event in the agent's work queue. func (a *Agent) createRepository(incoming *corev1.Secret) (*corev1.Secret, error) { + // Repository secrets must exist in the same namespace as the agent + incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "CreateRepository", "repo": incoming.Name, @@ -688,7 +715,9 @@ func (a *Agent) createRepository(incoming *corev1.Secret) (*corev1.Secret, error } func (a *Agent) updateRepository(incoming *corev1.Secret) (*corev1.Secret, error) { + // Repository secrets must exist in the same namespace as the agent incoming.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "UpdateRepository", "repo": incoming.Name, @@ -713,7 +742,9 @@ func (a *Agent) updateRepository(incoming *corev1.Secret) (*corev1.Secret, error } func (a *Agent) deleteRepository(repo *corev1.Secret) error { + // Repository secrets must exist in the same namespace as the agent repo.SetNamespace(a.namespace) + logCtx := log().WithFields(logrus.Fields{ "method": "DeleteRepository", "repo": repo.Name, diff --git a/agent/inbound_test.go b/agent/inbound_test.go index 950c03fb..10d0dbce 100644 --- a/agent/inbound_test.go +++ b/agent/inbound_test.go @@ -900,6 +900,185 @@ func Test_UpdateAppProject(t *testing.T) { require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app project") }) + // Namespace handling tests + t.Run("CreateAppProject sets correct namespace", func(t *testing.T) { + agentNamespace := "agent-namespace" + a, _ := newAgent(t) + a.namespace = agentNamespace + a.mode = types.AgentModeManaged + + be := backend_mocks.NewAppProject(t) + var err error + a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace, appproject.WithAllowUpsert(true)) + require.NoError(t, err) + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "wrong-namespace", // This should be overridden + }, + Spec: v1alpha1.AppProjectSpec{ + SourceNamespaces: []string{"default"}, + }, + } + + // Mock the backend Create method and capture the project passed to it + var capturedProject *v1alpha1.AppProject + createMock := be.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + capturedProject = args[1].(*v1alpha1.AppProject) + }).Return(&v1alpha1.AppProject{}, nil) + defer createMock.Unset() + + _, err = a.createAppProject(project) + require.NoError(t, err) + + // Verify that the namespace was set correctly + require.NotNil(t, capturedProject, "Project should have been passed to backend") + assert.Equal(t, agentNamespace, capturedProject.Namespace, "Project namespace should be set to agent namespace") + assert.Equal(t, "test-project", capturedProject.Name, "Project name should remain unchanged") + }) + + t.Run("UpdateAppProject sets correct namespace", func(t *testing.T) { + agentNamespace := "agent-namespace" + a, _ := newAgent(t) + a.namespace = agentNamespace + a.mode = types.AgentModeManaged + + be := backend_mocks.NewAppProject(t) + var err error + a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace, + appproject.WithAllowUpsert(true), + appproject.WithMode(manager.ManagerModeManaged), + appproject.WithRole(manager.ManagerRoleAgent)) + require.NoError(t, err) + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "wrong-namespace", // This should be overridden + ResourceVersion: "12345", + }, + Spec: v1alpha1.AppProjectSpec{ + SourceNamespaces: []string{"default"}, + }, + } + + // Set up project as managed + a.projectManager.Manage(project.Name) + defer a.projectManager.Unmanage(project.Name) + + // Mock the backend methods + getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil) + defer getMock.Unset() + + supportsPatchMock := be.On("SupportsPatch").Return(true) + defer supportsPatchMock.Unset() + + // Capture the namespace passed to Patch method + var capturedNamespace string + patchMock := be.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + capturedNamespace = args[2].(string) + }).Return(&v1alpha1.AppProject{}, nil) + defer patchMock.Unset() + + _, err = a.updateAppProject(project) + require.NoError(t, err) + + // Verify that the namespace was set correctly in the patch call + assert.Equal(t, agentNamespace, capturedNamespace, "Patch should use agent namespace") + }) + + t.Run("DeleteAppProject sets correct namespace", func(t *testing.T) { + agentNamespace := "agent-namespace" + a, _ := newAgent(t) + a.namespace = agentNamespace + a.mode = types.AgentModeManaged + + be := backend_mocks.NewAppProject(t) + var err error + a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace, appproject.WithAllowUpsert(true)) + require.NoError(t, err) + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "wrong-namespace", // This should be overridden + }, + Spec: v1alpha1.AppProjectSpec{ + SourceNamespaces: []string{"default"}, + }, + } + + // Set up project as managed + a.projectManager.Manage(project.Name) + + // Mock the backend Delete method and capture the namespace passed to it + var capturedNamespace string + deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + capturedNamespace = args[2].(string) + }).Return(nil) + defer deleteMock.Unset() + + err = a.deleteAppProject(project) + require.NoError(t, err) + + // Verify that the namespace was set correctly + assert.Equal(t, agentNamespace, capturedNamespace, "Delete should use agent namespace") + }) + + t.Run("AppProject operations with custom agent namespace", func(t *testing.T) { + customAgentNamespace := "custom-agent-ns" + a, _ := newAgent(t) + a.namespace = customAgentNamespace + a.mode = types.AgentModeManaged + + be := backend_mocks.NewAppProject(t) + var err error + a.projectManager, err = appproject.NewAppProjectManager(be, customAgentNamespace, appproject.WithAllowUpsert(true)) + require.NoError(t, err) + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "principal-namespace", // Different from agent + }, + Spec: v1alpha1.AppProjectSpec{ + SourceNamespaces: []string{"default"}, + }, + } + + // Test Create + var capturedCreateProject *v1alpha1.AppProject + createMock := be.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + capturedCreateProject = args[1].(*v1alpha1.AppProject) + }).Return(&v1alpha1.AppProject{}, nil) + + _, err = a.createAppProject(project) + require.NoError(t, err) + createMock.Unset() + + // Verify that the namespace was overridden to agent namespace + require.NotNil(t, capturedCreateProject, "Project should have been passed to backend") + assert.Equal(t, customAgentNamespace, capturedCreateProject.Namespace, "Project namespace should be set to custom agent namespace") + assert.NotEqual(t, "principal-namespace", capturedCreateProject.Namespace, "Project namespace should not remain as principal namespace") + + // Test Delete + a.projectManager.Manage(project.Name) + + var capturedDeleteNamespace string + deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + capturedDeleteNamespace = args[2].(string) + }).Return(nil) + + err = a.deleteAppProject(project) + require.NoError(t, err) + deleteMock.Unset() + + // Verify that the namespace was overridden to agent namespace + assert.Equal(t, customAgentNamespace, capturedDeleteNamespace, "Delete should use custom agent namespace") + assert.NotEqual(t, "principal-namespace", capturedDeleteNamespace, "Delete should not use principal namespace") + }) + } func Test_ProcessIncomingRepositoryWithUIDMismatch(t *testing.T) { diff --git a/principal/event.go b/principal/event.go index 66b7db7a..60eade22 100644 --- a/principal/event.go +++ b/principal/event.go @@ -320,6 +320,7 @@ func (s *Server) processAppProjectEvent(ctx context.Context, agentName string, e // AppProject creation event will only be processed in autonomous mode case event.Create.String(): if agentMode.IsAutonomous() { + incoming.SetNamespace(s.namespace) _, err := s.projectManager.Create(ctx, incoming) if err != nil { return fmt.Errorf("could not create app-project %s: %w", incoming.Name, err) @@ -335,6 +336,8 @@ func (s *Server) processAppProjectEvent(ctx context.Context, agentName string, e return event.NewEventNotAllowedErr("event type not allowed when mode is not autonomous") } + incoming.SetNamespace(s.namespace) + _, err := s.projectManager.UpdateAppProject(ctx, incoming) if err != nil { return fmt.Errorf("could not update app-project %s: %w", incoming.Name, err) @@ -346,6 +349,8 @@ func (s *Server) processAppProjectEvent(ctx context.Context, agentName string, e return event.NewEventNotAllowedErr("event type not allowed when mode is not autonomous") } + incoming.SetNamespace(s.namespace) + deletionPropagation := backend.DeletePropagationForeground err := s.projectManager.Delete(ctx, incoming, &deletionPropagation) if err != nil { diff --git a/principal/event_test.go b/principal/event_test.go index 1dd16526..611bbd81 100644 --- a/principal/event_test.go +++ b/principal/event_test.go @@ -680,6 +680,208 @@ func Test_processAppProjectEvent(t *testing.T) { require.Equal(t, ev, *got) assert.ErrorContains(t, err, "event type not allowed") }) + + t.Run("Create AppProject sets correct namespace in autonomous mode", func(t *testing.T) { + principalNamespace := "argocd-principal" + agentName := "test-agent" + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "wrong-namespace", // This should be overridden + }, + Spec: v1alpha1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []v1alpha1.ApplicationDestination{ + { + Name: "test-cluster", + Server: "https://test.server.com", + }, + }, + }, + } + + fac := kube.NewKubernetesFakeClientWithApps(principalNamespace) + s, err := NewServer(context.Background(), fac, principalNamespace, WithGeneratedTokenSigningKey()) + require.NoError(t, err) + s.setAgentMode(agentName, types.AgentModeAutonomous) + + ev := cloudevents.NewEvent() + ev.SetDataSchema("appproject") + ev.SetType(event.Create.String()) + err = ev.SetData(cloudevents.ApplicationJSON, project) + require.NoError(t, err) + + err = s.processAppProjectEvent(context.Background(), agentName, &ev) + assert.NoError(t, err) + + // Verify the project was created with the correct namespace + prefixedName, err := agentPrefixedProjectName(project.Name, agentName) + require.NoError(t, err) + + createdProject, err := fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(principalNamespace).Get(context.Background(), prefixedName, v1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, principalNamespace, createdProject.Namespace, "Project should be created in principal namespace") + }) + + t.Run("Update AppProject sets correct namespace in autonomous mode", func(t *testing.T) { + principalNamespace := "argocd-principal" + agentName := "test-agent" + + // Create existing project first + existingProject := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-agent-test-project", + Namespace: principalNamespace, + }, + Spec: v1alpha1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []v1alpha1.ApplicationDestination{ + { + Name: "test-cluster", + Server: "https://test.server.com", + }, + }, + SourceNamespaces: []string{agentName}, + }, + } + + fac := kube.NewKubernetesFakeClientWithApps(principalNamespace, existingProject) + s, err := NewServer(context.Background(), fac, principalNamespace, WithGeneratedTokenSigningKey()) + require.NoError(t, err) + s.setAgentMode(agentName, types.AgentModeAutonomous) + + // Update project with wrong namespace + updatedProject := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", // Original name (will be prefixed) + Namespace: "wrong-namespace", // This should be overridden + }, + Spec: v1alpha1.AppProjectSpec{ + Description: "Updated description", + SourceRepos: []string{"*"}, + Destinations: []v1alpha1.ApplicationDestination{ + { + Name: "test-cluster", + Server: "https://test.server.com", + }, + }, + }, + } + + ev := cloudevents.NewEvent() + ev.SetDataSchema("appproject") + ev.SetType(event.SpecUpdate.String()) + err = ev.SetData(cloudevents.ApplicationJSON, updatedProject) + require.NoError(t, err) + + err = s.processAppProjectEvent(context.Background(), agentName, &ev) + assert.NoError(t, err) + + // Verify the project was updated with the correct namespace + prefixedName, err := agentPrefixedProjectName(updatedProject.Name, agentName) + require.NoError(t, err) + + updatedProjectResult, err := fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(principalNamespace).Get(context.Background(), prefixedName, v1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, principalNamespace, updatedProjectResult.Namespace, "Project should be updated in principal namespace") + assert.Equal(t, "Updated description", updatedProjectResult.Spec.Description, "Project should have updated description") + }) + + t.Run("Delete AppProject uses correct namespace in autonomous mode", func(t *testing.T) { + principalNamespace := "argocd-principal" + agentName := "test-agent" + + // Create existing project first + existingProject := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-agent-test-project", + Namespace: principalNamespace, + }, + Spec: v1alpha1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []v1alpha1.ApplicationDestination{ + { + Name: "test-cluster", + Server: "https://test.server.com", + }, + }, + SourceNamespaces: []string{agentName}, + }, + } + + fac := kube.NewKubernetesFakeClientWithApps(principalNamespace, existingProject) + s, err := NewServer(context.Background(), fac, principalNamespace, WithGeneratedTokenSigningKey()) + require.NoError(t, err) + s.setAgentMode(agentName, types.AgentModeAutonomous) + + // Delete project with wrong namespace + projectToDelete := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", // Original name (will be prefixed) + Namespace: "wrong-namespace", // This should be overridden + }, + } + + ev := cloudevents.NewEvent() + ev.SetDataSchema("appproject") + ev.SetType(event.Delete.String()) + err = ev.SetData(cloudevents.ApplicationJSON, projectToDelete) + require.NoError(t, err) + + err = s.processAppProjectEvent(context.Background(), agentName, &ev) + assert.NoError(t, err) + + // Verify the project was deleted from the correct namespace + prefixedName, err := agentPrefixedProjectName(projectToDelete.Name, agentName) + require.NoError(t, err) + + _, err = fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(principalNamespace).Get(context.Background(), prefixedName, v1.GetOptions{}) + assert.True(t, err != nil, "Project should be deleted") + }) + + t.Run("Principal running in different namespace preserves correct namespace", func(t *testing.T) { + principalNamespace := "custom-principal-namespace" + agentName := "test-agent" + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-project", + Namespace: "agent-namespace", // Different from principal + }, + Spec: v1alpha1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []v1alpha1.ApplicationDestination{ + { + Name: "test-cluster", + Server: "https://test.server.com", + }, + }, + }, + } + + fac := kube.NewKubernetesFakeClientWithApps(principalNamespace) + s, err := NewServer(context.Background(), fac, principalNamespace, WithGeneratedTokenSigningKey()) + require.NoError(t, err) + s.setAgentMode(agentName, types.AgentModeAutonomous) + + ev := cloudevents.NewEvent() + ev.SetDataSchema("appproject") + ev.SetType(event.Create.String()) + err = ev.SetData(cloudevents.ApplicationJSON, project) + require.NoError(t, err) + + err = s.processAppProjectEvent(context.Background(), agentName, &ev) + assert.NoError(t, err) + + // Verify the project was created with the principal's namespace, not the agent's + prefixedName, err := agentPrefixedProjectName(project.Name, agentName) + require.NoError(t, err) + + createdProject, err := fac.ApplicationsClientset.ArgoprojV1alpha1().AppProjects(principalNamespace).Get(context.Background(), prefixedName, v1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, principalNamespace, createdProject.Namespace, "Project should be created in principal namespace, not agent namespace") + }) } func Test_processResourceEventResponse(t *testing.T) {