Skip to content

Commit e8208e3

Browse files
authoredFeb 8, 2022
add bundle size validator check (#210)
* add bundle size validator check Co-authored-by: Brett Tofel btofel@redhat.com * applying suggetions - review * add operator-registry as dep and use gzip method to check bundle size * go mod vendor * go fmt * add enconding to the api * refac name
1 parent 2e47b5a commit e8208e3

9 files changed

+544
-5
lines changed
 

‎pkg/encoding/encoding.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package encoding
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"encoding/base64"
7+
"io"
8+
)
9+
10+
// GzipBase64Encode applies gzip compression to the given bytes, followed by base64 encoding.
11+
func GzipBase64Encode(data []byte) ([]byte, error) {
12+
buf := &bytes.Buffer{}
13+
14+
bWriter := base64.NewEncoder(base64.StdEncoding, buf)
15+
zWriter := gzip.NewWriter(bWriter)
16+
_, err := zWriter.Write(data)
17+
if err != nil {
18+
zWriter.Close()
19+
bWriter.Close()
20+
return nil, err
21+
}
22+
23+
// Ensure all gzipped bytes are flushed to the underlying base64 encoder
24+
err = zWriter.Close()
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
// Ensure all base64d bytes are flushed to the underlying buffer
30+
err = bWriter.Close()
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
return buf.Bytes(), nil
36+
}
37+
38+
// GzipBase64Decode applies base64 decoding to the given bytes, followed by gzip decompression.
39+
func GzipBase64Decode(data []byte) ([]byte, error) {
40+
bBuffer := bytes.NewReader(data)
41+
42+
bReader := base64.NewDecoder(base64.StdEncoding, bBuffer)
43+
zReader, err := gzip.NewReader(bReader)
44+
if err != nil {
45+
return nil, err
46+
}
47+
defer zReader.Close()
48+
49+
return io.ReadAll(zReader)
50+
}

‎pkg/encoding/encoding_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package encoding
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestGzipBase64EncodeDecode(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
source string
14+
}{
15+
{
16+
name: "Encode-Decode-CSV",
17+
source: "testdata/etcdoperator.v0.9.4.clusterserviceversion.yaml",
18+
},
19+
{
20+
name: "Encode-Decode-CRD",
21+
source: "testdata/etcdclusters.etcd.database.coreos.com.crd.yaml",
22+
},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
data, err := os.ReadFile(tt.source)
28+
require.NoError(t, err, "unable to load from file %s", tt.source)
29+
30+
encoded, err := GzipBase64Encode(data)
31+
require.NoError(t, err, "unexpected error while encoding data")
32+
33+
require.Lessf(t, len(encoded), len(data),
34+
"encoded data (%d bytes) isn't lesser than original data (%d bytes)",
35+
len(encoded), len(data))
36+
37+
decoded, err := GzipBase64Decode(encoded)
38+
require.NoError(t, err, "unexpected error while decoding data")
39+
40+
require.Equal(t, data, decoded, "decoded data doesn't match original data")
41+
})
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: apiextensions.k8s.io/v1beta1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: etcdclusters.etcd.database.coreos.com
5+
spec:
6+
group: etcd.database.coreos.com
7+
names:
8+
kind: EtcdCluster
9+
listKind: EtcdClusterList
10+
plural: etcdclusters
11+
shortNames:
12+
- etcdclus
13+
- etcd
14+
singular: etcdcluster
15+
scope: Namespaced
16+
version: v1beta2

‎pkg/encoding/testdata/etcdoperator.v0.9.4.clusterserviceversion.yaml

+309
Large diffs are not rendered by default.

‎pkg/manifests/bundle.go

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type Bundle struct {
1919
V1beta1CRDs []*apiextensionsv1beta1.CustomResourceDefinition
2020
V1CRDs []*apiextensionsv1.CustomResourceDefinition
2121
Dependencies []*Dependency
22+
// CompressedSize stores the gzip size of the bundle
23+
CompressedSize *int64
2224
}
2325

2426
func (b *Bundle) ObjectsToValidate() []interface{} {

‎pkg/manifests/bundleloader.go

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
utilerrors "k8s.io/apimachinery/pkg/util/errors"
1414
"k8s.io/apimachinery/pkg/util/yaml"
1515

16+
"github.com/operator-framework/api/pkg/encoding"
1617
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
1718
)
1819

@@ -35,6 +36,14 @@ func (b *bundleLoader) LoadBundle() error {
3536
errs = append(errs, err)
3637
}
3738

39+
// Compress the bundle to check its size
40+
if data, err := os.ReadFile(b.dir); err == nil {
41+
if content, err := encoding.GzipBase64Encode(data); err != nil {
42+
total := int64(len(content))
43+
b.bundle.CompressedSize = &total
44+
}
45+
}
46+
3847
if !b.foundCSV {
3948
errs = append(errs, fmt.Errorf("unable to find a csv in bundle directory %s", b.dir))
4049
} else if b.bundle == nil {

‎pkg/validation/errors/error.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func ErrInvalidBundle(detail string, value interface{}) Error {
117117
}
118118

119119
func WarnInvalidBundle(detail string, value interface{}) Error {
120-
return invalidBundle(LevelError, detail, value)
120+
return invalidBundle(LevelWarn, detail, value)
121121
}
122122

123123
func invalidBundle(lvl Level, detail string, value interface{}) Error {

‎pkg/validation/internal/bundle.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import (
1515

1616
var BundleValidator interfaces.Validator = interfaces.ValidatorFunc(validateBundles)
1717

18+
// max_bundle_size is the maximum size of a bundle in bytes.
19+
// This ensures the bundle can be staged in a single ConfigMap by OLM during installation.
20+
// The value is derived from the standard upper bound for k8s resources (~4MB).
21+
const max_bundle_size = 4 << (10 * 2)
22+
1823
func validateBundles(objs ...interface{}) (results []errors.ManifestResult) {
1924
for _, obj := range objs {
2025
switch v := obj.(type) {
@@ -32,6 +37,10 @@ func validateBundle(bundle *manifests.Bundle) (result errors.ManifestResult) {
3237
if saErrors != nil {
3338
result.Add(saErrors...)
3439
}
40+
sizeErrors := validateBundleSize(bundle)
41+
if sizeErrors != nil {
42+
result.Add(sizeErrors...)
43+
}
3544
return result
3645
}
3746

@@ -99,7 +108,7 @@ func validateOwnedCRDs(bundle *manifests.Bundle, csv *operatorsv1alpha1.ClusterS
99108

100109
// All CRDs present in a CSV must be present in the bundle.
101110
for key := range keySet {
102-
result.Add(errors.WarnInvalidBundle(fmt.Sprintf("CRD %q is present in bundle %q but not defined in CSV", key, bundle.Name), key))
111+
result.Add(errors.ErrInvalidBundle(fmt.Sprintf("CRD %q is present in bundle %q but not defined in CSV", key, bundle.Name), key))
103112
}
104113

105114
return result
@@ -117,6 +126,30 @@ func getOwnedCustomResourceDefintionKeys(csv *operatorsv1alpha1.ClusterServiceVe
117126
return keys
118127
}
119128

129+
// validateBundleSize will check the bundle size according to its limits
130+
// note that this check will raise an error if the size is bigger than the max allowed
131+
// and warnings when:
132+
// - we are unable to check the bundle size because we are running a check without load the bundle
133+
// - we could identify that the bundle size is close to the limit (bigger than 85%)
134+
func validateBundleSize(bundle *manifests.Bundle) []errors.Error {
135+
warnPercent := 0.85
136+
warnSize := int64(max_bundle_size * warnPercent)
137+
var errs []errors.Error
138+
139+
if bundle.CompressedSize == nil || *bundle.CompressedSize == 0 {
140+
errs = append(errs, errors.WarnFailedValidation("unable to check the bundle size", nil))
141+
return errs
142+
}
143+
144+
if *bundle.CompressedSize > max_bundle_size {
145+
errs = append(errs, errors.ErrInvalidBundle(fmt.Sprintf("maximum bundle compressed size with gzip size exceeded: size=~%d MegaByte, max=%d MegaByte", *bundle.CompressedSize/(1<<(10*2)), max_bundle_size/(1<<(10*2))), nil))
146+
} else if *bundle.CompressedSize > warnSize {
147+
errs = append(errs, errors.WarnInvalidBundle(fmt.Sprintf("nearing maximum bundle compressed size with gzip: size=~%d MegaByte, max=%d MegaByte", *bundle.CompressedSize/(1<<(10*2)), max_bundle_size/(1<<(10*2))), nil))
148+
}
149+
150+
return errs
151+
}
152+
120153
// getBundleCRDKeys returns a set of definition keys for all CRDs in bundle.
121154
func getBundleCRDKeys(bundle *manifests.Bundle) (keys []schema.GroupVersionKind) {
122155
// Collect all v1 and v1beta1 CRD keys, skipping group which CSVs do not support.

‎pkg/validation/internal/bundle_test.go

+80-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package internal
33
import (
44
"testing"
55

6-
"github.com/operator-framework/api/pkg/manifests"
7-
"github.com/operator-framework/api/pkg/operators/v1alpha1"
6+
"github.com/stretchr/testify/require"
87
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
98

10-
"github.com/stretchr/testify/require"
9+
"github.com/operator-framework/api/pkg/manifests"
10+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
"github.com/operator-framework/api/pkg/validation/errors"
1112
)
1213

1314
func TestValidateBundle(t *testing.T) {
@@ -160,3 +161,79 @@ func TestValidateServiceAccount(t *testing.T) {
160161
})
161162
}
162163
}
164+
165+
func TestBundleSize(t *testing.T) {
166+
type args struct {
167+
size int64
168+
}
169+
tests := []struct {
170+
name string
171+
args args
172+
wantError bool
173+
wantWarning bool
174+
errStrings []string
175+
warnStrings []string
176+
}{
177+
{
178+
name: "should pass when the size is not bigger or closer of the limit",
179+
args: args{
180+
size: int64(max_bundle_size / 2),
181+
},
182+
},
183+
{
184+
name: "should warn when the size is closer of the limit",
185+
args: args{
186+
size: int64(max_bundle_size - 10),
187+
},
188+
wantWarning: true,
189+
warnStrings: []string{"Warning: : nearing maximum bundle compressed size with gzip: size=~3 MegaByte, max=4 MegaByte"},
190+
},
191+
{
192+
name: "should warn when is not possible to check the size",
193+
wantWarning: true,
194+
warnStrings: []string{"Warning: : unable to check the bundle size"},
195+
},
196+
{
197+
name: "should raise an error when the size is bigger than the limit",
198+
args: args{
199+
size: int64(2 * max_bundle_size),
200+
},
201+
wantError: true,
202+
errStrings: []string{"Error: : maximum bundle compressed size with gzip size exceeded: size=~8 MegaByte, max=4 MegaByte"},
203+
},
204+
}
205+
for _, tt := range tests {
206+
t.Run(tt.name, func(t *testing.T) {
207+
bundle := &manifests.Bundle{
208+
CompressedSize: &tt.args.size,
209+
}
210+
result := validateBundleSize(bundle)
211+
212+
var warns, errs []errors.Error
213+
for _, r := range result {
214+
if r.Level == errors.LevelWarn {
215+
warns = append(warns, r)
216+
} else if r.Level == errors.LevelError {
217+
errs = append(errs, r)
218+
}
219+
}
220+
require.Equal(t, tt.wantWarning, len(warns) > 0)
221+
if tt.wantWarning {
222+
require.Equal(t, len(tt.warnStrings), len(warns))
223+
for _, w := range warns {
224+
wString := w.Error()
225+
require.Contains(t, tt.warnStrings, wString)
226+
}
227+
}
228+
229+
require.Equal(t, tt.wantError, len(errs) > 0)
230+
if tt.wantError {
231+
require.Equal(t, len(tt.errStrings), len(errs))
232+
for _, err := range errs {
233+
errString := err.Error()
234+
require.Contains(t, tt.errStrings, errString)
235+
}
236+
}
237+
})
238+
}
239+
}

0 commit comments

Comments
 (0)
Please sign in to comment.