github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/argocd/reposerver/reposerver_test.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package reposerver 18 19 import ( 20 "context" 21 "os" 22 "os/exec" 23 "path" 24 "path/filepath" 25 "regexp" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testutil" 31 32 v1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 33 argorepo "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 34 "github.com/argoproj/argo-cd/v2/reposerver/cache" 35 "github.com/argoproj/argo-cd/v2/reposerver/metrics" 36 argosrv "github.com/argoproj/argo-cd/v2/reposerver/repository" 37 "github.com/argoproj/argo-cd/v2/util/argo" 38 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 39 "github.com/argoproj/argo-cd/v2/util/git" 40 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/config" 41 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository" 42 "github.com/google/go-cmp/cmp" 43 "github.com/google/go-cmp/cmp/cmpopts" 44 "google.golang.org/protobuf/testing/protocmp" 45 ) 46 47 // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false 48 type errMatcher struct { 49 msg string 50 } 51 52 func (e errMatcher) Error() string { 53 return e.msg 54 } 55 56 func (e errMatcher) Is(err error) bool { 57 return e.Error() == err.Error() 58 } 59 60 var createOneAppInDevelopment []repository.Transformer = []repository.Transformer{ 61 &repository.CreateEnvironment{ 62 Environment: "development", 63 Config: config.EnvironmentConfig{ 64 Upstream: &config.EnvironmentConfigUpstream{ 65 Latest: true, 66 }, 67 }, 68 }, 69 &repository.CreateApplicationVersion{ 70 Application: "app", 71 Manifests: map[string]string{ 72 "development": ` 73 api: v1 74 kind: ConfigMap 75 metadata: 76 name: something 77 namespace: something 78 data: 79 key: value 80 --- 81 api: v1 82 kind: ConfigMap 83 metadata: 84 name: somethingelse 85 namespace: somethingelse 86 data: 87 key: value 88 `, 89 }, 90 }, 91 } 92 93 var createOneAppInDevelopmentAndTesting []repository.Transformer = []repository.Transformer{ 94 &repository.CreateEnvironment{ 95 Environment: "development", 96 Config: config.EnvironmentConfig{ 97 Upstream: &config.EnvironmentConfigUpstream{ 98 Latest: true, 99 }, 100 ArgoCd: &config.EnvironmentConfigArgoCd{ 101 Destination: config.ArgoCdDestination{ 102 Server: "development", 103 }, 104 }, 105 }, 106 }, 107 &repository.CreateEnvironment{ 108 Environment: "testing", 109 Config: config.EnvironmentConfig{ 110 Upstream: &config.EnvironmentConfigUpstream{ 111 Latest: true, 112 }, 113 ArgoCd: &config.EnvironmentConfigArgoCd{ 114 Destination: config.ArgoCdDestination{ 115 Server: "testing", 116 }, 117 }, 118 }, 119 }, 120 &repository.CreateApplicationVersion{ 121 Application: "app", 122 Manifests: map[string]string{ 123 "development": ` 124 api: v1 125 kind: ConfigMap 126 metadata: 127 name: something 128 namespace: something 129 data: 130 key: value`, 131 "testing": ` 132 api: v1 133 kind: ConfigMap 134 metadata: 135 name: something 136 namespace: something 137 data: 138 key: value`, 139 }, 140 }, 141 } 142 143 func TestGenerateManifest(t *testing.T) { 144 tcs := []struct { 145 Name string 146 Setup []repository.Transformer 147 Request *argorepo.ManifestRequest 148 ExpectedResponse *argorepo.ManifestResponse 149 ExpectedError error 150 ExpectedArgoError *regexp.Regexp 151 }{ 152 { 153 Name: "generates a manifest for HEAD", 154 Setup: createOneAppInDevelopment, 155 Request: &argorepo.ManifestRequest{ 156 Revision: "HEAD", 157 Repo: &v1alpha1.Repository{ 158 Repo: "<the-repo-url>", 159 }, 160 ApplicationSource: &v1alpha1.ApplicationSource{ 161 Path: "environments/development/applications/app/manifests", 162 }, 163 }, 164 165 ExpectedResponse: &argorepo.ManifestResponse{ 166 Manifests: []string{ 167 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"something","namespace":"something"}}`, 168 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"somethingelse","namespace":"somethingelse"}}`, 169 }, 170 SourceType: "Directory", 171 }, 172 }, 173 { 174 Name: "generates a manifest for the branch itself", 175 Setup: createOneAppInDevelopment, 176 Request: &argorepo.ManifestRequest{ 177 Revision: "master", 178 Repo: &v1alpha1.Repository{ 179 Repo: "<the-repo-url>", 180 }, 181 ApplicationSource: &v1alpha1.ApplicationSource{ 182 Path: "environments/development/applications/app/manifests", 183 }, 184 }, 185 186 ExpectedResponse: &argorepo.ManifestResponse{ 187 Manifests: []string{ 188 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"something","namespace":"something"}}`, 189 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"somethingelse","namespace":"somethingelse"}}`, 190 }, 191 SourceType: "Directory", 192 }, 193 }, 194 { 195 Name: "supports the include filter", 196 Setup: createOneAppInDevelopmentAndTesting, 197 Request: &argorepo.ManifestRequest{ 198 Revision: "master", 199 Repo: &v1alpha1.Repository{ 200 Repo: "<the-repo-url>", 201 }, 202 ApplicationSource: &v1alpha1.ApplicationSource{ 203 Path: "argocd/v1alpha1", 204 Directory: &v1alpha1.ApplicationSourceDirectory{ 205 Include: "development.yaml", 206 }, 207 }, 208 }, 209 210 ExpectedResponse: &argorepo.ManifestResponse{ 211 Manifests: []string{ 212 `{"apiVersion":"argoproj.io/v1alpha1","kind":"AppProject","metadata":{"name":"development"},"spec":{"description":"development","destinations":[{"server":"development"}],"sourceRepos":["*"]}}`, 213 `{"apiVersion":"argoproj.io/v1alpha1","kind":"Application","metadata":{"annotations":{"argocd.argoproj.io/manifest-generate-paths":"/environments/development/applications/app/manifests","com.freiheit.kuberpult/application":"app","com.freiheit.kuberpult/environment":"development","com.freiheit.kuberpult/team":""},"finalizers":["resources-finalizer.argocd.argoproj.io"],"labels":{"com.freiheit.kuberpult/team":""},"name":"development-app"},"spec":{"destination":{"server":"development"},"project":"development","source":{"path":"environments/development/applications/app/manifests","repoURL":"<the-repo-url>","targetRevision":"master"},"syncPolicy":{"automated":{"allowEmpty":true,"prune":true,"selfHeal":true}}}}`, 214 }, 215 SourceType: "Directory", 216 }, 217 }, 218 { 219 Name: "generates a manifest for a fixed commit id", 220 Setup: createOneAppInDevelopment, 221 Request: &argorepo.ManifestRequest{ 222 Revision: "<last-commit-id>", 223 Repo: &v1alpha1.Repository{ 224 Repo: "<the-repo-url>", 225 }, 226 ApplicationSource: &v1alpha1.ApplicationSource{ 227 Path: "environments/development/applications/app/manifests", 228 }, 229 }, 230 231 ExpectedResponse: &argorepo.ManifestResponse{ 232 Manifests: []string{ 233 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"something","namespace":"something"}}`, 234 `{"api":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"name":"somethingelse","namespace":"somethingelse"}}`, 235 }, 236 SourceType: "Directory", 237 }, 238 }, 239 { 240 Name: "rejectes unknown refs", 241 Setup: createOneAppInDevelopment, 242 Request: &argorepo.ManifestRequest{ 243 Revision: "not-our-branch", 244 Repo: &v1alpha1.Repository{ 245 Repo: "<the-repo-url>", 246 }, 247 ApplicationSource: &v1alpha1.ApplicationSource{ 248 Path: "environments/development/applications/app/manifests", 249 }, 250 }, 251 252 ExpectedArgoError: regexp.MustCompile("\\AUnable to resolve 'not-our-branch' to a commit SHA\\z"), 253 ExpectedError: errMatcher{"rpc error: code = NotFound desc = unknown revision \"not-our-branch\", I only know \"HEAD\", \"master\" and commit hashes"}, 254 }, 255 { 256 Name: "rejectes unknown commit ids", 257 Setup: createOneAppInDevelopment, 258 Request: &argorepo.ManifestRequest{ 259 Revision: "b551320bc327abfabf9df32ee5a830f8ccb1e88d", 260 Repo: &v1alpha1.Repository{ 261 Repo: "<the-repo-url>", 262 }, 263 ApplicationSource: &v1alpha1.ApplicationSource{ 264 Path: "environments/development/applications/app/manifests", 265 }, 266 }, 267 268 // The error message from argo cd contains the output log of git which differs slightly with the git version. Therefore, we don't match on that. 269 ExpectedArgoError: regexp.MustCompile("\\A.*rpc error: code = Internal desc = Failed to checkout revision b551320bc327abfabf9df32ee5a830f8ccb1e88d:"), 270 ExpectedError: errMatcher{"rpc error: code = NotFound desc = unknown revision \"b551320bc327abfabf9df32ee5a830f8ccb1e88d\", I only know \"HEAD\", \"master\" and commit hashes"}, 271 }, 272 } 273 for _, tc := range tcs { 274 tc := tc 275 t.Run(tc.Name, func(t *testing.T) { 276 repo, cfg := testRepository(t) 277 err := repo.Apply(testutil.MakeTestContext(), tc.Setup...) 278 if err != nil { 279 t.Fatalf("failed setup: %s", err) 280 } 281 // These two values change every run: 282 if tc.Request.Repo.Repo == "<the-repo-url>" { 283 tc.Request.Repo.Repo = cfg.URL 284 } 285 if tc.Request.Revision == "<last-commit-id>" { 286 287 tc.Request.Revision = repo.State().Commit.Id().String() 288 } 289 if tc.ExpectedResponse != nil { 290 tc.ExpectedResponse.Revision = repo.State().Commit.Id().String() 291 mn := make([]string, 0) 292 for _, m := range tc.ExpectedResponse.Manifests { 293 mn = append(mn, strings.ReplaceAll(m, "<the-repo-url>", cfg.URL)) 294 } 295 tc.ExpectedResponse.Manifests = mn 296 } 297 298 srv := New(repo, cfg) 299 resp, err := srv.GenerateManifest(context.Background(), tc.Request) 300 if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" { 301 t.Fatalf("error mismatch (-want, +got):\n%s", diff) 302 } 303 if diff := cmp.Diff(tc.ExpectedResponse, resp, protocmp.Transform()); diff != "" { 304 t.Errorf("response mismatch (-want, +got):\n%s", diff) 305 } 306 307 asrv := testArgoServer(t) 308 aresp, err := asrv.GenerateManifest(context.Background(), tc.Request) 309 if tc.ExpectedError != nil { 310 if !tc.ExpectedArgoError.MatchString(err.Error()) { 311 t.Fatalf("got wrong error, expected to match %q but got %q", tc.ExpectedArgoError, err) 312 } 313 } else if err != nil { 314 t.Fatalf("unexpected error: %s", err.Error()) 315 } 316 if diff := cmp.Diff(tc.ExpectedResponse, aresp, protocmp.Transform()); diff != "" { 317 t.Errorf("response mismatch (-want, +got):\n%s", diff) 318 } 319 }) 320 } 321 } 322 323 func TestResolveRevision(t *testing.T) { 324 tcs := []struct { 325 Name string 326 Setup []repository.Transformer 327 Request *argorepo.ResolveRevisionRequest 328 ExpectedError error 329 ExpectedArgoError error 330 }{ 331 { 332 Name: "resolves HEAD", 333 Setup: createOneAppInDevelopment, 334 Request: &argorepo.ResolveRevisionRequest{ 335 App: &v1alpha1.Application{ 336 Spec: v1alpha1.ApplicationSpec{}, 337 }, 338 Repo: &v1alpha1.Repository{ 339 Repo: "<the-repo-url>", 340 }, 341 AmbiguousRevision: "HEAD", 342 }, 343 }, 344 { 345 Name: "resolves master", 346 Setup: createOneAppInDevelopment, 347 Request: &argorepo.ResolveRevisionRequest{ 348 App: &v1alpha1.Application{ 349 Spec: v1alpha1.ApplicationSpec{}, 350 }, 351 Repo: &v1alpha1.Repository{ 352 Repo: "<the-repo-url>", 353 }, 354 AmbiguousRevision: "master", 355 }, 356 }, 357 { 358 Name: "resolves a commit id", 359 Setup: createOneAppInDevelopment, 360 Request: &argorepo.ResolveRevisionRequest{ 361 App: &v1alpha1.Application{ 362 Spec: v1alpha1.ApplicationSpec{}, 363 }, 364 Repo: &v1alpha1.Repository{ 365 Repo: "<the-repo-url>", 366 }, 367 AmbiguousRevision: "<last-commit-id>", 368 }, 369 }, 370 { 371 Name: "rejects an unknown branch", 372 Setup: createOneAppInDevelopment, 373 Request: &argorepo.ResolveRevisionRequest{ 374 App: &v1alpha1.Application{ 375 Spec: v1alpha1.ApplicationSpec{}, 376 }, 377 Repo: &v1alpha1.Repository{ 378 Repo: "<the-repo-url>", 379 }, 380 AmbiguousRevision: "not-our-branch", 381 }, 382 383 ExpectedError: errMatcher{"rpc error: code = NotFound desc = unknown revision \"not-our-branch\", I only know \"HEAD\", \"master\" and commit hashes"}, 384 385 ExpectedArgoError: errMatcher{"Unable to resolve 'not-our-branch' to a commit SHA"}, 386 }, 387 { 388 Name: "accepts unknown commit ids", 389 Setup: createOneAppInDevelopment, 390 Request: &argorepo.ResolveRevisionRequest{ 391 App: &v1alpha1.Application{ 392 Spec: v1alpha1.ApplicationSpec{}, 393 }, 394 Repo: &v1alpha1.Repository{ 395 Repo: "<the-repo-url>", 396 }, 397 AmbiguousRevision: "b551320bc327abfabf9df32ee5a830f8ccb1e88d", 398 }, 399 }, 400 } 401 for _, tc := range tcs { 402 tc := tc 403 t.Run(tc.Name, func(t *testing.T) { 404 repo, cfg := testRepository(t) 405 err := repo.Apply(testutil.MakeTestContext(), tc.Setup...) 406 if err != nil { 407 t.Fatalf("failed setup: %s", err) 408 } 409 // These two values change every run: 410 if tc.Request.Repo.Repo == "<the-repo-url>" { 411 tc.Request.Repo.Repo = cfg.URL 412 } 413 if tc.Request.AmbiguousRevision == "<last-commit-id>" { 414 tc.Request.AmbiguousRevision = repo.State().Commit.Id().String() 415 } 416 asrv := testArgoServer(t) 417 aresp, err := asrv.ResolveRevision(context.Background(), tc.Request) 418 if diff := cmp.Diff(tc.ExpectedArgoError, err, cmpopts.EquateErrors()); diff != "" { 419 t.Errorf("error mismatch (-want, +got):\n%s", diff) 420 } 421 422 srv := New(repo, cfg) 423 resp, err := srv.ResolveRevision(context.Background(), tc.Request) 424 if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" { 425 t.Errorf("error mismatch (-want, +got):\n%s", diff) 426 } 427 428 if tc.ExpectedError == nil { 429 // We only need to check here if both are the same 430 if diff := cmp.Diff(aresp, resp, protocmp.Transform()); diff != "" { 431 t.Errorf("responses mismatch (-want, +got):\n%s", diff) 432 } 433 } 434 }) 435 } 436 } 437 438 func TestGetRevisionMetadata(t *testing.T) { 439 tcs := []struct { 440 Name string 441 }{ 442 { 443 Name: "returns a dummy", 444 }, 445 } 446 for _, tc := range tcs { 447 tc := tc 448 t.Run(tc.Name, func(t *testing.T) { 449 srv := (*reposerver)(nil) 450 req := argorepo.RepoServerRevisionMetadataRequest{} 451 _, err := srv.GetRevisionMetadata( 452 context.Background(), 453 &req, 454 ) 455 if err != nil { 456 t.Errorf("expected no error, but got %q", err) 457 } 458 }) 459 } 460 } 461 462 func testRepository(t *testing.T) (repository.Repository, repository.RepositoryConfig) { 463 dir := t.TempDir() 464 remoteDir := path.Join(dir, "remote") 465 localDir := path.Join(dir, "local") 466 cmd := exec.Command("git", "init", "--bare", remoteDir) 467 cmd.Run() 468 cfg := repository.RepositoryConfig{ 469 URL: "file://" + remoteDir, 470 Path: localDir, 471 Branch: "master", 472 } 473 repo, err := repository.New( 474 testutil.MakeTestContext(), 475 cfg, 476 ) 477 if err != nil { 478 t.Fatalf("expected no error, got '%e'", err) 479 } 480 return repo, cfg 481 } 482 483 func testArgoServer(t *testing.T) argorepo.RepoServerServiceServer { 484 argoRoot := t.TempDir() 485 t.Cleanup( 486 func() { 487 // argocd chmods all its directories in such a way that they can't be listed. 488 // this makes a lot of sense until you actually want to remove them cleanly. 489 os.Chmod(argoRoot, 0700) 490 dirs, _ := os.ReadDir(argoRoot) 491 for _, dir := range dirs { 492 os.Chmod(filepath.Join(argoRoot, dir.Name()), 0700) 493 } 494 }) 495 asrv := argosrv.NewService( 496 metrics.NewMetricsServer(), 497 cache.NewCache(cacheutil.NewCache(cacheutil.NewInMemoryCache(time.Hour)), time.Hour, time.Hour), 498 argosrv.RepoServerInitConstants{}, 499 argo.NewResourceTracking(), 500 &git.NoopCredsStore{}, 501 argoRoot, 502 ) 503 err := asrv.Init() 504 if err != nil { 505 t.Fatal(err) 506 } 507 return asrv 508 509 }