Skip to content

Commit

Permalink
Merge branch 'main' into update-test-suite
Browse files Browse the repository at this point in the history
  • Loading branch information
Reecepbcups committed Oct 18, 2024
2 parents eb741ec + bfeb32a commit 86798af
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 41 deletions.
14 changes: 11 additions & 3 deletions Dockerfile.local-interchain
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# docker build . -t local-interchain:local -f Dockerfile.local-interchain
# docker run -it local-interchain:local

FROM golang:1.22.2 as builder
FROM golang:1.22.5 AS builder

# Set destination for COPY
WORKDIR /app
Expand All @@ -21,8 +21,16 @@ RUN cd local-interchain && make build

RUN mv ./bin/local-ic /go/bin

# Reduces the size of the final image from 7GB -> 0.1GB
FROM busybox:1.35.0 as final
# Final stage
FROM debian:bookworm-slim AS final

# Install certificates and required libraries
RUN apt-get update && \
apt-get install -y ca-certificates libc6 && \
update-ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

RUN mkdir -p /usr/local/bin
COPY --from=builder /go/bin/local-ic /usr/local/bin/local-ic

Expand Down
2 changes: 1 addition & 1 deletion local-interchain/docs/WINDOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ After installation, open a new cmd or shell, and you will be able to run `go ver
### 4. Downloading Make
Make is a tool which controls the generation of executables and other non-source files of a program from the source files. It is necessary for building *`makefiles`*.

Make does not come with Windows, so we need to download the make binary which you can find provided by GNU [here](https://gnuwin32.sourceforge.net/packages/make.htm) and download the Binaries zip, or go to [this link](https://gnuwin32.sourceforge.net/downlinks/make-bin-zip.php) directly and begin downloading.
Make does not come with Windows, so we need to download the make binary which you can find provided by GNU [here](https://www.gnu.org/software/make/) and download the Binaries zip, or go to [this link](https://sourceforge.net/projects/gnuwin32/files/make/3.81/make-3.81-bin.zip/download?use_mirror=kent&download=) directly and begin downloading.

1. Extract the downloaded zip file
2. Go to the *`bin`* folder, copy *`make.exe`*
Expand Down
2 changes: 1 addition & 1 deletion local-interchain/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/cosmos/cosmos-sdk v0.50.9
github.com/cosmos/go-bip39 v1.0.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
github.com/spf13/cobra v1.8.1
Expand Down Expand Up @@ -139,7 +140,6 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
Expand Down
152 changes: 152 additions & 0 deletions local-interchain/interchain/handlers/container_log_stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package handlers

import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"unicode"

dockertypes "github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/strangelove-ventures/interchaintest/v8/chain/cosmos"
"go.uber.org/zap"
)

var removeColorRegex = regexp.MustCompile("\x1b\\[[0-9;]*m")

type ContainerStream struct {
ctx context.Context
logger *zap.Logger
cli *dockerclient.Client
authKey string
testName string

nameToID map[string]string
}

func NewContainerSteam(ctx context.Context, logger *zap.Logger, cli *dockerclient.Client, authKey, testName string, vals map[string][]*cosmos.ChainNode) *ContainerStream {
nameToID := make(map[string]string)
for _, nodes := range vals {
for _, node := range nodes {
nameToID[node.Name()] = node.ContainerID()
}
}

return &ContainerStream{
ctx: ctx,
authKey: authKey,
cli: cli,
logger: logger,
testName: testName,
nameToID: nameToID,
}
}

func (cs *ContainerStream) StreamContainer(w http.ResponseWriter, r *http.Request) {
if err := VerifyAuthKey(cs.authKey, r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

containerID := r.URL.Query().Get("id")
if containerID == "" {
output := "No container ID provided. Available containers:\n"
for name, id := range cs.nameToID {
output += fmt.Sprintf("- %s: %s\n", name, id)
}

fmt.Fprint(w, output)
fmt.Fprint(w, "Provide a container ID with ?id=<containerID>")
return
}

// if container id is in the cs.nameToID map, use the mapped container ID
if id, ok := cs.nameToID[containerID]; ok {
containerID = id
} else {
fmt.Fprintf(w, "Container ID %s not found\n", containerID)
return
}

// http://127.0.0.1:8080/container_logs?id=<ID>&colored=true
isColored := strings.HasPrefix(strings.ToLower(r.URL.Query().Get("colored")), "t")
tailLines := tailLinesParam(r.URL.Query().Get("lines"))

rr, err := cs.cli.ContainerLogs(cs.ctx, containerID, dockertypes.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Details: true,
Tail: strconv.FormatUint(tailLines, 10),
})
if err != nil {
http.Error(w, "Unable to get container logs", http.StatusInternalServerError)
return
}
defer rr.Close()

// Set headers to keep the connection open for SSE (Server-Sent Events)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

// Flush ensures data is sent to the client immediately
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}

for {
buf := make([]byte, 8*1024)
n, err := rr.Read(buf)
if err != nil {
break
}

text := string(buf[:n])
if !isColored {
text, err = removeAnsiColorCodesFromText(string(buf[:n]))
if err != nil {
http.Error(w, "Unable to remove ANSI color codes", http.StatusInternalServerError)
return
}
}

fmt.Fprint(w, cleanSpecialChars(text))
flusher.Flush()
}
}

func tailLinesParam(tailInput string) uint64 {
if tailInput == "" {
return defaultTailLines
}

tailLines, err := strconv.ParseUint(tailInput, 10, 64)
if err != nil {
return defaultTailLines
}

return tailLines
}

func removeAnsiColorCodesFromText(text string) (string, error) {
return removeColorRegex.ReplaceAllString(text, ""), nil
}

func cleanSpecialChars(text string) string {
return strings.Map(func(r rune) rune {
if r == '\n' {
return r
}

if unicode.IsPrint(r) {
return r
}
return -1
}, text)
}
164 changes: 164 additions & 0 deletions local-interchain/interchain/handlers/log_stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package handlers

import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"time"

"go.uber.org/zap"
)

const defaultTailLines = 50

type LogStream struct {
fName string
authKey string
logger *zap.Logger
}

func NewLogSteam(logger *zap.Logger, file string, authKey string) *LogStream {
return &LogStream{
fName: file,
authKey: authKey,
logger: logger,
}
}

func (ls *LogStream) StreamLogs(w http.ResponseWriter, r *http.Request) {
if err := VerifyAuthKey(ls.authKey, r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

// Set headers to keep the connection open for SSE (Server-Sent Events)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

// Flush ensures data is sent to the client immediately
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}

// Open the log file
file, err := os.Open(ls.fName)
if err != nil {
http.Error(w, "Unable to open log file", http.StatusInternalServerError)
return
}
defer file.Close()

// Seek to the end of the file to read only new log entries
file.Seek(0, io.SeekEnd)

// Read new lines from the log file
reader := bufio.NewReader(file)

for {
select {
// In case client closes the connection, break out of loop
case <-r.Context().Done():
return
default:
// Try to read a line
line, err := reader.ReadString('\n')
if err == nil {
// Send the log line to the client
fmt.Fprintf(w, "data: %s\n\n", line)
flusher.Flush() // Send to client immediately
} else {
// If no new log is available, wait for a short period before retrying
time.Sleep(100 * time.Millisecond)
}
}
}
}

func (ls *LogStream) TailLogs(w http.ResponseWriter, r *http.Request) {
if err := VerifyAuthKey(ls.authKey, r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

var linesToTail uint64 = defaultTailLines
tailInput := r.URL.Query().Get("lines")
if tailInput != "" {
tailLines, err := strconv.ParseUint(tailInput, 10, 64)
if err != nil {
http.Error(w, "Invalid lines input", http.StatusBadRequest)
return
}
linesToTail = tailLines
}

logs := TailFile(ls.logger, ls.fName, linesToTail)
for _, log := range logs {
fmt.Fprintf(w, "%s\n", log)
}
}

func TailFile(logger *zap.Logger, logFile string, lines uint64) []string {
// read the last n lines of a file
file, err := os.Open(logFile)
if err != nil {
log.Fatal(err)
}
defer file.Close()

totalLines, err := lineCounter(file)
if err != nil {
log.Fatal(err)
}

if lines > uint64(totalLines) {
lines = uint64(totalLines)
}

file.Seek(0, io.SeekStart)
reader := bufio.NewReader(file)

var logs []string
for i := 0; uint64(i) < totalLines-lines; i++ {
_, _, err := reader.ReadLine()
if err != nil {
logger.Fatal("Error reading log file", zap.Error(err))
}
}

for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
logs = append(logs, string(line))
}

return logs
}

func lineCounter(r io.Reader) (uint64, error) {
buf := make([]byte, 32*1024)
var count uint64 = 0
lineSep := []byte{'\n'}

for {
c, err := r.Read(buf)
count += uint64(bytes.Count(buf[:c], lineSep))

switch {
case err == io.EOF:
return count, nil

case err != nil:
return count, err
}
}
}
14 changes: 14 additions & 0 deletions local-interchain/interchain/handlers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ package handlers

import (
"encoding/json"
"fmt"
"net/http"

"github.com/strangelove-ventures/interchaintest/v8/ibc"
)

func VerifyAuthKey(expected string, r *http.Request) error {
if expected == "" {
return nil
}

if r.URL.Query().Get("auth_key") == expected {
return nil
}

return fmt.Errorf("unauthorized, incorrect or no ?auth_key= provided")
}

type IbcChainConfigAlias struct {
Type string `json:"type" yaml:"type"`
Name string `json:"name" yaml:"name"`
Expand Down
Loading

0 comments on commit 86798af

Please sign in to comment.