Files
ocdp-go/backend/internal/adapter/output/oci/real/oci_client.go
Ivan087 7f238a3168 refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- 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
2026-05-12 16:15:14 +08:00

899 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 layertar+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
}