- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
899 lines
25 KiB
Go
899 lines
25 KiB
Go
package real
|
||
|
||
import (
|
||
"archive/tar"
|
||
"compress/gzip"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||
"github.com/opencontainers/go-digest"
|
||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||
"oras.land/oras-go/v2/registry/remote"
|
||
"oras.land/oras-go/v2/registry/remote/auth"
|
||
)
|
||
|
||
// OCIClient 真实的 OCI 客户端实现(使用 ORAS)
|
||
type OCIClient struct {
|
||
httpClient *http.Client
|
||
}
|
||
|
||
type harborProject struct {
|
||
Name string `json:"name"`
|
||
}
|
||
|
||
type harborRepository struct {
|
||
Name string `json:"name"`
|
||
ArtifactCount int `json:"artifact_count"`
|
||
}
|
||
|
||
type harborTag struct {
|
||
Name string `json:"name"`
|
||
PushTime string `json:"push_time"`
|
||
}
|
||
|
||
type harborArtifact struct {
|
||
Digest string `json:"digest"`
|
||
MediaType string `json:"media_type"`
|
||
ArtifactType string `json:"artifact_type"`
|
||
Size int64 `json:"size"`
|
||
PushTime string `json:"push_time"`
|
||
Tags []harborTag `json:"tags"`
|
||
Annotations map[string]string `json:"annotations"`
|
||
}
|
||
|
||
// NewOCIClient 创建真实的 OCI 客户端
|
||
func NewOCIClient() repository.OCIClient {
|
||
return &OCIClient{
|
||
httpClient: &http.Client{},
|
||
}
|
||
}
|
||
|
||
// getRegistry 创建 ORAS Registry 客户端
|
||
func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error) {
|
||
// 解析 Registry URL
|
||
registryURL := strings.TrimPrefix(reg.URL, "https://")
|
||
registryURL = strings.TrimPrefix(registryURL, "http://")
|
||
|
||
registry, err := remote.NewRegistry(registryURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
||
}
|
||
|
||
// 设置认证
|
||
if reg.Username != "" && reg.Password != "" {
|
||
registry.Client = &auth.Client{
|
||
Client: c.httpClient,
|
||
Credential: auth.StaticCredential(registryURL, auth.Credential{
|
||
Username: reg.Username,
|
||
Password: reg.Password,
|
||
}),
|
||
}
|
||
}
|
||
|
||
// 设置 PlainHTTP(如果是 insecure)
|
||
registry.PlainHTTP = reg.Insecure
|
||
|
||
return registry, nil
|
||
}
|
||
|
||
// ListRepositories 列出 Registry 中的 repositories.
|
||
// Harbor registry 优先使用 Harbor v2.0 API,避免 robot 账号依赖 /v2/_catalog 全局权限。
|
||
func (c *OCIClient) ListRepositories(ctx context.Context, registry *entity.Registry, artifactType string) ([]string, error) {
|
||
repositories, harborErr := c.listHarborRepositories(ctx, registry, artifactType)
|
||
if harborErr == nil {
|
||
return repositories, nil
|
||
}
|
||
|
||
repositories, catalogErr := c.listOCIRepositories(ctx, registry)
|
||
if catalogErr != nil {
|
||
return nil, fmt.Errorf("failed to list repositories via Harbor API: %v; OCI catalog fallback also failed: %w", harborErr, catalogErr)
|
||
}
|
||
|
||
if strings.EqualFold(strings.TrimSpace(artifactType), "chart") {
|
||
chartRepos := make([]string, 0)
|
||
for _, repo := range repositories {
|
||
artifacts, err := c.ListArtifacts(ctx, registry, repo, "chart")
|
||
if err == nil && len(artifacts) > 0 {
|
||
chartRepos = append(chartRepos, repo)
|
||
}
|
||
}
|
||
return chartRepos, nil
|
||
}
|
||
|
||
return repositories, nil
|
||
}
|
||
|
||
func (c *OCIClient) listOCIRepositories(ctx context.Context, registry *entity.Registry) ([]string, error) {
|
||
reg, err := c.getRegistry(registry)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
repositories := make([]string, 0)
|
||
|
||
err = reg.Repositories(ctx, "", func(repos []string) error {
|
||
repositories = append(repositories, repos...)
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to list repositories: %w", err)
|
||
}
|
||
|
||
return repositories, nil
|
||
}
|
||
|
||
func (c *OCIClient) listHarborRepositories(ctx context.Context, registry *entity.Registry, artifactType string) ([]string, error) {
|
||
projects, err := c.harborListProjects(ctx, registry)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
repositorySet := make(map[string]struct{})
|
||
chartOnly := strings.EqualFold(strings.TrimSpace(artifactType), "chart") || strings.TrimSpace(artifactType) == ""
|
||
|
||
for _, project := range projects {
|
||
projectName := strings.TrimSpace(project.Name)
|
||
if projectName == "" {
|
||
continue
|
||
}
|
||
|
||
repositories, err := c.harborListProjectRepositories(ctx, registry, projectName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, harborRepo := range repositories {
|
||
repoName := normalizeHarborRepositoryName(projectName, harborRepo.Name)
|
||
if repoName == "" {
|
||
continue
|
||
}
|
||
if chartOnly {
|
||
artifacts, err := c.listHarborArtifacts(ctx, registry, repoName, "chart")
|
||
if err != nil || len(artifacts) == 0 {
|
||
continue
|
||
}
|
||
}
|
||
repositorySet[repoName] = struct{}{}
|
||
}
|
||
}
|
||
|
||
repositories := make([]string, 0, len(repositorySet))
|
||
for repo := range repositorySet {
|
||
repositories = append(repositories, repo)
|
||
}
|
||
sort.Strings(repositories)
|
||
return repositories, nil
|
||
}
|
||
|
||
func (c *OCIClient) harborListProjects(ctx context.Context, registry *entity.Registry) ([]harborProject, error) {
|
||
var projects []harborProject
|
||
if err := c.harborGetPaged(ctx, registry, "/api/v2.0/projects", url.Values{"member": []string{"true"}}, &projects); err != nil {
|
||
return nil, err
|
||
}
|
||
return projects, nil
|
||
}
|
||
|
||
func (c *OCIClient) harborListProjectRepositories(ctx context.Context, registry *entity.Registry, projectName string) ([]harborRepository, error) {
|
||
var repositories []harborRepository
|
||
path := "/api/v2.0/projects/" + url.PathEscape(projectName) + "/repositories"
|
||
if err := c.harborGetPaged(ctx, registry, path, nil, &repositories); err != nil {
|
||
return nil, err
|
||
}
|
||
return repositories, nil
|
||
}
|
||
|
||
func (c *OCIClient) listHarborArtifacts(ctx context.Context, registry *entity.Registry, repository, mediaTypeFilter string) ([]*entity.Artifact, error) {
|
||
projectName, repoName, ok := splitHarborRepository(repository)
|
||
if !ok {
|
||
return nil, fmt.Errorf("repository %q is not a Harbor project repository path", repository)
|
||
}
|
||
|
||
var harborArtifacts []harborArtifact
|
||
path := "/api/v2.0/projects/" + url.PathEscape(projectName) + "/repositories/" + url.PathEscape(repoName) + "/artifacts"
|
||
query := url.Values{
|
||
"with_tag": []string{"true"},
|
||
"with_label": []string{"false"},
|
||
}
|
||
if err := c.harborGetPaged(ctx, registry, path, query, &harborArtifacts); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
artifacts := make([]*entity.Artifact, 0)
|
||
for _, harborArtifact := range harborArtifacts {
|
||
tags := harborArtifact.Tags
|
||
if len(tags) == 0 {
|
||
continue
|
||
}
|
||
|
||
for _, tag := range tags {
|
||
if strings.TrimSpace(tag.Name) == "" {
|
||
continue
|
||
}
|
||
artifact := &entity.Artifact{
|
||
Repository: repository,
|
||
Tag: tag.Name,
|
||
Digest: harborArtifact.Digest,
|
||
MediaType: harborArtifact.MediaType,
|
||
ConfigType: harborArtifact.ArtifactType,
|
||
Size: harborArtifact.Size,
|
||
Annotations: harborArtifact.Annotations,
|
||
CreatedAt: parseHarborTime(firstNonEmpty(tag.PushTime, harborArtifact.PushTime)),
|
||
}
|
||
if artifact.Annotations == nil {
|
||
artifact.Annotations = make(map[string]string)
|
||
}
|
||
|
||
artifact.DetermineType()
|
||
if isHarborChartArtifact(harborArtifact) {
|
||
artifact.Type = entity.ArtifactTypeChart
|
||
}
|
||
|
||
if c.shouldIncludeArtifact(artifact, mediaTypeFilter) {
|
||
artifacts = append(artifacts, artifact)
|
||
}
|
||
}
|
||
}
|
||
|
||
return artifacts, nil
|
||
}
|
||
|
||
func (c *OCIClient) harborGetPaged(ctx context.Context, registry *entity.Registry, path string, query url.Values, target interface{}) error {
|
||
const pageSize = 100
|
||
|
||
accumulated := make([]json.RawMessage, 0)
|
||
for page := 1; ; page++ {
|
||
pageQuery := cloneValues(query)
|
||
pageQuery.Set("page", fmt.Sprintf("%d", page))
|
||
pageQuery.Set("page_size", fmt.Sprintf("%d", pageSize))
|
||
|
||
body, total, err := c.harborGet(ctx, registry, path, pageQuery)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var pageItems []json.RawMessage
|
||
if err := json.Unmarshal(body, &pageItems); err != nil {
|
||
return fmt.Errorf("failed to decode Harbor response for %s: %w", path, err)
|
||
}
|
||
accumulated = append(accumulated, pageItems...)
|
||
|
||
if len(pageItems) < pageSize || (total >= 0 && len(accumulated) >= total) {
|
||
break
|
||
}
|
||
}
|
||
|
||
combined, err := json.Marshal(accumulated)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to combine Harbor pages: %w", err)
|
||
}
|
||
if err := json.Unmarshal(combined, target); err != nil {
|
||
return fmt.Errorf("failed to decode Harbor pages: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *OCIClient) harborGet(ctx context.Context, registry *entity.Registry, path string, query url.Values) ([]byte, int, error) {
|
||
baseURL, err := harborBaseURL(registry)
|
||
if err != nil {
|
||
return nil, -1, err
|
||
}
|
||
|
||
requestURL := strings.TrimRight(baseURL, "/") + path
|
||
if len(query) > 0 {
|
||
requestURL += "?" + query.Encode()
|
||
}
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||
if err != nil {
|
||
return nil, -1, err
|
||
}
|
||
req.Header.Set("Accept", "application/json")
|
||
if registry.Username != "" || registry.Password != "" {
|
||
req.SetBasicAuth(registry.Username, registry.Password)
|
||
}
|
||
|
||
resp, err := c.httpClient.Do(req)
|
||
if err != nil {
|
||
return nil, -1, fmt.Errorf("Harbor API request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
|
||
if readErr != nil {
|
||
return nil, -1, fmt.Errorf("failed to read Harbor API response: %w", readErr)
|
||
}
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return nil, -1, fmt.Errorf("Harbor API %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
|
||
}
|
||
|
||
total := -1
|
||
if value := strings.TrimSpace(resp.Header.Get("X-Total-Count")); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
total = parsed
|
||
}
|
||
}
|
||
return body, total, nil
|
||
}
|
||
|
||
func harborBaseURL(registry *entity.Registry) (string, error) {
|
||
rawURL := strings.TrimSpace(registry.URL)
|
||
if rawURL == "" {
|
||
return "", fmt.Errorf("registry URL is empty")
|
||
}
|
||
if !strings.Contains(rawURL, "://") {
|
||
rawURL = "https://" + rawURL
|
||
}
|
||
parsed, err := url.Parse(rawURL)
|
||
if err != nil {
|
||
return "", fmt.Errorf("invalid registry URL %q: %w", registry.URL, err)
|
||
}
|
||
if parsed.Scheme == "" || parsed.Host == "" {
|
||
return "", fmt.Errorf("invalid registry URL %q", registry.URL)
|
||
}
|
||
return parsed.Scheme + "://" + parsed.Host, nil
|
||
}
|
||
|
||
func splitHarborRepository(repository string) (string, string, bool) {
|
||
projectName, repoName, ok := strings.Cut(strings.Trim(repository, "/"), "/")
|
||
if !ok || projectName == "" || repoName == "" {
|
||
return "", "", false
|
||
}
|
||
return projectName, repoName, true
|
||
}
|
||
|
||
func normalizeHarborRepositoryName(projectName, repositoryName string) string {
|
||
repositoryName = strings.Trim(repositoryName, "/")
|
||
if repositoryName == "" {
|
||
return ""
|
||
}
|
||
if strings.HasPrefix(repositoryName, projectName+"/") {
|
||
return repositoryName
|
||
}
|
||
return projectName + "/" + repositoryName
|
||
}
|
||
|
||
func isHarborChartArtifact(artifact harborArtifact) bool {
|
||
typeInfo := strings.ToLower(strings.TrimSpace(artifact.ArtifactType + " " + artifact.MediaType))
|
||
return strings.Contains(typeInfo, "chart") || strings.Contains(typeInfo, "helm")
|
||
}
|
||
|
||
func cloneValues(values url.Values) url.Values {
|
||
cloned := make(url.Values)
|
||
for key, items := range values {
|
||
cloned[key] = append([]string(nil), items...)
|
||
}
|
||
return cloned
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func parseHarborTime(value string) time.Time {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return time.Time{}
|
||
}
|
||
for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05.999999", "2006-01-02T15:04:05"} {
|
||
if parsed, err := time.Parse(layout, value); err == nil {
|
||
return parsed
|
||
}
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
// ListArtifacts 列出指定 repository 的所有 artifacts
|
||
// mediaTypeFilter: "all", "image", "chart", "other" - 使用模糊匹配过滤
|
||
func (c *OCIClient) ListArtifacts(ctx context.Context, registry *entity.Registry, repository, mediaTypeFilter string) ([]*entity.Artifact, error) {
|
||
if artifacts, err := c.listHarborArtifacts(ctx, registry, repository, mediaTypeFilter); err == nil {
|
||
return artifacts, nil
|
||
}
|
||
|
||
reg, err := c.getRegistry(registry)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
repo, err := reg.Repository(ctx, repository)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get repository: %w", err)
|
||
}
|
||
|
||
artifacts := make([]*entity.Artifact, 0)
|
||
|
||
err = repo.Tags(ctx, "", func(tags []string) error {
|
||
for _, tag := range tags {
|
||
// 获取 manifest 以获取更多信息
|
||
desc, err := repo.Resolve(ctx, tag)
|
||
if err != nil {
|
||
// 跳过无法解析的 tag
|
||
continue
|
||
}
|
||
|
||
artifact := &entity.Artifact{
|
||
Repository: repository,
|
||
Tag: tag,
|
||
Digest: desc.Digest.String(),
|
||
MediaType: desc.MediaType,
|
||
Size: desc.Size,
|
||
}
|
||
|
||
// 尝试获取 config.mediaType 以更准确判断类型
|
||
if manifestBytes, err := repo.Fetch(ctx, desc); err == nil {
|
||
defer manifestBytes.Close()
|
||
if manifestData, err := io.ReadAll(manifestBytes); err == nil {
|
||
var manifest map[string]interface{}
|
||
if err := json.Unmarshal(manifestData, &manifest); err == nil {
|
||
// 获取 config.mediaType
|
||
if config, ok := manifest["config"].(map[string]interface{}); ok {
|
||
if configMediaType, ok := config["mediaType"].(string); ok {
|
||
artifact.ConfigType = configMediaType
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用智能类型判断(综合多种信息)
|
||
artifact.DetermineType()
|
||
|
||
// 应用 mediaType 过滤
|
||
if c.shouldIncludeArtifact(artifact, mediaTypeFilter) {
|
||
artifacts = append(artifacts, artifact)
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to list artifacts: %w", err)
|
||
}
|
||
|
||
return artifacts, nil
|
||
}
|
||
|
||
// shouldIncludeArtifact 判断是否应该包含该 artifact
|
||
func (c *OCIClient) shouldIncludeArtifact(artifact *entity.Artifact, filter string) bool {
|
||
// 默认或 "all" 返回所有
|
||
if filter == "" || filter == "all" {
|
||
return true
|
||
}
|
||
|
||
filter = strings.ToLower(strings.TrimSpace(filter))
|
||
|
||
switch filter {
|
||
case "chart":
|
||
// 只返回 Helm Charts
|
||
return artifact.Type == entity.ArtifactTypeChart
|
||
case "image":
|
||
// 返回 Docker 或 OCI images
|
||
return artifact.Type == entity.ArtifactTypeImage
|
||
case "other":
|
||
// 返回其他类型
|
||
return artifact.Type == entity.ArtifactTypeOther
|
||
default:
|
||
// 未知的 filter,返回所有
|
||
return true
|
||
}
|
||
}
|
||
|
||
// GetArtifact 获取指定 artifact 的详细信息
|
||
func (c *OCIClient) GetArtifact(ctx context.Context, registry *entity.Registry, repository, reference string) (*entity.Artifact, error) {
|
||
reg, err := c.getRegistry(registry)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
repo, err := reg.Repository(ctx, repository)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get repository: %w", err)
|
||
}
|
||
|
||
// 解析 reference
|
||
desc, err := repo.Resolve(ctx, reference)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to resolve artifact: %w", err)
|
||
}
|
||
|
||
// 获取 manifest
|
||
manifestBytes, err := repo.Fetch(ctx, desc)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to fetch manifest: %w", err)
|
||
}
|
||
defer manifestBytes.Close()
|
||
|
||
manifestData, err := io.ReadAll(manifestBytes)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to read manifest: %w", err)
|
||
}
|
||
|
||
// 解析 manifest 获取配置信息
|
||
var manifest map[string]interface{}
|
||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||
return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
|
||
}
|
||
|
||
artifact := &entity.Artifact{
|
||
Repository: repository,
|
||
Tag: reference,
|
||
Digest: desc.Digest.String(),
|
||
MediaType: desc.MediaType,
|
||
Size: desc.Size,
|
||
Annotations: make(map[string]string),
|
||
}
|
||
|
||
// 获取 config.mediaType 和 annotations
|
||
if config, ok := manifest["config"].(map[string]interface{}); ok {
|
||
// 获取 config.mediaType(用于准确的类型判断)
|
||
if configMediaType, ok := config["mediaType"].(string); ok {
|
||
artifact.ConfigType = configMediaType
|
||
}
|
||
|
||
// 获取 annotations
|
||
if annotations, ok := config["annotations"].(map[string]interface{}); ok {
|
||
for k, v := range annotations {
|
||
if str, ok := v.(string); ok {
|
||
artifact.Annotations[k] = str
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用智能类型判断(综合 ConfigType, Annotations, Repository 名称等)
|
||
artifact.DetermineType()
|
||
|
||
return artifact, nil
|
||
}
|
||
|
||
// GetValuesSchema 获取 Helm Chart 的 values schema
|
||
func (c *OCIClient) GetValuesSchema(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)
|
||
}
|
||
|
||
// 优先查找是否存在独立的 values schema layer(一些 registry 会将 values.schema.json 作为单独的 layer 存储)
|
||
var valuesSchemaLayer *ocispec.Descriptor
|
||
for i := range manifest.Layers {
|
||
layer := manifest.Layers[i]
|
||
mediaType := strings.ToLower(layer.MediaType)
|
||
|
||
if strings.Contains(mediaType, "helm.values.schema") ||
|
||
strings.Contains(mediaType, "values.schema") {
|
||
valuesSchemaLayer = &manifest.Layers[i]
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果存在独立的 values schema layer,直接返回
|
||
if valuesSchemaLayer != nil {
|
||
reader, err := repo.Fetch(ctx, *valuesSchemaLayer)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to fetch values schema layer: %w", err)
|
||
}
|
||
defer reader.Close()
|
||
|
||
data, err := io.ReadAll(reader)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to read values schema layer: %w", err)
|
||
}
|
||
|
||
if len(data) == 0 {
|
||
return "", entity.ErrValuesSchemaNotFound
|
||
}
|
||
|
||
return string(data), nil
|
||
}
|
||
|
||
// 回退:查找 Helm Chart layer(tar+gzip 包含 chart 内容)并从中读取 values.schema.json
|
||
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.ErrValuesSchemaNotFound
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
if strings.HasSuffix(header.Name, "values.schema.json") {
|
||
data, err := io.ReadAll(tarReader)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to read values.schema.json: %w", err)
|
||
}
|
||
if len(data) == 0 {
|
||
return "", entity.ErrValuesSchemaNotFound
|
||
}
|
||
return string(data), nil
|
||
}
|
||
}
|
||
|
||
return "", entity.ErrValuesSchemaNotFound
|
||
}
|
||
|
||
// GetValuesYAML 获取 Helm Chart 包内原始 values.yaml
|
||
func (c *OCIClient) GetValuesYAML(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
|
||
data, err := c.readChartFile(ctx, registry, repository, reference, "values.yaml")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if strings.TrimSpace(data) == "" {
|
||
return "", entity.ErrArtifactNotFound
|
||
}
|
||
return data, nil
|
||
}
|
||
|
||
func (c *OCIClient) readChartFile(ctx context.Context, registry *entity.Registry, repository, reference, filename 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)
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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 "", fmt.Errorf("helm chart layer not found in manifest")
|
||
}
|
||
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)
|
||
bestDepth := int(^uint(0) >> 1)
|
||
var bestData []byte
|
||
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
|
||
}
|
||
if strings.HasSuffix(header.Name, filename) {
|
||
data, err := io.ReadAll(tarReader)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to read %s: %w", filename, err)
|
||
}
|
||
depth := strings.Count(strings.Trim(header.Name, "/"), "/")
|
||
if depth < bestDepth {
|
||
bestDepth = depth
|
||
bestData = data
|
||
}
|
||
}
|
||
}
|
||
if len(bestData) > 0 {
|
||
return string(bestData), nil
|
||
}
|
||
return "", fmt.Errorf("%s not found in chart", filename)
|
||
}
|
||
|
||
// PullArtifact 下载 artifact 到本地
|
||
func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath 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
|
||
desc, err := repo.Resolve(ctx, reference)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to resolve artifact: %w", err)
|
||
}
|
||
|
||
// 获取 manifest 内容
|
||
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)
|
||
}
|
||
|
||
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 = &layer
|
||
break
|
||
}
|
||
}
|
||
|
||
if chartLayer == nil {
|
||
return fmt.Errorf("helm chart layer not found in manifest")
|
||
}
|
||
|
||
content, err := repo.Fetch(ctx, *chartLayer)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to fetch chart layer: %w", err)
|
||
}
|
||
defer content.Close()
|
||
|
||
// 确保目标目录存在
|
||
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||
}
|
||
|
||
// 写入文件
|
||
file, err := os.Create(destPath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create file: %w", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
if _, err := io.Copy(file, content); err != nil {
|
||
return fmt.Errorf("failed to write artifact: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// PushArtifact 推送 artifact 到 Registry
|
||
func (c *OCIClient) PushArtifact(ctx context.Context, registry *entity.Registry, repository, tag, sourcePath string) error {
|
||
// 这是一个简化实现
|
||
// 实际应该实现完整的 OCI artifact push 流程
|
||
return fmt.Errorf("push artifact not fully implemented yet")
|
||
}
|
||
|
||
// CheckHealth 检查 Registry 健康状态
|
||
func (c *OCIClient) CheckHealth(ctx context.Context, registry *entity.Registry) error {
|
||
reg, err := c.getRegistry(registry)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 尝试 ping registry
|
||
err = reg.Ping(ctx)
|
||
if err != nil {
|
||
return fmt.Errorf("registry health check failed: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|