2018-03-14 14:12:26 +08:00
|
|
|
|
// Copyright (C) 2014-2018 Goodrain Co., Ltd.
|
2018-01-17 22:06:58 +08:00
|
|
|
|
// RAINBOND, Application Management Platform
|
2018-03-14 14:33:31 +08:00
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
// 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.
|
2018-03-14 14:33:31 +08:00
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
// 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.
|
2018-03-14 14:33:31 +08:00
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
// 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 parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"path"
|
|
|
|
|
"strconv"
|
2018-03-02 19:50:20 +08:00
|
|
|
|
"strings"
|
2018-01-17 22:06:58 +08:00
|
|
|
|
|
2018-02-27 22:31:44 +08:00
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
"github.com/pquerna/ffjson/ffjson"
|
|
|
|
|
|
|
|
|
|
"github.com/goodrain/rainbond/pkg/builder/parser/code"
|
|
|
|
|
"github.com/goodrain/rainbond/pkg/builder/sources"
|
|
|
|
|
"github.com/goodrain/rainbond/pkg/db/model"
|
|
|
|
|
"github.com/goodrain/rainbond/pkg/event"
|
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing/transport"
|
|
|
|
|
|
2018-02-07 11:28:31 +08:00
|
|
|
|
//"github.com/docker/docker/client"
|
|
|
|
|
"github.com/docker/engine-api/client"
|
2018-01-17 22:06:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//SourceCodeParse docker run 命令解析或直接镜像名解析
|
|
|
|
|
type SourceCodeParse struct {
|
|
|
|
|
ports map[int]*Port
|
|
|
|
|
volumes map[string]*Volume
|
|
|
|
|
envs map[string]*Env
|
|
|
|
|
source string
|
|
|
|
|
memory int
|
|
|
|
|
image Image
|
|
|
|
|
args []string
|
|
|
|
|
branchs []string
|
|
|
|
|
errors []ParseError
|
|
|
|
|
dockerclient *client.Client
|
|
|
|
|
logger event.Logger
|
2018-01-24 17:20:06 +08:00
|
|
|
|
Lang code.Lang
|
2018-02-23 17:21:14 +08:00
|
|
|
|
Runtime bool `json:"runtime"`
|
|
|
|
|
Library bool `json:"library"`
|
|
|
|
|
Procfile bool `json:"procfile"`
|
2018-01-17 22:06:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//CreateSourceCodeParse create parser
|
|
|
|
|
func CreateSourceCodeParse(source string, logger event.Logger) Parser {
|
|
|
|
|
return &SourceCodeParse{
|
|
|
|
|
source: source,
|
|
|
|
|
ports: make(map[int]*Port),
|
|
|
|
|
volumes: make(map[string]*Volume),
|
|
|
|
|
envs: make(map[string]*Env),
|
|
|
|
|
logger: logger,
|
|
|
|
|
image: parseImageName("goodrain.me/runner"),
|
|
|
|
|
args: []string{"start", "web"},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Parse 获取代码 解析代码 检验代码
|
|
|
|
|
func (d *SourceCodeParse) Parse() ParseErrorList {
|
|
|
|
|
if d.source == "" {
|
|
|
|
|
d.logger.Error("源码检查输入参数错误", map[string]string{"step": "parse"})
|
|
|
|
|
d.errappend(Errorf(FatalError, "source can not be empty"))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
var csi sources.CodeSourceInfo
|
|
|
|
|
err := ffjson.Unmarshal([]byte(d.source), &csi)
|
|
|
|
|
if err != nil {
|
|
|
|
|
d.logger.Error("源码检查输入参数错误", map[string]string{"step": "parse"})
|
|
|
|
|
d.errappend(Errorf(FatalError, "source data can not be read"))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
if csi.Branch == "" {
|
|
|
|
|
csi.Branch = "master"
|
|
|
|
|
}
|
|
|
|
|
if csi.ServerType == "" {
|
|
|
|
|
csi.ServerType = "git"
|
|
|
|
|
}
|
2018-02-27 22:31:44 +08:00
|
|
|
|
if csi.RepositoryURL == "" {
|
|
|
|
|
d.logger.Error("Git项目仓库地址不能为空", map[string]string{"step": "parse"})
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "Git项目仓库地址格式错误", SolveAdvice("modify_url", "请确认并修改仓库地址")))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-03-02 10:11:57 +08:00
|
|
|
|
//验证仓库地址
|
2018-03-27 16:16:03 +08:00
|
|
|
|
buildInfo, err := sources.CreateRepostoryBuildInfo(csi.RepositoryURL, csi.Branch, csi.TenantID)
|
2018-03-02 10:11:57 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
d.logger.Error("Git项目仓库地址格式错误", map[string]string{"step": "parse"})
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "Git项目仓库地址格式错误", SolveAdvice("modify_url", "请确认并修改仓库地址")))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-01-24 17:20:06 +08:00
|
|
|
|
gitFunc := func() ParseErrorList {
|
2018-01-22 15:20:49 +08:00
|
|
|
|
//获取代码
|
2018-03-02 10:11:57 +08:00
|
|
|
|
if sources.CheckFileExist(buildInfo.GetCodeHome()) {
|
|
|
|
|
if err := sources.RemoveDir(buildInfo.GetCodeHome()); err != nil {
|
2018-02-06 14:06:23 +08:00
|
|
|
|
//d.errappend(ErrorAndSolve(err, "清理cache dir错误", "请提交代码到仓库"))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-02 10:11:57 +08:00
|
|
|
|
csi.RepositoryURL = buildInfo.RepostoryURL
|
|
|
|
|
rs, err := sources.GitClone(csi, buildInfo.GetCodeHome(), d.logger, 5)
|
2018-01-22 15:20:49 +08:00
|
|
|
|
if err != nil {
|
2018-03-02 10:11:57 +08:00
|
|
|
|
if err == transport.ErrAuthenticationRequired || err == transport.ErrAuthorizationFailed {
|
|
|
|
|
if buildInfo.GetProtocol() == "ssh" {
|
2018-02-23 17:21:14 +08:00
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "Git项目仓库需要安全验证", SolveAdvice("get_publickey", "请获取授权Key配置到你的仓库项目中")))
|
2018-01-22 15:20:49 +08:00
|
|
|
|
} else {
|
2018-02-23 17:21:14 +08:00
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "Git项目仓库需要安全验证", SolveAdvice("modify_userpass", "请提供正确的账号密码")))
|
2018-01-22 15:20:49 +08:00
|
|
|
|
}
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
if err == plumbing.ErrReferenceNotFound {
|
|
|
|
|
solve := "请到代码仓库查看正确的分支情况"
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("Git项目仓库指定分支 %s 不存在", csi.Branch), solve))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-03-02 10:11:57 +08:00
|
|
|
|
if err == transport.ErrRepositoryNotFound {
|
|
|
|
|
solve := SolveAdvice("modify_repo", "请确认仓库地址是否正确")
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("Git项目仓库不存在"), solve))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
if err == transport.ErrEmptyRemoteRepository {
|
|
|
|
|
solve := SolveAdvice("open_repo", "请确认已提交代码")
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("Git项目仓库无有效文件"), solve))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-03-02 19:50:20 +08:00
|
|
|
|
if strings.Contains(err.Error(), "ssh: unable to authenticate") {
|
|
|
|
|
solve := SolveAdvice("get_publickey", "请获取授权Key配置到你的仓库项目试试?")
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("远程仓库SSH验证错误"), solve))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-03-06 11:53:27 +08:00
|
|
|
|
if strings.Contains(err.Error(), "context deadline exceeded") {
|
|
|
|
|
solve := "请确认源码仓库能否正常访问"
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("获取代码超时"), solve))
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-02-27 22:31:44 +08:00
|
|
|
|
logrus.Errorf("git clone error,%s", err.Error())
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, fmt.Sprintf("获取代码失败"), "请确认仓库能否正常访问,或联系客服咨询"))
|
2018-01-22 15:20:49 +08:00
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
//获取分支
|
|
|
|
|
branch, err := rs.Branches()
|
|
|
|
|
if err == nil {
|
|
|
|
|
branch.ForEach(func(re *plumbing.Reference) error {
|
|
|
|
|
name := re.Name()
|
|
|
|
|
if name.IsBranch() {
|
|
|
|
|
d.branchs = append(d.branchs, name.Short())
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
d.branchs = append(d.branchs, csi.Branch)
|
|
|
|
|
}
|
2018-01-24 17:20:06 +08:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//获取代码仓库
|
|
|
|
|
switch csi.ServerType {
|
|
|
|
|
case "git":
|
2018-02-27 22:31:44 +08:00
|
|
|
|
if err := gitFunc(); err != nil && err.IsFatalError() {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2018-01-17 22:06:58 +08:00
|
|
|
|
case "svn":
|
2018-02-27 22:31:44 +08:00
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "svn协议暂时不支持", "请使用git协议仓库"))
|
|
|
|
|
return d.errors
|
2018-02-23 17:21:14 +08:00
|
|
|
|
default:
|
2018-01-22 15:20:49 +08:00
|
|
|
|
//按照git处理处理
|
2018-02-27 22:31:44 +08:00
|
|
|
|
if err := gitFunc(); err != nil && err.IsFatalError() {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2018-01-17 22:06:58 +08:00
|
|
|
|
}
|
2018-01-24 17:20:06 +08:00
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
//读取云帮配置文件
|
2018-03-02 10:11:57 +08:00
|
|
|
|
rbdfileConfig, err := code.ReadRainbondFile(buildInfo.GetCodeHome())
|
2018-01-17 22:06:58 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
if err == code.ErrRainbondFileNotFound {
|
|
|
|
|
d.errappend(ErrorAndSolve(NegligibleError, "rainbondfile未定义", "可以参考文档说明配置此文件定义应用属性"))
|
2018-02-27 22:01:37 +08:00
|
|
|
|
} else {
|
|
|
|
|
d.errappend(ErrorAndSolve(NegligibleError, "rainbondfile定义格式有误", "可以参考文档说明配置此文件定义应用属性"))
|
2018-01-17 22:06:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//判断对象目录
|
2018-03-02 10:11:57 +08:00
|
|
|
|
var buildPath = buildInfo.GetCodeBuildAbsPath()
|
2018-01-17 22:06:58 +08:00
|
|
|
|
//解析代码类型
|
|
|
|
|
var lang code.Lang
|
|
|
|
|
if rbdfileConfig != nil && rbdfileConfig.Language != "" {
|
|
|
|
|
lang = code.Lang(rbdfileConfig.Language)
|
|
|
|
|
} else {
|
|
|
|
|
lang, err = code.GetLangType(buildPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == code.ErrCodeDirNotExist {
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "源码目录不存在", "获取代码任务失败,请联系客服"))
|
|
|
|
|
} else if err == code.ErrCodeNotExist {
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "仓库中代码不存在", "请提交代码到仓库"))
|
|
|
|
|
} else {
|
2018-02-27 22:31:44 +08:00
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "代码无法识别语言类型", "请参考文档查看平台语言支持规范"))
|
2018-01-17 22:06:58 +08:00
|
|
|
|
}
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-24 17:20:06 +08:00
|
|
|
|
d.Lang = lang
|
2018-01-17 22:06:58 +08:00
|
|
|
|
if lang == code.NO {
|
2018-02-23 17:21:14 +08:00
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, "代码无法识别语言类型", "请参考文档查看平台语言支持规范"))
|
2018-01-17 22:06:58 +08:00
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-02-23 17:21:14 +08:00
|
|
|
|
//判断代码<E4BBA3><E7A081><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>范
|
2018-01-17 22:06:58 +08:00
|
|
|
|
spec := code.CheckCodeSpecification(buildPath, lang)
|
|
|
|
|
if spec.Advice != nil {
|
|
|
|
|
for k, v := range spec.Advice {
|
|
|
|
|
d.errappend(ErrorAndSolve(NegligibleError, k, v))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if spec.Noconform != nil {
|
|
|
|
|
for k, v := range spec.Noconform {
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, k, v))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !spec.Conform {
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
2018-02-07 09:58:55 +08:00
|
|
|
|
d.Library = true
|
2018-01-17 22:06:58 +08:00
|
|
|
|
//如果是dockerfile 解析dockerfile文件
|
|
|
|
|
if lang == code.Dockerfile {
|
|
|
|
|
if ok := d.parseDockerfileInfo(path.Join(buildPath, "Dockerfile")); !ok {
|
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-07 09:58:55 +08:00
|
|
|
|
d.Runtime = code.CheckRuntime(buildPath, lang)
|
2018-03-07 12:12:00 +08:00
|
|
|
|
d.memory = getRecommendedMemory(lang)
|
2018-02-07 09:58:55 +08:00
|
|
|
|
d.Procfile = true
|
2018-01-17 22:06:58 +08:00
|
|
|
|
return d.errors
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-07 12:12:00 +08:00
|
|
|
|
func getRecommendedMemory(lang code.Lang) int {
|
|
|
|
|
//java语言推荐1024
|
|
|
|
|
if lang == code.JavaJar || lang == code.JavaMaven || lang == code.JaveWar {
|
|
|
|
|
return 1024
|
|
|
|
|
}
|
|
|
|
|
if lang == code.Python {
|
|
|
|
|
return 512
|
|
|
|
|
}
|
|
|
|
|
if lang == code.Nodejs {
|
|
|
|
|
return 512
|
|
|
|
|
}
|
|
|
|
|
return 128
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
func (d *SourceCodeParse) errappend(pe ParseError) {
|
|
|
|
|
d.errors = append(d.errors, pe)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetBranchs 获取分支列表
|
|
|
|
|
func (d *SourceCodeParse) GetBranchs() []string {
|
|
|
|
|
return d.branchs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetPorts 获取端口列表
|
|
|
|
|
func (d *SourceCodeParse) GetPorts() (ports []Port) {
|
|
|
|
|
for _, cv := range d.ports {
|
|
|
|
|
ports = append(ports, *cv)
|
|
|
|
|
}
|
|
|
|
|
return ports
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetVolumes 获取存储列表
|
|
|
|
|
func (d *SourceCodeParse) GetVolumes() (volumes []Volume) {
|
|
|
|
|
for _, cv := range d.volumes {
|
|
|
|
|
volumes = append(volumes, *cv)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetValid 获取源是否合法
|
|
|
|
|
func (d *SourceCodeParse) GetValid() bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetEnvs 环境变量
|
|
|
|
|
func (d *SourceCodeParse) GetEnvs() (envs []Env) {
|
|
|
|
|
for _, cv := range d.envs {
|
|
|
|
|
envs = append(envs, *cv)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetImage 获取镜像
|
|
|
|
|
func (d *SourceCodeParse) GetImage() Image {
|
|
|
|
|
return d.image
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetArgs 启动参数
|
|
|
|
|
func (d *SourceCodeParse) GetArgs() []string {
|
|
|
|
|
return d.args
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//GetMemory 获取内存
|
|
|
|
|
func (d *SourceCodeParse) GetMemory() int {
|
|
|
|
|
return d.memory
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-24 17:20:06 +08:00
|
|
|
|
//GetLang 获取识别语言
|
|
|
|
|
func (d *SourceCodeParse) GetLang() code.Lang {
|
|
|
|
|
return d.Lang
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 09:58:55 +08:00
|
|
|
|
//GetRuntime GetRuntime
|
2018-02-23 17:21:14 +08:00
|
|
|
|
func (d *SourceCodeParse) GetRuntime() bool {
|
2018-02-07 09:58:55 +08:00
|
|
|
|
return d.Runtime
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-17 22:06:58 +08:00
|
|
|
|
//GetServiceInfo 获取service info
|
|
|
|
|
func (d *SourceCodeParse) GetServiceInfo() []ServiceInfo {
|
|
|
|
|
serviceInfo := ServiceInfo{
|
2018-02-23 17:21:14 +08:00
|
|
|
|
Ports: d.GetPorts(),
|
|
|
|
|
Envs: d.GetEnvs(),
|
|
|
|
|
Volumes: d.GetVolumes(),
|
|
|
|
|
Image: d.GetImage(),
|
|
|
|
|
Args: d.GetArgs(),
|
|
|
|
|
Branchs: d.GetBranchs(),
|
|
|
|
|
Memory: d.memory,
|
|
|
|
|
Lang: d.GetLang(),
|
|
|
|
|
Library: true,
|
2018-02-07 09:58:55 +08:00
|
|
|
|
Procfile: true,
|
2018-02-23 17:21:14 +08:00
|
|
|
|
Runtime: true,
|
2018-01-17 22:06:58 +08:00
|
|
|
|
}
|
|
|
|
|
return []ServiceInfo{serviceInfo}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *SourceCodeParse) parseDockerfileInfo(dockerfile string) bool {
|
|
|
|
|
commands, err := sources.ParseFile(dockerfile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
d.errappend(ErrorAndSolve(FatalError, err.Error(), "请确认Dockerfile格式是否符合规范"))
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, cm := range commands {
|
|
|
|
|
switch cm.Cmd {
|
|
|
|
|
case "env":
|
|
|
|
|
if len(cm.Value) == 2 {
|
|
|
|
|
d.envs[cm.Value[0]] = &Env{Name: cm.Value[0], Value: cm.Value[1]}
|
|
|
|
|
}
|
|
|
|
|
case "expose":
|
2018-03-07 15:11:20 +08:00
|
|
|
|
for _, v := range cm.Value {
|
|
|
|
|
port, _ := strconv.Atoi(v)
|
2018-01-17 22:06:58 +08:00
|
|
|
|
if port != 0 {
|
|
|
|
|
d.ports[port] = &Port{ContainerPort: port, Protocol: GetPortProtocol(port)}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "volume":
|
|
|
|
|
for _, v := range cm.Value {
|
|
|
|
|
d.volumes[v] = &Volume{VolumePath: v, VolumeType: model.ShareFileVolumeType.String()}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-20 18:34:56 +08:00
|
|
|
|
// dockerfile empty args
|
|
|
|
|
d.args = []string{}
|
2018-01-17 22:06:58 +08:00
|
|
|
|
return true
|
|
|
|
|
}
|