github.com/argoproj/argo-cd/v2@v2.10.9/server/repository/repository.go (about) 1 package repository 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 8 "github.com/argoproj/gitops-engine/pkg/utils/kube" 9 "github.com/argoproj/gitops-engine/pkg/utils/text" 10 log "github.com/sirupsen/logrus" 11 "google.golang.org/grpc/codes" 12 "google.golang.org/grpc/status" 13 apierr "k8s.io/apimachinery/pkg/api/errors" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/client-go/tools/cache" 16 17 "github.com/argoproj/argo-cd/v2/common" 18 repositorypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository" 19 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 20 appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 21 applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" 22 "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 23 servercache "github.com/argoproj/argo-cd/v2/server/cache" 24 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 25 "github.com/argoproj/argo-cd/v2/util/argo" 26 "github.com/argoproj/argo-cd/v2/util/db" 27 "github.com/argoproj/argo-cd/v2/util/errors" 28 "github.com/argoproj/argo-cd/v2/util/io" 29 "github.com/argoproj/argo-cd/v2/util/rbac" 30 "github.com/argoproj/argo-cd/v2/util/settings" 31 ) 32 33 // Server provides a Repository service 34 type Server struct { 35 db db.ArgoDB 36 repoClientset apiclient.Clientset 37 enf *rbac.Enforcer 38 cache *servercache.Cache 39 appLister applisters.ApplicationLister 40 projLister cache.SharedIndexInformer 41 settings *settings.SettingsManager 42 namespace string 43 } 44 45 // NewServer returns a new instance of the Repository service 46 func NewServer( 47 repoClientset apiclient.Clientset, 48 db db.ArgoDB, 49 enf *rbac.Enforcer, 50 cache *servercache.Cache, 51 appLister applisters.ApplicationLister, 52 projLister cache.SharedIndexInformer, 53 namespace string, 54 settings *settings.SettingsManager, 55 ) *Server { 56 return &Server{ 57 db: db, 58 repoClientset: repoClientset, 59 enf: enf, 60 cache: cache, 61 appLister: appLister, 62 projLister: projLister, 63 namespace: namespace, 64 settings: settings, 65 } 66 } 67 68 var ( 69 errPermissionDenied = status.Error(codes.PermissionDenied, "permission denied") 70 ) 71 72 func (s *Server) getRepo(ctx context.Context, url string) (*appsv1.Repository, error) { 73 repo, err := s.db.GetRepository(ctx, url) 74 if err != nil { 75 return nil, errPermissionDenied 76 } 77 return repo, nil 78 } 79 80 func createRBACObject(project string, repo string) string { 81 if project != "" { 82 return project + "/" + repo 83 } 84 return repo 85 } 86 87 // Get the connection state for a given repository URL by connecting to the 88 // repo and evaluate the results. Unless forceRefresh is set to true, the 89 // result may be retrieved out of the cache. 90 func (s *Server) getConnectionState(ctx context.Context, url string, forceRefresh bool) appsv1.ConnectionState { 91 if !forceRefresh { 92 if connectionState, err := s.cache.GetRepoConnectionState(url); err == nil { 93 return connectionState 94 } 95 } 96 now := metav1.Now() 97 connectionState := appsv1.ConnectionState{ 98 Status: appsv1.ConnectionStatusSuccessful, 99 ModifiedAt: &now, 100 } 101 var err error 102 repo, err := s.db.GetRepository(ctx, url) 103 if err == nil { 104 err = s.testRepo(ctx, repo) 105 } 106 if err != nil { 107 connectionState.Status = appsv1.ConnectionStatusFailed 108 if errors.IsCredentialsConfigurationError(err) { 109 connectionState.Message = "Configuration error - please check the server logs" 110 log.Warnf("could not retrieve repo: %s", err.Error()) 111 } else { 112 connectionState.Message = fmt.Sprintf("Unable to connect to repository: %v", err) 113 } 114 } 115 err = s.cache.SetRepoConnectionState(url, &connectionState) 116 if err != nil { 117 log.Warnf("getConnectionState cache set error %s: %v", url, err) 118 } 119 return connectionState 120 } 121 122 // List returns list of repositories 123 // Deprecated: Use ListRepositories instead 124 func (s *Server) List(ctx context.Context, q *repositorypkg.RepoQuery) (*appsv1.RepositoryList, error) { 125 return s.ListRepositories(ctx, q) 126 } 127 128 // Get return the requested configured repository by URL and the state of its connections. 129 func (s *Server) Get(ctx context.Context, q *repositorypkg.RepoQuery) (*appsv1.Repository, error) { 130 repo, err := s.getRepo(ctx, q.Repo) 131 if err != nil { 132 return nil, err 133 } 134 135 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil { 136 return nil, err 137 } 138 139 // getRepo does not return an error for unconfigured repositories, so we are checking here 140 exists, err := s.db.RepositoryExists(ctx, q.Repo) 141 if err != nil { 142 return nil, err 143 } 144 if !exists { 145 return nil, status.Errorf(codes.NotFound, "repo '%s' not found", q.Repo) 146 } 147 148 // For backwards compatibility, if we have no repo type set assume a default 149 rType := repo.Type 150 if rType == "" { 151 rType = common.DefaultRepoType 152 } 153 // remove secrets 154 item := appsv1.Repository{ 155 Repo: repo.Repo, 156 Type: rType, 157 Name: repo.Name, 158 Username: repo.Username, 159 Insecure: repo.IsInsecure(), 160 EnableLFS: repo.EnableLFS, 161 GithubAppId: repo.GithubAppId, 162 GithubAppInstallationId: repo.GithubAppInstallationId, 163 GitHubAppEnterpriseBaseURL: repo.GitHubAppEnterpriseBaseURL, 164 Proxy: repo.Proxy, 165 Project: repo.Project, 166 InheritedCreds: repo.InheritedCreds, 167 } 168 169 item.ConnectionState = s.getConnectionState(ctx, item.Repo, q.ForceRefresh) 170 171 return &item, nil 172 } 173 174 // ListRepositories returns a list of all configured repositories and the state of their connections 175 func (s *Server) ListRepositories(ctx context.Context, q *repositorypkg.RepoQuery) (*appsv1.RepositoryList, error) { 176 repos, err := s.db.ListRepositories(ctx) 177 if err != nil { 178 return nil, err 179 } 180 items := appsv1.Repositories{} 181 for _, repo := range repos { 182 if s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)) { 183 // For backwards compatibility, if we have no repo type set assume a default 184 rType := repo.Type 185 if rType == "" { 186 rType = common.DefaultRepoType 187 } 188 // remove secrets 189 items = append(items, &appsv1.Repository{ 190 Repo: repo.Repo, 191 Type: rType, 192 Name: repo.Name, 193 Username: repo.Username, 194 Insecure: repo.IsInsecure(), 195 EnableLFS: repo.EnableLFS, 196 EnableOCI: repo.EnableOCI, 197 Proxy: repo.Proxy, 198 Project: repo.Project, 199 ForceHttpBasicAuth: repo.ForceHttpBasicAuth, 200 InheritedCreds: repo.InheritedCreds, 201 }) 202 } 203 } 204 err = kube.RunAllAsync(len(items), func(i int) error { 205 items[i].ConnectionState = s.getConnectionState(ctx, items[i].Repo, q.ForceRefresh) 206 return nil 207 }) 208 if err != nil { 209 return nil, err 210 } 211 return &appsv1.RepositoryList{Items: items}, nil 212 } 213 214 func (s *Server) ListRefs(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.Refs, error) { 215 repo, err := s.getRepo(ctx, q.Repo) 216 if err != nil { 217 return nil, err 218 } 219 220 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil { 221 return nil, err 222 } 223 224 conn, repoClient, err := s.repoClientset.NewRepoServerClient() 225 if err != nil { 226 return nil, err 227 } 228 defer io.Close(conn) 229 230 return repoClient.ListRefs(ctx, &apiclient.ListRefsRequest{ 231 Repo: repo, 232 }) 233 } 234 235 // ListApps performs discovery of a git repository for potential sources of applications. Used 236 // as a convenience to the UI for auto-complete. 237 func (s *Server) ListApps(ctx context.Context, q *repositorypkg.RepoAppsQuery) (*repositorypkg.RepoAppsResponse, error) { 238 repo, err := s.getRepo(ctx, q.Repo) 239 if err != nil { 240 return nil, err 241 } 242 243 claims := ctx.Value("claims") 244 if err := s.enf.EnforceErr(claims, rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil { 245 return nil, err 246 } 247 248 // This endpoint causes us to clone git repos & invoke config management tooling for the purposes 249 // of app discovery. Only allow this to happen if user has privileges to create or update the 250 // application which it wants to retrieve these details for. 251 appRBACresource := fmt.Sprintf("%s/%s", q.AppProject, q.AppName) 252 if !s.enf.Enforce(claims, rbacpolicy.ResourceApplications, rbacpolicy.ActionCreate, appRBACresource) && 253 !s.enf.Enforce(claims, rbacpolicy.ResourceApplications, rbacpolicy.ActionUpdate, appRBACresource) { 254 return nil, errPermissionDenied 255 } 256 // Also ensure the repo is actually allowed in the project in question 257 if err := s.isRepoPermittedInProject(ctx, q.Repo, q.AppProject); err != nil { 258 return nil, err 259 } 260 261 // Test the repo 262 conn, repoClient, err := s.repoClientset.NewRepoServerClient() 263 if err != nil { 264 return nil, err 265 } 266 defer io.Close(conn) 267 268 apps, err := repoClient.ListApps(ctx, &apiclient.ListAppsRequest{ 269 Repo: repo, 270 Revision: q.Revision, 271 }) 272 if err != nil { 273 return nil, err 274 } 275 items := make([]*repositorypkg.AppInfo, 0) 276 for app, appType := range apps.Apps { 277 items = append(items, &repositorypkg.AppInfo{Path: app, Type: appType}) 278 } 279 return &repositorypkg.RepoAppsResponse{Items: items}, nil 280 } 281 282 // GetAppDetails shows parameter values to various config tools (e.g. helm/kustomize values) 283 // This is used by UI for parameter form fields during app create & edit pages. 284 // It is also used when showing history of parameters used in previous syncs in the app history. 285 func (s *Server) GetAppDetails(ctx context.Context, q *repositorypkg.RepoAppDetailsQuery) (*apiclient.RepoAppDetailsResponse, error) { 286 if q.Source == nil { 287 return nil, status.Errorf(codes.InvalidArgument, "missing payload in request") 288 } 289 repo, err := s.getRepo(ctx, q.Source.RepoURL) 290 if err != nil { 291 return nil, err 292 } 293 claims := ctx.Value("claims") 294 if err := s.enf.EnforceErr(claims, rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil { 295 return nil, err 296 } 297 appName, appNs := argo.ParseFromQualifiedName(q.AppName, s.settings.GetNamespace()) 298 app, err := s.appLister.Applications(appNs).Get(appName) 299 appRBACObj := createRBACObject(q.AppProject, q.AppName) 300 // ensure caller has read privileges to app 301 if err := s.enf.EnforceErr(claims, rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, appRBACObj); err != nil { 302 return nil, err 303 } 304 if apierr.IsNotFound(err) { 305 // app doesn't exist since it still is being formulated. verify they can create the app 306 // before we reveal repo details 307 if err := s.enf.EnforceErr(claims, rbacpolicy.ResourceApplications, rbacpolicy.ActionCreate, appRBACObj); err != nil { 308 return nil, err 309 } 310 } else { 311 // if we get here we are returning repo details of an existing app 312 if q.AppProject != app.Spec.Project { 313 return nil, errPermissionDenied 314 } 315 // verify caller is not making a request with arbitrary source values which were not in our history 316 if !isSourceInHistory(app, *q.Source) { 317 return nil, errPermissionDenied 318 } 319 } 320 // Ensure the repo is actually allowed in the project in question 321 if err := s.isRepoPermittedInProject(ctx, q.Source.RepoURL, q.AppProject); err != nil { 322 return nil, err 323 } 324 325 conn, repoClient, err := s.repoClientset.NewRepoServerClient() 326 if err != nil { 327 return nil, err 328 } 329 defer io.Close(conn) 330 helmRepos, err := s.db.ListHelmRepositories(ctx) 331 if err != nil { 332 return nil, err 333 } 334 kustomizeSettings, err := s.settings.GetKustomizeSettings() 335 if err != nil { 336 return nil, err 337 } 338 kustomizeOptions, err := kustomizeSettings.GetOptions(*q.Source) 339 if err != nil { 340 return nil, err 341 } 342 helmOptions, err := s.settings.GetHelmSettings() 343 if err != nil { 344 return nil, err 345 } 346 return repoClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{ 347 Repo: repo, 348 Source: q.Source, 349 Repos: helmRepos, 350 KustomizeOptions: kustomizeOptions, 351 HelmOptions: helmOptions, 352 AppName: q.AppName, 353 }) 354 } 355 356 // GetHelmCharts returns list of helm charts in the specified repository 357 func (s *Server) GetHelmCharts(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.HelmChartsResponse, error) { 358 repo, err := s.getRepo(ctx, q.Repo) 359 if err != nil { 360 return nil, err 361 } 362 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil { 363 return nil, err 364 } 365 conn, repoClient, err := s.repoClientset.NewRepoServerClient() 366 if err != nil { 367 return nil, err 368 } 369 defer io.Close(conn) 370 return repoClient.GetHelmCharts(ctx, &apiclient.HelmChartsRequest{Repo: repo}) 371 } 372 373 // Create creates a repository or repository credential set 374 // Deprecated: Use CreateRepository() instead 375 func (s *Server) Create(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*appsv1.Repository, error) { 376 return s.CreateRepository(ctx, q) 377 } 378 379 // CreateRepository creates a repository configuration 380 func (s *Server) CreateRepository(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*appsv1.Repository, error) { 381 if q.Repo == nil { 382 return nil, status.Errorf(codes.InvalidArgument, "missing payload in request") 383 } 384 385 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionCreate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil { 386 return nil, err 387 } 388 389 var repo *appsv1.Repository 390 var err error 391 392 // check we can connect to the repo, copying any existing creds (not supported for project scoped repositories) 393 if q.Repo.Project == "" { 394 repo := q.Repo.DeepCopy() 395 if !repo.HasCredentials() { 396 creds, err := s.db.GetRepositoryCredentials(ctx, repo.Repo) 397 if err != nil { 398 return nil, err 399 } 400 repo.CopyCredentialsFrom(creds) 401 } 402 403 err = s.testRepo(ctx, repo) 404 if err != nil { 405 return nil, err 406 } 407 } 408 409 r := q.Repo 410 r.ConnectionState = appsv1.ConnectionState{Status: appsv1.ConnectionStatusSuccessful} 411 repo, err = s.db.CreateRepository(ctx, r) 412 if status.Convert(err).Code() == codes.AlreadyExists { 413 // act idempotent if existing spec matches new spec 414 existing, getErr := s.db.GetRepository(ctx, r.Repo) 415 if getErr != nil { 416 return nil, status.Errorf(codes.Internal, "unable to check existing repository details: %v", getErr) 417 } 418 419 existing.Type = text.FirstNonEmpty(existing.Type, "git") 420 // repository ConnectionState may differ, so make consistent before testing 421 existing.ConnectionState = r.ConnectionState 422 if reflect.DeepEqual(existing, r) { 423 repo, err = existing, nil 424 } else if q.Upsert { 425 r.Project = q.Repo.Project 426 return s.UpdateRepository(ctx, &repositorypkg.RepoUpdateRequest{Repo: r}) 427 } else { 428 return nil, status.Errorf(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("repository", existing, r)) 429 } 430 } 431 if err != nil { 432 return nil, err 433 } 434 return &appsv1.Repository{Repo: repo.Repo, Type: repo.Type, Name: repo.Name}, nil 435 } 436 437 // Update updates a repository or credential set 438 // Deprecated: Use UpdateRepository() instead 439 func (s *Server) Update(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*appsv1.Repository, error) { 440 return s.UpdateRepository(ctx, q) 441 } 442 443 // UpdateRepository updates a repository configuration 444 func (s *Server) UpdateRepository(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*appsv1.Repository, error) { 445 if q.Repo == nil { 446 return nil, status.Errorf(codes.InvalidArgument, "missing payload in request") 447 } 448 449 repo, err := s.getRepo(ctx, q.Repo.Repo) 450 if err != nil { 451 return nil, err 452 } 453 454 // verify that user can do update inside project where repository is located 455 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionUpdate, createRBACObject(repo.Project, repo.Repo)); err != nil { 456 return nil, err 457 } 458 // verify that user can do update inside project where repository will be located 459 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionUpdate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil { 460 return nil, err 461 } 462 _, err = s.db.UpdateRepository(ctx, q.Repo) 463 return &appsv1.Repository{Repo: q.Repo.Repo, Type: q.Repo.Type, Name: q.Repo.Name}, err 464 } 465 466 // Delete removes a repository from the configuration 467 // Deprecated: Use DeleteRepository() instead 468 func (s *Server) Delete(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) { 469 return s.DeleteRepository(ctx, q) 470 } 471 472 // DeleteRepository removes a repository from the configuration 473 func (s *Server) DeleteRepository(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) { 474 repo, err := s.getRepo(ctx, q.Repo) 475 if err != nil { 476 return nil, err 477 } 478 479 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionDelete, createRBACObject(repo.Project, repo.Repo)); err != nil { 480 return nil, err 481 } 482 483 // invalidate cache 484 if err := s.cache.SetRepoConnectionState(q.Repo, nil); err == nil { 485 log.Errorf("error invalidating cache: %v", err) 486 } 487 488 err = s.db.DeleteRepository(ctx, q.Repo) 489 return &repositorypkg.RepoResponse{}, err 490 } 491 492 // ValidateAccess checks whether access to a repository is possible with the 493 // given URL and credentials. 494 func (s *Server) ValidateAccess(ctx context.Context, q *repositorypkg.RepoAccessQuery) (*repositorypkg.RepoResponse, error) { 495 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionCreate, createRBACObject(q.Project, q.Repo)); err != nil { 496 return nil, err 497 } 498 499 repo := &appsv1.Repository{ 500 Repo: q.Repo, 501 Type: q.Type, 502 Name: q.Name, 503 Username: q.Username, 504 Password: q.Password, 505 SSHPrivateKey: q.SshPrivateKey, 506 Insecure: q.Insecure, 507 TLSClientCertData: q.TlsClientCertData, 508 TLSClientCertKey: q.TlsClientCertKey, 509 EnableOCI: q.EnableOci, 510 GithubAppPrivateKey: q.GithubAppPrivateKey, 511 GithubAppId: q.GithubAppID, 512 GithubAppInstallationId: q.GithubAppInstallationID, 513 GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl, 514 Proxy: q.Proxy, 515 GCPServiceAccountKey: q.GcpServiceAccountKey, 516 } 517 518 // If repo does not have credentials, check if there are credentials stored 519 // for it and if yes, copy them 520 if !repo.HasCredentials() { 521 repoCreds, err := s.db.GetRepositoryCredentials(ctx, q.Repo) 522 if err != nil { 523 return nil, err 524 } 525 if repoCreds != nil { 526 repo.CopyCredentialsFrom(repoCreds) 527 } 528 } 529 err := s.testRepo(ctx, repo) 530 if err != nil { 531 return nil, err 532 } 533 return &repositorypkg.RepoResponse{}, nil 534 } 535 536 func (s *Server) testRepo(ctx context.Context, repo *appsv1.Repository) error { 537 conn, repoClient, err := s.repoClientset.NewRepoServerClient() 538 if err != nil { 539 return err 540 } 541 defer io.Close(conn) 542 543 _, err = repoClient.TestRepository(ctx, &apiclient.TestRepositoryRequest{ 544 Repo: repo, 545 }) 546 return err 547 } 548 549 func (s *Server) isRepoPermittedInProject(ctx context.Context, repo string, projName string) error { 550 proj, err := argo.GetAppProjectByName(projName, applisters.NewAppProjectLister(s.projLister.GetIndexer()), s.namespace, s.settings, s.db, ctx) 551 if err != nil { 552 return err 553 } 554 if !proj.IsSourcePermitted(appsv1.ApplicationSource{RepoURL: repo}) { 555 return status.Errorf(codes.PermissionDenied, "repository '%s' not permitted in project '%s'", repo, projName) 556 } 557 return nil 558 } 559 560 // isSourceInHistory checks if the supplied application source is either our current application 561 // source, or was something which we synced to previously. 562 func isSourceInHistory(app *v1alpha1.Application, source v1alpha1.ApplicationSource) bool { 563 appSource := app.Spec.GetSource() 564 if source.Equals(&appSource) { 565 return true 566 } 567 // Iterate history. When comparing items in our history, use the actual synced revision to 568 // compare with the supplied source.targetRevision in the request. This is because 569 // history[].source.targetRevision is ambiguous (e.g. HEAD), whereas 570 // history[].revision will contain the explicit SHA 571 for _, h := range app.Status.History { 572 h.Source.TargetRevision = h.Revision 573 if source.Equals(&h.Source) { 574 return true 575 } 576 } 577 return false 578 }