mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai
Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).
CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.
Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).
CGO_ENABLED=0 go build ./... verified green.
* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)
Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.
No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.
* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)
Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).
Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.
Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).
* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy
Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.
Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).
CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.
Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.
---------
Co-authored-by: Hanzo Dev <[email protected]>
666 lines
24 KiB
Go
666 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/hanzoai/deploy/applicationset/controllers/template"
|
|
"github.com/hanzoai/deploy/applicationset/generators"
|
|
"github.com/hanzoai/deploy/applicationset/services"
|
|
appsetstatus "github.com/hanzoai/deploy/applicationset/status"
|
|
appsetutils "github.com/hanzoai/deploy/applicationset/utils"
|
|
argocommon "github.com/hanzoai/deploy/common"
|
|
"github.com/hanzoai/deploy/pkg/apiclient/applicationset"
|
|
eventspb "github.com/hanzoai/deploy/pkg/apiclient/events"
|
|
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
|
|
appclientset "github.com/hanzoai/deploy/pkg/client/clientset/versioned"
|
|
applisters "github.com/hanzoai/deploy/pkg/client/listers/application/v1alpha1"
|
|
repoapiclient "github.com/hanzoai/deploy/reposerver/apiclient"
|
|
"github.com/hanzoai/deploy/server/broadcast"
|
|
applog "github.com/hanzoai/deploy/util/app/log"
|
|
"github.com/hanzoai/deploy/util/argo"
|
|
"github.com/hanzoai/deploy/util/collections"
|
|
"github.com/hanzoai/deploy/util/db"
|
|
"github.com/hanzoai/deploy/util/github_app"
|
|
"github.com/hanzoai/deploy/util/rbac"
|
|
"github.com/hanzoai/deploy/util/security"
|
|
"github.com/hanzoai/deploy/util/session"
|
|
"github.com/hanzoai/deploy/util/settings"
|
|
|
|
serverevents "github.com/hanzoai/deploy/server/events"
|
|
)
|
|
|
|
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, namespace, "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) (*eventspb.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 serverevents.K8sEventListToAPIEventList(list), nil
|
|
}
|