mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 08:57:17 +00:00
Signed-off-by: nitishfy <justnitish06@gmail.com> Signed-off-by: Peter Jiang <peterjiang823@gmail.com> Co-authored-by: nitishfy <justnitish06@gmail.com>
662 lines
24 KiB
Go
662 lines
24 KiB
Go
package applicationset
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/argoproj/pkg/v2/sync"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
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/apimachinery/pkg/watch"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/tools/cache"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
appsettemplate "github.com/argoproj/argo-cd/v3/applicationset/controllers/template"
|
|
"github.com/argoproj/argo-cd/v3/applicationset/generators"
|
|
"github.com/argoproj/argo-cd/v3/applicationset/services"
|
|
appsetstatus "github.com/argoproj/argo-cd/v3/applicationset/status"
|
|
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
|
argocommon "github.com/argoproj/argo-cd/v3/common"
|
|
"github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset"
|
|
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
|
appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
|
|
applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
|
|
repoapiclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
|
"github.com/argoproj/argo-cd/v3/server/broadcast"
|
|
applog "github.com/argoproj/argo-cd/v3/util/app/log"
|
|
"github.com/argoproj/argo-cd/v3/util/argo"
|
|
"github.com/argoproj/argo-cd/v3/util/collections"
|
|
"github.com/argoproj/argo-cd/v3/util/db"
|
|
"github.com/argoproj/argo-cd/v3/util/github_app"
|
|
"github.com/argoproj/argo-cd/v3/util/rbac"
|
|
"github.com/argoproj/argo-cd/v3/util/security"
|
|
"github.com/argoproj/argo-cd/v3/util/session"
|
|
"github.com/argoproj/argo-cd/v3/util/settings"
|
|
)
|
|
|
|
type Server struct {
|
|
ns string
|
|
db db.ArgoDB
|
|
enf *rbac.Enforcer
|
|
k8sClient kubernetes.Interface
|
|
dynamicClient dynamic.Interface
|
|
client client.Client
|
|
repoClientSet repoapiclient.Clientset
|
|
appclientset appclientset.Interface
|
|
appsetInformer cache.SharedIndexInformer
|
|
appsetLister applisters.ApplicationSetLister
|
|
appSetBroadcaster broadcast.Broadcaster[v1alpha1.ApplicationSetWatchEvent]
|
|
auditLogger *argo.AuditLogger
|
|
projectLock sync.KeyLock
|
|
enabledNamespaces []string
|
|
clusterInformer *settings.ClusterInformer
|
|
GitSubmoduleEnabled bool
|
|
EnableNewGitFileGlobbing bool
|
|
ScmRootCAPath string
|
|
AllowedScmProviders []string
|
|
EnableScmProviders bool
|
|
EnableGitHubAPIMetrics bool
|
|
}
|
|
|
|
func (s *Server) Watch(q *applicationset.ApplicationSetWatchQuery, ws applicationset.ApplicationSetService_WatchServer) error {
|
|
appsetName := q.GetName()
|
|
appsetNs := q.GetAppSetNamespace()
|
|
logCtx := log.NewEntry(log.New())
|
|
if q.Name != "" {
|
|
logCtx = logCtx.WithField("applicationset", q.Name)
|
|
}
|
|
projects := map[string]bool{}
|
|
for _, project := range q.Projects {
|
|
projects[project] = true
|
|
}
|
|
claims := ws.Context().Value("claims")
|
|
selector, err := labels.Parse(q.GetSelector())
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing labels with selectors: %w", err)
|
|
}
|
|
minVersion := 0
|
|
if q.GetResourceVersion() != "" {
|
|
if minVersion, err = strconv.Atoi(q.GetResourceVersion()); err != nil {
|
|
minVersion = 0
|
|
}
|
|
}
|
|
sendIfPermitted := func(a v1alpha1.ApplicationSet, eventType watch.EventType) {
|
|
permitted := s.isApplicationsetPermitted(selector, minVersion, claims, appsetName, appsetNs, projects, a)
|
|
if !permitted {
|
|
return
|
|
}
|
|
err := ws.Send(&v1alpha1.ApplicationSetWatchEvent{
|
|
Type: eventType,
|
|
ApplicationSet: a,
|
|
})
|
|
if err != nil {
|
|
logCtx.Warnf("Unable to send stream message: %v", err)
|
|
return
|
|
}
|
|
}
|
|
events := make(chan *v1alpha1.ApplicationSetWatchEvent, argocommon.WatchAPIBufferSize)
|
|
// Subscribe before listing so that events arriving between list and subscribe are not lost
|
|
unsubscribe := s.appSetBroadcaster.Subscribe(events)
|
|
defer unsubscribe()
|
|
if q.GetName() != "" {
|
|
appsets, err := s.appsetLister.List(selector)
|
|
if err != nil {
|
|
return fmt.Errorf("error listing appsets with selector: %w", err)
|
|
}
|
|
sort.Slice(appsets, func(i, j int) bool {
|
|
return appsets[i].QualifiedName() < appsets[j].QualifiedName()
|
|
})
|
|
for i := range appsets {
|
|
sendIfPermitted(*appsets[i], watch.Added)
|
|
}
|
|
}
|
|
for {
|
|
select {
|
|
case event := <-events:
|
|
sendIfPermitted(event.ApplicationSet, event.Type)
|
|
case <-ws.Context().Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// isApplicationsetPermitted checks if an appset is permitted
|
|
func (s *Server) isApplicationsetPermitted(selector labels.Selector, minVersion int, claims any, appsetName, appsetNs string, projects map[string]bool, appset v1alpha1.ApplicationSet) bool {
|
|
if len(projects) > 0 && !projects[appset.Spec.Template.Spec.Project] {
|
|
return false
|
|
}
|
|
|
|
if appsetVersion, err := strconv.Atoi(appset.ResourceVersion); err == nil && appsetVersion < minVersion {
|
|
return false
|
|
}
|
|
// Match by name, and optionally by namespace if provided
|
|
nameMatches := appsetName == "" || appset.Name == appsetName
|
|
nsMatches := appsetNs == "" || appset.Namespace == appsetNs
|
|
matchedEvent := nameMatches && nsMatches && selector.Matches(labels.Set(appset.Labels))
|
|
if !matchedEvent {
|
|
return false
|
|
}
|
|
// Skip any applicationsets that is neither in the control plane's namespace
|
|
// nor in the list of enabled namespaces.
|
|
if !security.IsNamespaceEnabled(appset.Namespace, s.ns, s.enabledNamespaces) {
|
|
return false
|
|
}
|
|
|
|
if !s.enf.Enforce(claims, rbac.ResourceApplicationSets, rbac.ActionGet, appset.RBACName(s.ns)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// NewServer returns a new instance of the ApplicationSet service
|
|
func NewServer(
|
|
db db.ArgoDB,
|
|
kubeclientset kubernetes.Interface,
|
|
dynamicClientset dynamic.Interface,
|
|
kubeControllerClientset client.Client,
|
|
enf *rbac.Enforcer,
|
|
repoClientSet repoapiclient.Clientset,
|
|
appclientset appclientset.Interface,
|
|
appsetInformer cache.SharedIndexInformer,
|
|
appsetLister applisters.ApplicationSetLister,
|
|
appSetBroadcaster broadcast.Broadcaster[v1alpha1.ApplicationSetWatchEvent],
|
|
namespace string,
|
|
projectLock sync.KeyLock,
|
|
enabledNamespaces []string,
|
|
gitSubmoduleEnabled bool,
|
|
enableNewGitFileGlobbing bool,
|
|
scmRootCAPath string,
|
|
allowedScmProviders []string,
|
|
enableScmProviders bool,
|
|
enableGitHubAPIMetrics bool,
|
|
enableK8sEvent []string,
|
|
clusterInformer *settings.ClusterInformer,
|
|
) applicationset.ApplicationSetServiceServer {
|
|
if appSetBroadcaster == nil {
|
|
appSetBroadcaster = broadcast.NewHandler[v1alpha1.ApplicationSet, v1alpha1.ApplicationSetWatchEvent](
|
|
func(appset *v1alpha1.ApplicationSet, eventType watch.EventType) *v1alpha1.ApplicationSetWatchEvent {
|
|
return &v1alpha1.ApplicationSetWatchEvent{ApplicationSet: *appset, Type: eventType}
|
|
},
|
|
applog.GetAppSetLogFields,
|
|
)
|
|
}
|
|
// Register ApplicationSet level broadcaster to receive create/update/delete events
|
|
// and handle general applicationset event processing.
|
|
_, err := appsetInformer.AddEventHandler(appSetBroadcaster)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
s := &Server{
|
|
ns: namespace,
|
|
db: db,
|
|
enf: enf,
|
|
dynamicClient: dynamicClientset,
|
|
client: kubeControllerClientset,
|
|
k8sClient: kubeclientset,
|
|
repoClientSet: repoClientSet,
|
|
appclientset: appclientset,
|
|
appsetInformer: appsetInformer,
|
|
appsetLister: appsetLister,
|
|
appSetBroadcaster: appSetBroadcaster,
|
|
projectLock: projectLock,
|
|
auditLogger: argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent),
|
|
enabledNamespaces: enabledNamespaces,
|
|
clusterInformer: clusterInformer,
|
|
GitSubmoduleEnabled: gitSubmoduleEnabled,
|
|
EnableNewGitFileGlobbing: enableNewGitFileGlobbing,
|
|
ScmRootCAPath: scmRootCAPath,
|
|
AllowedScmProviders: allowedScmProviders,
|
|
EnableScmProviders: enableScmProviders,
|
|
EnableGitHubAPIMetrics: enableGitHubAPIMetrics,
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *Server) Get(ctx context.Context, q *applicationset.ApplicationSetGetQuery) (*v1alpha1.ApplicationSet, error) {
|
|
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
|
return s.getAppSetEnforceRBAC(ctx, rbac.ActionGet, namespace, q.Name)
|
|
}
|
|
|
|
// List returns list of ApplicationSets
|
|
func (s *Server) List(ctx context.Context, q *applicationset.ApplicationSetListQuery) (*v1alpha1.ApplicationSetList, error) {
|
|
selector, err := labels.Parse(q.GetSelector())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing the selector: %w", err)
|
|
}
|
|
|
|
var appsets []*v1alpha1.ApplicationSet
|
|
if q.AppsetNamespace == "" {
|
|
appsets, err = s.appsetLister.List(selector)
|
|
} else {
|
|
appsets, err = s.appsetLister.ApplicationSets(q.AppsetNamespace).List(selector)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing ApplicationSets with selectors: %w", err)
|
|
}
|
|
|
|
newItems := make([]v1alpha1.ApplicationSet, 0)
|
|
for _, a := range appsets {
|
|
// Skip any applicationsets that is neither in the conrol plane's namespace
|
|
// nor in the list of enabled namespaces.
|
|
if !security.IsNamespaceEnabled(a.Namespace, s.ns, s.enabledNamespaces) {
|
|
continue
|
|
}
|
|
|
|
if s.enf.Enforce(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns)) {
|
|
newItems = append(newItems, *a)
|
|
}
|
|
}
|
|
|
|
newItems = argo.FilterAppSetsByProjects(newItems, q.Projects)
|
|
|
|
// Sort found applicationsets by name
|
|
sort.Slice(newItems, func(i, j int) bool {
|
|
return newItems[i].Name < newItems[j].Name
|
|
})
|
|
|
|
appsetList := &v1alpha1.ApplicationSetList{
|
|
ListMeta: metav1.ListMeta{
|
|
ResourceVersion: s.appsetInformer.LastSyncResourceVersion(),
|
|
},
|
|
Items: newItems,
|
|
}
|
|
return appsetList, nil
|
|
}
|
|
|
|
func (s *Server) Create(ctx context.Context, q *applicationset.ApplicationSetCreateRequest) (*v1alpha1.ApplicationSet, error) {
|
|
appset := q.GetApplicationset()
|
|
|
|
if appset == nil {
|
|
return nil, errors.New("error creating ApplicationSets: ApplicationSets is nil in request")
|
|
}
|
|
|
|
projectName, err := s.validateAppSet(appset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error validating ApplicationSets: %w", err)
|
|
}
|
|
|
|
namespace := s.appsetNamespaceOrDefault(appset.Namespace)
|
|
|
|
if !s.isNamespaceEnabled(namespace) {
|
|
return nil, security.NamespaceNotPermittedError(namespace)
|
|
}
|
|
|
|
if err := s.checkCreatePermissions(ctx, appset, projectName); err != nil {
|
|
return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %w", appset.Name, err)
|
|
}
|
|
|
|
if q.GetDryRun() {
|
|
apps, err := s.generateApplicationSetApps(ctx, log.WithField("applicationset", appset.Name), *appset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to generate Applications of ApplicationSet: %w", err)
|
|
}
|
|
|
|
statusMap := appsetstatus.GetResourceStatusMap(appset)
|
|
statusMap = appsetstatus.BuildResourceStatus(statusMap, apps)
|
|
|
|
statuses := []v1alpha1.ResourceStatus{}
|
|
for _, status := range statusMap {
|
|
statuses = append(statuses, status)
|
|
}
|
|
appset.Status.Resources = statuses
|
|
return appset, nil
|
|
}
|
|
|
|
s.projectLock.RLock(projectName)
|
|
defer s.projectLock.RUnlock(projectName)
|
|
|
|
created, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Create(ctx, appset, metav1.CreateOptions{})
|
|
if err == nil {
|
|
s.logAppSetEvent(ctx, created, argo.EventReasonResourceCreated, "created ApplicationSet")
|
|
s.waitSync(created)
|
|
return created, nil
|
|
}
|
|
|
|
if !apierrors.IsAlreadyExists(err) {
|
|
return nil, fmt.Errorf("error creating ApplicationSet: %w", err)
|
|
}
|
|
// act idempotent if existing spec matches new spec
|
|
existing, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, appset.Name, metav1.GetOptions{
|
|
ResourceVersion: "",
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "unable to check existing ApplicationSet details: %v", err)
|
|
}
|
|
|
|
equalSpecs := reflect.DeepEqual(existing.Spec, appset.Spec) &&
|
|
reflect.DeepEqual(existing.Labels, appset.Labels) &&
|
|
reflect.DeepEqual(existing.Annotations, appset.Annotations) &&
|
|
reflect.DeepEqual(existing.Finalizers, appset.Finalizers)
|
|
|
|
if equalSpecs {
|
|
return existing, nil
|
|
}
|
|
|
|
if !q.Upsert {
|
|
return nil, status.Errorf(codes.InvalidArgument, "existing ApplicationSet spec is different, use upsert flag to force update")
|
|
}
|
|
err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionUpdate, appset.RBACName(s.ns))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updated, err := s.updateAppSet(ctx, existing, appset, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating ApplicationSets: %w", err)
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
func (s *Server) generateApplicationSetApps(ctx context.Context, logEntry *log.Entry, appset v1alpha1.ApplicationSet) ([]v1alpha1.Application, error) {
|
|
argoCDDB := s.db
|
|
|
|
scmConfig := generators.NewSCMConfig(s.ScmRootCAPath, s.AllowedScmProviders, s.EnableScmProviders, s.EnableGitHubAPIMetrics, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)), true)
|
|
argoCDService := services.NewArgoCDService(s.db, s.GitSubmoduleEnabled, s.repoClientSet, s.EnableNewGitFileGlobbing)
|
|
appSetGenerators := generators.GetGenerators(ctx, s.client, s.k8sClient, s.ns, argoCDService, s.dynamicClient, scmConfig, s.clusterInformer)
|
|
|
|
apps, _, err := appsettemplate.GenerateApplications(logEntry, appset, appSetGenerators, &appsetutils.Render{}, s.client)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generating applications: %w", err)
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
func (s *Server) updateAppSet(ctx context.Context, appset *v1alpha1.ApplicationSet, newAppset *v1alpha1.ApplicationSet, merge bool) (*v1alpha1.ApplicationSet, error) {
|
|
if appset != nil && appset.Spec.Template.Spec.Project != newAppset.Spec.Template.Spec.Project {
|
|
// When changing projects, caller must have applicationset create and update privileges in new project
|
|
// NOTE: the update check was already verified in the caller to this function
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionCreate, newAppset.RBACName(s.ns)); err != nil {
|
|
return nil, err
|
|
}
|
|
// They also need 'update' privileges in the old project
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionUpdate, appset.RBACName(s.ns)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for range 10 {
|
|
appset.Spec = newAppset.Spec
|
|
if merge {
|
|
appset.Labels = collections.Merge(appset.Labels, newAppset.Labels)
|
|
appset.Annotations = collections.Merge(appset.Annotations, newAppset.Annotations)
|
|
} else {
|
|
appset.Labels = newAppset.Labels
|
|
appset.Annotations = newAppset.Annotations
|
|
}
|
|
appset.Finalizers = newAppset.Finalizers
|
|
res, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(appset.Namespace).Update(ctx, appset, metav1.UpdateOptions{})
|
|
if err == nil {
|
|
s.logAppSetEvent(ctx, appset, argo.EventReasonResourceUpdated, "updated ApplicationSets spec")
|
|
s.waitSync(res)
|
|
return res, nil
|
|
}
|
|
if !apierrors.IsConflict(err) {
|
|
return nil, err
|
|
}
|
|
|
|
appset, err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(appset.Namespace).Get(ctx, appset.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting ApplicationSets: %w", err)
|
|
}
|
|
}
|
|
return nil, status.Errorf(codes.Internal, "Failed to update ApplicationSets. Too many conflicts")
|
|
}
|
|
|
|
func (s *Server) Delete(ctx context.Context, q *applicationset.ApplicationSetDeleteRequest) (*applicationset.ApplicationSetResponse, error) {
|
|
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
|
|
|
appset, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting ApplicationSets: %w", err)
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionDelete, appset.RBACName(s.ns)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.projectLock.RLock(appset.Spec.Template.Spec.Project)
|
|
defer s.projectLock.RUnlock(appset.Spec.Template.Spec.Project)
|
|
|
|
err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Delete(ctx, q.Name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error deleting ApplicationSets: %w", err)
|
|
}
|
|
s.logAppSetEvent(ctx, appset, argo.EventReasonResourceDeleted, "deleted ApplicationSets")
|
|
return &applicationset.ApplicationSetResponse{}, nil
|
|
}
|
|
|
|
func (s *Server) ResourceTree(ctx context.Context, q *applicationset.ApplicationSetTreeQuery) (*v1alpha1.ApplicationSetTree, error) {
|
|
namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
|
|
|
|
appset, err := s.getAppSetEnforceRBAC(ctx, rbac.ActionGet, namespace, q.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.buildApplicationSetTree(appset)
|
|
}
|
|
|
|
func (s *Server) Generate(ctx context.Context, q *applicationset.ApplicationSetGenerateRequest) (*applicationset.ApplicationSetGenerateResponse, error) {
|
|
appset := q.GetApplicationSet()
|
|
|
|
if appset == nil {
|
|
return nil, errors.New("error creating ApplicationSets: ApplicationSets is nil in request")
|
|
}
|
|
|
|
// The RBAC check needs to be performed against the appset namespace
|
|
// However, when trying to generate params, the server namespace needs
|
|
// to be passed.
|
|
namespace := s.appsetNamespaceOrDefault(appset.Namespace)
|
|
if !s.isNamespaceEnabled(namespace) {
|
|
return nil, security.NamespaceNotPermittedError(namespace)
|
|
}
|
|
|
|
projectName, err := s.validateAppSet(appset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error validating ApplicationSets: %w", err)
|
|
}
|
|
if err := s.checkCreatePermissions(ctx, appset, projectName); err != nil {
|
|
return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %w", appset.Name, err)
|
|
}
|
|
|
|
logs := bytes.NewBuffer(nil)
|
|
logger := log.New()
|
|
logger.SetOutput(logs)
|
|
|
|
// The server namespace will be used in the function
|
|
// since this is the exact namespace that is being used
|
|
// to generate parameters (especially for git generator).
|
|
//
|
|
// In case of Git generator, if the namespace is set to
|
|
// appset namespace, we'll look for a project in the appset
|
|
// namespace that would lead to error when generating params
|
|
// for an appset in any namespace feature.
|
|
// See https://github.com/argoproj/argo-cd/issues/22942
|
|
apps, err := s.generateApplicationSetApps(ctx, logger.WithField("applicationset", appset.Name), *appset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to generate Applications of ApplicationSet: %w\n%s", err, logs.String())
|
|
}
|
|
res := &applicationset.ApplicationSetGenerateResponse{}
|
|
for i := range apps {
|
|
res.Applications = append(res.Applications, &apps[i])
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Server) buildApplicationSetTree(a *v1alpha1.ApplicationSet) (*v1alpha1.ApplicationSetTree, error) {
|
|
var tree v1alpha1.ApplicationSetTree
|
|
|
|
gvk := v1alpha1.ApplicationSetSchemaGroupVersionKind
|
|
parentRefs := []v1alpha1.ResourceRef{
|
|
{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind, Name: a.Name, Namespace: a.Namespace, UID: string(a.UID)},
|
|
}
|
|
|
|
apps := a.Status.Resources
|
|
for _, app := range apps {
|
|
tree.Nodes = append(tree.Nodes, v1alpha1.ResourceNode{
|
|
Health: app.Health,
|
|
ResourceRef: v1alpha1.ResourceRef{
|
|
Name: app.Name,
|
|
Group: app.Group,
|
|
Version: app.Version,
|
|
Kind: app.Kind,
|
|
Namespace: a.Namespace,
|
|
},
|
|
ParentRefs: parentRefs,
|
|
})
|
|
}
|
|
tree.Normalize()
|
|
|
|
return &tree, nil
|
|
}
|
|
|
|
func (s *Server) validateAppSet(appset *v1alpha1.ApplicationSet) (string, error) {
|
|
if appset == nil {
|
|
return "", errors.New("ApplicationSet cannot be validated for nil value")
|
|
}
|
|
|
|
projectName := appset.Spec.Template.Spec.Project
|
|
|
|
if strings.Contains(projectName, "{{") {
|
|
return "", errors.New("the Argo CD API does not currently support creating ApplicationSets with templated `project` fields")
|
|
}
|
|
|
|
if err := appsetutils.CheckInvalidGenerators(appset); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return projectName, nil
|
|
}
|
|
|
|
func (s *Server) checkCreatePermissions(ctx context.Context, appset *v1alpha1.ApplicationSet, projectName string) error {
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionCreate, appset.RBACName(s.ns)); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, projectName, metav1.GetOptions{})
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return status.Errorf(codes.InvalidArgument, "ApplicationSet references project %s which does not exist", projectName)
|
|
}
|
|
return fmt.Errorf("error getting ApplicationSet's project %q: %w", projectName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var informerSyncTimeout = 2 * time.Second
|
|
|
|
// waitSync is a helper to wait until the application informer cache is synced after create/update.
|
|
// It waits until the app in the informer, has a resource version greater than the version in the
|
|
// supplied app, or after 2 seconds, whichever comes first. Returns true if synced.
|
|
// We use an informer cache for read operations (Get, List). Since the cache is only
|
|
// eventually consistent, it is possible that it doesn't reflect an application change immediately
|
|
// after a mutating API call (create/update). This function should be called after a creates &
|
|
// update to give a probable (but not guaranteed) chance of being up-to-date after the create/update.
|
|
func (s *Server) waitSync(appset *v1alpha1.ApplicationSet) {
|
|
logCtx := log.WithField("applicationset", appset.Name)
|
|
deadline := time.Now().Add(informerSyncTimeout)
|
|
minVersion, err := strconv.Atoi(appset.ResourceVersion)
|
|
if err != nil {
|
|
logCtx.Warnf("waitSync failed: could not parse resource version %s", appset.ResourceVersion)
|
|
time.Sleep(50 * time.Millisecond) // sleep anyway
|
|
return
|
|
}
|
|
for {
|
|
if currAppset, err := s.appsetLister.ApplicationSets(appset.Namespace).Get(appset.Name); err == nil {
|
|
currVersion, err := strconv.Atoi(currAppset.ResourceVersion)
|
|
if err == nil && currVersion >= minVersion {
|
|
return
|
|
}
|
|
}
|
|
if time.Now().After(deadline) {
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
logCtx.Warnf("waitSync failed: timed out")
|
|
}
|
|
|
|
func (s *Server) logAppSetEvent(ctx context.Context, a *v1alpha1.ApplicationSet, reason string, action string) {
|
|
eventInfo := argo.EventInfo{Type: corev1.EventTypeNormal, Reason: reason}
|
|
user := session.Username(ctx)
|
|
if user == "" {
|
|
user = "Unknown user"
|
|
}
|
|
message := fmt.Sprintf("%s %s", user, action)
|
|
s.auditLogger.LogAppSetEvent(a, eventInfo, message, user)
|
|
}
|
|
|
|
func (s *Server) appsetNamespaceOrDefault(appNs string) string {
|
|
if appNs == "" {
|
|
return s.ns
|
|
}
|
|
return appNs
|
|
}
|
|
|
|
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
|
|
}
|