Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add devbox command #5971

Merged
merged 28 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0d9d3af
feat: add initial `testkube devbox` command
rangoo94 Oct 24, 2024
c8a82a5
feat: clean up development tool a bit
rangoo94 Oct 25, 2024
a535434
fix: GZip the binaries before sending
rangoo94 Oct 25, 2024
3065b6e
fix: small issues with devbox, add dashboard link, add README
rangoo94 Oct 28, 2024
965b784
feat: parallelize devbox better
rangoo94 Oct 28, 2024
8a905e5
chore: add links for CRD Sync workflows and templates
rangoo94 Oct 28, 2024
1c446e4
chore: adjust devbox messages
rangoo94 Oct 28, 2024
4fefdca
chore: reduce size of binaries for devbox
rangoo94 Oct 28, 2024
3b1cc3d
fix: reduce Init Process size from 35MB to 5MB
rangoo94 Oct 28, 2024
bd2cf80
chore: round time in devbox
rangoo94 Oct 28, 2024
3a51866
fix: generate properly slug for devbox environment
rangoo94 Oct 28, 2024
85e5479
fixup lint
rangoo94 Oct 28, 2024
f4f8453
chore: add option to open dashboard
rangoo94 Oct 28, 2024
f084f70
fix: delete debug
rangoo94 Oct 28, 2024
a879137
chore: avoid logs for canceled operation
rangoo94 Oct 28, 2024
90bf0a7
chore: increase timeout for build bucket
rangoo94 Oct 28, 2024
c953c53
fix: restarting pod
rangoo94 Oct 28, 2024
845fa81
fix: restarting pod
rangoo94 Oct 28, 2024
d06fc1b
chore: clean messages and add transfer size
rangoo94 Oct 29, 2024
9d91e7b
feat: add basic binary storage to avoid transferring too much data in…
rangoo94 Oct 30, 2024
0832b7b
feat: make BinaryPatch more stable
rangoo94 Oct 30, 2024
e489e79
fixup delete unused code
rangoo94 Oct 30, 2024
a86ac21
fix: make binary patch more stable
rangoo94 Oct 30, 2024
cf4e378
chore: adjust a bit binary patch constants
rangoo94 Oct 30, 2024
d1ee6d4
chore: reuse buffer better
rangoo94 Oct 30, 2024
4a32325
fix: corner cases where binary patch was stuck
rangoo94 Oct 31, 2024
4084253
fixup lint
rangoo94 Oct 31, 2024
fff985d
fix: avoid unnecessary container for agent in devbox
rangoo94 Oct 31, 2024
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
3 changes: 3 additions & 0 deletions cmd/kubectl-testkube/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/pro"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/cmd/tcl/kubectl-testkube/devbox"
"github.com/kubeshop/testkube/pkg/telemetry"
"github.com/kubeshop/testkube/pkg/ui"
)
Expand Down Expand Up @@ -65,6 +66,8 @@ func init() {
RootCmd.AddCommand(NewDockerCmd())
RootCmd.AddCommand(pro.NewLoginCmd())

RootCmd.AddCommand(devbox.NewDevBoxCommand())

RootCmd.SetHelpCommand(NewHelpCmd())
}

Expand Down
227 changes: 227 additions & 0 deletions cmd/tcl/devbox-binary-storage/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Copyright 2024 Testkube.
//
// Licensed as a Testkube Pro file under the Testkube Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt

package main

import (
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"

"github.com/dustin/go-humanize"

"github.com/kubeshop/testkube/cmd/tcl/kubectl-testkube/devbox/devutils"
)

var (
locks = make(map[string]*sync.RWMutex)
locksMu sync.Mutex
hashCache = make(map[string]string)
)

func getLock(filePath string) *sync.RWMutex {
locksMu.Lock()
defer locksMu.Unlock()
if locks[filePath] == nil {
locks[filePath] = new(sync.RWMutex)
}
return locks[filePath]
}

func rebuildHash(filePath string) {
hashCache[filePath] = ""
f, err := os.Open(filePath)
Dismissed Show dismissed Hide dismissed
if err != nil {
return
}
defer f.Close()

h := sha256.New()
if _, err := io.Copy(h, f); err == nil {
hashCache[filePath] = fmt.Sprintf("%x", h.Sum(nil))
}
}

func getHash(filePath string) string {
if hashCache[filePath] == "" {
rebuildHash(filePath)
}
return hashCache[filePath]
}

func main() {
storagePath := "/storage"
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
filePath := filepath.Clean(strings.TrimPrefix(r.URL.Path, "/"))
if filePath == "" {
w.WriteHeader(http.StatusNotFound)
return
}
localPath := filepath.Join(storagePath, filePath)
if r.Method == http.MethodGet {
getLock(filePath).RLock()
defer getLock(filePath).RUnlock()

file, err := os.Open(localPath)
Dismissed Show dismissed Hide dismissed
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
stat, err := file.Stat()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
w.WriteHeader(http.StatusOK)
io.Copy(w, file)
return
} else if r.Method == http.MethodPost {
getLock(filePath).Lock()
defer getLock(filePath).Unlock()

body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading body", err)
return
}
if r.ContentLength != int64(len(body)) {
w.WriteHeader(http.StatusBadRequest)
return
}
if r.Header.Get("Content-Encoding") == "gzip" {
gz, err := gzip.NewReader(bytes.NewBuffer(body))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading body into gzip", err)
return
}
body, err = io.ReadAll(gz)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading back data from gzip stream", err)
return
}
}

err = os.WriteFile(localPath, body, 0666)
Dismissed Show dismissed Hide dismissed
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed to write file", err)
return
}

h := sha256.New()
if _, err := io.Copy(h, bytes.NewBuffer(body)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed to build hash", err)
}
hashCache[filePath] = fmt.Sprintf("%x", h.Sum(nil))

fmt.Println("saved file", filePath, humanize.Bytes(uint64(len(body))))
w.WriteHeader(http.StatusOK)
return
} else if r.Method == http.MethodPatch {
getLock(filePath).Lock()
defer getLock(filePath).Unlock()

body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading body", err)
return
}
if r.ContentLength != int64(len(body)) {
w.WriteHeader(http.StatusBadRequest)
return
}
if r.Header.Get("Content-Encoding") == "gzip" {
gz, err := gzip.NewReader(bytes.NewBuffer(body))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading body into gzip", err)
return
}
body, err = io.ReadAll(gz)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading back data from gzip stream", err)
return
}
}

// Verify if patch can be applied
if r.Header.Get("X-Prev-Hash") != getHash(filePath) {
w.WriteHeader(http.StatusConflict)
return
}

// Apply patch
prevFile, err := os.ReadFile(localPath)
Dismissed Show dismissed Hide dismissed
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed reading existing file", err)
return
}
patch := devutils.NewBinaryPatchFromBytes(body)
file := patch.Apply(prevFile)

h := sha256.New()
if _, err := io.Copy(h, bytes.NewBuffer(file)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed to build hash", err)
return
}

// Validate hash
nextHash := fmt.Sprintf("%x", h.Sum(nil))
if r.Header.Get("X-Hash") != nextHash {
w.WriteHeader(http.StatusBadRequest)
fmt.Println("after applying patch result has different hash than expected", err)
return
}
fmt.Println("Expected hash", r.Header.Get("X-Hash"), "got", nextHash)
err = os.WriteFile(localPath, file, 0666)
Dismissed Show dismissed Hide dismissed
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("failed to write file", err)
return
}
hashCache[filePath] = nextHash
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
})

stopSignal := make(chan os.Signal, 1)
signal.Notify(stopSignal, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-stopSignal
os.Exit(0)
}()

fmt.Println("Starting server...")

panic(http.ListenAndServe(":8080", nil))
}
Loading
Loading