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]>
854 lines
30 KiB
Go
854 lines
30 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"github.com/hanzoai/deploy/gitops-engine/pkg/utils/kube"
|
|
"github.com/hanzoai/deploy/gitops-engine/pkg/utils/text"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
|
|
"github.com/hanzoai/deploy/common"
|
|
repositorypkg "github.com/hanzoai/deploy/pkg/apiclient/repository"
|
|
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
|
|
applisters "github.com/hanzoai/deploy/pkg/client/listers/application/v1alpha1"
|
|
"github.com/hanzoai/deploy/reposerver/apiclient"
|
|
servercache "github.com/hanzoai/deploy/server/cache"
|
|
"github.com/hanzoai/deploy/util/argo"
|
|
"github.com/hanzoai/deploy/util/db"
|
|
"github.com/hanzoai/deploy/util/errors"
|
|
"github.com/hanzoai/deploy/util/git"
|
|
utilio "github.com/hanzoai/deploy/util/io"
|
|
"github.com/hanzoai/deploy/util/rbac"
|
|
"github.com/hanzoai/deploy/util/settings"
|
|
)
|
|
|
|
// Server provides a Repository service
|
|
type Server struct {
|
|
db db.ArgoDB
|
|
repoClientset apiclient.Clientset
|
|
enf *rbac.Enforcer
|
|
cache *servercache.Cache
|
|
appLister applisters.ApplicationLister
|
|
projLister cache.SharedIndexInformer
|
|
settings *settings.SettingsManager
|
|
namespace string
|
|
hydratorEnabled bool
|
|
}
|
|
|
|
// NewServer returns a new instance of the Repository service
|
|
func NewServer(
|
|
repoClientset apiclient.Clientset,
|
|
db db.ArgoDB,
|
|
enf *rbac.Enforcer,
|
|
cache *servercache.Cache,
|
|
appLister applisters.ApplicationLister,
|
|
projLister cache.SharedIndexInformer,
|
|
namespace string,
|
|
settings *settings.SettingsManager,
|
|
hydratorEnabled bool,
|
|
) *Server {
|
|
return &Server{
|
|
db: db,
|
|
repoClientset: repoClientset,
|
|
enf: enf,
|
|
cache: cache,
|
|
appLister: appLister,
|
|
projLister: projLister,
|
|
namespace: namespace,
|
|
settings: settings,
|
|
hydratorEnabled: hydratorEnabled,
|
|
}
|
|
}
|
|
|
|
func (s *Server) getRepo(ctx context.Context, url, project string) (*v1alpha1.Repository, error) {
|
|
repo, err := s.db.GetRepository(ctx, url, project)
|
|
if err != nil {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
func (s *Server) getWriteRepo(ctx context.Context, url, project string) (*v1alpha1.Repository, error) {
|
|
repo, err := s.db.GetWriteRepository(ctx, url, project)
|
|
if err != nil {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
func createRBACObject(project string, repo string) string {
|
|
if project != "" {
|
|
return project + "/" + repo
|
|
}
|
|
return repo
|
|
}
|
|
|
|
// Get the connection state for a given repository URL by connecting to the
|
|
// repo and evaluate the results. Unless forceRefresh is set to true, the
|
|
// result may be retrieved out of the cache.
|
|
func (s *Server) getConnectionState(ctx context.Context, url string, project string, forceRefresh bool) v1alpha1.ConnectionState {
|
|
if !forceRefresh {
|
|
if connectionState, err := s.cache.GetRepoConnectionState(url, project); err == nil {
|
|
return connectionState
|
|
}
|
|
}
|
|
now := metav1.Now()
|
|
connectionState := v1alpha1.ConnectionState{
|
|
Status: v1alpha1.ConnectionStatusSuccessful,
|
|
ModifiedAt: &now,
|
|
}
|
|
var err error
|
|
repo, err := s.db.GetRepository(ctx, url, project)
|
|
if err == nil {
|
|
err = s.testRepo(ctx, repo)
|
|
}
|
|
if err != nil {
|
|
connectionState.Status = v1alpha1.ConnectionStatusFailed
|
|
if errors.IsCredentialsConfigurationError(err) {
|
|
connectionState.Message = "Configuration error - please check the server logs"
|
|
log.Warnf("could not retrieve repo: %s", err.Error())
|
|
} else {
|
|
connectionState.Message = fmt.Sprintf("Unable to connect to repository: %v", err)
|
|
}
|
|
}
|
|
err = s.cache.SetRepoConnectionState(url, project, &connectionState)
|
|
if err != nil {
|
|
log.Warnf("getConnectionState cache set error %s: %v", url, err)
|
|
}
|
|
return connectionState
|
|
}
|
|
|
|
// List returns list of repositories
|
|
//
|
|
// Deprecated: Use ListRepositories instead
|
|
func (s *Server) List(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
|
|
return s.ListRepositories(ctx, q)
|
|
}
|
|
|
|
// Get return the requested configured repository by URL and the state of its connections.
|
|
func (s *Server) Get(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
|
|
// ListRepositories normalizes the repo, sanitizes it, and augments it with connection details.
|
|
repo, err := getRepository(ctx, s.ListRepositories, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists, err := s.db.RepositoryExists(ctx, q.Repo, repo.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, status.Errorf(codes.NotFound, "repo '%s' not found", q.Repo)
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
func (s *Server) GetWrite(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
repo, err := getRepository(ctx, s.ListWriteRepositories, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists, err := s.db.WriteRepositoryExists(ctx, q.Repo, repo.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, status.Errorf(codes.NotFound, "write repo '%s' not found", q.Repo)
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
// ListRepositories returns a list of all configured repositories and the state of their connections
|
|
func (s *Server) ListRepositories(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
|
|
repos, err := s.db.ListRepositories(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := s.prepareRepoList(ctx, rbac.ResourceRepositories, repos, q.ForceRefresh)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1alpha1.RepositoryList{Items: items}, nil
|
|
}
|
|
|
|
// ListWriteRepositories returns a list of all configured repositories where the user has write access and the state of
|
|
// their connections
|
|
func (s *Server) ListWriteRepositories(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
repos, err := s.db.ListWriteRepositories(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := s.prepareRepoList(ctx, rbac.ResourceWriteRepositories, repos, q.ForceRefresh)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1alpha1.RepositoryList{Items: items}, nil
|
|
}
|
|
|
|
// prepareRepoList returns a list of all configured repositories and the state of their connections. It
|
|
// normalizes, sanitizes, and filters out repositories that the user does not have access to in the specified project.
|
|
// It also sorts the repositories by project and repo name.
|
|
func (s *Server) prepareRepoList(ctx context.Context, resourceType string, repos []*v1alpha1.Repository, forceRefresh bool) (v1alpha1.Repositories, error) {
|
|
items := v1alpha1.Repositories{}
|
|
for _, repo := range repos {
|
|
items = append(items, repo.Normalize().Sanitized())
|
|
}
|
|
items = items.Filter(func(r *v1alpha1.Repository) bool {
|
|
return s.enf.Enforce(ctx.Value("claims"), resourceType, rbac.ActionGet, createRBACObject(r.Project, r.Repo))
|
|
})
|
|
err := kube.RunAllAsync(len(items), func(i int) error {
|
|
items[i].ConnectionState = s.getConnectionState(ctx, items[i].Repo, items[i].Project, forceRefresh)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
first := items[i]
|
|
second := items[j]
|
|
return fmt.Sprintf("%s/%s", first.Project, first.Repo) < fmt.Sprintf("%s/%s", second.Project, second.Repo)
|
|
})
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Server) ListOCITags(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.Refs, error) {
|
|
repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer utilio.Close(conn)
|
|
|
|
return repoClient.ListOCITags(ctx, &apiclient.ListRefsRequest{
|
|
Repo: repo,
|
|
})
|
|
}
|
|
|
|
func (s *Server) ListRefs(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.Refs, error) {
|
|
repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer utilio.Close(conn)
|
|
|
|
return repoClient.ListRefs(ctx, &apiclient.ListRefsRequest{
|
|
Repo: repo,
|
|
})
|
|
}
|
|
|
|
// ListApps performs discovery of a git repository for potential sources of applications. Used
|
|
// as a convenience to the UI for auto-complete.
|
|
func (s *Server) ListApps(ctx context.Context, q *repositorypkg.RepoAppsQuery) (*repositorypkg.RepoAppsResponse, error) {
|
|
repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
claims := ctx.Value("claims")
|
|
if err := s.enf.EnforceErr(claims, rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// This endpoint causes us to clone git repos & invoke config management tooling for the purposes
|
|
// of app discovery. Only allow this to happen if user has privileges to create or update the
|
|
// application which it wants to retrieve these details for.
|
|
appRBACresource := fmt.Sprintf("%s/%s", q.AppProject, q.AppName)
|
|
if !s.enf.Enforce(claims, rbac.ResourceApplications, rbac.ActionCreate, appRBACresource) &&
|
|
!s.enf.Enforce(claims, rbac.ResourceApplications, rbac.ActionUpdate, appRBACresource) {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
// Also ensure the repo is actually allowed in the project in question
|
|
if err := s.isRepoPermittedInProject(ctx, q.Repo, q.AppProject); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Test the repo
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer utilio.Close(conn)
|
|
|
|
apps, err := repoClient.ListApps(ctx, &apiclient.ListAppsRequest{
|
|
Repo: repo,
|
|
Revision: q.Revision,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]*repositorypkg.AppInfo, 0)
|
|
for app, appType := range apps.Apps {
|
|
items = append(items, &repositorypkg.AppInfo{Path: app, Type: appType})
|
|
}
|
|
return &repositorypkg.RepoAppsResponse{Items: items}, nil
|
|
}
|
|
|
|
// GetAppDetails shows parameter values to various config tools (e.g. helm/kustomize values)
|
|
// This is used by UI for parameter form fields during app create & edit pages.
|
|
// It is also used when showing history of parameters used in previous syncs in the app history.
|
|
func (s *Server) GetAppDetails(ctx context.Context, q *repositorypkg.RepoAppDetailsQuery) (*apiclient.RepoAppDetailsResponse, error) {
|
|
if q.Source == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
|
|
}
|
|
repo, err := s.getRepo(ctx, q.Source.RepoURL, q.GetAppProject())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
claims := ctx.Value("claims")
|
|
if err := s.enf.EnforceErr(claims, rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
appName, appNs := argo.ParseFromQualifiedName(q.AppName, s.settings.GetNamespace())
|
|
app, err := s.appLister.Applications(appNs).Get(appName)
|
|
appRBACObj := createRBACObject(q.AppProject, q.AppName)
|
|
// ensure caller has read privileges to app
|
|
if err := s.enf.EnforceErr(claims, rbac.ResourceApplications, rbac.ActionGet, appRBACObj); err != nil {
|
|
return nil, err
|
|
}
|
|
if apierrors.IsNotFound(err) {
|
|
// app doesn't exist since it still is being formulated. verify they can create the app
|
|
// before we reveal repo details
|
|
if err := s.enf.EnforceErr(claims, rbac.ResourceApplications, rbac.ActionCreate, appRBACObj); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// if we get here we are returning repo details of an existing app
|
|
if q.AppProject != app.Spec.Project {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
// verify caller is not making a request with arbitrary source values which were not in our history
|
|
if !isSourceInHistory(app, *q.Source, q.SourceIndex, q.VersionId) {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
}
|
|
// Ensure the repo is actually allowed in the project in question
|
|
if err := s.isRepoPermittedInProject(ctx, q.Source.RepoURL, q.AppProject); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer utilio.Close(conn)
|
|
helmRepos, err := s.db.ListHelmRepositories(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kustomizeSettings, err := s.settings.GetKustomizeSettings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
helmOptions, err := s.settings.GetHelmSettings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refSources := make(v1alpha1.RefTargetRevisionMapping)
|
|
if app != nil && app.Spec.HasMultipleSources() {
|
|
// Store the map of all sources having ref field into a map for applications with sources field
|
|
refSources, err = argo.GetRefSources(ctx, app.Spec.Sources, q.AppProject, s.db.GetRepository, []string{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get ref sources: %w", err)
|
|
}
|
|
}
|
|
|
|
return repoClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: repo,
|
|
Source: q.Source,
|
|
Repos: helmRepos,
|
|
KustomizeOptions: kustomizeSettings,
|
|
HelmOptions: helmOptions,
|
|
AppName: q.AppName,
|
|
RefSources: refSources,
|
|
})
|
|
}
|
|
|
|
// GetHelmCharts returns list of helm charts in the specified repository
|
|
func (s *Server) GetHelmCharts(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.HelmChartsResponse, error) {
|
|
repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer utilio.Close(conn)
|
|
return repoClient.GetHelmCharts(ctx, &apiclient.HelmChartsRequest{Repo: repo})
|
|
}
|
|
|
|
// Create creates a repository or repository credential set
|
|
//
|
|
// Deprecated: Use CreateRepository() instead
|
|
func (s *Server) Create(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
|
|
return s.CreateRepository(ctx, q)
|
|
}
|
|
|
|
// CreateRepository creates a repository configuration
|
|
func (s *Server) CreateRepository(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
|
|
if q.Repo == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionCreate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var repo *v1alpha1.Repository
|
|
var err error
|
|
|
|
// check we can connect to the repo, copying any existing creds (not supported for project scoped repositories)
|
|
if q.Repo.Project == "" {
|
|
repo := q.Repo.DeepCopy()
|
|
if !repo.HasCredentials() {
|
|
creds, err := s.db.GetRepositoryCredentials(ctx, repo.Repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repo.CopyCredentialsFrom(creds)
|
|
}
|
|
|
|
err = s.testRepo(ctx, repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
r := q.Repo
|
|
r.ConnectionState = v1alpha1.ConnectionState{Status: v1alpha1.ConnectionStatusSuccessful}
|
|
repo, err = s.db.CreateRepository(ctx, r)
|
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
|
// act idempotent if existing spec matches new spec
|
|
existing, getErr := s.db.GetRepository(ctx, r.Repo, q.Repo.Project)
|
|
if getErr != nil {
|
|
return nil, status.Errorf(codes.Internal, "unable to check existing repository details: %v", getErr)
|
|
}
|
|
|
|
existing.Type = text.FirstNonEmpty(existing.Type, "git")
|
|
// repository ConnectionState may differ, so make consistent before testing
|
|
existing.ConnectionState = r.ConnectionState
|
|
switch {
|
|
case reflect.DeepEqual(existing, r):
|
|
repo, err = existing, nil
|
|
case q.Upsert:
|
|
r.Project = q.Repo.Project
|
|
return s.db.UpdateRepository(ctx, r)
|
|
default:
|
|
return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("repository", existing, r))
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1alpha1.Repository{Repo: repo.Repo, Type: repo.Type, Name: repo.Name}, nil
|
|
}
|
|
|
|
// CreateWriteRepository creates a repository configuration with write credentials
|
|
func (s *Server) CreateWriteRepository(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
if q.Repo == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionCreate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !q.Repo.HasCredentials() {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing credentials in request")
|
|
}
|
|
|
|
err := s.testRepo(ctx, q.Repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo, err := s.db.CreateWriteRepository(ctx, q.Repo)
|
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
|
// act idempotent if existing spec matches new spec
|
|
existing, getErr := s.db.GetWriteRepository(ctx, q.Repo.Repo, q.Repo.Project)
|
|
if getErr != nil {
|
|
return nil, status.Errorf(codes.Internal, "unable to check existing repository details: %v", getErr)
|
|
}
|
|
switch {
|
|
case reflect.DeepEqual(existing, q.Repo):
|
|
repo, err = existing, nil
|
|
case q.Upsert:
|
|
return s.db.UpdateWriteRepository(ctx, q.Repo)
|
|
default:
|
|
return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("write repository", existing, q.Repo))
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1alpha1.Repository{Repo: repo.Repo, Type: repo.Type, Name: repo.Name}, nil
|
|
}
|
|
|
|
// Update updates a repository or credential set
|
|
//
|
|
// Deprecated: Use UpdateRepository() instead
|
|
func (s *Server) Update(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
|
|
return s.UpdateRepository(ctx, q)
|
|
}
|
|
|
|
// UpdateRepository updates a repository configuration
|
|
func (s *Server) UpdateRepository(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
|
|
if q.Repo == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
|
|
}
|
|
|
|
repo, err := s.getRepo(ctx, q.Repo.Repo, q.Repo.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// verify that user can do update inside project where repository is located
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionUpdate, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
// verify that user can do update inside project where repository will be located
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionUpdate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = s.db.UpdateRepository(ctx, q.Repo)
|
|
return &v1alpha1.Repository{Repo: q.Repo.Repo, Type: q.Repo.Type, Name: q.Repo.Name}, err
|
|
}
|
|
|
|
// UpdateWriteRepository updates a repository configuration with write credentials
|
|
func (s *Server) UpdateWriteRepository(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
if q.Repo == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
|
|
}
|
|
|
|
repo, err := s.getWriteRepo(ctx, q.Repo.Repo, q.Repo.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// verify that user can do update inside project where repository is located
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionUpdate, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
// verify that user can do update inside project where repository will be located
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionUpdate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = s.db.UpdateWriteRepository(ctx, q.Repo)
|
|
return &v1alpha1.Repository{Repo: q.Repo.Repo, Type: q.Repo.Type, Name: q.Repo.Name}, err
|
|
}
|
|
|
|
// Delete removes a repository from the configuration
|
|
//
|
|
// Deprecated: Use DeleteRepository() instead
|
|
func (s *Server) Delete(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
|
|
return s.DeleteRepository(ctx, q)
|
|
}
|
|
|
|
// DeleteRepository removes a repository from the configuration
|
|
func (s *Server) DeleteRepository(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
|
|
repo, err := getRepository(ctx, s.ListRepositories, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionDelete, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// invalidate cache
|
|
if err := s.cache.SetRepoConnectionState(repo.Repo, repo.Project, nil); err != nil {
|
|
log.Errorf("error invalidating cache: %v", err)
|
|
}
|
|
|
|
err = s.db.DeleteRepository(ctx, repo.Repo, repo.Project)
|
|
return &repositorypkg.RepoResponse{}, err
|
|
}
|
|
|
|
// DeleteWriteRepository removes a repository from the configuration
|
|
func (s *Server) DeleteWriteRepository(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
repo, err := getRepository(ctx, s.ListWriteRepositories, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionDelete, createRBACObject(repo.Project, repo.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.db.DeleteWriteRepository(ctx, repo.Repo, repo.Project)
|
|
return &repositorypkg.RepoResponse{}, err
|
|
}
|
|
|
|
// getRepository fetches a single repository which the user has access to. If only one repository can be found which
|
|
// matches the same URL, that will be returned (this is for backward compatibility reasons). If multiple repositories
|
|
// are matched, a repository is only returned if it matches the app project of the incoming request.
|
|
func getRepository(ctx context.Context, listRepositories func(context.Context, *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error), q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
|
|
repositories, err := listRepositories(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var foundRepos []*v1alpha1.Repository
|
|
for _, repo := range repositories.Items {
|
|
if git.SameURL(repo.Repo, q.Repo) {
|
|
foundRepos = append(foundRepos, repo)
|
|
}
|
|
}
|
|
|
|
if len(foundRepos) == 0 {
|
|
return nil, common.PermissionDeniedAPIError
|
|
}
|
|
|
|
var foundRepo *v1alpha1.Repository
|
|
if len(foundRepos) == 1 && q.GetAppProject() == "" {
|
|
foundRepo = foundRepos[0]
|
|
} else if len(foundRepos) > 0 {
|
|
for _, repo := range foundRepos {
|
|
if repo.Project == q.GetAppProject() {
|
|
foundRepo = repo
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundRepo == nil {
|
|
return nil, fmt.Errorf("repository not found for url %q and project %q", q.Repo, q.GetAppProject())
|
|
}
|
|
|
|
return foundRepo, nil
|
|
}
|
|
|
|
// ValidateAccess checks whether access to a repository is possible with the
|
|
// given URL and credentials.
|
|
func (s *Server) ValidateAccess(ctx context.Context, q *repositorypkg.RepoAccessQuery) (*repositorypkg.RepoResponse, error) {
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionCreate, createRBACObject(q.Project, q.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo := &v1alpha1.Repository{
|
|
Repo: q.Repo,
|
|
Type: q.Type,
|
|
Name: q.Name,
|
|
Username: q.Username,
|
|
Password: q.Password,
|
|
BearerToken: q.BearerToken,
|
|
SSHPrivateKey: q.SshPrivateKey,
|
|
Insecure: q.Insecure,
|
|
TLSClientCertData: q.TlsClientCertData,
|
|
TLSClientCertKey: q.TlsClientCertKey,
|
|
EnableOCI: q.EnableOci,
|
|
GithubAppPrivateKey: q.GithubAppPrivateKey,
|
|
GithubAppId: q.GithubAppID,
|
|
GithubAppInstallationId: q.GithubAppInstallationID,
|
|
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
|
|
Proxy: q.Proxy,
|
|
GCPServiceAccountKey: q.GcpServiceAccountKey,
|
|
InsecureOCIForceHttp: q.InsecureOciForceHttp,
|
|
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
|
|
AzureServicePrincipalClientId: q.AzureServicePrincipalClientId,
|
|
AzureServicePrincipalClientSecret: q.AzureServicePrincipalClientSecret,
|
|
AzureServicePrincipalTenantId: q.AzureServicePrincipalTenantId,
|
|
AzureActiveDirectoryEndpoint: q.AzureActiveDirectoryEndpoint,
|
|
}
|
|
|
|
// If repo does not have credentials, check if there are credentials stored
|
|
// for it and if yes, copy them
|
|
if !repo.HasCredentials() {
|
|
repoCreds, err := s.db.GetRepositoryCredentials(ctx, q.Repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if repoCreds != nil {
|
|
repo.CopyCredentialsFrom(repoCreds)
|
|
}
|
|
}
|
|
err := s.testRepo(ctx, repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &repositorypkg.RepoResponse{}, nil
|
|
}
|
|
|
|
// ValidateWriteAccess checks whether write access to a repository is possible with the
|
|
// given URL and credentials.
|
|
func (s *Server) ValidateWriteAccess(ctx context.Context, q *repositorypkg.RepoAccessQuery) (*repositorypkg.RepoResponse, error) {
|
|
if !s.hydratorEnabled {
|
|
return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
|
|
}
|
|
|
|
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionCreate, createRBACObject(q.Project, q.Repo)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo := &v1alpha1.Repository{
|
|
Repo: q.Repo,
|
|
Type: q.Type,
|
|
Name: q.Name,
|
|
Username: q.Username,
|
|
Password: q.Password,
|
|
BearerToken: q.BearerToken,
|
|
SSHPrivateKey: q.SshPrivateKey,
|
|
Insecure: q.Insecure,
|
|
TLSClientCertData: q.TlsClientCertData,
|
|
TLSClientCertKey: q.TlsClientCertKey,
|
|
EnableOCI: q.EnableOci,
|
|
GithubAppPrivateKey: q.GithubAppPrivateKey,
|
|
GithubAppId: q.GithubAppID,
|
|
GithubAppInstallationId: q.GithubAppInstallationID,
|
|
GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
|
|
Proxy: q.Proxy,
|
|
GCPServiceAccountKey: q.GcpServiceAccountKey,
|
|
UseAzureWorkloadIdentity: q.UseAzureWorkloadIdentity,
|
|
AzureServicePrincipalClientId: q.AzureServicePrincipalClientId,
|
|
AzureServicePrincipalClientSecret: q.AzureServicePrincipalClientSecret,
|
|
AzureServicePrincipalTenantId: q.AzureServicePrincipalTenantId,
|
|
AzureActiveDirectoryEndpoint: q.AzureActiveDirectoryEndpoint,
|
|
}
|
|
|
|
err := s.testRepo(ctx, repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &repositorypkg.RepoResponse{}, nil
|
|
}
|
|
|
|
func (s *Server) testRepo(ctx context.Context, repo *v1alpha1.Repository) error {
|
|
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to repo-server: %w", err)
|
|
}
|
|
defer utilio.Close(conn)
|
|
|
|
_, err = repoClient.TestRepository(ctx, &apiclient.TestRepositoryRequest{
|
|
Repo: repo,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Server) isRepoPermittedInProject(ctx context.Context, repo string, projName string) error {
|
|
proj, err := argo.GetAppProjectByName(ctx, projName, applisters.NewAppProjectLister(s.projLister.GetIndexer()), s.namespace, s.settings, s.db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !proj.IsSourcePermitted(v1alpha1.ApplicationSource{RepoURL: repo}) {
|
|
return status.Errorf(codes.PermissionDenied, "repository '%s' not permitted in project '%s'", repo, projName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isSourceInHistory checks if the supplied application source is either our current application
|
|
// source, or was something which we synced to previously.
|
|
func isSourceInHistory(app *v1alpha1.Application, source v1alpha1.ApplicationSource, index int32, versionId int32) bool {
|
|
if app.Spec.SourceHydrator != nil {
|
|
drySource := app.Spec.SourceHydrator.GetDrySource()
|
|
syncSource := app.Spec.SourceHydrator.GetSyncSource()
|
|
if source.Equals(&drySource) || source.Equals(&syncSource) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
// We have to check if the spec is within the source or sources split
|
|
// and then iterate over the historical
|
|
if app.Spec.HasMultipleSources() {
|
|
appSources := app.Spec.GetSources()
|
|
for _, s := range appSources {
|
|
if source.Equals(&s) {
|
|
return true
|
|
}
|
|
}
|
|
} else {
|
|
appSource := app.Spec.GetSource()
|
|
if source.Equals(&appSource) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Iterate history. When comparing items in our history, use the actual synced revision to
|
|
// compare with the supplied source.targetRevision in the request. This is because
|
|
// history[].source.targetRevision is ambiguous (e.g. HEAD), whereas
|
|
// history[].revision will contain the explicit SHA
|
|
// In case of multi source apps, we have to check the specific versionID because users
|
|
// could have removed/added new sources and we cannot check all the versions due to that
|
|
for _, h := range app.Status.History {
|
|
// multi source revision
|
|
if len(h.Sources) > 0 {
|
|
if h.ID == int64(versionId) {
|
|
if h.Revisions == nil {
|
|
continue
|
|
}
|
|
h.Sources[index].TargetRevision = h.Revisions[index]
|
|
if source.Equals(&h.Sources[index]) {
|
|
return true
|
|
}
|
|
}
|
|
} else { // single source revision
|
|
h.Source.TargetRevision = h.Revision
|
|
if source.Equals(&h.Source) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|