Initial commit
This commit is contained in:
85
.dockerignore
Normal file
85
.dockerignore
Normal file
@@ -0,0 +1,85 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.angular
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
|
||||
# Docker files (except main ones needed for build)
|
||||
docker-compose.yml
|
||||
docker-compose.*.yml
|
||||
*.dockerfile
|
||||
|
||||
# Test files
|
||||
src/**/*.spec.ts
|
||||
e2e/
|
||||
karma.conf.js
|
||||
*.test.js
|
||||
|
||||
# Development files
|
||||
proxy.conf.json
|
||||
start-dev.*
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary deletion files
|
||||
.delete-*
|
||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
# Multi-stage build for optimized production image
|
||||
FROM node:18-alpine as build
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production --silent
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the Angular application
|
||||
RUN npm run build -- --configuration production
|
||||
|
||||
# Production stage
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
# Install curl for health checks
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# Copy built application
|
||||
COPY --from=build /app/dist/docker-registry-browser /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Create nginx user and set permissions
|
||||
RUN addgroup -g 1001 -S nginx && \
|
||||
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx && \
|
||||
chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chown -R nginx:nginx /var/cache/nginx && \
|
||||
chown -R nginx:nginx /var/log/nginx && \
|
||||
chown -R nginx:nginx /etc/nginx/conf.d
|
||||
|
||||
# Switch to non-root user
|
||||
USER nginx
|
||||
|
||||
# Add labels for better container management
|
||||
LABEL maintainer="Your Name <your.email@example.com>"
|
||||
LABEL description="Docker Registry Browser - A web interface for browsing Docker registries"
|
||||
LABEL version="1.1.0"
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:80/ || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 NinjaPug
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
207
README.md
Normal file
207
README.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Docker Registry Browser - Production Deployment
|
||||
|
||||
A modern, responsive web interface for browsing Docker registries with support for both Docker v2 and OCI manifest formats.
|
||||
|
||||
## Features
|
||||
|
||||
- Browse repositories and tags
|
||||
- View detailed image information (layers, environment, labels, etc.)
|
||||
- Copy docker pull commands
|
||||
- Push command generator with examples
|
||||
- Dark/Light mode toggle
|
||||
- Fully responsive design
|
||||
- Support for OCI and Docker v2 manifests
|
||||
- Multi-platform image support
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Docker Run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name docker-registry-browser \
|
||||
-p 8080:80 \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-e REGISTRY_HOST=localhost:5000 \
|
||||
-e REGISTRY_PROTOCOL=http \
|
||||
your-dockerhub-username/docker-registry-browser:latest
|
||||
```
|
||||
|
||||
### Option 2: Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/docker-registry-browser.git
|
||||
cd docker-registry-browser
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Option 3: Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/docker-registry-browser.git
|
||||
cd docker-registry-browser
|
||||
docker build -t docker-registry-browser .
|
||||
docker run -d -p 8080:80 --add-host=host.docker.internal:host-gateway docker-registry-browser
|
||||
```
|
||||
|
||||
## Unraid Installation
|
||||
|
||||
### Method 1: Community Applications (Recommended)
|
||||
|
||||
1. In Unraid, go to **Apps** tab
|
||||
2. Search for "Docker Registry Browser"
|
||||
3. Click **Install**
|
||||
4. Configure the settings and click **Apply**
|
||||
|
||||
### Method 2: Manual Template
|
||||
|
||||
1. In Unraid, go to **Docker** tab
|
||||
2. Click **Add Container**
|
||||
3. Set **Template** to the template URL or upload the XML template
|
||||
4. Configure the required settings
|
||||
5. Click **Apply**
|
||||
|
||||
### Method 3: Docker Compose (Unraid 6.12+)
|
||||
|
||||
1. Install the "Compose Manager" plugin
|
||||
2. Create a new compose stack with the provided `docker-compose.yml`
|
||||
3. Deploy the stack
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `REGISTRY_HOST` | `localhost:5000` | Docker registry hostname and port |
|
||||
| `REGISTRY_PROTOCOL` | `http` | Protocol (http/https) |
|
||||
| `REGISTRY_USERNAME` | - | Registry username (optional) |
|
||||
| `REGISTRY_PASSWORD` | - | Registry password (optional) |
|
||||
|
||||
### Unraid Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| **WebUI Port** | `8080` | Port for web interface |
|
||||
| **Registry Host** | `localhost:5000` | Your registry address |
|
||||
| **Registry Protocol** | `http` | http or https |
|
||||
| **Registry Username** | - | Optional authentication |
|
||||
| **Registry Password** | - | Optional authentication |
|
||||
|
||||
## Usage
|
||||
|
||||
1. Access the web interface at `http://your-server:8080`
|
||||
2. Browse repositories on the left panel
|
||||
3. Select a repository to view its tags
|
||||
4. Click the info button to view detailed image information
|
||||
5. Use the menu for push commands and settings
|
||||
6. Toggle dark/light mode using the theme button in the toolbar
|
||||
|
||||
## Features Guide
|
||||
|
||||
### Dark Mode
|
||||
- Toggle between light and dark themes using the moon/sun icon in the toolbar
|
||||
- Theme preference is saved locally and persists between sessions
|
||||
- All UI components are properly themed for optimal visibility in both modes
|
||||
|
||||
### Browsing Images
|
||||
- Repository list shows all available repositories
|
||||
- Click a repository to load its tags
|
||||
- Search repositories using the search field
|
||||
- View tag details by clicking the info button
|
||||
|
||||
### Push Commands
|
||||
- Access via the menu (three dots) in the toolbar
|
||||
- Get step-by-step instructions for pushing images
|
||||
- Copy commands to clipboard
|
||||
- Includes multi-architecture build instructions
|
||||
|
||||
### Image Details
|
||||
- View comprehensive image information
|
||||
- See layer details, environment variables, labels
|
||||
- Check image size, architecture, and creation date
|
||||
- Inspect exposed ports and volumes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Registry Connection Issues
|
||||
|
||||
**Problem**: Cannot connect to registry
|
||||
**Solution**:
|
||||
1. Verify `REGISTRY_HOST` is correct
|
||||
2. Check if registry is accessible from container
|
||||
3. For local registries, ensure `--add-host=host.docker.internal:host-gateway` is set
|
||||
|
||||
### CORS Issues
|
||||
|
||||
**Problem**: API requests blocked by CORS
|
||||
**Solution**: The nginx configuration includes CORS headers, but ensure your registry allows cross-origin requests
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
**Problem**: 401 Unauthorized errors
|
||||
**Solution**: Set `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables
|
||||
|
||||
### Manifest Issues
|
||||
|
||||
**Problem**: "OCI index found" errors
|
||||
**Solution**: This should be resolved in the current version which supports OCI manifests
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Development server
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Build Docker image
|
||||
docker build -t docker-registry-browser .
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## Health Check
|
||||
|
||||
The container includes a health check endpoint at `/health` that returns:
|
||||
- `200 OK` with "healthy" response when running properly
|
||||
- Checks every 30 seconds with 3 retries
|
||||
|
||||
## Security
|
||||
|
||||
- Runs as non-root user (nginx:nginx)
|
||||
- Includes security headers
|
||||
- No sensitive data stored in container
|
||||
- Registry credentials passed via environment variables
|
||||
|
||||
## Support
|
||||
|
||||
- **GitHub**: [Issues and Discussions](https://github.com/your-username/docker-registry-browser)
|
||||
- **Docker Hub**: [Container Images](https://hub.docker.com/r/your-dockerhub-username/docker-registry-browser)
|
||||
- **Unraid Forums**: [Community Support](https://forums.unraid.net/)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.0.0
|
||||
- Initial release
|
||||
- Support for Docker v2 and OCI manifests
|
||||
- Responsive design
|
||||
- Push command generation
|
||||
- Dark/Light mode
|
||||
- Multi-platform image support
|
||||
80
angular.json
Normal file
80
angular.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"docker-registry-browser": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/docker-registry-browser",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "docker-registry-browser:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "docker-registry-browser:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
77
build.bat
Normal file
77
build.bat
Normal file
@@ -0,0 +1,77 @@
|
||||
@echo off
|
||||
REM Docker Registry Browser - Build and Deploy Script for Windows
|
||||
REM Usage: build.bat [tag] [registry]
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Configuration
|
||||
set IMAGE_NAME=docker-registry-browser
|
||||
set DEFAULT_TAG=latest
|
||||
set DEFAULT_REGISTRY=
|
||||
|
||||
REM Parse arguments
|
||||
if "%1"=="" (
|
||||
set TAG=%DEFAULT_TAG%
|
||||
) else (
|
||||
set TAG=%1
|
||||
)
|
||||
|
||||
if "%2"=="" (
|
||||
set REGISTRY=%DEFAULT_REGISTRY%
|
||||
) else (
|
||||
set REGISTRY=%2
|
||||
)
|
||||
|
||||
REM Build the full image name
|
||||
if "%REGISTRY%"=="" (
|
||||
set FULL_IMAGE_NAME=%IMAGE_NAME%:%TAG%
|
||||
) else (
|
||||
set FULL_IMAGE_NAME=%REGISTRY%/%IMAGE_NAME%:%TAG%
|
||||
)
|
||||
|
||||
echo Building Docker Registry Browser...
|
||||
echo Image: %FULL_IMAGE_NAME%
|
||||
echo.
|
||||
|
||||
REM Build the Docker image
|
||||
echo Building Docker image...
|
||||
docker build -t "%FULL_IMAGE_NAME%" .
|
||||
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo.
|
||||
echo To run the container:
|
||||
echo docker run -d --name docker-registry-browser -p 8080:80 --add-host=host.docker.internal:host-gateway %FULL_IMAGE_NAME%
|
||||
echo.
|
||||
echo To push to registry (if configured):
|
||||
if "%REGISTRY%"=="" (
|
||||
echo Please specify a registry: build.bat %TAG% your-registry.com
|
||||
) else (
|
||||
echo docker push %FULL_IMAGE_NAME%
|
||||
)
|
||||
echo.
|
||||
|
||||
REM Optional: Run the container immediately
|
||||
set /p REPLY="Do you want to run the container now? (y/N): "
|
||||
if /i "%REPLY%"=="y" (
|
||||
echo Starting container...
|
||||
docker run -d --name docker-registry-browser -p 8080:80 --add-host=host.docker.internal:host-gateway -e REGISTRY_HOST=localhost:5000 -e REGISTRY_PROTOCOL=http "%FULL_IMAGE_NAME%"
|
||||
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
echo.
|
||||
echo Container started successfully!
|
||||
echo Access the application at: http://localhost:8080
|
||||
echo View container logs: docker logs docker-registry-browser
|
||||
echo Stop container: docker stop docker-registry-browser
|
||||
) else (
|
||||
echo Failed to start container!
|
||||
)
|
||||
)
|
||||
|
||||
pause
|
||||
64
build.sh
Normal file
64
build.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker Registry Browser - Build and Deploy Script
|
||||
# Usage: ./build.sh [tag] [registry]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
IMAGE_NAME="docker-registry-browser"
|
||||
DEFAULT_TAG="latest"
|
||||
DEFAULT_REGISTRY=""
|
||||
|
||||
# Parse arguments
|
||||
TAG=${1:-$DEFAULT_TAG}
|
||||
REGISTRY=${2:-$DEFAULT_REGISTRY}
|
||||
|
||||
# Build the full image name
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||
else
|
||||
FULL_IMAGE_NAME="$IMAGE_NAME:$TAG"
|
||||
fi
|
||||
|
||||
echo "Building Docker Registry Browser..."
|
||||
echo "Image: $FULL_IMAGE_NAME"
|
||||
echo ""
|
||||
|
||||
# Build the Docker image
|
||||
echo "Building Docker image..."
|
||||
docker build -t "$FULL_IMAGE_NAME" .
|
||||
|
||||
echo ""
|
||||
echo "Build completed successfully!"
|
||||
echo ""
|
||||
echo "To run the container:"
|
||||
echo "docker run -d --name docker-registry-browser -p 8080:80 --add-host=host.docker.internal:host-gateway $FULL_IMAGE_NAME"
|
||||
echo ""
|
||||
echo "To push to registry (if configured):"
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
echo "docker push $FULL_IMAGE_NAME"
|
||||
else
|
||||
echo "Please specify a registry: ./build.sh $TAG your-registry.com"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Optional: Run the container immediately
|
||||
read -p "Do you want to run the container now? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Starting container..."
|
||||
docker run -d \
|
||||
--name docker-registry-browser \
|
||||
-p 8080:80 \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-e REGISTRY_HOST=localhost:5000 \
|
||||
-e REGISTRY_PROTOCOL=http \
|
||||
"$FULL_IMAGE_NAME"
|
||||
|
||||
echo ""
|
||||
echo "Container started successfully!"
|
||||
echo "Access the application at: http://localhost:8080"
|
||||
echo "View container logs: docker logs docker-registry-browser"
|
||||
echo "Stop container: docker stop docker-registry-browser"
|
||||
fi
|
||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
docker-registry-browser:
|
||||
build: .
|
||||
container_name: docker-registry-browser
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
# Registry configuration
|
||||
- REGISTRY_HOST=host.docker.internal:5000
|
||||
- REGISTRY_PROTOCOL=http
|
||||
# Optional: Basic auth if your registry requires it
|
||||
# - REGISTRY_USERNAME=username
|
||||
# - REGISTRY_PASSWORD=password
|
||||
extra_hosts:
|
||||
# For accessing host services (like local registry)
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
# Unraid template labels
|
||||
- "net.unraid.docker.managed=dockerman"
|
||||
- "net.unraid.docker.icon=https://raw.githubusercontent.com/docker/docs/main/assets/images/docker-icon.png"
|
||||
networks:
|
||||
- registry-network
|
||||
|
||||
networks:
|
||||
registry-network:
|
||||
driver: bridge
|
||||
132
nginx.conf
Normal file
132
nginx.conf
Normal file
@@ -0,0 +1,132 @@
|
||||
# nginx.conf for production deployment
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Basic Settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 100m;
|
||||
|
||||
# Gzip Settings
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# Security Headers
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security: Hide nginx version
|
||||
server_tokens off;
|
||||
|
||||
# Main application
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache control for HTML files (no cache)
|
||||
location ~* \.html$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
}
|
||||
|
||||
# Static assets with long-term caching
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
}
|
||||
|
||||
# Proxy for Docker Registry API (configurable via environment)
|
||||
location /api/ {
|
||||
# Remove /api prefix and forward to registry
|
||||
rewrite ^/api/(.*)$ /$1 break;
|
||||
|
||||
# Default to localhost:5000, but can be overridden
|
||||
proxy_pass http://host.docker.internal:5000;
|
||||
|
||||
# Proxy headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers for registry API
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" always;
|
||||
|
||||
# Handle preflight requests
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization";
|
||||
add_header Access-Control-Max-Age 1728000;
|
||||
add_header Content-Type "text/plain charset=UTF-8";
|
||||
add_header Content-Length 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
add_header Content-Type text/plain;
|
||||
return 200 "healthy\n";
|
||||
}
|
||||
|
||||
# 404 error handling
|
||||
error_page 404 /index.html;
|
||||
|
||||
# 50x error handling
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
||||
14099
package-lock.json
generated
Normal file
14099
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "docker-registry-browser",
|
||||
"version": "1.1.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"serve": "ng serve --host 0.0.0.0 --port 4200"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/cdk": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/compiler": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"@angular/material": "^17.0.0",
|
||||
"@angular/platform-browser": "^17.0.0",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||
"@angular/router": "^17.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.0.0",
|
||||
"@angular/cli": "^17.0.0",
|
||||
"@angular/compiler-cli": "^17.0.0",
|
||||
"@types/node": "^18.7.0",
|
||||
"typescript": "~5.2.0"
|
||||
}
|
||||
}
|
||||
16
proxy.conf.json
Normal file
16
proxy.conf.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://192.168.1.193:5000",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"headers": {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
||||
},
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
292
src/app/app.component.html
Normal file
292
src/app/app.component.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<mat-toolbar color="primary" class="app-toolbar">
|
||||
<mat-icon>storage</mat-icon>
|
||||
<span class="toolbar-title">Docker Registry Browser</span>
|
||||
<span class="spacer"></span>
|
||||
|
||||
<!-- Dark Mode Toggle -->
|
||||
<button mat-icon-button (click)="toggleTheme()" [matTooltip]="isDarkMode ? 'Light mode' : 'Dark mode'">
|
||||
<mat-icon>{{ isDarkMode ? 'light_mode' : 'dark_mode' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Menu Button -->
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu" matTooltip="Actions">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="openPushCommandsDialog()">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span>Push Image Commands</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-raised-button color="accent" (click)="loadRepositories()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Refresh
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="content">
|
||||
<!-- Registry Configuration -->
|
||||
<mat-card class="config-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<mat-icon>settings</mat-icon>
|
||||
Registry Configuration
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>Connected to: <code>{{ registryHost }}</code></p>
|
||||
<p><small>Using development proxy to bypass CORS restrictions</small></p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Copy Success Message -->
|
||||
<div *ngIf="copyMessage" class="copy-message">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
{{ copyMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Error Display -->
|
||||
<mat-card *ngIf="error" class="error-card">
|
||||
<mat-card-content>
|
||||
<div class="error-content">
|
||||
<mat-icon color="warn">error</mat-icon>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="content-grid">
|
||||
<!-- Repositories List -->
|
||||
<mat-card class="repositories-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<mat-icon>folder</mat-icon>
|
||||
Repositories ({{ filteredRepositories.length }})
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<!-- Search -->
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Search repositories</mat-label>
|
||||
<input matInput [(ngModel)]="searchTerm" placeholder="Filter repositories...">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Loading -->
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<p>Loading repositories...</p>
|
||||
</div>
|
||||
|
||||
<!-- Repositories List -->
|
||||
<mat-nav-list *ngIf="!loading">
|
||||
<mat-list-item *ngFor="let repo of filteredRepositories"
|
||||
(click)="loadTags(repo)"
|
||||
[class.selected]="selectedRepo?.name === repo.name">
|
||||
<mat-icon matListItemIcon>folder</mat-icon>
|
||||
<div matListItemTitle>{{ repo.name }}</div>
|
||||
<mat-icon matListItemMeta>chevron_right</mat-icon>
|
||||
</mat-list-item>
|
||||
<mat-list-item *ngIf="filteredRepositories.length === 0 && !loading">
|
||||
<div matListItemTitle class="no-data">No repositories found</div>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Tags List -->
|
||||
<mat-card class="tags-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<mat-icon>label</mat-icon>
|
||||
Tags
|
||||
<span *ngIf="selectedRepo" class="tag-count">({{ selectedRepo.name }}) - {{ tags.length }} tag{{ tags.length !== 1 ? 's' : '' }}</span>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="!selectedRepo" class="no-selection">
|
||||
<mat-icon>info</mat-icon>
|
||||
<p>Select a repository to view tags</p>
|
||||
<button mat-raised-button color="primary" (click)="openPushCommandsDialog()">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
View Push Commands
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div *ngIf="loading && selectedRepo" class="loading-container">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<p>Loading tags...</p>
|
||||
</div>
|
||||
|
||||
<!-- Tags List -->
|
||||
<div *ngIf="selectedRepo && !loading" class="tags-container">
|
||||
<div class="tags-list-wrapper">
|
||||
<mat-nav-list>
|
||||
<mat-list-item *ngFor="let tag of tags" class="tag-item">
|
||||
<mat-icon matListItemIcon>label</mat-icon>
|
||||
<div matListItemTitle class="tag-name">{{ tag.name }}</div>
|
||||
<div matListItemMeta class="tag-actions">
|
||||
<button mat-icon-button
|
||||
color="accent"
|
||||
matTooltip="View image details"
|
||||
(click)="loadImageDetails(tag)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button
|
||||
color="primary"
|
||||
matTooltip="Copy docker pull command"
|
||||
(click)="copyToClipboard(getDockerPullCommand(selectedRepo!.name, tag.name))">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item *ngIf="tags.length === 0">
|
||||
<div matListItemTitle class="no-data">No tags found</div>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
</div>
|
||||
|
||||
<!-- Image Details Panel -->
|
||||
<div *ngIf="showingDetails && selectedTag" class="image-details-panel">
|
||||
<div class="details-header">
|
||||
<h3>
|
||||
<mat-icon>info</mat-icon>
|
||||
Image Details: {{ selectedTag.name }}
|
||||
</h3>
|
||||
<button mat-icon-button (click)="closeDetails()" matTooltip="Close details">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loadingDetails" class="loading-container">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
<p>Loading image details...</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loadingDetails && selectedTag.details" class="details-content">
|
||||
<!-- Basic Info -->
|
||||
<div class="details-section">
|
||||
<h4>Basic Information</h4>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Size:</span>
|
||||
<span class="detail-value">{{ formatBytes(selectedTag.details.size) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Architecture:</span>
|
||||
<span class="detail-value">{{ selectedTag.details.architecture }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">OS:</span>
|
||||
<span class="detail-value">{{ selectedTag.details.os }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Created:</span>
|
||||
<span class="detail-value">{{ formatDate(selectedTag.details.created) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Layers:</span>
|
||||
<span class="detail-value">{{ selectedTag.details.layers.length }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Digest:</span>
|
||||
<span class="detail-value digest">{{ selectedTag.details.digest.substring(7, 19) }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="details-section" *ngIf="selectedTag.details.config">
|
||||
<h4>Configuration</h4>
|
||||
<div class="config-grid">
|
||||
<div *ngIf="selectedTag.details.config.workingDir" class="detail-item">
|
||||
<span class="detail-label">Working Directory:</span>
|
||||
<span class="detail-value">{{ selectedTag.details.config.workingDir }}</span>
|
||||
</div>
|
||||
<div *ngIf="selectedTag.details.config.user" class="detail-item">
|
||||
<span class="detail-label">User:</span>
|
||||
<span class="detail-value">{{ selectedTag.details.config.user }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<div *ngIf="selectedTag.details.config.env && selectedTag.details.config.env.length > 0" class="env-section">
|
||||
<h5>Environment Variables</h5>
|
||||
<div class="env-list">
|
||||
<div *ngFor="let env of selectedTag.details.config.env" class="env-item">
|
||||
<code>{{ env }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exposed Ports -->
|
||||
<div *ngIf="hasObjectKeys(selectedTag.details.config.exposedPorts)" class="ports-section">
|
||||
<h5>Exposed Ports</h5>
|
||||
<div class="ports-list">
|
||||
<span *ngFor="let port of getObjectKeys(selectedTag.details.config.exposedPorts)" class="port-chip">
|
||||
{{ port }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Labels -->
|
||||
<div *ngIf="hasObjectKeys(selectedTag.details.config.labels)" class="labels-section">
|
||||
<h5>Labels</h5>
|
||||
<div class="labels-list">
|
||||
<div *ngFor="let label of getObjectKeys(selectedTag.details.config.labels)" class="label-item">
|
||||
<span class="label-key">{{ label }}:</span>
|
||||
<span class="label-value">{{ getLabelValue(selectedTag.details.config.labels, label) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layers -->
|
||||
<div class="details-section">
|
||||
<h4>Layers ({{ selectedTag.details.layers.length }})</h4>
|
||||
<div class="layers-list">
|
||||
<div *ngFor="let layer of selectedTag.details.layers; let i = index" class="layer-item">
|
||||
<div class="layer-info">
|
||||
<span class="layer-number">{{ i + 1 }}</span>
|
||||
<span class="layer-size">{{ formatBytes(layer.size) }}</span>
|
||||
<span class="layer-digest">{{ layer.digest.substring(7, 19) }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Pull Commands -->
|
||||
<mat-expansion-panel *ngIf="tags.length > 0" class="pull-commands">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>code</mat-icon>
|
||||
Docker Pull Commands
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="commands-content">
|
||||
<div *ngFor="let tag of tags" class="command-item">
|
||||
<div class="command-text">
|
||||
<code>{{ getDockerPullCommand(selectedRepo!.name, tag.name) }}</code>
|
||||
</div>
|
||||
<button mat-icon-button
|
||||
color="primary"
|
||||
(click)="copyToClipboard(getDockerPullCommand(selectedRepo!.name, tag.name))"
|
||||
matTooltip="Copy to clipboard">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1057
src/app/app.component.scss
Normal file
1057
src/app/app.component.scss
Normal file
File diff suppressed because it is too large
Load Diff
157
src/app/app.component.ts
Normal file
157
src/app/app.component.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DockerRegistryService } from './services/docker-registry.service';
|
||||
import { Repository, Tag, ImageDetails } from './models/registry.model';
|
||||
import { PushCommandsDialogComponent } from './components/push-commands-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
registryUrl = '/api'; // Using proxy for CORS
|
||||
registryHost = '192.168.1.193:5000'; // Actual registry host for commands
|
||||
repositories: Repository[] = [];
|
||||
selectedRepo: Repository | null = null;
|
||||
tags: Tag[] = [];
|
||||
loading = false;
|
||||
loadingDetails = false;
|
||||
error = '';
|
||||
searchTerm = '';
|
||||
copyMessage = '';
|
||||
selectedTag: Tag | null = null;
|
||||
showingDetails = false;
|
||||
isDarkMode = false;
|
||||
|
||||
constructor(
|
||||
private registryService: DockerRegistryService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Check for saved theme preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
this.isDarkMode = savedTheme === 'dark';
|
||||
this.applyTheme();
|
||||
|
||||
this.loadRepositories();
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
this.isDarkMode = !this.isDarkMode;
|
||||
localStorage.setItem('theme', this.isDarkMode ? 'dark' : 'light');
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
private applyTheme() {
|
||||
if (this.isDarkMode) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme');
|
||||
}
|
||||
}
|
||||
|
||||
openPushCommandsDialog() {
|
||||
this.dialog.open(PushCommandsDialogComponent, {
|
||||
width: '800px',
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '90vh',
|
||||
data: {
|
||||
registryHost: this.registryHost,
|
||||
selectedRepo: this.selectedRepo?.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadRepositories() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
this.repositories = await this.registryService.getRepositories(this.registryUrl);
|
||||
} catch (err: any) {
|
||||
this.error = `Failed to fetch repositories: ${err.message}`;
|
||||
this.repositories = [];
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadTags(repo: Repository) {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.selectedRepo = repo;
|
||||
this.selectedTag = null;
|
||||
this.showingDetails = false;
|
||||
try {
|
||||
this.tags = await this.registryService.getTags(this.registryUrl, repo.name);
|
||||
} catch (err: any) {
|
||||
this.error = `Failed to fetch tags: ${err.message}`;
|
||||
this.tags = [];
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadImageDetails(tag: Tag) {
|
||||
if (!this.selectedRepo) return;
|
||||
|
||||
this.loadingDetails = true;
|
||||
this.selectedTag = tag;
|
||||
this.showingDetails = true;
|
||||
|
||||
try {
|
||||
if (!tag.details) {
|
||||
tag.details = await this.registryService.getImageDetails(
|
||||
this.registryUrl,
|
||||
this.selectedRepo.name,
|
||||
tag.name
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.error = `Failed to fetch image details: ${err.message}`;
|
||||
}
|
||||
this.loadingDetails = false;
|
||||
}
|
||||
|
||||
closeDetails() {
|
||||
this.selectedTag = null;
|
||||
this.showingDetails = false;
|
||||
}
|
||||
|
||||
getDockerPullCommand(repoName: string, tagName: string): string {
|
||||
return `docker pull ${this.registryHost}/${repoName}:${tagName}`;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text);
|
||||
this.copyMessage = 'Copied to clipboard!';
|
||||
setTimeout(() => {
|
||||
this.copyMessage = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
formatBytes = (bytes: number): string => {
|
||||
return this.registryService.formatBytes(bytes);
|
||||
}
|
||||
|
||||
formatDate = (dateString: string): string => {
|
||||
return this.registryService.formatDate(dateString);
|
||||
}
|
||||
|
||||
getObjectKeys(obj: any): string[] {
|
||||
return obj ? Object.keys(obj) : [];
|
||||
}
|
||||
|
||||
hasObjectKeys(obj: any): boolean {
|
||||
return obj && Object.keys(obj).length > 0;
|
||||
}
|
||||
|
||||
getLabelValue(labels: { [key: string]: string } | undefined, key: string): string {
|
||||
return labels?.[key] || 'N/A';
|
||||
}
|
||||
|
||||
get filteredRepositories(): Repository[] {
|
||||
return this.repositories.filter(repo =>
|
||||
repo.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
52
src/app/app.module.ts
Normal file
52
src/app/app.module.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
// Angular Material imports
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { PushCommandsDialogComponent } from './components/push-commands-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
PushCommandsDialogComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatListModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatExpansionModule,
|
||||
MatTooltipModule,
|
||||
MatDialogModule,
|
||||
MatMenuModule,
|
||||
MatDividerModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
188
src/app/components/push-commands-dialog.component.html
Normal file
188
src/app/components/push-commands-dialog.component.html
Normal file
@@ -0,0 +1,188 @@
|
||||
<div class="push-dialog">
|
||||
<h2 mat-dialog-title>
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
Push Image to Registry
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<div class="intro-text">
|
||||
<p>Use these commands to build, tag, and push Docker images to the registry:</p>
|
||||
<p><strong>Registry:</strong> <code>{{ data.registryHost }}</code></p>
|
||||
</div>
|
||||
|
||||
<!-- Build and Push Section -->
|
||||
<mat-expansion-panel class="command-section" expanded>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>build</mat-icon>
|
||||
Build and Push New Image
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="commands-list">
|
||||
<div class="command-step">
|
||||
<h4>1. Build your Docker image</h4>
|
||||
<div class="command-item">
|
||||
<code>docker build -t my-app:latest .</code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker build -t my-app:latest .')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="command-step">
|
||||
<h4>2. Tag the image for the registry</h4>
|
||||
<div class="command-item">
|
||||
<code>docker tag my-app:latest {{ data.registryHost }}/my-app:latest</code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker tag my-app:latest ' + data.registryHost + '/my-app:latest')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="command-step">
|
||||
<h4>3. Push to registry</h4>
|
||||
<div class="command-item">
|
||||
<code>docker push {{ data.registryHost }}/my-app:latest</code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker push ' + data.registryHost + '/my-app:latest')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Push Existing Image Section -->
|
||||
<mat-expansion-panel class="command-section">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
Push Existing Image
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="commands-list">
|
||||
<div class="command-step">
|
||||
<h4>Tag existing image</h4>
|
||||
<div class="command-item">
|
||||
<code>docker tag <existing-image> {{ data.registryHost }}/<repository>:<tag></code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker tag <existing-image> ' + data.registryHost + '/<repository>:<tag>')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="command-step">
|
||||
<h4>Push to registry</h4>
|
||||
<div class="command-item">
|
||||
<code>docker push {{ data.registryHost }}/<repository>:<tag></code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker push ' + data.registryHost + '/<repository>:<tag>')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Push to Specific Repository Section -->
|
||||
<mat-expansion-panel class="command-section" *ngIf="data.selectedRepo">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>folder</mat-icon>
|
||||
Push to "{{ data.selectedRepo }}"
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="commands-list">
|
||||
<div class="command-step">
|
||||
<h4>Tag for this repository</h4>
|
||||
<div class="command-item">
|
||||
<code>docker tag <your-image> {{ data.registryHost }}/{{ data.selectedRepo }}:<tag></code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker tag <your-image> ' + data.registryHost + '/' + data.selectedRepo + ':<tag>')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="command-step">
|
||||
<h4>Push to this repository</h4>
|
||||
<div class="command-item">
|
||||
<code>docker push {{ data.registryHost }}/{{ data.selectedRepo }}:<tag></code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker push ' + data.registryHost + '/' + data.selectedRepo + ':<tag>')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Multi-arch Build Section -->
|
||||
<mat-expansion-panel class="command-section">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>architecture</mat-icon>
|
||||
Multi-Architecture Build
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="commands-list">
|
||||
<div class="command-step">
|
||||
<h4>Create and use buildx builder</h4>
|
||||
<div class="command-item">
|
||||
<code>docker buildx create --use</code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker buildx create --use')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="command-step">
|
||||
<h4>Build and push multi-arch image</h4>
|
||||
<div class="command-item">
|
||||
<code>docker buildx build --platform linux/amd64,linux/arm64 -t {{ data.registryHost }}/my-app:latest --push .</code>
|
||||
<button mat-icon-button (click)="copyToClipboard('docker buildx build --platform linux/amd64,linux/arm64 -t ' + data.registryHost + '/my-app:latest --push .')" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Registry Configuration Section -->
|
||||
<mat-expansion-panel class="command-section">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>settings</mat-icon>
|
||||
Registry Configuration
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="commands-list">
|
||||
<div class="info-section">
|
||||
<h4>Allow insecure registry (if needed)</h4>
|
||||
<p>Add this to your Docker daemon configuration (<code>/etc/docker/daemon.json</code> on Linux/Mac or Docker Desktop settings):</p>
|
||||
<div class="command-item">
|
||||
<code>{{ dockerDaemonConfig }}</code>
|
||||
<button mat-icon-button (click)="copyDaemonConfig()" matTooltip="Copy">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<p><small>Restart Docker daemon after making this change.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div class="copy-success" *ngIf="copyMessage">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
{{ copyMessage }}
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="dialogRef.close()">Close</button>
|
||||
<button mat-raised-button color="primary" (click)="openDockerDocs()">
|
||||
<mat-icon>help</mat-icon>
|
||||
Docker Docs
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
138
src/app/components/push-commands-dialog.component.scss
Normal file
138
src/app/components/push-commands-dialog.component.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
.push-dialog {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3f51b5;
|
||||
}
|
||||
|
||||
.dark-theme .intro-text {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.command-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.commands-list {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.command-step {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.command-step h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #3f51b5;
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark-theme .command-step h4 {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.command-item code {
|
||||
flex: 1;
|
||||
background-color: #f8f9fa;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
border: 1px solid #e0e0e0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.dark-theme .command-item code {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.info-section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #3f51b5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark-theme .info-section h4 {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
.info-section p {
|
||||
margin: 8px 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.dark-theme .info-section p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.copy-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid #c8e6c9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark-theme .copy-success {
|
||||
background-color: #2d5016;
|
||||
color: #90ee90;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.push-dialog {
|
||||
max-width: 95vw;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.command-item code {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.command-item button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
43
src/app/components/push-commands-dialog.component.ts
Normal file
43
src/app/components/push-commands-dialog.component.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
export interface PushDialogData {
|
||||
registryHost: string;
|
||||
selectedRepo?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-push-commands-dialog',
|
||||
templateUrl: './push-commands-dialog.component.html',
|
||||
styleUrls: ['./push-commands-dialog.component.scss']
|
||||
})
|
||||
export class PushCommandsDialogComponent {
|
||||
copyMessage = '';
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<PushCommandsDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: PushDialogData
|
||||
) {}
|
||||
|
||||
get dockerDaemonConfig(): string {
|
||||
return `{
|
||||
"insecure-registries": ["${this.data.registryHost}"]
|
||||
}`;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text);
|
||||
this.copyMessage = 'Copied to clipboard!';
|
||||
setTimeout(() => {
|
||||
this.copyMessage = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
copyDaemonConfig() {
|
||||
this.copyToClipboard(this.dockerDaemonConfig);
|
||||
}
|
||||
|
||||
openDockerDocs() {
|
||||
window.open('https://docs.docker.com/engine/reference/commandline/push/', '_blank');
|
||||
}
|
||||
}
|
||||
72
src/app/models/registry.model.ts
Normal file
72
src/app/models/registry.model.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export interface Repository {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
name: string;
|
||||
details?: ImageDetails;
|
||||
}
|
||||
|
||||
export interface RegistryResponse {
|
||||
repositories: string[];
|
||||
}
|
||||
|
||||
export interface TagsResponse {
|
||||
name: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface ImageDetails {
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
size: number;
|
||||
created: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
layers: LayerInfo[];
|
||||
config: ImageConfig;
|
||||
}
|
||||
|
||||
export interface LayerInfo {
|
||||
digest: string;
|
||||
size: number;
|
||||
mediaType: string;
|
||||
}
|
||||
|
||||
export interface ImageConfig {
|
||||
labels?: { [key: string]: string };
|
||||
env?: string[];
|
||||
cmd?: string[];
|
||||
entrypoint?: string[];
|
||||
workingDir?: string;
|
||||
user?: string;
|
||||
exposedPorts?: { [key: string]: any };
|
||||
volumes?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface ManifestResponse {
|
||||
schemaVersion: number;
|
||||
mediaType: string;
|
||||
config: {
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
size: number;
|
||||
};
|
||||
layers: {
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
size: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
created: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
config: ImageConfig;
|
||||
history: {
|
||||
created: string;
|
||||
created_by: string;
|
||||
empty_layer?: boolean;
|
||||
}[];
|
||||
}
|
||||
199
src/app/services/docker-registry.service.ts
Normal file
199
src/app/services/docker-registry.service.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Repository, Tag, RegistryResponse, TagsResponse, ImageDetails, ManifestResponse, ConfigResponse } from '../models/registry.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DockerRegistryService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
async getRepositories(registryUrl: string): Promise<Repository[]> {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.http.get<RegistryResponse>(`${registryUrl}/v2/_catalog`)
|
||||
);
|
||||
return (response?.repositories || []).map(name => ({ name }));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to connect to registry: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTags(registryUrl: string, repositoryName: string): Promise<Tag[]> {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.http.get<TagsResponse>(`${registryUrl}/v2/${repositoryName}/tags/list`)
|
||||
);
|
||||
return (response?.tags || []).map(name => ({ name }));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch tags for ${repositoryName}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getImageDetails(registryUrl: string, repositoryName: string, tag: string): Promise<ImageDetails> {
|
||||
try {
|
||||
// Enhanced Accept header to support both Docker v2 and OCI manifest formats
|
||||
const manifestHeaders = new HttpHeaders({
|
||||
'Accept': [
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
'application/vnd.docker.distribution.manifest.list.v2+json',
|
||||
'application/vnd.oci.image.manifest.v1+json',
|
||||
'application/vnd.oci.image.index.v1+json'
|
||||
].join(', ')
|
||||
});
|
||||
|
||||
const manifest = await firstValueFrom(
|
||||
this.http.get<any>(
|
||||
`${registryUrl}/v2/${repositoryName}/manifests/${tag}`,
|
||||
{ headers: manifestHeaders }
|
||||
)
|
||||
);
|
||||
|
||||
console.log('Manifest response:', manifest);
|
||||
|
||||
// Handle different manifest types
|
||||
if (this.isManifestList(manifest)) {
|
||||
// Handle manifest lists (multi-platform images)
|
||||
return await this.handleManifestList(registryUrl, repositoryName, tag, manifest);
|
||||
} else if (this.isImageManifest(manifest)) {
|
||||
// Handle single platform manifests
|
||||
return await this.handleImageManifest(registryUrl, repositoryName, manifest);
|
||||
} else {
|
||||
throw new Error(`Unsupported manifest type: ${manifest.mediaType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching image details:', error);
|
||||
throw new Error(`Failed to fetch image details for ${repositoryName}:${tag}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private isManifestList(manifest: any): boolean {
|
||||
return manifest.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' ||
|
||||
manifest.mediaType === 'application/vnd.oci.image.index.v1+json';
|
||||
}
|
||||
|
||||
private isImageManifest(manifest: any): boolean {
|
||||
return manifest.mediaType === 'application/vnd.docker.distribution.manifest.v2+json' ||
|
||||
manifest.mediaType === 'application/vnd.oci.image.manifest.v1+json';
|
||||
}
|
||||
|
||||
private async handleManifestList(registryUrl: string, repositoryName: string, tag: string, manifestList: any): Promise<ImageDetails> {
|
||||
// For manifest lists, we'll use the first manifest (usually linux/amd64)
|
||||
// You could enhance this to let users choose the platform
|
||||
const firstManifest = manifestList.manifests?.[0];
|
||||
|
||||
if (!firstManifest) {
|
||||
throw new Error('No manifests found in manifest list');
|
||||
}
|
||||
|
||||
console.log('Using manifest from list:', firstManifest);
|
||||
|
||||
// Fetch the actual manifest using its digest
|
||||
const manifestHeaders = new HttpHeaders({
|
||||
'Accept': [
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
'application/vnd.oci.image.manifest.v1+json'
|
||||
].join(', ')
|
||||
});
|
||||
|
||||
const actualManifest = await firstValueFrom(
|
||||
this.http.get<any>(
|
||||
`${registryUrl}/v2/${repositoryName}/manifests/${firstManifest.digest}`,
|
||||
{ headers: manifestHeaders }
|
||||
)
|
||||
);
|
||||
|
||||
return await this.handleImageManifest(registryUrl, repositoryName, actualManifest, firstManifest);
|
||||
}
|
||||
|
||||
private async handleImageManifest(registryUrl: string, repositoryName: string, manifest: any, platformInfo?: any): Promise<ImageDetails> {
|
||||
try {
|
||||
// Get the config blob
|
||||
const configDigest = manifest.config?.digest;
|
||||
if (!configDigest) {
|
||||
throw new Error('No config digest found in manifest');
|
||||
}
|
||||
|
||||
const config = await firstValueFrom(
|
||||
this.http.get<ConfigResponse>(
|
||||
`${registryUrl}/v2/${repositoryName}/blobs/${configDigest}`
|
||||
)
|
||||
);
|
||||
|
||||
console.log('Config response:', config);
|
||||
|
||||
// Calculate total size
|
||||
const layers = manifest.layers || [];
|
||||
const totalSize = layers.reduce((sum: number, layer: any) => sum + (layer.size || 0), 0) + (manifest.config?.size || 0);
|
||||
|
||||
return {
|
||||
digest: configDigest,
|
||||
mediaType: manifest.mediaType,
|
||||
size: totalSize,
|
||||
created: config.created || new Date().toISOString(),
|
||||
architecture: config.architecture || platformInfo?.platform?.architecture || 'unknown',
|
||||
os: config.os || platformInfo?.platform?.os || 'unknown',
|
||||
layers: layers.map((layer: any) => ({
|
||||
digest: layer.digest,
|
||||
size: layer.size || 0,
|
||||
mediaType: layer.mediaType || 'application/vnd.docker.image.rootfs.diff.tar.gzip'
|
||||
})),
|
||||
config: config.config || {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error handling image manifest:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
formatDate(dateString: string): string {
|
||||
try {
|
||||
return new Date(dateString).toLocaleString();
|
||||
} catch (error) {
|
||||
return 'Invalid Date';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get available platforms for a manifest list
|
||||
async getAvailablePlatforms(registryUrl: string, repositoryName: string, tag: string): Promise<any[]> {
|
||||
try {
|
||||
const manifestHeaders = new HttpHeaders({
|
||||
'Accept': [
|
||||
'application/vnd.docker.distribution.manifest.list.v2+json',
|
||||
'application/vnd.oci.image.index.v1+json'
|
||||
].join(', ')
|
||||
});
|
||||
|
||||
const manifest = await firstValueFrom(
|
||||
this.http.get<any>(
|
||||
`${registryUrl}/v2/${repositoryName}/manifests/${tag}`,
|
||||
{ headers: manifestHeaders }
|
||||
)
|
||||
);
|
||||
|
||||
if (this.isManifestList(manifest)) {
|
||||
return manifest.manifests?.map((m: any) => ({
|
||||
digest: m.digest,
|
||||
platform: m.platform,
|
||||
size: m.size
|
||||
})) || [];
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error getting platforms:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/environments/environment.ts
Normal file
3
src/environments/environment.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
1
src/favicon.ico
Normal file
1
src/favicon.ico
Normal file
@@ -0,0 +1 @@
|
||||
<!-- Placeholder favicon.ico - Replace with actual favicon -->
|
||||
17
src/index.html
Normal file
17
src/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Docker Registry Browser</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
1
src/polyfills.ts
Normal file
1
src/polyfills.ts
Normal file
@@ -0,0 +1 @@
|
||||
import 'zone.js';
|
||||
332
src/styles.scss
Normal file
332
src/styles.scss
Normal file
@@ -0,0 +1,332 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
// Include the common styles for Angular Material
|
||||
@include mat.core();
|
||||
|
||||
// Define a light theme
|
||||
$light-primary: mat.define-palette(mat.$indigo-palette);
|
||||
$light-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
|
||||
$light-warn: mat.define-palette(mat.$red-palette);
|
||||
$light-theme: mat.define-light-theme((
|
||||
color: (
|
||||
primary: $light-primary,
|
||||
accent: $light-accent,
|
||||
warn: $light-warn,
|
||||
)
|
||||
));
|
||||
|
||||
// Define a dark theme
|
||||
$dark-primary: mat.define-palette(mat.$blue-grey-palette);
|
||||
$dark-accent: mat.define-palette(mat.$amber-palette, A200, A100, A400);
|
||||
$dark-warn: mat.define-palette(mat.$deep-orange-palette);
|
||||
$dark-theme: mat.define-dark-theme((
|
||||
color: (
|
||||
primary: $dark-primary,
|
||||
accent: $dark-accent,
|
||||
warn: $dark-warn,
|
||||
)
|
||||
));
|
||||
|
||||
// Apply the light theme by default
|
||||
@include mat.all-component-themes($light-theme);
|
||||
|
||||
// Apply the dark theme when the .dark-theme class is present
|
||||
.dark-theme {
|
||||
@include mat.all-component-colors($dark-theme);
|
||||
}
|
||||
|
||||
// Ensure Material Icons load properly
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons' !important;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-family: 'Material Icons' !important;
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
font-family: 'Material Icons' !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
// Custom dark theme enhancements
|
||||
.dark-theme {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
|
||||
.content {
|
||||
background-color: #121212;
|
||||
}
|
||||
|
||||
.copy-message {
|
||||
background-color: #2d5016;
|
||||
color: #90ee90;
|
||||
border: 1px solid #4caf50;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background-color: #4a1a1a;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
color: #ffaaaa;
|
||||
}
|
||||
|
||||
.no-data,
|
||||
.no-selection {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.image-details-panel {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #444444;
|
||||
|
||||
.details-header {
|
||||
background-color: #424242 !important;
|
||||
color: white !important;
|
||||
|
||||
h3 {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
button {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.details-content {
|
||||
background-color: #1e1e1e;
|
||||
|
||||
.details-section {
|
||||
h4 {
|
||||
color: #64b5f6 !important;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
.detail-item {
|
||||
.detail-label {
|
||||
color: #999999 !important;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #ffffff !important;
|
||||
|
||||
&.digest {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #64b5f6 !important;
|
||||
border: 1px solid #444444;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
.detail-item {
|
||||
.detail-label {
|
||||
color: #999999 !important;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.env-item code {
|
||||
background-color: #333333;
|
||||
border: 1px solid #555555;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.port-chip {
|
||||
background-color: #1976d2;
|
||||
color: #ffffff;
|
||||
border: 1px solid #2196f3;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
border-bottom: 1px solid #444444;
|
||||
|
||||
.label-key {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.label-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
background-color: #333333;
|
||||
border: 1px solid #555555;
|
||||
|
||||
.layer-info {
|
||||
.layer-number {
|
||||
background-color: #616161 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.layer-size {
|
||||
background-color: #444444;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.layer-digest {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layers-list {
|
||||
.layer-item {
|
||||
background-color: #333333;
|
||||
border: 1px solid #555555;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pull-commands {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #444444;
|
||||
|
||||
.mat-expansion-panel-header {
|
||||
background-color: #333333 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #424242 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.commands-content {
|
||||
background-color: #1e1e1e;
|
||||
|
||||
.command-item {
|
||||
border-bottom: 1px solid #444444;
|
||||
}
|
||||
|
||||
.command-text code {
|
||||
background-color: #333333;
|
||||
border: 1px solid #555555;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Material card enhancements
|
||||
mat-card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
|
||||
&.config-card {
|
||||
background-color: #1e1e1e;
|
||||
|
||||
code {
|
||||
background-color: #333333;
|
||||
color: #64b5f6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List items in dark mode
|
||||
mat-list-item {
|
||||
&:hover {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #424242 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Search field in dark mode
|
||||
.mat-mdc-form-field {
|
||||
.mat-mdc-text-field-wrapper {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-focus-overlay {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs in dark mode
|
||||
.mat-mdc-dialog-container {
|
||||
background-color: #1e1e1e !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
// Expansion panels
|
||||
mat-expansion-panel {
|
||||
background-color: #1e1e1e;
|
||||
|
||||
.mat-expansion-panel-header {
|
||||
background-color: #333333;
|
||||
|
||||
&:hover {
|
||||
background-color: #424242 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollbars in dark mode
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #555555;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
start-dev.bat
Normal file
29
start-dev.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo 🐳 Docker Registry Browser - Quick Start
|
||||
echo ========================================
|
||||
|
||||
REM Check if npm is installed
|
||||
npm --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ npm is not installed. Please install Node.js and npm first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Angular CLI is installed globally
|
||||
ng version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 📦 Installing Angular CLI globally...
|
||||
npm install -g @angular/cli
|
||||
)
|
||||
|
||||
REM Install dependencies
|
||||
echo 📦 Installing dependencies...
|
||||
npm install
|
||||
|
||||
REM Start the development server
|
||||
echo 🚀 Starting development server...
|
||||
echo The application will be available at http://localhost:4200
|
||||
npm start
|
||||
|
||||
pause
|
||||
25
start-dev.sh
Normal file
25
start-dev.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🐳 Docker Registry Browser - Quick Start"
|
||||
echo "========================================"
|
||||
|
||||
# Check if npm is installed
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "❌ npm is not installed. Please install Node.js and npm first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Angular CLI is installed globally
|
||||
if ! command -v ng &> /dev/null; then
|
||||
echo "📦 Installing Angular CLI globally..."
|
||||
npm install -g @angular/cli
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
|
||||
# Start the development server
|
||||
echo "🚀 Starting development server..."
|
||||
echo "The application will be available at http://localhost:4200"
|
||||
npm start
|
||||
14
tsconfig.app.json
Normal file
14
tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
30
unraid-template.xml
Normal file
30
unraid-template.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0"?>
|
||||
<Container version="2">
|
||||
<n>docker-registry-browser</n>
|
||||
<Repository>your-dockerhub-username/docker-registry-browser:latest</Repository>
|
||||
<Registry>https://hub.docker.com/</Registry>
|
||||
<Network>bridge</Network>
|
||||
<MyIP/>
|
||||
<Shell>sh</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support>https://github.com/your-username/docker-registry-browser</Support>
|
||||
<Project>https://github.com/your-username/docker-registry-browser</Project>
|
||||
<Overview>Docker Registry Browser - A modern web interface for browsing and managing Docker registries. Features include repository browsing, tag listing, image details inspection, and push command generation.</Overview>
|
||||
<Category>Tools:</Category>
|
||||
<WebUI>http://[IP]:[PORT:8080]/</WebUI>
|
||||
<TemplateURL>https://raw.githubusercontent.com/your-username/docker-registry-browser/main/unraid-template.xml</TemplateURL>
|
||||
<Icon>https://raw.githubusercontent.com/docker/docs/main/assets/images/docker-icon.png</Icon>
|
||||
<ExtraParams>--add-host=host.docker.internal:host-gateway</ExtraParams>
|
||||
<PostArgs/>
|
||||
<CPUset/>
|
||||
<DateInstalled/>
|
||||
<DonateText/>
|
||||
<DonateLink/>
|
||||
<Requires/>
|
||||
<Config Name="WebUI Port" Target="80" Default="8080" Mode="tcp" Description="Port for accessing the web interface" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
||||
<Config Name="Registry Host" Target="REGISTRY_HOST" Default="localhost:5000" Mode="" Description="Docker registry host and port (e.g., localhost:5000, registry.example.com:5000)" Type="Variable" Display="always" Required="true" Mask="false">localhost:5000</Config>
|
||||
<Config Name="Registry Protocol" Target="REGISTRY_PROTOCOL" Default="http" Mode="" Description="Protocol to use for registry connection (http or https)" Type="Variable" Display="always" Required="true" Mask="false">http</Config>
|
||||
<Config Name="Registry Username" Target="REGISTRY_USERNAME" Default="" Mode="" Description="Username for registry authentication (leave empty for no auth)" Type="Variable" Display="always" Required="false" Mask="false"></Config>
|
||||
<Config Name="Registry Password" Target="REGISTRY_PASSWORD" Default="" Mode="" Description="Password for registry authentication (leave empty for no auth)" Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="App Data" Target="/app/data" Default="/mnt/user/appdata/docker-registry-browser" Mode="rw" Description="Application data directory" Type="Path" Display="advanced" Required="false" Mask="false">/mnt/user/appdata/docker-registry-browser</Config>
|
||||
</Container>
|
||||
Reference in New Issue
Block a user