mirror of
https://github.com/siderolabs/omni.git
synced 2025-08-07 01:56:59 +02:00
Some checks are pending
default / default (push) Waiting to run
default / e2e-backups (push) Blocked by required conditions
default / e2e-forced-removal (push) Blocked by required conditions
default / e2e-scaling (push) Blocked by required conditions
default / e2e-short (push) Blocked by required conditions
default / e2e-short-secureboot (push) Blocked by required conditions
default / e2e-templates (push) Blocked by required conditions
default / e2e-upgrades (push) Blocked by required conditions
default / e2e-workload-proxy (push) Blocked by required conditions
Omni can now be configured via a config file instead of the command line flags. The flags `--config-path` will now read the config provided in the YAML format. The config structure was completely changed. It was not public before, so it's fine to ignore backward compatibility. The command line flags were not changed. Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
207 lines
5.0 KiB
Go
207 lines
5.0 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
|
|
config *config.Registries
|
|
}
|
|
|
|
// NewHandler creates a new factory proxy handler.
|
|
func NewHandler(state state.State, logger *zap.Logger, config *config.Registries) *Handler {
|
|
return &Handler{
|
|
state: state,
|
|
logger: logger,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
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, handler.config)
|
|
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, config *config.Registries) (*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.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
|
|
}
|