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
File renamed without changes.
32 changes: 19 additions & 13 deletions go/Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
# Copyright 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.

GOLANGCI_LINT_VERSION ?= 2.12.2

.PHONY: all
all: fix vet fmt test lint

GOPATH := $(shell go env GOPATH)
GOBIN := $(shell go env GOPATH)/bin
OUT_DIR := .out
MODULES = $(shell find . -name 'go.mod' -print)

.PHONY: fix
Expand All @@ -14,18 +27,11 @@ fix: $(MODULES)
fmt: $(MODULES)
@for f in $(^D); do (cd $$f; echo "Formatting $$f"; go fmt ./...); done

.PHONY: install-golangci-lint
install-golangci-lint:
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest

.PHONY: lint
lint: install-golangci-lint lint-modules

.PHONY: lint-modules
lint-modules: $(MODULES)
lint: $(MODULES)
@for f in $(^D); do \
(cd $$f; echo "Checking golangci-lint $$f"; \
$(GOBIN)/golangci-lint run ./...); \
(cd $$f; echo "Linting $$f"; \
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$(GOLANGCI_LINT_VERSION) run ./...) || exit 1; \
done

.PHONY: test
Expand Down
31 changes: 22 additions & 9 deletions go/fn/internal/docs/render.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025 The kpt Authors
// Copyright 2025-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.
Expand Down Expand Up @@ -40,29 +40,42 @@ type DocOutput struct {
// RenderHelp writes formatted help text to w.
// If sections are empty and metadata is zero-value, writes a minimal
// "no documentation available" message.
func RenderHelp(w io.Writer, sections Sections, meta Metadata) {
// Returns the first write error encountered.
func RenderHelp(w io.Writer, sections Sections, meta Metadata) error {
if sections.Short == "" && sections.Long == "" && sections.Examples == "" && isMetadataEmpty(meta) {
fmt.Fprint(w, "No documentation available. Pass fn.WithDocs to fn.AsMain to enable --help.\n")
return
_, err := fmt.Fprint(w, "No documentation available. Pass fn.WithDocs to fn.AsMain to enable --help.\n")
return err
}

if sections.Short != "" {
fmt.Fprintf(w, "%s\n", sections.Short)
if _, err := fmt.Fprintf(w, "%s\n", sections.Short); err != nil {
return err
}
}

if sections.Long != "" {
if sections.Short != "" {
fmt.Fprint(w, "\n")
if _, err := fmt.Fprint(w, "\n"); err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "%s\n", sections.Long); err != nil {
return err
}
fmt.Fprintf(w, "%s\n", sections.Long)
}

if sections.Examples != "" {
if sections.Short != "" || sections.Long != "" {
fmt.Fprint(w, "\n")
if _, err := fmt.Fprint(w, "\n"); err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "Examples:\n%s\n", sections.Examples); err != nil {
return err
}
fmt.Fprintf(w, "Examples:\n%s\n", sections.Examples)
}

return nil
}

// isMetadataEmpty reports whether all fields of meta are zero-value.
Expand Down
16 changes: 12 additions & 4 deletions go/fn/internal/docs/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ func TestProperty4_HelpOutputExcludesCobraBoilerplate(t *testing.T) {

// Render help output.
var buf bytes.Buffer
RenderHelp(&buf, sections, meta)
if err := RenderHelp(&buf, sections, meta); err != nil {
t.Errorf("RenderHelp failed: %v", err)
}
Comment thread
aravindtga marked this conversation as resolved.
output := buf.String()

// Assert that the help output does NOT contain cobra-style boilerplate.
Expand Down Expand Up @@ -218,7 +220,9 @@ func TestProperty3_HelpOutputContainsParsedSections(t *testing.T) {

// Render help output.
var buf bytes.Buffer
RenderHelp(&buf, sections, Metadata{})
if err := RenderHelp(&buf, sections, Metadata{}); err != nil {
t.Errorf("RenderHelp failed: %v", err)
}
output := buf.String()

// Assert that the help output contains each parsed section.
Expand Down Expand Up @@ -250,7 +254,9 @@ func TestRenderHelp_FullSectionsAndMetadata(t *testing.T) {
}

var buf bytes.Buffer
RenderHelp(&buf, sections, meta)
if err := RenderHelp(&buf, sections, meta); err != nil {
t.Errorf("RenderHelp failed: %v", err)
}
output := buf.String()

// Verify output contains the Short description.
Expand Down Expand Up @@ -283,7 +289,9 @@ func TestRenderHelp_EmptySections(t *testing.T) {
meta := Metadata{}

var buf bytes.Buffer
RenderHelp(&buf, sections, meta)
if err := RenderHelp(&buf, sections, meta); err != nil {
t.Errorf("RenderHelp failed: %v", err)
}
output := buf.String()

expected := "No documentation available. Pass fn.WithDocs to fn.AsMain to enable --help.\n"
Expand Down
22 changes: 10 additions & 12 deletions go/fn/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"

Expand Down Expand Up @@ -141,8 +142,8 @@ func AsMain(input any, opts ...Option) error {
// handleHelp renders help text to STDOUT based on registered docs.
func handleHelp(cfg *mainConfig) error {
if cfg.readme == nil && cfg.metadata == nil {
fmt.Fprint(os.Stdout, "No documentation available. Pass fn.WithDocs to fn.AsMain to enable --help.\n")
return nil
_, err := fmt.Fprint(os.Stdout, "No documentation available. Pass fn.WithDocs to fn.AsMain to enable --help.\n")
return err
}

sections := docs.ParseMarkers(cfg.readme)
Expand All @@ -152,15 +153,14 @@ func handleHelp(cfg *mainConfig) error {
meta = docs.Metadata{}
}

docs.RenderHelp(os.Stdout, sections, meta)
return nil
return docs.RenderHelp(os.Stdout, sections, meta)
}

// handleDoc renders JSON documentation to STDOUT based on registered docs.
func handleDoc(cfg *mainConfig) error {
if cfg.readme == nil && cfg.metadata == nil {
fmt.Fprint(os.Stdout, "{}")
return nil
_, err := fmt.Fprint(os.Stdout, "{}")
return err
}

sections := docs.ParseMarkers(cfg.readme)
Expand All @@ -183,20 +183,18 @@ func readFilesAsResourceList(paths []string) (*ResourceList, error) {
FunctionConfig: NewEmptyKubeObject(),
}
for _, path := range paths {
data, err := os.ReadFile(path)
cleanPath := filepath.Clean(path)
data, err := os.ReadFile(cleanPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("file not found: %s", path)
}
return nil, fmt.Errorf("failed to read file %s: %v", path, err)
return nil, fmt.Errorf("file %s: %w", path, err)
}
Comment thread
aravindtga marked this conversation as resolved.
// Empty files are valid — proceed with no items from this file.
if len(strings.TrimSpace(string(data))) == 0 {
continue
}
objects, err := ParseKubeObjects(data)
if err != nil {
return nil, fmt.Errorf("failed to parse KRM resources from %s: %v", path, err)
return nil, fmt.Errorf("failed to parse KRM resources from %s: %w", path, err)
}
for _, obj := range objects {
rl.Items = append(rl.Items, obj)
Expand Down
10 changes: 8 additions & 2 deletions go/fn/run_filemode_property_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func genKRMResource() *rapid.Generator[string] {
for i := range numEntries {
key := rapid.StringMatching(`[a-z][a-z0-9]{1,8}`).Draw(t, fmt.Sprintf("key%d", i))
value := rapid.StringMatching(`[a-zA-Z0-9]{1,15}`).Draw(t, fmt.Sprintf("value%d", i))
dataLines.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
if _, err := fmt.Fprintf(&dataLines, " %s: %s\n", key, value); err != nil {
t.Errorf("failed to write data line: %v", err)
}
}
return fmt.Sprintf(`apiVersion: v1
kind: ConfigMap
Expand All @@ -65,7 +67,11 @@ func TestProperty6_FileModeEquivalence(t *testing.T) {
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
t.Errorf("failed to remove temp dir %s: %v", tmpDir, err)
}
}()

var filePaths []string
for i, res := range resources {
Expand Down
6 changes: 3 additions & 3 deletions go/fn/run_filemode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestFileMode_NonExistentFile(t *testing.T) {

err := AsMain(noopProcessor)
require.Error(t, err, "non-existent file should return an error")
assert.Contains(t, err.Error(), "file not found")
assert.Contains(t, err.Error(), "no such file or directory")
assert.Contains(t, err.Error(), nonExistentPath, "error should include the file path")
}

Expand Down Expand Up @@ -244,7 +244,7 @@ func TestReadFilesAsResourceList_NonExistentFile(t *testing.T) {
rl, err := readFilesAsResourceList([]string{nonExistentPath})
require.Error(t, err)
assert.Nil(t, rl)
assert.Contains(t, err.Error(), "file not found")
assert.Contains(t, err.Error(), "no such file or directory")
assert.Contains(t, err.Error(), nonExistentPath)
}

Expand Down Expand Up @@ -422,7 +422,7 @@ func TestFileMode_NonExistentAmongValid(t *testing.T) {
captureStderr(t, func() {
err := AsMain(noopProcessor)
require.Error(t, err)
assert.Contains(t, err.Error(), "file not found")
assert.Contains(t, err.Error(), "no such file or directory")
assert.Contains(t, err.Error(), strings.TrimPrefix(nonExistent, ""))
})
}
12 changes: 6 additions & 6 deletions go/fn/run_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ func captureStdout(t *testing.T, fn func()) string {

fn()

w.Close()
require.NoError(t, w.Close())
os.Stdout = origStdout

var buf bytes.Buffer
_, err = io.Copy(&buf, r)
require.NoError(t, err)
r.Close()
require.NoError(t, r.Close())

return buf.String()
}
Expand All @@ -66,13 +66,13 @@ func captureStderr(t *testing.T, fn func()) string {

fn()

w.Close()
require.NoError(t, w.Close())
os.Stderr = origStderr

var buf bytes.Buffer
_, err = io.Copy(&buf, r)
require.NoError(t, err)
r.Close()
require.NoError(t, r.Close())

return buf.String()
}
Expand All @@ -96,11 +96,11 @@ func TestAsMain_HelpFlag_ExitsZero(t *testing.T) {
origStdin := os.Stdin
r, w, err := os.Pipe()
require.NoError(t, err)
w.Close() // Close write end immediately — reading would get EOF
require.NoError(t, w.Close()) // Close write end immediately — reading would get EOF
os.Stdin = r
t.Cleanup(func() {
os.Stdin = origStdin
r.Close()
assert.NoError(t, r.Close())
})
Comment thread
aravindtga marked this conversation as resolved.

output := captureStdout(t, func() {
Expand Down
14 changes: 8 additions & 6 deletions go/kfn/commands/build.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 The kpt Authors
// Copyright 2022, 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.
Expand Down Expand Up @@ -44,6 +44,8 @@ const (
// Ko constant variables
KoDockerRepoEnvVar = "KO_DOCKER_REPO"
KoLocalRepo = "ko.local"

build = "build"
)

func NewBuildRunner(ctx context.Context) *BuildRunner {
Expand All @@ -53,7 +55,7 @@ func NewBuildRunner(ctx context.Context) *BuildRunner {
Docker: &DockerBuilder{},
}
r.Command = &cobra.Command{
Use: "build",
Use: build,
Short: "build your KRM function to a container image",
RunE: r.RunE,
}
Expand Down Expand Up @@ -114,7 +116,7 @@ func (r *BuildRunner) RunE(cmd *cobra.Command, args []string) error {
}

func (r *DockerBuilder) Build() error {
args := []string{"build", ".", "-f", r.DockerfilePath, "--tag", r.Image}
args := []string{build, ".", "-f", r.DockerfilePath, "--tag", r.Image}
err := execCmdFn(nil, "docker", args...)
if err != nil {
return err
Expand Down Expand Up @@ -157,7 +159,7 @@ func (r *DockerBuilder) createDockerfile() error {
if err != nil {
return err
}
if err = os.WriteFile(DockerfilePath, dockerfileContent, 0644); err != nil {
if err = os.WriteFile(DockerfilePath, dockerfileContent, 0600); err != nil {
return err
}
fmt.Println("created Dockerfile")
Expand Down Expand Up @@ -187,7 +189,7 @@ func (r *KoBuilder) GuaranteeKoInstalled() error {
return nil
}
func (r *KoBuilder) Build() error {
args := []string{"build", "-B", "--tags", r.Tag}
args := []string{build, "-B", "--tags", r.Tag}
envs := []string{KoDockerRepoEnvVar + "=" + r.Repo}
err := execCmdFn(envs, "ko", args...)
if err != nil {
Expand Down Expand Up @@ -219,7 +221,7 @@ func (r *KoBuilder) Validate() error {
}

func execCmd(envs []string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd := exec.Command(name, args...) //nolint:gosec // CLI tool: args constructed internally from user's own flags
if len(envs) != 0 {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, envs...)
Expand Down
1 change: 0 additions & 1 deletion go/kfn/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ func TestBuild(t *testing.T) {
}
for name, test := range testcases {
t.Run(name, func(t *testing.T) {

r := NewBuildRunner(context.TODO())
execCmdFn = func(envs []string, name string, args ...string) error {
fakeExecCmd(t, test.cmdExpected, envs, name, args...)
Expand Down
4 changes: 2 additions & 2 deletions go/kfn/commands/init.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 The kpt Authors
// Copyright 2022, 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.
Expand Down Expand Up @@ -74,7 +74,7 @@ func (r *InitRunner) RunE(cmd *cobra.Command, args []string) error {
}

func (r *InitRunner) GetFnPackage() error {
cmd := exec.Command("kpt", "pkg", "get", r.FnPkgPath, r.FnName)
cmd := exec.Command("kpt", "pkg", "get", r.FnPkgPath, r.FnName) //nolint:gosec // CLI tool: args from user's own flags

var out, errout bytes.Buffer
cmd.Stdout = &out
Expand Down