Commit 6623508b authored by Jacob Vosmaer's avatar Jacob Vosmaer

Merge branch '298-set-img-resizing-const-via-env-vars' into 'master'

Allow configure Img Resizing params

See merge request gitlab-org/gitlab-workhorse!615
parents 926eb55f d6e2712f
---
title: Allow configure image resizing params
merge_request: 615
author:
type: changed
...@@ -11,3 +11,7 @@ URL = "unix:/home/git/gitlab/redis/redis.socket" ...@@ -11,3 +11,7 @@ URL = "unix:/home/git/gitlab/redis/redis.socket"
[object_store.azurerm] [object_store.azurerm]
azure_storage_account_name = "YOUR ACCOUNT NAME" azure_storage_account_name = "YOUR ACCOUNT NAME"
azure_storage_access_key = "YOUR ACCOUNT KEY" azure_storage_access_key = "YOUR ACCOUNT KEY"
[image_resizer]
max_scaler_procs = 100
max_filesize = 250000
...@@ -80,6 +80,11 @@ type RedisConfig struct { ...@@ -80,6 +80,11 @@ type RedisConfig struct {
MaxActive *int MaxActive *int
} }
type ImageResizerConfig struct {
MaxScalerProcs uint32 `toml:"max_scaler_procs"`
MaxFilesize uint64 `toml:"max_filesize"`
}
type Config struct { type Config struct {
Redis *RedisConfig `toml:"redis"` Redis *RedisConfig `toml:"redis"`
Backend *url.URL `toml:"-"` Backend *url.URL `toml:"-"`
...@@ -97,11 +102,23 @@ type Config struct { ...@@ -97,11 +102,23 @@ type Config struct {
ObjectStorageConfig ObjectStorageConfig `toml:"-"` ObjectStorageConfig ObjectStorageConfig `toml:"-"`
ObjectStorageCredentials *ObjectStorageCredentials `toml:"object_storage"` ObjectStorageCredentials *ObjectStorageCredentials `toml:"object_storage"`
PropagateCorrelationID bool `toml:"-"` PropagateCorrelationID bool `toml:"-"`
ImageResizerConfig *ImageResizerConfig `toml:"image_resizer"`
}
const (
DefaultImageResizerMaxScalerProcs = 100
DefaultImageResizerMaxFilesize = 250 * 1000 // 250kB
)
var DefaultImageResizerConfig = &ImageResizerConfig{
MaxScalerProcs: DefaultImageResizerMaxScalerProcs,
MaxFilesize: DefaultImageResizerMaxFilesize,
} }
// LoadConfig from a file // LoadConfig from a file
func LoadConfig(filename string) (*Config, error) { func LoadConfig(filename string) (*Config, error) {
cfg := &Config{} cfg := &Config{ImageResizerConfig: DefaultImageResizerConfig}
if _, err := toml.DecodeFile(filename, cfg); err != nil { if _, err := toml.DecodeFile(filename, cfg); err != nil {
return nil, err return nil, err
} }
......
...@@ -80,6 +80,42 @@ func TestRegisterGoCloudURLOpeners(t *testing.T) { ...@@ -80,6 +80,42 @@ func TestRegisterGoCloudURLOpeners(t *testing.T) {
require.Equal(t, []string{"azblob"}, cfg.ObjectStorageConfig.URLMux.BucketSchemes()) require.Equal(t, []string{"azblob"}, cfg.ObjectStorageConfig.URLMux.BucketSchemes())
} }
func TestLoadDefaultConfig(t *testing.T) {
config := ``
tmpFile, cfg := loadTempConfig(t, config)
defer os.Remove(tmpFile.Name())
expected := Config{
ImageResizerConfig: &ImageResizerConfig{
MaxScalerProcs: DefaultImageResizerMaxScalerProcs,
MaxFilesize: DefaultImageResizerMaxFilesize,
},
}
require.Equal(t, expected, *cfg)
}
func TestLoadImageResizerConfig(t *testing.T) {
config := `
[image_resizer]
max_scaler_procs = 200
max_filesize = 350000
`
tmpFile, cfg := loadTempConfig(t, config)
defer os.Remove(tmpFile.Name())
require.NotNil(t, cfg.ImageResizerConfig, "Expected image resizer config")
expected := ImageResizerConfig{
MaxScalerProcs: 200,
MaxFilesize: 350000,
}
require.Equal(t, expected, *cfg.ImageResizerConfig)
}
func loadTempConfig(t *testing.T, config string) (f *os.File, cfg *Config) { func loadTempConfig(t *testing.T, config string) (f *os.File, cfg *Config) {
tmpFile, err := ioutil.TempFile(os.TempDir(), "test-") tmpFile, err := ioutil.TempFile(os.TempDir(), "test-")
require.NoError(t, err) require.NoError(t, err)
......
...@@ -14,6 +14,8 @@ import ( ...@@ -14,6 +14,8 @@ import (
"syscall" "syscall"
"time" "time"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"gitlab.com/gitlab-org/labkit/correlation" "gitlab.com/gitlab-org/labkit/correlation"
...@@ -25,9 +27,10 @@ import ( ...@@ -25,9 +27,10 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/senddata" "gitlab.com/gitlab-org/gitlab-workhorse/internal/senddata"
) )
type resizer struct{ senddata.Prefix } type Resizer struct {
config.Config
var SendScaledImage = &resizer{"send-scaled-img:"} senddata.Prefix
}
type resizeParams struct { type resizeParams struct {
Location string Location string
...@@ -41,11 +44,6 @@ type processCounter struct { ...@@ -41,11 +44,6 @@ type processCounter struct {
var numScalerProcs processCounter var numScalerProcs processCounter
const (
maxImageScalerProcs = 100
maxAllowedFileSizeBytes = 250 * 1000 // 250kB
)
var envInjector = tracing.NewEnvInjector() var envInjector = tracing.NewEnvInjector()
// Images might be located remotely in object storage, in which case we need to stream // Images might be located remotely in object storage, in which case we need to stream
...@@ -123,9 +121,13 @@ func init() { ...@@ -123,9 +121,13 @@ func init() {
prometheus.MustRegister(imageResizeDurations) prometheus.MustRegister(imageResizeDurations)
} }
func NewResizer(cfg config.Config) *Resizer {
return &Resizer{cfg, "send-scaled-img:"}
}
// This Injecter forks into a dedicated scaler process to resize an image identified by path or URL // This Injecter forks into a dedicated scaler process to resize an image identified by path or URL
// and streams the resized image back to the client // and streams the resized image back to the client
func (r *resizer) Inject(w http.ResponseWriter, req *http.Request, paramsData string) { func (r *Resizer) Inject(w http.ResponseWriter, req *http.Request, paramsData string) {
start := time.Now() start := time.Now()
logger := log.ContextLogger(req.Context()) logger := log.ContextLogger(req.Context())
params, err := r.unpackParameters(paramsData) params, err := r.unpackParameters(paramsData)
...@@ -156,7 +158,7 @@ func (r *resizer) Inject(w http.ResponseWriter, req *http.Request, paramsData st ...@@ -156,7 +158,7 @@ func (r *resizer) Inject(w http.ResponseWriter, req *http.Request, paramsData st
// We first attempt to rescale the image; if this should fail for any reason, imageReader // We first attempt to rescale the image; if this should fail for any reason, imageReader
// will point to the original image, i.e. we render it unchanged. // will point to the original image, i.e. we render it unchanged.
imageReader, resizeCmd, err := tryResizeImage(req, sourceImageReader, logger.Writer(), params, fileSize) imageReader, resizeCmd, err := tryResizeImage(req, sourceImageReader, logger.Writer(), params, fileSize, r.Config.ImageResizerConfig)
if err != nil { if err != nil {
// something failed, but we can still write out the original image, do don't return early // something failed, but we can still write out the original image, do don't return early
helper.LogErrorWithFields(req, err, *logFields(0)) helper.LogErrorWithFields(req, err, *logFields(0))
...@@ -204,7 +206,7 @@ func handleFailedCommand(w http.ResponseWriter, req *http.Request, bytesWritten ...@@ -204,7 +206,7 @@ func handleFailedCommand(w http.ResponseWriter, req *http.Request, bytesWritten
} }
} }
func (r *resizer) unpackParameters(paramsData string) (*resizeParams, error) { func (r *Resizer) unpackParameters(paramsData string) (*resizeParams, error) {
var params resizeParams var params resizeParams
if err := r.Unpack(&params, paramsData); err != nil { if err := r.Unpack(&params, paramsData); err != nil {
return nil, err return nil, err
...@@ -222,12 +224,12 @@ func (r *resizer) unpackParameters(paramsData string) (*resizeParams, error) { ...@@ -222,12 +224,12 @@ func (r *resizer) unpackParameters(paramsData string) (*resizeParams, error) {
} }
// Attempts to rescale the given image data, or in case of errors, falls back to the original image. // Attempts to rescale the given image data, or in case of errors, falls back to the original image.
func tryResizeImage(req *http.Request, r io.Reader, errorWriter io.Writer, params *resizeParams, fileSize int64) (io.Reader, *exec.Cmd, error) { func tryResizeImage(req *http.Request, r io.Reader, errorWriter io.Writer, params *resizeParams, fileSize int64, cfg *config.ImageResizerConfig) (io.Reader, *exec.Cmd, error) {
if fileSize > maxAllowedFileSizeBytes { if fileSize > int64(cfg.MaxFilesize) {
return r, nil, fmt.Errorf("ImageResizer: %db exceeds maximum file size of %db", fileSize, maxAllowedFileSizeBytes) return r, nil, fmt.Errorf("ImageResizer: %db exceeds maximum file size of %db", fileSize, cfg.MaxFilesize)
} }
if !numScalerProcs.tryIncrement() { if !numScalerProcs.tryIncrement(int32(cfg.MaxScalerProcs)) {
return r, nil, fmt.Errorf("ImageResizer: too many running scaler processes") return r, nil, fmt.Errorf("ImageResizer: too many running scaler processes")
} }
...@@ -312,8 +314,8 @@ func openFromFile(location string) (io.ReadCloser, int64, error) { ...@@ -312,8 +314,8 @@ func openFromFile(location string) (io.ReadCloser, int64, error) {
// Only allow more scaling requests if we haven't yet reached the maximum // Only allow more scaling requests if we haven't yet reached the maximum
// allowed number of concurrent scaler processes // allowed number of concurrent scaler processes
func (c *processCounter) tryIncrement() bool { func (c *processCounter) tryIncrement(maxScalerProcs int32) bool {
if p := atomic.AddInt32(&c.n, 1); p > maxImageScalerProcs { if p := atomic.AddInt32(&c.n, 1); p > maxScalerProcs {
c.decrement() c.decrement()
imageResizeConcurrencyLimitExceeds.Inc() imageResizeConcurrencyLimitExceeds.Inc()
......
...@@ -8,6 +8,8 @@ import ( ...@@ -8,6 +8,8 @@ import (
"os" "os"
"testing" "testing"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -15,7 +17,7 @@ import ( ...@@ -15,7 +17,7 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
) )
var r = resizer{} var r = Resizer{}
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if err := testhelper.BuildExecutables(); err != nil { if err := testhelper.BuildExecutables(); err != nil {
...@@ -56,7 +58,14 @@ func TestTryResizeImageSuccess(t *testing.T) { ...@@ -56,7 +58,14 @@ func TestTryResizeImageSuccess(t *testing.T) {
req, err := http.NewRequest("GET", "/foo", nil) req, err := http.NewRequest("GET", "/foo", nil)
require.NoError(t, err) require.NoError(t, err)
reader, cmd, err := tryResizeImage(req, inFile, os.Stderr, &inParams, maxAllowedFileSizeBytes) reader, cmd, err := tryResizeImage(
req,
inFile,
os.Stderr,
&inParams,
int64(config.DefaultImageResizerConfig.MaxFilesize),
config.DefaultImageResizerConfig,
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cmd) require.NotNil(t, cmd)
...@@ -70,7 +79,14 @@ func TestTryResizeImageSkipsResizeWhenSourceImageTooLarge(t *testing.T) { ...@@ -70,7 +79,14 @@ func TestTryResizeImageSkipsResizeWhenSourceImageTooLarge(t *testing.T) {
req, err := http.NewRequest("GET", "/foo", nil) req, err := http.NewRequest("GET", "/foo", nil)
require.NoError(t, err) require.NoError(t, err)
reader, cmd, err := tryResizeImage(req, inFile, os.Stderr, &inParams, maxAllowedFileSizeBytes+1) reader, cmd, err := tryResizeImage(
req,
inFile,
os.Stderr,
&inParams,
int64(config.DefaultImageResizerConfig.MaxFilesize)+1,
config.DefaultImageResizerConfig,
)
require.Error(t, err) require.Error(t, err)
require.Nil(t, cmd) require.Nil(t, cmd)
...@@ -83,7 +99,14 @@ func TestTryResizeImageFailsWhenContentTypeNotMatchingFileContents(t *testing.T) ...@@ -83,7 +99,14 @@ func TestTryResizeImageFailsWhenContentTypeNotMatchingFileContents(t *testing.T)
req, err := http.NewRequest("GET", "/foo", nil) req, err := http.NewRequest("GET", "/foo", nil)
require.NoError(t, err) require.NoError(t, err)
_, cmd, err := tryResizeImage(req, inFile, os.Stderr, &inParams, maxAllowedFileSizeBytes) _, cmd, err := tryResizeImage(
req,
inFile,
os.Stderr,
&inParams,
int64(config.DefaultImageResizerConfig.MaxFilesize),
config.DefaultImageResizerConfig,
)
require.NoError(t, err) require.NoError(t, err)
require.Error(t, cmd.Wait(), "Expected to fail due to content-type mismatch") require.Error(t, cmd.Wait(), "Expected to fail due to content-type mismatch")
......
...@@ -142,7 +142,7 @@ func (ro *routeEntry) isMatch(cleanedPath string, req *http.Request) bool { ...@@ -142,7 +142,7 @@ func (ro *routeEntry) isMatch(cleanedPath string, req *http.Request) bool {
return ok return ok
} }
func buildProxy(backend *url.URL, version string, rt http.RoundTripper) http.Handler { func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg config.Config) http.Handler {
proxier := proxypkg.NewProxy(backend, version, rt) proxier := proxypkg.NewProxy(backend, version, rt)
return senddata.SendData( return senddata.SendData(
...@@ -154,7 +154,7 @@ func buildProxy(backend *url.URL, version string, rt http.RoundTripper) http.Han ...@@ -154,7 +154,7 @@ func buildProxy(backend *url.URL, version string, rt http.RoundTripper) http.Han
git.SendSnapshot, git.SendSnapshot,
artifacts.SendEntry, artifacts.SendEntry,
sendurl.SendURL, sendurl.SendURL,
imageresizer.SendScaledImage, imageresizer.NewResizer(cfg),
) )
} }
...@@ -170,11 +170,11 @@ func (u *upstream) configureRoutes() { ...@@ -170,11 +170,11 @@ func (u *upstream) configureRoutes() {
) )
static := &staticpages.Static{DocumentRoot: u.DocumentRoot} static := &staticpages.Static{DocumentRoot: u.DocumentRoot}
proxy := buildProxy(u.Backend, u.Version, u.RoundTripper) proxy := buildProxy(u.Backend, u.Version, u.RoundTripper, u.Config)
cableProxy := proxypkg.NewProxy(u.CableBackend, u.Version, u.CableRoundTripper) cableProxy := proxypkg.NewProxy(u.CableBackend, u.Version, u.CableRoundTripper)
signingTripper := secret.NewRoundTripper(u.RoundTripper, u.Version) signingTripper := secret.NewRoundTripper(u.RoundTripper, u.Version)
signingProxy := buildProxy(u.Backend, u.Version, signingTripper) signingProxy := buildProxy(u.Backend, u.Version, signingTripper, u.Config)
preparers := createUploadPreparers(u.Config) preparers := createUploadPreparers(u.Config)
uploadPath := path.Join(u.DocumentRoot, "uploads/tmp") uploadPath := path.Join(u.DocumentRoot, "uploads/tmp")
......
...@@ -157,6 +157,7 @@ func main() { ...@@ -157,6 +157,7 @@ func main() {
APIQueueTimeout: *apiQueueTimeout, APIQueueTimeout: *apiQueueTimeout,
APICILongPollingDuration: *apiCiLongPollingDuration, APICILongPollingDuration: *apiCiLongPollingDuration,
PropagateCorrelationID: *propagateCorrelationID, PropagateCorrelationID: *propagateCorrelationID,
ImageResizerConfig: config.DefaultImageResizerConfig,
} }
if *configFile != "" { if *configFile != "" {
...@@ -167,6 +168,7 @@ func main() { ...@@ -167,6 +168,7 @@ func main() {
cfg.Redis = cfgFromFile.Redis cfg.Redis = cfgFromFile.Redis
cfg.ObjectStorageCredentials = cfgFromFile.ObjectStorageCredentials cfg.ObjectStorageCredentials = cfgFromFile.ObjectStorageCredentials
cfg.ImageResizerConfig = cfgFromFile.ImageResizerConfig
if cfg.Redis != nil { if cfg.Redis != nil {
redis.Configure(cfg.Redis, redis.DefaultDialFunc) redis.Configure(cfg.Redis, redis.DefaultDialFunc)
......
...@@ -645,9 +645,10 @@ func testAuthServer(t *testing.T, url *regexp.Regexp, params url.Values, code in ...@@ -645,9 +645,10 @@ func testAuthServer(t *testing.T, url *regexp.Regexp, params url.Values, code in
func newUpstreamConfig(authBackend string) *config.Config { func newUpstreamConfig(authBackend string) *config.Config {
return &config.Config{ return &config.Config{
Version: "123", Version: "123",
DocumentRoot: testDocumentRoot, DocumentRoot: testDocumentRoot,
Backend: helper.URLMustParse(authBackend), Backend: helper.URLMustParse(authBackend),
ImageResizerConfig: config.DefaultImageResizerConfig,
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment