Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec7de50e3 | |||
| 9c465cbd4c | |||
| ee6e581b9d | |||
| 2a8e544de3 | |||
| 847eeb839f |
@@ -22,6 +22,8 @@ steps:
|
|||||||
repo: git.unkin.net/unkin/artifactapi-ui
|
repo: git.unkin.net/unkin/artifactapi-ui
|
||||||
dockerfile: ui/Dockerfile.ui
|
dockerfile: ui/Dockerfile.ui
|
||||||
context: ui
|
context: ui
|
||||||
|
build_args:
|
||||||
|
BASE_PATH: /ui
|
||||||
username: droneci
|
username: droneci
|
||||||
password:
|
password:
|
||||||
from_secret: DRONECI_PASSWORD
|
from_secret: DRONECI_PASSWORD
|
||||||
|
|||||||
@@ -37,6 +37,20 @@ func (h *ProxyHandler) Routes() chi.Router {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ProxyHandler) DockerV2Routes() chi.Router {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Get("/", h.handleDockerPing)
|
||||||
|
r.Head("/", h.handleDockerPing)
|
||||||
|
r.Get("/{remoteName}/*", h.handleProxy)
|
||||||
|
r.Head("/{remoteName}/*", h.handleProxy)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProxyHandler) handleDockerPing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *ProxyHandler) handleProxy(w http.ResponseWriter, r *http.Request) {
|
func (h *ProxyHandler) handleProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
remoteName := chi.URLParam(r, "remoteName")
|
remoteName := chi.URLParam(r, "remoteName")
|
||||||
path := chi.URLParam(r, "*")
|
path := chi.URLParam(r, "*")
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.unkin.net/unkin/artifactapi/internal/cache"
|
"git.unkin.net/unkin/artifactapi/internal/cache"
|
||||||
@@ -147,6 +149,21 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa
|
|||||||
return nil, &UpstreamError{Err: err}
|
return nil, &UpstreamError{Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
resp.Body.Close()
|
||||||
|
token, err := fetchBearerToken(ctx, resp.Header.Get("Www-Authenticate"), remote)
|
||||||
|
if err == nil && token != "" {
|
||||||
|
req2, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
req2.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
resp, err = http.DefaultClient.Do(req2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &UpstreamError{Err: err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, &ProxyError{Status: http.StatusUnauthorized, Message: "upstream returned 401"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil, &ProxyError{Status: resp.StatusCode, Message: fmt.Sprintf("upstream returned %d", resp.StatusCode)}
|
return nil, &ProxyError{Status: resp.StatusCode, Message: fmt.Sprintf("upstream returned %d", resp.StatusCode)}
|
||||||
@@ -319,6 +336,71 @@ func (r readerAt) ReadAt(p []byte, off int64) (n int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchBearerToken(ctx context.Context, wwwAuth string, remote models.Remote) (string, error) {
|
||||||
|
if !strings.HasPrefix(wwwAuth, "Bearer ") {
|
||||||
|
return "", fmt.Errorf("not a Bearer challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]string{}
|
||||||
|
for _, part := range strings.Split(wwwAuth[7:], ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
eq := strings.Index(part, "=")
|
||||||
|
if eq < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := part[:eq]
|
||||||
|
val := strings.Trim(part[eq+1:], `"`)
|
||||||
|
params[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
realm := params["realm"]
|
||||||
|
if realm == "" {
|
||||||
|
return "", fmt.Errorf("no realm in Bearer challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenURL := realm
|
||||||
|
sep := "?"
|
||||||
|
if s, ok := params["service"]; ok {
|
||||||
|
tokenURL += sep + "service=" + s
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
if s, ok := params["scope"]; ok {
|
||||||
|
tokenURL += sep + "scope=" + s
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if remote.Username != "" && remote.Password != "" {
|
||||||
|
req.SetBasicAuth(remote.Username, remote.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("token endpoint returned %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenResp.Token != "" {
|
||||||
|
return tokenResp.Token, nil
|
||||||
|
}
|
||||||
|
return tokenResp.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyError struct {
|
type ProxyError struct {
|
||||||
Status int
|
Status int
|
||||||
Message string
|
Message string
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func (s *Server) routes() chi.Router {
|
|||||||
|
|
||||||
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
|
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
|
||||||
r.Mount("/api/v1", proxyHandler.Routes())
|
r.Mount("/api/v1", proxyHandler.Routes())
|
||||||
|
r.Mount("/v2", proxyHandler.DockerV2Routes())
|
||||||
|
|
||||||
remotesHandler := v2.NewRemotesHandler(s.db)
|
remotesHandler := v2.NewRemotesHandler(s.db)
|
||||||
virtualsHandler := v2.NewVirtualsHandler(s.db)
|
virtualsHandler := v2.NewVirtualsHandler(s.db)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (e *Engine) fetchMemberIndexes(ctx context.Context, virt models.Virtual, pa
|
|||||||
results[idx] = result{err: fmt.Errorf("local index %q: %w", name, err)}
|
results[idx] = result{err: fmt.Errorf("local index %q: %w", name, err)}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
results[idx] = result{index: MemberIndex{RemoteName: name, RepoType: remote.RepoType, Body: body}}
|
results[idx] = result{index: MemberIndex{RemoteName: name, RepoType: remote.RepoType, BaseURL: remote.BaseURL, Body: body}}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ func (e *Engine) fetchMemberIndexes(ctx context.Context, virt models.Virtual, pa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results[idx] = result{index: MemberIndex{RemoteName: name, RepoType: remote.RepoType, Body: body}}
|
results[idx] = result{index: MemberIndex{RemoteName: name, RepoType: remote.RepoType, BaseURL: remote.BaseURL, Body: body}}
|
||||||
}(i, memberName)
|
}(i, memberName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,15 +54,26 @@ func (m *HelmMerger) MergeIndexes(members []MemberIndex, proxyBaseURL string) ([
|
|||||||
seen[chart][ver.Version] = true
|
seen[chart][ver.Version] = true
|
||||||
|
|
||||||
if proxyBaseURL != "" {
|
if proxyBaseURL != "" {
|
||||||
|
routePrefix := "remote"
|
||||||
|
if member.RepoType == "local" {
|
||||||
|
routePrefix = "local"
|
||||||
|
}
|
||||||
|
baseHost := extractHost(member.BaseURL)
|
||||||
|
|
||||||
for i, u := range ver.URLs {
|
for i, u := range ver.URLs {
|
||||||
if strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") {
|
if strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") {
|
||||||
ver.URLs[i] = fmt.Sprintf("%s/api/v1/remote/%s/%s",
|
if baseHost != "" && extractHost(u) != baseHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ver.URLs[i] = fmt.Sprintf("%s/api/v1/%s/%s/%s",
|
||||||
strings.TrimRight(proxyBaseURL, "/"),
|
strings.TrimRight(proxyBaseURL, "/"),
|
||||||
|
routePrefix,
|
||||||
member.RemoteName,
|
member.RemoteName,
|
||||||
extractPath(u))
|
extractPath(u))
|
||||||
} else {
|
} else {
|
||||||
ver.URLs[i] = fmt.Sprintf("%s/api/v1/remote/%s/%s",
|
ver.URLs[i] = fmt.Sprintf("%s/api/v1/%s/%s/%s",
|
||||||
strings.TrimRight(proxyBaseURL, "/"),
|
strings.TrimRight(proxyBaseURL, "/"),
|
||||||
|
routePrefix,
|
||||||
member.RemoteName,
|
member.RemoteName,
|
||||||
u)
|
u)
|
||||||
}
|
}
|
||||||
@@ -78,6 +89,19 @@ func (m *HelmMerger) MergeIndexes(members []MemberIndex, proxyBaseURL string) ([
|
|||||||
return yaml.Marshal(merged)
|
return yaml.Marshal(merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractHost(rawURL string) string {
|
||||||
|
idx := strings.Index(rawURL, "://")
|
||||||
|
if idx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest := rawURL[idx+3:]
|
||||||
|
slashIdx := strings.Index(rest, "/")
|
||||||
|
if slashIdx == -1 {
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
return rest[:slashIdx]
|
||||||
|
}
|
||||||
|
|
||||||
func extractPath(rawURL string) string {
|
func extractPath(rawURL string) string {
|
||||||
idx := strings.Index(rawURL, "://")
|
idx := strings.Index(rawURL, "://")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
type MemberIndex struct {
|
type MemberIndex struct {
|
||||||
RemoteName string
|
RemoteName string
|
||||||
RepoType models.RepoType
|
RepoType models.RepoType
|
||||||
|
BaseURL string
|
||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ COPY package.json package-lock.json* ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
ARG BASE_PATH=/
|
||||||
|
ENV BASE_PATH=${BASE_PATH}
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
ARG BASE_PATH=/
|
||||||
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
RUN sed -i "s|\${BASE_PATH}|${BASE_PATH}|g" /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
+1
-27
@@ -5,33 +5,7 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location /api/ {
|
location ${BASE_PATH} {
|
||||||
proxy_pass http://artifactapi:8000;
|
|
||||||
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;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /v2/ {
|
|
||||||
proxy_pass http://artifactapi:8000;
|
|
||||||
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;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /health {
|
|
||||||
proxy_pass http://artifactapi:8000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /metrics {
|
|
||||||
proxy_pass http://artifactapi:8000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -4,9 +4,13 @@ import { BrowserRouter } from 'react-router-dom';
|
|||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
declare const __BASE_PATH__: string;
|
||||||
|
|
||||||
|
const basename = __BASE_PATH__.replace(/\/+$/, '') || '/';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={basename}>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
const basePath = process.env.BASE_PATH || '/'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: basePath,
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -11,4 +14,7 @@ export default defineConfig({
|
|||||||
'/metrics': 'http://localhost:8000',
|
'/metrics': 'http://localhost:8000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
'__BASE_PATH__': JSON.stringify(basePath),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user