Rainbond/builder/sources/image.go
2020-04-16 17:13:50 +08:00

548 lines
15 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.

// Copyright (C) 2014-2018 Goodrain Co., Ltd.
// RAINBOND, Application Management Platform
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package sources
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/goodrain/rainbond/builder"
"github.com/goodrain/rainbond/builder/model"
"github.com/goodrain/rainbond/event"
"golang.org/x/net/context"
)
//ErrorNoAuth error no auth
var ErrorNoAuth = fmt.Errorf("pull image require docker login")
//ErrorNoImage error no image
var ErrorNoImage = fmt.Errorf("image not exist")
//ImagePull pull docker image
//timeout minutes of the unit
func ImagePull(dockerCli *client.Client, image string, username, password string, logger event.Logger, timeout int) (*types.ImageInspect, error) {
if logger != nil {
logger.Info(fmt.Sprintf("start get image:%s", image), map[string]string{"step": "pullimage"})
}
var pullipo types.ImagePullOptions
if username != "" && password != "" {
auth, err := EncodeAuthToBase64(types.AuthConfig{Username: username, Password: password})
if err != nil {
logrus.Errorf("make auth base63 push image error: %s", err.Error())
logger.Error(fmt.Sprintf("Failed to generate a Token to get the image"), map[string]string{"step": "builder-exector", "status": "failure"})
return nil, err
}
pullipo = types.ImagePullOptions{
RegistryAuth: auth,
}
} else {
pullipo = types.ImagePullOptions{}
}
rf, err := reference.ParseAnyReference(image)
if err != nil {
logrus.Errorf("reference image error: %s", err.Error())
return nil, err
}
//最少一分钟
if timeout < 1 {
timeout = 1
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(timeout))
defer cancel()
//TODO: 使用1.12版本api的bug “repository name must be canonical”使用rf.String()完整的镜像地址
readcloser, err := dockerCli.ImagePull(ctx, rf.String(), pullipo)
logger.Info(fmt.Sprintf("Success Pull Image%s", image), map[string]string{"step": "pullimage"})
if err != nil {
logrus.Debugf("image name: %s readcloser error: %v", image, err.Error())
if strings.HasSuffix(err.Error(), "does not exist or no pull access") {
if logger != nil {
logger.Error(fmt.Sprintf("image: %s does not exist or is not available", image), map[string]string{"step": "pullimage", "status": "failure"})
}
return nil, fmt.Errorf("Image(%s) does not exist or no pull access", image)
}
return nil, err
}
defer readcloser.Close()
dec := json.NewDecoder(readcloser)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
var jm JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
logrus.Debugf("error decoding jm(JSONMessage): %v", err)
return nil, err
}
if jm.Error != nil {
logrus.Debugf("error pulling image: %v", jm.Error)
return nil, jm.Error
}
if logger != nil {
logger.Debug(fmt.Sprintf(jm.JSONString()), map[string]string{"step": "progress"})
} else {
logrus.Debug(jm.JSONString())
}
}
logger.Debug("Get the image information and its raw representation", map[string]string{"step": "progress"})
ins, _, err := dockerCli.ImageInspectWithRaw(ctx, image)
if err != nil {
logger.Debug("Fail to get the image information and its raw representation", map[string]string{"step": "progress"})
return nil, err
}
return &ins, nil
}
//ImageTag change docker image tag
func ImageTag(dockerCli *client.Client, source, target string, logger event.Logger, timeout int) error {
if logger != nil {
logger.Info(fmt.Sprintf("change image tag%s -> %s", source, target), map[string]string{"step": "changetag"})
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(timeout))
defer cancel()
err := dockerCli.ImageTag(ctx, source, target)
if err != nil {
logrus.Debugf("image tag err: %s", err.Error())
return err
}
logger.Info("change image tag success", map[string]string{"step": "changetag"})
return nil
}
//ImageNameHandle 解析imagename
func ImageNameHandle(imageName string) *model.ImageName {
var i model.ImageName
if strings.Contains(imageName, "/") {
mm := strings.Split(imageName, "/")
i.Host = mm[0]
names := strings.Join(mm[1:], "/")
if strings.Contains(names, ":") {
nn := strings.Split(names, ":")
i.Name = nn[0]
i.Tag = nn[1]
} else {
i.Name = names
i.Tag = "latest"
}
} else {
if strings.Contains(imageName, ":") {
nn := strings.Split(imageName, ":")
i.Name = nn[0]
i.Tag = nn[1]
} else {
i.Name = imageName
i.Tag = "latest"
}
}
return &i
}
//ImageNameWithNamespaceHandle if have namespace,will parse namespace
func ImageNameWithNamespaceHandle(imageName string) *model.ImageName {
var i model.ImageName
if strings.Contains(imageName, "/") {
mm := strings.Split(imageName, "/")
i.Host = mm[0]
names := strings.Join(mm[1:], "/")
if len(mm) >= 3 {
i.Namespace = mm[1]
names = strings.Join(mm[2:], "/")
}
if strings.Contains(names, ":") {
nn := strings.Split(names, ":")
i.Name = nn[0]
i.Tag = nn[1]
} else {
i.Name = names
i.Tag = "latest"
}
} else {
if strings.Contains(imageName, ":") {
nn := strings.Split(imageName, ":")
i.Name = nn[0]
i.Tag = nn[1]
} else {
i.Name = imageName
i.Tag = "latest"
}
}
return &i
}
// GenSaveImageName generates the final name of the image, which is the name of
// the image in the exported tar package.
func GenSaveImageName(name string) string {
imageName := ImageNameWithNamespaceHandle(name)
return fmt.Sprintf("%s/%s:%s", builder.REGISTRYDOMAIN, imageName.Name, imageName.Tag)
}
//ImagePush push image to registry
//timeout minutes of the unit
func ImagePush(dockerCli *client.Client, image, user, pass string, logger event.Logger, timeout int) error {
if logger != nil {
logger.Info(fmt.Sprintf("start push image%s", image), map[string]string{"step": "pushimage"})
}
if timeout < 1 {
timeout = 1
}
if user == "" {
user = os.Getenv("LOCAL_HUB_USER")
}
if pass == "" {
pass = os.Getenv("LOCAL_HUB_PASS")
}
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
}
var opts types.ImagePushOptions
pushauth, err := EncodeAuthToBase64(types.AuthConfig{
Username: user,
Password: pass,
ServerAddress: reference.Domain(ref),
})
if err != nil {
logrus.Errorf("make auth base63 push image error: %s", err.Error())
if logger != nil {
logger.Error(fmt.Sprintf("Failed to generate a token to get the image"), map[string]string{"step": "builder-exector", "status": "failure"})
}
return err
}
opts.RegistryAuth = pushauth
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(timeout))
defer cancel()
readcloser, err := dockerCli.ImagePush(ctx, image, opts)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
if logger != nil {
logger.Error(fmt.Sprintf("image %s does not exist, cannot be pushed", image), map[string]string{"step": "pushimage", "status": "failure"})
}
return fmt.Errorf("Image(%s) does not exist", image)
}
return err
}
logger.Info(fmt.Sprintf("success push image%s", image), map[string]string{"step": "pushimage"})
if readcloser != nil {
defer readcloser.Close()
dec := json.NewDecoder(readcloser)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var jm JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
return err
}
if jm.Error != nil {
return jm.Error
}
logger.Debug(jm.JSONString(), map[string]string{"step": "progress"})
}
}
return nil
}
//TrustedImagePush push image to trusted registry
func TrustedImagePush(dockerCli *client.Client, image, user, pass string, logger event.Logger, timeout int) error {
if err := CheckTrustedRepositories(image, user, pass); err != nil {
return err
}
return ImagePush(dockerCli, image, user, pass, logger, timeout)
}
//CheckTrustedRepositories check Repositories is exist ,if not create it.
func CheckTrustedRepositories(image, user, pass string) error {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
}
var server string
if reference.IsNameOnly(ref) {
server = "docker.io"
} else {
server = reference.Domain(ref)
}
cli, err := createTrustedRegistryClient(server, user, pass)
if err != nil {
return err
}
var namespace, repName string
infos := strings.Split(reference.TrimNamed(ref).String(), "/")
if len(infos) == 3 && infos[0] == server {
namespace = infos[1]
repName = infos[2]
}
if len(infos) == 2 {
namespace = infos[0]
repName = infos[1]
}
_, err = cli.GetRepository(namespace, repName)
if err != nil {
if err.Error() == "resource does not exist" {
rep := Repostory{
Name: repName,
ShortDescription: image, // The maximum length is 140
LongDescription: fmt.Sprintf("push image for %s", image),
Visibility: "private",
}
if len(rep.ShortDescription) > 140 {
rep.ShortDescription = rep.ShortDescription[0:140]
}
err := cli.CreateRepository(namespace, &rep)
if err != nil {
return fmt.Errorf("create repostory error,%s", err.Error())
}
return nil
}
return fmt.Errorf("get repostory error,%s", err.Error())
}
return err
}
// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload
func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf), nil
}
//ImageBuild ImageBuild
func ImageBuild(dockerCli *client.Client, contextDir string, options types.ImageBuildOptions, logger event.Logger, timeout int) error {
var ctx context.Context
if timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), time.Minute*time.Duration(timeout))
defer cancel()
} else {
ctx = context.Background()
}
buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: archive.Uncompressed,
ExcludePatterns: []string{""},
IncludeFiles: []string{"."},
})
if err != nil {
return err
}
// pull image runner
if _, err := ImagePull(dockerCli, builder.RUNNERIMAGENAME, builder.REGISTRYUSER, builder.REGISTRYPASS, logger, timeout); err != nil {
return fmt.Errorf("pull image %s: %v", builder.RUNNERIMAGENAME, err)
}
logrus.Infof("pull image %s successfully.", builder.RUNNERIMAGENAME)
rc, err := dockerCli.ImageBuild(ctx, buildCtx, options)
if err != nil {
return err
}
if rc.Body != nil {
defer rc.Body.Close()
dec := json.NewDecoder(rc.Body)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var jm JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
return err
}
if jm.Error != nil {
return jm.Error
}
logger.Info(jm.JSONString(), map[string]string{"step": "build-progress"})
}
}
return nil
}
//ImageInspectWithRaw get image inspect
func ImageInspectWithRaw(dockerCli *client.Client, image string) (*types.ImageInspect, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ins, _, err := dockerCli.ImageInspectWithRaw(ctx, image)
if err != nil {
return nil, err
}
return &ins, nil
}
//ImageSave save image to tar file
// destination destination file name eg. /tmp/xxx.tar
func ImageSave(dockerCli *client.Client, image, destination string, logger event.Logger) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
rc, err := dockerCli.ImageSave(ctx, []string{image})
if err != nil {
return err
}
defer rc.Close()
return CopyToFile(destination, rc)
}
//ImageLoad load image from tar file
// destination destination file name eg. /tmp/xxx.tar
func ImageLoad(dockerCli *client.Client, tarFile string, logger event.Logger) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reader, err := os.OpenFile(tarFile, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer reader.Close()
rc, err := dockerCli.ImageLoad(ctx, reader, false)
if err != nil {
return err
}
if rc.Body != nil {
defer rc.Body.Close()
dec := json.NewDecoder(rc.Body)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var jm JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
return err
}
if jm.Error != nil {
return jm.Error
}
logger.Info(jm.JSONString(), map[string]string{"step": "build-progress"})
}
}
return nil
}
//ImageImport save image to tar file
// source source file name eg. /tmp/xxx.tar
func ImageImport(dockerCli *client.Client, image, source string, logger event.Logger) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
file, err := os.Open(source)
if err != nil {
return err
}
defer file.Close()
isource := types.ImageImportSource{
Source: file,
SourceName: "-",
}
options := types.ImageImportOptions{}
readcloser, err := dockerCli.ImageImport(ctx, isource, image, options)
if err != nil {
return err
}
if readcloser != nil {
defer readcloser.Close()
r := bufio.NewReader(readcloser)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if line, _, err := r.ReadLine(); err == nil {
if logger != nil {
logger.Debug(string(line), map[string]string{"step": "progress"})
} else {
fmt.Println(string(line))
}
} else {
if err.Error() == "EOF" {
return nil
}
return err
}
}
}
return nil
}
// CopyToFile writes the content of the reader to the specified file
func CopyToFile(outfile string, r io.Reader) error {
// We use sequential file access here to avoid depleting the standby list
// on Windows. On Linux, this is a call directly to ioutil.TempFile
tmpFile, err := os.OpenFile(path.Join(filepath.Dir(outfile), ".docker_temp_"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer tmpFile.Close()
tmpPath := tmpFile.Name()
_, err = io.Copy(tmpFile, r)
if err != nil {
os.Remove(tmpPath)
return err
}
if err = os.Rename(tmpPath, outfile); err != nil {
os.Remove(tmpPath)
return err
}
return nil
}
//ImageRemove remove image
func ImageRemove(dockerCli *client.Client, image string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
_, err := dockerCli.ImageRemove(ctx, image, types.ImageRemoveOptions{Force: true})
return err
}