Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/docs/generated/pkgdocs/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 68 additions & 4 deletions thirdparty/cmdconfig/commands/cmdtree/cmdtree.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Copyright 2019,2026 The kpt Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmdtree

import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/kptdev/kpt/internal/docs/generated/pkgdocs"
"github.com/kptdev/kpt/internal/util/argutil"
Expand Down Expand Up @@ -45,6 +60,9 @@ type TreeRunner struct {
}

func (r *TreeRunner) runE(c *cobra.Command, args []string) error {
if err := r.Ctx.Err(); err != nil {
return err
}
var input kio.Reader
var root = "."
if len(args) == 0 {
Expand All @@ -69,12 +87,58 @@ func (r *TreeRunner) runE(c *cobra.Command, args []string) error {
Inputs: []kio.Reader{input},
Filters: fltrs,
Outputs: []kio.Writer{TreeWriter{
Root: root,
Writer: printer.FromContextOrDie(r.Ctx).OutStream(),
Root: root,
Writer: printer.FromContextOrDie(r.Ctx).OutStream(),
NonKRMFiles: discoverNonKRMFiles(r.Ctx, resolvedPath),
}},
}.Execute())
}

// discoverNonKRMFiles walks the package tree and returns filenames
// indexed by their containing directory path relative to root.
// Symlinks are skipped. Files that are successfully rendered as KRM resources
// will be deduplicated by the TreeWriter.
func discoverNonKRMFiles(ctx context.Context, root string) map[string][]string {
result := map[string][]string{}
pr := printer.FromContextOrDie(ctx)

if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Fprintf(pr.ErrStream(), "[WARN] %s: %v\n", path, err)
return nil
}
if ctx.Err() != nil {
return ctx.Err()
}
if d.Type()&os.ModeSymlink != 0 {
return nil
}
name := d.Name()
if d.IsDir() {
if path != root && strings.HasPrefix(name, ".") {
return filepath.SkipDir
}
return nil
}
if strings.HasPrefix(name, ".") {
return nil
}
if name == kptfilev1.KptFileName {
return nil
}
rel, err := filepath.Rel(root, filepath.Dir(path))
if err != nil {
fmt.Fprintf(pr.ErrStream(), "[WARN] %s: %v\n", path, err)
return nil
}
result[rel] = append(result[rel], name)
return nil
}); err != nil && ctx.Err() == nil {
fmt.Fprintf(pr.ErrStream(), "[WARN] failed to walk %s: %v\n", root, err)
}
Comment thread
aravindtga marked this conversation as resolved.
return result
}

func (r *TreeRunner) getMatchFilesGlob() []string {
return append([]string{kptfilev1.KptFileName}, kio.DefaultMatch...)
}
159 changes: 156 additions & 3 deletions thirdparty/cmdconfig/commands/cmdtree/cmdtree_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Copyright 2019,2026 The kpt Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmdtree

Expand All @@ -8,11 +19,14 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/kptdev/kpt/internal/testutil"
"github.com/kptdev/kpt/pkg/printer/fake"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTreeCommandDefaultCurDir_files(t *testing.T) {
Expand Down Expand Up @@ -207,7 +221,8 @@ resources:
}

if !assert.Equal(t, fmt.Sprintf(`%s
└── [f2.yaml] Deployment bar
├── [f2.yaml] Deployment bar
└── Kustomization
`, filepath.Base(d)), b.String()) {
return
}
Expand Down Expand Up @@ -480,3 +495,141 @@ spec:
}
assert.Contains(t, stderr.String(), "please note that the symlinks within the package are ignored")
}

// TestTreeCommand_NonKRMInSubpackage verifies non-KRM files in a subpackage
// appear under the subpackage branch, not the parent.
func TestTreeCommand_NonKRMInSubpackage(t *testing.T) {
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(d, "sub"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(d, "sub", "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: sub\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "sub", "NOTES.txt"), []byte("hello\n"), 0600))

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
require.Contains(t, out, `Package "sub"`)
require.Contains(t, out, "NOTES.txt")
subIdx := strings.Index(out, `Package "sub"`)
notesIdx := strings.Index(out, "NOTES.txt")
assert.Greater(t, notesIdx, subIdx, "NOTES.txt should be under the subpackage branch")
}

// TestTreeCommand_DotfilesExcluded verifies dotfiles and dot-dirs are excluded.
func TestTreeCommand_DotfilesExcluded(t *testing.T) {
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, ".hidden"), []byte("secret\n"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(d, ".git"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(d, ".git", "config"), []byte("[core]\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "visible.txt"), []byte("hi\n"), 0600))

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
assert.Contains(t, out, "visible.txt")
assert.NotContains(t, out, ".hidden")
assert.NotContains(t, out, ".git")
assert.NotContains(t, out, "config")
}

// TestTreeCommand_SymlinkFileSkipped verifies symlinked files inside a package are skipped.
func TestTreeCommand_SymlinkFileSkipped(t *testing.T) {
Comment thread
aravindtga marked this conversation as resolved.
if runtime.GOOS == "windows" {
t.SkipNow()
}
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "real.txt"), []byte("content\n"), 0600))
require.NoError(t, os.Symlink(filepath.Join(d, "real.txt"), filepath.Join(d, "link.txt")))
Comment thread
aravindtga marked this conversation as resolved.

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
assert.Contains(t, out, "real.txt")
assert.NotContains(t, out, "link.txt")
}

// TestTreeCommand_MultipleNonKRMSorted verifies multiple non-KRM files are sorted.
func TestTreeCommand_MultipleNonKRMSorted(t *testing.T) {
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "zebra.md"), []byte("z\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "alpha.txt"), []byte("a\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "middle.log"), []byte("m\n"), 0600))

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
alphaIdx := strings.Index(out, "alpha.txt")
middleIdx := strings.Index(out, "middle.log")
zebraIdx := strings.Index(out, "zebra.md")
Comment thread
aravindtga marked this conversation as resolved.
require.NotEqual(t, -1, alphaIdx, "alpha.txt should be present in output")
require.NotEqual(t, -1, middleIdx, "middle.log should be present in output")
require.NotEqual(t, -1, zebraIdx, "zebra.md should be present in output")
assert.Less(t, alphaIdx, middleIdx, "alpha.txt should come before middle.log")
assert.Less(t, middleIdx, zebraIdx, "middle.log should come before zebra.md")
}

// TestTreeCommand_NonKRMInNonPackageSubdir verifies that non-KRM files inside
// a non-package subdirectory (no Kptfile) are rendered under the parent package
// branch (not as a spurious directory branch), and KRM files in that subdir are
// deduplicated properly.
func TestTreeCommand_NonKRMInNonPackageSubdir(t *testing.T) {
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "deployment.yaml"), []byte("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: root-deploy\n"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(d, "docs"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(d, "docs", "README.md"), []byte("# Hello\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "docs", "svc.yaml"), []byte("apiVersion: v1\nkind: Service\nmetadata:\n name: my-svc\n"), 0600))

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
// KRM file in subdir should appear as a resource under the "docs" branch
assert.Contains(t, out, "[svc.yaml] Service my-svc")
// Non-KRM file should appear under the same "docs" branch
assert.Contains(t, out, "README.md")
// "docs" should NOT appear as a Package branch (no Kptfile)
assert.NotContains(t, out, `Package "docs"`)
// svc.yaml should appear only once (as KRM, not duplicated as non-KRM)
assert.Equal(t, 1, strings.Count(out, "svc.yaml"), "svc.yaml should appear exactly once")
}

// TestTreeCommand_DedupKRMFile verifies a YAML file rendered as KRM is not
// duplicated in the non-KRM list.
func TestTreeCommand_DedupKRMFile(t *testing.T) {
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "Kptfile"), []byte("apiVersion: kpt.dev/v1\nkind: Kptfile\nmetadata:\n name: root\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(d, "cm.yaml"), []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cfg\n"), 0600))

b := &bytes.Buffer{}
r := GetTreeRunner(fake.CtxWithPrinter(b, nil), "")
r.Command.SetArgs([]string{d})
r.Command.SetOut(b)
require.NoError(t, r.Command.Execute())

out := b.String()
assert.Contains(t, out, "[cm.yaml] ConfigMap cfg")
assert.Equal(t, 1, strings.Count(out, "cm.yaml"), "cm.yaml should appear exactly once (as KRM, not duplicated)")
}
Loading
Loading