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