github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/testutil/pkgbuilder/builder.go (about) 1 // Copyright 2020 The kpt Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package pkgbuilder 16 17 import ( 18 "fmt" 19 "os" 20 "path/filepath" 21 "regexp" 22 "strconv" 23 "testing" 24 25 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 26 rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" 27 "github.com/stretchr/testify/assert" 28 "sigs.k8s.io/kustomize/kyaml/yaml" 29 ) 30 31 var ( 32 deploymentResourceManifest = ` 33 apiVersion: apps/v1 34 kind: Deployment 35 metadata: 36 namespace: myspace 37 name: mysql-deployment 38 spec: 39 replicas: 3 40 foo: bar 41 template: 42 spec: 43 containers: 44 - name: mysql 45 image: mysql:1.7.9 46 ` 47 48 configMapResourceManifest = ` 49 apiVersion: v1 50 kind: ConfigMap 51 metadata: 52 name: configmap 53 data: 54 foo: bar 55 ` 56 57 secretResourceManifest = ` 58 apiVersion: v1 59 kind: Secret 60 metadata: 61 name: secret 62 type: Opaque 63 data: 64 foo: bar 65 ` 66 ) 67 68 var ( 69 DeploymentResource = "deployment" 70 ConfigMapResource = "configmap" 71 SecretResource = "secret" 72 resources = map[string]resourceInfo{ 73 DeploymentResource: { 74 filename: "deployment.yaml", 75 manifest: deploymentResourceManifest, 76 }, 77 ConfigMapResource: { 78 filename: "configmap.yaml", 79 manifest: configMapResourceManifest, 80 }, 81 SecretResource: { 82 filename: "secret.yaml", 83 manifest: secretResourceManifest, 84 }, 85 } 86 ) 87 88 // Pkg represents a package that can be created on the file system 89 // by using the Build function 90 type pkg struct { 91 Kptfile *Kptfile 92 93 RGFile *RGFile 94 95 resources []resourceInfoWithMutators 96 97 files map[string]string 98 99 subPkgs []*SubPkg 100 } 101 102 // WithRGFile configures the current package to have a resourcegroup file. 103 func (rp *RootPkg) WithRGFile(rg *RGFile) *RootPkg { 104 rp.pkg.RGFile = rg 105 return rp 106 } 107 108 // withKptfile configures the current package to have a Kptfile. Only 109 // zero or one Kptfiles are accepted. 110 func (p *pkg) withKptfile(kf ...*Kptfile) { 111 if len(kf) > 1 { 112 panic("only 0 or 1 Kptfiles are allowed") 113 } 114 if len(kf) == 0 { 115 p.Kptfile = NewKptfile() 116 } else { 117 p.Kptfile = kf[0] 118 } 119 } 120 121 // withResource configures the package to include the provided resource 122 func (p *pkg) withResource(resourceName string, mutators ...yaml.Filter) { 123 resourceInfo, ok := resources[resourceName] 124 if !ok { 125 panic(fmt.Errorf("unknown resource %s", resourceName)) 126 } 127 p.resources = append(p.resources, resourceInfoWithMutators{ 128 resourceInfo: resourceInfo, 129 mutators: mutators, 130 }) 131 } 132 133 // withRawResource configures the package to include the provided resource 134 func (p *pkg) withRawResource(resourceName, manifest string, mutators ...yaml.Filter) { 135 p.resources = append(p.resources, resourceInfoWithMutators{ 136 resourceInfo: resourceInfo{ 137 filename: resourceName, 138 manifest: manifest, 139 }, 140 mutators: mutators, 141 }) 142 } 143 144 // withFile configures the package to contain a file with the provided name 145 // and the given content. 146 func (p *pkg) withFile(name, content string) { 147 p.files[name] = content 148 } 149 150 // withSubPackages adds the provided packages as subpackages to the current 151 // package 152 func (p *pkg) withSubPackages(ps ...*SubPkg) { 153 p.subPkgs = append(p.subPkgs, ps...) 154 } 155 156 // allReferencedRepos traverses the root package and all subpackages to 157 // capture all references to other repos. 158 func (p *pkg) allReferencedRepos(collector map[string]bool) { 159 for i := range p.subPkgs { 160 p.subPkgs[i].pkg.allReferencedRepos(collector) 161 } 162 if p.Kptfile != nil && p.Kptfile.Upstream != nil { 163 collector[p.Kptfile.Upstream.RepoRef] = true 164 } 165 } 166 167 // RootPkg is a package without any parent package. 168 type RootPkg struct { 169 pkg *pkg 170 } 171 172 // NewRootPkg creates a new package for testing. 173 func NewRootPkg() *RootPkg { 174 return &RootPkg{ 175 pkg: &pkg{ 176 files: make(map[string]string), 177 }, 178 } 179 } 180 181 // WithKptfile configures the current package to have a Kptfile. Only 182 // zero or one Kptfiles are accepted. 183 func (rp *RootPkg) WithKptfile(kf ...*Kptfile) *RootPkg { 184 rp.pkg.withKptfile(kf...) 185 return rp 186 } 187 188 // HasKptfile tells whether the package contains a Kptfile. 189 func (rp *RootPkg) HasKptfile() bool { 190 return rp.pkg.Kptfile != nil 191 } 192 193 // AllReferencedRepos returns the name of all remote subpackages referenced 194 // in the package (including any local subpackages). 195 func (rp *RootPkg) AllReferencedRepos() []string { 196 repoNameMap := make(map[string]bool) 197 rp.pkg.allReferencedRepos(repoNameMap) 198 199 var repoNames []string 200 for n := range repoNameMap { 201 repoNames = append(repoNames, n) 202 } 203 return repoNames 204 } 205 206 // WithResource configures the package to include the provided resource 207 func (rp *RootPkg) WithResource(resourceName string, mutators ...yaml.Filter) *RootPkg { 208 rp.pkg.withResource(resourceName, mutators...) 209 return rp 210 } 211 212 // WithRawResource configures the package to include the provided resource 213 func (rp *RootPkg) WithRawResource(resourceName, manifest string, mutators ...yaml.Filter) *RootPkg { 214 rp.pkg.withRawResource(resourceName, manifest, mutators...) 215 return rp 216 } 217 218 // WithFile configures the package to contain a file with the provided name 219 // and the given content. 220 func (rp *RootPkg) WithFile(name, content string) *RootPkg { 221 rp.pkg.withFile(name, content) 222 return rp 223 } 224 225 // WithSubPackages adds the provided packages as subpackages to the current 226 // package 227 func (rp *RootPkg) WithSubPackages(ps ...*SubPkg) *RootPkg { 228 rp.pkg.withSubPackages(ps...) 229 return rp 230 } 231 232 // Build outputs the current data structure as a set of (nested) package 233 // in the provided path. 234 func (rp *RootPkg) Build(path string, pkgName string, reposInfo ReposInfo) error { 235 pkgPath := filepath.Join(path, pkgName) 236 err := os.Mkdir(pkgPath, 0700) 237 if err != nil { 238 return err 239 } 240 if rp == nil { 241 return nil 242 } 243 err = buildPkg(pkgPath, rp.pkg, pkgName, reposInfo) 244 if err != nil { 245 return err 246 } 247 for i := range rp.pkg.subPkgs { 248 subPkg := rp.pkg.subPkgs[i] 249 err := buildSubPkg(pkgPath, subPkg, reposInfo) 250 if err != nil { 251 return err 252 } 253 } 254 return nil 255 } 256 257 // SubPkg is a subpackage, so it is contained inside another package. The 258 // name sets both the name of the directory in which the package is stored 259 // and the metadata.name field in the Kptfile (if there is one). 260 type SubPkg struct { 261 pkg *pkg 262 263 Name string 264 } 265 266 // NewSubPkg returns a new subpackage for testing. 267 func NewSubPkg(name string) *SubPkg { 268 return &SubPkg{ 269 pkg: &pkg{ 270 files: make(map[string]string), 271 }, 272 Name: name, 273 } 274 } 275 276 // WithKptfile configures the current package to have a Kptfile. Only 277 // zero or one Kptfiles are accepted. 278 func (sp *SubPkg) WithKptfile(kf ...*Kptfile) *SubPkg { 279 sp.pkg.withKptfile(kf...) 280 return sp 281 } 282 283 // WithResource configures the package to include the provided resource 284 func (sp *SubPkg) WithResource(resourceName string, mutators ...yaml.Filter) *SubPkg { 285 sp.pkg.withResource(resourceName, mutators...) 286 return sp 287 } 288 289 // WithRawResource configures the package to include the provided resource 290 func (sp *SubPkg) WithRawResource(resourceName, manifest string, mutators ...yaml.Filter) *SubPkg { 291 sp.pkg.withRawResource(resourceName, manifest, mutators...) 292 return sp 293 } 294 295 // WithFile configures the package to contain a file with the provided name 296 // and the given content. 297 func (sp *SubPkg) WithFile(name, content string) *SubPkg { 298 sp.pkg.withFile(name, content) 299 return sp 300 } 301 302 // WithSubPackages adds the provided packages as subpackages to the current 303 // package 304 func (sp *SubPkg) WithSubPackages(ps ...*SubPkg) *SubPkg { 305 sp.pkg.withSubPackages(ps...) 306 return sp 307 } 308 309 // RGFile represents a minimal resourcegroup. 310 type RGFile struct { 311 Name, Namespace, ID string 312 } 313 314 func NewRGFile() *RGFile { 315 return &RGFile{} 316 } 317 318 func (rg *RGFile) WithInventory(inv Inventory) *RGFile { 319 rg.Name = inv.Name 320 rg.Namespace = inv.Namespace 321 rg.ID = inv.ID 322 return rg 323 } 324 325 // Kptfile represents the Kptfile of a package. 326 type Kptfile struct { 327 Upstream *Upstream 328 UpstreamLock *UpstreamLock 329 Pipeline *Pipeline 330 Inventory *Inventory 331 } 332 333 func NewKptfile() *Kptfile { 334 return &Kptfile{} 335 } 336 337 // WithUpstream adds information about the upstream information to the Kptfile. 338 // The upstream section of the Kptfile is only added if this information is 339 // provided. 340 func (k *Kptfile) WithUpstream(repo, dir, ref, strategy string) *Kptfile { 341 k.Upstream = &Upstream{ 342 Repo: repo, 343 Dir: dir, 344 Ref: ref, 345 Strategy: strategy, 346 } 347 return k 348 } 349 350 // WithUpstreamRef adds information about the upstream information to the 351 // Kptfile. Unlike WithUpstream, this function allows providing just a 352 // reference to the repo rather than the actual path. The reference will 353 // be resolved to an actual path when the package is written to disk. 354 func (k *Kptfile) WithUpstreamRef(repoRef, dir, ref, strategy string) *Kptfile { 355 k.Upstream = &Upstream{ 356 RepoRef: repoRef, 357 Dir: dir, 358 Ref: ref, 359 Strategy: strategy, 360 } 361 return k 362 } 363 364 // WithUpstreamLock adds upstreamLock information to the Kptfile. If no 365 // upstreamLock information is provided, 366 func (k *Kptfile) WithUpstreamLock(repo, dir, ref, commit string) *Kptfile { 367 k.UpstreamLock = &UpstreamLock{ 368 Repo: repo, 369 Dir: dir, 370 Ref: ref, 371 Commit: commit, 372 } 373 return k 374 } 375 376 // WithUpstreamLockRef adds upstreamLock information to the Kptfile. But unlike 377 // WithUpstreamLock, this function takes a the name to a repo and will resolve 378 // the actual path when expanding the package. The commit SHA is also not provided, 379 // but rather the index of a commit that will be resolved when expanding the 380 // package. 381 func (k *Kptfile) WithUpstreamLockRef(repoRef, dir, ref string, index int) *Kptfile { 382 k.UpstreamLock = &UpstreamLock{ 383 RepoRef: repoRef, 384 Dir: dir, 385 Ref: ref, 386 Index: index, 387 } 388 return k 389 } 390 391 type Upstream struct { 392 Repo string 393 RepoRef string 394 Dir string 395 Ref string 396 Strategy string 397 } 398 399 type UpstreamLock struct { 400 Repo string 401 RepoRef string 402 Dir string 403 Ref string 404 Index int 405 Commit string 406 } 407 408 func (k *Kptfile) WithInventory(inv Inventory) *Kptfile { 409 k.Inventory = &inv 410 return k 411 } 412 413 type Inventory struct { 414 Name string 415 Namespace string 416 ID string 417 } 418 419 func (k *Kptfile) WithPipeline(functions ...Function) *Kptfile { 420 k.Pipeline = &Pipeline{ 421 Functions: functions, 422 } 423 return k 424 } 425 426 type Pipeline struct { 427 Functions []Function 428 } 429 430 func NewFunction(image string) Function { 431 return Function{ 432 Image: image, 433 } 434 } 435 436 type Function struct { 437 Image string 438 ConfigPath string 439 } 440 441 func (f Function) WithConfigPath(configPath string) Function { 442 f.ConfigPath = configPath 443 return f 444 } 445 446 // RemoteSubpackage contains information about remote subpackages that should 447 // be listed in the Kptfile. 448 type RemoteSubpackage struct { 449 // Name is the name of the remote subpackage. It will be used as the value 450 // for the LocalDir property and also used to resolve the Repo path from 451 // other defined repos. 452 RepoRef string 453 Repo string 454 Directory string 455 Ref string 456 Strategy string 457 LocalDir string 458 } 459 460 type resourceInfo struct { 461 filename string 462 manifest string 463 } 464 465 type resourceInfoWithMutators struct { 466 resourceInfo resourceInfo 467 mutators []yaml.Filter 468 } 469 470 func buildSubPkg(path string, pkg *SubPkg, reposInfo ReposInfo) error { 471 pkgPath := filepath.Join(path, pkg.Name) 472 err := os.Mkdir(pkgPath, 0700) 473 if err != nil { 474 return err 475 } 476 err = buildPkg(pkgPath, pkg.pkg, pkg.Name, reposInfo) 477 if err != nil { 478 return err 479 } 480 for i := range pkg.pkg.subPkgs { 481 subPkg := pkg.pkg.subPkgs[i] 482 err := buildSubPkg(pkgPath, subPkg, reposInfo) 483 if err != nil { 484 return err 485 } 486 } 487 return nil 488 } 489 490 func buildPkg(pkgPath string, pkg *pkg, pkgName string, reposInfo ReposInfo) error { 491 if pkg.Kptfile != nil { 492 content := buildKptfile(pkg, pkgName, reposInfo) 493 494 err := os.WriteFile(filepath.Join(pkgPath, kptfilev1.KptFileName), 495 []byte(content), 0600) 496 if err != nil { 497 return err 498 } 499 } 500 501 if pkg.RGFile != nil { 502 content := buildRGFile(pkg) 503 504 err := os.WriteFile(filepath.Join(pkgPath, rgfilev1alpha1.RGFileName), 505 []byte(content), 0600) 506 if err != nil { 507 return err 508 } 509 } 510 511 for _, ri := range pkg.resources { 512 m := ri.resourceInfo.manifest 513 r := yaml.MustParse(m) 514 515 for _, m := range ri.mutators { 516 if err := r.PipeE(m); err != nil { 517 return err 518 } 519 } 520 521 filePath := filepath.Join(pkgPath, ri.resourceInfo.filename) 522 err := os.WriteFile(filePath, []byte(r.MustString()), 0600) 523 if err != nil { 524 return err 525 } 526 } 527 528 for name, content := range pkg.files { 529 filePath := filepath.Join(pkgPath, name) 530 _, err := os.Stat(filePath) 531 if err != nil && !os.IsNotExist(err) { 532 return err 533 } 534 if !os.IsNotExist(err) { 535 return fmt.Errorf("file %s already exists", name) 536 } 537 err = os.WriteFile(filePath, []byte(content), 0600) 538 if err != nil { 539 return err 540 } 541 } 542 return nil 543 } 544 545 // buildRGFile creates a ResourceGroup inventory file. 546 func buildRGFile(pkg *pkg) string { 547 tmp := rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta} 548 tmp.ObjectMeta.Name = pkg.RGFile.Name 549 tmp.ObjectMeta.Namespace = pkg.RGFile.Namespace 550 if pkg.RGFile.ID != "" { 551 tmp.ObjectMeta.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: pkg.RGFile.ID} 552 } 553 554 b, err := yaml.MarshalWithOptions(tmp, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) 555 if err != nil { 556 panic(err) 557 } 558 559 return string(b) 560 } 561 562 type ReposInfo interface { 563 ResolveRepoRef(repoRef string) (string, bool) 564 ResolveCommitIndex(repoRef string, index int) (string, bool) 565 } 566 567 func buildKptfile(pkg *pkg, pkgName string, reposInfo ReposInfo) string { 568 if pkg.Kptfile.Upstream != nil && len(pkg.Kptfile.Upstream.RepoRef) > 0 { 569 repoRef := pkg.Kptfile.Upstream.RepoRef 570 ref := pkg.Kptfile.Upstream.Ref 571 pkg.Kptfile.Upstream.Repo = resolveRepoRef(repoRef, reposInfo) 572 573 if newRef, ok := resolveCommitRef(repoRef, ref, reposInfo); ok { 574 pkg.Kptfile.Upstream.Ref = newRef 575 } 576 } 577 if pkg.Kptfile.UpstreamLock != nil && len(pkg.Kptfile.UpstreamLock.RepoRef) > 0 { 578 repoRef := pkg.Kptfile.UpstreamLock.RepoRef 579 ref := pkg.Kptfile.UpstreamLock.Ref 580 pkg.Kptfile.UpstreamLock.Repo = resolveRepoRef(repoRef, reposInfo) 581 582 index := pkg.Kptfile.UpstreamLock.Index 583 pkg.Kptfile.UpstreamLock.Commit = resolveCommitIndex(repoRef, index, reposInfo) 584 585 if newRef, ok := resolveCommitRef(repoRef, ref, reposInfo); ok { 586 pkg.Kptfile.UpstreamLock.Ref = newRef 587 } 588 } 589 590 kptfile := &kptfilev1.KptFile{} 591 kptfile.APIVersion, kptfile.Kind = kptfilev1.KptFileGVK().ToAPIVersionAndKind() 592 kptfile.ObjectMeta.Name = pkgName 593 if pkg.Kptfile.Upstream != nil { 594 kptfile.Upstream = &kptfilev1.Upstream{ 595 Type: "git", 596 Git: &kptfilev1.Git{ 597 Repo: pkg.Kptfile.Upstream.Repo, 598 Directory: pkg.Kptfile.Upstream.Dir, 599 Ref: pkg.Kptfile.Upstream.Ref, 600 }, 601 UpdateStrategy: kptfilev1.UpdateStrategyType(pkg.Kptfile.Upstream.Strategy), 602 } 603 } 604 if pkg.Kptfile.UpstreamLock != nil { 605 kptfile.UpstreamLock = &kptfilev1.UpstreamLock{ 606 Type: "git", 607 Git: &kptfilev1.GitLock{ 608 Repo: pkg.Kptfile.UpstreamLock.Repo, 609 Directory: pkg.Kptfile.UpstreamLock.Dir, 610 Ref: pkg.Kptfile.UpstreamLock.Ref, 611 Commit: pkg.Kptfile.UpstreamLock.Commit, 612 }, 613 } 614 } 615 if pkg.Kptfile.Pipeline != nil { 616 kptfile.Pipeline = &kptfilev1.Pipeline{} 617 for _, fn := range pkg.Kptfile.Pipeline.Functions { 618 mutator := kptfilev1.Function{ 619 Image: fn.Image, 620 } 621 if fn.ConfigPath != "" { 622 mutator.ConfigPath = fn.ConfigPath 623 } 624 kptfile.Pipeline.Mutators = append(kptfile.Pipeline.Mutators, mutator) 625 } 626 } 627 628 if inventory := pkg.Kptfile.Inventory; inventory != nil { 629 kptfile.Inventory = &kptfilev1.Inventory{} 630 if inventory.Name != "" { 631 kptfile.Inventory.Name = inventory.Name 632 } 633 if inventory.Namespace != "" { 634 kptfile.Inventory.Namespace = inventory.Namespace 635 } 636 if inventory.ID != "" { 637 kptfile.Inventory.InventoryID = inventory.ID 638 } 639 } 640 b, err := yaml.Marshal(kptfile) 641 if err != nil { 642 panic(err) 643 } 644 return string(b) 645 } 646 647 // resolveRepoRef looks up the repo path for a repo from the reposInfo 648 // object based on the provided reference. 649 func resolveRepoRef(repoRef string, reposInfo ReposInfo) string { 650 repo, found := reposInfo.ResolveRepoRef(repoRef) 651 if !found { 652 panic(fmt.Errorf("path for package %s not found", repoRef)) 653 } 654 return repo 655 } 656 657 // resolveCommitIndex looks up the commit SHA for a specific commit in a repo. 658 // It looks up the repo based on the provided repoRef and returns the commit for 659 // the commit with the provided index. 660 func resolveCommitIndex(repoRef string, index int, reposInfo ReposInfo) string { 661 commit, found := reposInfo.ResolveCommitIndex(repoRef, index) 662 if !found { 663 panic(fmt.Errorf("can't find commit for index %d in repo %s", index, repoRef)) 664 } 665 return commit 666 } 667 668 // resolveCommitRef looks up the commit SHA for a commit with the index given 669 // through a special string format as the ref. If the string value follows the 670 // correct format, the commit will looked up from the repo given by the RepoRef 671 // and returned with the second value being true. If the ref string does not 672 // follow the correct format, the second return value will be false. 673 func resolveCommitRef(repoRef, ref string, reposInfo ReposInfo) (string, bool) { 674 re := regexp.MustCompile(`^COMMIT-INDEX:([0-9]+)$`) 675 matches := re.FindStringSubmatch(ref) 676 if len(matches) != 2 { 677 return "", false 678 } 679 index, err := strconv.Atoi(matches[1]) 680 if err != nil { 681 return "", false 682 } 683 return resolveCommitIndex(repoRef, index, reposInfo), true 684 } 685 686 // ExpandPkg writes the provided package to disk. The name of the root package 687 // will just be set to "base". 688 func (rp *RootPkg) ExpandPkg(t *testing.T, reposInfo ReposInfo) string { 689 return rp.ExpandPkgWithName(t, "base", reposInfo) 690 } 691 692 // ExpandPkgWithName writes the provided package to disk and uses the given 693 // rootName to set the value of the package directory and the metadata.name 694 // field of the root package. 695 func (rp *RootPkg) ExpandPkgWithName(t *testing.T, rootName string, reposInfo ReposInfo) string { 696 dir, err := os.MkdirTemp("", "test-kpt-builder-") 697 if !assert.NoError(t, err) { 698 t.FailNow() 699 } 700 err = rp.Build(dir, rootName, reposInfo) 701 if !assert.NoError(t, err) { 702 t.FailNow() 703 } 704 return filepath.Join(dir, rootName) 705 }