* 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 (
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
url2 "net/url"
"sort"
"strings"
commonutils "gopkg.openfuyao.cn/cluster-api-provider-bke/common/utils"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
const (
Nexus = "nexus"
Harbor = "harbor"
DockerHub = "dockerhub"
Registry = "registry"
urlSplitMinParts = 2
urlSplitThreeParts = 3
tagSplitTwoParts = 2
tagSplitThreeParts = 3
tagThirdElementIndex = 2
)
type imageInfo struct {
Name string `json:"image_name"`
Tag []string `json:"image_tag"`
Architecture []string `json:"architecture"`
}
type dockerHubTagList struct {
Count int `json:"count"`
Next string `json:"next"`
Previous string `json:"previous"`
Results []*struct {
Name string `json:"name"`
FullSize int `json:"full_size"`
Images []*struct {
Size int `json:"size"`
Digest string `json:"digest"`
Architecture string `json:"architecture"`
Os string `json:"os"`
Variant string `json:"variant"`
} `json:"images"`
Id int `json:"id"`
Repository int `json:"repository"`
Creator int `json:"creator"`
LastUpdater int `json:"last_updater"`
LastUpdaterUserName string `json:"last_updater_user_name"`
V2 bool `json:"v2"`
LastUpdated string `json:"last_updated"`
} `json:"results"`
}
type nexusTagList struct {
Items []*struct {
Id string `json:"id"`
Repository string `json:"repository"`
Format string `json:"format"`
Group string `json:"group"`
Name string `json:"name"`
Version string `json:"version"`
Assets []*struct {
DownloadUrl string `json:"downloadUrl"`
Path string `json:"path"`
Id string `json:"id"`
Repository string `json:"repository"`
Format string `json:"format"`
Checksum struct {
Sha1 string `json:"sha1"`
Sha256 string `json:"sha256"`
} `json:"checksum"`
} `json:"assets"`
} `json:"items"`
}
type harborTag struct {
Id uint `json:"id"`
Digest string `json:"digest"`
ExtraAttrs struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
} `json:"extra_attrs"`
References []struct {
Platform struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
}
} `json:"references"`
Tags []struct {
Name string `json:"name"`
} `json:"tags"`
}
type registryTag struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
func imageTrack(sourceRepo, imageTrack, imageName, imageTag string, arch []string) (string, error) {
var err error
source := ""
if len(imageTrack) == 0 || strings.Contains(imageTag, cut) {
source = fmt.Sprintf("%s/%s:%s", sourceRepo, imageName, imageTag)
source = strings.ReplaceAll(source, "//", "/")
return source, nil
}
repo, url := splitRepo1(imageTrack)
newUrl, projectName := splitRepo2(url)
var imageTagList []*imageInfo
switch repo {
case DockerHub:
imageTagList, err = dockerHubTags(imageName)
if err != nil {
return source, err
}
case Nexus:
imageTagList, err = nexusTags(newUrl, imageName)
if err != nil {
return source, err
}
case Harbor:
if projectName == "" {
return "", errors.New("Project name cannot be empty ")
}
imageTagList, err = harborTags(newUrl, projectName, imageName)
if err != nil {
return source, err
}
case Registry:
if projectName == "" {
return "", errors.New("Project name cannot be empty ")
}
imageTagList, err = registryTags(newUrl, projectName, imageName)
if err != nil {
return source, err
}
default:
return "", errors.New(fmt.Sprintf("unsupported warehouse type %s", repo))
}
latestTag, err := findLatestTag(imageTagList, imageTag, arch)
if err != nil {
return source, err
}
source = fmt.Sprintf("%s/%s:%s", sourceRepo, imageName, latestTag)
source = strings.ReplaceAll(source, "//", "/")
return source, nil
}
func dockerHubTags(imageName string) ([]*imageInfo, error) {
url := fmt.Sprintf("https://registry.hub.docker.com/v2/repositories/library/%s/tags?page_size=100&&page=1", imageName)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debug(url)
log.Debug(string(body))
var tags dockerHubTagList
if err = json.Unmarshal(body, &tags); err != nil {
return nil, errors.New("image not found")
}
var it []*imageInfo
for _, t := range tags.Results {
it1 := imageInfo{Name: imageName}
it1.Tag = []string{t.Name}
for _, t1 := range t.Images {
it1.Architecture = append(it1.Architecture, t1.Architecture)
}
it = append(it, &it1)
}
return it, nil
}
func nexusTags(url, imageName string) ([]*imageInfo, error) {
userName, password, url := splitRepo3(url)
tagUrl := fmt.Sprintf("%s/service/rest/v1/search?docker.imageName=%s", url, imageName)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
cookie := ""
if userName != "" && password != "" {
loginUrl := fmt.Sprintf("%s/service/rapture/session", url)
data := make(url2.Values)
data["username"] = []string{base64.StdEncoding.EncodeToString([]byte(userName))}
data["password"] = []string{base64.StdEncoding.EncodeToString([]byte(password))}
resp1, err := http.PostForm(loginUrl, data)
if err != nil {
return nil, err
}
defer resp1.Body.Close()
cookie = resp1.Header.Get("Set-Cookie")
}
req, err := http.NewRequest("GET", tagUrl, nil)
if err != nil {
return nil, err
}
req.Header.Add("Cookie", cookie)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debug(url)
log.Debug(string(body))
var tags nexusTagList
if err = json.Unmarshal(body, &tags); err != nil {
return nil, err
}
var it []*imageInfo
for _, t := range tags.Items {
it = append(it, &imageInfo{
Name: imageName,
Tag: []string{t.Version},
Architecture: []string{},
})
}
return it, nil
}
func harborTags(url, projectName, imageName string) ([]*imageInfo, error) {
userName, password, url := splitRepo3(url)
tagUrl := fmt.Sprintf("%s/api/v2.0/projects/%s/repositories/%s/artifacts?page=1&page_size=100&with_tag=true&"+
"with_label=false&with_scan_overview=false&with_signature=false&with_immutable_status=false",
url, projectName, imageName)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
req, err := http.NewRequest("GET", tagUrl, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(userName, password)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debug(url)
log.Debug(string(body))
var tags []*harborTag
if err = json.Unmarshal(body, &tags); err != nil {
return nil, err
}
var it []*imageInfo
for _, tg := range tags {
it1 := imageInfo{Name: imageName}
for _, t1 := range tg.Tags {
it1.Tag = append(it1.Tag, t1.Name)
}
for _, t1 := range tg.References {
it1.Architecture = append(it1.Architecture, t1.Platform.Architecture)
}
if len(it1.Architecture) == 0 {
it1.Architecture = append(it1.Architecture, tg.ExtraAttrs.Architecture)
}
if it1.Tag == nil {
continue
}
it = append(it, &it1)
}
return it, nil
}
func registryTags(url, projectName, imageName string) ([]*imageInfo, error) {
userName, password, url := splitRepo3(url)
tagUrl := fmt.Sprintf("%s/v2/%s/%s/tags/list", url, projectName, imageName)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
req, err := http.NewRequest("GET", tagUrl, nil)
if err != nil {
return nil, err
}
if len(userName) > 0 {
req.SetBasicAuth(userName, password)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debug(tagUrl)
log.Debug(string(body))
var img registryTag
if err = json.Unmarshal(body, &img); err != nil {
return nil, err
}
if len(img.Tags) == 0 {
return nil, errors.New(fmt.Sprintf("%s has no tags", imageName))
}
var it []*imageInfo
for _, tg := range img.Tags {
it1 := imageInfo{Name: imageName}
it1.Tag = append(it1.Tag, tg)
it = append(it, &it1)
}
return it, nil
}
func splitRepo1(compoundAddress string) (string, string) {
repos := strings.Split(compoundAddress, "@")
url := ""
if len(repos) == urlSplitMinParts {
url = repos[1]
}
if len(repos) == urlSplitThreeParts {
url = repos[1] + "@" + repos[2]
}
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
return repos[0], url
}
func splitRepo2(compoundAddress string) (string, string) {
url := compoundAddress
projectName := ""
u1 := strings.Split(compoundAddress, "//")
if len(u1) == urlSplitMinParts {
u2 := strings.Split(u1[1], "/")
if len(u2) > 1 {
projectName = strings.Join(u2[1:len(u2)], "/")
if strings.HasSuffix(projectName, "/") {
projectName = projectName[0 : len(projectName)-1]
}
url = u1[0] + "//" + u2[0]
}
}
return url, projectName
}
func splitRepo3(url string) (string, string, string) {
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
if !strings.Contains(url, "@") {
return "", "", url
} else {
a := strings.Split(url, "@")
if len(a) < urlSplitMinParts {
return "", "", url
}
b := strings.Split(a[0], "//")
if len(b) < urlSplitMinParts {
return "", "", url
}
c := strings.Split(b[1], ":")
if len(c) < urlSplitMinParts {
return "", "", url
}
return c[0], c[1], b[0] + "//" + a[1]
}
}
type tagParseContext struct {
tagPrefix string
arch []string
tagMap map[string]string
tagList *[]string
defaultTag *string
}
func parseTagFormat(tag string, ctx tagParseContext) {
if !strings.HasPrefix(tag, ctx.tagPrefix) {
return
}
if ctx.tagMap == nil {
return
}
tag1 := strings.Split(tag, "-")
tag1Len := len(tag1)
switch tag1Len {
case 1:
*ctx.defaultTag = tag
case tagSplitTwoParts:
if tag1Len >= tagSplitTwoParts && len(tag1) > 1 {
ctx.tagMap[tag1[1]] = tag1[0]
*ctx.tagList = append(*ctx.tagList, tag1[1])
}
case tagSplitThreeParts:
if tag1Len >= tagSplitThreeParts && len(tag1) > tagThirdElementIndex && utils.ContainsString(ctx.arch, tag1[1]) {
*ctx.tagList = append(*ctx.tagList, tag1[tagThirdElementIndex])
ctx.tagMap[tag1[tagThirdElementIndex]] = tag1[0] + cut
}
default:
log.Warnf("unexpected tag format %s", tag)
}
}
busybox:v2.1
busybox:v2.1-202212242122
busybox:v2.1-amd64-202112242132
busybox:v2.1-arm64-202212242132
busybox:v2.1-arm64-202312242132
*/
func findLatestTag(imageTagList []*imageInfo, tagPrefix string, arch []string) (string, error) {
if len(imageTagList) == 0 {
return "", errors.New(fmt.Sprintf("tag %s not found. ", tagPrefix))
}
defaultTag := ""
var tagList []string
tagMap := map[string]string{}
ctx := tagParseContext{
tagPrefix: tagPrefix,
arch: arch,
tagMap: tagMap,
tagList: &tagList,
defaultTag: &defaultTag,
}
for _, image := range imageTagList {
if len(image.Architecture) != 0 && !commonutils.SliceContainsSlice(image.Architecture, arch) {
continue
}
for _, tag := range image.Tag {
parseTagFormat(tag, ctx)
}
}
if len(tagList) == 0 {
if defaultTag != "" {
return defaultTag, nil
}
return "", errors.New(fmt.Sprintf("%s %s tags %s not found. ", imageTagList[0].Name, strings.Join(arch, ","), tagPrefix))
}
sort.Strings(tagList)
source := tagMap[tagList[len(tagList)-1]] + tagList[len(tagList)-1]
return source, nil
}