Compare commits
2 Commits
v3.3.0
..
7f569cdcdc
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f569cdcdc | |||
| ab44271e82 |
@@ -6,6 +6,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
@@ -15,8 +17,11 @@ import (
|
|||||||
"git.unkin.net/unkin/artifactapi/internal/proxy"
|
"git.unkin.net/unkin/artifactapi/internal/proxy"
|
||||||
"git.unkin.net/unkin/artifactapi/internal/storage"
|
"git.unkin.net/unkin/artifactapi/internal/storage"
|
||||||
"git.unkin.net/unkin/artifactapi/internal/virtual"
|
"git.unkin.net/unkin/artifactapi/internal/virtual"
|
||||||
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var semverRe = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?$`)
|
||||||
|
|
||||||
type ProxyHandler struct {
|
type ProxyHandler struct {
|
||||||
engine *proxy.Engine
|
engine *proxy.Engine
|
||||||
virtualEngine *virtual.Engine
|
virtualEngine *virtual.Engine
|
||||||
@@ -110,9 +115,8 @@ func (h *ProxyHandler) handleLocal(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prov, _ := provider.Get(remote.PackageType)
|
if remote.PackageType == models.PackageTerraform {
|
||||||
if indexer, ok := prov.(provider.LocalIndexer); ok {
|
if h.serveTerraformMirror(w, r, remote, path) {
|
||||||
if indexer.ServeLocalIndex(w, r, h.db, remote.Name, path) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +124,31 @@ func (h *ProxyHandler) handleLocal(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.serveLocalFile(w, r, localName, path)
|
h.serveLocalFile(w, r, localName, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ProxyHandler) serveTerraformMirror(w http.ResponseWriter, r *http.Request, remote *models.Remote, path string) bool {
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, typeName := parts[0], parts[1]
|
||||||
|
tail := parts[2]
|
||||||
|
|
||||||
|
if tail == "index.json" {
|
||||||
|
h.local.ServeTerraformIndex(w, r, remote.Name, namespace, typeName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(tail, ".json") {
|
||||||
|
version := strings.TrimSuffix(tail, ".json")
|
||||||
|
if semverRe.MatchString(version) {
|
||||||
|
h.local.ServeTerraformVersionDoc(w, r, remote.Name, namespace, typeName, version)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (h *ProxyHandler) serveLocalFile(w http.ResponseWriter, r *http.Request, repoName, path string) {
|
func (h *ProxyHandler) serveLocalFile(w http.ResponseWriter, r *http.Request, repoName, path string) {
|
||||||
file, err := h.db.GetLocalFile(r.Context(), repoName, path)
|
file, err := h.db.GetLocalFile(r.Context(), repoName, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+119
-15
@@ -1,19 +1,25 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
||||||
"git.unkin.net/unkin/artifactapi/internal/storage"
|
"git.unkin.net/unkin/artifactapi/internal/storage"
|
||||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var providerZipRe = regexp.MustCompile(
|
||||||
|
`^terraform-provider-([a-zA-Z0-9_-]+)_([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?)_([a-z0-9]+)_([a-z0-9]+)\.zip$`,
|
||||||
|
)
|
||||||
|
|
||||||
type LocalHandler struct {
|
type LocalHandler struct {
|
||||||
db *database.DB
|
db *database.DB
|
||||||
store *storage.S3
|
store *storage.S3
|
||||||
@@ -55,22 +61,41 @@ func (h *LocalHandler) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prov, _ := provider.Get(remote.PackageType)
|
if remote.PackageType == models.PackageTerraform {
|
||||||
|
h.uploadTerraformProvider(w, r, remote, filePath)
|
||||||
if uploader, ok := prov.(provider.LocalUploader); ok {
|
|
||||||
h.uploadValidated(w, r, remote, filePath, uploader)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.uploadGeneric(w, r, remote, filePath)
|
h.uploadGeneric(w, r, remote, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string, uploader provider.LocalUploader) {
|
func (h *LocalHandler) uploadTerraformProvider(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string) {
|
||||||
storagePath, contentType, err := uploader.ValidateUpload(filePath)
|
parts := strings.Split(filePath, "/")
|
||||||
if err != nil {
|
if len(parts) != 3 {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, "path must be {namespace}/{type}/{filename}.zip", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
namespace, typeName, filename := parts[0], parts[1], parts[2]
|
||||||
|
|
||||||
|
m := providerZipRe.FindStringSubmatch(filename)
|
||||||
|
if m == nil {
|
||||||
|
http.Error(w, fmt.Sprintf(
|
||||||
|
"filename %q does not match terraform-provider-{type}_{version}_{os}_{arch}.zip",
|
||||||
|
filename,
|
||||||
|
), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileType, version, os, arch := m[1], m[2], m[3], m[4]
|
||||||
|
|
||||||
|
if fileType != typeName {
|
||||||
|
http.Error(w, fmt.Sprintf(
|
||||||
|
"provider type in filename %q does not match path type %q",
|
||||||
|
fileType, typeName,
|
||||||
|
), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storagePath := fmt.Sprintf("%s/%s/%s", namespace, typeName, filename)
|
||||||
|
|
||||||
existing, err := h.db.GetLocalFile(r.Context(), remote.Name, storagePath)
|
existing, err := h.db.GetLocalFile(r.Context(), remote.Name, storagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,17 +103,20 @@ func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
http.Error(w, fmt.Sprintf("file %q already exists; overwrites are not allowed", storagePath), http.StatusConflict)
|
http.Error(w, fmt.Sprintf(
|
||||||
|
"provider %s/%s version %s for %s_%s already exists; overwrites are not allowed",
|
||||||
|
namespace, typeName, version, os, arch,
|
||||||
|
), http.StatusConflict)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := h.cas.Store(r.Context(), r.Body, contentType)
|
result, err := h.cas.Store(r.Context(), r.Body, "application/zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("store failed: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("store failed: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.UpsertBlob(r.Context(), result.ContentHash, result.S3Key, result.SizeBytes, contentType); err != nil {
|
if err := h.db.UpsertBlob(r.Context(), result.ContentHash, result.S3Key, result.SizeBytes, "application/zip"); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("record blob: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("record blob: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -102,7 +130,15 @@ func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusCreated, uploader.UploadResponse(storagePath, result.ContentHash, result.SizeBytes))
|
writeJSON(w, http.StatusCreated, map[string]any{
|
||||||
|
"namespace": namespace,
|
||||||
|
"type": typeName,
|
||||||
|
"version": version,
|
||||||
|
"os": os,
|
||||||
|
"arch": arch,
|
||||||
|
"content_hash": result.ContentHash,
|
||||||
|
"size_bytes": result.SizeBytes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalHandler) uploadGeneric(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string) {
|
func (h *LocalHandler) uploadGeneric(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string) {
|
||||||
@@ -187,6 +223,74 @@ func (h *LocalHandler) remove(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalHandler) DB() *database.DB {
|
type terraformIndex struct {
|
||||||
return h.db
|
Versions map[string]json.RawMessage `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type terraformVersionDoc struct {
|
||||||
|
Archives map[string]terraformArchive `json:"archives"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type terraformArchive struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Hashes []string `json:"hashes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocalHandler) ServeTerraformIndex(w http.ResponseWriter, r *http.Request, repoName, namespace, typeName string) {
|
||||||
|
prefix := fmt.Sprintf("%s/%s/", namespace, typeName)
|
||||||
|
files, err := h.db.ListLocalFilesByPrefix(r.Context(), repoName, prefix)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := map[string]json.RawMessage{}
|
||||||
|
for _, f := range files {
|
||||||
|
filename := strings.TrimPrefix(f.FilePath, prefix)
|
||||||
|
m := providerZipRe.FindStringSubmatch(filename)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
versions[m[2]] = json.RawMessage(`{}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(versions) == 0 {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(terraformIndex{Versions: versions})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocalHandler) ServeTerraformVersionDoc(w http.ResponseWriter, r *http.Request, repoName, namespace, typeName, version string) {
|
||||||
|
prefix := fmt.Sprintf("%s/%s/terraform-provider-%s_%s_", namespace, typeName, typeName, version)
|
||||||
|
files, err := h.db.ListLocalFilesByPrefix(r.Context(), repoName, prefix)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
archives := map[string]terraformArchive{}
|
||||||
|
for _, f := range files {
|
||||||
|
filename := strings.TrimPrefix(f.FilePath, fmt.Sprintf("%s/%s/", namespace, typeName))
|
||||||
|
m := providerZipRe.FindStringSubmatch(filename)
|
||||||
|
if m == nil || m[2] != version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
platform := m[3] + "_" + m[4]
|
||||||
|
archive := terraformArchive{URL: filename}
|
||||||
|
if f.ContentHash != "" {
|
||||||
|
archive.Hashes = []string{"zh:" + strings.TrimPrefix(f.ContentHash, "sha256:")}
|
||||||
|
}
|
||||||
|
archives[platform] = archive
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(archives) == 0 {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(terraformVersionDoc{Archives: archives})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
|
||||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalFile struct {
|
type LocalFile struct {
|
||||||
@@ -101,45 +99,6 @@ func (db *DB) ListLocalFilesByPrefix(ctx context.Context, repoName, prefix strin
|
|||||||
return files, rows.Err()
|
return files, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) ListLocalFilePackages(ctx context.Context, repoName string) ([]string, error) {
|
|
||||||
rows, err := db.Pool.Query(ctx, `
|
|
||||||
SELECT DISTINCT split_part(file_path, '/', 1)
|
|
||||||
FROM local_files
|
|
||||||
WHERE repo_name = $1
|
|
||||||
ORDER BY 1
|
|
||||||
`, repoName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var packages []string
|
|
||||||
for rows.Next() {
|
|
||||||
var pkg string
|
|
||||||
if err := rows.Scan(&pkg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
packages = append(packages, pkg)
|
|
||||||
}
|
|
||||||
return packages, rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) ListFilesByPrefix(ctx context.Context, repoName, prefix string) ([]provider.FileEntry, error) {
|
|
||||||
files, err := db.ListLocalFilesByPrefix(ctx, repoName, prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]provider.FileEntry, len(files))
|
|
||||||
for i, f := range files {
|
|
||||||
result[i] = provider.FileEntry{FilePath: f.FilePath, ContentHash: f.ContentHash}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) ListPackages(ctx context.Context, repoName string) ([]string, error) {
|
|
||||||
return db.ListLocalFilePackages(ctx, repoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) DeleteLocalFile(ctx context.Context, repoName, filePath string) error {
|
func (db *DB) DeleteLocalFile(ctx context.Context, repoName, filePath string) error {
|
||||||
_, err := db.Pool.Exec(ctx, `DELETE FROM local_files WHERE repo_name = $1 AND file_path = $2`, repoName, filePath)
|
_, err := db.Pool.Exec(ctx, `DELETE FROM local_files WHERE repo_name = $1 AND file_path = $2`, repoName, filePath)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -24,26 +24,6 @@ type Provider interface {
|
|||||||
AuthHeaders(ctx context.Context, remote models.Remote) (http.Header, error)
|
AuthHeaders(ctx context.Context, remote models.Remote) (http.Header, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileEntry struct {
|
|
||||||
FilePath string
|
|
||||||
ContentHash string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileStore interface {
|
|
||||||
ListFilesByPrefix(ctx context.Context, repoName, prefix string) ([]FileEntry, error)
|
|
||||||
ListPackages(ctx context.Context, repoName string) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalUploader interface {
|
|
||||||
ValidateUpload(filePath string) (storagePath, contentType string, err error)
|
|
||||||
UploadResponse(storagePath, contentHash string, sizeBytes int64) map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalIndexer interface {
|
|
||||||
ServeLocalIndex(w http.ResponseWriter, r *http.Request, files FileStore, repoName, path string) bool
|
|
||||||
GenerateLocalIndex(ctx context.Context, files FileStore, repoName, path string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexMerger interface {
|
type IndexMerger interface {
|
||||||
MergeIndexes(members []MemberIndex, proxyBaseURL string) ([]byte, error)
|
MergeIndexes(members []MemberIndex, proxyBaseURL string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package pypi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.unkin.net/unkin/artifactapi/internal/auth"
|
"git.unkin.net/unkin/artifactapi/internal/auth"
|
||||||
@@ -17,9 +14,6 @@ func init() {
|
|||||||
provider.Register(&Provider{})
|
provider.Register(&Provider{})
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*\.(whl|tar\.gz|zip)$`)
|
|
||||||
var normalizeRe = regexp.MustCompile(`[-_.]+`)
|
|
||||||
|
|
||||||
type Provider struct{}
|
type Provider struct{}
|
||||||
|
|
||||||
func (p *Provider) Type() models.PackageType { return models.PackagePyPI }
|
func (p *Provider) Type() models.PackageType { return models.PackagePyPI }
|
||||||
@@ -66,177 +60,3 @@ func (p *Provider) RewriteResponse(body []byte, remote models.Remote, proxyBaseU
|
|||||||
func (p *Provider) AuthHeaders(_ context.Context, remote models.Remote) (http.Header, error) {
|
func (p *Provider) AuthHeaders(_ context.Context, remote models.Remote) (http.Header, error) {
|
||||||
return auth.BasicHeaders(remote), nil
|
return auth.BasicHeaders(remote), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalize(name string) string {
|
|
||||||
return strings.ToLower(normalizeRe.ReplaceAllString(name, "-"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func packageFromWheel(filename string) string {
|
|
||||||
parts := strings.SplitN(filename, "-", 3)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return normalize(parts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func packageFromSdist(filename string) string {
|
|
||||||
name := filename
|
|
||||||
for _, suffix := range []string{".tar.gz", ".zip"} {
|
|
||||||
if strings.HasSuffix(name, suffix) {
|
|
||||||
name = strings.TrimSuffix(name, suffix)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx := strings.LastIndex(name, "-")
|
|
||||||
if idx <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return normalize(name[:idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) ValidateUpload(filePath string) (storagePath, contentType string, err error) {
|
|
||||||
filename := filePath
|
|
||||||
if idx := strings.LastIndex(filePath, "/"); idx >= 0 {
|
|
||||||
filename = filePath[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fileRe.MatchString(filename) {
|
|
||||||
return "", "", fmt.Errorf("filename %q must be a .whl, .tar.gz, or .zip file", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkgName string
|
|
||||||
if strings.HasSuffix(filename, ".whl") {
|
|
||||||
pkgName = packageFromWheel(filename)
|
|
||||||
} else {
|
|
||||||
pkgName = packageFromSdist(filename)
|
|
||||||
}
|
|
||||||
if pkgName == "" {
|
|
||||||
return "", "", fmt.Errorf("cannot parse package name from %q", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
ct := "application/zip"
|
|
||||||
if strings.HasSuffix(filename, ".tar.gz") {
|
|
||||||
ct = "application/gzip"
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkgName + "/" + filename, ct, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) UploadResponse(storagePath, contentHash string, sizeBytes int64) map[string]any {
|
|
||||||
parts := strings.SplitN(storagePath, "/", 2)
|
|
||||||
filename := storagePath
|
|
||||||
if len(parts) == 2 {
|
|
||||||
filename = parts[1]
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"package": parts[0],
|
|
||||||
"filename": filename,
|
|
||||||
"content_hash": contentHash,
|
|
||||||
"size_bytes": sizeBytes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) ServeLocalIndex(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, path string) bool {
|
|
||||||
if path == "simple" || path == "simple/" {
|
|
||||||
p.servePackageList(w, r, files, repoName)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(path, "simple/") {
|
|
||||||
pkg := strings.TrimPrefix(path, "simple/")
|
|
||||||
pkg = strings.TrimSuffix(pkg, "/")
|
|
||||||
if pkg != "" && !strings.Contains(pkg, "/") {
|
|
||||||
p.servePackageFiles(w, r, files, repoName, pkg)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) GenerateLocalIndex(ctx context.Context, files provider.FileStore, repoName, path string) ([]byte, error) {
|
|
||||||
if !strings.HasPrefix(path, "simple/") {
|
|
||||||
return nil, fmt.Errorf("unsupported index path: %q", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkg := strings.TrimPrefix(path, "simple/")
|
|
||||||
pkg = strings.TrimSuffix(pkg, "/")
|
|
||||||
if pkg == "" {
|
|
||||||
return p.generatePackageListHTML(ctx, files, repoName)
|
|
||||||
}
|
|
||||||
return p.generatePackageFilesHTML(ctx, files, repoName, pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) servePackageList(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName string) {
|
|
||||||
body, err := p.generatePackageListHTML(r.Context(), files, repoName)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) servePackageFiles(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, packageName string) {
|
|
||||||
normalized := normalize(packageName)
|
|
||||||
prefix := normalized + "/"
|
|
||||||
entries, err := files.ListFilesByPrefix(r.Context(), repoName, prefix)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("<!DOCTYPE html>\n<html><body>\n")
|
|
||||||
for _, f := range entries {
|
|
||||||
filename := strings.TrimPrefix(f.FilePath, normalized+"/")
|
|
||||||
hash := strings.TrimPrefix(f.ContentHash, "sha256:")
|
|
||||||
fmt.Fprintf(&b, "<a href=\"../../%s/%s#sha256=%s\">%s</a>\n",
|
|
||||||
normalized, filename, hash, filename)
|
|
||||||
}
|
|
||||||
b.WriteString("</body></html>\n")
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
io.WriteString(w, b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) generatePackageListHTML(ctx context.Context, files provider.FileStore, repoName string) ([]byte, error) {
|
|
||||||
packages, err := files.ListPackages(ctx, repoName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("<!DOCTYPE html>\n<html><body>\n")
|
|
||||||
for _, pkg := range packages {
|
|
||||||
fmt.Fprintf(&b, "<a href=\"%s/\">%s</a>\n", pkg, pkg)
|
|
||||||
}
|
|
||||||
b.WriteString("</body></html>\n")
|
|
||||||
return []byte(b.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) generatePackageFilesHTML(ctx context.Context, files provider.FileStore, repoName, packageName string) ([]byte, error) {
|
|
||||||
normalized := normalize(packageName)
|
|
||||||
prefix := normalized + "/"
|
|
||||||
entries, err := files.ListFilesByPrefix(ctx, repoName, prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("<!DOCTYPE html>\n<html><body>\n")
|
|
||||||
for _, f := range entries {
|
|
||||||
filename := strings.TrimPrefix(f.FilePath, normalized+"/")
|
|
||||||
hash := strings.TrimPrefix(f.ContentHash, "sha256:")
|
|
||||||
fmt.Fprintf(&b, "<a href=\"%s/%s#sha256=%s\">%s</a>\n",
|
|
||||||
normalized, filename, hash, filename)
|
|
||||||
}
|
|
||||||
b.WriteString("</body></html>\n")
|
|
||||||
return []byte(b.String()), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package terraform
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -20,12 +19,6 @@ func init() {
|
|||||||
|
|
||||||
var versionsRe = regexp.MustCompile(`[^/]+/[^/]+/versions$`)
|
var versionsRe = regexp.MustCompile(`[^/]+/[^/]+/versions$`)
|
||||||
|
|
||||||
var providerZipRe = regexp.MustCompile(
|
|
||||||
`^terraform-provider-([a-zA-Z0-9_-]+)_([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?)_([a-z0-9]+)_([a-z0-9]+)\.zip$`,
|
|
||||||
)
|
|
||||||
|
|
||||||
var semverRe = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?$`)
|
|
||||||
|
|
||||||
type Provider struct{}
|
type Provider struct{}
|
||||||
|
|
||||||
func (p *Provider) Type() models.PackageType { return models.PackageTerraform }
|
func (p *Provider) Type() models.PackageType { return models.PackageTerraform }
|
||||||
@@ -93,145 +86,3 @@ func rewriteDownloadURL(originalURL, releasesRemote, proxyBaseURL string) string
|
|||||||
func (p *Provider) AuthHeaders(_ context.Context, remote models.Remote) (http.Header, error) {
|
func (p *Provider) AuthHeaders(_ context.Context, remote models.Remote) (http.Header, error) {
|
||||||
return auth.BasicHeaders(remote), nil
|
return auth.BasicHeaders(remote), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ValidateUpload(filePath string) (storagePath, contentType string, err error) {
|
|
||||||
parts := strings.Split(filePath, "/")
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return "", "", fmt.Errorf("path must be {namespace}/{type}/{filename}.zip")
|
|
||||||
}
|
|
||||||
namespace, typeName, filename := parts[0], parts[1], parts[2]
|
|
||||||
|
|
||||||
m := providerZipRe.FindStringSubmatch(filename)
|
|
||||||
if m == nil {
|
|
||||||
return "", "", fmt.Errorf("filename %q does not match terraform-provider-{type}_{version}_{os}_{arch}.zip", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m[1] != typeName {
|
|
||||||
return "", "", fmt.Errorf("provider type in filename %q does not match path type %q", m[1], typeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s/%s", namespace, typeName, filename), "application/zip", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) UploadResponse(storagePath, contentHash string, sizeBytes int64) map[string]any {
|
|
||||||
parts := strings.Split(storagePath, "/")
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return map[string]any{"path": storagePath, "content_hash": contentHash, "size_bytes": sizeBytes}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := providerZipRe.FindStringSubmatch(parts[2])
|
|
||||||
if m == nil {
|
|
||||||
return map[string]any{"path": storagePath, "content_hash": contentHash, "size_bytes": sizeBytes}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"namespace": parts[0],
|
|
||||||
"type": parts[1],
|
|
||||||
"version": m[2],
|
|
||||||
"os": m[3],
|
|
||||||
"arch": m[4],
|
|
||||||
"content_hash": contentHash,
|
|
||||||
"size_bytes": sizeBytes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type terraformIndex struct {
|
|
||||||
Versions map[string]json.RawMessage `json:"versions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type terraformVersionDoc struct {
|
|
||||||
Archives map[string]terraformArchive `json:"archives"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type terraformArchive struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
Hashes []string `json:"hashes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) ServeLocalIndex(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, path string) bool {
|
|
||||||
parts := strings.Split(path, "/")
|
|
||||||
if len(parts) < 3 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace, typeName := parts[0], parts[1]
|
|
||||||
tail := parts[2]
|
|
||||||
|
|
||||||
if tail == "index.json" {
|
|
||||||
p.serveIndex(w, r, files, repoName, namespace, typeName)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(tail, ".json") {
|
|
||||||
version := strings.TrimSuffix(tail, ".json")
|
|
||||||
if semverRe.MatchString(version) {
|
|
||||||
p.serveVersionDoc(w, r, files, repoName, namespace, typeName, version)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) GenerateLocalIndex(ctx context.Context, files provider.FileStore, repoName, path string) ([]byte, error) {
|
|
||||||
return nil, fmt.Errorf("terraform local index generation for virtual repos not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) serveIndex(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, namespace, typeName string) {
|
|
||||||
prefix := fmt.Sprintf("%s/%s/", namespace, typeName)
|
|
||||||
entries, err := files.ListFilesByPrefix(r.Context(), repoName, prefix)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
versions := map[string]json.RawMessage{}
|
|
||||||
for _, f := range entries {
|
|
||||||
filename := strings.TrimPrefix(f.FilePath, prefix)
|
|
||||||
m := providerZipRe.FindStringSubmatch(filename)
|
|
||||||
if m == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
versions[m[2]] = json.RawMessage(`{}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(versions) == 0 {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(terraformIndex{Versions: versions})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) serveVersionDoc(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, namespace, typeName, version string) {
|
|
||||||
prefix := fmt.Sprintf("%s/%s/terraform-provider-%s_%s_", namespace, typeName, typeName, version)
|
|
||||||
entries, err := files.ListFilesByPrefix(r.Context(), repoName, prefix)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
archives := map[string]terraformArchive{}
|
|
||||||
for _, f := range entries {
|
|
||||||
filename := strings.TrimPrefix(f.FilePath, fmt.Sprintf("%s/%s/", namespace, typeName))
|
|
||||||
m := providerZipRe.FindStringSubmatch(filename)
|
|
||||||
if m == nil || m[2] != version {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
platform := m[3] + "_" + m[4]
|
|
||||||
archive := terraformArchive{URL: filename}
|
|
||||||
if f.ContentHash != "" {
|
|
||||||
archive.Hashes = []string{"zh:" + strings.TrimPrefix(f.ContentHash, "sha256:")}
|
|
||||||
}
|
|
||||||
archives[platform] = archive
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(archives) == 0 {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(terraformVersionDoc{Archives: archives})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type Server struct {
|
|||||||
store *storage.S3
|
store *storage.S3
|
||||||
engine *proxy.Engine
|
engine *proxy.Engine
|
||||||
virtEngine *virtual.Engine
|
virtEngine *virtual.Engine
|
||||||
localHandler *v2.LocalHandler
|
|
||||||
gc *gc.Collector
|
gc *gc.Collector
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ func New(cfg *config.Config) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
engine := proxy.NewEngine(db, redis, s3)
|
engine := proxy.NewEngine(db, redis, s3)
|
||||||
localHandler := v2.NewLocalHandler(db, s3)
|
|
||||||
virtEngine := virtual.NewEngine(db, engine)
|
virtEngine := virtual.NewEngine(db, engine)
|
||||||
collector := gc.New(db, s3, 1*time.Hour)
|
collector := gc.New(db, s3, 1*time.Hour)
|
||||||
|
|
||||||
@@ -73,7 +71,6 @@ func New(cfg *config.Config) (*Server, error) {
|
|||||||
store: s3,
|
store: s3,
|
||||||
engine: engine,
|
engine: engine,
|
||||||
virtEngine: virtEngine,
|
virtEngine: virtEngine,
|
||||||
localHandler: localHandler,
|
|
||||||
gc: collector,
|
gc: collector,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +91,9 @@ func (s *Server) routes() chi.Router {
|
|||||||
r.Get("/health", s.handleHealth)
|
r.Get("/health", s.handleHealth)
|
||||||
r.Get("/", s.handleRoot)
|
r.Get("/", s.handleRoot)
|
||||||
|
|
||||||
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
|
localHandler := v2.NewLocalHandler(s.db, s.store)
|
||||||
|
|
||||||
|
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, localHandler)
|
||||||
r.Mount("/api/v1", proxyHandler.Routes())
|
r.Mount("/api/v1", proxyHandler.Routes())
|
||||||
|
|
||||||
remotesHandler := v2.NewRemotesHandler(s.db)
|
remotesHandler := v2.NewRemotesHandler(s.db)
|
||||||
@@ -119,9 +118,9 @@ func (s *Server) routes() chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/remotes/{name}/files", func(r chi.Router) {
|
r.Route("/remotes/{name}/files", func(r chi.Router) {
|
||||||
r.Put("/*", s.localHandler.Routes().ServeHTTP)
|
r.Put("/*", localHandler.Routes().ServeHTTP)
|
||||||
r.Get("/*", s.localHandler.Routes().ServeHTTP)
|
r.Get("/*", localHandler.Routes().ServeHTTP)
|
||||||
r.Delete("/*", s.localHandler.Routes().ServeHTTP)
|
r.Delete("/*", localHandler.Routes().ServeHTTP)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -73,16 +73,6 @@ func (e *Engine) fetchMemberIndexes(ctx context.Context, virt models.Virtual, pa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if remote.RepoType == models.RepoTypeLocal {
|
|
||||||
body, err := e.fetchLocalIndex(ctx, *remote, path)
|
|
||||||
if err != nil {
|
|
||||||
results[idx] = result{err: fmt.Errorf("local index %q: %w", name, err)}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results[idx] = result{index: MemberIndex{RemoteName: name, RepoType: remote.RepoType, Body: body}}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prov, err := provider.Get(remote.PackageType)
|
prov, err := provider.Get(remote.PackageType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results[idx] = result{err: fmt.Errorf("provider %q: %w", remote.PackageType, err)}
|
results[idx] = result{err: fmt.Errorf("provider %q: %w", remote.PackageType, err)}
|
||||||
@@ -102,7 +92,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, Body: body}}
|
||||||
}(i, memberName)
|
}(i, memberName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,17 +109,3 @@ func (e *Engine) fetchMemberIndexes(ctx context.Context, virt models.Virtual, pa
|
|||||||
|
|
||||||
return members, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) fetchLocalIndex(ctx context.Context, remote models.Remote, path string) ([]byte, error) {
|
|
||||||
prov, err := provider.Get(remote.PackageType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("no provider for %q: %w", remote.PackageType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
indexer, ok := prov.(provider.LocalIndexer)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("provider %q does not support local index generation", remote.PackageType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexer.GenerateLocalIndex(ctx, e.db, remote.Name, path)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
type MemberIndex struct {
|
type MemberIndex struct {
|
||||||
RemoteName string
|
RemoteName string
|
||||||
RepoType models.RepoType
|
|
||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,13 +36,8 @@ func (m *PyPIMerger) MergeIndexes(members []MemberIndex, proxyBaseURL string) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if proxyBaseURL != "" && href != "" {
|
if proxyBaseURL != "" && href != "" {
|
||||||
routePrefix := "remote"
|
href = fmt.Sprintf("%s/api/v1/remote/%s/%s",
|
||||||
if member.RepoType == "local" {
|
|
||||||
routePrefix = "local"
|
|
||||||
}
|
|
||||||
href = fmt.Sprintf("%s/api/v1/%s/%s/%s",
|
|
||||||
strings.TrimRight(proxyBaseURL, "/"),
|
strings.TrimRight(proxyBaseURL, "/"),
|
||||||
routePrefix,
|
|
||||||
member.RemoteName,
|
member.RemoteName,
|
||||||
strings.TrimLeft(href, "/"))
|
strings.TrimLeft(href, "/"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user