feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages
Add new frontend pages for the multi-tenant OCDP platform: - Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories and versions, with deploy modal to launch charts on selected clusters - Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage) and per-node details with resource utilization bars - Chart References page (/chart-references): CRUD for chart metadata references - Values Templates page (/templates): CRUD for Helm values templates with version history and rollback support - Sidebar: Add Charts navigation, update Storage and Templates links - api.ts: Add all API client functions (clusterApi, registryApi, instanceApi, monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi, workspaceApi, userApi) with full TypeScript types Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
@ -43,13 +43,26 @@ func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error)
|
||||
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
||||
}
|
||||
|
||||
// 设置认证
|
||||
if reg.Username != "" && reg.Password != "" {
|
||||
// 设置认证 - 优先使用 registry 自己的凭证,否则使用 .env 中的默认凭证
|
||||
username := reg.Username
|
||||
password := reg.Password
|
||||
|
||||
// 如果没有提供凭证,尝试从环境变量加载
|
||||
if (username == "" || password == "") && strings.Contains(reg.URL, "harbor") {
|
||||
if envUser := os.Getenv("HARBOR_USERNAME"); envUser != "" {
|
||||
username = envUser
|
||||
}
|
||||
if envPass := os.Getenv("HARBOR_PASSWORD"); envPass != "" {
|
||||
password = envPass
|
||||
}
|
||||
}
|
||||
|
||||
if username != "" && password != "" {
|
||||
registry.Client = &auth.Client{
|
||||
Client: c.httpClient,
|
||||
Credential: auth.StaticCredential(registryURL, auth.Credential{
|
||||
Username: reg.Username,
|
||||
Password: reg.Password,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -370,6 +383,105 @@ func (c *OCIClient) GetValuesSchema(ctx context.Context, registry *entity.Regist
|
||||
return "", entity.ErrValuesSchemaNotFound
|
||||
}
|
||||
|
||||
// GetValues 获取 Helm Chart 的 values.yaml
|
||||
func (c *OCIClient) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
|
||||
reg, err := c.getRegistry(registry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
repo, err := reg.Repository(ctx, repository)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get repository: %w", err)
|
||||
}
|
||||
|
||||
// 解析 reference (tag 或 digest)
|
||||
desc, err := repo.Resolve(ctx, reference)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve artifact: %w", err)
|
||||
}
|
||||
|
||||
manifestReader, err := repo.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch manifest: %w", err)
|
||||
}
|
||||
defer manifestReader.Close()
|
||||
|
||||
manifestBytes, err := io.ReadAll(manifestReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read manifest: %w", err)
|
||||
}
|
||||
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
|
||||
}
|
||||
|
||||
// 查找 Helm Chart layer(tar+gzip 包含 chart 内容)并从中读取 values.yaml
|
||||
var chartLayer *ocispec.Descriptor
|
||||
for i := range manifest.Layers {
|
||||
layer := manifest.Layers[i]
|
||||
if strings.Contains(layer.MediaType, "cncf.helm.chart") ||
|
||||
strings.Contains(layer.MediaType, "helm.chart.content") {
|
||||
chartLayer = &manifest.Layers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if chartLayer == nil {
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
|
||||
if chartLayer.Digest == "" {
|
||||
return "", fmt.Errorf("chart layer digest is empty")
|
||||
}
|
||||
if _, err := digest.Parse(string(chartLayer.Digest)); err != nil {
|
||||
return "", fmt.Errorf("invalid chart layer digest: %w", err)
|
||||
}
|
||||
|
||||
layerReader, err := repo.Fetch(ctx, *chartLayer)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch chart layer: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
|
||||
gzipReader, err := gzip.NewReader(layerReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read chart archive: %w", err)
|
||||
}
|
||||
|
||||
if header.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找 values.yaml 文件(可能在 chart 根目录或子目录中)
|
||||
// 通常路径格式为: {chart-name}/values.yaml
|
||||
if strings.HasSuffix(header.Name, "values.yaml") {
|
||||
data, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read values.yaml: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
|
||||
// PullArtifact 下载 artifact 到本地
|
||||
func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
|
||||
reg, err := c.getRegistry(registry)
|
||||
|
||||
Reference in New Issue
Block a user