* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain n copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
******************************************************************/
package build
import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"gopkg.in/yaml.v3"
"gopkg.openfuyao.cn/bkeadm/pkg/common"
reg "gopkg.openfuyao.cn/bkeadm/pkg/registry"
"gopkg.openfuyao.cn/bkeadm/pkg/server"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
func loadLocalRepository(imageFile string) error {
return common.LoadLocalRepositoryFromFile(imageFile)
}
func SpecificSync(source, target string) {
newTarget := normalizeTargetPath(target)
if !validateSourceFiles(source) {
return
}
cfg, err := loadManifestConfig(source)
if err != nil {
log.Errorf("Load patch manifests from %s failed: %v", source, err)
return
}
mountPath := path.Join(source, "mount")
ensureMountDirectoryExists(mountPath)
format := DetectPatchFormat(source)
log.Infof("Detected patch format: %s", format)
switch format {
case "oci":
log.Info("Detected OCI format, using direct OCI-to-target conversion")
if err = syncImagesFromOCI(source, cfg, newTarget); err != nil {
log.Errorf("Sync images from OCI %s to %s failed: %v", source, newTarget, err)
return
}
log.Info("OCI images synced successfully to target registry")
case "registry":
if err = prepareImageData(source); err != nil {
log.Errorf("Prepare image data from %s failed: %v", source, err)
return
}
if err = loadAndStartRegistry(source); err != nil {
log.Errorf("Load and start patch image registry from %s failed: %v", source, err)
if err = server.RemoveImageRegistry(utils.PatchImageRegistryName); err != nil {
log.Errorf("Remove patch image registry %s failed: %v", utils.PatchImageRegistryName, err)
}
return
}
if err = syncImages(cfg, newTarget); err != nil {
log.Errorf("Sync patch images to %s failed: %v", newTarget, err)
}
if err = server.RemoveImageRegistry(utils.PatchImageRegistryName); err != nil {
log.Errorf("Remove patch image registry %s failed: %v", utils.PatchImageRegistryName, err)
}
default:
log.Errorf("Unknown patch format: %s", format)
return
}
}
func normalizeTargetPath(target string) string {
if !strings.HasSuffix(target, "/") {
return target + "/"
}
return target
}
func validateSourceFiles(source string) bool {
if !utils.IsDir(source) {
log.Errorf("The %s is not a directory", source)
return false
}
manifestFile := path.Join(source, "manifests.yaml")
if !utils.IsFile(manifestFile) {
log.Errorf("The manifests.yaml is not found in %s", source)
return false
}
hasRegistry := utils.IsFile(path.Join(source, utils.ImageDataFile))
hasOCI := utils.IsDir(path.Join(source, "volumes", "oci-layout"))
if !hasRegistry && !hasOCI {
log.Error("Neither image.tar.gz nor oci-layout directory found")
return false
}
if hasRegistry && !hasOCI {
imageFile := path.Join(source, utils.ImageFile+"-"+runtime.GOARCH)
if !utils.IsFile(imageFile) {
log.Warnf("The %s is not found, may cause issues", utils.ImageFile+"-"+runtime.GOARCH)
}
}
return true
}
func loadManifestConfig(source string) (*BuildConfig, error) {
manifests := path.Join(source, "manifests.yaml")
yamlFile, err := os.ReadFile(manifests)
if err != nil {
return nil, fmt.Errorf("failed to read %s, %v", manifests, err)
}
cfg := &BuildConfig{}
if err = yaml.Unmarshal(yamlFile, cfg); err != nil {
return nil, fmt.Errorf("unable to serialize file, %v", err)
}
return cfg, nil
}
func ensureMountDirectoryExists(mountPath string) {
if !utils.Exists(mountPath) {
if err := os.MkdirAll(mountPath, utils.DefaultDirPermission); err != nil {
log.Errorf("Create mount directory %s failed: %v", mountPath, err)
}
}
}
func prepareImageData(source string) error {
imageDataDirectory := path.Join(source, utils.ImageDataDirectory)
if !utils.Exists(imageDataDirectory) || utils.DirectoryIsEmpty(imageDataDirectory) {
log.Info("Decompressing the image package...")
if err := utils.UnTar(path.Join(source, utils.ImageDataFile), imageDataDirectory); err != nil {
return err
}
if !utils.Exists(imageDataDirectory) {
log.Errorf("%s, not found", imageDataDirectory)
return fmt.Errorf("image data directory not found")
}
} else {
log.Warn("If the image file already exists, skip decompressing the volumes/image.tar.gz file")
}
return nil
}
func loadAndStartRegistry(source string) error {
imageFile := path.Join(source, utils.ImageFile+"-"+runtime.GOARCH)
if err := loadLocalRepository(imageFile); err != nil {
log.Errorf("Load local repository from %s failed: %v", imageFile, err)
return err
}
return server.StartImageRegistry(
utils.PatchImageRegistryName,
utils.DefaultLocalImageRegistry,
"40448",
path.Join(source, utils.ImageDataDirectory),
)
}
func syncImages(cfg *BuildConfig, target string) error {
for _, repos := range cfg.Repos {
if !repos.NeedDownload {
continue
}
opts := buildRepoSyncOptions(repos)
for _, subImage := range repos.SubImages {
if err := syncSubImage(subImage, opts, target); err != nil {
return err
}
}
}
return nil
}
func syncSubImage(subImage SubImage, opts reg.Options, target string) error {
sourcePrefix := normalizeRegistryPath("0.0.0.0:40448/" + subImage.TargetRepo)
targetPrefix := normalizeRegistryPath(target + subImage.TargetRepo)
for _, image := range subImage.Images {
if err := syncImageTags(image, sourcePrefix, targetPrefix, opts); err != nil {
return err
}
}
return nil
}
func syncImageTags(image Image, sourcePrefix, targetPrefix string, opts reg.Options) error {
for _, tag := range image.Tag {
tagName := strings.Split(tag, cut)[0]
opts.Source = sourcePrefix + image.Name + ":" + tagName
opts.Target = targetPrefix + image.Name + ":" + tagName
if err := reg.CopyRegistry(opts); err != nil {
log.Errorf("Sync image %s to %s failed: %v", opts.Source, opts.Target, err)
return err
}
}
return nil
}
func getArchitecture(archs []string) string {
if len(archs) == 1 {
return archs[0]
}
return ""
}
func buildRepoSyncOptions(repos Repo) reg.Options {
return reg.Options{
MultiArch: len(repos.Architecture) > 1,
Arch: getArchitecture(repos.Architecture),
SrcTLSVerify: false,
DestTLSVerify: false,
}
}
func normalizeRegistryPath(path string) string {
if !strings.HasSuffix(path, "/") {
path += "/"
}
return strings.ReplaceAll(path, "//", "/")
}
func syncImagesFromOCI(source string, cfg *BuildConfig, target string) error {
ociDir := filepath.Join(source, "volumes", "oci-layout")
absOciDir, err := filepath.Abs(ociDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for OCI directory: %v", err)
}
for _, repos := range cfg.Repos {
if !repos.NeedDownload {
continue
}
opts := buildRepoSyncOptions(repos)
for _, subImage := range repos.SubImages {
if err := syncSubImageFromOCI(absOciDir, subImage, opts, target); err != nil {
return err
}
}
}
return nil
}
func syncSubImageFromOCI(ociDir string, subImage SubImage, opts reg.Options, target string) error {
targetPrefix := normalizeRegistryPath(target + subImage.TargetRepo)
for _, image := range subImage.Images {
if err := syncImageTagsFromOCI(ociDir, image, targetPrefix, opts); err != nil {
return err
}
}
return nil
}
func copyImageFromOCI(ociSource, dockerTarget string, opts reg.Options) error {
return reg.CopyRegistry(reg.Options{
Source: ociSource,
Target: dockerTarget,
Arch: opts.Arch,
MultiArch: opts.MultiArch,
SrcTLSVerify: false,
DestTLSVerify: opts.DestTLSVerify,
})
}
func syncImageTagsFromOCI(ociDir string, image Image, targetPrefix string, opts reg.Options) error {
for _, tag := range image.Tag {
tagName := strings.Split(tag, cut)[0]
fullTargetRef := targetPrefix + image.Name + ":" + tagName
ociImageRef := fullTargetRef
if idx := strings.Index(ociImageRef, "/"); idx != -1 {
ociImageRef = ociImageRef[idx+1:]
}
ociSource := fmt.Sprintf("oci:%s:%s", ociDir, ociImageRef)
dockerTarget := fmt.Sprintf("docker://%s", fullTargetRef)
log.Infof("Syncing from OCI: %s -> %s", ociImageRef, fullTargetRef)
if err := copyImageFromOCI(ociSource, dockerTarget, opts); err != nil {
log.Errorf("Copy OCI image %s to %s failed: %v", ociSource, dockerTarget, err)
log.Error("=== Error Details ===")
log.Errorf("Failed to copy from: %s", ociSource)
log.Errorf("Failed to copy to: %s", dockerTarget)
log.Error("Possible causes:")
log.Error(" 1. OCI layout directory does not exist or is corrupted")
log.Error(" 2. Reference name in index.json does not match")
log.Error(" 3. Target registry is not accessible")
log.Error("====================")
return err
}
log.Infof("Successfully synced: %s", ociImageRef)
}
return nil
}