Compare commits
2 Commits
cc176b4d29
...
ac9ac4cbc7
| Author | SHA1 | Date | |
|---|---|---|---|
| ac9ac4cbc7 | |||
| 8855bb77fb |
@@ -11,6 +11,7 @@ import "strings"
|
|||||||
import "codit/internal/auth"
|
import "codit/internal/auth"
|
||||||
import "codit/internal/config"
|
import "codit/internal/config"
|
||||||
import "codit/internal/db"
|
import "codit/internal/db"
|
||||||
|
import "codit/internal/docker"
|
||||||
import "codit/internal/git"
|
import "codit/internal/git"
|
||||||
import "codit/internal/handlers"
|
import "codit/internal/handlers"
|
||||||
import httpx "codit/internal/http"
|
import httpx "codit/internal/http"
|
||||||
@@ -246,7 +247,9 @@ func main() {
|
|||||||
var repoManager git.RepoManager
|
var repoManager git.RepoManager
|
||||||
repoManager = git.RepoManager{BaseDir: filepath.Join(cfg.DataDir, "git")}
|
repoManager = git.RepoManager{BaseDir: filepath.Join(cfg.DataDir, "git")}
|
||||||
var rpmBase string
|
var rpmBase string
|
||||||
|
var dockerBase string
|
||||||
rpmBase = filepath.Join(cfg.DataDir, "rpm")
|
rpmBase = filepath.Join(cfg.DataDir, "rpm")
|
||||||
|
dockerBase = filepath.Join(cfg.DataDir, "docker")
|
||||||
var rpmMeta *rpm.MetaManager
|
var rpmMeta *rpm.MetaManager
|
||||||
rpmMeta = rpm.NewMetaManager()
|
rpmMeta = rpm.NewMetaManager()
|
||||||
var uploadStore storage.FileStore
|
var uploadStore storage.FileStore
|
||||||
@@ -255,6 +258,10 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("rpm dir error: %v", err)
|
log.Fatalf("rpm dir error: %v", err)
|
||||||
}
|
}
|
||||||
|
err = os.MkdirAll(dockerBase, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("docker dir error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var api *handlers.API
|
var api *handlers.API
|
||||||
api = &handlers.API{
|
api = &handlers.API{
|
||||||
@@ -263,6 +270,7 @@ func main() {
|
|||||||
Repos: repoManager,
|
Repos: repoManager,
|
||||||
RpmBase: rpmBase,
|
RpmBase: rpmBase,
|
||||||
RpmMeta: rpmMeta,
|
RpmMeta: rpmMeta,
|
||||||
|
DockerBase: dockerBase,
|
||||||
Uploads: uploadStore,
|
Uploads: uploadStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +302,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
var rpmServer http.Handler
|
var rpmServer http.Handler
|
||||||
rpmServer = rpm.NewHTTPServer(rpmBase, authFunc, logger)
|
rpmServer = rpm.NewHTTPServer(rpmBase, authFunc, logger)
|
||||||
|
var dockerServer http.Handler
|
||||||
|
dockerServer = docker.NewHTTPServer(store, dockerBase, authFunc, logger)
|
||||||
|
|
||||||
var router *httpx.Router
|
var router *httpx.Router
|
||||||
router = httpx.NewRouter()
|
router = httpx.NewRouter()
|
||||||
@@ -353,6 +363,11 @@ func main() {
|
|||||||
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
||||||
router.Handle("GET", "/api/repos/:id/rpm/tree", api.RepoRPMTree)
|
router.Handle("GET", "/api/repos/:id/rpm/tree", api.RepoRPMTree)
|
||||||
router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
|
router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
|
||||||
|
router.Handle("GET", "/api/repos/:id/docker/images", api.RepoDockerImages)
|
||||||
|
router.Handle("GET", "/api/repos/:id/docker/tags", api.RepoDockerTags)
|
||||||
|
router.Handle("GET", "/api/repos/:id/docker/manifest", api.RepoDockerManifest)
|
||||||
|
router.Handle("DELETE", "/api/repos/:id/docker/tag", api.RepoDockerDeleteTag)
|
||||||
|
router.Handle("DELETE", "/api/repos/:id/docker/image", api.RepoDockerDeleteImage)
|
||||||
|
|
||||||
router.Handle("GET", "/api/projects/:projectId/issues", api.ListIssues)
|
router.Handle("GET", "/api/projects/:projectId/issues", api.ListIssues)
|
||||||
router.Handle("POST", "/api/projects/:projectId/issues", api.CreateIssue)
|
router.Handle("POST", "/api/projects/:projectId/issues", api.CreateIssue)
|
||||||
@@ -377,6 +392,8 @@ func main() {
|
|||||||
mux.Handle(cfg.RPMHTTPPrefix+"/", http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite))
|
mux.Handle(cfg.RPMHTTPPrefix+"/", http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite))
|
||||||
rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store}
|
rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store}
|
||||||
mux.Handle(cfg.RPMHTTPPrefix+"-id/", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite))
|
mux.Handle(cfg.RPMHTTPPrefix+"-id/", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite))
|
||||||
|
mux.Handle("/v2", dockerServer)
|
||||||
|
mux.Handle("/v2/", dockerServer)
|
||||||
mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler)))
|
mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler)))
|
||||||
mux.Handle("/api/", middleware.WithUser(store, middleware.AccessLog(logger, middleware.RequireAuth(router))))
|
mux.Handle("/api/", middleware.WithUser(store, middleware.AccessLog(logger, middleware.RequireAuth(router))))
|
||||||
mux.Handle("/api/login", middleware.WithUser(store, middleware.AccessLog(logger, router)))
|
mux.Handle("/api/login", middleware.WithUser(store, middleware.AccessLog(logger, router)))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import "unicode"
|
|||||||
import "codit/internal/auth"
|
import "codit/internal/auth"
|
||||||
import "codit/internal/config"
|
import "codit/internal/config"
|
||||||
import "codit/internal/db"
|
import "codit/internal/db"
|
||||||
|
import "codit/internal/docker"
|
||||||
import "codit/internal/git"
|
import "codit/internal/git"
|
||||||
import "codit/internal/middleware"
|
import "codit/internal/middleware"
|
||||||
import "codit/internal/models"
|
import "codit/internal/models"
|
||||||
@@ -27,6 +28,7 @@ type API struct {
|
|||||||
Repos git.RepoManager
|
Repos git.RepoManager
|
||||||
RpmBase string
|
RpmBase string
|
||||||
RpmMeta *rpm.MetaManager
|
RpmMeta *rpm.MetaManager
|
||||||
|
DockerBase string
|
||||||
Uploads storage.FileStore
|
Uploads storage.FileStore
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +68,14 @@ type repoResponse struct {
|
|||||||
models.Repo
|
models.Repo
|
||||||
CloneURL string `json:"clone_url"`
|
CloneURL string `json:"clone_url"`
|
||||||
RPMURL string `json:"rpm_url"`
|
RPMURL string `json:"rpm_url"`
|
||||||
|
DockerURL string `json:"docker_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type repoListItem struct {
|
type repoListItem struct {
|
||||||
models.Repo
|
models.Repo
|
||||||
CloneURL string `json:"clone_url"`
|
CloneURL string `json:"clone_url"`
|
||||||
RPMURL string `json:"rpm_url"`
|
RPMURL string `json:"rpm_url"`
|
||||||
|
DockerURL string `json:"docker_url"`
|
||||||
OwnerProject string `json:"owner_project"`
|
OwnerProject string `json:"owner_project"`
|
||||||
OwnerSlug string `json:"owner_slug"`
|
OwnerSlug string `json:"owner_slug"`
|
||||||
}
|
}
|
||||||
@@ -646,16 +650,21 @@ func (api *API) ListRepos(w http.ResponseWriter, r *http.Request, params map[str
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
if repo.Type == "rpm" {
|
if repo.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
||||||
|
}
|
||||||
resp = append(resp, repoListItem{
|
resp = append(resp, repoListItem{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
CloneURL: cloneURL,
|
CloneURL: cloneURL,
|
||||||
RPMURL: rpmURL,
|
RPMURL: rpmURL,
|
||||||
|
DockerURL: dockerURL,
|
||||||
OwnerProject: owner.ID,
|
OwnerProject: owner.ID,
|
||||||
OwnerSlug: owner.Slug,
|
OwnerSlug: owner.Slug,
|
||||||
})
|
})
|
||||||
@@ -728,16 +737,21 @@ func (api *API) ListAllRepos(w http.ResponseWriter, r *http.Request, _ map[strin
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
if repo.Type == "rpm" {
|
if repo.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
||||||
|
}
|
||||||
resp = append(resp, repoListItem{
|
resp = append(resp, repoListItem{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
CloneURL: cloneURL,
|
CloneURL: cloneURL,
|
||||||
RPMURL: rpmURL,
|
RPMURL: rpmURL,
|
||||||
|
DockerURL: dockerURL,
|
||||||
OwnerProject: owner.ID,
|
OwnerProject: owner.ID,
|
||||||
OwnerSlug: owner.Slug,
|
OwnerSlug: owner.Slug,
|
||||||
})
|
})
|
||||||
@@ -764,13 +778,17 @@ func (api *API) GetRepo(w http.ResponseWriter, r *http.Request, params map[strin
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
if repo.Type == "rpm" {
|
if repo.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
@@ -833,13 +851,20 @@ func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
|||||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else if repoType == "rpm" {
|
||||||
repoPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
repoPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
||||||
err = os.MkdirAll(repoPath, 0o755)
|
err = os.MkdirAll(repoPath, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else if repoType == "docker" {
|
||||||
|
repoPath = filepath.Join(api.DockerBase, project.ID, req.Name)
|
||||||
|
err = os.MkdirAll(repoPath, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repo = models.Repo{
|
repo = models.Repo{
|
||||||
ID: repoID,
|
ID: repoID,
|
||||||
@@ -857,13 +882,17 @@ func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if created.Type == "git" {
|
if created.Type == "git" {
|
||||||
cloneURL = api.cloneURL(project.Slug, created.Name)
|
cloneURL = api.cloneURL(project.Slug, created.Name)
|
||||||
}
|
}
|
||||||
if created.Type == "rpm" {
|
if created.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(project.Slug, created.Name)
|
rpmURL = api.rpmURL(project.Slug, created.Name)
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL})
|
if created.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(project.Slug, created.Name)
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) ListAvailableRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
func (api *API) ListAvailableRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
@@ -963,9 +992,23 @@ func (api *API) AttachForeignRepo(w http.ResponseWriter, r *http.Request, params
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
repo.IsForeign = true
|
repo.IsForeign = true
|
||||||
|
var cloneURL string
|
||||||
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
|
if repo.Type == "git" {
|
||||||
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
|
if repo.Type == "rpm" {
|
||||||
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
WriteJSON(w, http.StatusCreated, repoListItem{
|
WriteJSON(w, http.StatusCreated, repoListItem{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
CloneURL: api.cloneURL(project.Slug, repo.Name),
|
CloneURL: cloneURL,
|
||||||
|
RPMURL: rpmURL,
|
||||||
|
DockerURL: dockerURL,
|
||||||
OwnerProject: project.ID,
|
OwnerProject: project.ID,
|
||||||
OwnerSlug: project.Slug,
|
OwnerSlug: project.Slug,
|
||||||
})
|
})
|
||||||
@@ -1031,8 +1074,10 @@ func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
|||||||
}
|
}
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
newPath = api.Repos.RepoPathFor(project.ID, req.Name)
|
newPath = api.Repos.RepoPathFor(project.ID, req.Name)
|
||||||
} else {
|
} else if repo.Type == "rpm" {
|
||||||
newPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
newPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
||||||
|
} else if repo.Type == "docker" {
|
||||||
|
newPath = filepath.Join(api.DockerBase, project.ID, req.Name)
|
||||||
}
|
}
|
||||||
if repo.Path != newPath {
|
if repo.Path != newPath {
|
||||||
oldPath = repo.Path
|
oldPath = repo.Path
|
||||||
@@ -1069,13 +1114,17 @@ func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
if repo.Type == "rpm" {
|
if repo.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
@@ -1116,13 +1165,17 @@ func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[st
|
|||||||
}
|
}
|
||||||
var cloneURL string
|
var cloneURL string
|
||||||
var rpmURL string
|
var rpmURL string
|
||||||
|
var dockerURL string
|
||||||
if repo.Type == "git" {
|
if repo.Type == "git" {
|
||||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
if repo.Type == "rpm" {
|
if repo.Type == "rpm" {
|
||||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
if repo.Type == "docker" {
|
||||||
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveToTrash(path string) (string, error) {
|
func moveToTrash(path string) (string, error) {
|
||||||
@@ -1774,6 +1827,7 @@ func (api *API) RepoTypes(w http.ResponseWriter, r *http.Request, _ map[string]s
|
|||||||
types = []repoTypeItem{
|
types = []repoTypeItem{
|
||||||
{Value: "git", Label: "Git"},
|
{Value: "git", Label: "Git"},
|
||||||
{Value: "rpm", Label: "RPM"},
|
{Value: "rpm", Label: "RPM"},
|
||||||
|
{Value: "docker", Label: "Docker"},
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusOK, types)
|
WriteJSON(w, http.StatusOK, types)
|
||||||
}
|
}
|
||||||
@@ -2357,6 +2411,154 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
|||||||
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) RepoDockerTags(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var repo models.Repo
|
||||||
|
var err error
|
||||||
|
var tags []docker.TagInfo
|
||||||
|
var image string
|
||||||
|
var imagePath string
|
||||||
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if repo.Type != "docker" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||||
|
imagePath = docker.ImagePath(repo.Path, image)
|
||||||
|
tags, err = docker.ListTags(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) RepoDockerManifest(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var repo models.Repo
|
||||||
|
var err error
|
||||||
|
var ref string
|
||||||
|
var detail docker.ManifestDetail
|
||||||
|
var image string
|
||||||
|
var imagePath string
|
||||||
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if repo.Type != "docker" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ref = strings.TrimSpace(r.URL.Query().Get("ref"))
|
||||||
|
if ref == "" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "ref required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||||
|
imagePath = docker.ImagePath(repo.Path, image)
|
||||||
|
detail, err = docker.GetManifestDetail(imagePath, ref)
|
||||||
|
if err != nil {
|
||||||
|
if err == docker.ErrNotFound {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "manifest not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) RepoDockerImages(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var repo models.Repo
|
||||||
|
var err error
|
||||||
|
var images []string
|
||||||
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if repo.Type != "docker" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
images, err = docker.ListImages(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, images)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) RepoDockerDeleteTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var repo models.Repo
|
||||||
|
var err error
|
||||||
|
var image string
|
||||||
|
var tag string
|
||||||
|
var imagePath string
|
||||||
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if repo.Type != "docker" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||||
|
tag = strings.TrimSpace(r.URL.Query().Get("tag"))
|
||||||
|
if tag == "" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tag required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imagePath = docker.ImagePath(repo.Path, image)
|
||||||
|
err = docker.DeleteTag(imagePath, tag)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) RepoDockerDeleteImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var repo models.Repo
|
||||||
|
var err error
|
||||||
|
var image string
|
||||||
|
repo, err = api.Store.GetRepo(params["id"])
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if repo.Type != "docker" {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||||
|
err = docker.DeleteImage(repo.Path, image)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) ListIssues(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
func (api *API) ListIssues(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
var issues []models.Issue
|
var issues []models.Issue
|
||||||
var err error
|
var err error
|
||||||
@@ -2737,7 +2939,7 @@ func normalizeRepoType(value string) (string, bool) {
|
|||||||
if v == "" {
|
if v == "" {
|
||||||
return "git", true
|
return "git", true
|
||||||
}
|
}
|
||||||
if v == "git" || v == "rpm" {
|
if v == "git" || v == "rpm" || v == "docker" {
|
||||||
return v, true
|
return v, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
@@ -2916,3 +3118,9 @@ func (api *API) rpmURL(projectSlug, repoName string) string {
|
|||||||
}
|
}
|
||||||
return base + prefix + "/" + projectSlug + "/" + repoName
|
return base + prefix + "/" + projectSlug + "/" + repoName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) dockerURL(projectSlug, repoName string) string {
|
||||||
|
var base string
|
||||||
|
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
|
||||||
|
return base + "/v2/" + projectSlug + "/" + repoName
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ export interface Repo {
|
|||||||
id: string
|
id: string
|
||||||
project_id: string
|
project_id: string
|
||||||
name: string
|
name: string
|
||||||
type?: 'git' | 'rpm'
|
type?: 'git' | 'rpm' | 'docker'
|
||||||
clone_url?: string
|
clone_url?: string
|
||||||
rpm_url?: string
|
rpm_url?: string
|
||||||
|
docker_url?: string
|
||||||
is_foreign?: boolean
|
is_foreign?: boolean
|
||||||
owner_project?: string
|
owner_project?: string
|
||||||
owner_slug?: string
|
owner_slug?: string
|
||||||
@@ -64,6 +65,30 @@ export interface RpmPackageDetail extends RpmPackageSummary {
|
|||||||
provides: string[]
|
provides: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DockerTagInfo {
|
||||||
|
tag: string
|
||||||
|
digest: string
|
||||||
|
size: number
|
||||||
|
media_type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockerManifestDetail {
|
||||||
|
reference: string
|
||||||
|
digest: string
|
||||||
|
media_type: string
|
||||||
|
size: number
|
||||||
|
config: {
|
||||||
|
created?: string
|
||||||
|
architecture?: string
|
||||||
|
os?: string
|
||||||
|
}
|
||||||
|
layers: {
|
||||||
|
mediaType: string
|
||||||
|
digest: string
|
||||||
|
size: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface RpmTreeEntry {
|
export interface RpmTreeEntry {
|
||||||
name: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
@@ -375,6 +400,29 @@ export const api = {
|
|||||||
params.set('path', path)
|
params.set('path', path)
|
||||||
return requestBinary(`/api/repos/${repoId}/rpm/file?${params.toString()}`)
|
return requestBinary(`/api/repos/${repoId}/rpm/file?${params.toString()}`)
|
||||||
},
|
},
|
||||||
|
listDockerImages: (repoId: string) => request<string[]>(`/api/repos/${repoId}/docker/images`),
|
||||||
|
listDockerTags: (repoId: string, image: string) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (image) params.set('image', image)
|
||||||
|
return request<DockerTagInfo[]>(`/api/repos/${repoId}/docker/tags?${params.toString()}`)
|
||||||
|
},
|
||||||
|
getDockerManifest: (repoId: string, ref: string, image: string) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('ref', ref)
|
||||||
|
if (image) params.set('image', image)
|
||||||
|
return request<DockerManifestDetail>(`/api/repos/${repoId}/docker/manifest?${params.toString()}`)
|
||||||
|
},
|
||||||
|
deleteDockerTag: (repoId: string, image: string, tag: string) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (image) params.set('image', image)
|
||||||
|
params.set('tag', tag)
|
||||||
|
return request<{ status: string }>(`/api/repos/${repoId}/docker/tag?${params.toString()}`, { method: 'DELETE' })
|
||||||
|
},
|
||||||
|
deleteDockerImage: (repoId: string, image: string) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (image) params.set('image', image)
|
||||||
|
return request<{ status: string }>(`/api/repos/${repoId}/docker/image?${params.toString()}`, { method: 'DELETE' })
|
||||||
|
},
|
||||||
|
|
||||||
listIssues: (projectId: string) => request<Issue[]>(`/api/projects/${projectId}/issues`),
|
listIssues: (projectId: string) => request<Issue[]>(`/api/projects/${projectId}/issues`),
|
||||||
createIssue: (projectId: string, payload: { title: string; body: string }) =>
|
createIssue: (projectId: string, payload: { title: string; body: string }) =>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function RepoSubNav(props: RepoSubNavProps) {
|
|||||||
const isCommits = currentPath.startsWith(`${basePath}/commits`)
|
const isCommits = currentPath.startsWith(`${basePath}/commits`)
|
||||||
const isBranches = currentPath.startsWith(`${basePath}/branches`)
|
const isBranches = currentPath.startsWith(`${basePath}/branches`)
|
||||||
const isRPM = repoType === 'rpm'
|
const isRPM = repoType === 'rpm'
|
||||||
|
const isDocker = repoType === 'docker'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -35,9 +36,9 @@ export default function RepoSubNav(props: RepoSubNavProps) {
|
|||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button component={Link} to={codePath} variant={isCode ? 'contained' : 'outlined'} size="small">
|
<Button component={Link} to={codePath} variant={isCode ? 'contained' : 'outlined'} size="small">
|
||||||
{isRPM ? 'Packages' : 'Code'}
|
{isRPM ? 'Packages' : isDocker ? 'Images' : 'Code'}
|
||||||
</Button>
|
</Button>
|
||||||
{!isRPM ? (
|
{!isRPM && !isDocker ? (
|
||||||
<>
|
<>
|
||||||
<Button component={Link} to={commitsPath} variant={isCommits ? 'contained' : 'outlined'} size="small">
|
<Button component={Link} to={commitsPath} variant={isCommits ? 'contained' : 'outlined'} size="small">
|
||||||
Commits
|
Commits
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function GlobalReposPage() {
|
|||||||
const [reposError, setReposError] = useState<string | null>(null)
|
const [reposError, setReposError] = useState<string | null>(null)
|
||||||
const [reposLoading, setReposLoading] = useState(false)
|
const [reposLoading, setReposLoading] = useState(false)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [typeFilter, setTypeFilter] = useState<'all' | 'git' | 'rpm'>('all')
|
const [typeFilter, setTypeFilter] = useState<'all' | 'git' | 'rpm' | 'docker'>('all')
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [pageSizeInput, setPageSizeInput] = useState('20')
|
const [pageSizeInput, setPageSizeInput] = useState('20')
|
||||||
@@ -82,7 +82,7 @@ export default function GlobalReposPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setTypeFilter(event.target.value as 'all' | 'git' | 'rpm')
|
setTypeFilter(event.target.value as 'all' | 'git' | 'rpm' | 'docker')
|
||||||
setPage(0)
|
setPage(0)
|
||||||
}}
|
}}
|
||||||
sx={{ minWidth: 120 }}
|
sx={{ minWidth: 120 }}
|
||||||
@@ -90,6 +90,7 @@ export default function GlobalReposPage() {
|
|||||||
<MenuItem value="all">All</MenuItem>
|
<MenuItem value="all">All</MenuItem>
|
||||||
<MenuItem value="git">Git</MenuItem>
|
<MenuItem value="git">Git</MenuItem>
|
||||||
<MenuItem value="rpm">RPM</MenuItem>
|
<MenuItem value="rpm">RPM</MenuItem>
|
||||||
|
<MenuItem value="docker">Docker</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
label="Items"
|
label="Items"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { api, Repo } from '../api'
|
import { api, Repo } from '../api'
|
||||||
import RepoGitDetailPage from './RepoGitDetailPage'
|
import RepoGitDetailPage from './RepoGitDetailPage'
|
||||||
|
import RepoDockerDetailPage from './RepoDockerDetailPage'
|
||||||
import RepoRpmDetailPage from './RepoRpmDetailPage'
|
import RepoRpmDetailPage from './RepoRpmDetailPage'
|
||||||
|
|
||||||
export default function RepoDetailPage() {
|
export default function RepoDetailPage() {
|
||||||
@@ -69,5 +70,8 @@ export default function RepoDetailPage() {
|
|||||||
if (repo.type === 'rpm') {
|
if (repo.type === 'rpm') {
|
||||||
return <RepoRpmDetailPage initialRepo={repo} />
|
return <RepoRpmDetailPage initialRepo={repo} />
|
||||||
}
|
}
|
||||||
|
if (repo.type === 'docker') {
|
||||||
|
return <RepoDockerDetailPage initialRepo={repo} />
|
||||||
|
}
|
||||||
return <RepoGitDetailPage initialRepo={repo} />
|
return <RepoGitDetailPage initialRepo={repo} />
|
||||||
}
|
}
|
||||||
|
|||||||
460
frontend/src/pages/RepoDockerDetailPage.tsx
Normal file
460
frontend/src/pages/RepoDockerDetailPage.tsx
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemText,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { api, DockerManifestDetail, DockerTagInfo, Project, Repo } from '../api'
|
||||||
|
import ProjectNavBar from '../components/ProjectNavBar'
|
||||||
|
import RepoSubNav from '../components/RepoSubNav'
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
|
||||||
|
|
||||||
|
type RepoDockerDetailPageProps = {
|
||||||
|
initialRepo?: Repo
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
|
||||||
|
const { initialRepo } = props
|
||||||
|
const { projectId, repoId } = useParams()
|
||||||
|
const [repo, setRepo] = useState<Repo | null>(initialRepo || null)
|
||||||
|
const [project, setProject] = useState<Project | null>(null)
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null)
|
||||||
|
const [images, setImages] = useState<string[]>([])
|
||||||
|
const [imagesError, setImagesError] = useState<string | null>(null)
|
||||||
|
const [selectedImage, setSelectedImage] = useState('')
|
||||||
|
const [tags, setTags] = useState<DockerTagInfo[]>([])
|
||||||
|
const [tagsError, setTagsError] = useState<string | null>(null)
|
||||||
|
const [selectedTag, setSelectedTag] = useState<DockerTagInfo | null>(null)
|
||||||
|
const [detail, setDetail] = useState<DockerManifestDetail | null>(null)
|
||||||
|
const [detailError, setDetailError] = useState<string | null>(null)
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
|
const [deleteTagOpen, setDeleteTagOpen] = useState(false)
|
||||||
|
const [deleteTagName, setDeleteTagName] = useState('')
|
||||||
|
const [deleteTagConfirm, setDeleteTagConfirm] = useState('')
|
||||||
|
const [deleteImageOpen, setDeleteImageOpen] = useState(false)
|
||||||
|
const [deleteImagePath, setDeleteImagePath] = useState('')
|
||||||
|
const [deleteImageLabel, setDeleteImageLabel] = useState('')
|
||||||
|
const [deleteImageConfirm, setDeleteImageConfirm] = useState('')
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const initRepoRef = useRef<string | null>(null)
|
||||||
|
const initProjectRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!repoId) return
|
||||||
|
if (initRepoRef.current === repoId) return
|
||||||
|
initRepoRef.current = repoId
|
||||||
|
setLoadError(null)
|
||||||
|
if (initialRepo && initialRepo.id === repoId) {
|
||||||
|
setRepo(initialRepo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.getRepo(repoId).then((data) => setRepo(data)).catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load repository'
|
||||||
|
setLoadError(message)
|
||||||
|
setRepo(null)
|
||||||
|
})
|
||||||
|
}, [repoId, initialRepo])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return
|
||||||
|
if (initProjectRef.current === projectId) return
|
||||||
|
initProjectRef.current = projectId
|
||||||
|
api
|
||||||
|
.getProject(projectId)
|
||||||
|
.then((data) => setProject(data))
|
||||||
|
.catch(() => setProject(null))
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!repoId) return
|
||||||
|
if (!repo || repo.type !== 'docker') {
|
||||||
|
setImages([])
|
||||||
|
setImagesError(null)
|
||||||
|
setSelectedImage('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api
|
||||||
|
.listDockerImages(repoId)
|
||||||
|
.then((data) => {
|
||||||
|
const list = Array.isArray(data) ? data : []
|
||||||
|
setImages(list)
|
||||||
|
setImagesError(null)
|
||||||
|
if (list.length && !selectedImage) {
|
||||||
|
setSelectedImage(list[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load images'
|
||||||
|
setImagesError(message)
|
||||||
|
setImages([])
|
||||||
|
})
|
||||||
|
}, [repoId, repo])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!repoId) return
|
||||||
|
if (!repo || repo.type !== 'docker') {
|
||||||
|
setTags([])
|
||||||
|
setTagsError(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api
|
||||||
|
.listDockerTags(repoId, selectedImage)
|
||||||
|
.then((data) => {
|
||||||
|
setTags(Array.isArray(data) ? data : [])
|
||||||
|
setTagsError(null)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load tags'
|
||||||
|
setTagsError(message)
|
||||||
|
setTags([])
|
||||||
|
})
|
||||||
|
}, [repoId, repo, selectedImage])
|
||||||
|
|
||||||
|
const handleTagSelect = (tag: DockerTagInfo) => {
|
||||||
|
if (!repoId) return
|
||||||
|
setSelectedTag(tag)
|
||||||
|
setDetail(null)
|
||||||
|
setDetailError(null)
|
||||||
|
setDetailLoading(true)
|
||||||
|
api
|
||||||
|
.getDockerManifest(repoId, tag.tag, selectedImage)
|
||||||
|
.then((data) => {
|
||||||
|
setDetail(data)
|
||||||
|
setDetailError(null)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load manifest'
|
||||||
|
setDetailError(message)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDetailLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshImages = () => {
|
||||||
|
if (!repoId) return
|
||||||
|
api
|
||||||
|
.listDockerImages(repoId)
|
||||||
|
.then((data) => {
|
||||||
|
const list = Array.isArray(data) ? data : []
|
||||||
|
setImages(list)
|
||||||
|
setImagesError(null)
|
||||||
|
if (list.length && !list.includes(selectedImage)) {
|
||||||
|
setSelectedImage(list[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load images'
|
||||||
|
setImagesError(message)
|
||||||
|
setImages([])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTags = () => {
|
||||||
|
if (!repoId) return
|
||||||
|
api
|
||||||
|
.listDockerTags(repoId, selectedImage)
|
||||||
|
.then((data) => {
|
||||||
|
setTags(Array.isArray(data) ? data : [])
|
||||||
|
setTagsError(null)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load tags'
|
||||||
|
setTagsError(message)
|
||||||
|
setTags([])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTag = async () => {
|
||||||
|
if (!repoId || !deleteTagName) return
|
||||||
|
if (deleteTagConfirm.trim() !== deleteTagName) {
|
||||||
|
setDeleteError('Type the tag name to confirm deletion.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDeleteError(null)
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await api.deleteDockerTag(repoId, selectedImage, deleteTagName)
|
||||||
|
setDeleteTagOpen(false)
|
||||||
|
setDeleteTagConfirm('')
|
||||||
|
setDeleteTagName('')
|
||||||
|
refreshTags()
|
||||||
|
setDetail(null)
|
||||||
|
setSelectedTag(null)
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to delete tag'
|
||||||
|
setDeleteError(message)
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteImage = async () => {
|
||||||
|
if (!repoId) return
|
||||||
|
if (deleteImageConfirm.trim() !== deleteImageLabel) {
|
||||||
|
setDeleteError('Type the image name to confirm deletion.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDeleteError(null)
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await api.deleteDockerImage(repoId, deleteImagePath)
|
||||||
|
setDeleteImageOpen(false)
|
||||||
|
setDeleteImageConfirm('')
|
||||||
|
setDeleteImagePath('')
|
||||||
|
setDeleteImageLabel('')
|
||||||
|
setSelectedImage('')
|
||||||
|
setSelectedTag(null)
|
||||||
|
setDetail(null)
|
||||||
|
refreshImages()
|
||||||
|
refreshTags()
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to delete image'
|
||||||
|
setDeleteError(message)
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Button component={Link} to="/projects" size="small">
|
||||||
|
Projects
|
||||||
|
</Button>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
/
|
||||||
|
</Typography>
|
||||||
|
{project ? (
|
||||||
|
<>
|
||||||
|
<Button component={Link} to={`/projects/${projectId}`} size="small">
|
||||||
|
{project.name}
|
||||||
|
</Button>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
/
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<Button component={Link} to={`/projects/${projectId}/repos`} size="small">
|
||||||
|
Repositories
|
||||||
|
</Button>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
/
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{repo?.name || 'Repository'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{projectId ? <ProjectNavBar projectId={projectId} sx={{ mb: 0, minWidth: 320 }} /> : null}
|
||||||
|
</Box>
|
||||||
|
{projectId && repoId ? <RepoSubNav projectId={projectId} repoId={repoId} repoType={repo?.type} /> : null}
|
||||||
|
<Divider sx={{ mb: 1 }} />
|
||||||
|
{loadError ? (
|
||||||
|
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
|
||||||
|
{loadError}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: '280px minmax(0, 1fr)', gap: 1, mb: 1 }}>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
{imagesError ? (
|
||||||
|
<Alert severity="warning" sx={{ mb: 1 }}>
|
||||||
|
{imagesError}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
<List dense>
|
||||||
|
{images.map((image) => (
|
||||||
|
<ListItem key={image || 'root'} disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedImage(image)
|
||||||
|
setSelectedTag(null)
|
||||||
|
setDetail(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: selectedImage === image ? 600 : 400 }}>
|
||||||
|
{image || '(root)'}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setDeleteError(null)
|
||||||
|
setDeleteImagePath(image)
|
||||||
|
setDeleteImageLabel(image || '(root)')
|
||||||
|
setDeleteImageConfirm('')
|
||||||
|
setDeleteImageOpen(true)
|
||||||
|
}}
|
||||||
|
aria-label={`Delete image ${image || '(root)'}`}
|
||||||
|
>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
{!images.length && !imagesError ? (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}>
|
||||||
|
No images found.
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
{tagsError ? (
|
||||||
|
<Alert severity="warning" sx={{ mb: 1 }}>
|
||||||
|
{tagsError}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{selectedImage !== '' || images.includes('') ? (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Image: {selectedImage || '(root)'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<List dense>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<ListItem key={tag.tag} disablePadding>
|
||||||
|
<ListItemButton onClick={() => handleTagSelect(tag)}>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: selectedTag?.tag === tag.tag ? 600 : 400 }}>
|
||||||
|
{tag.tag}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
secondary={tag.digest ? tag.digest.slice(0, 12) : ''}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteError(null)
|
||||||
|
setDeleteTagName(tag.tag)
|
||||||
|
setDeleteTagConfirm('')
|
||||||
|
setDeleteTagOpen(true)
|
||||||
|
}}
|
||||||
|
aria-label={`Delete tag ${tag.tag}`}
|
||||||
|
>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
{!tags.length && !tagsError ? (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}>
|
||||||
|
No tags found.
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</List>
|
||||||
|
{detailLoading ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Loading manifest...
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{detailError ? (
|
||||||
|
<Alert severity="warning">{detailError}</Alert>
|
||||||
|
) : null}
|
||||||
|
{detail ? (
|
||||||
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
|
<Typography variant="body2">Tag: {detail.reference}</Typography>
|
||||||
|
<Typography variant="body2">Digest: {detail.digest}</Typography>
|
||||||
|
<Typography variant="body2">Media: {detail.media_type}</Typography>
|
||||||
|
<Typography variant="body2">Size: {detail.size} bytes</Typography>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<Typography variant="body2">OS: {detail.config?.os || 'n/a'}</Typography>
|
||||||
|
<Typography variant="body2">Architecture: {detail.config?.architecture || 'n/a'}</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Created: {detail.config?.created ? new Date(detail.config.created).toLocaleString() : 'n/a'}
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<Typography variant="body2">Layers: {detail.layers?.length || 0}</Typography>
|
||||||
|
<List dense>
|
||||||
|
{detail.layers?.map((layer) => (
|
||||||
|
<ListItem key={layer.digest}>
|
||||||
|
<ListItemText primary={layer.digest} secondary={`${layer.size} bytes`} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Select a tag to view details.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
<Dialog open={deleteTagOpen} onClose={() => setDeleteTagOpen(false)} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle>Delete tag</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null}
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Delete tag "{deleteTagName}"?
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Type tag to confirm"
|
||||||
|
value={deleteTagConfirm}
|
||||||
|
onChange={(event) => setDeleteTagConfirm(event.target.value)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteTagOpen(false)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDeleteTag}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
disabled={deleting || deleteTagConfirm.trim() !== deleteTagName}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog open={deleteImageOpen} onClose={() => setDeleteImageOpen(false)} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle>Delete image</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null}
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Delete image "{deleteImageLabel || '(root)'}" and all tags?
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Type image name to confirm"
|
||||||
|
value={deleteImageConfirm}
|
||||||
|
onChange={(event) => setDeleteImageConfirm(event.target.value)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteImageOpen(false)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDeleteImage}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
disabled={deleting || deleteImageConfirm.trim() !== deleteImageLabel}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -463,7 +463,7 @@ export default function ReposPage() {
|
|||||||
<Chip label={repo.type || 'git'} size="small" variant="outlined" />
|
<Chip label={repo.type || 'git'} size="small" variant="outlined" />
|
||||||
{repo.is_foreign ? <Chip label="foreign" size="small" color="warning" variant="outlined" /> : null}
|
{repo.is_foreign ? <Chip label="foreign" size="small" color="warning" variant="outlined" /> : null}
|
||||||
{repo.is_foreign && repo.owner_slug ? <Chip label={repo.owner_slug} size="small" variant="outlined" /> : null}
|
{repo.is_foreign && repo.owner_slug ? <Chip label={repo.owner_slug} size="small" variant="outlined" /> : null}
|
||||||
{(repo.type === 'rpm' ? repo.rpm_url : repo.clone_url) ? (
|
{(repo.type === 'rpm' ? repo.rpm_url : repo.type === 'docker' ? repo.docker_url : repo.clone_url) ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
@@ -476,7 +476,7 @@ export default function ReposPage() {
|
|||||||
textOverflow: 'ellipsis'
|
textOverflow: 'ellipsis'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{repo.type === 'rpm' ? repo.rpm_url : repo.clone_url}
|
{repo.type === 'rpm' ? repo.rpm_url : repo.type === 'docker' ? repo.docker_url : repo.clone_url}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -530,6 +530,7 @@ export default function ReposPage() {
|
|||||||
>
|
>
|
||||||
<MenuItem value="git">Git</MenuItem>
|
<MenuItem value="git">Git</MenuItem>
|
||||||
<MenuItem value="rpm">RPM</MenuItem>
|
<MenuItem value="rpm">RPM</MenuItem>
|
||||||
|
<MenuItem value="docker">Docker</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1 }}>
|
||||||
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
|
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user