Ctrl K
ComunidadGuías…

api/controller/pods.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// RAINBOND, Application Management Platform
// Copyright (C) 2014-2017 Goodrain Co., Ltd.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package controller

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/go-chi/chi"
	"github.com/goodrain/rainbond-operator/util/constants"
	"github.com/goodrain/rainbond/api/handler"
	apimodel "github.com/goodrain/rainbond/api/model"
	ctxutil "github.com/goodrain/rainbond/api/util/ctx"
	"github.com/goodrain/rainbond/db"
	"github.com/goodrain/rainbond/db/model"
	"github.com/goodrain/rainbond/pkg/component/k8s"
	"github.com/goodrain/rainbond/util"
	utils "github.com/goodrain/rainbond/util"
	httputil "github.com/goodrain/rainbond/util/http"
	"github.com/goodrain/rainbond/worker/server"
	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// PodController is an implementation of PodInterface
type PodController struct{}

// Pods get some service pods
// swagger:operation GET /v2/tenants/{tenant_name}/pods v2/tenants pods
//
// 获取一些应用的Pod信息
//
// get some service pods
//
// ---
// consumes:
// - application/json
// - application/x-protobuf
//
// produces:
// - application/json
// - application/xml
//
// responses:
//
//	default:
//	  schema:
//	    "$ref": "#/responses/commandResponse"
//	  description: get some service pods
func Pods(w http.ResponseWriter, r *http.Request) {
	serviceIDs := strings.Split(r.FormValue("service_ids"), ",")
	if serviceIDs == nil || len(serviceIDs) == 0 {
		tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*model.Tenants)
		services, _ := db.GetManager().TenantServiceDao().GetServicesByTenantID(tenant.UUID)
		for _, s := range services {
			serviceIDs = append(serviceIDs, s.ServiceID)
		}
	}
	var allpods []*handler.K8sPodInfo
	podinfo, err := handler.GetServiceManager().GetMultiServicePods(serviceIDs)
	if err != nil {
		logrus.Errorf("get service pod failure %s", err.Error())
	}
	if podinfo != nil {
		var pods []*handler.K8sPodInfo
		if podinfo.OldPods != nil {
			pods = append(podinfo.NewPods, podinfo.OldPods...)
		} else {
			pods = podinfo.NewPods
		}
		for _, pod := range pods {
			allpods = append(allpods, pod)
		}
	}
	httputil.ReturnSuccess(r, w, allpods)
}

// PodNums reutrns the number of pods for components.
func PodNums(w http.ResponseWriter, r *http.Request) {
	componentIDs := strings.Split(r.FormValue("service_ids"), ",")
	podNums, err := handler.GetServiceManager().GetComponentPodNums(r.Context(), componentIDs)
	if err != nil {
		httputil.ReturnBcodeError(r, w, err)
		return
	}
	httputil.ReturnSuccess(r, w, podNums)
}

// SystemPodDetail -
func (p *PodController) SystemPodDetail(w http.ResponseWriter, r *http.Request) {
	ns := r.URL.Query().Get("ns")
	name := r.URL.Query().Get("name")
	list, err := k8s.Default().Clientset.CoreV1().Pods(ns).List(r.Context(), metav1.ListOptions{
		LabelSelector: "name=" + name,
	})
	if err != nil {
		logrus.Errorf("error getting pod detail: %v", err)
		return
	}
	httputil.ReturnSuccess(r, w, list)
}

// PodDetail -
func (p *PodController) PodDetail(w http.ResponseWriter, r *http.Request) {
	podName := chi.URLParam(r, "pod_name")

	tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*model.Tenants)
	pd, err := handler.GetPodHandler().PodDetail(tenant.Namespace, podName)
	if err != nil {
		logrus.Errorf("error getting pod detail: %v", err)
		if err == server.ErrPodNotFound {
			httputil.ReturnError(r, w, 404, fmt.Sprintf("error getting pod detail: %v", err))
			return
		}
		httputil.ReturnError(r, w, 500, fmt.Sprintf("error getting pod detail: %v", err))
		return
	}
	httputil.ReturnSuccess(r, w, pd)
}

func logs(w http.ResponseWriter, r *http.Request, podName string, namespace string) {
	lines, err := strconv.Atoi(r.URL.Query().Get("lines"))
	if err != nil {
		lines = 100
	}
	tailLines := int64(lines)
	follow, err := parseFollow(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// previous=true returns logs of the last terminated instance of the
	// container, used for crash diagnosis. Following previous logs is not
	// meaningful, so follow is forced off in that case.
	previous := parsePrevious(r)
	if previous {
		follow = false
	}

	// Get container name from query parameter
	container := r.URL.Query().Get("container")
	logrus.Infof(
		"resource center pod logs request path=%s namespace=%s pod=%s container=%s lines=%d follow=%t previous=%t",
		r.URL.String(), namespace, podName, container, lines, follow, previous,
	)

	// Get pod to check how many containers it has
	pod, err := k8s.Default().Clientset.CoreV1().Pods(namespace).Get(r.Context(), podName, metav1.GetOptions{})
	if err != nil {
		logrus.Errorf("Error getting pod %s: %v", podName, err)
		http.Error(w, fmt.Sprintf("Error getting pod: %v", err), http.StatusInternalServerError)
		return
	}

	// Determine which containers to stream logs from
	var containers []string
	if container != "" {
		// Use specified container
		containers = append(containers, container)
		logrus.Infof("Streaming logs from specified container: %s", container)
	} else {
		// Special handling for rbd-gateway pods
		if strings.HasPrefix(podName, "rbd-gateway") {
			containers = append(containers, "apisix")
			logrus.Infof("rbd-gateway pod detected, using container: ingress-apisix")
		} else {
			// Default behavior: stream all containers
			for _, c := range pod.Spec.Containers {
				containers = append(containers, c.Name)
			}
			logrus.Infof("No container specified, streaming logs from all containers: %v", containers)
		}
	}

	if len(containers) == 0 {
		http.Error(w, "No containers found in pod", http.StatusNotFound)
		return
	}

	// Use Flusher to send headers to the client
	flusher, ok := w.(http.Flusher)
	if !ok {
		logrus.Errorf("Streaming not supported")
		http.Error(w, "Streaming not supported", http.StatusInternalServerError)
		return
	}

	// Set headers for SSE
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	// If single container, use original logic
	if len(containers) == 1 {
		streamContainerLogs(w, r, podName, namespace, containers[0], tailLines, follow, previous, flusher)
		return
	}

	// For multiple containers, merge streams
	logrus.Infof("Opening log streams for pod %s with %d containers", podName, len(containers))

	// Create a channel to merge logs from all containers
	logChan := make(chan string, 100)
	doneChan := make(chan struct{})

	// Start goroutine for each container
	for _, containerName := range containers {
		go func(cName string) {
			req := k8s.Default().Clientset.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{
				Follow:     follow,
				Timestamps: true,
				TailLines:  &tailLines,
				Container:  cName,
				Previous:   previous,
			})

			stream, err := req.Stream(r.Context())
			if err != nil {
				logrus.Errorf("Error opening log stream for container %s: %v", cName, err)
				return
			}
			defer stream.Close()

			scanner := bufio.NewScanner(stream)
			// Increase buffer size to handle large log lines
			buf := make([]byte, 0, 64*1024)
			scanner.Buffer(buf, 1024*1024)

			for scanner.Scan() {
				select {
				case <-r.Context().Done():
					return
				case <-doneChan:
					return
				default:
					// Prefix log line with container name and sanitize non-UTF8 bytes
					logLine := fmt.Sprintf("[%s] %s", cName, sanitizeLogLine(scanner.Text()))
					logChan <- logLine
				}
			}
			if err := scanner.Err(); err != nil {
				if isExpectedLogStreamClose(r.Context(), err) {
					logrus.Infof("Log stream closed for container %s in pod %s/%s: %v", cName, namespace, podName, err)
					return
				}
				// Log transport errors at warning level since they're often transient
				if isTransportError(err) {
					logrus.Warningf("Transport error in log stream for container %s in pod %s/%s: %v", cName, namespace, podName, err)
					return
				}
				logrus.Errorf("Error scanning log stream for container %s in pod %s/%s: %v", cName, namespace, podName, err)
			} else {
				logrus.Infof("Log stream ended for container %s in pod %s/%s", cName, namespace, podName)
			}
		}(containerName)
	}

	// Stream merged logs to client
	for {
		select {
		case <-r.Context().Done():
			close(doneChan)
			logrus.Warningf("Request context done: %v", r.Context().Err())
			return
		case logLine, ok := <-logChan:
			if !ok {
				// Channel closed, all container streams ended
				logrus.Infof("All container log streams ended for pod %s/%s", namespace, podName)
				return
			}
			msg := "data: " + logLine + "\n\n"
			_, err := fmt.Fprint(w, msg)
			flusher.Flush()
			if err != nil {
				if isTransportError(err) {
					logrus.Warningf("Client disconnected during log stream for pod %s/%s: %v", namespace, podName, err)
					close(doneChan)
					return
				}
				logrus.Errorf("Error writing to response: %v", err)
				close(doneChan)
				return
			}
		}
	}
}

// streamContainerLogs streams logs from a single container
func streamContainerLogs(
	w http.ResponseWriter, r *http.Request, podName, namespace, container string, tailLines int64, follow, previous bool,
	flusher http.Flusher,
) {
	req := k8s.Default().Clientset.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{
		Follow:     follow,
		Timestamps: true,
		TailLines:  &tailLines,
		Container:  container,
		Previous:   previous,
	})
	logrus.Infof("Opening log stream for pod %s, container %s", podName, container)

	stream, err := req.Stream(r.Context())
	if err != nil {
		logrus.Errorf("Error opening log stream: %v", err)
		http.Error(w, "Error opening log stream", http.StatusInternalServerError)
		return
	}
	defer stream.Close()

	scanner := bufio.NewScanner(stream)
	// Increase buffer size to handle large log lines
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, 1024*1024)

	for scanner.Scan() {
		select {
		case <-r.Context().Done():
			logrus.Warningf("Request context done: %v", r.Context().Err())
			return
		default:
			// Sanitize non-UTF8 bytes to prevent encoding errors
			logLine := sanitizeLogLine(scanner.Text())
			msg := "data: " + logLine + "\n\n"
			_, err := fmt.Fprint(w, msg)
			flusher.Flush()
			if err != nil {
				if isTransportError(err) {
					logrus.Warningf("Client disconnected during log stream for pod %s/%s container %s: %v", namespace, podName, container, err)
					return
				}
				logrus.Errorf("Error writing to response: %v", err)
				return
			}
		}
	}
	if err := scanner.Err(); err != nil {
		if isExpectedLogStreamClose(r.Context(), err) {
			logrus.Infof("Single-container log stream closed for pod %s/%s container %s: %v", namespace, podName, container, err)
			return
		}
		// Log transport errors at warning level since they're often transient
		if isTransportError(err) {
			logrus.Warningf("Transport error in single-container log stream for pod %s/%s container %s: %v", namespace, podName, container, err)
			return
		}
		logrus.Errorf("Error scanning single-container log stream for pod %s/%s container %s: %v", namespace, podName, container, err)
	} else {
		logrus.Infof("Single-container log stream ended for pod %s/%s container %s", namespace, podName, container)
	}
}

func parseFollow(r *http.Request) (bool, error) {
	followValue := r.URL.Query().Get("follow")
	if followValue == "" {
		return true, nil
	}
	follow, err := strconv.ParseBool(followValue)
	if err != nil {
		return false, fmt.Errorf("invalid follow value: %s", followValue)
	}
	return follow, nil
}

// parsePrevious reports whether the caller requested logs of the previously
// terminated container instance via previous=true. Defaults to false; any
// unparseable value is treated as false.
func parsePrevious(r *http.Request) bool {
	previousValue := r.URL.Query().Get("previous")
	if previousValue == "" {
		return false
	}
	previous, err := strconv.ParseBool(previousValue)
	if err != nil {
		return false
	}
	return previous
}

func isExpectedLogStreamClose(ctx context.Context, err error) bool {
	if err == nil {
		return false
	}
	if ctx != nil && errors.Is(ctx.Err(), context.Canceled) {
		return true
	}
	return errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed)
}

// sanitizeLogLine replaces invalid UTF-8 sequences with the Unicode replacement
// character so that log lines can be safely written to SSE responses without
// causing encoding errors on the client side.
func sanitizeLogLine(line string) string {
	if utf8.ValidString(line) {
		return line
	}
	var buf bytes.Buffer
	buf.Grow(len(line))
	for i := 0; i < len(line); {
		r, size := utf8.DecodeRuneInString(line[i:])
		if r == utf8.RuneError && size == 1 {
			buf.WriteRune('�') // Unicode replacement character
			i++
		} else {
			buf.WriteRune(r)
			i += size
		}
	}
	return buf.String()
}

// isTransportError checks if the error is a transport-level error that indicates
// a connection issue (connection reset, broken pipe, etc.)
func isTransportError(err error) bool {
	if err == nil {
		return false
	}
	var netErr net.Error
	if errors.As(err, &netErr) {
		return true
	}
	// Check for common transport error strings
	errStr := err.Error()
	return strings.Contains(errStr, "connection reset") ||
		strings.Contains(errStr, "broken pipe") ||
		strings.Contains(errStr, "use of closed network connection")
}

// SystemPodLogs -
func (p *PodController) SystemPodLogs(w http.ResponseWriter, r *http.Request) {
	ns, err := util.GetMyNamespace()
	if err != nil {
		httputil.ReturnError(r, w, 500, fmt.Sprintf("error getting namespace: %v", err))
		return
	}
	if ns == "" {
		ns = utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)
	}
	name := r.URL.Query().Get("name")
	logs(w, r, name, ns)
}

// PodLogs -
func (p *PodController) PodLogs(w http.ResponseWriter, r *http.Request) {
	tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*model.Tenants)
	logs(w, r, chi.URLParam(r, "pod_name"), tenant.Namespace)
}

// PodExec runs a one-shot, non-interactive command in a pod container and
// returns the captured stdout/stderr, the container exit code, and whether the
// output was truncated. Authorization is enforced by the existing
// InitTenant/InitService middleware; arbitrary commands are allowed, mirroring
// the Web Terminal. Safety rails: context timeout, output cap, audit log.
func (p *PodController) PodExec(w http.ResponseWriter, r *http.Request) {
	podName := chi.URLParam(r, "pod_name")
	tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*model.Tenants)
	service := r.Context().Value(ctxutil.ContextKey("service")).(*model.TenantServices)

	var req apimodel.PodExecRequest
	if !httputil.ValidatorRequestStructAndErrorResponse(r, w, &req, nil) {
		return
	}
	if len(req.Command) == 0 {
		httputil.ReturnError(r, w, 400, "command must not be empty")
		return
	}

	// Default to the component's main container when none is specified.
	containerName := req.Container
	if containerName == "" {
		containerName = service.K8sComponentName
	}

	stdout, stderr, exitCode, truncated, err := handler.GetServiceManager().ExecCommand(
		podName, tenant.Namespace, containerName, req.Command, req.TimeoutSeconds,
	)
	if err != nil {
		// Distinguish the not-running case so callers can fall back to
		// previous-container logs.
		if errors.Is(err, handler.ErrContainerNotRunning) {
			logrus.Warningf(
				"pod exec on non-running container: tenant=%s service=%s pod=%s container=%s err=%v",
				tenant.Name, service.ServiceAlias, podName, containerName, err,
			)
			httputil.ReturnError(r, w, 409, fmt.Sprintf("container not running: %v", err))
			return
		}
		logrus.Errorf(
			"pod exec failed: tenant=%s service=%s pod=%s container=%s command=%v err=%v",
			tenant.Name, service.ServiceAlias, podName, containerName, req.Command, err,
		)
		httputil.ReturnError(r, w, 500, fmt.Sprintf("exec failed: %v", err))
		return
	}

	// Audit log: one structured line for traceability.
	logrus.Infof(
		"pod exec audit: tenant=%s service=%s pod=%s container=%s command=%v exit_code=%d truncated=%t",
		tenant.Name, service.ServiceAlias, podName, containerName, req.Command, exitCode, truncated,
	)

	httputil.ReturnSuccess(r, w, &apimodel.PodExecResult{
		Stdout:    stdout,
		Stderr:    stderr,
		ExitCode:  exitCode,
		Truncated: truncated,
	})
}