From 9266eca23d8664d5213c52d0b74658d0bbee6303 Mon Sep 17 00:00:00 2001 From: Benedikt Iltisberger Date: Fri, 6 Oct 2023 16:16:29 +0200 Subject: [PATCH] feat: exec added. --- cmd/operator.go | 1 + cmd/root.go | 4 +- cmd/user.go | 30 ++++++-- config/local.yaml | 6 +- config/operator.yaml | 7 +- config/prod.yaml | 6 +- kubernetes/addResources.go | 11 ++- kubernetes/exec.go | 4 +- operator/api.go | 20 ++++- operator/context-middleware.go | 39 ++++++++++ operator/routes-websocket.go | 101 +++++++++++++++++++++++++ services/context-service.go | 21 +++++ services/user-service.go | 5 +- utils/config.go | 6 +- utils/format.go | 80 ++++++++++++++++++++ utils/routes-utils.go | 1 + utils/yaml-templates/punq-service.yaml | 6 +- 17 files changed, 325 insertions(+), 23 deletions(-) create mode 100644 operator/routes-websocket.go diff --git a/cmd/operator.go b/cmd/operator.go index 1cb6684..16e4dfd 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -28,6 +28,7 @@ var operatorCmd = &cobra.Command{ kubernetes.ContextAddMany(contexts) go operator.InitBackend() + go operator.InitWebsocket() operator.InitFrontend() }, } diff --git a/cmd/root.go b/cmd/root.go index dbe39ce..d0d432a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -50,7 +50,9 @@ var rootCmd = &cobra.Command{ os.Exit(0) } - utils.InitConfigYaml(debug, customConfig, stage) + if cmd.CommandPath() != "punq system reset-config" { + utils.InitConfigYaml(debug, customConfig, stage) + } if !utils.ContainsEqual(cmdsWithoutContext, cmd.CommandPath()) { mokubernetes.InitKubernetes(utils.CONFIG.Kubernetes.RunInCluster) diff --git a/cmd/user.go b/cmd/user.go index 3d70af0..a56ebb3 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "strings" "github.com/mogenius/punq/dtos" "github.com/mogenius/punq/services" @@ -31,25 +32,42 @@ var addUserCmd = &cobra.Command{ Short: "Add punq user.", Long: `The add command lets you add a user into punq.`, Run: func(cmd *cobra.Command, args []string) { - RequireStringFlag(email, "email") - RequireStringFlag(displayName, "displayname") - RequireStringFlag(password, "password") - selectedAccess := dtos.READER // default level if accessLevel != "" { selectedAccess = dtos.AccessLevelFromString(accessLevel) + } else { + selectedAccess = dtos.ADMIN + } + + firstname := utils.RandomFirstName() + middlename := utils.RandomMiddleName() + lastname := utils.RandomLastName() + + if email == "" { + email = fmt.Sprintf("%s-%s@punq.dev", strings.ToLower(firstname), strings.ToLower(lastname)) } - _, err := services.AddUser(dtos.PunqUserCreateInput{ + if password == "" { + password = utils.NanoId() + } + + if displayName == "" { + displayName = fmt.Sprintf("%s %s %s", firstname, middlename, lastname) + } + + newUser := dtos.PunqUserCreateInput{ Email: email, Password: password, DisplayName: displayName, AccessLevel: selectedAccess, - }) + } + + _, err := services.AddUser(newUser) if err != nil { utils.FatalError(err.Error()) } else { utils.PrintInfo("User added succesfully ✅.") + structs.PrettyPrint(newUser) } }, } diff --git a/config/local.yaml b/config/local.yaml index 1055b1c..9ccb967 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -1,5 +1,5 @@ config: - version: 1 + version: 2 backend: host: 127.0.0.1 @@ -8,6 +8,10 @@ backend: frontend: host: 127.0.0.1 port: 8081 + +websocket: + host: 127.0.0.1 + port: 8082 kubernetes: cluster_name: your-cluster-name diff --git a/config/operator.yaml b/config/operator.yaml index 04e9082..0c85317 100644 --- a/config/operator.yaml +++ b/config/operator.yaml @@ -1,5 +1,5 @@ config: - version: 1 + version: 2 backend: host: 127.0.0.1 @@ -9,6 +9,11 @@ frontend: host: 127.0.0.1 port: 8081 +websocket: + host: 127.0.0.1 + port: 8082 + + kubernetes: cluster_name: your-cluster-name own_namespace: punq diff --git a/config/prod.yaml b/config/prod.yaml index c552665..c0ab518 100644 --- a/config/prod.yaml +++ b/config/prod.yaml @@ -1,5 +1,5 @@ config: - version: 1 + version: 2 backend: host: 127.0.0.1 @@ -9,6 +9,10 @@ frontend: host: 127.0.0.1 port: 8081 +websocket: + host: 127.0.0.1 + port: 8082 + kubernetes: cluster_name: your-cluster-name own_namespace: punq diff --git a/kubernetes/addResources.go b/kubernetes/addResources.go index 2ed4e4e..a83d4e8 100644 --- a/kubernetes/addResources.go +++ b/kubernetes/addResources.go @@ -37,11 +37,6 @@ func Deploy(clusterName string, ingressHostname string) { addRbac(provider) addDeployment(provider) - // _, err = CreateClusterSecretIfNotExist(provider) - // if err != nil { - // logger.Log.Fatalf("Error creating cluster secret. Aborting: %s.", err.Error()) - // } - _, err = CreateContextSecretIfNotExist(provider) if err != nil { logger.Log.Fatalf("Error creating context secret. Aborting: %s.", err.Error()) @@ -69,6 +64,10 @@ func addService(provider *KubeProvider) { punqService.Spec.Ports[1].Protocol = core.ProtocolTCP punqService.Spec.Ports[1].Port = int32(utils.CONFIG.Frontend.Port) punqService.Spec.Ports[1].TargetPort = intstr.Parse(fmt.Sprint(utils.CONFIG.Frontend.Port)) + punqService.Spec.Ports[2].Name = fmt.Sprintf("%d-%s-websocket", utils.CONFIG.Websocket.Port, SERVICENAME) + punqService.Spec.Ports[2].Protocol = core.ProtocolTCP + punqService.Spec.Ports[2].Port = int32(utils.CONFIG.Websocket.Port) + punqService.Spec.Ports[2].TargetPort = intstr.Parse(fmt.Sprint(utils.CONFIG.Websocket.Port)) punqService.Spec.Selector["app"] = version.Name serviceClient := provider.ClientSet.CoreV1().Services(utils.CONFIG.Kubernetes.OwnNamespace) @@ -331,7 +330,7 @@ func addDeployment(provider *KubeProvider) { deploymentContainer.WithName(version.Name) deploymentContainer.WithImage(version.OperatorImage) - deploymentContainer.WithPorts(applyconfcore.ContainerPort().WithContainerPort(int32(utils.CONFIG.Backend.Port)).WithContainerPort(int32(utils.CONFIG.Frontend.Port))) + deploymentContainer.WithPorts(applyconfcore.ContainerPort().WithContainerPort(int32(utils.CONFIG.Backend.Port)).WithContainerPort(int32(utils.CONFIG.Frontend.Port)).WithContainerPort(int32(utils.CONFIG.Websocket.Port))) envVars := []applyconfcore.EnvVarApplyConfiguration{} envVars = append(envVars, applyconfcore.EnvVarApplyConfiguration{ diff --git a/kubernetes/exec.go b/kubernetes/exec.go index 48e374d..1919100 100644 --- a/kubernetes/exec.go +++ b/kubernetes/exec.go @@ -30,7 +30,7 @@ func ExecTest() error { // cmd.Stdout = os.Stdout // cmd.Stderr = os.Stderr - go sendData(cmdStdin, cmdStdout) + go SendData(cmdStdin, cmdStdout) // Run the command err := cmd.Run() @@ -84,7 +84,7 @@ func ExecTest() error { // return err // } -func sendData(cmdStdin io.WriteCloser, cmdStdout io.ReadCloser) { +func SendData(cmdStdin io.WriteCloser, cmdStdout io.ReadCloser) { // Create a dialer dialer := websocket.DefaultDialer diff --git a/operator/api.go b/operator/api.go index 07a006c..a4d9f7c 100644 --- a/operator/api.go +++ b/operator/api.go @@ -36,6 +36,7 @@ func InitFrontend() { c.Data(http.StatusOK, "text/html; charset=utf-8", getIndexHtml()) }) + utils.PrintInfo(fmt.Sprintf("Frontend started: http://%s:%d", utils.CONFIG.Frontend.Host, utils.CONFIG.Frontend.Port)) err := router.Run(fmt.Sprintf(":%d", utils.CONFIG.Frontend.Port)) logger.Log.Errorf("Frontend (gin) stopped with error: %s", err.Error()) } @@ -62,10 +63,27 @@ func InitBackend() { InitGeneralRoutes(router) InitWorkloadRoutes(router) + utils.PrintInfo(fmt.Sprintf("Backend started: http://%s:%d", utils.CONFIG.Backend.Host, utils.CONFIG.Backend.Port)) err := router.Run(fmt.Sprintf(":%d", utils.CONFIG.Backend.Port)) logger.Log.Errorf("Operator (gin) stopped with error: %s", err.Error()) } +func InitWebsocket() { + gin.SetMode(gin.ReleaseMode) + router := gin.New() + config := cors.DefaultConfig() + config.AllowAllOrigins = true + + router.Use(cors.New(config)) + router.Use(CreateLogger("WEBSOCK")) + + InitWebsocketRoutes(router) + + utils.PrintInfo(fmt.Sprintf("Websocket started: ws://%s:%d", utils.CONFIG.Websocket.Host, utils.CONFIG.Websocket.Port)) + err := router.Run(fmt.Sprintf(":%d", utils.CONFIG.Websocket.Port)) + logger.Log.Errorf("Websocket (gin) stopped with error: %s", err.Error()) +} + func embedFs() http.FileSystem { sub, err := fs.Sub(HtmlDirFs, "ui/dist") if err != nil { @@ -80,7 +98,7 @@ func embedFs() http.FileSystem { if len(dirContent) <= 0 { panic("dist folder empty. Cannnot serve site. FATAL.") } else { - logger.Log.Noticef("Loaded %d static files from embed.", len(dirContent)) + fmt.Printf("Loaded %d static files from embed.\n", len(dirContent)) } return http.FS(sub) } diff --git a/operator/context-middleware.go b/operator/context-middleware.go index 5096560..02b37e8 100644 --- a/operator/context-middleware.go +++ b/operator/context-middleware.go @@ -18,3 +18,42 @@ func RequireContextId() gin.HandlerFunc { } } } + +func RequireNamespace() gin.HandlerFunc { + return func(c *gin.Context) { + contextId := services.GetGinNamespace(c) + if contextId == nil { + utils.MissingHeader(c, "X-Namespace") + c.Abort() + return + } else { + c.Next() + } + } +} + +func RequirePodName() gin.HandlerFunc { + return func(c *gin.Context) { + contextId := services.GetGinPodname(c) + if contextId == nil { + utils.MissingHeader(c, "X-Podname") + c.Abort() + return + } else { + c.Next() + } + } +} + +func RequireContainerName() gin.HandlerFunc { + return func(c *gin.Context) { + contextId := services.GetGinContainername(c) + if contextId == nil { + utils.MissingHeader(c, "X-Container") + c.Abort() + return + } else { + c.Next() + } + } +} diff --git a/operator/routes-websocket.go b/operator/routes-websocket.go new file mode 100644 index 0000000..fe7e642 --- /dev/null +++ b/operator/routes-websocket.go @@ -0,0 +1,101 @@ +package operator + +import ( + "bufio" + "fmt" + "log" + "net/http" + "os" + "os/exec" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/mogenius/punq/dtos" + "github.com/mogenius/punq/services" + "github.com/mogenius/punq/utils" +) + +func InitWebsocketRoutes(router *gin.Engine) { + router.GET("/exec-sh", Auth(dtos.ADMIN), RequireNamespace(), RequirePodName(), RequireContainerName(), connectWs) +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true // adjust to implement your origin validation logic + }, +} + +func connectWs(c *gin.Context) { + namespace := services.GetGinNamespace(c) + container := services.GetGinContainername(c) + podName := services.GetGinPodname(c) + + log.Printf("exec-sh: %s %s %s\n", *namespace, *container, *podName) + + ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Printf("Failed to upgrade ws: %+v", err) + return + } + defer func() { + ws.Close() + }() + + cmd := exec.Command("sh", "-c", fmt.Sprintf("kubectl exec -i --tty -c %s -n %s %s -- /bin/sh", *container, *namespace, *podName)) + cmd.Env = os.Environ() + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal("Error creating stdin pipe:", err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal("Error creating stdout pipe:", err) + } + + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + data := scanner.Bytes() + if utils.CONFIG.Misc.Debug { + fmt.Printf("Response-Line: '%s'\n", string(data)) + } + err = ws.WriteMessage(websocket.TextMessage, data) + if err != nil { + log.Printf("Error writing to ws: %+v", err) + } + } + }() + + go func() { + for { + _, msg, err := ws.ReadMessage() + if utils.CONFIG.Misc.Debug { + fmt.Printf("Received Cmd: '%s'", string(msg)) + } + msg = append(msg, '\n') + if err != nil { + log.Printf("Error reading from ws: %+v", err) + log.Printf("CLOSE: exec-sh: %s %s %s\n", *namespace, *container, *podName) + break + } + _, err = stdin.Write(msg) + if err != nil { + log.Printf("Error writing to stdin: %+v", err) + } + } + }() + + err = cmd.Start() + if err != nil { + log.Printf("Error starting cmd: %+v", err) + return + } + + err = cmd.Wait() + if err != nil { + log.Printf("Cmd returned error: %+v", err.Error()) + return + } +} diff --git a/services/context-service.go b/services/context-service.go index f1ebf19..dca2692 100644 --- a/services/context-service.go +++ b/services/context-service.go @@ -162,6 +162,27 @@ func GetGinContextId(c *gin.Context) *string { return nil } +func GetGinNamespace(c *gin.Context) *string { + if namespace := c.GetHeader("X-Namespace"); namespace != "" { + return &namespace + } + return nil +} + +func GetGinPodname(c *gin.Context) *string { + if podname := c.GetHeader("X-Podname"); podname != "" { + return &podname + } + return nil +} + +func GetGinContainername(c *gin.Context) *string { + if container := c.GetHeader("X-Container"); container != "" { + return &container + } + return nil +} + // func GetGinContextContexts(c *gin.Context) *[]dtos.PunqContext { // if contextArray, exists := c.Get("contexts"); exists { // contexts, ok := contextArray.([]dtos.PunqContext) diff --git a/services/user-service.go b/services/user-service.go index e174183..d25ad66 100644 --- a/services/user-service.go +++ b/services/user-service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "github.com/gin-gonic/gin" @@ -65,9 +66,9 @@ func CreateAdminUser() { password := utils.NanoId() adminUser, _ := AddUser(dtos.PunqUserCreateInput{ - Email: "admin@punq.dev", + Email: fmt.Sprintf("%s-%s@punq.dev", strings.ToLower(utils.RandomFirstName()), strings.ToLower(utils.RandomLastName())), Password: password, - DisplayName: "Admin User", + DisplayName: "ADMIN USER", AccessLevel: dtos.ADMIN, }) diff --git a/utils/config.go b/utils/config.go index 2cec167..a79a7b3 100644 --- a/utils/config.go +++ b/utils/config.go @@ -17,7 +17,7 @@ import ( "github.com/ilyakaznacheev/cleanenv" ) -const CONFIGVERSION = 1 +const CONFIGVERSION = 2 const USERSSECRET = "punq-users" const JWTSECRET = "punq-jwt" const USERADMIN = "admin" @@ -43,6 +43,10 @@ type Config struct { Host string `yaml:"host" env:"backend_host" env-description:"Host of the backend server."` Port int `yaml:"port" env:"backend_port" env-description:"Port of the backend server."` } `yaml:"backend"` + Websocket struct { + Host string `yaml:"host" env:"websocket_host" env-description:"Host of the websocket server."` + Port int `yaml:"port" env:"websocket_port" env-description:"Port of the websocket server."` + } `yaml:"websocket"` Kubernetes struct { ClusterName string `yaml:"cluster_name" env:"cluster_name" env-description:"The Name of the Kubernetes Cluster"` OwnNamespace string `yaml:"own_namespace" env:"OWN_NAMESPACE" env-description:"The Namespace of mogenius platform"` diff --git a/utils/format.go b/utils/format.go index 8476bbc..8414d17 100644 --- a/utils/format.go +++ b/utils/format.go @@ -4,6 +4,7 @@ import ( "fmt" "hash/fnv" "math" + "math/rand" "os" "time" @@ -12,6 +13,85 @@ import ( "github.com/mogenius/punq/logger" ) +var FirstNameList = []string{ + "Liam", "Emma", "Noah", "Olivia", "Ava", + "Sophia", "Jackson", "Aiden", "Lucas", "Muhammad", + "Amelia", "Mateo", "Ethan", "Harper", "Evelyn", + "Mia", "Ella", "Riley", "Aria", "Logan", + "Zoe", "Benjamin", "Oliver", "Lily", "Leo", + "Charlotte", "Mason", "Isabella", "Layla", "Isaac", + "Mila", "Sophie", "Elijah", "Emily", "Daniel", + "James", "Aiden", "Abigail", "Levi", "Chloe", + "Henry", "Alexander", "Sebastian", "Jack", "Hannah", + "Jayden", "Gabriel", "Matthew", "Alice", "Oscar", + "Josiah", "Evie", "Theo", "Isla", "Jaxon", + "Grace", "Eva", "Samuel", "Owen", "Victoria", + "Joseph", "Zachary", "Violet", "John", "William", + "Ezra", "Ellie", "Freya", "Dylan", "Penelope", + "Michael", "Scarlett", "Luna", "Max", "Alyssa", + "Isabelle", "Eliza", "Luca", "Thomas", "Poppy", + "David", "Ruby", "Christopher", "Jade", "Rose", + "Sienna", "George", "Harvey", "Kaylee", "Annie", + "Nathan", "Madison", "Jacob", "Noelle", "Parker", + "Sarah", "Evelina", "Leo", "Ruby", "Abigail", +} + +var MiddleNamesList = []string{ + "Bumblebee", "Rainbow", "Whiz", "Jolly", "Bubbles", + "Sparkle", "Noodle", "Waffle", "Pickle", "Jiggle", + "Twinkle", "Giggle", "Fizzle", "Muffin", "Pumpkin", + "Squiggle", "Tofu", "Jazz", "Fizz", "Sunny", + "Fluffy", "Peanut", "Jellybean", "Snicker", "Ripple", + "Glimmer", "Cupcake", "Pudding", "Tinker", "Pebble", + "Cuddle", "Bumpkin", "Dizzy", "Lolly", "Nugget", + "Twirl", "Fizzypop", "Wiggles", "Snuggles", "Squishy", + "Blinky", "Bubblegum", "Frodo", "Sizzle", "Taco", + "Smiley", "Snickerdoodle", "Wobble", "Popsicle", "Zigzag", + "Sprinkles", "Doodle", "Pizzazz", "Quicksilver", "Razzmatazz", + "Duckling", "Hiccup", "Pumpernickel", "Zoodle", "Quizzical", + "Flitter", "Whisper", "Mustard", "Wacky", "Scooter", + "Moose", "Tizzy", "Bamboo", "Zephyr", "Rolo", + "Sniffle", "Gobble", "Beep", "Cobweb", "Twizzle", + "Bizz", "Fuddle", "Puzzle", "Rumble", "Rover", + "Squabble", "Tumbleweed", "Vroom", "Whizzle", "YoYo", +} + +var LastNamesList = []string{ + "Smith", "Kim", "Johnson", "Lee", "Brown", + "Patel", "Garcia", "Rodriguez", "Martinez", "Chen", + "Jones", "Nguyen", "Williams", "Lopez", "Gonzalez", + "Perez", "Hernandez", "Tanaka", "Silva", "Santos", + "Cohen", "Kumar", "Wang", "Meyer", "Schneider", + "Taylor", "Anderson", "White", "Young", "Harris", + "Clark", "Lewis", "Turner", "Walker", "Hall", + "Allen", "Roberts", "Wright", "King", "Hill", + "Scott", "Green", "Baker", "Adams", "Nelson", + "Campbell", "Mitchell", "Robinson", "Carter", "Thomas", + "Mueller", "Fernandez", "Oliveira", "Sharma", "Singh", + "Liu", "Lin", "Ali", "Khan", "Jackson", + "Parker", "Phillips", "Davis", "Murphy", "Price", + "Suzuki", "Ross", "Reyes", "Jenkins", "Morris", + "Sanchez", "Perry", "Powell", "Russell", "Moore", + "Ramirez", "Gray", "James", "Watson", "Brooks", + "Kelly", "Sanders", "Foster", "Evans", "Barnes", +} + +func RandomFirstName() string { + return FirstNameList[RandomInt(0, len(FirstNameList))] +} + +func RandomMiddleName() string { + return MiddleNamesList[RandomInt(0, len(MiddleNamesList))] +} + +func RandomLastName() string { + return LastNamesList[RandomInt(0, len(LastNamesList))] +} + +func RandomInt(min int, max int) int { + return min + rand.Intn(max-min) +} + func FatalError(message string) { red := color.New(color.FgRed).SprintFunc() fmt.Printf(red("Error: %s\n"), message) diff --git a/utils/routes-utils.go b/utils/routes-utils.go index 506e6ab..1a92ac8 100644 --- a/utils/routes-utils.go +++ b/utils/routes-utils.go @@ -52,6 +52,7 @@ func MissingHeader(c *gin.Context, header string) { c.JSON(http.StatusBadRequest, gin.H{ "err": fmt.Sprintf("Missing header '%s'.", header), }) + c.AbortWithError(http.StatusBadRequest, fmt.Errorf("%s header is required", header)) } func MalformedMessage(c *gin.Context, msg string) { diff --git a/utils/yaml-templates/punq-service.yaml b/utils/yaml-templates/punq-service.yaml index 7b85078..6ce84eb 100644 --- a/utils/yaml-templates/punq-service.yaml +++ b/utils/yaml-templates/punq-service.yaml @@ -13,6 +13,10 @@ spec: - port: 8081 targetPort: 8081 protocol: TCP - name: frontend + name: frontend + - port: 8082 + targetPort: 8082 + protocol: TCP + name: websocket selector: app: punq \ No newline at end of file