diff --git a/Containerfile.download b/Containerfile.download index d350b84..613636b 100644 --- a/Containerfile.download +++ b/Containerfile.download @@ -15,15 +15,13 @@ COPY . . RUN make release-archives && \ mkdir -p /archives && \ - mv *.tar.gz /archives/ + mv *.tar.gz *.sha256 /archives/ && \ + rm -rf /root/.cache/go-build /tmp/* release-build/ # Build the download server for the TARGET platform (the arch this container will run on) -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o download-server ./cmd/downloads/server.go - -# Clean up to reduce layer size -RUN go clean -cache -modcache -testcache && \ - rm -rf /root/.cache/go-build && \ - rm -rf /go/pkg +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o download-server ./cmd/downloads/ && \ + go clean -cache -modcache -testcache && \ + rm -rf /root/.cache/go-build /go/pkg FROM registry.access.redhat.com/ubi9/ubi-minimal:latest diff --git a/cmd/downloads/server.go b/cmd/downloads/server.go index 47ac9be..b766f95 100644 --- a/cmd/downloads/server.go +++ b/cmd/downloads/server.go @@ -1,7 +1,10 @@ package main import ( + "embed" "fmt" + "html/template" + "io/fs" "log" "net/http" "os" @@ -9,19 +12,45 @@ import ( "strings" ) -const ( - archiveDir = "/archives" - port = "8080" +//go:embed templates/*.html +var templateFS embed.FS + +//go:embed static/* +var staticFS embed.FS + +var ( + archiveDir = getEnv("ARCHIVE_DIR", "/archives") + port = getEnv("PORT", "8080") + pageTemplate = template.Must(template.ParseFS(templateFS, "templates/index.html")) ) +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +type archiveFile struct { + Name string + Size float64 + OS string + Arch string + Checksum string +} + func main() { - // Verify archives exist files, err := filepath.Glob(filepath.Join(archiveDir, "*.tar.gz")) if err != nil || len(files) == 0 { log.Fatal("No archives found in ", archiveDir) } log.Printf("Found %d archives", len(files)) + staticContent, err := fs.Sub(staticFS, "static") + if err != nil { + log.Fatal("Failed to load static files: ", err) + } + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent)))) http.HandleFunc("/", listBinaries) http.HandleFunc("/download/", downloadBinary) @@ -40,32 +69,65 @@ func listBinaries(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, "
Download pre-built binaries for your platform:
tar -xzf kubectl-oadp_*.tar.gz\n") - fmt.Fprintf(w, "chmod +x kubectl-oadp\n") - fmt.Fprintf(w, "sudo mv kubectl-oadp /usr/local/bin/") + data := struct { + LinuxFiles []archiveFile + DarwinFiles []archiveFile + WindowsFiles []archiveFile + }{linuxFiles, darwinFiles, windowsFiles} + + w.Header().Set("Content-Type", "text/html") + if err := pageTemplate.Execute(w, data); err != nil { + log.Printf("Template error: %v", err) + } +} + +func readChecksum(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + fields := strings.Fields(string(data)) + if len(fields) > 0 { + return fields[0] + } + return "" +} + +func parsePlatform(filename string) (string, string) { + name := strings.TrimSuffix(filename, ".tar.gz") + parts := strings.Split(name, "_") + if len(parts) >= 3 { + return parts[len(parts)-2], parts[len(parts)-1] + } + return "unknown", "unknown" } func downloadBinary(w http.ResponseWriter, r *http.Request) { filename := filepath.Base(r.URL.Path[len("/download/"):]) - // Security: ensure filename is just the archive name if filepath.Dir(filename) != "." || !strings.HasSuffix(filename, ".tar.gz") { http.Error(w, "Invalid filename", http.StatusBadRequest) return @@ -73,7 +135,6 @@ func downloadBinary(w http.ResponseWriter, r *http.Request) { filePath := filepath.Join(archiveDir, filename) - // Verify file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { http.Error(w, "Archive not found", http.StatusNotFound) return diff --git a/cmd/downloads/static/style.css b/cmd/downloads/static/style.css new file mode 100644 index 0000000..4dae33e --- /dev/null +++ b/cmd/downloads/static/style.css @@ -0,0 +1,259 @@ +/* Red Hat Brand Colors (https://ux.redhat.com/foundations/color/) */ +:root { + --rh-red: #ee0000; + --rh-red-dark: #a60000; + --rh-black: #151515; + --rh-white: #ffffff; + --rh-gray-10: #f2f2f2; + --rh-gray-20: #e0e0e0; + --rh-gray-30: #c7c7c7; + --rh-gray-50: #707070; + --rh-gray-60: #4d4d4d; + --rh-gray-90: #1f1f1f; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: "Red Hat Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--rh-gray-10); + color: var(--rh-black); + min-height: 100vh; +} + +.header { + background: var(--rh-red); + padding: 2.5rem 0; + color: var(--rh-white); +} + +.container { + max-width: 860px; + margin: 0 auto; + padding: 0 1.5rem; +} + +.logo { + font-size: 1.6rem; + font-weight: 700; +} + +.logo-link { + color: var(--rh-white); + text-decoration: none; +} + +.logo-link:hover { text-decoration: underline; } + +.subtitle { + color: rgba(255,255,255,0.8); + margin-top: 0.4rem; + font-size: 0.95rem; +} + +.binary-note { + margin-top: 0.5rem; + font-size: 0.8rem; + color: rgba(255,255,255,0.65); +} + +.binary-note code { + background: rgba(255,255,255,0.15); + padding: 0.15rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.8rem; +} + +.content { + padding: 2rem 0; +} + +.section { + background: var(--rh-white); + border: 1px solid var(--rh-gray-20); + border-radius: 0.75rem; + overflow: hidden; + margin-bottom: 1.5rem; +} + +.section-header { + padding: 0.85rem 1.25rem; + background: var(--rh-gray-10); + border-bottom: 1px solid var(--rh-gray-20); + font-weight: 600; + font-size: 0.9rem; + color: var(--rh-black); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.section-header .os-icon { + font-size: 1.4rem; +} + +.section-header .os-logo { + height: 1.6rem; + width: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 0.6rem 1.25rem; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--rh-gray-50); + border-bottom: 1px solid var(--rh-gray-20); + background: var(--rh-gray-10); +} + +td { + padding: 0.85rem 1.25rem; + border-bottom: 1px solid var(--rh-gray-20); + font-size: 0.9rem; +} + +tr:last-child td { border-bottom: none; } +tr:hover td { background: var(--rh-gray-10); } + +.arch-badge { + display: inline-block; + background: var(--rh-gray-10); + color: var(--rh-gray-60); + padding: 0.2rem 0.6rem; + border-radius: 0.35rem; + font-size: 0.8rem; + font-weight: 500; + font-family: "Red Hat Mono", "SF Mono", Consolas, monospace; + border: 1px solid var(--rh-gray-20); +} + +.size { + color: var(--rh-gray-50); + font-size: 0.85rem; +} + +.checksum { + font-family: "Red Hat Mono", "SF Mono", Consolas, monospace; + font-size: 0.7rem; + color: var(--rh-gray-50); + word-break: break-all; + max-width: 220px; + cursor: pointer; + position: relative; +} + +.checksum:hover { color: var(--rh-gray-60); } + +.checksum .copy-hint { + display: none; + position: absolute; + top: -1.6rem; + left: 0; + background: var(--rh-black); + color: var(--rh-white); + padding: 0.15rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.65rem; + white-space: nowrap; +} + +.checksum:hover .copy-hint { display: block; } + +.download-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background: var(--rh-red); + color: var(--rh-white); + padding: 0.45rem 0.9rem; + border-radius: 0.4rem; + text-decoration: none; + font-size: 0.82rem; + font-weight: 500; + transition: background 0.15s; +} + +.download-btn:hover { background: var(--rh-red-dark); } + +.install-section { + margin-top: 0.5rem; +} + +.install-section h3 { + font-size: 1rem; + font-weight: 600; + color: var(--rh-black); + margin-bottom: 0.75rem; +} + +.code-block { + background: var(--rh-gray-90); + border-radius: 0.5rem; + padding: 0.75rem 0; + overflow-x: auto; + font-family: "Red Hat Mono", "SF Mono", Consolas, monospace; + font-size: 0.85rem; + line-height: 1.4; +} + +.code-line { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.3rem 1.25rem; +} + +.code-line:hover { background: rgba(255,255,255,0.04); } + +.code-line .comment { + color: var(--rh-gray-50); + padding-top: 0.4rem; +} + +.code-line .cmd { + color: var(--rh-gray-20); + font-family: inherit; +} + +.copy-btn { + background: transparent; + border: 1px solid var(--rh-gray-60); + color: var(--rh-gray-50); + padding: 0.2rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.7rem; + font-family: "Red Hat Text", -apple-system, sans-serif; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; + flex-shrink: 0; + margin-left: 1rem; +} + +.copy-btn:hover { + border-color: var(--rh-gray-30); + color: var(--rh-gray-20); + background: rgba(255,255,255,0.08); +} + +.footer { + text-align: center; + padding: 2rem 0; + color: var(--rh-gray-50); + font-size: 0.8rem; + border-top: 1px solid var(--rh-gray-20); +} + +.footer a { + color: var(--rh-red); + text-decoration: none; +} + +.footer a:hover { text-decoration: underline; } diff --git a/cmd/downloads/templates/index.html b/cmd/downloads/templates/index.html new file mode 100644 index 0000000..a552ad9 --- /dev/null +++ b/cmd/downloads/templates/index.html @@ -0,0 +1,149 @@ + + + + + +
OpenShift API for Data Protection — Command Line Interface
+CLI for managing backups and restores with the OADP Operator — works as a kubectl / oc plugin
| Binary | +Architecture | +Size | +SHA256 | ++ |
|---|---|---|---|---|
| {{.Name}} | +{{.Arch}} | +{{printf "%.2f" .Size}} MB | +{{if .Checksum}}Click to copy{{slice .Checksum 0 16}}...{{else}}—{{end}} | +Download | +
| Binary | +Architecture | +Size | +SHA256 | ++ |
|---|---|---|---|---|
| {{.Name}} | +{{.Arch}} | +{{printf "%.2f" .Size}} MB | +{{if .Checksum}}Click to copy{{slice .Checksum 0 16}}...{{else}}—{{end}} | +Download | +
| Binary | +Architecture | +Size | +SHA256 | ++ |
|---|---|---|---|---|
| {{.Name}} | +{{.Arch}} | +{{printf "%.2f" .Size}} MB | +{{if .Checksum}}Click to copy{{slice .Checksum 0 16}}...{{else}}—{{end}} | +Download | +
tar -xzf kubectl-oadp_*.tar.gz
+
+ chmod +x kubectl-oadp
+
+ sudo mv kubectl-oadp /usr/local/bin/
+
+ oc oadp --help
+
+