支持 gcr.io k8s.gcr.io registry.k8s.io quay.io ghcr.io 等镜像库

This commit is contained in:
AnJia
2022-09-06 15:10:54 +08:00
parent a9743d3808
commit e93d0a31ab
5 changed files with 374 additions and 194 deletions

308
main.go Normal file
View File

@@ -0,0 +1,308 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/google/go-github/v47/github"
"golang.org/x/oauth2"
"gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v3"
"io"
"io/ioutil"
"os"
"regexp"
"strings"
"text/template"
)
func main() {
ctx := context.Background()
var (
ghToken = kingpin.Flag("github.token", "Github token.").Short('t').String()
ghUser = kingpin.Flag("github.user", "Github Owner.").Short('u').String()
ghRepo = kingpin.Flag("github.repo", "Github Repo.").Short('p').String()
registry = kingpin.Flag("docker.registry", "Docker Registry.").Short('r').Default("").String()
registryNamespace = kingpin.Flag("docker.namespace", "Docker Registry Namespace.").Short('n').String()
registryUserName = kingpin.Flag("docker.user", "Docker Registry User.").Short('a').String()
registryPassword = kingpin.Flag("docker.password", "Docker Registry Password.").Short('p').String()
runId = kingpin.Flag("github.run_id", "Github Run Id.").Short('i').String()
)
kingpin.HelpFlag.Short('h')
kingpin.Parse()
config := &Config{
GhToken: *ghToken,
GhUser: *ghUser,
Repo: *ghRepo,
Registry: *registry,
RegistryNamespace: *registryNamespace,
RegistryUserName: *registryUserName,
RegistryPassword: *registryPassword,
RunId: *runId,
Rules: map[string]string{
"^gcr.io": "",
"^k8s.gcr.io": "google-containers",
"^registry.k8s.io": "google-containers",
"^quay.io": "quay",
"^ghcr.io": "ghcr",
},
}
rulesFile, err := ioutil.ReadFile("rules.yaml")
if err == nil {
rules := make(map[string]string)
err2 := yaml.Unmarshal(rulesFile, &rules)
if err2 == nil {
config.Rules = rules
}
}
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: config.GhToken},
)
tc := oauth2.NewClient(ctx, ts)
cli := github.NewClient(tc)
issues, err := getIssues(cli, ctx, config)
if err != nil {
fmt.Println("查询 Issues 报错,", err.Error())
os.Exit(-1)
}
if len(issues) == 0 {
fmt.Println("暂无需要搬运的镜像")
os.Exit(0)
}
// 可以用协程,但是懒得写
issue := issues[0]
fmt.Println("添加 构建进展 Comment")
commentIssues(issue, cli, ctx, "[构建进展](https://github.com/"+config.GhUser+"/"+config.Repo+"/actions/runs/"+config.RunId+")")
err, originImageName, targetImageName := mirrorByIssues(issue, config)
if err != nil {
commentErr := commentIssues(issue, cli, ctx, err.Error())
if commentErr != nil {
fmt.Println("提交 comment 报错", commentErr)
}
}
result := struct {
Success bool
Registry string
OriginImageName string
TargetImageName string
GhUser string
Repo string
RunId string
}{
Success: err == nil,
Registry: config.Registry,
OriginImageName: originImageName,
TargetImageName: targetImageName,
GhUser: *ghUser,
Repo: *ghRepo,
RunId: *runId,
}
var buf bytes.Buffer
tmpl, err := template.New("result").Parse(resultTpl)
err = tmpl.Execute(&buf, &result)
fmt.Println("添加 转换结果 Comment")
commentIssues(issue, cli, ctx, strings.ReplaceAll(buf.String(), "^", "`"))
fmt.Println("添加 转换结果 Label")
issuesAddLabels(issue, cli, ctx, result.Success)
fmt.Println("关闭 Issues")
issuesClose(issue, cli, ctx)
}
var resultTpl = `
{{ if .Success }}
{{ if .Registry }}
**转换完成**\n
^^^bash
#原镜像\n
{{ .OriginImageName }}\n\n\n
#转换后镜像\n
{{ .TargetImageName }}\n\n\n
#下载并重命名镜像\n
docker pull {{ .TargetImageName }}\n
docker tag {{ .TargetImageName }} {{ .originImageName }}\n
docker images | grep $(echo {{ .OriginImageName }} |awk -F':' '{print $1}')\n\n\n
^^^
{{ end }}
{{ else }}
**转换失败**\n
详见 [构建任务](https://github.com/{{ .GhUser }}/{{ .Repo }}/actions/runs/{{ .RunId }})
{{ end }}
`
func issuesClose(issues *github.Issue, cli *github.Client, ctx context.Context) {
names := strings.Split(*issues.RepositoryURL, "/")
state := "closed"
cli.Issues.Edit(ctx, names[len(names)-2], names[len(names)-1], issues.GetNumber(), &github.IssueRequest{
State: &state,
})
}
func issuesAddLabels(issues *github.Issue, cli *github.Client, ctx context.Context, success bool) {
names := strings.Split(*issues.RepositoryURL, "/")
label := "success"
if !success {
label = "failed"
}
cli.Issues.AddLabelsToIssue(ctx, names[len(names)-2], names[len(names)-1], issues.GetNumber(), []string{label})
}
func commentIssues(issues *github.Issue, cli *github.Client, ctx context.Context, comment string) error {
names := strings.Split(*issues.RepositoryURL, "/")
_, _, err := cli.Issues.CreateComment(ctx, names[len(names)-2], names[len(names)-1], issues.GetNumber(), &github.IssueComment{
Body: &comment,
})
return err
}
func mirrorByIssues(issues *github.Issue, config *Config) (err error, originImageName string, targetImageName string) {
// 去掉前缀 [PORTER] 整体去除前后空格
originImageName = strings.TrimSpace(strings.Replace(*issues.Title, "[PORTER]", "", 1))
targetImageName = originImageName
if strings.ContainsAny(originImageName, "@") {
return errors.New("@" + *issues.GetUser().Login + " 不支持同步带摘要信息的镜像"), originImageName, targetImageName
}
registrys := []string{}
for k, v := range config.Rules {
targetImageName = regexp.MustCompile(k).ReplaceAllString(targetImageName, v)
registrys = append(registrys, k)
}
if strings.EqualFold(targetImageName, originImageName) {
return errors.New("@" + *issues.GetUser().Login + " 暂不支持同步" + originImageName + ",目前仅支持同步 `" + strings.Join(registrys, " ,") + "`镜像"), originImageName, targetImageName
}
if len(config.RegistryNamespace) > 0 {
targetImageName = config.RegistryNamespace + targetImageName
}
if len(config.Registry) > 0 {
targetImageName = config.Registry + "/" + targetImageName
}
fmt.Println("source:", originImageName, " , target:", targetImageName)
cli, ctx, err := dockerLogin(config)
if err != nil {
return errors.New("@" + config.GhUser + " ,docker login 报错 `" + err.Error() + "`"), originImageName, targetImageName
}
//execCmd("docker", "login", config.Registry, "-u", config.RegistryUserName, "-p", config.RegistryPassword)
//execCmd("docker", "pull", originImageName)
err = dockerPull(originImageName, cli, ctx)
if err != nil {
return errors.New("@" + *issues.GetUser().Login + " ,docker pull 报错 `" + err.Error() + "`"), originImageName, targetImageName
}
//execCmd("docker", "tag", originImageName, targetImageName)
err = dockerTag(originImageName, targetImageName, cli, ctx)
if err != nil {
return errors.New("@" + config.GhUser + " ,docker tag 报错 `" + err.Error() + "`"), originImageName, targetImageName
}
//execCmd("docker", "push", targetImageName)
err = dockerPush(targetImageName, cli, ctx, config)
if err != nil {
return errors.New("@" + config.GhUser + " ,docker push 报错 `" + err.Error() + "`"), originImageName, targetImageName
}
return nil, originImageName, targetImageName
}
func dockerLogin(config *Config) (*client.Client, context.Context, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, nil, err
}
fmt.Println("docker login, server: ", config.Registry, " user: ", config.RegistryUserName, ", password: ***")
authConfig := types.AuthConfig{
Username: config.RegistryUserName,
Password: config.RegistryPassword,
ServerAddress: config.Registry,
}
ctx := context.Background()
_, err = cli.RegistryLogin(ctx, authConfig)
if err != nil {
return nil, nil, err
}
return cli, ctx, nil
}
func dockerPull(originImageName string, cli *client.Client, ctx context.Context) error {
fmt.Println("docker pull ", originImageName)
pullOut, err := cli.ImagePull(ctx, originImageName, types.ImagePullOptions{})
if err != nil {
return err
}
defer pullOut.Close()
io.Copy(os.Stdout, pullOut)
return nil
}
func dockerTag(originImageName string, targetImageName string, cli *client.Client, ctx context.Context) error {
fmt.Println("docker tag ", originImageName, " ", targetImageName)
err := cli.ImageTag(ctx, originImageName, targetImageName)
return err
}
func dockerPush(targetImageName string, cli *client.Client, ctx context.Context, config *Config) error {
fmt.Println("docker push ", targetImageName)
authConfig := types.AuthConfig{
Username: config.RegistryUserName,
Password: config.RegistryPassword,
ServerAddress: config.Registry,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
pushOut, err := cli.ImagePush(ctx, targetImageName, types.ImagePushOptions{
RegistryAuth: authStr,
})
if err != nil {
return err
}
defer pushOut.Close()
io.Copy(os.Stdout, pushOut)
return nil
}
type Config struct {
GhToken string `yaml:"gh_token"`
GhUser string `yaml:"gh_user"`
Repo string `yaml:"repo"`
Registry string `yaml:"registry"`
RegistryNamespace string `yaml:"registry_namespace"`
RegistryUserName string `yaml:"registry_user_name"`
RegistryPassword string `yaml:"registry_password"`
Rules map[string]string `yaml:"rules"`
RunId string `yaml:"run_id"`
}
func getIssues(cli *github.Client, ctx context.Context, config *Config) ([]*github.Issue, error) {
issues, _, err := cli.Issues.ListByRepo(ctx, config.GhUser, config.Repo, &github.IssueListByRepoOptions{
//State: "closed",
State: "open",
Labels: []string{"porter"},
Sort: "created",
Direction: "desc",
// 防止被滥用每次最多只能拉20条虽然可以递归但是没必要。
//ListOptions: github.ListOptions{Page: 1, PerPage: 20},
// 考虑了下,每次还是只允许转一个吧
ListOptions: github.ListOptions{Page: 1, PerPage: 1},
})
return issues, err
}