Skip to content

Commit

Permalink
Save entry list compressed in extension state (#416)
Browse files Browse the repository at this point in the history
* Save entry list compressed in extension state

* address review feedback
  • Loading branch information
MartinWeindel authored Jan 16, 2025
1 parent f04e971 commit f2f0817
Show file tree
Hide file tree
Showing 8 changed files with 495 additions and 42 deletions.
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ sast-report: $(GOSEC)
@./hack/sast.sh --gosec-report true

.PHONY: test
test:
test: $(GINKGO)
@bash $(GARDENER_HACK_DIR)/test.sh ./cmd/... ./pkg/...

.PHONY: test-cov
Expand All @@ -135,8 +135,12 @@ test-cov:
test-clean:
@bash $(GARDENER_HACK_DIR)/test-cover-clean.sh

.PHONY: test-integration-lifecycle
test-integration-lifecycle: $(REPORT_COLLECTOR) $(SETUP_ENVTEST)
@bash $(GARDENER_HACK_DIR)/test-integration.sh ./test/integration/lifecycle/...

.PHONY: verify
verify: check format test sast
verify: check format test test-integration-lifecycle sast

.PHONY: verify-extended
verify-extended: check-generate check format test-cov test-clean sast-report
verify-extended: check-generate check format test-cov test-clean test-integration-lifecycle sast-report
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23.0

require (
github.com/ahmetb/gen-crd-api-reference-docs v0.3.1-0.20241014194617-ffc4efda75d4
github.com/andybalholm/brotli v1.1.1
github.com/gardener/external-dns-management v0.22.2
github.com/gardener/gardener v1.110.0
github.com/go-logr/logr v1.4.2
Expand All @@ -29,7 +30,6 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
19 changes: 0 additions & 19 deletions pkg/apis/helper/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,18 @@
package helper

import (
"fmt"

extapi "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"

api "github.com/gardener/gardener-extension-shoot-dns-service/pkg/apis"
"github.com/gardener/gardener-extension-shoot-dns-service/pkg/apis/install"
)

var (
// Scheme is a scheme with the types relevant for vSphere actuators.
Scheme *runtime.Scheme

decoder runtime.Decoder
)

func init() {
Scheme = runtime.NewScheme()
utilruntime.Must(install.AddToScheme(Scheme))

decoder = serializer.NewCodecFactory(Scheme).UniversalDecoder()
}

func GetExtensionState(ext *extapi.Extension) (*api.DNSState, error) {
state := &api.DNSState{}
if ext.Status.State != nil && ext.Status.State.Raw != nil {
if _, _, err := decoder.Decode(ext.Status.State.Raw, nil, state); err != nil {
return state, fmt.Errorf("could not decode extension state: %w", err)
}
}
return state, nil
}
70 changes: 70 additions & 0 deletions pkg/controller/common/compressedstate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package common

import (
"bytes"
"encoding/json"
"fmt"

"github.com/andybalholm/brotli"
)

type compressedEntriesState struct {
CompressedState []byte `json:"compressedState"`
}

// CompressEntriesState compresses the entries state data.
func CompressEntriesState(state []byte) ([]byte, error) {
if len(state) == 0 || string(state) == "{}" {
return nil, nil
}

var stateCompressed bytes.Buffer
writer := brotli.NewWriter(&stateCompressed)
defer writer.Close()

if _, err := writer.Write(state); err != nil {
return nil, fmt.Errorf("failed writing entries state data for compression: %w", err)
}

// Close ensures any unwritten data is flushed. Without this, the `stateCompressed`
// buffer would not contain any data. Hence, we have to call it explicitly here after writing, in addition to the
// 'defer' call above.
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("failed closing the brotli writer after compressing the entries state data: %w", err)
}

return json.Marshal(&compressedEntriesState{CompressedState: stateCompressed.Bytes()})
}

// LooksLikeCompressedEntriesState checks if the given state data has the string compressedState in the first 20 bytes.
func LooksLikeCompressedEntriesState(state []byte) bool {
if len(state) < len("compressedState") {
return false
}

return bytes.Contains(state[:min(20, len(state))], []byte("compressedState"))
}

// DecompressEntriesState decompresses the entries state data.
func DecompressEntriesState(stateCompressed []byte) ([]byte, error) {
if len(stateCompressed) == 0 {
return nil, nil
}

var entriesState compressedEntriesState
if err := json.Unmarshal(stateCompressed, &entriesState); err != nil {
return nil, fmt.Errorf("failed unmarshalling JSON to compressed entries state structure: %w", err)
}

reader := brotli.NewReader(bytes.NewReader(entriesState.CompressedState))
var state bytes.Buffer
if _, err := state.ReadFrom(reader); err != nil {
return nil, fmt.Errorf("failed reading machine state data for decompression: %w", err)
}

return state.Bytes(), nil
}
34 changes: 34 additions & 0 deletions pkg/controller/common/compressedstate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package common

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("CompressedState", func() {
It("should compress and decompress", func() {
state := []byte(`{"entries":[{"name":"entry1","spec":{}},{"name":"entry2","spec":{}}]}`)
data, err := CompressEntriesState(state)
Expect(err).To(BeNil())
Expect(data).NotTo(BeNil())

state2, err := DecompressEntriesState(data)
Expect(err).To(BeNil())
Expect(state2).NotTo(BeNil())
Expect(state).To(Equal(state2))
})

It("should recognise compressed state data by heuristic", func() {
state := []byte(`{"entries":[{"name":"entry1","spec":{}},{"name":"entry2","spec":{}}]}`)
data, err := CompressEntriesState(state)
Expect(err).To(BeNil())
Expect(data).NotTo(BeNil())

Expect(LooksLikeCompressedEntriesState(data)).To(BeTrue())
Expect(LooksLikeCompressedEntriesState(state)).To(BeFalse())
})
})
59 changes: 40 additions & 19 deletions pkg/controller/common/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import (

dnsapi "github.com/gardener/external-dns-management/pkg/apis/dns/v1alpha1"
"github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
extapi "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand All @@ -23,6 +25,32 @@ import (
"github.com/gardener/gardener-extension-shoot-dns-service/pkg/service"
)

var (
decoder runtime.Decoder
)

func init() {
decoder = serializer.NewCodecFactory(helper.Scheme).UniversalDecoder()
}

func GetExtensionState(ext *extapi.Extension) (*apis.DNSState, error) {
state := &apis.DNSState{}
if ext.Status.State != nil && ext.Status.State.Raw != nil {
data := ext.Status.State.Raw
if LooksLikeCompressedEntriesState(data) {
var err error
data, err = DecompressEntriesState(data)
if err != nil {
return state, fmt.Errorf("could not decompress extension state: %w", err)
}
}
if _, _, err := decoder.Decode(data, nil, state); err != nil {
return state, fmt.Errorf("could not decode extension state: %w", err)
}
}
return state, nil
}

////////////////////////////////////////////////////////////////////////////////
// state update handling

Expand Down Expand Up @@ -51,7 +79,7 @@ func NewStateHandler(ctx context.Context, env *Env, ext *v1alpha1.Extension, ref
elem: elem,
helper: NewShootDNSEntriesHelper(ctx, env.Client(), ext),
}
handler.state, err = helper.GetExtensionState(ext)
handler.state, err = GetExtensionState(ext)
if err != nil || refresh {
if err != nil {
handler.modified = true
Expand Down Expand Up @@ -93,17 +121,6 @@ func (s *StateHandler) Refresh() (bool, error) {
if err != nil {
return false, err
}
/*
list = append(list, dnsapi.DNSEntry{
ObjectMeta: metav1.ObjectMeta{
Name: "DUMMY",
},
Spec: dnsapi.DNSEntrySpec{
DNSName: "bla.blub.de",
Targets: []string{"8.8.8.8"},
},
})
*/
return s.EnsureEntries(list), nil
}

Expand Down Expand Up @@ -178,25 +195,29 @@ func (s *StateHandler) Update(reason string) error {
wire.Kind = wireapi.DNSStateKind
err := helper.Scheme.Convert(s.state, wire, nil)
if err != nil {
s.Infof("state conversion failed: %s", err)
s.Error(err, "state conversion failed")
return err
}
if s.ext.Status.State == nil {
s.ext.Status.State = &runtime.RawExtension{}
}
s.ext.Status.State.Raw, err = json.Marshal(wire)
data, err := json.Marshal(wire)
if err != nil {
s.Error(err, "marshalling failed")
return err
}
s.ext.Status.State.Raw, err = CompressEntriesState(data)
if err != nil {
s.Info("marshalling failed", "error", err)
s.Error(err, "compressing failed")
return err
}
s.ext.Status.State.Object = nil
err = s.client.Status().Update(s.ctx, s.ext)
if err != nil {
s.Info("update failed", "error", err)
} else {
s.modified = false
s.Error(err, "update failed")
return err
}
return err
s.modified = false
}
return nil
}
17 changes: 17 additions & 0 deletions test/integration/lifecycle/lifecycle_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package lifecycle_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestLifecycle(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Lifecycle Suite")
}
Loading

0 comments on commit f2f0817

Please sign in to comment.