omni/internal/backend/factory/factory.go
Artem Chernyshev ed946b30a6
feat: display OMNI_ENDPOINT in the service account creation UI
Fixes: https://github.com/siderolabs/omni/issues/858

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2025-01-29 15:27:36 +03:00

197 lines
4.7 KiB
Go

// Copyright (c) 2025 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
// Package factory provides the code to call Talos image factory.
package factory
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/siderolabs/omni/client/pkg/constants"
"github.com/siderolabs/omni/client/pkg/omni/resources"
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
"github.com/siderolabs/omni/internal/pkg/config"
)
// Handler of image requests.
type Handler struct {
State state.State
Logger *zap.Logger
}
func setContentHeaders(w http.ResponseWriter, contentType, filename string) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
}
func httpNotFound(w http.ResponseWriter) {
http.Error(w, "Not found", http.StatusNotFound)
}
func (handler *Handler) handleError(msg string, w http.ResponseWriter, err error) {
handler.Logger.Error(msg, zap.Error(err))
switch status.Code(err) { //nolint:exhaustive
case codes.Unauthenticated:
http.Error(w, "Unauthorized", http.StatusUnauthorized)
case codes.PermissionDenied:
http.Error(w, "Permission denied", http.StatusForbidden)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// ServeHTTP handles image requests.
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
io.Copy(io.Discard, r.Body) //nolint:errcheck
r.Body.Close() //nolint:errcheck
switch r.Method {
case http.MethodGet, http.MethodHead:
// supported
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
params, err := parseRequest(r, handler.State)
if err != nil {
if errors.Is(err, errNotFound) {
httpNotFound(w)
return
}
handler.handleError("failed to parse request", w, err)
return
}
handler.Logger.Info("proxy request", zap.String("url", params.ProxyURL))
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, params.ProxyURL, nil)
if err != nil {
handler.handleError("failed to call image factory", w, err)
return
}
setContentHeaders(w, params.ContentType, params.DestinationFilename)
for name, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(name, value)
}
}
client := http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
handler.handleError("failed to call image factory", w, err)
return
}
defer resp.Body.Close() //nolint:errcheck
for name, values := range resp.Header {
// ignore content disposition header from the image factory
// as we override it here
if name == "Content-Disposition" {
continue
}
for _, value := range values {
w.Header().Add(name, value)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body) //nolint:errcheck
}
var errNotFound = errors.New("not found")
// ProxyParams is exposed for the unit tests.
type ProxyParams struct {
ProxyURL string
ContentType string
DestinationFilename string
}
func parseRequest(r *http.Request, st state.State) (*ProxyParams, error) {
segments := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(segments) < 4 {
return nil, errNotFound
}
if segments[0] != "image" {
return nil, errNotFound
}
ctx := r.Context()
media, err := safe.ReaderGet[*omni.InstallationMedia](ctx, st, resource.NewMetadata(
resources.EphemeralNamespace, omni.InstallationMediaType, segments[3], resource.VersionUndefined,
))
if err != nil {
if state.IsNotFoundError(err) {
return nil, errNotFound
}
return nil, err
}
secureBoot := r.URL.Query().Get(constants.SecureBoot) == "true"
talosVersion := segments[2]
srcFilename := talosVersion
if secureBoot {
srcFilename += "-secureboot"
}
p := &ProxyParams{
ContentType: media.TypedSpec().Value.ContentType,
DestinationFilename: fmt.Sprintf("%s-%s.%s", media.TypedSpec().Value.DestFilePrefix, srcFilename, media.TypedSpec().Value.Extension),
}
proxyURL, err := url.Parse(config.Config.ImageFactoryBaseURL)
if err != nil {
return nil, err
}
filename := media.TypedSpec().Value.GenerateFilename(!quirks.New(talosVersion).SupportsOverlay(), secureBoot, true)
// strip the last element from the URL
// replace it with the generated filename
segments = append(segments[:3], filename)
proxyURL = proxyURL.JoinPath(segments...)
p.ProxyURL = proxyURL.String()
return p, nil
}