diff --git a/.gitignore b/.gitignore index a02c16ca82fd1114f73a053dd9ad71ef9b4bf29a..70b979c53c49fdf51e194559a59b73ad8f4d86e6 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,5 @@ SimpleDocker yarn.lock static/ + +SimpleDocker.tar.gz diff --git a/App.go b/App.go index c7ed4fe81227661f106b0ec523124357b57d515b..757ef488669b8136c33061090fb8f216925edb59 100644 --- a/App.go +++ b/App.go @@ -14,7 +14,7 @@ import ( "strconv" ) -var port = flag.Int("port", 4040, "help message for flagname") +var port = flag.Int("port", 4050, "help message for flagname") func stopCommand() { // 读取PID diff --git a/SimpleDocker.tar.gz b/SimpleDocker.tar.gz deleted file mode 100644 index a4c9677b8d732fd0cca0ce12f36689f36a7e04a3..0000000000000000000000000000000000000000 Binary files a/SimpleDocker.tar.gz and /dev/null differ diff --git a/api/ContainerController.go b/api/ContainerController.go index fc1ea0377e2dd9a3ba90aa3a9a3ce7496e7f648f..2f173a50b6764fb646296e72e4540efe9a7ed20a 100644 --- a/api/ContainerController.go +++ b/api/ContainerController.go @@ -3,6 +3,7 @@ package api import ( "SimpleDocker/docker" "SimpleDocker/utils" + "bytes" "fmt" "github.com/astaxie/beego" "github.com/docker/docker/api/types" @@ -175,10 +176,29 @@ func (c *ContainerController) GetContainerAllLog(containerId string) { } // 准备下载文件 - logsByte := []byte(logs) c.Ctx.Output.Header("Content-Type", "application/force-download") c.Ctx.Output.Header("Content-Disposition", fmt.Sprintf("attachment;filename=%s-all.log", containerId)) c.Ctx.Output.Header("Content-Transfer-Encoding", "binary") _, _ = c.Ctx.ResponseWriter.Write(logsByte) } + +// 容器导出 +// @router /api/container/:containerId/export [get] +func (c *ContainerController) ExportContainer(containerId string) { + info, err := docker.ExportContainer(containerId) + if err != nil { + c.Data["json"] = utils.PackageError(err) + c.ServeJSON() + return + } + + // 转换为文件下载 + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(info) + bytesData := buf.Bytes() + c.Ctx.Output.Header("Content-Type", "application/force-download") + c.Ctx.Output.Header("Content-Disposition", fmt.Sprintf("attachment;filename=%s.tar.gz", containerId)) + c.Ctx.Output.Header("Content-Transfer-Encoding", "binary") + _, _ = c.Ctx.ResponseWriter.Write(bytesData) +} diff --git a/api/ImageController.go b/api/ImageController.go index 69f8c2bb5d5023f17a2962c673159a9b5248dc2b..d86878d18d1794e6b73a3c6df6d6ca827f19dc04 100644 --- a/api/ImageController.go +++ b/api/ImageController.go @@ -9,6 +9,7 @@ import ( "github.com/astaxie/beego/logs" "strconv" "strings" + "time" ) type ImageController struct { @@ -109,8 +110,29 @@ func (c *ImageController) PullImage() { } /** 导入Image */ -// @router +// @router /api/image/import func (c *ImageController) ImportImage() { - c.Data["json"] = utils.PackageErrorMsg("暂不支持导入镜像,请期待后续版本") + file, _, err := c.GetFile("file") + if err != nil { + c.Data["json"] = "获取文件失败,请重新上传文件" + c.ServeJSON() + return + } + defer file.Close() + saveFilePath := "/tmp/" + strconv.FormatInt(time.Now().Unix(), 10) + err = c.SaveToFile("file", saveFilePath) + if err != nil { + c.Data["json"] = "保存文件失败,请检查文件是否存在" + c.ServeJSON() + return + } + _, err = docker.ImportImage(saveFilePath) + if err != nil { + c.Data["json"] = utils.PackageError(err) + c.ServeJSON() + return + } + + c.Data["json"] = utils.Success() c.ServeJSON() } diff --git a/api/LoginController.go b/api/LoginController.go new file mode 100644 index 0000000000000000000000000000000000000000..d497098a294cfe04a8a1c9f8c03e801aaf493a7f --- /dev/null +++ b/api/LoginController.go @@ -0,0 +1,41 @@ +package api + +import ( + "SimpleDocker/docker" + "SimpleDocker/utils" + "github.com/astaxie/beego" + "strings" +) + +type LoginController struct { + beego.Controller +} + +// router /api/docker/login +func (c *LoginController) Login() { + + name := c.Ctx.Input.Query("username") + password := c.Ctx.Input.Query("password") + + if name = strings.Trim(name, " "); name == "" { + c.Data["json"] = utils.PackageErrorMsg("登录失败,用户名无效") + c.ServeJSON() + return + } + + if name = strings.Trim(password, " "); password == "" { + c.Data["json"] = utils.PackageErrorMsg("登录失败,密码无效") + c.ServeJSON() + return + } + + _, err := docker.Login(name, password) + if err != nil { + c.Data["json"] = utils.PackageErrorMsg("登录失败,密码无效") + c.ServeJSON() + return + } + + c.Data["json"] = utils.Success() + c.ServeJSON() +} diff --git a/context/Context.go b/context/Context.go index 0d07bc6786e25fa631f92094208269e138713ce2..5e84446a41eead97e9041cd91d9e7be12fa12fdd 100644 --- a/context/Context.go +++ b/context/Context.go @@ -4,16 +4,26 @@ import ( "context" "github.com/astaxie/beego/logs" "github.com/docker/docker/client" + "net" + "net/http" ) var Ctx context.Context var Cli *client.Client +var _ http.Client func init() { var err error Ctx = context.Background() Cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + _ = http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", "/var/run/docker.sock") + }, + }, + } if err != nil { logs.Info("初始化Docker上下文....FAIL!") return diff --git a/docker/auth.go b/docker/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..52fe4d002e560c0435e198e4ab97e3fca31d6cb9 --- /dev/null +++ b/docker/auth.go @@ -0,0 +1,23 @@ +package docker + +import ( + "SimpleDocker/context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/registry" +) + +type UsernamePassword map[string]string + +/** Docker 登录到镜像中心 */ +func Login(username string, password string) (registry.AuthenticateOKBody, error) { + authConfig := types.AuthConfig{Username: username, Password: password} + return context.Cli.RegistryLogin(context.Ctx, authConfig) +} + +/** 获取登录记录密码 */ +func GetLoginRecord() []UsernamePassword { + u := make(map[string]string) + u["user"] = "user" + u["password"] = "password" + return []UsernamePassword{u} +} diff --git a/docker/Container.go b/docker/container.go similarity index 94% rename from docker/Container.go rename to docker/container.go index 518f008d13323f70db3b95ef828872f6b566dfe9..b07245e6e0d6cd695fa7614977c2fd3d58a936e7 100644 --- a/docker/Container.go +++ b/docker/container.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "io" "time" ) @@ -83,6 +84,7 @@ func GetContainerLog(containerId string, tail string) (string, error) { return buf.String(), nil } -func GetContainer() { - +/** 导出容器 */ +func ExportContainer(containerId string) (io.ReadCloser, error) { + return context.Cli.ContainerExport(context.Ctx, containerId) } diff --git a/docker/image.go b/docker/image.go index 61960b409b661d40757d045397786fcd77ed7563..5813999e7a619cdd4ab1813c96f5935194eefd77 100644 --- a/docker/image.go +++ b/docker/image.go @@ -3,7 +3,9 @@ package docker import ( "SimpleDocker/context" "github.com/docker/docker/api/types" + "github.com/pkg/errors" "io" + "os" ) func GetImageList() ([]types.ImageSummary, error) { @@ -44,3 +46,12 @@ func DeleteImage(imageId string, forge bool) error { func SaveImage(imageId string) (io.ReadCloser, error) { return context.Cli.ImageSave(context.Ctx, []string{imageId}) } + +/** 导入镜像 */ +func ImportImage(filePath string) (types.ImageLoadResponse, error) { + open, err := os.OpenFile(filePath, os.O_RDWR, 666) + if err != nil { + return types.ImageLoadResponse{}, errors.New("读取文件失败") + } + return context.Cli.ImageLoad(context.Ctx, open, false) +} diff --git a/go.mod b/go.mod index e56145ad6cfd8a81ad2b42dfbc5edf5cef6414a5..844e992f13ab1c1fdab2b037fcf7d5a45d497c03 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.7.0 // indirect golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 // indirect golang.org/x/text v0.3.3 // indirect diff --git a/routers/commentsRouter_api.go b/routers/commentsRouter_api.go index 1efa78f1e3df209ab41a4cde4a1b03e3958491ef..46b76ad258d5102c369c84c9543787005a669a45 100644 --- a/routers/commentsRouter_api.go +++ b/routers/commentsRouter_api.go @@ -27,6 +27,17 @@ func init() { Filters: nil, Params: nil}) + beego.GlobalControllerRouter["SimpleDocker/api:ContainerController"] = append(beego.GlobalControllerRouter["SimpleDocker/api:ContainerController"], + beego.ControllerComments{ + Method: "ExportContainer", + Router: "/api/container/:containerId/export", + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make( + param.New("containerId", param.InPath), + ), + Filters: nil, + Params: nil}) + beego.GlobalControllerRouter["SimpleDocker/api:ContainerController"] = append(beego.GlobalControllerRouter["SimpleDocker/api:ContainerController"], beego.ControllerComments{ Method: "GetContainerInfo", @@ -183,6 +194,15 @@ func init() { Filters: nil, Params: nil}) + beego.GlobalControllerRouter["SimpleDocker/api:ImageController"] = append(beego.GlobalControllerRouter["SimpleDocker/api:ImageController"], + beego.ControllerComments{ + Method: "ImportImage", + Router: "/api/image/import", + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + beego.GlobalControllerRouter["SimpleDocker/api:ImageController"] = append(beego.GlobalControllerRouter["SimpleDocker/api:ImageController"], beego.ControllerComments{ Method: "PullImage", diff --git a/test/api_test.go b/test/api_test.go new file mode 100644 index 0000000000000000000000000000000000000000..36bde24a34585553ac0bd1d0df414132a473c42c --- /dev/null +++ b/test/api_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "io" + "net" + "net/http" + "os" + "testing" +) + +func TestDockerApi(t *testing.T) { + httpc := http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", "/var/run/docker.sock") + }, + }, + } + + var response *http.Response + var err error + + + response, err = httpc.PostForm("http://unix/images/create?fromImage=python:latest", nil) + + if err != nil { + panic(err) + } + io.Copy(os.Stdout, response.Body) +} diff --git a/ui/src/main.js b/ui/src/main.js index a114420417e2164bbe422408844a8e12d62f8350..fc1fae0ccf6f82c22e7e0229fbeb7f803dcd3ef3 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -9,7 +9,7 @@ import 'ant-design-vue/dist/antd.css'; import axios from 'axios' // axios.defaults.baseURL = 'http://10.0.30.78:8081'; -// axios.defaults.baseURL = 'http://localhost:8081'; +axios.defaults.baseURL = 'http://localhost:4050'; Vue.prototype.$lodash = _ Vue.config.productionTip = false Vue.prototype.$axios = axios diff --git a/ui/src/views/Image.vue b/ui/src/views/Image.vue index 45bc71ce4d2effb93adf8c0d554906c674ef4f16..b8fafda771a8b7906583f9846416a1f755ddf6e5 100644 --- a/ui/src/views/Image.vue +++ b/ui/src/views/Image.vue @@ -20,6 +20,12 @@ 拉取 + + + + 导入 + + @@ -27,16 +33,15 @@ style="margin-top: 30px"> - 详情 + 运行 删除 - 更多 + 更多 - 运行镜像 + 镜像详情 导出镜像 @@ -44,6 +49,9 @@ 重新标记 + + 推送镜像 + @@ -90,6 +98,24 @@ + + + + + + 导入镜像 + + + + + + + + + + + + 授权信息1 + 授权信息2 + 授权信息3 + 授权信息4 + 授权信息5 + +
{{pullLog}}
@@ -136,7 +173,7 @@ import {mapActions} from "vuex"; import imageApi from "../api/ImageApi"; - import {guid} from '../utils/index' + import {guid, download} from '../utils/index' const columns = [ { @@ -185,9 +222,10 @@ env: '', volume: '' }, - pullImageConfig: {imageName: ''}, + pullImageConfig: {imageName: '', auth: ''}, pullLog: '', pulling: false, + importImageVisible: false, // 导入Image的Modal pullImageVisible: false, runImageVisible: false, tagImageVisible: false, @@ -325,7 +363,7 @@ } this.$axios.create(config).get(`/api/image/${imageId}/save`, {responseType: 'blob'}).then( (res) => { - this.download(res.data, `${imageId}.tar.gz`) + download(res.data, `${imageId}.tar.gz`) this.$message.info({content: "镜像已成功导出并下载....", key}); }) .catch(e => { @@ -343,24 +381,6 @@ } else { this.$message.error(Msg); } - }, - download: function (data, fileName) { - if (window.navigator && window.navigator.msSaveOrOpenBlob) { - let blob = new Blob([data], { - type: 'application/vnd.ms-excel' - }) - window.navigator.msSaveOrOpenBlob(blob, fileName) - } else { - let blob = new Blob([data]) - let downloadElement = document.createElement('a') - let href = window.URL.createObjectURL(blob) - downloadElement.href = href - downloadElement.download = fileName - document.body.appendChild(downloadElement) - downloadElement.click() - document.body.removeChild(downloadElement) - window.URL.revokeObjectURL(href) - } }, callReTagApi: function () { if (this.newTag === '' || this.newTag.trim() === '') { this.tagImageVisible = false @@ -370,7 +390,6 @@ }); return } - if (this.newTag.trim() === this.oldTag.trim()) { this.tagImageVisible = false this.$notification['warning']({ @@ -406,6 +425,8 @@ description: "镜像标记失败,请检查 Docker 服务是否正常" }); }) + }, callImportImageApi() { + } } }