Skip to content

Commit

Permalink
Support gzipped runtime configurations (#571)
Browse files Browse the repository at this point in the history
* Support gzipped runtime configurations

Some runtime configs can be too large to fit in one kubernetes
configmap. However, Kubernetes configmaps accept binary data, so we can
gzip the yaml, which offers up to 10x compression.

This adds a new flag to runtimeconfig.Manager's config that enabled gzip
uncompression of the files, disabled by default.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Just check the extension

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Fix test

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Update CHANGELOG.md

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

---------

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>
  • Loading branch information
colega authored Aug 22, 2024
1 parent f96e399 commit f25f206
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 3 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@
* [EHNANCEMENT] crypto/tls: Support reloading client certificates #537 #552
* [ENHANCEMENT] Add read only support for ingesters in the ring and lifecycler. #553 #554 #556
* [ENHANCEMENT] Added new ring methods to expose number of writable instances with tokens per zone, and overall. #560 #562
* [ENHANCEMENT] `services.FailureWatcher` can now be closed, which unregisters all service and manager listeners, and closes channel used to receive errors. #564
* [ENHANCEMENT] `services.FailureWatcher` can now be closed, which unregisters all service and manager listeners, and closes channel used to receive errors. #564
* [ENHANCEMENT] Runtimeconfig: support gzip-compressed files with `.gz` extension. #571
* [CHANGE] Backoff: added `Backoff.ErrCause()` which is like `Backoff.Err()` but returns the context cause if backoff is terminated because the context has been canceled. #538
* [BUGFIX] spanlogger: Support multiple tenant IDs. #59
* [BUGFIX] Memberlist: fixed corrupted packets when sending compound messages with more than 255 messages or messages bigger than 64KB. #85
Expand Down
32 changes: 30 additions & 2 deletions runtimeconfig/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package runtimeconfig

import (
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"flag"
"fmt"
"io"
"os"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -183,8 +185,8 @@ func (om *Manager) loadConfig() error {

mergedConfig := map[string]interface{}{}
for _, f := range om.cfg.LoadPath {
yamlFile := map[string]interface{}{}
err := yaml.Unmarshal(rawData[f], &yamlFile)
data := rawData[f]
yamlFile, err := om.unmarshalMaybeGzipped(f, data)
if err != nil {
om.configLoadSuccess.Set(0)
return errors.Wrapf(err, "unmarshal file %q", f)
Expand Down Expand Up @@ -218,6 +220,32 @@ func (om *Manager) loadConfig() error {
return nil
}

func (om *Manager) unmarshalMaybeGzipped(filename string, data []byte) (map[string]any, error) {
yamlFile := map[string]any{}
if strings.HasSuffix(filename, ".gz") {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "read gzipped file")
}
defer r.Close()
err = yaml.NewDecoder(r).Decode(&yamlFile)
return yamlFile, errors.Wrap(err, "uncompress/unmarshal gzipped file")
}

if err := yaml.Unmarshal(data, &yamlFile); err != nil {
// Give a hint if we think that file is gzipped.
if isGzip(data) {
return nil, errors.Wrap(err, "file looks gzipped but doesn't have a .gz extension")
}
return nil, err
}
return yamlFile, nil
}

func isGzip(data []byte) bool {
return len(data) > 2 && data[0] == 0x1f && data[1] == 0x8b
}

func mergeConfigMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
Expand Down
67 changes: 67 additions & 0 deletions runtimeconfig/manager_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package runtimeconfig

import (
"compress/gzip"
"context"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -176,6 +178,71 @@ func TestNewOverridesManager(t *testing.T) {
require.Equal(t, 150, conf.Overrides["user1"].Limit2)
}

func TestManagerGzip(t *testing.T) {
writeConfig := func(filename string, gzipped bool) string {
dir := t.TempDir()
filePath := filepath.Join(dir, filename)
f, err := os.Create(filePath)
require.NoError(t, err)
defer f.Close()
w := io.Writer(f)
if gzipped {
gw := gzip.NewWriter(f)
defer gw.Close()
w = gw
}
require.NoError(t, yaml.NewEncoder(w).Encode(map[string]any{
"overrides": map[string]any{
"user1": map[string]any{
"limit2": 150,
},
},
}))
return filePath
}

cfg := func(file string) Config {
return Config{
ReloadPeriod: time.Second,
LoadPath: []string{file},
Loader: testLoadOverrides,
}
}

defaultTestLimits = &TestLimits{Limit1: 100}
t.Run("gzipped with .gz extension should succeed", func(t *testing.T) {
file := writeConfig("overrides.yaml.gz", true)
manager, err := New(cfg(file), "overrides", nil, log.NewNopLogger())
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(context.Background(), manager))
t.Cleanup(func() { require.NoError(t, services.StopAndAwaitTerminated(context.Background(), manager)) })

// Make sure test limits were loaded.
require.NotNil(t, manager.GetConfig())
conf := manager.GetConfig().(*testOverrides)
require.NotNil(t, conf)
require.Equal(t, 150, conf.Overrides["user1"].Limit2)
})

t.Run("non-gzipped with .gz extension should fail", func(t *testing.T) {
file := writeConfig("overrides.yaml.gz", false)
manager, err := New(cfg(file), "overrides", nil, log.NewNopLogger())
require.NoError(t, err)
err = services.StartAndAwaitRunning(context.Background(), manager)
require.Error(t, err)
require.ErrorIs(t, err, gzip.ErrHeader)
})

t.Run("gzipped without .gz extension should mention that in the error", func(t *testing.T) {
file := writeConfig("overrides.yaml", true)
manager, err := New(cfg(file), "overrides", nil, log.NewNopLogger())
require.NoError(t, err)
err = services.StartAndAwaitRunning(context.Background(), manager)
require.Error(t, err)
require.Contains(t, err.Error(), "file looks gzipped but doesn't have a .gz extension")
})
}

func TestOverridesManagerMultipleFilesAppend(t *testing.T) {
tempFiles, err := generateRuntimeFiles(t,
[]string{`overrides:
Expand Down

0 comments on commit f25f206

Please sign in to comment.