api/handler/cluster.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
rainbondv1alpha1 "github.com/goodrain/rainbond-operator/api/v1alpha1"
"github.com/goodrain/rainbond-operator/util/rbdutil"
"github.com/goodrain/rainbond/api/client/prometheus"
"github.com/goodrain/rainbond/api/model"
"github.com/goodrain/rainbond/api/util"
"github.com/goodrain/rainbond/api/util/bcode"
"github.com/goodrain/rainbond/config/configs"
"github.com/goodrain/rainbond/db"
dbmodel "github.com/goodrain/rainbond/db/model"
"github.com/goodrain/rainbond/grctl/clients"
mqclient "github.com/goodrain/rainbond/mq/client"
"github.com/goodrain/rainbond/pkg/apis/rainbond/v1alpha1"
"github.com/goodrain/rainbond/pkg/component/grpc"
"github.com/goodrain/rainbond/pkg/component/k8s"
"github.com/goodrain/rainbond/pkg/component/mq"
"github.com/goodrain/rainbond/pkg/component/prom"
"github.com/goodrain/rainbond/pkg/generated/clientset/versioned"
utils "github.com/goodrain/rainbond/util"
"github.com/goodrain/rainbond/util/constants"
workerclient "github.com/goodrain/rainbond/worker/client"
"github.com/goodrain/rainbond/worker/server/pb"
"github.com/pkg/errors"
"github.com/shirou/gopsutil/v4/disk"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/util/flushwriter"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/typed/apis/v1beta1"
)
const (
// OfficialPluginLabel Those with this label are official plug-ins
OfficialPluginLabel = "plugin.rainbond.io/name"
)
// NamespaceInfo holds name and creation timestamp of a namespace
type NamespaceInfo struct {
Name string `json:"name"`
CreationTimestamp string `json:"creation_timestamp"`
}
// ClusterHandler -
type ClusterHandler interface {
GetClusterInfo(ctx context.Context) (*model.ClusterResource, error)
MavenSettingAdd(ctx context.Context, ms *MavenSetting) *util.APIHandleError
MavenSettingList(ctx context.Context) (re []MavenSetting)
MavenSettingUpdate(ctx context.Context, ms *MavenSetting) *util.APIHandleError
MavenSettingDelete(ctx context.Context, name string) *util.APIHandleError
MavenSettingDetail(ctx context.Context, name string) (*MavenSetting, *util.APIHandleError)
BatchGetGateway(ctx context.Context) ([]*model.GatewayResource, *util.APIHandleError)
GetNamespace(ctx context.Context, content string) ([]string, *util.APIHandleError)
GetNamespaceDetail(ctx context.Context, content string) ([]NamespaceInfo, *util.APIHandleError)
GetNamespaceSource(ctx context.Context, content string, namespace string) (map[string]model.LabelResource, *util.APIHandleError)
ConvertResource(ctx context.Context, namespace string, lr map[string]model.LabelResource) (map[string]model.ApplicationResource, *util.APIHandleError)
ResourceImport(namespace string, as map[string]model.ApplicationResource, eid string) (*model.ReturnResourceImport, *util.APIHandleError)
AddAppK8SResource(ctx context.Context, namespace string, appID string, resourceYaml string) ([]*dbmodel.K8sResource, *util.APIHandleError)
DeleteAppK8SResource(ctx context.Context, namespace, appID, name, yaml, kind string)
PreviewK8SResourceDeletion(ctx context.Context, req *model.K8sResourceDeletionRequest) (*model.K8sResourceDeletionImpact, *util.APIHandleError)
DeleteK8SResources(ctx context.Context, req *model.K8sResourceDeletionRequest) (*model.K8sResourceDeletionResult, *util.APIHandleError)
ReconcileK8SResources(ctx context.Context, req *model.K8sResourceReconcileRequest) (*model.K8sResourceReconcileResult, *util.APIHandleError)
GetAppK8SResource(ctx context.Context, namespace, appID, name, resourceYaml, kind string) (dbmodel.K8sResource, *util.APIHandleError)
UpdateAppK8SResource(ctx context.Context, namespace, appID, name, resourceYaml, kind string) (dbmodel.K8sResource, *util.APIHandleError)
SyncAppK8SResources(ctx context.Context, resources *model.SyncResources) ([]*dbmodel.K8sResource, *util.APIHandleError)
AppYamlResourceName(yamlResource model.YamlResource) (map[string]model.LabelResource, *util.APIHandleError)
AppYamlResourceDetailed(yamlResource model.YamlResource, yamlImport bool) (model.ApplicationResource, *util.APIHandleError)
AppYamlResourceImport(namespace, tenantID, appID string, components model.ApplicationResource) (model.AppComponent, *util.APIHandleError)
RbdLog(w http.ResponseWriter, r *http.Request, podName string, follow bool) error
GetRbdPods() (rbds []model.RbdResp, err error)
CreateShellPod(regionName string) (pod *corev1.Pod, err error)
DeleteShellPod(podName string) error
ListPlugins(official bool) (rbds []*model.RainbondPlugins, err error)
PluginExists(name string) (bool, error)
CreatePlugin(req *model.CreateRBDPluginReq) error
ListAbilities() (rbds []unstructured.Unstructured, err error)
GetAbility(abilityID string) (rbd *unstructured.Unstructured, err error)
UpdateAbility(abilityID string, ability *unstructured.Unstructured) error
GenerateAbilityID(ability *unstructured.Unstructured) string
ListRainbondComponents(ctx context.Context) (res []*model.RainbondComponent, err error)
ListUpgradeStatus() ([]model.ComponentStatus, error)
GetClusterRegionStatus() (map[string]interface{}, error)
CheckReadiness(ctx context.Context) (*model.RegionReadiness, error)
BootstrapAgentKubeconfig(ctx context.Context, req *AgentKubeconfigBootstrapRequest) (*AgentKubeconfigBootstrapResponse, error)
}
// NewClusterHandler -
func NewClusterHandler() ClusterHandler {
return &clusterAction{
namespace: configs.Default().PublicConfig.RbdNamespace,
clientset: k8s.Default().Clientset,
config: k8s.Default().RestConfig,
mapper: k8s.Default().Mapper,
client: k8s.Default().K8sClient,
grctlImage: configs.Default().APIConfig.GrctlImage,
prometheusCli: prom.Default().PrometheusCli,
rainbondClient: k8s.Default().RainbondClient,
statusCli: grpc.Default().StatusClient,
dynamicClient: k8s.Default().DynamicClient,
gatewayClient: k8s.Default().GatewayClient,
mqclient: mq.Default().MqClient,
}
}
type clusterAction struct {
namespace string
clientset *kubernetes.Clientset
clusterInfoCache *model.ClusterResource
cacheTime time.Time
cacheMu sync.Mutex
infoRefresh *clusterInfoRefresh
diskUsage func(string) (*disk.UsageStat, error)
localDisk clusterLocalDiskSample
config *rest.Config
mapper meta.RESTMapper
grctlImage string
client client.Client
prometheusCli prometheus.Interface
rainbondClient versioned.Interface
statusCli *workerclient.AppRuntimeSyncClient
dynamicClient dynamic.Interface
gatewayClient *gateway.GatewayV1beta1Client
mqclient mqclient.MQClient
}
type clusterNodePodResources struct {
PodNumber int64
Memory int64
CPU int64
EphemeralStorage int64
RainbondMemory int64
RainbondCPU int64
}
// GetClusterInfo -
func (c *clusterAction) GetClusterInfo(ctx context.Context) (*model.ClusterResource, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
timeout, _ := strconv.Atoi(os.Getenv("CLUSTER_INFO_CACHE_TIME"))
if timeout <= 0 {
timeout = 30
}
c.cacheMu.Lock()
if c.clusterInfoCache != nil && c.cacheTime.Add(time.Second*time.Duration(timeout)).After(time.Now()) {
result := c.clusterInfoCache
c.cacheMu.Unlock()
return c.withLocalDiskStats(result), nil
}
if pending := c.infoRefresh; pending != nil {
c.cacheMu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-pending.done:
return c.withLocalDiskStats(pending.result), pending.err
}
}
pending := &clusterInfoRefresh{done: make(chan struct{})}
c.infoRefresh = pending
c.cacheMu.Unlock()
queryCtx, cancel := context.WithTimeout(ctx, clusterInfoQueryTimeout)
defer cancel()
result, err := c.loadClusterInfo(queryCtx)
c.cacheMu.Lock()
if err == nil {
c.clusterInfoCache = result
c.cacheTime = time.Now()
}
pending.result, pending.err = result, err
c.infoRefresh = nil
close(pending.done)
c.cacheMu.Unlock()
return c.withLocalDiskStats(result), err
}
func (c *clusterAction) getClusterPodResources(instance string) map[string]*clusterNodePodResources {
queries := []struct {
expr string
apply func(*clusterNodePodResources, float64)
}{
{
expr: fmt.Sprintf(`count(rbd_api_exporter_cluster_pod_memory{instance="%v"}) by (node_name)`, instance),
apply: func(resources *clusterNodePodResources, value float64) {
resources.PodNumber = int64(value)
},
},
{
expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_memory{instance="%v"}) by (node_name)`, instance),
apply: func(resources *clusterNodePodResources, value float64) {
resources.Memory = int64(value)
resources.RainbondMemory = int64(value)
},
},
{
expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_cpu{instance="%v"}) by (node_name)`, instance),
apply: func(resources *clusterNodePodResources, value float64) {
resources.CPU = int64(value)
resources.RainbondCPU = int64(value)
},
},
{
expr: fmt.Sprintf(`sum(rbd_api_exporter_cluster_pod_ephemeral_storage{instance="%v"}) by (node_name)`, instance),
apply: func(resources *clusterNodePodResources, value float64) {
resources.EphemeralStorage = int64(value)
},
},
}
metrics := make([]prometheus.Metric, len(queries))
var wg sync.WaitGroup
for i := range queries {
wg.Add(1)
go func(index int) {
defer wg.Done()
metrics[index] = c.prometheusCli.GetMetric(queries[index].expr, time.Now())
}(i)
}
wg.Wait()
result := make(map[string]*clusterNodePodResources)
for i, metric := range metrics {
for _, value := range metric.MetricData.MetricValues {
nodeName := value.Metadata["node_name"]
if nodeName == "" || value.Sample == nil {
continue
}
resources := result[nodeName]
if resources == nil {
resources = &clusterNodePodResources{}
result[nodeName] = resources
}
queries[i].apply(resources, value.Sample.Value())
}
}
return result
}
func (c *clusterAction) listNodes(ctx context.Context) ([]*corev1.Node, error) {
opts := metav1.ListOptions{}
nodeList, err := c.clientset.CoreV1().Nodes().List(ctx, opts)
if err != nil {
return nil, err
}
var nodes []*corev1.Node
for idx := range nodeList.Items {
node := &nodeList.Items[idx]
nodes = append(nodes, node)
}
return nodes, nil
}
func isNodeReady(node *corev1.Node) bool {
for _, cond := range node.Status.Conditions {
if cond.Type == corev1.NodeReady && cond.Status == corev1.ConditionTrue {
return true
}
}
return false
}
func containsTaints(node *corev1.Node) bool {
for _, taint := range node.Spec.Taints {
if taint.Effect == corev1.TaintEffectNoSchedule && taint.Key != "node.kubernetes.io/unschedulable" {
return true
}
}
return false
}
func (c *clusterAction) listPods(ctx context.Context, nodeName string) (pods []corev1.Pod, err error) {
podList, err := c.clientset.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{
FieldSelector: fields.SelectorFromSet(fields.Set{"spec.nodeName": nodeName}).String()})
if err != nil {
return pods, err
}
return podList.Items, nil
}
// MavenSetting maven setting
type MavenSetting struct {
Name string `json:"name" validate:"required"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
Content string `json:"content" validate:"required"`
IsDefault bool `json:"is_default"`
}
// MavenSettingList maven setting list
func (c *clusterAction) MavenSettingList(ctx context.Context) (re []MavenSetting) {
cms, err := c.clientset.CoreV1().ConfigMaps(c.namespace).List(ctx, metav1.ListOptions{
LabelSelector: "configtype=mavensetting",
})
if err != nil {
logrus.Errorf("list maven setting config list failure %s", err.Error())
}
for _, sm := range cms.Items {
isDefault := false
if sm.Labels["default"] == "true" {
isDefault = true
}
re = append(re, MavenSetting{
Name: sm.Name,
CreateTime: sm.CreationTimestamp.Format(time.RFC3339),
UpdateTime: sm.Labels["updateTime"],
Content: sm.Data["mavensetting"],
IsDefault: isDefault,
})
}
return
}
// MavenSettingAdd maven setting add
func (c *clusterAction) MavenSettingAdd(ctx context.Context, ms *MavenSetting) *util.APIHandleError {
config := &corev1.ConfigMap{}
config.Name = ms.Name
config.Namespace = c.namespace
config.Labels = map[string]string{
"creator": "Rainbond",
"configtype": "mavensetting",
}
config.Annotations = map[string]string{
"updateTime": time.Now().Format(time.RFC3339),
}
config.Data = map[string]string{
"mavensetting": ms.Content,
}
_, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Create(ctx, config, metav1.CreateOptions{})
if err != nil {
if apierrors.IsAlreadyExists(err) {
return &util.APIHandleError{Code: 400, Err: fmt.Errorf("setting name is exist")}
}
logrus.Errorf("create maven setting configmap failure %s", err.Error())
return &util.APIHandleError{Code: 500, Err: fmt.Errorf("create setting config failure")}
}
ms.CreateTime = time.Now().Format(time.RFC3339)
ms.UpdateTime = time.Now().Format(time.RFC3339)
return nil
}
// MavenSettingUpdate maven setting file update
func (c *clusterAction) MavenSettingUpdate(ctx context.Context, ms *MavenSetting) *util.APIHandleError {
sm, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Get(ctx, ms.Name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return &util.APIHandleError{Code: 404, Err: fmt.Errorf("setting name is not exist")}
}
logrus.Errorf("get maven setting config list failure %s", err.Error())
return &util.APIHandleError{Code: 400, Err: fmt.Errorf("get setting failure")}
}
if sm.Data == nil {
sm.Data = make(map[string]string)
}
if sm.Annotations == nil {
sm.Annotations = make(map[string]string)
}
sm.Data["mavensetting"] = ms.Content
sm.Annotations["updateTime"] = time.Now().Format(time.RFC3339)
if _, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Update(ctx, sm, metav1.UpdateOptions{}); err != nil {
logrus.Errorf("update maven setting configmap failure %s", err.Error())
return &util.APIHandleError{Code: 500, Err: fmt.Errorf("update setting config failure")}
}
ms.UpdateTime = sm.Annotations["updateTime"]
ms.CreateTime = sm.CreationTimestamp.Format(time.RFC3339)
return nil
}
// MavenSettingDelete maven setting file delete
func (c *clusterAction) MavenSettingDelete(ctx context.Context, name string) *util.APIHandleError {
err := c.clientset.CoreV1().ConfigMaps(c.namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return &util.APIHandleError{Code: 404, Err: fmt.Errorf("setting not found")}
}
logrus.Errorf("delete maven setting config list failure %s", err.Error())
return &util.APIHandleError{Code: 500, Err: fmt.Errorf("setting delete failure")}
}
return nil
}
// MavenSettingDetail maven setting file delete
func (c *clusterAction) MavenSettingDetail(ctx context.Context, name string) (*MavenSetting, *util.APIHandleError) {
sm, err := c.clientset.CoreV1().ConfigMaps(c.namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
logrus.Errorf("get maven setting config failure %s", err.Error())
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("setting not found")}
}
return &MavenSetting{
Name: sm.Name,
CreateTime: sm.CreationTimestamp.Format(time.RFC3339),
UpdateTime: sm.Annotations["updateTime"],
Content: sm.Data["mavensetting"],
}, nil
}
func (c *clusterAction) BatchGetGateway(ctx context.Context) ([]*model.GatewayResource, *util.APIHandleError) {
gateways, err := c.gatewayClient.Gateways(corev1.NamespaceAll).List(ctx, metav1.ListOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil, nil
}
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("failed to batch get gateway:%v", err)}
}
var gatewayList []*model.GatewayResource
nodes, err := c.clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{LabelSelector: "rainbond-gateway-node=true"})
if err != nil {
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("failed to batch get nodes:%v", err)}
}
if len(nodes.Items) == 0 {
nodes, err = c.clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("failed to batch get nodes:%v", err)}
}
}
nodeIP := "0.0.0.0"
if len(nodes.Items) > 0 {
addresses := nodes.Items[0].Status.Addresses
if addresses != nil {
for _, address := range addresses {
if address.Type == corev1.NodeExternalIP {
nodeIP = address.Address
break
}
if address.Type == corev1.NodeInternalIP {
nodeIP = address.Address
}
}
}
}
for _, gc := range gateways.Items {
serviceName, ok := gc.Labels["service-name"]
var loadBalancerIP []string
var nodePort []string
if ok {
var gatewaySvc corev1.Service
gatewaySvcList, err := c.clientset.CoreV1().Services(gc.Namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
logrus.Errorf("get gateway(%v) service(%v) failure: %v", gc.Name, serviceName, err)
}
for _, gatewayService := range gatewaySvcList.Items {
if strings.HasPrefix(gatewayService.Name, serviceName) {
externalIPs := gatewaySvc.Spec.ExternalIPs
ports := gatewayService.Spec.Ports
if ports != nil {
for _, port := range ports {
if port.NodePort != 0 {
nodePort = append(nodePort, fmt.Sprintf("%v:%v(%v)", nodeIP, port.NodePort, port.Name))
}
if externalIPs != nil {
for _, ip := range externalIPs {
loadBalancerIP = append(loadBalancerIP, fmt.Sprintf("%v:%v(%v)", ip, port.Port, port.Name))
}
}
}
}
break
}
}
}
var listenerNames []string
for _, listener := range gc.Spec.Listeners {
name := string(listener.Name)
listenerNames = append(listenerNames, name)
}
gatewayList = append(gatewayList, &model.GatewayResource{
Name: gc.GetName(),
Namespace: gc.GetNamespace(),
LoadBalancerIP: loadBalancerIP,
NodePortIP: nodePort,
ListenerNames: listenerNames,
})
}
return gatewayList, nil
}
// isExcludedNamespace reports whether ns should be excluded from namespace listings.
// When content == "unmanaged", also excludes Rainbond-managed namespaces.
func isExcludedNamespace(ns corev1.Namespace, content string) bool {
if strings.HasPrefix(ns.Name, "kube-") || ns.Name == "rainbond" || ns.Name == utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace) {
return true
}
if labelValue, ok := ns.Labels[constants.ResourceManagedByLabel]; ok && labelValue == "rainbond" && content == "unmanaged" {
return true
}
return false
}
// GetNamespace Get namespace of the current cluster
func (c *clusterAction) GetNamespace(ctx context.Context, content string) ([]string, *util.APIHandleError) {
namespaceList, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("failed to get namespace: %w", err)}
}
namespaces := new([]string)
for _, ns := range namespaceList.Items {
if isExcludedNamespace(ns, content) {
continue
}
*namespaces = append(*namespaces, ns.Name)
}
return *namespaces, nil
}
// GetNamespaceDetail Get unmanaged namespaces with creation timestamp
func (c *clusterAction) GetNamespaceDetail(ctx context.Context, content string) ([]NamespaceInfo, *util.APIHandleError) {
namespaceList, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, &util.APIHandleError{Code: 404, Err: fmt.Errorf("failed to get namespace: %w", err)}
}
result := make([]NamespaceInfo, 0)
for _, ns := range namespaceList.Items {
if isExcludedNamespace(ns, content) {
continue
}
result = append(result, NamespaceInfo{
Name: ns.Name,
CreationTimestamp: ns.CreationTimestamp.UTC().Format(time.RFC3339),
})
}
return result, nil
}
// MergeMap map去重合并
func MergeMap(map1 map[string][]string, map2 map[string][]string) map[string][]string {
for k, v := range map1 {
if _, ok := map2[k]; ok {
map2[k] = append(map2[k], v...)
continue
}
map2[k] = v
}
return map2
}
// CreateShellPod -
func (c *clusterAction) CreateShellPod(regionName string) (pod *corev1.Pod, err error) {
ctx := context.Background()
labels := make(map[string]string)
labels["app.kubernetes.io/part-of"] = "shell-tool"
shellPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: buildShellPodGenerateName(regionName),
Namespace: c.namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
TerminationGracePeriodSeconds: new(int64),
RestartPolicy: corev1.RestartPolicyNever,
NodeSelector: map[string]string{
"kubernetes.io/os": "linux",
},
ServiceAccountName: "rainbond-operator",
Containers: []corev1.Container{
{
Name: "shell",
TTY: true,
Stdin: true,
StdinOnce: true,
Image: c.grctlImage,
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
},
}
pod, err = c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).Create(ctx, shellPod, metav1.CreateOptions{})
if err != nil {
logrus.Error("create shell pod error:", err)
return nil, err
}
return pod, nil
}
func buildShellPodGenerateName(regionName string) string {
return fmt.Sprintf("shell-%s-", strings.ToLower(regionName))
}
// DeleteShellPod -
func (c *clusterAction) DeleteShellPod(podName string) (err error) {
err = c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).Delete(context.Background(), podName, metav1.DeleteOptions{})
if err != nil {
logrus.Error("delete shell pod error:", err)
return err
}
return nil
}
// RbdLog returns the logs reader for a container in a pod, a pod or a component.
func (c *clusterAction) RbdLog(w http.ResponseWriter, r *http.Request, podName string, follow bool) error {
if podName == "" {
// Only support return the logs reader for a container now.
return errors.WithStack(bcode.NewBadRequest("the field 'podName' and 'containerName' is required"))
}
request := c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).GetLogs(podName, &corev1.PodLogOptions{
Follow: follow,
})
out, err := request.Stream(context.TODO())
if err != nil {
if apierrors.IsNotFound(err) {
return errors.Wrap(bcode.ErrPodNotFound, "get pod log")
}
return errors.Wrap(err, "get stream from request")
}
defer out.Close()
w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK)
// Flush headers, if possible
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
writer := flushwriter.Wrap(w)
_, err = io.Copy(writer, out)
if err != nil {
if strings.HasSuffix(err.Error(), "write: broken pipe") {
return nil
}
logrus.Warningf("write stream to response: %v", err)
}
return nil
}
// GetRbdPods -
func (c *clusterAction) GetRbdPods() (rbds []model.RbdResp, err error) {
pods, err := c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).List(context.Background(), metav1.ListOptions{})
if err != nil {
logrus.Error("get rbd pod list error:", err)
return nil, err
}
var rbd model.RbdResp
for _, pod := range pods.Items {
if strings.Contains(pod.Name, "rbd-chaos") || strings.Contains(pod.Name, "rbd-api") || strings.Contains(pod.Name, "rbd-worker") || strings.Contains(pod.Name, "rbd-gateway") {
rbdSplit := strings.Split(pod.Name, "-")
rbdName := fmt.Sprintf("%s-%s", rbdSplit[0], rbdSplit[1])
rbd.RbdName = rbdName
rbd.PodName = pod.Name
rbd.NodeName = pod.Spec.NodeName
rbds = append(rbds, rbd)
}
}
return rbds, nil
}
func (c *clusterAction) ListRainbondComponents(ctx context.Context) (res []*model.RainbondComponent, err error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// rainbond components
podList, err := c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).List(ctx, metav1.ListOptions{
LabelSelector: fields.SelectorFromSet(rbdutil.LabelsForRainbond(nil)).String(),
})
if err != nil {
return nil, errors.WithStack(err)
}
// rainbond operator
roPods, err := c.clientset.CoreV1().Pods(utils.GetenvDefault("RBD_NAMESPACE", constants.Namespace)).List(ctx, metav1.ListOptions{
LabelSelector: fields.SelectorFromSet(map[string]string{
"name": "rainbond-operator",
}).String(),
})
if err != nil {
return nil, errors.WithStack(err)
}
allPods := make([]corev1.Pod, 0, len(podList.Items)+len(roPods.Items))
allPods = append(allPods, podList.Items...)
allPods = append(allPods, roPods.Items...)
allPods = deduplicateRainbondComponentPods(allPods)
res = finalizeRainbondComponentOverview(buildRainbondComponentOverview(allPods))
return res, nil
}
func deduplicateRainbondComponentPods(pods []corev1.Pod) []corev1.Pod {
result := make([]corev1.Pod, 0, len(pods))
seen := make(map[string]struct{}, len(pods))
for _, pod := range pods {
key := "uid:" + string(pod.UID)
if pod.UID == "" {
key = "name:" + pod.Namespace + "/" + pod.Name
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, pod)
}
return result
}
func finalizeRainbondComponentOverview(components []*model.RainbondComponent) []*model.RainbondComponent {
result := append([]*model.RainbondComponent(nil), components...)
operatorFound := false
for _, component := range result {
if component.Name == "rainbond-operator" {
operatorFound = true
break
}
}
if !operatorFound {
result = append(result, &model.RainbondComponent{
Name: "rainbond-operator",
Pods: []corev1.Pod{},
})
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result
}
func podReadyForOverview(pod corev1.Pod) bool {
if pod.Status.Phase != corev1.PodRunning {
return false
}
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady {
return condition.Status == corev1.ConditionTrue
}
}
return false
}
func buildRainbondComponentOverview(pods []corev1.Pod) []*model.RainbondComponent {
components := make(map[string]*model.RainbondComponent)
for _, pod := range pods {
componentName := pod.Labels["name"]
if componentName == "" {
logrus.Warningf("list rainbond components. label 'name' not found for pod(%s/%s)", pod.Namespace, pod.Name)
continue
}
component, ok := components[componentName]
if !ok {
component = &model.RainbondComponent{Name: componentName}
components[componentName] = component
}
component.Pods = append(component.Pods, pod)
component.AllPods++
if podReadyForOverview(pod) {
component.RunPods++
}
}
componentNames := make([]string, 0, len(components))
for name := range components {
componentNames = append(componentNames, name)
}
sort.Strings(componentNames)
result := make([]*model.RainbondComponent, 0, len(componentNames))
for _, name := range componentNames {
result = append(result, components[name])
}
return result
}
// ListPlugins -
func (c *clusterAction) ListPlugins(official bool) (plugins []*model.RainbondPlugins, err error) {
plugins = make([]*model.RainbondPlugins, 0)
res, err := c.HandlePlugins()
if err != nil {
return nil, errors.Wrap(err, "get rbd plugins")
}
if official {
for _, plugin := range res {
if name, ok := plugin.Labels[OfficialPluginLabel]; ok && name != "" {
plugin.Name = name
plugins = append(plugins, plugin)
}
}
return plugins, nil
}
return res, nil
}
// PluginExists reports whether an official plugin with the given name exists in
// the cluster. It uses a label selector so the K8s API does the filtering and
// skips the heavy app-status/tenant enrichment done by HandlePlugins, making it
// a cheap probe for callers that only need existence (e.g. edition detection).
func (c *clusterAction) PluginExists(name string) (bool, error) {
if name == "" {
return false, nil
}
selector := labels.SelectorFromSet(labels.Set{OfficialPluginLabel: name}).String()
list, err := c.rainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceAll).List(
context.Background(), metav1.ListOptions{LabelSelector: selector, Limit: 1})
if err != nil {
return false, errors.Wrap(err, "list rbd plugins by name")
}
return len(list.Items) > 0, nil
}
// CreatePlugin creates or updates an RBDPlugin CR
func (c *clusterAction) CreatePlugin(req *model.CreateRBDPluginReq) error {
var pluginViews []v1alpha1.PluginView
for _, v := range req.PluginViews {
pluginViews = append(pluginViews, v1alpha1.PluginView(v))
}
labels := make(map[string]string)
if req.AppID != "" {
labels["app_id"] = req.AppID
}
// Add official plugin label for platform plugins
if req.PluginID != "" {
labels[OfficialPluginLabel] = req.PluginID
}
plugin := &v1alpha1.RBDPlugin{
ObjectMeta: metav1.ObjectMeta{
Name: req.PluginID,
Labels: labels,
},
Spec: v1alpha1.RBDPluginSpec{
DisplayName: req.PluginName,
PluginType: v1alpha1.PluginType(req.PluginType),
FrontendComponent: req.FrontendComponent,
EntryPath: req.EntryPath,
PluginViews: pluginViews,
MenuTitle: req.MenuTitle,
RoutePath: req.RoutePath,
Namespace: req.Namespace,
BackendService: req.BackendService,
FrontendService: req.FrontendService,
},
}
existing, err := c.rainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceNone).Get(context.Background(), req.PluginID, metav1.GetOptions{})
if err == nil {
existing.Spec = plugin.Spec
if existing.Labels == nil {
existing.Labels = make(map[string]string)
}
for k, v := range labels {
existing.Labels[k] = v
}
_, err = c.rainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceNone).Update(context.Background(), existing, metav1.UpdateOptions{})
return err
}
_, err = c.rainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceNone).Create(context.Background(), plugin, metav1.CreateOptions{})
return err
}
// ListAbilities -
func (c *clusterAction) ListAbilities() (rbds []unstructured.Unstructured, err error) {
list, err := c.rainbondClient.RainbondV1alpha1().RBDAbilities(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, errors.Wrap(err, "get rbd abilities")
}
// Remove duplicate watchGroups
existWatchGroups := make(map[string]struct{})
var watchGroups []v1alpha1.WatchGroup
for _, item := range list.Items {
for _, wg := range item.Spec.WatchGroups {
if wg.APIVersion == "" || wg.Kind == "" {
continue
}
if _, ok := existWatchGroups[wg.APIVersion+wg.Kind]; ok {
continue
}
existWatchGroups[wg.APIVersion+wg.Kind] = struct{}{}
watchGroups = append(watchGroups, wg)
}
}
for _, wg := range watchGroups {
gvk := schema.FromAPIVersionAndKind(wg.APIVersion, wg.Kind)
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
logrus.Warningf("get rest mapping for %s/%s: %v", wg.APIVersion, wg.Kind, err)
continue
}
list, err := c.dynamicClient.Resource(mapping.Resource).Namespace(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
if err != nil {
logrus.Warningf("list %s/%s: %v", wg.APIVersion, wg.Kind, err)
continue
}
rbds = append(rbds, list.Items...)
}
return rbds, nil
}
func (c *clusterAction) GenerateAbilityID(ability *unstructured.Unstructured) string {
// example abilityID: namespace_apiVersion_kind_name
return fmt.Sprintf("%s_%s_%s_%s", ability.GetNamespace(), strings.Replace(ability.GetAPIVersion(), "/", "-", -1), ability.GetKind(), ability.GetName())
}
func (c *clusterAction) ParseAbilityID(abilityID string) (namespace, apiVersion, kind, name string, err error) {
// example abilityID: namespace_apiVersion_kind_name
split := strings.SplitN(abilityID, "_", 4)
if len(split) < 4 {
return "", "", "", "", fmt.Errorf("invalid abilityID: %s", abilityID)
}
namespace, apiVersion, kind, name = split[0], split[1], split[2], split[3]
if strings.ContainsAny(apiVersion, "-") {
apiVersion = strings.Replace(apiVersion, "-", "/", -1)
}
return namespace, apiVersion, kind, name, nil
}
// GetAbility -
func (c *clusterAction) GetAbility(abilityID string) (*unstructured.Unstructured, error) {
namespace, apiVersion, kind, name, err := c.ParseAbilityID(abilityID)
if err != nil {
return nil, err
}
logrus.Debugf("get ability: [%v, %v, %v, %v]", namespace, apiVersion, kind, name)
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, errors.Wrap(err, "ability_id invalid")
}
resource, err := c.dynamicClient.Resource(mapping.Resource).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "get ability")
}
resource.SetManagedFields(nil)
return resource, nil
}
// UpdateAbility -
func (c *clusterAction) UpdateAbility(abilityID string, ability *unstructured.Unstructured) error {
namespace, apiVersion, kind, name, err := c.ParseAbilityID(abilityID)
if err != nil {
return err
}
logrus.Debugf("update ability: [%v, %v, %v, %v]", namespace, apiVersion, kind, name)
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return errors.Wrap(err, "ability_id invalid")
}
_, err = c.dynamicClient.Resource(mapping.Resource).Namespace(namespace).Update(context.Background(), ability, metav1.UpdateOptions{})
if err != nil {
return errors.Wrap(err, "update ability")
}
return nil
}
// HandlePlugins -
func (c *clusterAction) HandlePlugins() (plugins []*model.RainbondPlugins, err error) {
plugins = make([]*model.RainbondPlugins, 0)
list, err := c.rainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, errors.Wrap(err, "get rbd plugins")
}
if len(list.Items) == 0 {
return plugins, nil
}
// list plugin status
var appIDs []string
for _, item := range list.Items {
if item.Labels["app_id"] != "" {
appIDs = append(appIDs, item.Labels["app_id"])
}
}
appStatuses := make(map[string]string)
statuses, err := c.statusCli.ListAppStatuses(context.Background(), &pb.AppStatusesReq{
AppIds: appIDs,
})
if err != nil {
return nil, errors.Wrap(err, "get app statuses")
}
for _, status := range statuses.AppStatuses {
appStatuses[status.AppId] = status.Status
}
// Establish the mapping relationship between appID and teamName
apps, err := db.GetManager().ApplicationDao().ListByAppIDs(appIDs)
if err != nil {
return nil, errors.Wrap(err, "get apps by app ids")
}
tenants, err := db.GetManager().TenantDao().GetALLTenants("")
if err != nil {
return nil, errors.Wrap(err, "get tenants")
}
var (
appTeamIDs = make(map[string]string)
teamNames = make(map[string]string)
)
for _, app := range apps {
appTeamIDs[app.AppID] = app.TenantID
}
for _, tenant := range tenants {
teamNames[tenant.UUID] = tenant.Name
}
// Construct the returned data
for _, plugin := range list.Items {
appID := plugin.Labels["app_id"]
status := "NIL"
if appStatuses[appID] != "" {
status = appStatuses[appID]
}
logrus.Debugf("plugin Name: %v, namespace %v", plugin.Name, plugin.Namespace)
var pluginViews []string
for _, view := range plugin.Spec.PluginViews {
pluginViews = append(pluginViews, view.String())
}
enableStatus := plugin.GetLabels()[v1alpha1.PluginEnableLabel.String()]
if enableStatus == "" {
enableStatus = v1alpha1.True.String()
}
plugins = append(plugins, &model.RainbondPlugins{
RegionAppID: appID,
Name: plugin.GetName(),
TeamName: teamNames[appTeamIDs[appID]],
Status: status,
Alias: plugin.Spec.DisplayName,
Labels: plugin.Labels,
PluginType: plugin.Spec.PluginType.String(),
PluginViews: pluginViews,
EnableStatus: enableStatus,
FrontendComponent: plugin.Spec.FrontendComponent,
EntryPath: plugin.Spec.EntryPath,
MenuTitle: plugin.Spec.MenuTitle,
RoutePath: plugin.Spec.RoutePath,
Namespace: plugin.Spec.Namespace,
BackendService: plugin.Spec.BackendService,
FrontendService: plugin.Spec.FrontendService,
})
}
return plugins, nil
}
// ComponentRainbondOperator and related constants identify workloads checked during platform upgrades.
const (
ComponentRainbondOperator = "rainbond-operator" // deployment
ComponentRBDAPI = "rbd-api" // deployment
ComponentRBDWorker = "rbd-worker" // deployment
ComponentRBDAPPUI = "rbd-app-ui" // deployment, optional
ComponentRBDChaos = "rbd-chaos" // daemonset
ComponentRBDMQ = "rbd-mq" // deployment
)
func (c *clusterAction) ListUpgradeStatus() ([]model.ComponentStatus, error) {
components := []string{ComponentRainbondOperator, ComponentRBDAPI, ComponentRBDWorker, ComponentRBDAPPUI, ComponentRBDChaos, ComponentRBDMQ}
var statuses []model.ComponentStatus
// 遍历所有组件并查询其状态
for _, component := range components {
var status model.ComponentStatus
status.Name = component
// 根据组件类型查询状态
if component == ComponentRBDChaos {
// DaemonSet 状态检查,包含重试机制
dsStatus, err := retryStatusCheck(func() (model.ComponentStatus, error) {
return c.checkDaemonSetStatus(component)
})
if err != nil {
status.Status = "Failed"
status.Message = err.Error()
} else {
status = dsStatus
}
} else {
// 检查组件是否存在,ComponentRBDAPPUI 是可选的
exists, err := c.checkComponentExists(component)
if err != nil {
// 处理组件存在性检查中的错误
status.Status = "Failed"
status.Message = fmt.Sprintf("error checking component existence: %v", err)
statuses = append(statuses, status)
continue
}
if !exists {
// 如果组件不存在则跳过
continue
}
// Deployment 状态检查,包含重试机制
deployStatus, err := retryStatusCheck(func() (model.ComponentStatus, error) {
return c.checkDeploymentStatus(component)
})
if err != nil {
status.Status = "Failed"
status.Message = err.Error()
} else {
status = deployStatus
}
}
statuses = append(statuses, status)
}
return statuses, nil
}
// 重试机制封装,防止短暂的错误导致失败
func retryStatusCheck(checkFunc func() (model.ComponentStatus, error)) (model.ComponentStatus, error) {
var status model.ComponentStatus
var err error
retryErr := retry.OnError(retry.DefaultRetry, func(err error) bool {
// Retry on any error
return true
}, func() error {
status, err = checkFunc()
return err
})
if retryErr != nil {
return model.ComponentStatus{}, retryErr
}
return status, nil
}
// 检查组件是否存在(用于处理可选的组件,如 ComponentRBDAPPUI)
func (c *clusterAction) checkComponentExists(componentName string) (bool, error) {
// 检查 Deployment 是否存在
_, err := c.clientset.AppsV1().Deployments(c.namespace).Get(context.TODO(), componentName, metav1.GetOptions{})
if err == nil {
return true, nil
} else if apierrors.IsNotFound(err) {
// 如果返回 404 错误,说明该组件不存在
return false, nil
} else {
// 其他类型的错误
return false, fmt.Errorf("error checking component %s existence: %v", componentName, err)
}
}
func (c *clusterAction) checkDeploymentStatus(deploymentName string) (model.ComponentStatus, error) {
deployment, err := c.clientset.AppsV1().Deployments(c.namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
if err != nil {
return model.ComponentStatus{}, fmt.Errorf("error getting deployment %s: %v", deploymentName, err)
}
replicas := *deployment.Spec.Replicas // 总期望副本数
updatedReplicas := deployment.Status.UpdatedReplicas // 更新到新镜像的副本数
availableReplicas := deployment.Status.AvailableReplicas // 可用副本数
unavailableReplicas := deployment.Status.UnavailableReplicas // 不可用副本数
status := model.ComponentStatus{
Name: deploymentName,
}
// 检查升级是否还在进行
isProgressing := false
for _, condition := range deployment.Status.Conditions {
if condition.Type == appsv1.DeploymentProgressing && condition.Status == corev1.ConditionTrue {
isProgressing = true
break
}
}
// 如果 replicas 与 updatedReplicas 相等,且 unavailableReplicas 为 0,说明升级完成
if updatedReplicas == replicas && unavailableReplicas == 0 {
status.Status = "Completed"
status.Progress = 100
} else {
status.Status = "Upgrading"
// 更新进度:结合 updatedReplicas 和 availableReplicas 的比例
logrus.Infof("updatedReplicas/replicas*50 [%f], int64(updatedReplicas/replicas*50) [%d]", float64(updatedReplicas)/float64(replicas)*50, int64(updatedReplicas*50/replicas))
logrus.Infof("availableReplicas/replicas*50 [%f], int64(availableReplicas/replicas*50) [%d]", float64(availableReplicas)/float64(replicas)*50, int64(availableReplicas/replicas*50))
progress := int64(updatedReplicas*50/replicas + availableReplicas*50/replicas)
floatval := (float64(updatedReplicas)/float64(replicas))*50 + (float64(availableReplicas)/float64(replicas))*50
logrus.Infof("deployment %s progress: %d, %f", deploymentName, progress, floatval)
// 如果进度为 100,但是 unavailableReplicas 不等于 0,则说明升级卡住了
if progress == 100 {
progress = 99
}
status.Progress = progress
// 如果有不可用副本,说明可能有问题
if unavailableReplicas > 0 {
// 获取不可用 Pod 的详细状态信息
podMessage := c.getPodMessage(deploymentName)
if podMessage != "" {
status.Message = podMessage
} else {
status.Message = fmt.Sprintf("%d replicas are unavailable", unavailableReplicas)
}
}
// 如果进展缓慢,或者长时间没有变化,可以增加错误提示
if isProgressing {
if status.Message == "" {
status.Message = "Deployment is progressing, but not all replicas are ready"
} else {
status.Message += "; Deployment is progressing, but not all replicas are ready"
}
}
}
return status, nil
}
// 获取Pod的详细信息,展示包括创建中的信息和终止状态
func (c *clusterAction) getPodMessage(deploymentName string) string {
// 根据Deployment的label来获取相关的Pod
podList, err := c.clientset.CoreV1().Pods(c.namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("name=%s", deploymentName), // 假设使用name=deploymentName作为label
})
if err != nil {
return fmt.Sprintf("Error listing pods for %s: %v", deploymentName, err)
}
var messages []string
// 遍历所有Pod,检查每个Pod的状态
for _, pod := range podList.Items {
for _, containerStatus := range pod.Status.ContainerStatuses {
// 检查容器是否在等待状态,展示创建中的详细信息
if containerStatus.State.Waiting != nil {
messages = append(messages, fmt.Sprintf("Pod %s is in waiting state: %s", pod.Name, containerStatus.State.Waiting.Message))
}
// 检查容器是否处于终止状态且有错误
if containerStatus.State.Terminated != nil && containerStatus.State.Terminated.ExitCode != 0 {
messages = append(messages, fmt.Sprintf("Pod %s terminated with exit code %d: %s", pod.Name, containerStatus.State.Terminated.ExitCode, containerStatus.State.Terminated.Message))
}
// 检查容器是否处于Running但Not Ready状态
if containerStatus.State.Running != nil && !containerStatus.Ready {
messages = append(messages, fmt.Sprintf("Pod %s: Container %s is running but not ready", pod.Name, containerStatus.Name))
}
}
}
// 如果有详细状态信息,返回所有状态
if len(messages) > 0 {
return fmt.Sprintf("Pod details: %s", strings.Join(messages, "; "))
}
// 如果没有发现详细信息
return ""
}
func (c *clusterAction) checkDaemonSetStatus(daemonsetName string) (model.ComponentStatus, error) {
daemonset, err := c.clientset.AppsV1().DaemonSets(c.namespace).Get(context.TODO(), daemonsetName, metav1.GetOptions{})
if err != nil {
return model.ComponentStatus{}, fmt.Errorf("error getting daemonset %s: %v", daemonsetName, err)
}
desired := daemonset.Status.DesiredNumberScheduled
updated := daemonset.Status.UpdatedNumberScheduled
available := daemonset.Status.NumberAvailable
status := model.ComponentStatus{
Name: daemonsetName,
}
// 检查所有 pods 是否都更新并可用
if updated == desired && available == desired {
status.Status = "Completed"
status.Progress = 100
} else {
status.Status = "Upgrading"
progress := int64(available * 100 / desired)
// 如果进度为 100,但是 available 与 desired 不相等,说明升级卡住了
if progress == 100 {
progress = 99
}
status.Progress = progress
// 如果升级卡住,检查 pod 的错误信息
podMessage := c.checkPodStatus(daemonsetName)
if podMessage != "" {
status.Message = podMessage
}
}
return status, nil
}
func (c *clusterAction) checkPodStatus(componentName string) string {
// 根据组件名称列出相关的 Pod
podList, err := c.clientset.CoreV1().Pods(c.namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("name=%s", componentName),
})
if err != nil {
return fmt.Sprintf("Error listing pods for %s: %v", componentName, err)
}
for _, pod := range podList.Items {
for _, containerStatus := range pod.Status.ContainerStatuses {
// 检查容器是否处于等待或终止状态
if containerStatus.State.Terminated != nil && containerStatus.State.Terminated.ExitCode != 0 {
return fmt.Sprintf("Pod %s terminated with exit code: %d", pod.Name, containerStatus.State.Terminated.ExitCode)
} else if containerStatus.State.Waiting != nil {
return containerStatus.State.Waiting.Message
}
}
// 检查 Pod 的健康状态
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady && condition.Status != corev1.ConditionTrue {
return fmt.Sprintf("Pod %s is running but not ready: %s", pod.Name, condition.Message)
}
}
}
return ""
}
func (c *clusterAction) GetClusterRegionStatus() (map[string]interface{}, error) {
secret := &corev1.Secret{}
if err := c.client.Get(context.Background(), types.NamespacedName{Namespace: c.namespace, Name: "rbd-api-server-cert"}, secret); err != nil {
return nil, err
}
var cluster rainbondv1alpha1.RainbondCluster
if err := clients.RainbondKubeClient.Get(context.Background(), types.NamespacedName{Namespace: c.namespace, Name: "rainbondcluster"}, &cluster); err != nil {
return nil, err
}
var gatewayIngressIP string
if len(cluster.Spec.GatewayIngressIPs) > 0 && cluster.Spec.GatewayIngressIPs[0] != "" {
gatewayIngressIP = cluster.Spec.GatewayIngressIPs[0]
} else if len(cluster.Spec.NodesForGateway) > 0 {
gatewayIngressIP = cluster.Spec.NodesForGateway[0].InternalIP
}
if secret != nil {
var ips = strings.ReplaceAll(strings.Join(cluster.GatewayIngressIPs(), "-"), ".", "_")
if availableips, ok := secret.Labels["availableips"]; ok && availableips == ips {
caPem := secret.Data["ca.pem"]
clientPem := secret.Data["server.pem"]
clientKey := secret.Data["server.key.pem"]
// Validate PEM certificates before returning them to the console.
// Invalid PEM data causes SSLError: PEM lib on the console side.
if err := validatePEMCertificates(caPem, clientPem, clientKey); err != nil {
logrus.Errorf("region certificate validation failed: %v", err)
return nil, fmt.Errorf("region certificates invalid: %v", err)
}
regionInfo := make(map[string]interface{})
regionInfo["regionName"] = time.Now().Unix()
regionInfo["regionType"] = []string{"custom"}
regionInfo["sslCaCert"] = string(caPem)
regionInfo["keyFile"] = string(clientKey)
regionInfo["certFile"] = string(clientPem)
regionInfo["url"] = fmt.Sprintf("https://%s:%s", gatewayIngressIP, "8443")
regionInfo["wsUrl"] = fmt.Sprintf("ws://%s:%s", gatewayIngressIP, "6060")
regionInfo["httpDomain"] = cluster.Spec.SuffixHTTPHost
regionInfo["tcpDomain"] = cluster.GatewayIngressIP()
regionInfo["desc"] = "Helm"
regionInfo["regionAlias"] = "对接集群"
regionInfo["provider"] = "helm"
regionInfo["providerClusterId"] = ""
regionInfo["token"] = os.Getenv("HELM_TOKEN")
if os.Getenv("ENTERPRISE_ID") != "" {
regionInfo["enterpriseId"] = os.Getenv("ENTERPRISE_ID")
}
if os.Getenv("CLOUD_SERVER") != "" {
cloud := os.Getenv("CLOUD_SERVER")
switch cloud {
case "aliyun":
regionInfo["regionType"] = []string{"aliyun"}
case "huawei":
regionInfo["regionType"] = []string{"huawei"}
case "tencent":
regionInfo["regionType"] = []string{"tencent"}
}
}
return regionInfo, nil
}
}
return nil, fmt.Errorf("get rbd-api-server-cert secret is nil")
}
// validatePEMCertificates checks that the CA, certificate, and key PEM blocks
// are parseable and that the certificate/key pair is consistent. This prevents
// the console from receiving malformed certificates that cause SSLError: PEM lib.
func validatePEMCertificates(caPEM, certPEM, keyPEM []byte) error {
if len(caPEM) == 0 {
return fmt.Errorf("ca.pem is empty")
}
if len(certPEM) == 0 {
return fmt.Errorf("server.pem is empty")
}
if len(keyPEM) == 0 {
return fmt.Errorf("server.key.pem is empty")
}
// Validate CA certificate
caBlock, _ := pem.Decode(caPEM)
if caBlock == nil {
return fmt.Errorf("ca.pem: no valid PEM block found")
}
caCert, err := x509.ParseCertificate(caBlock.Bytes)
if err != nil {
return fmt.Errorf("ca.pem: failed to parse certificate: %v", err)
}
if !caCert.IsCA {
return fmt.Errorf("ca.pem: certificate is not a CA certificate")
}
// Validate server certificate
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return fmt.Errorf("server.pem: no valid PEM block found")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return fmt.Errorf("server.pem: failed to parse certificate: %v", err)
}
// Validate private key
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return fmt.Errorf("server.key.pem: no valid PEM block found")
}
switch keyBlock.Type {
case "RSA PRIVATE KEY":
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
if err != nil {
return fmt.Errorf("server.key.pem: failed to parse RSA private key: %v", err)
}
if pubKey, ok := cert.PublicKey.(*rsa.PublicKey); ok {
if key.N.Cmp(pubKey.N) != 0 {
return fmt.Errorf("server.key.pem: private key does not match server certificate")
}
}
case "EC PRIVATE KEY":
key, err := x509.ParseECPrivateKey(keyBlock.Bytes)
if err != nil {
return fmt.Errorf("server.key.pem: failed to parse EC private key: %v", err)
}
if pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey); ok {
if key.PublicKey.X.Cmp(pubKey.X) != 0 || key.PublicKey.Y.Cmp(pubKey.Y) != 0 {
return fmt.Errorf("server.key.pem: private key does not match server certificate")
}
}
case "PRIVATE KEY":
// PKCS#8 format
parsedKey, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if err != nil {
return fmt.Errorf("server.key.pem: failed to parse PKCS8 private key: %v", err)
}
switch k := parsedKey.(type) {
case *rsa.PrivateKey:
if pubKey, ok := cert.PublicKey.(*rsa.PublicKey); ok {
if k.N.Cmp(pubKey.N) != 0 {
return fmt.Errorf("server.key.pem: private key does not match server certificate")
}
}
case *ecdsa.PrivateKey:
if pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey); ok {
if k.PublicKey.X.Cmp(pubKey.X) != 0 || k.PublicKey.Y.Cmp(pubKey.Y) != 0 {
return fmt.Errorf("server.key.pem: private key does not match server certificate")
}
}
default:
return fmt.Errorf("server.key.pem: unsupported key type in PKCS8 container")
}
default:
return fmt.Errorf("server.key.pem: unsupported key type %q", keyBlock.Type)
}
// Validate certificate chain: server cert must be signed by the CA
caPool := x509.NewCertPool()
caPool.AddCert(caCert)
if _, err := cert.Verify(x509.VerifyOptions{
Roots: caPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return fmt.Errorf("certificate chain validation failed: server cert is not signed by the CA: %v", err)
}
return nil
}
// CheckReadiness verifies that the region API dependencies are available.
// The console can call this endpoint to detect region unavailability before
// making API calls, returning structured errors instead of raw stack traces.
func (c *clusterAction) CheckReadiness(ctx context.Context) (*model.RegionReadiness, error) {
checks := make(map[string]string)
ready := true
// Check K8s API connectivity
k8sCtx, k8sCancel := context.WithTimeout(ctx, 3*time.Second)
defer k8sCancel()
if _, err := c.clientset.CoreV1().Namespaces().List(k8sCtx, metav1.ListOptions{Limit: 1}); err != nil {
ready = false
checks["k8s_api"] = fmt.Sprintf("unavailable: %v", err)
logrus.Warnf("readiness check: k8s api unavailable: %v", err)
} else {
checks["k8s_api"] = "ok"
}
// Check region certificate secret exists and is valid
secretCtx, secretCancel := context.WithTimeout(ctx, 3*time.Second)
defer secretCancel()
secret := &corev1.Secret{}
if err := c.client.Get(secretCtx, types.NamespacedName{Namespace: c.namespace, Name: "rbd-api-server-cert"}, secret); err != nil {
ready = false
checks["region_cert"] = fmt.Sprintf("unavailable: %v", err)
logrus.Warnf("readiness check: region cert secret unavailable: %v", err)
} else {
if err := validatePEMCertificates(secret.Data["ca.pem"], secret.Data["server.pem"], secret.Data["server.key.pem"]); err != nil {
ready = false
checks["region_cert"] = fmt.Sprintf("invalid: %v", err)
logrus.Warnf("readiness check: region certificates invalid: %v", err)
} else {
checks["region_cert"] = "ok"
}
}
result := &model.RegionReadiness{
Ready: ready,
Checks: checks,
}
if !ready {
result.Message = "region is not ready; some dependencies are unavailable"
}
return result, nil
}