Skip to content
Merged
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
58 changes: 58 additions & 0 deletions devnet-sdk/kt/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"os"
"path/filepath"

"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services"
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context"
)

// EnclaveContextIface abstracts the EnclaveContext for testing
type EnclaveContextIface interface {
GetAllFilesArtifactNamesAndUuids(ctx context.Context) ([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, error)
DownloadFilesArtifact(ctx context.Context, name string) ([]byte, error)
UploadFiles(pathToUpload string, artifactName string) (services.FilesArtifactUUID, services.FileArtifactName, error)
}
Expand Down Expand Up @@ -46,6 +49,20 @@ type Artifact struct {
reader *tar.Reader
}

func (fs *EnclaveFS) GetAllArtifactNames(ctx context.Context) ([]string, error) {
artifacts, err := fs.enclaveCtx.GetAllFilesArtifactNamesAndUuids(ctx)
if err != nil {
return nil, err
}

names := make([]string, len(artifacts))
for i, artifact := range artifacts {
names[i] = artifact.GetFileName()
}

return names, nil
}

func (fs *EnclaveFS) GetArtifact(ctx context.Context, name string) (*Artifact, error) {
artifact, err := fs.enclaveCtx.DownloadFilesArtifact(ctx, name)
if err != nil {
Expand Down Expand Up @@ -73,6 +90,47 @@ func NewArtifactFileWriter(path string, writer io.Writer) *ArtifactFileWriter {
}
}

func (a *Artifact) Download(path string) error {
for {
header, err := a.reader.Next()
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("failed to read tar header: %w", err)
}

fpath := filepath.Join(path, filepath.Clean(header.Name))

switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("failed to create directory %s: %w", fpath, err)
}
case tar.TypeReg:
// Create parent directories if they don't exist
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
return fmt.Errorf("failed to create directory for %s: %w", fpath, err)
}

// Create the file
f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
if err != nil {
return fmt.Errorf("failed to create file %s: %w", fpath, err)
}

// Copy contents from tar reader to file
if _, err := io.Copy(f, a.reader); err != nil {
f.Close()
return fmt.Errorf("failed to write contents to %s: %w", fpath, err)
}
f.Close()
default:
return fmt.Errorf("unsupported file type %d for %s", header.Typeflag, header.Name)
}
}
}

func (a *Artifact) ExtractFiles(writers ...*ArtifactFileWriter) error {
paths := make(map[string]io.Writer)
for _, writer := range writers {
Expand Down
7 changes: 7 additions & 0 deletions devnet-sdk/kt/fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"testing"

"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -53,6 +54,12 @@ func (m *mockEnclaveContext) UploadFiles(pathToUpload string, artifactName strin
return "test-uuid", services.FileArtifactName(artifactName), err
}

func (m *mockEnclaveContext) GetAllFilesArtifactNamesAndUuids(ctx context.Context) ([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, error) {
return nil, nil
}

var _ EnclaveContextIface = (*mockEnclaveContext)(nil)

func createTarGzArtifact(t *testing.T, files map[string]string) []byte {
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
Expand Down
58 changes: 54 additions & 4 deletions devnet-sdk/shell/env/kt_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
)

const (
KurtosisDevnetEnvArtifactName = "devnet"
KurtosisDevnetEnvArtifactPath = "env.json"
KurtosisDevnetEnvArtifactNamePrefix = "devnet-descriptor-"
KurtosisDevnetEnvArtifactPath = "env.json"
)

// EnclaveFS is an interface that both our mock and the real implementation satisfy
type EnclaveFS interface {
GetArtifact(ctx context.Context, name string) (*ktfs.Artifact, error)
GetAllArtifactNames(ctx context.Context) ([]string, error)
Close() error
}

Expand All @@ -30,6 +31,10 @@ func (w *enclaveFSWrapper) GetArtifact(ctx context.Context, name string) (*ktfs.
return w.fs.GetArtifact(ctx, name)
}

func (w *enclaveFSWrapper) GetAllArtifactNames(ctx context.Context) ([]string, error) {
return w.fs.GetAllArtifactNames(ctx)
}

func (w *enclaveFSWrapper) Close() error {
// The underlying EnclaveFS doesn't have a Close method, but we need it for our interface
return nil
Expand All @@ -49,11 +54,11 @@ var NewEnclaveFS NewEnclaveFSFunc = func(ctx context.Context, enclave string) (E
}

// parseKurtosisURL parses a Kurtosis URL of the form kt://enclave/artifact/file
// If artifact is omitted, it defaults to "devnet"
// If artifact is omitted, it defaults to ""
// If file is omitted, it defaults to "env.json"
func parseKurtosisURL(u *url.URL) (enclave, artifactName, fileName string) {
enclave = u.Host
artifactName = KurtosisDevnetEnvArtifactName
artifactName = ""
fileName = KurtosisDevnetEnvArtifactPath

// Trim both prefix and suffix slashes before splitting
Expand All @@ -69,6 +74,43 @@ func parseKurtosisURL(u *url.URL) (enclave, artifactName, fileName string) {
return
}

func getDefaultDescriptor(ctx context.Context, fs EnclaveFS) (string, error) {
prefix := KurtosisDevnetEnvArtifactNamePrefix

names, err := fs.GetAllArtifactNames(ctx)
if err != nil {
return "", err
}

var maxSuffix int
var maxName string
for _, name := range names {
if !strings.HasPrefix(name, prefix) {
continue
}

// Extract the suffix after the prefix
suffix := name[len(prefix):]
// Parse the suffix as a number
var num int
if _, err := fmt.Sscanf(suffix, "%d", &num); err != nil {
continue // Skip if suffix is not a valid number
}

// Update maxName if this number is larger
if maxName == "" || num > maxSuffix {
maxSuffix = num
maxName = name
}
}

if maxName == "" {
return "", fmt.Errorf("no descriptor found with valid numerical suffix")
}

return maxName, nil
}

// fetchKurtosisData reads data from a Kurtosis artifact
func fetchKurtosisData(u *url.URL) (string, []byte, error) {
enclave, artifactName, fileName := parseKurtosisURL(u)
Expand All @@ -78,6 +120,14 @@ func fetchKurtosisData(u *url.URL) (string, []byte, error) {
return "", nil, fmt.Errorf("error creating enclave fs: %w", err)
}

if artifactName == "" {
artifactName, err = getDefaultDescriptor(context.Background(), fs)
if err != nil {
return "", nil, fmt.Errorf("error getting default descriptor: %w", err)
}
fmt.Printf("Using default descriptor: %s\n", artifactName)
}

art, err := fs.GetArtifact(context.Background(), artifactName)
if err != nil {
return "", nil, fmt.Errorf("error getting artifact: %w", err)
Expand Down
92 changes: 89 additions & 3 deletions devnet-sdk/shell/env/kt_fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,38 @@ import (
)

// testFS implements EnclaveFS for testing
type testFS struct{}
type testFS struct {
artifacts map[string]bool
}

func (m *testFS) GetArtifact(_ context.Context, name string) (*ktfs.Artifact, error) {
if name == "error" {
return nil, fmt.Errorf("mock error")
}
if !m.artifacts[name] {
return nil, fmt.Errorf("artifact %s not found", name)
}
// We don't need to return a real artifact since we're only testing error cases
return nil, nil
}

func (m *testFS) GetAllArtifactNames(_ context.Context) ([]string, error) {
if m.artifacts == nil {
return nil, nil
}
names := make([]string, 0, len(m.artifacts))
for name := range m.artifacts {
names = append(names, name)
}
return names, nil
}

func (m *testFS) Close() error {
return nil
}

var _ EnclaveFS = (*testFS)(nil)

func TestParseKurtosisURL(t *testing.T) {
tests := []struct {
name string
Expand All @@ -39,7 +57,7 @@ func TestParseKurtosisURL(t *testing.T) {
name: "basic url",
urlStr: "kt://myenclave",
wantEnclave: "myenclave",
wantArtifact: "devnet",
wantArtifact: "",
wantFile: "env.json",
},
{
Expand Down Expand Up @@ -106,11 +124,20 @@ func TestFetchKurtosisDataErrors(t *testing.T) {
name: "error getting artifact",
setupMock: func() {
NewEnclaveFS = func(_ context.Context, _ string) (EnclaveFS, error) {
return &testFS{}, nil
return &testFS{artifacts: map[string]bool{"error": true}}, nil
}
},
urlStr: "kt://myenclave/error",
},
{
name: "no default descriptor",
setupMock: func() {
NewEnclaveFS = func(_ context.Context, _ string) (EnclaveFS, error) {
return &testFS{artifacts: map[string]bool{}}, nil
}
},
urlStr: "kt://myenclave",
},
}

for _, tt := range tests {
Expand All @@ -128,3 +155,62 @@ func TestFetchKurtosisDataErrors(t *testing.T) {
})
}
}

func TestGetDefaultDescriptor(t *testing.T) {
tests := []struct {
name string
artifacts map[string]bool
wantName string
wantErrText string
}{
{
name: "finds highest numbered descriptor",
artifacts: map[string]bool{
"devnet-descriptor-1": true,
"devnet-descriptor-5": true,
"devnet-descriptor-10": true,
"other": true,
},
wantName: "devnet-descriptor-10",
},
{
name: "handles non-numeric suffixes",
artifacts: map[string]bool{
"devnet-descriptor-1": true,
"devnet-descriptor-5": true,
"devnet-descriptor-abc": true,
"devnet-descriptor-10": true,
"other": true,
"devnet-descriptor-def": true,
},
wantName: "devnet-descriptor-10",
},
{
name: "no descriptors",
artifacts: map[string]bool{},
wantErrText: "no descriptor found with valid numerical suffix",
},
{
name: "no valid descriptors",
artifacts: map[string]bool{
"other": true,
"devnet-abc": true,
},
wantErrText: "no descriptor found with valid numerical suffix",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := &testFS{artifacts: tt.artifacts}
got, err := getDefaultDescriptor(context.Background(), fs)
if tt.wantErrText != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrText)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantName, got)
})
}
}
3 changes: 0 additions & 3 deletions devnet-sdk/testing/systest/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ type defaultProvider struct{}
func (p *defaultProvider) NewSystemFromURL(url string) (system.System, error) {
return system.NewSystemFromURL(url)
}

// currentPackage is the current package implementation
var currentPackage systemProvider = &defaultProvider{}
Loading