Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AppProject synchronization #175

Merged
merged 23 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b6a1d8
Initial commit
ishitasequeira Sep 3, 2024
b1e3d12
configure principal to start with app-project informer
ishitasequeira Sep 4, 2024
6883758
Add kubernetes backend for appproject
ishitasequeira Sep 4, 2024
da673ed
fix principal configuration for appproject manager
ishitasequeira Sep 6, 2024
d7392ad
update callbacks for principal event listener
ishitasequeira Sep 9, 2024
af9953f
Add mocks for AppProject
ishitasequeira Sep 9, 2024
82b5182
fix partial tests
ishitasequeira Sep 9, 2024
5460623
fix lint
ishitasequeira Sep 9, 2024
54d4b38
fix tests and lint errors
ishitasequeira Sep 9, 2024
79b0397
update logic for adding agent inbound appProject events
ishitasequeira Sep 10, 2024
767fd6c
rebase
ishitasequeira Sep 10, 2024
bfd50cd
remove unwanted metrcis configuration from agent
ishitasequeira Sep 10, 2024
e529886
Add logic for sourceNamespaces glob matching with agent namespace
ishitasequeira Sep 10, 2024
42c9948
fix lint
ishitasequeira Sep 10, 2024
11d60a8
optimize logic and fix tests
ishitasequeira Sep 10, 2024
9800a72
update references of application
ishitasequeira Sep 10, 2024
3054fc1
fix agent panic
ishitasequeira Sep 17, 2024
331bc0c
Add default appproject to control plane
ishitasequeira Sep 20, 2024
73b9cbf
revert setup changes
ishitasequeira Sep 20, 2024
fa102e1
fix test
ishitasequeira Sep 20, 2024
1fcd84e
fix agent panic due to null ctx
ishitasequeira Sep 24, 2024
6c02a66
remove misleading log statements
ishitasequeira Sep 25, 2024
ea387a2
Merge branch 'main' into appproject-synchronization
ishitasequeira Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ import (
"time"

kubeapp "github.com/argoproj-labs/argocd-agent/internal/backend/kubernetes/application"
kubeappproject "github.com/argoproj-labs/argocd-agent/internal/backend/kubernetes/appproject"
"github.com/argoproj-labs/argocd-agent/internal/event"
appinformer "github.com/argoproj-labs/argocd-agent/internal/informer/application"
appprojectinformer "github.com/argoproj-labs/argocd-agent/internal/informer/appproject"
"github.com/argoproj-labs/argocd-agent/internal/manager"
"github.com/argoproj-labs/argocd-agent/internal/manager/application"
"github.com/argoproj-labs/argocd-agent/internal/manager/appproject"
"github.com/argoproj-labs/argocd-agent/internal/queue"
"github.com/argoproj-labs/argocd-agent/internal/version"
"github.com/argoproj-labs/argocd-agent/pkg/client"
"github.com/argoproj-labs/argocd-agent/pkg/types"
"github.com/sirupsen/logrus"

"k8s.io/client-go/kubernetes"

appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
)

Expand All @@ -52,10 +53,11 @@ type Agent struct {
infStopCh chan struct{}
connected atomic.Bool
// syncCh is not currently used
syncCh chan bool
remote *client.Remote
appManager *application.ApplicationManager
mode types.AgentMode
syncCh chan bool
remote *client.Remote
appManager *application.ApplicationManager
projectManager *appproject.AppProjectManager
mode types.AgentMode
// queues is a queue of create/update/delete events to send to the principal
queues *queue.SendRecvQueues
emitter *event.EventSource
Expand All @@ -79,7 +81,7 @@ type AgentOption func(*Agent) error

// NewAgent creates a new agent instance, using the given client interfaces and
// options.
func NewAgent(ctx context.Context, client kubernetes.Interface, appclient appclientset.Interface, namespace string, opts ...AgentOption) (*Agent, error) {
func NewAgent(ctx context.Context, appclient appclientset.Interface, namespace string, opts ...AgentOption) (*Agent, error) {
a := &Agent{
version: version.New("argocd-agent", "agent"),
}
Expand Down Expand Up @@ -118,7 +120,7 @@ func NewAgent(ctx context.Context, client kubernetes.Interface, appclient appcli
return nil, fmt.Errorf("unexpected agent mode: %v", a.mode)
}

informer := appinformer.NewAppInformer(ctx, appclient, a.namespace,
appInformer := appinformer.NewAppInformer(ctx, appclient, a.namespace,
appinformer.WithListAppCallback(a.listAppCallback),
appinformer.WithNewAppCallback(a.addAppCreationToQueue),
appinformer.WithUpdateAppCallback(a.addAppUpdateToQueue),
Expand All @@ -133,9 +135,20 @@ func NewAgent(ctx context.Context, client kubernetes.Interface, appclient appcli

var err error

appProjectManagerOption := []appproject.AppProjectManagerOption{
appproject.WithAllowUpsert(true),
appproject.WithRole(manager.ManagerRoleAgent),
appproject.WithMode(managerMode),
}

projectInformer, err := appprojectinformer.NewAppProjectInformer(ctx, appclient, a.namespace)
if err != nil {
return nil, err
}

// The agent only supports Kubernetes as application backend
a.appManager, err = application.NewApplicationManager(
kubeapp.NewKubernetesBackend(appclient, a.namespace, informer, true),
kubeapp.NewKubernetesBackend(appclient, a.namespace, appInformer, true),
a.namespace,
application.WithAllowUpsert(allowUpsert),
application.WithRole(manager.ManagerRoleAgent),
Expand All @@ -146,6 +159,11 @@ func NewAgent(ctx context.Context, client kubernetes.Interface, appclient appcli
return nil, err
}

a.projectManager, err = appproject.NewAppProjectManager(kubeappproject.NewKubernetesBackend(appclient, a.namespace, projectInformer, true), a.namespace, appProjectManagerOption...)
if err != nil {
return nil, err
}

a.syncCh = make(chan bool, 1)
return a, nil
}
Expand All @@ -157,7 +175,11 @@ func (a *Agent) Start(ctx context.Context) error {
a.cancelFn = cancelFn
go func() {
a.appManager.StartBackend(a.context)
log().Warnf("Informer has exited")
log().Warnf("App Informer has exited")
}()
go func() {
a.projectManager.StartBackend(a.context)
log().Warnf("Project Informer has exited")
}()
if a.remote != nil {
a.remote.SetClientMode(a.mode)
Expand All @@ -168,8 +190,12 @@ func (a *Agent) Start(ctx context.Context) error {

a.emitter = event.NewEventSource(fmt.Sprintf("agent://%s", "agent-managed"))

// Wait for the informer to be synced
// Wait for the app informer to be synced
err := a.appManager.EnsureSynced(waitForSyncedDuration)
if err != nil {
return fmt.Errorf("failed to sync applications: %w", err)
}

return err
}

Expand Down
7 changes: 2 additions & 5 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,22 @@ import (
"github.com/sirupsen/logrus"

"github.com/argoproj-labs/argocd-agent/pkg/client"
fakekube "github.com/argoproj-labs/argocd-agent/test/fake/kube"
"github.com/stretchr/testify/require"
)

func newAgent(t *testing.T) *Agent {
t.Helper()
fakec := fakekube.NewFakeClientsetWithResources()
appc := fakeappclient.NewSimpleClientset()
remote, err := client.NewRemote("127.0.0.1", 8080)
require.NoError(t, err)
agent, err := NewAgent(context.TODO(), fakec, appc, "argocd", WithRemote(remote))
agent, err := NewAgent(context.TODO(), appc, "argocd", WithRemote(remote))
require.NoError(t, err)
return agent
}

func Test_NewAgent(t *testing.T) {
fakec := fakekube.NewFakeClientsetWithResources()
appc := fakeappclient.NewSimpleClientset()
agent, err := NewAgent(context.TODO(), fakec, appc, "agent", WithRemote(&client.Remote{}))
agent, err := NewAgent(context.TODO(), appc, "agent", WithRemote(&client.Remote{}))
require.NotNil(t, agent)
require.NoError(t, err)
}
Expand Down
119 changes: 119 additions & 0 deletions agent/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (a *Agent) processIncomingEvent(ev *event.Event) error {
case event.TargetApplication:
err = a.processIncomingApplication(ev)
case event.TargetAppProject:
err = a.processIncomingAppProject(ev)
default:
err = fmt.Errorf("unknown event target: %s", ev.Target())
}
Expand Down Expand Up @@ -76,6 +77,38 @@ func (a *Agent) processIncomingApplication(ev *event.Event) error {
return err
}

func (a *Agent) processIncomingAppProject(ev *event.Event) error {
logCtx := log().WithFields(logrus.Fields{
"method": "processIncomingEvents",
})
incomingAppProject, err := ev.AppProject()
if err != nil {
return err
}

switch ev.Type() {
case event.Create:
_, err = a.createAppProject(incomingAppProject)
if err != nil {
logCtx.Errorf("Error creating appproject: %v", err)
}
case event.SpecUpdate:
_, err = a.updateAppProject(incomingAppProject)
if err != nil {
logCtx.Errorf("Error updating appproject: %v", err)
}
case event.Delete:
err = a.deleteAppProject(incomingAppProject)
if err != nil {
logCtx.Errorf("Error deleting appproject: %v", err)
}
default:
logCtx.Warnf("Received an unknown event: %s. Protocol mismatch?", ev.Type())
}

return err
}

// createApplication creates an Application upon an event in the agent's work
// queue.
func (a *Agent) createApplication(incoming *v1alpha1.Application) (*v1alpha1.Application, error) {
Expand Down Expand Up @@ -172,3 +205,89 @@ func (a *Agent) deleteApplication(app *v1alpha1.Application) error {
}
return nil
}

// createAppProject creates an AppProject upon an event in the agent's work
// queue.
func (a *Agent) createAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppProject, error) {
incoming.ObjectMeta.SetNamespace(a.namespace)
logCtx := log().WithFields(logrus.Fields{
"method": "CreateAppProject",
"app": incoming.Name,
})

// In modes other than "managed", we don't process new AppProject events
// that are incoming.
if a.mode != types.AgentModeManaged {
logCtx.Trace("Discarding this event, because agent is not in managed mode")
return nil, fmt.Errorf("cannot create appproject: agent is not in managed mode")
}

// If we receive a new AppProject event for an AppProject we already manage, it usually
// means that we're out-of-sync from the control plane.
if a.appManager.IsManaged(incoming.Name) {
logCtx.Trace("Discarding this event, because AppProject is already managed on this agent")
return nil, fmt.Errorf("appproject %s is already managed", incoming.Name)
}

logCtx.Infof("Creating a new AppProject on behalf of an incoming event")

// Get rid of some fields that we do not want to have on the AppProject
// as we start fresh.
if incoming.Annotations != nil {
delete(incoming.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
}

created, err := a.projectManager.Create(a.context, incoming)
return created, err
}

func (a *Agent) updateAppProject(incoming *v1alpha1.AppProject) (*v1alpha1.AppProject, error) {
//
incoming.ObjectMeta.SetNamespace(a.namespace)
logCtx := log().WithFields(logrus.Fields{
"method": "UpdateAppProject",
"app": incoming.Name,
"resourceVersion": incoming.ResourceVersion,
})
if a.appManager.IsChangeIgnored(incoming.Name, incoming.ResourceVersion) {
logCtx.Tracef("Discarding this event, because agent has seen this version %s already", incoming.ResourceVersion)
return nil, fmt.Errorf("the version %s has already been seen by this agent", incoming.ResourceVersion)
} else {
logCtx.Tracef("New resource version: %s", incoming.ResourceVersion)
}

logCtx.Infof("Updating appProject")

logCtx.Tracef("Calling update spec for this event")
return a.projectManager.UpdateAppProject(a.context, incoming)

}

func (a *Agent) deleteAppProject(project *v1alpha1.AppProject) error {
project.ObjectMeta.SetNamespace(a.namespace)
logCtx := log().WithFields(logrus.Fields{
"method": "DeleteAppProject",
"app": project.Name,
})

// If we receive an update appproject event for an AppProject we don't know about yet it
// means that we're out-of-sync from the control plane.
//
// TODO(jannfis): Handle this situation properly instead of throwing an error.
if !a.appManager.IsManaged(project.Name) {
return fmt.Errorf("appProject %s is not managed", project.Name)
}

logCtx.Infof("Deleting appProject")

deletionPropagation := backend.DeletePropagationBackground
err := a.projectManager.Delete(a.context, a.namespace, project, &deletionPropagation)
if err != nil {
return err
}
err = a.projectManager.Unmanage(project.Name)
if err != nil {
log().Warnf("Could not unmanage appProject %s: %v", project.Name, err)
}
return nil
}
77 changes: 77 additions & 0 deletions agent/inbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
backend_mocks "github.com/argoproj-labs/argocd-agent/internal/backend/mocks"
"github.com/argoproj-labs/argocd-agent/internal/manager"
"github.com/argoproj-labs/argocd-agent/internal/manager/application"
"github.com/argoproj-labs/argocd-agent/internal/manager/appproject"
"github.com/argoproj-labs/argocd-agent/pkg/types"
)

Expand Down Expand Up @@ -147,3 +148,79 @@ func Test_UpdateApplication(t *testing.T) {
})

}

func Test_CreateAppProject(t *testing.T) {
a := newAgent(t)
be := backend_mocks.NewAppProject(t)
var err error
a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true))
require.NoError(t, err)
require.NotNil(t, a)
project := &v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "default",
}, Spec: v1alpha1.AppProjectSpec{
SourceNamespaces: []string{"default", "argocd"},
},
}

t.Run("Discard event in unmanaged mode", func(t *testing.T) {
napp, err := a.createAppProject(project)
require.Nil(t, napp)
require.ErrorContains(t, err, "not in managed mode")
})

t.Run("Discard event because appproject is already managed", func(t *testing.T) {
defer a.appManager.Unmanage(project.Name)
a.mode = types.AgentModeManaged
a.appManager.Manage(project.Name)
napp, err := a.createAppProject(project)
require.ErrorContains(t, err, "is already managed")
require.Nil(t, napp)
})

t.Run("Create appproject", func(t *testing.T) {
defer a.appManager.Unmanage(project.Name)
a.mode = types.AgentModeManaged
createMock := be.On("Create", mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
defer createMock.Unset()
napp, err := a.createAppProject(project)
require.NoError(t, err)
require.NotNil(t, napp)
})

}

func Test_UpdateAppProject(t *testing.T) {
a := newAgent(t)
be := backend_mocks.NewAppProject(t)
var err error
a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true))
require.NoError(t, err)
require.NotNil(t, a)
project := &v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "argocd",
ResourceVersion: "12345",
},
Spec: v1alpha1.AppProjectSpec{
SourceNamespaces: []string{"default", "argocd"},
}}

t.Run("Update appproject using patch", func(t *testing.T) {
a.mode = types.AgentModeManaged
a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true), appproject.WithMode(manager.ManagerModeManaged), appproject.WithRole(manager.ManagerRoleAgent))
getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
defer getEvent.Unset()
supportsPatchEvent := be.On("SupportsPatch").Return(true)
defer supportsPatchEvent.Unset()
patchEvent := be.On("Patch", mock.Anything, "test", "argocd", mock.Anything).Return(&v1alpha1.AppProject{}, nil)
defer patchEvent.Unset()
napp, err := a.updateAppProject(project)
require.NoError(t, err)
require.NotNil(t, napp)
})

}
2 changes: 1 addition & 1 deletion cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func NewAgentRunCommand() *cobra.Command {
}
agentOpts = append(agentOpts, agent.WithRemote(remote))
agentOpts = append(agentOpts, agent.WithMode(agentMode))
ag, err := agent.NewAgent(ctx, kubeConfig.Clientset, kubeConfig.ApplicationsClientset, namespace, agentOpts...)
ag, err := agent.NewAgent(ctx, kubeConfig.ApplicationsClientset, namespace, agentOpts...)
if err != nil {
cmd.Fatal("Could not create a new agent instance: %v", err)
}
Expand Down
1 change: 1 addition & 0 deletions hack/demo-env/control-plane/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ patches:
- path: server-service.yaml
- path: repo-server-service.yaml
- path: redis-service.yaml
- path: appproject-default.yaml
2 changes: 1 addition & 1 deletion hack/demo-env/gen-creds.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ for ag in agent-managed agent-autonomous; do
password=$(pwmake 56)
htpasswd -b -B "${creds_path}/users.control-plane" "${ag}" "${password}"
echo "${ag}:${password}" > "${creds_path}/creds.${ag}"
done
done
1 change: 0 additions & 1 deletion hack/demo-env/resources/metallb-ipaddresspool.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ kind: IPAddressPool
metadata:
name: default-addresspool
namespace: metallb-system
uid: 1812d024-a3f6-4c4e-8aa3-68d2260618ed
spec:
addresses:
- 192.168.56.100-192.168.56.254
Expand Down
Loading
Loading