feat: auto-discover container images from registries

- Add ListImages() to registry interface, implement for Gitea
- Add owner field to registry config (needed for Gitea packages API)
- GET /api/registries/:id/images endpoint
- "Browse Images" button on Projects and Quick Deploy pages
- Image dropdown with registry grouping and search
- i18n support (EN/RU) for all new UI strings
This commit is contained in:
2026-03-28 14:04:11 +03:00
parent 77251c540b
commit 37e251da85
12 changed files with 355 additions and 18 deletions
+52
View File
@@ -17,6 +17,7 @@ type registryRequest struct {
URL string `json:"url"`
Type string `json:"type"`
Token string `json:"token"`
Owner string `json:"owner"`
}
// listRegistries handles GET /api/registries.
@@ -34,6 +35,7 @@ func (s *Server) listRegistries(w http.ResponseWriter, r *http.Request) {
URL string `json:"url"`
Type string `json:"type"`
HasToken bool `json:"has_token"`
Owner string `json:"owner"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
@@ -46,6 +48,7 @@ func (s *Server) listRegistries(w http.ResponseWriter, r *http.Request) {
URL: reg.URL,
Type: reg.Type,
HasToken: reg.Token != "",
Owner: reg.Owner,
CreatedAt: reg.CreatedAt,
UpdatedAt: reg.UpdatedAt,
}
@@ -84,6 +87,7 @@ func (s *Server) createRegistry(w http.ResponseWriter, r *http.Request) {
URL: req.URL,
Type: req.Type,
Token: encToken,
Owner: req.Owner,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to create registry: "+err.Error())
@@ -125,6 +129,8 @@ func (s *Server) updateRegistry(w http.ResponseWriter, r *http.Request) {
if req.Type != "" {
updated.Type = req.Type
}
// Owner can be set to empty string intentionally, so always update it.
updated.Owner = req.Owner
// Only re-encrypt if a new token is provided.
if req.Token != "" {
@@ -265,3 +271,49 @@ func (s *Server) listRegistryTags(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, tags)
}
// listRegistryImages handles GET /api/registries/{id}/images.
// Returns all container images available in the registry for the configured owner.
func (s *Server) listRegistryImages(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
reg, err := s.store.GetRegistryByID(id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
respondNotFound(w, "registry")
return
}
respondError(w, http.StatusInternalServerError, "failed to get registry: "+err.Error())
return
}
if reg.Owner == "" {
respondError(w, http.StatusBadRequest, "registry has no owner configured; set the owner in registry settings")
return
}
// Decrypt the token.
token := reg.Token
if token != "" {
decrypted, err := crypto.Decrypt(s.encKey, token)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to decrypt registry token")
return
}
token = decrypted
}
client, err := registry.NewClient(reg.Type, reg.URL, token)
if err != nil {
respondError(w, http.StatusBadRequest, "unsupported registry type: "+reg.Type)
return
}
images, err := client.ListImages(r.Context(), reg.Owner)
if err != nil {
respondError(w, http.StatusBadGateway, "failed to list images: "+err.Error())
return
}
respondJSON(w, http.StatusOK, images)
}