diff --git a/Dockerfile.download b/Dockerfile.download new file mode 100644 index 00000000..0a645c46 --- /dev/null +++ b/Dockerfile.download @@ -0,0 +1,36 @@ +# This Dockerfile is used to cross-build the kubectl-oadp binaries for all platforms +# It also builds a Go server that serves the binaries for download + +FROM golang:1.24 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + +# Build everything, create tarballs, and clean up in one layer to reduce size +RUN make release-build && \ + CGO_ENABLED=0 go build -o download-server ./cmd/downloads/server.go && \ + mkdir -p /archives && \ + for binary in kubectl-oadp-*; do \ + archive_name=$(echo "$binary" | sed 's/\.exe$//' ).tar.gz; \ + tar -czf "/archives/$archive_name" "$binary"; \ + echo "Created /archives/$archive_name"; \ + done && \ + go clean -cache -modcache -testcache && \ + rm -rf /root/.cache/go-build && \ + rm -rf /go/pkg + +FROM registry.access.redhat.com/ubi8/ubi-minimal:latest + +# Only copy compressed tarballs (much smaller than raw binaries) +COPY --from=builder /archives /archives + +# Copy the download server +COPY --from=builder /app/download-server /usr/local/bin/download-server + +EXPOSE 8080 + +CMD ["/usr/local/bin/download-server"] diff --git a/cmd/downloads/server.go b/cmd/downloads/server.go new file mode 100644 index 00000000..a8642994 --- /dev/null +++ b/cmd/downloads/server.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" +) + +const ( + archiveDir = "/archives" + port = "8080" +) + +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)) + + http.HandleFunc("/", listBinaries) + http.HandleFunc("/download/", downloadBinary) + + log.Printf("Starting server on port %s", port) + log.Printf("Serving archives from %s", archiveDir) + + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func listBinaries(w http.ResponseWriter, r *http.Request) { + files, err := filepath.Glob(filepath.Join(archiveDir, "*.tar.gz")) + if err != nil { + http.Error(w, "Error listing archives", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, "kubectl-oadp Downloads") + fmt.Fprintf(w, "

kubectl-oadp Binary Downloads

") + fmt.Fprintf(w, "

Download pre-built binaries for your platform:

") + fmt.Fprintf(w, "

Installation:

") + fmt.Fprintf(w, "
tar -xzf kubectl-oadp-<platform>.tar.gz\n")
+	fmt.Fprintf(w, "chmod +x kubectl-oadp\n")
+	fmt.Fprintf(w, "sudo mv kubectl-oadp /usr/local/bin/
") + fmt.Fprintf(w, "") +} + +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 + } + + 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 + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + w.Header().Set("Content-Type", "application/gzip") + + http.ServeFile(w, r, filePath) + log.Printf("Downloaded: %s from %s", filename, r.RemoteAddr) +}