From c2bac94c97276a5ec1302379c6608d0a341c8d69 Mon Sep 17 00:00:00 2001 From: wangdepeng Date: Thu, 4 Jul 2024 20:14:46 +0800 Subject: [PATCH 1/2] fix: node agent client can't get full log from server Signed-off-by: wangdepeng --- cmd/kubenest/node-agent/app.go | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/cmd/kubenest/node-agent/app.go b/cmd/kubenest/node-agent/app.go index 60a6e88ea..6640d63bc 100644 --- a/cmd/kubenest/node-agent/app.go +++ b/cmd/kubenest/node-agent/app.go @@ -271,21 +271,22 @@ func handleScript(conn *websocket.Conn, params url.Values, command []string) { func execCmd(conn *websocket.Conn, command string, args []string) { // #nosec G204 cmd := exec.Command(command, args...) + log.Infof("Executing command: %s, %v", command, args) stdout, err := cmd.StdoutPipe() if err != nil { log.Warnf("Error obtaining command output pipe: %v", err) } defer stdout.Close() - if err := cmd.Start(); err != nil { - errStr := strings.ToLower(err.Error()) - log.Warnf("Error starting command: %v, %s", err, errStr) - if strings.Contains(errStr, "no such file") { - exitCode := 127 - _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%d", exitCode))) - } + stderr, err := cmd.StderrPipe() + if err != nil { + log.Warnf("Error obtaining command error pipe: %v", err) } + defer stderr.Close() + // Channel for signaling command completion + doneCh := make(chan struct{}) + defer close(doneCh) // processOutput go func() { scanner := bufio.NewScanner(stdout) @@ -294,7 +295,23 @@ func execCmd(conn *websocket.Conn, command string, args []string) { log.Warnf("%s", data) _ = conn.WriteMessage(websocket.TextMessage, data) } + scanner = bufio.NewScanner(stderr) + for scanner.Scan() { + data := scanner.Bytes() + log.Warnf("%s", data) + _ = conn.WriteMessage(websocket.TextMessage, data) + } + doneCh <- struct{}{} }() + if err := cmd.Start(); err != nil { + errStr := strings.ToLower(err.Error()) + log.Warnf("Error starting command: %v, %s", err, errStr) + _ = conn.WriteMessage(websocket.TextMessage, []byte(errStr)) + if strings.Contains(errStr, "no such file") { + exitCode := 127 + _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%d", exitCode))) + } + } // Wait for the command to finish if err := cmd.Wait(); err != nil { @@ -303,6 +320,7 @@ func execCmd(conn *websocket.Conn, command string, args []string) { log.Warnf("Command : %s exited with non-zero status: %v", command, exitError) } } + <-doneCh exitCode := cmd.ProcessState.ExitCode() log.Infof("Command : %s finished with exit code %d", command, exitCode) _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%d", exitCode))) From 2b3c1378699f6c3bcd1668aa6fb314a4031b620e Mon Sep 17 00:00:00 2001 From: wangdepeng Date: Fri, 5 Jul 2024 21:11:56 +0800 Subject: [PATCH 2/2] feat: use cobra and viper to refactor the node agent Signed-off-by: wangdepeng --- cmd/kubenest/node-agent/app/client/client.go | 378 ++++++++++++++++++ .../client/client_test.go} | 85 +--- cmd/kubenest/node-agent/app/logger/logger.go | 27 ++ cmd/kubenest/node-agent/app/root.go | 74 ++++ .../node-agent/{app.go => app/serve/serve.go} | 166 ++++++-- cmd/kubenest/node-agent/main.go | 13 + go.mod | 10 + go.sum | 16 + hack/node-agent/init.sh | 4 +- hack/node-agent/node-agent.service | 2 +- .../pelletier/go-toml/example-crlf.toml | 58 +-- 11 files changed, 686 insertions(+), 147 deletions(-) create mode 100644 cmd/kubenest/node-agent/app/client/client.go rename cmd/kubenest/node-agent/{app_test.go => app/client/client_test.go} (56%) create mode 100644 cmd/kubenest/node-agent/app/logger/logger.go create mode 100644 cmd/kubenest/node-agent/app/root.go rename cmd/kubenest/node-agent/{app.go => app/serve/serve.go} (70%) create mode 100644 cmd/kubenest/node-agent/main.go diff --git a/cmd/kubenest/node-agent/app/client/client.go b/cmd/kubenest/node-agent/app/client/client.go new file mode 100644 index 000000000..08419d2cc --- /dev/null +++ b/cmd/kubenest/node-agent/app/client/client.go @@ -0,0 +1,378 @@ +package client + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "os/signal" + "path/filepath" + "sync" + + "github.com/gorilla/websocket" + "github.com/spf13/cobra" + "golang.org/x/term" + + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/logger" +) + +var ( + log = logger.GetLogger() + ClientCmd = &cobra.Command{ + Use: "client", + Short: "A WebSocket client CLI tool to execute commands and file uploads", + Long: "support execute remote command, upload file and pty", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + shCmd = &cobra.Command{ + Use: "sh [command]", + Short: "Execute a command via WebSocket", + Long: "Execute command on remote server", + RunE: cmdCmdRun, + Example: `node-agent client sh -u=[user] -p=[pass] -a="127.0.0.1:5678" -o ls -r "-l"`, + } + uploadCmd = &cobra.Command{ + Use: "upload", + Short: "Upload a file via WebSocket", + Long: "upload file to remote servers", + RunE: cmdUploadRun, + Example: `node-agent upload -u=[user] -p=[pass] -a="127.0.0.1:5678" -f /tmp -n=app.go`, + } + ttyCmd = &cobra.Command{ + Use: "tty", + Short: "Execute a command via WebSocket with TTY", + Long: "execute command on remote server use pyt", + RunE: cmdTtyRun, + } + wg sync.WaitGroup + + wsAddr []string // websocket client connect address list + filePath string // the server path to save upload file + fileName string // local file to upload + params []string // New slice to hold multiple command parameters + operation string // operation for client to execute +) +var uniqueValuesMap = make(map[string]bool) +var dialer = websocket.DefaultDialer + +func BasicAuth(user, password string) string { + auth := user + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} +func init() { + // #nosec G402 + dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + ClientCmd.PersistentFlags().StringSliceVarP(&wsAddr, "addr", "a", []string{}, "WebSocket address (e.g., host1:port1,host2:port2)") + err := ClientCmd.MarkPersistentFlagRequired("addr") + if err != nil { + return + } + + // PreRunE check param + ClientCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + for _, value := range wsAddr { + if _, exists := uniqueValuesMap[value]; exists { + return errors.New("duplicate values are not allowed") + } + uniqueValuesMap[value] = true + } + return nil + } + + shCmd.Flags().StringArrayVarP(¶ms, "param", "r", []string{}, "Command parameters") + shCmd.Flags().StringVarP(&operation, "operation", "o", "", "Operation to perform") + _ = shCmd.MarkFlagRequired("addr") + + uploadCmd.Flags().StringVarP(&fileName, "name", "n", "", "Name of the file to upload") + uploadCmd.Flags().StringVarP(&filePath, "path", "f", "", "Path to the file to upload") + // avoid can't show subcommand help and execute subcommand + _ = uploadCmd.MarkFlagRequired("name") + _ = uploadCmd.MarkFlagRequired("path") + + ttyCmd.Flags().StringVarP(&operation, "operation", "o", "", "Operation to perform") + err = ttyCmd.MarkFlagRequired("operation") // Ensure 'operation' flag is required for ttyCmd + if err != nil { + return + } + ClientCmd.AddCommand(shCmd) + ClientCmd.AddCommand(uploadCmd) + ClientCmd.AddCommand(ttyCmd) +} + +func cmdTtyRun(cmd *cobra.Command, args []string) error { + auth, err := getAuth(cmd) + if err != nil { + return err + } + headers := http.Header{ + "Authorization": {"Basic " + auth}, + } + cmdStr := fmt.Sprintf("command=%s", operation) + // execute one every wsAddr + for _, addr := range wsAddr { + wsURL := fmt.Sprintf("wss://%s/tty/?%s", addr, cmdStr) + fmt.Println("Executing tty:", cmdStr, "on", addr) + err := connectTty(wsURL, headers) + if err != nil { + log.Errorf("failed to execute command: %v on %s: %v\n", err, addr, cmdStr) + } + } + return nil +} + +func connectTty(wsURL string, headers http.Header) error { + ws, resp, err := dialer.Dial(wsURL, headers) + defer wsRespClose(resp) + if err != nil { + return fmt.Errorf("WebSocket dial error: %v", err) + } + defer ws.Close() + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + // set raw for control char + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("failed to set raw terminal: %v", err) + } + defer func(fd int, oldState *term.State) { + err := term.Restore(fd, oldState) + if err != nil { + log.Errorf("failed to restore terminal: %v", err) + } + }(int(os.Stdin.Fd()), oldState) + + inputChan := make(chan []byte) + go func() { + buf := make([]byte, 1024) + for { + n, err := os.Stdin.Read(buf) + if err != nil { + log.Println("Read input error:", err) + return + } + inputChan <- buf[0:n] + } + }() + done := make(chan struct{}) + // Read messages from the WebSocket server + go func() { + defer close(done) + for { + _, message, err := ws.ReadMessage() + if err != nil { + log.Infof("ReadMessage: %v", err) + interrupt <- os.Interrupt + return + } + fmt.Printf("%s", message) + } + }() + // Main event loop + go func() { + <-interrupt + // Cleanly close the connection on interrupt + log.Infof("Interrupt received, closing connection...") + if err := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { + log.Infof("CloseMessage: %v", err) + return + } + }() + + for { + select { + case msg, ok := <-inputChan: + if !ok { + return nil + } + // Send user input to the WebSocket server + if err := ws.WriteMessage(websocket.BinaryMessage, msg); err != nil { + log.Infof("WriteMessage: %v", err) + return err + } + if bytes.Equal(msg, []byte("exit")) { + return nil + } + case <-done: + return nil + } + } +} + +func cmdCmdRun(cmd *cobra.Command, args []string) error { + if len(operation) == 0 { + log.Errorf("operation is required") + return fmt.Errorf("operation is required") + } + auth, err := getAuth(cmd) + if err != nil { + return err + } + // use set to remove duplicate for wsAddr + return executeWebSocketCommand(auth) +} + +func cmdUploadRun(cmd *cobra.Command, args []string) error { + auth, err := getAuth(cmd) + if err != nil { + return err + } + return uploadFile(filePath, fileName, auth) +} + +func getAuth(cmd *cobra.Command) (string, error) { + user, _ := cmd.Flags().GetString("user") + password, _ := cmd.Flags().GetString("password") + if len(user) == 0 || len(password) == 0 { + log.Errorf("user and password are required") + return "", fmt.Errorf("user and password are required") + } + auth := BasicAuth(user, password) + return auth, nil +} + +func executeWebSocketCommand(auth string) error { + headers := http.Header{ + "Authorization": {"Basic " + auth}, + } + cmdStr := fmt.Sprintf("command=%s", operation) + // Build params part of the URL + if len(params) > 1 { + paramsStr := "args=" + for _, param := range params { + paramsStr += param + "&&args=" + } + paramsStr = paramsStr[:len(paramsStr)-7] + cmdStr = fmt.Sprintf("command=%s&&%s", operation, paramsStr) + } + + // execute one every wsAddr + for _, addr := range wsAddr { + wg.Add(1) + go func(addr string) { + defer wg.Done() + wsURL := fmt.Sprintf("wss://%s/cmd/?%s", addr, cmdStr) + fmt.Println("Executing command:", cmdStr, "on", addr) + err := connectAndHandleMessages(wsURL, headers) + if err != nil { + log.Errorf("failed to execute command: %v on %s: %v\n", err, addr, cmdStr) + } + }(addr) + } + wg.Wait() + return nil +} + +func uploadFile(filePath, fileName, auth string) error { + headers := http.Header{ + "Authorization": {"Basic " + auth}, + } + for _, addr := range wsAddr { + wg.Add(1) + go func(addr string) { + defer wg.Done() + wsURL := fmt.Sprintf("wss://%s/upload/?file_name=%s&file_path=%s", addr, url.QueryEscape(filepath.Base(fileName)), url.QueryEscape(filePath)) + fmt.Println("Uploading file:", fileName, "from", filePath, "to", addr) + err := connectAndSendFile(wsURL, headers, filePath, fileName) + if err != nil { + log.Errorf("failed to upload file: %v on %s: %v\n", err, addr, fileName) + } + }(addr) + } + wg.Wait() + return nil +} + +func wsRespClose(resp *http.Response) { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } +} + +func connectAndHandleMessages(wsURL string, headers http.Header) error { + ws, resp, err := dialer.Dial(wsURL, headers) + defer wsRespClose(resp) + if err != nil { + return fmt.Errorf("WebSocket dial error: %v", err) + } + defer ws.Close() + + handleMessages(ws) + return nil +} + +func connectAndSendFile(wsURL string, headers http.Header, filePath, fileName string) error { + ws, resp, err := dialer.Dial(wsURL, headers) + if err != nil { + return fmt.Errorf("WebSocket dial error: %v", err) + } + defer wsRespClose(resp) + defer ws.Close() + + sendFile(ws, fileName) + + handleMessages(ws) + return nil +} + +func handleMessages(ws *websocket.Conn) { + defer ws.Close() + for { + _, message, err := ws.ReadMessage() + if err != nil { + log.Println("Read message error:", err) + return + } + fmt.Printf("Received message: %s\n", message) + } +} + +func sendFile(ws *websocket.Conn, filePath string) { + //if file not exists, close connection + if _, err := os.Stat(filePath); os.IsNotExist(err) { + log.Errorf("File not exists: %v", err) + err := ws.WriteMessage(websocket.BinaryMessage, []byte("EOF")) + if err != nil { + log.Printf("Write message error: %v", err) + } + return + } + + file, err := os.Open(filePath) + if err != nil { + log.Errorf("File open error: %v", err) + } + defer file.Close() + // 指定每次读取的数据块大小 + bufferSize := 1024 // 例如每次读取 1024 字节 + buffer := make([]byte, bufferSize) + + reader := bufio.NewReader(file) + for { + n, err := reader.Read(buffer) + if err != nil { + // check if EOF + if err.Error() == "EOF" { + break + } + log.Errorf("failed to read file %v:", err) + return + } + dataToSend := buffer[:n] + + _ = ws.WriteMessage(websocket.BinaryMessage, dataToSend) + } + + err = ws.WriteMessage(websocket.BinaryMessage, []byte("EOF")) + log.Infof("send EOF ----") + if err != nil { + log.Errorf("Write message error: %v", err) + } +} diff --git a/cmd/kubenest/node-agent/app_test.go b/cmd/kubenest/node-agent/app/client/client_test.go similarity index 56% rename from cmd/kubenest/node-agent/app_test.go rename to cmd/kubenest/node-agent/app/client/client_test.go index 0c45d5387..737990443 100644 --- a/cmd/kubenest/node-agent/app_test.go +++ b/cmd/kubenest/node-agent/app/client/client_test.go @@ -1,9 +1,7 @@ -package main +package client import ( - "bufio" "crypto/tls" - "encoding/base64" "fmt" "net/http" "net/url" @@ -13,12 +11,9 @@ import ( "testing" "time" - "github.com/gorilla/websocket" + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/serve" ) -// create dialer -var dialer = *websocket.DefaultDialer - // test addr user pass var testAddr, username, pass string var headers http.Header @@ -37,17 +32,19 @@ func init() { username = os.Getenv("WEB_USER") pass = os.Getenv("WEB_PASS") testAddr = "127.0.0.1:5678" + headers = http.Header{ - "Authorization": {"Basic " + basicAuth(username, pass)}, + "Authorization": {"Basic " + BasicAuth(username, pass)}, } - go start(":5678", "cert.pem", "key.pem", username, pass) + go func() { + err := serve.Start(":5678", "cert.pem", "key.pem", username, pass) + if err != nil { + log.Fatal(err) + } + }() time.Sleep(10 * time.Second) } -func wsRespClose(resp *http.Response) { - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } -} + func TestCmd(t *testing.T) { fmt.Println("Command test") command := url.QueryEscape("ls -l") @@ -106,63 +103,3 @@ func TestPyScript(t *testing.T) { sendFile(ws, filepath.Join(parentDir, "count.py")) handleMessages(ws) } - -func basicAuth(username, password string) string { - auth := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(auth)) -} - -func handleMessages(ws *websocket.Conn) { - defer ws.Close() - for { - _, message, err := ws.ReadMessage() - if err != nil { - log.Println("Read message end :", err) - return - } - fmt.Printf("Received message: %s\n", message) - } -} - -func sendFile(ws *websocket.Conn, filePath string) { - //if file not exists, close connection - if _, err := os.Stat(filePath); os.IsNotExist(err) { - log.Printf("File not exists: %v", err) - err := ws.WriteMessage(websocket.BinaryMessage, []byte("EOF")) - if err != nil { - log.Printf("Write message error: %v", err) - } - return - } - - file, err := os.Open(filePath) - if err != nil { - log.Printf("File open error: %v", err) - } - defer file.Close() - // 指定每次读取的数据块大小 - bufferSize := 1024 // 例如每次读取 1024 字节 - buffer := make([]byte, bufferSize) - - reader := bufio.NewReader(file) - for { - n, err := reader.Read(buffer) - if err != nil { - // check if EOF - if err.Error() == "EOF" { - break - } - log.Printf("failed to read file %v:", err) - return - } - dataToSend := buffer[:n] - - _ = ws.WriteMessage(websocket.BinaryMessage, dataToSend) - } - - err = ws.WriteMessage(websocket.BinaryMessage, []byte("EOF")) - log.Printf("send EOF ----") - if err != nil { - log.Printf("Write message error: %v", err) - } -} diff --git a/cmd/kubenest/node-agent/app/logger/logger.go b/cmd/kubenest/node-agent/app/logger/logger.go new file mode 100644 index 000000000..cd9f36443 --- /dev/null +++ b/cmd/kubenest/node-agent/app/logger/logger.go @@ -0,0 +1,27 @@ +package logger + +import ( + "io" + "os" + + "github.com/sirupsen/logrus" +) + +var log *logrus.Logger + +func init() { + log = logrus.New() + // setup log + log.Out = os.Stdout + logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err == nil { + log.SetOutput(io.MultiWriter(os.Stdout, logFile)) + } else { + log.Info("Failed to log to file, using default stderr") + } + log.SetLevel(logrus.InfoLevel) +} + +func GetLogger() *logrus.Logger { + return log +} diff --git a/cmd/kubenest/node-agent/app/root.go b/cmd/kubenest/node-agent/app/root.go new file mode 100644 index 000000000..69c6dc648 --- /dev/null +++ b/cmd/kubenest/node-agent/app/root.go @@ -0,0 +1,74 @@ +package app + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/client" + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/logger" + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/serve" +) + +var ( + user string // username for authentication + password string // password for authentication + log = logger.GetLogger() +) + +var RootCmd = &cobra.Command{ + Use: "node-agent", + Short: "node-agent is a tool for node to start websocket server and client", + Long: `node-agent client for connect server to execute command and upload file to node + node-agent serve for start websocket server to receive message from client and download file from client`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func initConfig() { + // Tell Viper to automatically look for a .env file + viper.SetConfigFile("agent.env") + currentDir, _ := os.Getwd() + viper.AddConfigPath(currentDir) + viper.AddConfigPath("/srv/node-agent/agent.env") + // If a agent.env file is found, read it in. + if err := viper.ReadInConfig(); err != nil { + log.Warnf("Load config file error, %s", err) + } + // set default value from agent.env + if len(user) == 0 { + user = viper.GetString("WEB_USER") + } + if len(password) == 0 { + password = viper.GetString("WEB_PASS") + } +} + +func init() { + cobra.OnInitialize(initConfig) + + RootCmd.PersistentFlags().StringVarP(&user, "user", "u", "", "Username for authentication") + RootCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "Password for authentication") + // bind flags to viper + err := viper.BindPFlag("WEB_USER", RootCmd.PersistentFlags().Lookup("user")) + if err != nil { + log.Fatal(err) + } + err = viper.BindPFlag("WEB_PASS", RootCmd.PersistentFlags().Lookup("password")) + if err != nil { + log.Fatal(err) + } + // bind environment variables + err = viper.BindEnv("WEB_USER", "WEB_USER") + if err != nil { + log.Fatal(err) + } + err = viper.BindEnv("WEB_PASS", "WEB_PASS") + if err != nil { + log.Fatal(err) + } + RootCmd.AddCommand(client.ClientCmd) + RootCmd.AddCommand(serve.ServeCmd) +} diff --git a/cmd/kubenest/node-agent/app.go b/cmd/kubenest/node-agent/app/serve/serve.go similarity index 70% rename from cmd/kubenest/node-agent/app.go rename to cmd/kubenest/node-agent/app/serve/serve.go index 6640d63bc..dd3afdaf7 100644 --- a/cmd/kubenest/node-agent/app.go +++ b/cmd/kubenest/node-agent/app/serve/serve.go @@ -1,5 +1,4 @@ -// main.go -package main +package serve import ( "bufio" @@ -7,67 +6,65 @@ import ( "crypto/tls" "encoding/base64" "errors" - "flag" "fmt" "net/http" "net/url" "os" "os/exec" + "os/signal" "path/filepath" "strings" + "syscall" "time" + "github.com/creack/pty" "github.com/gorilla/websocket" - "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app/logger" ) var ( - addr = flag.String("addr", ":5678", "websocket service address") - certFile = flag.String("cert", "cert.pem", "SSL certificate file") - keyFile = flag.String("key", "key.pem", "SSL key file") - user = flag.String("user", "", "Username for authentication") - password = flag.String("password", "", "Password for authentication") - log = logrus.New() + ServeCmd = &cobra.Command{ + Use: "serve", + Short: "Start a WebSocket server", + RunE: serveCmdRun, + } + + certFile string // SSL certificate file + keyFile string // SSL key file + addr string // server listen address + log = logger.GetLogger() ) -var upgrader = websocket.Upgrader{} // use default options +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} // use default options func init() { - log.Out = os.Stdout - - file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err == nil { - log.Out = file - } else { - log.Info("Failed to log to file, using default stderr") - } - log.SetLevel(logrus.InfoLevel) + // setup flags + ServeCmd.PersistentFlags().StringVarP(&addr, "addr", "a", ":5678", "websocket service address") + ServeCmd.PersistentFlags().StringVarP(&certFile, "cert", "c", "cert.pem", "SSL certificate file") + ServeCmd.PersistentFlags().StringVarP(&keyFile, "key", "k", "key.pem", "SSL key file") } -func main() { - flag.Parse() - if *user == "" { - _user := os.Getenv("WEB_USER") - if _user != "" { - *user = _user - } - } - if *password == "" { - _password := os.Getenv("WEB_PASS") - if _password != "" { - *password = _password - } - } - if len(*user) == 0 || len(*password) == 0 { - flag.Usage() - log.Errorf("-user and -password are required %s %s", *user, *password) - return +func serveCmdRun(cmd *cobra.Command, args []string) error { + user := viper.GetString("WEB_USER") + password := viper.GetString("WEB_PASS") + if len(user) == 0 || len(password) == 0 { + log.Errorf("-user and -password are required %s %s", user, password) + return errors.New("-user and -password are required") } - start(*addr, *certFile, *keyFile, *user, *password) + return Start(addr, certFile, keyFile, user, password) } // start server -func start(addr, certFile, keyFile, user, password string) { +func Start(addr, certFile, keyFile, user, password string) error { passwordHash := sha256.Sum256([]byte(password)) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -119,6 +116,8 @@ func start(addr, certFile, keyFile, user, password string) { handleScript(conn, queryParams, []string{"python3", "-u"}) case strings.HasPrefix(r.URL.Path, "/sh"): handleScript(conn, queryParams, []string{"sh"}) + case strings.HasPrefix(r.URL.Path, "/tty"): + handleTty(conn, queryParams) default: _ = conn.WriteMessage(websocket.TextMessage, []byte("Invalid path")) } @@ -135,8 +134,93 @@ func start(addr, certFile, keyFile, user, password string) { TLSConfig: tlsConfig, ReadHeaderTimeout: 10 * time.Second, } + err := server.ListenAndServeTLS("", "") + if err != nil { + log.Errorf("failed to start server %v", err) + } + return err +} + +func handleTty(conn *websocket.Conn, queryParams url.Values) { + entrypoint := queryParams.Get("command") + if len(entrypoint) == 0 { + log.Errorf("command is required") + return + } + log.Infof("Executing command %s", entrypoint) + cmd := exec.Command(entrypoint) + ptmx, err := pty.Start(cmd) + if err != nil { + log.Errorf("failed to start command %v", err) + return + } + defer func() { + _ = ptmx.Close() + }() + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + go func() { + for range ch { + if err := pty.InheritSize(os.Stdin, ptmx); err != nil { + log.Errorf("error resizing pty: %s", err) + } + } + }() + ch <- syscall.SIGWINCH // Initial resize. + defer func() { signal.Stop(ch); close(ch) }() // Cleanup signals when done. + done := make(chan struct{}) + // Use a goroutine to copy PTY output to WebSocket + go func() { + buf := make([]byte, 1024) + for { + n, err := ptmx.Read(buf) + if err != nil { + log.Errorf("PTY read error: %v", err) + break + } + log.Printf("Received message: %s", buf[:n]) + if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil { + log.Errorf("WebSocket write error: %v", err) + break + } + } + done <- struct{}{} + }() + // echo off + //ptmx.Write([]byte("stty -echo\n")) + // Set stdin in raw mode. + //oldState, err := term.MakeRaw(int(ptmx.Fd())) + //if err != nil { + // panic(err) + //} + //defer func() { _ = term.Restore(int(ptmx.Fd()), oldState) }() // Best effort. + + // Disable Bracketed Paste Mode in bash shell + // _, err = ptmx.Write([]byte("printf '\\e[?2004l'\n")) + // if err != nil { + // log.Fatal(err) + // } + + // Use a goroutine to copy WebSocket input to PTY + go func() { + for { + _, message, err := conn.ReadMessage() + if err != nil { + log.Printf("read from websocket failed: %v, %s", err, string(message)) + break + } + log.Printf("Received message: %s", message) // Debugging line + if _, err := ptmx.Write(message); err != nil { // Ensure newline character for commands + log.Printf("PTY write error: %v", err) + break + } + } + // Signal the done channel when this goroutine finishes + done <- struct{}{} + }() - log.Errorf("failed to start server %v", server.ListenAndServeTLS("", "")) + // Wait for the done channel to be closed + <-done } func handleUpload(conn *websocket.Conn, params url.Values) { diff --git a/cmd/kubenest/node-agent/main.go b/cmd/kubenest/node-agent/main.go new file mode 100644 index 000000000..e2eca2fb0 --- /dev/null +++ b/cmd/kubenest/node-agent/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/kosmos.io/kosmos/cmd/kubenest/node-agent/app" +) + +func main() { + if err := app.RootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 592bf813e..46fe33f77 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.6.14 github.com/coreos/go-iptables v0.7.1-0.20231102141700-50d824baaa46 + github.com/creack/pty v1.1.11 github.com/docker/docker v24.0.6+incompatible github.com/evanphx/json-patch v4.12.0+incompatible github.com/go-logr/logr v1.2.3 @@ -24,6 +25,7 @@ require ( github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.6.0 github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.12.0 github.com/vishvananda/netlink v1.2.1-beta.2.0.20220630165224-c591ada0fb2b golang.org/x/sys v0.12.0 golang.org/x/time v0.3.0 @@ -103,6 +105,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -111,10 +114,12 @@ require ( github.com/klauspost/compress v1.11.13 // indirect github.com/leodido/go-urn v0.0.0-20181204092800-a67a23e1c1af // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect @@ -131,6 +136,7 @@ require ( github.com/opencontainers/runtime-spec v1.1.0-rc.1 // indirect github.com/opencontainers/selinux v1.10.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/projectcalico/go-json v0.0.0-20161128004156-6219dc7339ba // indirect github.com/projectcalico/go-yaml-wrapper v0.0.0-20191112210931-090425220c54 // indirect @@ -139,7 +145,10 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/subosito/gotenv v1.3.0 // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect github.com/xlab/treeprint v1.1.0 // indirect go.etcd.io/etcd/api/v3 v3.5.7 // indirect @@ -176,6 +185,7 @@ require ( google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect gopkg.in/go-playground/validator.v9 v9.27.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/cloud-provider v0.26.3 // indirect diff --git a/go.sum b/go.sum index 0c25b4561..9d65cf458 100644 --- a/go.sum +++ b/go.sum @@ -993,6 +993,7 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -1076,6 +1077,8 @@ github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -1117,6 +1120,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/moby/ipvs v1.0.1/go.mod h1:2pngiyseZbIKXNv7hsKj3O9UEz30c53MT9005gt2hxQ= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -1240,6 +1245,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1347,6 +1354,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= @@ -1360,6 +1368,8 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -1367,6 +1377,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1389,6 +1401,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -2207,6 +2221,8 @@ gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWd gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/hack/node-agent/init.sh b/hack/node-agent/init.sh index 4d0ae3340..3a0609861 100644 --- a/hack/node-agent/init.sh +++ b/hack/node-agent/init.sh @@ -2,7 +2,7 @@ WEB_USER="$WEB_USER" sed -i 's/^WEB_USER=.*/WEB_USER='"$WEB_USER"'/' /app/agent.env WEB_PASS="$WEB_PASS" sed -i 's/^WEB_PASS=.*/WEB_PASS='"$WEB_PASS"'/' /app/agent.env -sha256sum /app/node-agent > node-agent.sum -sha256sum /host-path/node-agent >> node-agent.sum +sha256sum /app/node-agent > /app/node-agent.sum +sha256sum /host-path/node-agent >> /app/node-agent.sum rsync -avz /app/ /host-path/ cp /app/node-agent.service /host-systemd/node-agent.service \ No newline at end of file diff --git a/hack/node-agent/node-agent.service b/hack/node-agent/node-agent.service index f6d93ef3c..94e18885e 100644 --- a/hack/node-agent/node-agent.service +++ b/hack/node-agent/node-agent.service @@ -8,7 +8,7 @@ Group=root WorkingDirectory=/srv/node-agent EnvironmentFile=-/srv/node-agent/agent.env Environment="TMPDIR=/srv/node-agent" -ExecStart=/srv/node-agent/node-agent +ExecStart=/srv/node-agent/node-agent serve Restart=on-failure RestartSec=5 diff --git a/vendor/github.com/pelletier/go-toml/example-crlf.toml b/vendor/github.com/pelletier/go-toml/example-crlf.toml index 780d9c68f..f45bf88b8 100644 --- a/vendor/github.com/pelletier/go-toml/example-crlf.toml +++ b/vendor/github.com/pelletier/go-toml/example-crlf.toml @@ -1,30 +1,30 @@ -# This is a TOML document. Boom. - -title = "TOML Example" - -[owner] -name = "Tom Preston-Werner" -organization = "GitHub" -bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." -dob = 1979-05-27T07:32:00Z # First class dates? Why not? - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] -connection_max = 5000 -enabled = true - -[servers] - - # You can indent as you please. Tabs or spaces. TOML don't care. - [servers.alpha] - ip = "10.0.0.1" - dc = "eqdc10" - - [servers.beta] - ip = "10.0.0.2" - dc = "eqdc10" - -[clients] -data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported \ No newline at end of file