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
21 changes: 21 additions & 0 deletions api/testworkflows/v1/content_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,32 @@ type ContentTarball struct {
Mount *bool `json:"mount,omitempty" expr:"ignore"`
}

type ContentOci struct {
// OCI image reference to fetch the artifact from
Image string `json:"image,omitempty" expr:"template"`
// where to mount the fetched content (defaults to "repo" directory in the data volume)
MountPath string `json:"mountPath,omitempty" expr:"template"`
// path to extract the artifact content to (relative to mount path)
Path string `json:"path,omitempty" expr:"template"`
// registry username
Username string `json:"username,omitempty" expr:"template"`
// external registry username
UsernameFrom *corev1.EnvVarSource `json:"usernameFrom,omitempty" expr:"force"`
// registry token
Token string `json:"token,omitempty" expr:"template"`
// external registry token
TokenFrom *corev1.EnvVarSource `json:"tokenFrom,omitempty" expr:"force"`
// registry address
Registry string `json:"registry,omitempty" expr:"template"`
}

type Content struct {
// git repository details
Git *ContentGit `json:"git,omitempty" expr:"include"`
// files to load
Files []ContentFile `json:"files,omitempty" expr:"include"`
// tarballs to unpack
Tarball []ContentTarball `json:"tarball,omitempty" expr:"include"`
// OCI artifact details
Oci *ContentOci `json:"oci,omitempty" expr:"include"`
}
30 changes: 30 additions & 0 deletions api/testworkflows/v1/zz_generated.deepcopy.go

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

28 changes: 28 additions & 0 deletions api/v1/testkube.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10002,6 +10002,8 @@ components:
properties:
git:
$ref: "#/components/schemas/TestWorkflowContentGit"
oci:
$ref: "#/components/schemas/TestWorkflowContentOCI"
files:
type: array
items:
Expand Down Expand Up @@ -10048,6 +10050,32 @@ components:
description: paths to fetch for the sparse checkout
items:
type: string

TestWorkflowContentOCI:
type: object
properties:
image:
type: string
description: OCI image reference to fetch the artifact from
mountPath:
type: string
description: where to mount the fetched content (defaults to "repo" directory in the data volume)
path:
type: string
description: path to extract the artifact content to (relative to mount path)
username:
type: string
description: registry username
usernameFrom:
$ref: "#/components/schemas/EnvVarSource"
token:
type: string
description: registry token
tokenFrom:
$ref: "#/components/schemas/EnvVarSource"
registry:
type: string
description: registry address

TestWorkflowContentFile:
type: object
Expand Down
67 changes: 1 addition & 66 deletions cmd/testworkflow-toolkit/commands/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package commands
import (
"context"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
Expand All @@ -12,7 +11,6 @@ import (
"time"

"github.com/kballard/go-shellquote"
"github.com/otiai10/copy"
"github.com/spf13/cobra"

"github.com/kubeshop/testkube/cmd/testworkflow-toolkit/env"
Expand Down Expand Up @@ -126,7 +124,7 @@ func RunClone(ctx context.Context, rawURI string, outputPath string, opts *Clone
}

// Copy files to destination
if err := copyRepositoryContents(tmpPath, destinationPath); err != nil {
if err := copyDirContents(tmpPath, destinationPath); err != nil {
return fmt.Errorf("error copying files to destination: %w", err)
}

Expand Down Expand Up @@ -360,66 +358,3 @@ func isCommitHash(s string) bool {
}
return true
}

// copyRepositoryContents copies repository contents from source to destination
func copyRepositoryContents(src, dest string) error {
fmt.Printf("📥 Moving the contents to %s...\n", dest)

return copy.Copy(src, dest, copy.Options{
OnError: func(srcPath, destPath string, err error) error {
if err != nil {
// Ignore chmod errors on mounted directories
if srcPath == src && strings.Contains(err.Error(), "chmod") {
return nil
}
fmt.Printf("warn: copying to %s: %s\n", destPath, err.Error())
}
return nil
},
})
}

// adjustFilePermissions ensures files have appropriate permissions
func adjustFilePermissions(path string) error {
fmt.Printf("📥 Adjusting access permissions...\n")

return filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error {
if err != nil {
return err
}

info, err := d.Info()
if err != nil {
return err
}

mode := info.Mode()
// Ensure group has read/write permissions
if mode.Perm()&0o060 != 0o060 {
if err := os.Chmod(filePath, mode|0o060); err != nil {
// Log but don't fail on permission errors
fmt.Printf("warn: chmod %s: %s\n", filePath, err.Error())
}
}
return nil
})
}

// listDirectoryContents displays the contents of a directory
func listDirectoryContents(path string) error {
fmt.Printf("🔎 Destination folder contains following files ...\n")

return filepath.Walk(path, func(name string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

// Bold directory names
if info.IsDir() {
fmt.Printf("\x1b[1m%s\x1b[0m\n", name)
} else {
fmt.Println(name)
}
return nil
})
}
102 changes: 0 additions & 102 deletions cmd/testworkflow-toolkit/commands/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -289,107 +288,6 @@ func TestCleanPaths(t *testing.T) {
}
}

func TestAdjustFilePermissions(t *testing.T) {
t.Run("successful permission adjustment", func(t *testing.T) {
// Create temporary directory structure
tmpDir := t.TempDir()

// Create test files with restrictive permissions
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test"), 0400) // read-only
require.NoError(t, err)

// Create subdirectory with file
subDir := filepath.Join(tmpDir, "subdir")
err = os.Mkdir(subDir, 0700)
require.NoError(t, err)

subFile := filepath.Join(subDir, "subtest.txt")
err = os.WriteFile(subFile, []byte("test"), 0400)
require.NoError(t, err)

// Adjust permissions
err = adjustFilePermissions(tmpDir)
require.NoError(t, err)

// Check permissions were adjusted
info, err := os.Stat(testFile)
require.NoError(t, err)
assert.True(t, info.Mode().Perm()&0o060 == 0o060, "File should have group read/write permissions")

info, err = os.Stat(subFile)
require.NoError(t, err)
assert.True(t, info.Mode().Perm()&0o060 == 0o060, "Subdir file should have group read/write permissions")
})

t.Run("non-existent directory", func(t *testing.T) {
err := adjustFilePermissions("/non/existent/path")
assert.Error(t, err)
})

t.Run("handles chmod errors gracefully", func(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("Cannot test permission denied as root")
}

// Create a directory structure where we can't change permissions
tmpDir := t.TempDir()

// Create a file
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test"), 0644)
require.NoError(t, err)

// Make the parent directory read-only
err = os.Chmod(tmpDir, 0555)
require.NoError(t, err)
defer os.Chmod(tmpDir, 0755) // Restore for cleanup

// adjustFilePermissions should not fail, just log warnings
err = adjustFilePermissions(tmpDir)
// The function should complete without error even if chmod fails
assert.NoError(t, err)
})
}

func TestCopyRepositoryContents(t *testing.T) {
// Create source directory with content
srcDir := t.TempDir()
destDir := t.TempDir()

// Create test file in source
testFile := filepath.Join(srcDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0644)
require.NoError(t, err)

// Create subdirectory with file
subDir := filepath.Join(srcDir, "subdir")
err = os.Mkdir(subDir, 0755)
require.NoError(t, err)

subFile := filepath.Join(subDir, "subtest.txt")
err = os.WriteFile(subFile, []byte("sub content"), 0644)
require.NoError(t, err)

// Copy contents
err = copyRepositoryContents(srcDir, destDir)
require.NoError(t, err)

// Verify files were copied
destFile := filepath.Join(destDir, "test.txt")
content, err := os.ReadFile(destFile)
require.NoError(t, err)
assert.Equal(t, "test content", string(content))

destSubFile := filepath.Join(destDir, "subdir", "subtest.txt")
content, err = os.ReadFile(destSubFile)
require.NoError(t, err)
assert.Equal(t, "sub content", string(content))

// Note: copyRepositoryContents intentionally swallows errors in OnError callback
// to continue copying even if some files fail. This is by design.
}

func TestCloneOptions(t *testing.T) {
opts := &CloneOptions{
RawPaths: []string{"src", "docs"},
Expand Down
Loading