mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
feat: add ApplicationSet listResourceEvents API (#25537)
Signed-off-by: Peter Jiang <peterjiang823@gmail.com> Co-authored-by: Alexy Mantha <alexy@mantha.dev>
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -113,21 +114,7 @@ func NewServer(
|
||||
|
||||
func (s *Server) Get(ctx context.Context, q *applicationset.ApplicationSetGetQuery) (*v1alpha1.ApplicationSet, error) {
|
||||
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
||||
|
||||
if !s.isNamespaceEnabled(namespace) {
|
||||
return nil, security.NamespaceNotPermittedError(namespace)
|
||||
}
|
||||
|
||||
a, err := s.appsetLister.ApplicationSets(namespace).Get(q.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting ApplicationSet: %w", err)
|
||||
}
|
||||
err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
return s.getAppSetEnforceRBAC(ctx, rbac.ActionGet, namespace, q.Name)
|
||||
}
|
||||
|
||||
// List returns list of ApplicationSets
|
||||
@@ -341,20 +328,12 @@ func (s *Server) Delete(ctx context.Context, q *applicationset.ApplicationSetDel
|
||||
func (s *Server) ResourceTree(ctx context.Context, q *applicationset.ApplicationSetTreeQuery) (*v1alpha1.ApplicationSetTree, error) {
|
||||
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
||||
|
||||
if !s.isNamespaceEnabled(namespace) {
|
||||
return nil, security.NamespaceNotPermittedError(namespace)
|
||||
}
|
||||
|
||||
a, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting ApplicationSet: %w", err)
|
||||
}
|
||||
err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns))
|
||||
appset, err := s.getAppSetEnforceRBAC(ctx, rbac.ActionGet, namespace, q.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.buildApplicationSetTree(a)
|
||||
return s.buildApplicationSetTree(appset)
|
||||
}
|
||||
|
||||
func (s *Server) Generate(ctx context.Context, q *applicationset.ApplicationSetGenerateRequest) (*applicationset.ApplicationSetGenerateResponse, error) {
|
||||
@@ -518,3 +497,50 @@ func (s *Server) appsetNamespaceOrDefault(appNs string) string {
|
||||
func (s *Server) isNamespaceEnabled(namespace string) bool {
|
||||
return security.IsNamespaceEnabled(namespace, s.ns, s.enabledNamespaces)
|
||||
}
|
||||
|
||||
// getAppSetEnforceRBAC gets the ApplicationSet with the given name in the given namespace and
|
||||
// verifies that the user has the specified RBAC action permission on it.
|
||||
//
|
||||
// Note: Unlike Applications, ApplicationSets are not currently scoped to Projects for RBAC purposes.
|
||||
// The RBAC name is derived from the template's project field, but there is no project-level isolation
|
||||
// or validation (e.g., verifying the AppSet belongs to the claimed project)
|
||||
func (s *Server) getAppSetEnforceRBAC(ctx context.Context, action, namespace, name string) (*v1alpha1.ApplicationSet, error) {
|
||||
if !s.isNamespaceEnabled(namespace) {
|
||||
return nil, security.NamespaceNotPermittedError(namespace)
|
||||
}
|
||||
|
||||
appset, err := s.appsetLister.ApplicationSets(namespace).Get(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting ApplicationSet: %w", err)
|
||||
}
|
||||
|
||||
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, action, appset.RBACName(s.ns)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return appset, nil
|
||||
}
|
||||
|
||||
// ListResourceEvents returns a list of event resources for an applicationset
|
||||
func (s *Server) ListResourceEvents(ctx context.Context, q *applicationset.ApplicationSetGetQuery) (*corev1.EventList, error) {
|
||||
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
||||
|
||||
appset, err := s.getAppSetEnforceRBAC(ctx, rbac.ActionGet, namespace, q.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fieldSelector := fields.SelectorFromSet(map[string]string{
|
||||
"involvedObject.name": appset.Name,
|
||||
"involvedObject.uid": string(appset.UID),
|
||||
"involvedObject.namespace": appset.Namespace,
|
||||
}).String()
|
||||
|
||||
log.Debugf("Querying for resource events with field selector: %s", fieldSelector)
|
||||
opts := metav1.ListOptions{FieldSelector: fieldSelector}
|
||||
list, err := s.k8sClient.CoreV1().Events(namespace).List(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing resource events: %w", err)
|
||||
}
|
||||
return list.DeepCopy(), nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ option go_package = "github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset
|
||||
package applicationset;
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "k8s.io/api/core/v1/generated.proto";
|
||||
import "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1/generated.proto";
|
||||
|
||||
// ApplicationSetGetQuery is a query for applicationset resources
|
||||
@@ -102,4 +103,9 @@ service ApplicationSetService {
|
||||
option (google.api.http).get = "/api/v1/applicationsets/{name}/resource-tree";
|
||||
}
|
||||
|
||||
// ListResourceEvents returns a list of event resources
|
||||
rpc ListResourceEvents(ApplicationSetGetQuery) returns (k8s.io.api.core.v1.EventList) {
|
||||
option (google.api.http).get = "/api/v1/applicationsets/{name}/events";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
k8scache "k8s.io/client-go/tools/cache"
|
||||
|
||||
@@ -52,8 +53,16 @@ func fakeCluster() *appsv1.Cluster {
|
||||
}
|
||||
}
|
||||
|
||||
// return an ApplicationServiceServer which returns fake data
|
||||
// newTestAppSetServer returns an ApplicationSetServer with fake data for testing
|
||||
func newTestAppSetServer(t *testing.T, objects ...client.Object) *Server {
|
||||
t.Helper()
|
||||
server, _ := newTestAppSetServerWithK8sClient(t, objects...)
|
||||
return server
|
||||
}
|
||||
|
||||
// newTestAppSetServerWithK8sClient returns an ApplicationSetServer and the kubernetes clientset for testing.
|
||||
// Use this variant when you need to create kubernetes resources (e.g., Events) after server creation.
|
||||
func newTestAppSetServerWithK8sClient(t *testing.T, objects ...client.Object) (*Server, kubernetes.Interface) {
|
||||
t.Helper()
|
||||
f := func(enf *rbac.Enforcer) {
|
||||
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
||||
@@ -63,7 +72,7 @@ func newTestAppSetServer(t *testing.T, objects ...client.Object) *Server {
|
||||
return newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
|
||||
}
|
||||
|
||||
// return an ApplicationServiceServer which returns fake data
|
||||
// newTestNamespacedAppSetServer returns an ApplicationSetServer with fake data for testing with namespaced scope
|
||||
func newTestNamespacedAppSetServer(t *testing.T, objects ...client.Object) *Server {
|
||||
t.Helper()
|
||||
f := func(enf *rbac.Enforcer) {
|
||||
@@ -71,10 +80,11 @@ func newTestNamespacedAppSetServer(t *testing.T, objects ...client.Object) *Serv
|
||||
enf.SetDefaultRole("role:admin")
|
||||
}
|
||||
scopedNamespaces := "argocd"
|
||||
return newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
|
||||
server, _ := newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
|
||||
return server
|
||||
}
|
||||
|
||||
func newTestAppSetServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), namespace string, objects ...client.Object) *Server {
|
||||
func newTestAppSetServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), namespace string, objects ...client.Object) (*Server, kubernetes.Interface) {
|
||||
t.Helper()
|
||||
kubeclientset := fake.NewClientset(&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -179,7 +189,7 @@ func newTestAppSetServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforce
|
||||
true,
|
||||
testEnableEventList,
|
||||
)
|
||||
return server.(*Server)
|
||||
return server.(*Server), kubeclientset
|
||||
}
|
||||
|
||||
func newTestAppSet(opts ...func(appset *appsv1.ApplicationSet)) *appsv1.ApplicationSet {
|
||||
@@ -705,6 +715,106 @@ func TestResourceTree(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestListResourceEvents(t *testing.T) {
|
||||
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
||||
appset.Name = "AppSet1"
|
||||
appset.UID = "test-uid-123"
|
||||
})
|
||||
|
||||
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
||||
appset.Name = "AppSet2"
|
||||
})
|
||||
|
||||
t.Run("ListResourceEvents in default namespace", func(t *testing.T) {
|
||||
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
||||
|
||||
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
|
||||
|
||||
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
||||
require.NoError(t, err)
|
||||
// No events exist in the fake client, so we expect an empty list
|
||||
assert.Empty(t, res.Items)
|
||||
})
|
||||
|
||||
t.Run("ListResourceEvents in named namespace", func(t *testing.T) {
|
||||
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
||||
|
||||
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
|
||||
|
||||
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.Items)
|
||||
})
|
||||
|
||||
t.Run("ListResourceEvents in not allowed namespace", func(t *testing.T) {
|
||||
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
||||
|
||||
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
|
||||
|
||||
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
||||
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
||||
})
|
||||
|
||||
t.Run("ListResourceEvents for non-existent appset", func(t *testing.T) {
|
||||
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
||||
|
||||
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "DoesNotExist"}
|
||||
|
||||
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "error getting ApplicationSet")
|
||||
})
|
||||
|
||||
t.Run("ListResourceEvents with events returns non-empty list", func(t *testing.T) {
|
||||
appSetServer, kubeclientset := newTestAppSetServerWithK8sClient(t, appSet1, appSet2)
|
||||
|
||||
// Create events after server creation using the kubeclientset
|
||||
_, err := kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "appset1-event-1",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
Name: "AppSet1",
|
||||
Namespace: testNamespace,
|
||||
UID: "test-uid-123",
|
||||
},
|
||||
Reason: "Created",
|
||||
Message: "ApplicationSet created successfully",
|
||||
Type: corev1.EventTypeNormal,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "appset1-event-2",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
Name: "AppSet1",
|
||||
Namespace: testNamespace,
|
||||
UID: "test-uid-123",
|
||||
},
|
||||
Reason: "Updated",
|
||||
Message: "ApplicationSet updated successfully",
|
||||
Type: corev1.EventTypeNormal,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
|
||||
|
||||
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Items)
|
||||
assert.Len(t, res.Items, 2)
|
||||
|
||||
// Verify the returned events have the expected content
|
||||
eventNames := []string{res.Items[0].Name, res.Items[1].Name}
|
||||
assert.Contains(t, eventNames, "appset1-event-1")
|
||||
assert.Contains(t, eventNames, "appset1-event-2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppSet_Generate_Cluster(t *testing.T) {
|
||||
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
||||
appset.Name = "AppSet1"
|
||||
|
||||
Reference in New Issue
Block a user