ocdp v1
This commit is contained in:
196
backend/internal/adapter/output/helm/mock/helm_client_mock.go
Normal file
196
backend/internal/adapter/output/helm/mock/helm_client_mock.go
Normal file
@ -0,0 +1,196 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// HelmClientMock Helm 客户端 Mock 实现
|
||||
type HelmClientMock struct {
|
||||
// Mock 数据存储
|
||||
releases map[string]map[string]*entity.Instance // clusterID -> releaseName -> instance
|
||||
history map[string]map[string][]*entity.ReleaseHistory // clusterID -> releaseName -> []history
|
||||
}
|
||||
|
||||
// NewHelmClientMock 创建 Mock 实现
|
||||
func NewHelmClientMock() repository.HelmClient {
|
||||
return &HelmClientMock{
|
||||
releases: make(map[string]map[string]*entity.Instance),
|
||||
history: make(map[string]map[string][]*entity.ReleaseHistory),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
|
||||
// 初始化集群数据
|
||||
if c.releases[cluster.ID] == nil {
|
||||
c.releases[cluster.ID] = make(map[string]*entity.Instance)
|
||||
c.history[cluster.ID] = make(map[string][]*entity.ReleaseHistory)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
key := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name)
|
||||
if _, exists := c.releases[cluster.ID][key]; exists {
|
||||
return entity.ErrInstanceExists
|
||||
}
|
||||
|
||||
// Mock 安装
|
||||
instance.Status = entity.StatusDeployed
|
||||
instance.Revision = 1
|
||||
instance.UpdatedAt = time.Now()
|
||||
|
||||
c.releases[cluster.ID][key] = instance
|
||||
|
||||
// 添加历史记录
|
||||
c.history[cluster.ID][key] = []*entity.ReleaseHistory{
|
||||
{
|
||||
Revision: 1,
|
||||
Updated: time.Now(),
|
||||
Status: entity.StatusDeployed,
|
||||
Chart: fmt.Sprintf("%s-%s", instance.Chart, instance.Version),
|
||||
AppVersion: instance.Version,
|
||||
Description: "Install complete",
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
|
||||
key := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name)
|
||||
|
||||
existing, exists := c.releases[cluster.ID][key]
|
||||
if !exists {
|
||||
return entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
// Mock 升级
|
||||
instance.Revision = existing.Revision + 1
|
||||
instance.Status = entity.StatusDeployed
|
||||
instance.UpdatedAt = time.Now()
|
||||
|
||||
c.releases[cluster.ID][key] = instance
|
||||
|
||||
// 添加历史记录
|
||||
history := &entity.ReleaseHistory{
|
||||
Revision: instance.Revision,
|
||||
Updated: time.Now(),
|
||||
Status: entity.StatusDeployed,
|
||||
Chart: fmt.Sprintf("%s-%s", instance.Chart, instance.Version),
|
||||
AppVersion: instance.Version,
|
||||
Description: "Upgrade complete",
|
||||
}
|
||||
c.history[cluster.ID][key] = append(c.history[cluster.ID][key], history)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error {
|
||||
key := fmt.Sprintf("%s/%s", namespace, releaseName)
|
||||
|
||||
if _, exists := c.releases[cluster.ID][key]; !exists {
|
||||
return entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
// Mock 卸载
|
||||
delete(c.releases[cluster.ID], key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error {
|
||||
key := fmt.Sprintf("%s/%s", namespace, releaseName)
|
||||
|
||||
instance, exists := c.releases[cluster.ID][key]
|
||||
if !exists {
|
||||
return entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
// 检查历史记录是否存在
|
||||
histories := c.history[cluster.ID][key]
|
||||
if revision > len(histories) || revision < 1 {
|
||||
return fmt.Errorf("revision %d not found", revision)
|
||||
}
|
||||
|
||||
// Mock 回滚
|
||||
instance.Revision = len(histories) + 1
|
||||
instance.Status = entity.StatusDeployed
|
||||
instance.UpdatedAt = time.Now()
|
||||
|
||||
c.releases[cluster.ID][key] = instance
|
||||
|
||||
// 添加回滚历史记录
|
||||
history := &entity.ReleaseHistory{
|
||||
Revision: instance.Revision,
|
||||
Updated: time.Now(),
|
||||
Status: entity.StatusDeployed,
|
||||
Chart: instance.Chart,
|
||||
AppVersion: instance.Version,
|
||||
Description: fmt.Sprintf("Rollback to revision %d", revision),
|
||||
}
|
||||
c.history[cluster.ID][key] = append(c.history[cluster.ID][key], history)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error) {
|
||||
key := fmt.Sprintf("%s/%s", namespace, releaseName)
|
||||
|
||||
instance, exists := c.releases[cluster.ID][key]
|
||||
if !exists {
|
||||
return nil, entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error) {
|
||||
key := fmt.Sprintf("%s/%s", namespace, releaseName)
|
||||
|
||||
if _, exists := c.releases[cluster.ID][key]; !exists {
|
||||
return nil, entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
histories := c.history[cluster.ID][key]
|
||||
if histories == nil {
|
||||
return []*entity.ReleaseHistory{}, nil
|
||||
}
|
||||
|
||||
return histories, nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error) {
|
||||
clusterReleases := c.releases[cluster.ID]
|
||||
if clusterReleases == nil {
|
||||
return []*entity.Instance{}, nil
|
||||
}
|
||||
|
||||
instances := make([]*entity.Instance, 0)
|
||||
for key, instance := range clusterReleases {
|
||||
// 如果指定了 namespace,只返回该 namespace 的
|
||||
if namespace != "" && namespace != "all" {
|
||||
keyNamespace := instance.Namespace
|
||||
if keyNamespace != namespace {
|
||||
continue
|
||||
}
|
||||
}
|
||||
instances = append(instances, c.releases[cluster.ID][key])
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error) {
|
||||
key := fmt.Sprintf("%s/%s", namespace, releaseName)
|
||||
|
||||
instance, exists := c.releases[cluster.ID][key]
|
||||
if !exists {
|
||||
return nil, entity.ErrInstanceNotFound
|
||||
}
|
||||
|
||||
return instance.Values, nil
|
||||
}
|
||||
|
||||
313
backend/internal/adapter/output/helm/real/helm_client.go
Normal file
313
backend/internal/adapter/output/helm/real/helm_client.go
Normal file
@ -0,0 +1,313 @@
|
||||
package real
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"helm.sh/helm/v3/pkg/storage/driver"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// HelmClient 真实的 Helm 客户端实现
|
||||
type HelmClient struct {
|
||||
settings *cli.EnvSettings
|
||||
}
|
||||
|
||||
// NewHelmClient 创建真实的 Helm 客户端
|
||||
func NewHelmClient() repository.HelmClient {
|
||||
return &HelmClient{
|
||||
settings: cli.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// getActionConfig 获取 Helm action configuration
|
||||
func (h *HelmClient) getActionConfig(cluster *entity.Cluster, namespace string) (*action.Configuration, error) {
|
||||
actionConfig := new(action.Configuration)
|
||||
|
||||
// 创建临时 kubeconfig 文件
|
||||
kubeconfigContent := cluster.GetKubeConfig()
|
||||
tmpDir, err := os.MkdirTemp("", "helm-kubeconfig-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
|
||||
kubeconfigPath := filepath.Join(tmpDir, "kubeconfig")
|
||||
if err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
// 使用 kubeconfig 初始化 action config
|
||||
if err := actionConfig.Init(
|
||||
&kubeconfigGetter{kubeconfigPath: kubeconfigPath},
|
||||
namespace,
|
||||
os.Getenv("HELM_DRIVER"), // storage driver: configmap, secret, memory
|
||||
func(format string, v ...interface{}) {
|
||||
// Log function
|
||||
},
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize action config: %w", err)
|
||||
}
|
||||
|
||||
return actionConfig, nil
|
||||
}
|
||||
|
||||
// kubeconfigGetter implements RESTClientGetter
|
||||
type kubeconfigGetter struct {
|
||||
kubeconfigPath string
|
||||
}
|
||||
|
||||
func (k *kubeconfigGetter) ToRESTConfig() (*rest.Config, error) {
|
||||
return clientcmd.BuildConfigFromFlags("", k.kubeconfigPath)
|
||||
}
|
||||
|
||||
func (k *kubeconfigGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
config, err := k.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config)
|
||||
// Wrap in a memory cache
|
||||
return memory.NewMemCacheClient(discoveryClient), nil
|
||||
}
|
||||
|
||||
func (k *kubeconfigGetter) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
discoveryClient, err := k.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
|
||||
return mapper, nil
|
||||
}
|
||||
|
||||
func (k *kubeconfigGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: k.kubeconfigPath},
|
||||
&clientcmd.ConfigOverrides{},
|
||||
)
|
||||
}
|
||||
|
||||
// Install 安装 Helm Chart
|
||||
func (h *HelmClient) Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
|
||||
actionConfig, err := h.getActionConfig(cluster, instance.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
install := action.NewInstall(actionConfig)
|
||||
install.ReleaseName = instance.Name
|
||||
install.Namespace = instance.Namespace
|
||||
install.CreateNamespace = true
|
||||
install.Wait = true
|
||||
install.Timeout = 5 * time.Minute
|
||||
|
||||
// 加载 Chart(从本地路径或 OCI registry)
|
||||
// 这里简化处理,假设 chart 已经被拉取到本地
|
||||
chartPath := fmt.Sprintf("/tmp/charts/%s-%s.tgz", instance.Chart, instance.Version)
|
||||
|
||||
chart, err := loader.Load(chartPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load chart: %w", err)
|
||||
}
|
||||
|
||||
// 执行安装
|
||||
rel, err := install.Run(chart, instance.Values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install release: %w", err)
|
||||
}
|
||||
|
||||
// 更新 revision(状态由调用方根据操作结果设置)
|
||||
instance.Revision = rel.Version
|
||||
// 注意:不在这里设置 Status,让调用方通过 MarkSuccess/MarkFailure 来设置
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upgrade 升级 Helm Release
|
||||
func (h *HelmClient) Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
|
||||
actionConfig, err := h.getActionConfig(cluster, instance.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upgrade := action.NewUpgrade(actionConfig)
|
||||
upgrade.Namespace = instance.Namespace
|
||||
upgrade.Wait = true
|
||||
upgrade.Timeout = 5 * time.Minute
|
||||
|
||||
// 加载 Chart
|
||||
chartPath := fmt.Sprintf("/tmp/charts/%s-%s.tgz", instance.Chart, instance.Version)
|
||||
|
||||
chart, err := loader.Load(chartPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load chart: %w", err)
|
||||
}
|
||||
|
||||
// 执行升级
|
||||
rel, err := upgrade.Run(instance.Name, chart, instance.Values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upgrade release: %w", err)
|
||||
}
|
||||
|
||||
// 更新 revision(状态由调用方根据操作结果设置)
|
||||
instance.Revision = rel.Version
|
||||
// 注意:不在这里设置 Status,让调用方通过 MarkSuccess/MarkFailure 来设置
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall 卸载 Helm Release
|
||||
func (h *HelmClient) Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uninstall := action.NewUninstall(actionConfig)
|
||||
uninstall.Wait = true
|
||||
uninstall.Timeout = 5 * time.Minute
|
||||
|
||||
_, err = uninstall.Run(releaseName)
|
||||
if err != nil {
|
||||
if errors.Is(err, driver.ErrReleaseNotFound) {
|
||||
return entity.ErrInstanceNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to uninstall release: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback 回滚 Helm Release
|
||||
func (h *HelmClient) Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rollback := action.NewRollback(actionConfig)
|
||||
rollback.Version = revision
|
||||
rollback.Wait = true
|
||||
rollback.Timeout = 5 * time.Minute
|
||||
|
||||
if err := rollback.Run(releaseName); err != nil {
|
||||
return fmt.Errorf("failed to rollback release: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatus 获取 Release 状态
|
||||
func (h *HelmClient) GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error) {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status := action.NewStatus(actionConfig)
|
||||
rel, err := status.Run(releaseName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get release status: %w", err)
|
||||
}
|
||||
|
||||
return h.convertReleaseToInstance(rel), nil
|
||||
}
|
||||
|
||||
// GetHistory 获取 Release 历史
|
||||
func (h *HelmClient) GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error) {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
history := action.NewHistory(actionConfig)
|
||||
history.Max = 256
|
||||
|
||||
releases, err := history.Run(releaseName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get release history: %w", err)
|
||||
}
|
||||
|
||||
result := make([]*entity.ReleaseHistory, 0, len(releases))
|
||||
for _, rel := range releases {
|
||||
result = append(result, &entity.ReleaseHistory{
|
||||
Revision: rel.Version,
|
||||
Updated: rel.Info.LastDeployed.Time,
|
||||
Status: entity.InstanceStatus(rel.Info.Status),
|
||||
Chart: rel.Chart.Metadata.Name,
|
||||
AppVersion: rel.Chart.Metadata.AppVersion,
|
||||
Description: rel.Info.Description,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List 列出集群中的所有 Releases
|
||||
func (h *HelmClient) List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error) {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := action.NewList(actionConfig)
|
||||
if namespace == "" {
|
||||
list.AllNamespaces = true
|
||||
}
|
||||
|
||||
releases, err := list.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list releases: %w", err)
|
||||
}
|
||||
|
||||
instances := make([]*entity.Instance, 0, len(releases))
|
||||
for _, rel := range releases {
|
||||
instances = append(instances, h.convertReleaseToInstance(rel))
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// GetValues 获取 Release 的 values
|
||||
func (h *HelmClient) GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error) {
|
||||
actionConfig, err := h.getActionConfig(cluster, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
getValues := action.NewGetValues(actionConfig)
|
||||
values, err := getValues.Run(releaseName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values: %w", err)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// convertReleaseToInstance 转换 Helm Release 为 Instance
|
||||
func (h *HelmClient) convertReleaseToInstance(rel *release.Release) *entity.Instance {
|
||||
return &entity.Instance{
|
||||
Name: rel.Name,
|
||||
Namespace: rel.Namespace,
|
||||
Chart: rel.Chart.Metadata.Name,
|
||||
Version: rel.Chart.Metadata.Version,
|
||||
Status: entity.InstanceStatus(rel.Info.Status),
|
||||
Revision: rel.Version,
|
||||
Values: rel.Config,
|
||||
UpdatedAt: rel.Info.LastDeployed.Time,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user