diff --git a/documentation/content/en/book/01-getting-started/_index.md b/documentation/content/en/book/01-getting-started/_index.md index f86aee96c4..b74030fb2b 100644 --- a/documentation/content/en/book/01-getting-started/_index.md +++ b/documentation/content/en/book/01-getting-started/_index.md @@ -75,7 +75,7 @@ Next, use the following command to view the content of the package: ```shell kpt pkg tree -Package "nginx" +Package "nginx" (independent) ├── [Kptfile] Kptfile nginx ├── [deployment.yaml] Deployment my-nginx └── [svc.yaml] Service my-nginx-svc diff --git a/documentation/content/en/book/02-concepts/_index.md b/documentation/content/en/book/02-concepts/_index.md index e58cf33e97..61546aabd3 100644 --- a/documentation/content/en/book/02-concepts/_index.md +++ b/documentation/content/en/book/02-concepts/_index.md @@ -144,17 +144,17 @@ View the package hierarchy using the `tree` command: ```shell kpt pkg tree wordpress/ -Package "wordpress" +Package "wordpress" (independent) ├── [Kptfile] Kptfile wordpress ├── [service.yaml] Service wordpress ├── deployment │   ├── [deployment.yaml] Deployment wordpress │   └── [volume.yaml] PersistentVolumeClaim wp-pv-claim -└── Package "mysql" +└── Package "mysql" (dependent) ├── [Kptfile] Kptfile mysql ├── [deployment.yaml] PersistentVolumeClaim mysql-pv-claim ├── [deployment.yaml] Deployment wordpress-mysql - └── [deployment.yaml] Service wordpress-mysql + └── [service.yaml] Service wordpress-mysql ``` This _package hierarchy_ contains two packages: diff --git a/documentation/content/en/book/03-packages/_index.md b/documentation/content/en/book/03-packages/_index.md index 72c1639cde..4cd78b8a23 100644 --- a/documentation/content/en/book/03-packages/_index.md +++ b/documentation/content/en/book/03-packages/_index.md @@ -138,17 +138,17 @@ kpt also provides the `tree` command which is handy for quickly viewing the pack ```shell kpt pkg tree wordpress/ -Package "wordpress" +Package "wordpress" (independent) ├── [Kptfile] Kptfile wordpress ├── [service.yaml] Service wordpress ├── deployment │   ├── [deployment.yaml] Deployment wordpress │   └── [volume.yaml] PersistentVolumeClaim wp-pv-claim -└── Package "mysql" +└── Package "mysql" (dependent) ├── [Kptfile] Kptfile mysql ├── [deployment.yaml] PersistentVolumeClaim mysql-pv-claim ├── [deployment.yaml] Deployment wordpress-mysql - └── [deployment.yaml] Service wordpress-mysql + └── [service.yaml] Service wordpress-mysql ``` See the [tree command reference](../../reference/cli/pkg/tree/) for usage. @@ -336,17 +336,17 @@ Let us revisit the `wordpress` package and see how it was composed in the first ```shell kpt pkg tree wordpress/ -Package "wordpress" +Package "wordpress" (independent) ├── [Kptfile] Kptfile wordpress ├── [service.yaml] Service wordpress ├── deployment │   ├── [deployment.yaml] Deployment wordpress │   └── [volume.yaml] PersistentVolumeClaim wp-pv-claim -└── Package "mysql" +└── Package "mysql" (dependent) ├── [Kptfile] Kptfile mysql ├── [deployment.yaml] PersistentVolumeClaim mysql-pv-claim ├── [deployment.yaml] Deployment wordpress-mysql - └── [deployment.yaml] Service wordpress-mysql + └── [service.yaml] Service wordpress-mysql ``` First, delete the `mysql` subpackage. Deleting a subpackage is done by deleting the subdirectory, as follows: diff --git a/documentation/content/en/guides/namespace-provisioning-cli.md b/documentation/content/en/guides/namespace-provisioning-cli.md index e89793133d..37919bcb96 100644 --- a/documentation/content/en/guides/namespace-provisioning-cli.md +++ b/documentation/content/en/guides/namespace-provisioning-cli.md @@ -393,7 +393,7 @@ Successfully executed 2 function(s) in 1 package(s). # examine the output of backend package $ kpt pkg tree backend -Package "backend" +Package "backend" (independent) ├── [Kptfile]  Kptfile backend ├── [namespace.yaml]  Namespace backend ├── [package-context.yaml]  ConfigMap kptfile.kpt.dev diff --git a/documentation/content/en/reference/cli/pkg/tree/_index.md b/documentation/content/en/reference/cli/pkg/tree/_index.md index d9a974ada0..82a083225d 100644 --- a/documentation/content/en/reference/cli/pkg/tree/_index.md +++ b/documentation/content/en/reference/cli/pkg/tree/_index.md @@ -13,6 +13,16 @@ description: | `tree` displays resources, files and packages in a tree structure. +Each package (directory containing a `Kptfile`) is annotated with its type: + +- **(independent)**: The package has an `upstream` section in its `Kptfile`, + meaning it tracks its own upstream source and can be updated independently. +- **(dependent)**: A subpackage that does not have an `upstream` section in its + `Kptfile`. Its upstream is implicitly inherited from its parent package. + +A root package created locally (e.g. via `kpt pkg init`) has no annotation since +it is neither independent nor dependent — it simply has no upstream source. + ### Synopsis @@ -41,4 +51,19 @@ DIR: $ kpt pkg tree ``` +```shell +# Example output showing independent and dependent packages: +$ kpt pkg tree wordpress/ +Package "wordpress" (independent) +├── [Kptfile] Kptfile wordpress +├── [service.yaml] Service wordpress +├── deployment +│ ├── [deployment.yaml] Deployment wordpress +│ └── [volume.yaml] PersistentVolumeClaim wp-pv-claim +└── Package "mysql" (dependent) + ├── [Kptfile] Kptfile mysql + ├── [deployment.yaml] Deployment wordpress-mysql + └── [service.yaml] Service wordpress-mysql +``` + diff --git a/internal/docs/generated/pkgdocs/docs.go b/internal/docs/generated/pkgdocs/docs.go index 0921f9cc82..c6978fcfc8 100644 --- a/internal/docs/generated/pkgdocs/docs.go +++ b/internal/docs/generated/pkgdocs/docs.go @@ -245,6 +245,19 @@ Args: var TreeExamples = ` # Show resources in the current directory. $ kpt pkg tree + + # Example output showing independent and dependent packages: + $ kpt pkg tree wordpress/ + Package "wordpress" (independent) + ├── [Kptfile] Kptfile wordpress + ├── [service.yaml] Service wordpress + ├── deployment + │ ├── [deployment.yaml] Deployment wordpress + │ └── [volume.yaml] PersistentVolumeClaim wp-pv-claim + └── Package "mysql" (dependent) + ├── [Kptfile] Kptfile mysql + ├── [deployment.yaml] Deployment wordpress-mysql + └── [service.yaml] Service wordpress-mysql ` var UpdateShort = `Apply upstream package updates.` diff --git a/thirdparty/cmdconfig/commands/cmdtree/cmdtree_test.go b/thirdparty/cmdconfig/commands/cmdtree/cmdtree_test.go index ea6079ac71..869ef10e26 100644 --- a/thirdparty/cmdconfig/commands/cmdtree/cmdtree_test.go +++ b/thirdparty/cmdconfig/commands/cmdtree/cmdtree_test.go @@ -325,7 +325,7 @@ openAPI: ├── [f1.yaml] Abstraction foo ├── [f1.yaml] Deployment foo ├── [f1.yaml] Service foo -└── Package "subpkg" +└── Package "subpkg" (dependent) ├── [Kptfile] Kptfile subpkg └── [f2.yaml] Deployment bar `, filepath.Base(d)), b.String()) { @@ -405,7 +405,7 @@ metadata: if !assert.Equal(t, `Package "Mainpkg" ├── [Kptfile] Kptfile Mainpkg ├── [f1.yaml] Deployment foo -└── Package "Subpkg" +└── Package "Subpkg" (dependent) ├── [Kptfile] Kptfile Subpkg └── [f2.yaml] Deployment bar `, b.String()) { @@ -413,6 +413,187 @@ metadata: } } +func TestTreeCommand_independentAndDependent(t *testing.T) { + d, err := os.MkdirTemp("", "tree-test") + defer os.RemoveAll(d) + if !assert.NoError(t, err) { + t.FailNow() + } + + err = os.MkdirAll(filepath.Join(d, "subpkg"), 0700) + if !assert.NoError(t, err) { + t.FailNow() + } + + err = os.WriteFile(filepath.Join(d, "f1.yaml"), []byte(`kind: Deployment +metadata: + name: foo +spec: + replicas: 1 +`), 0600) + if !assert.NoError(t, err) { + return + } + err = os.WriteFile(filepath.Join(d, "subpkg", "f2.yaml"), []byte(`kind: Deployment +metadata: + name: bar +spec: + replicas: 3 +`), 0600) + if !assert.NoError(t, err) { + return + } + + // Root package is independent (has upstream) + err = os.WriteFile(filepath.Join(d, "Kptfile"), []byte(`apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: mainpkg +upstream: + type: git + git: + repo: https://github.com/example/repo + directory: / + ref: main + updateStrategy: resource-merge +`), 0600) + if !assert.NoError(t, err) { + return + } + // Subpackage is dependent (no upstream) + err = os.WriteFile(filepath.Join(d, "subpkg", "Kptfile"), []byte(`apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: subpkg +`), 0600) + if !assert.NoError(t, err) { + return + } + + b := &bytes.Buffer{} + r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "") + r.Command.SetArgs([]string{d}) + r.Command.SetOut(b) + if !assert.NoError(t, r.Command.Execute()) { + return + } + + if !assert.Equal(t, fmt.Sprintf(`Package %q (independent) +├── [Kptfile] Kptfile mainpkg +├── [f1.yaml] Deployment foo +└── Package "subpkg" (dependent) + ├── [Kptfile] Kptfile subpkg + └── [f2.yaml] Deployment bar +`, filepath.Base(d)), b.String()) { + return + } +} + +func TestTreeCommand_nestedSubpackages(t *testing.T) { + d, err := os.MkdirTemp("", "tree-test") + defer os.RemoveAll(d) + if !assert.NoError(t, err) { + t.FailNow() + } + + err = os.MkdirAll(filepath.Join(d, "subpkg-a", "subpkg-b"), 0700) + if !assert.NoError(t, err) { + t.FailNow() + } + + err = os.WriteFile(filepath.Join(d, "f1.yaml"), []byte(`kind: Deployment +metadata: + name: root-deploy +spec: + replicas: 1 +`), 0600) + if !assert.NoError(t, err) { + return + } + err = os.WriteFile(filepath.Join(d, "subpkg-a", "f2.yaml"), []byte(`kind: Service +metadata: + name: my-service +spec: + selector: + app: test +`), 0600) + if !assert.NoError(t, err) { + return + } + err = os.WriteFile(filepath.Join(d, "subpkg-a", "subpkg-b", "f3.yaml"), []byte(`kind: ConfigMap +metadata: + name: config +data: + key: value +`), 0600) + if !assert.NoError(t, err) { + return + } + + // Root package is independent + err = os.WriteFile(filepath.Join(d, "Kptfile"), []byte(`apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: root +upstream: + type: git + git: + repo: https://github.com/example/repo + directory: / + ref: main + updateStrategy: resource-merge +`), 0600) + if !assert.NoError(t, err) { + return + } + // subpkg-a is also independent (different upstream) + err = os.WriteFile(filepath.Join(d, "subpkg-a", "Kptfile"), []byte(`apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: subpkg-a +upstream: + type: git + git: + repo: https://github.com/example/other-repo + directory: /networking + ref: v2.0 + updateStrategy: resource-merge +`), 0600) + if !assert.NoError(t, err) { + return + } + // subpkg-b is dependent (no upstream) + err = os.WriteFile(filepath.Join(d, "subpkg-a", "subpkg-b", "Kptfile"), []byte(`apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: subpkg-b +`), 0600) + if !assert.NoError(t, err) { + return + } + + b := &bytes.Buffer{} + r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "") + r.Command.SetArgs([]string{d}) + r.Command.SetOut(b) + if !assert.NoError(t, r.Command.Execute()) { + return + } + + if !assert.Equal(t, fmt.Sprintf(`Package %q (independent) +├── [Kptfile] Kptfile root +├── [f1.yaml] Deployment root-deploy +└── Package "subpkg-a" (independent) + ├── [Kptfile] Kptfile subpkg-a + ├── [f2.yaml] Service my-service + └── Package "subpkg-b" (dependent) + ├── [Kptfile] Kptfile subpkg-b + └── [f3.yaml] ConfigMap config +`, filepath.Base(d)), b.String()) { + return + } +} + func TestTreeCommand_symlink(t *testing.T) { d, err := os.MkdirTemp("", "tree-test") if !assert.NoError(t, err) { diff --git a/thirdparty/cmdconfig/commands/cmdtree/tree.go b/thirdparty/cmdconfig/commands/cmdtree/tree.go index a165c21100..3ba7f7001f 100644 --- a/thirdparty/cmdconfig/commands/cmdtree/tree.go +++ b/thirdparty/cmdconfig/commands/cmdtree/tree.go @@ -16,6 +16,7 @@ package cmdtree import ( + "bytes" "fmt" "io" "os" @@ -37,6 +38,11 @@ const ( TreeStructurePackage TreeStructure = "directory" // %q holds the package name PkgNameFormat = "Package %q" + + // PkgTypeIndependent indicates a package with its own upstream source. + PkgTypeIndependent = "independent" + // PkgTypeDependent indicates a package without upstream (inherits from parent). + PkgTypeDependent = "dependent" ) var GraphStructures = []string{string(TreeStructurePackage)} @@ -171,11 +177,19 @@ func (p TreeWriter) packageStructure(nodes []*yaml.RNode) error { } p.Root = d } - _, err := os.Stat(filepath.Join(p.Root, kptfilev1.KptFileName)) + kptfilePath := filepath.Join(p.Root, kptfilev1.KptFileName) + _, err := os.Stat(kptfilePath) if !os.IsNotExist(err) { - // if Kptfile exists in the root directory, it is a kpt package - // print only package name and not entire path - tree.SetValue(fmt.Sprintf(PkgNameFormat, filepath.Base(p.Root))) + // if Kptfile exists in the root directory, it is a kpt package. + // Only annotate with type if it is independent (has upstream). + // A root package without upstream is not "dependent" — it simply + // has no upstream source (e.g. created with kpt pkg init). + pkgType := packageType(p.Root) + if pkgType == PkgTypeIndependent { + tree.SetValue(fmt.Sprintf(PkgNameFormat+" (%s)", filepath.Base(p.Root), pkgType)) + } else { + tree.SetValue(fmt.Sprintf(PkgNameFormat, filepath.Base(p.Root))) + } } else { // else it is just a directory, so print only directory name tree.SetValue(filepath.Base(p.Root)) @@ -187,16 +201,42 @@ func (p TreeWriter) packageStructure(nodes []*yaml.RNode) error { } // branchName takes the root directory and relative path to the directory -// and returns the branch name +// and returns the branch name. For packages (directories with a Kptfile), +// it also indicates whether the package is independent or dependent. +// An independent package has an upstream section in its Kptfile; a dependent +// package does not. func branchName(root, dirRelPath string) string { name := filepath.Base(dirRelPath) - _, err := os.Stat(filepath.Join(root, dirRelPath, kptfilev1.KptFileName)) - if !os.IsNotExist(err) { - // add Package prefix indicating that it is a separate package as it has - // Kptfile - return fmt.Sprintf(PkgNameFormat, name) + pkgDir := filepath.Join(root, dirRelPath) + _, err := os.Stat(filepath.Join(pkgDir, kptfilev1.KptFileName)) + if os.IsNotExist(err) { + return name + } + // It is a package (has Kptfile). Determine if independent or dependent. + pkgType := packageType(pkgDir) + if pkgType != "" { + return fmt.Sprintf(PkgNameFormat+" (%s)", name, pkgType) + } + return fmt.Sprintf(PkgNameFormat, name) +} + +// packageType reads the Kptfile in the given directory and returns "independent" +// if it has an upstream section, "dependent" otherwise. If the file cannot +// be read or parsed, it returns an empty string. +func packageType(dir string) string { + data, err := os.ReadFile(filepath.Join(dir, kptfilev1.KptFileName)) + if err != nil { + return "" + } + var kf kptfilev1.KptFile + d := yaml.NewDecoder(bytes.NewReader(data)) + if err := d.Decode(&kf); err != nil { + return "" + } + if kf.Upstream != nil { + return PkgTypeIndependent } - return name + return PkgTypeDependent } // Write writes the ascii tree to p.Writer