mirror of
https://github.com/siderolabs/image-factory.git
synced 2025-12-05 17:41:32 +01:00
Fixes #18 The intention is to provide educational frontend so that people can get comfortable using the Image Service by building the appropriate links. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
143 lines
5.1 KiB
Go
143 lines
5.1 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Package http implements the HTTP frontend.
|
|
package http
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/siderolabs/gen/ensure"
|
|
"github.com/siderolabs/gen/xerrors"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/sync/singleflight"
|
|
|
|
"github.com/siderolabs/image-service/internal/artifacts"
|
|
"github.com/siderolabs/image-service/internal/asset"
|
|
flvr "github.com/siderolabs/image-service/internal/flavor"
|
|
"github.com/siderolabs/image-service/internal/flavor/storage"
|
|
"github.com/siderolabs/image-service/internal/profile"
|
|
"github.com/siderolabs/image-service/pkg/flavor"
|
|
)
|
|
|
|
// Frontend is the HTTP frontend.
|
|
type Frontend struct {
|
|
router *httprouter.Router
|
|
flavorService *flvr.Service
|
|
assetBuilder *asset.Builder
|
|
artifactsManager *artifacts.Manager
|
|
logger *zap.Logger
|
|
puller *remote.Puller
|
|
pusher *remote.Pusher
|
|
sf singleflight.Group
|
|
options Options
|
|
}
|
|
|
|
// Options configures the HTTP frontend.
|
|
type Options struct {
|
|
ExternalURL *url.URL
|
|
|
|
InstallerInternalRepository name.Repository
|
|
InstallerExternalRepository name.Repository
|
|
|
|
RemoteOptions []remote.Option
|
|
}
|
|
|
|
// NewFrontend creates a new HTTP frontend.
|
|
func NewFrontend(logger *zap.Logger, flavorService *flvr.Service, assetBuilder *asset.Builder, artifactsManager *artifacts.Manager, opts Options) (*Frontend, error) {
|
|
frontend := &Frontend{
|
|
router: httprouter.New(),
|
|
flavorService: flavorService,
|
|
assetBuilder: assetBuilder,
|
|
artifactsManager: artifactsManager,
|
|
logger: logger.With(zap.String("frontend", "http")),
|
|
options: opts,
|
|
}
|
|
|
|
var err error
|
|
|
|
frontend.puller, err = remote.NewPuller(opts.RemoteOptions...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create puller: %w", err)
|
|
}
|
|
|
|
frontend.pusher, err = remote.NewPusher(opts.RemoteOptions...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create pusher: %w", err)
|
|
}
|
|
|
|
// images
|
|
frontend.router.GET("/image/:flavor/:version/:path", frontend.wrapper(frontend.handleImage))
|
|
frontend.router.HEAD("/image/:flavor/:version/:path", frontend.wrapper(frontend.handleImage))
|
|
|
|
// PXE
|
|
frontend.router.GET("/pxe/:flavor/:version/:path", frontend.wrapper(frontend.handlePXE))
|
|
|
|
// registry
|
|
frontend.router.GET("/v2", frontend.wrapper(frontend.handleHealth))
|
|
frontend.router.HEAD("/v2", frontend.wrapper(frontend.handleHealth))
|
|
frontend.router.GET("/healthz", frontend.wrapper(frontend.handleHealth))
|
|
frontend.router.HEAD("/healthz", frontend.wrapper(frontend.handleHealth))
|
|
frontend.router.GET("/v2/:image/:flavor/blobs/:digest", frontend.wrapper(frontend.handleBlob))
|
|
frontend.router.HEAD("/v2/:image/:flavor/blobs/:digest", frontend.wrapper(frontend.handleBlob))
|
|
frontend.router.GET("/v2/:image/:flavor/manifests/:tag", frontend.wrapper(frontend.handleManifest))
|
|
frontend.router.HEAD("/v2/:image/:flavor/manifests/:tag", frontend.wrapper(frontend.handleManifest))
|
|
|
|
// flavor
|
|
frontend.router.POST("/flavors", frontend.wrapper(frontend.handleFlavorCreate))
|
|
|
|
// meta
|
|
frontend.router.GET("/versions", frontend.wrapper(frontend.handleVersions))
|
|
frontend.router.GET("/version/:version/extensions/official", frontend.wrapper(frontend.handleOfficialExtensions))
|
|
|
|
// UI
|
|
frontend.router.GET("/", frontend.wrapper(frontend.handleUI))
|
|
frontend.router.HEAD("/", frontend.wrapper(frontend.handleUI))
|
|
frontend.router.GET("/ui/flavor-config", frontend.wrapper(frontend.handleUIFlavorConfig))
|
|
frontend.router.GET("/ui/versions", frontend.wrapper(frontend.handleUIVersions))
|
|
frontend.router.POST("/ui/flavors", frontend.wrapper(frontend.handleUIFlavors))
|
|
frontend.router.ServeFiles("/css/*filepath", http.FS(ensure.Value(fs.Sub(cssFS, "css"))))
|
|
frontend.router.ServeFiles("/favicons/*filepath", http.FS(ensure.Value(fs.Sub(faviconsFS, "favicons"))))
|
|
frontend.router.ServeFiles("/js/*filepath", http.FS(ensure.Value(fs.Sub(jsFS, "js"))))
|
|
|
|
return frontend, nil
|
|
}
|
|
|
|
// Handler returns the HTTP handler.
|
|
func (f *Frontend) Handler() http.Handler {
|
|
return f.router
|
|
}
|
|
|
|
func (f *Frontend) wrapper(h func(ctx context.Context, w http.ResponseWriter, r *http.Request, p httprouter.Params) error) httprouter.Handle {
|
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
ctx := r.Context()
|
|
|
|
err := h(ctx, w, r, p)
|
|
|
|
f.logger.Info("request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Error(err))
|
|
|
|
switch {
|
|
case err == nil:
|
|
// happy case
|
|
case xerrors.TagIs[storage.ErrNotFoundTag](err):
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
case xerrors.TagIs[profile.InvalidErrorTag](err),
|
|
xerrors.TagIs[flavor.InvalidErrorTag](err):
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
case errors.Is(err, context.Canceled):
|
|
// client closed connection
|
|
default:
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|