Skip to content

Commit 7cc7a25

Browse files
committed
pkg: Separate validation and linting of packages
Crossplane uses package linters in two places: at build time (in the CLI) and at install time (in the revision controller). In both cases, linter errors are treated as errors and stop the operation. At build time, this makes sense: we should be strict about the packages we produce. At install time, this means we may reject packages that we could successfully install. For example (as described in crossplane#6525) if we allow new resource types in a package in a new version of Crossplane (as we've done with Operations and MRDs), older versions should still be able to install packages that contain them; the new resources would just be ignored. If package authors want to limit the versions their package works with, they can set a version constraint in the metadata. Introduce a new Validator type, which is identical to a linter except in name. Add validators for all package types, which are identical to their linters except that they allow any object to be included. In the revision controller, use validators to determine whether package installation should be attempted, where we previously used linters. Run the linters as well and record any lint errors as events, to help the user diagnose any unexpected behavior. Fixes crossplane#6525 Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
1 parent 82d565b commit 7cc7a25

3 files changed

Lines changed: 194 additions & 13 deletions

File tree

internal/controller/pkg/revision/reconciler.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const (
8282
errInitParserBackend = "cannot initialize parser backend"
8383
errParsePackage = "cannot parse package contents"
8484
errLintPackage = "linting package contents failed"
85+
errValidatePackage = "validating package contents failed"
8586
errNotOneMeta = "cannot install package with multiple meta types"
8687
errIncompatible = "incompatible Crossplane version"
8788

@@ -108,6 +109,7 @@ const (
108109
reasonImageConfig event.Reason = "ImageConfigSelection"
109110
reasonParse event.Reason = "ParsePackage"
110111
reasonLint event.Reason = "LintPackage"
112+
reasonValidate event.Reason = "ValidatePackage"
111113
reasonDependencies event.Reason = "ResolveDependencies"
112114
reasonConvertCRD event.Reason = "ConvertCRDToMRD"
113115
reasonSync event.Reason = "SyncPackage"
@@ -204,6 +206,13 @@ func WithLinter(l parser.Linter) ReconcilerOption {
204206
}
205207
}
206208

209+
// WithValidator specifies how the Reconciler should validate a package.
210+
func WithValidator(v xpkg.Validator) ReconcilerOption {
211+
return func(r *Reconciler) {
212+
r.validator = v
213+
}
214+
}
215+
207216
// WithVersioner specifies how the Reconciler should fetch the current
208217
// Crossplane version.
209218
func WithVersioner(v version.Operations) ReconcilerOption {
@@ -243,6 +252,7 @@ type Reconciler struct {
243252
objects Establisher
244253
parser parser.Parser
245254
linter parser.Linter
255+
validator xpkg.Validator
246256
versioner version.Operations
247257
backend parser.Backend
248258
config xpkg.ConfigStore
@@ -298,6 +308,7 @@ func SetupProviderRevision(mgr ctrl.Manager, o controller.Options) error {
298308
WithParserBackend(NewImageBackend(fetcher)),
299309
WithConfigStore(xpkg.NewImageConfigStore(mgr.GetClient(), o.Namespace)),
300310
WithLinter(xpkg.NewProviderLinter()),
311+
WithValidator(xpkg.NewProviderValidator()),
301312
WithLogger(log),
302313
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name), o.EventFilterFunctions...)),
303314
WithNamespace(o.Namespace),
@@ -344,6 +355,7 @@ func SetupConfigurationRevision(mgr ctrl.Manager, o controller.Options) error {
344355
WithParserBackend(NewImageBackend(f)),
345356
WithConfigStore(xpkg.NewImageConfigStore(mgr.GetClient(), o.Namespace)),
346357
WithLinter(xpkg.NewConfigurationLinter()),
358+
WithValidator(xpkg.NewConfigurationValidator()),
347359
WithLogger(log),
348360
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name), o.EventFilterFunctions...)),
349361
WithNamespace(o.Namespace),
@@ -402,6 +414,7 @@ func SetupFunctionRevision(mgr ctrl.Manager, o controller.Options) error {
402414
WithParserBackend(NewImageBackend(fetcher)),
403415
WithConfigStore(xpkg.NewImageConfigStore(mgr.GetClient(), o.Namespace)),
404416
WithLinter(xpkg.NewFunctionLinter()),
417+
WithValidator(xpkg.NewFunctionValidator()),
405418
WithLogger(log),
406419
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name), o.EventFilterFunctions...)),
407420
WithNamespace(o.Namespace),
@@ -784,22 +797,29 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
784797
return reconcile.Result{}, err
785798
}
786799

787-
// Lint package using package-specific linter.
788-
if err := r.linter.Lint(pkg); err != nil {
789-
err = errors.Wrap(err, errLintPackage)
800+
// Validate the package using a package-specific validator. If validation
801+
// fails, we won't try to install the package.
802+
if err := r.validator.Lint(pkg); err != nil {
803+
err = errors.Wrap(err, errValidatePackage)
790804
status.MarkConditions(v1.RevisionUnhealthy().WithMessage(err.Error()))
791805

792806
_ = r.client.Status().Update(ctx, pr)
793807

794-
r.record.Event(pr, event.Warning(reasonLint, err))
808+
r.record.Event(pr, event.Warning(reasonValidate, err))
795809

796-
// NOTE(hasheddan): a failed lint typically will require manual
797-
// intervention, but on the off chance that we read pod logs
798-
// early, which caused a linting failure, we will requeue by
799-
// returning an error.
800810
return reconcile.Result{}, err
801811
}
802812

813+
// Lint package using package-specific linter. We can proceed with
814+
// installation even there are lint errors; we just record them in an event
815+
// for the user's information since they may cause unexpected behavior.
816+
if err := r.linter.Lint(pkg); err != nil {
817+
err = errors.Wrap(err, errLintPackage)
818+
r.record.Event(pr, event.Warning(reasonLint, err))
819+
// TODO(adamwg): Should we also record lint errors in the status for
820+
// posterity? Events are ephemeral.
821+
}
822+
803823
// NOTE(hasheddan): the linter should check this property already, but
804824
// if a consumer forgets to pass an option to guarantee one meta object,
805825
// we check here to avoid a potential panic on 0 index below.

internal/controller/pkg/revision/reconciler_test.go

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636

3737
xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1"
3838
"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
39+
"github.com/crossplane/crossplane-runtime/v2/pkg/event"
3940
"github.com/crossplane/crossplane-runtime/v2/pkg/feature"
4041
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
4142
"github.com/crossplane/crossplane-runtime/v2/pkg/meta"
@@ -753,8 +754,8 @@ func TestReconcile(t *testing.T) {
753754
err: errors.Wrap(errBoom, errParsePackage),
754755
},
755756
},
756-
"ErrLint": {
757-
reason: "We should return an error if fail to lint the package.",
757+
"ErrValidate": {
758+
reason: "We should return an error if validation fails.",
758759
args: args{
759760
mgr: &fake.Manager{},
760761
rec: []ReconcilerOption{
@@ -771,7 +772,7 @@ func TestReconcile(t *testing.T) {
771772
want := &v1.ProviderRevision{}
772773
want.SetGroupVersionKind(v1.ProviderRevisionGroupVersionKind)
773774
want.SetDesiredState(v1.PackageRevisionActive)
774-
want.SetConditions(v1.RevisionUnhealthy().WithMessage("linting package contents failed: boom"))
775+
want.SetConditions(v1.RevisionUnhealthy().WithMessage("validating package contents failed: boom"))
775776

776777
if diff := cmp.Diff(want, o); diff != "" {
777778
t.Errorf("-want, +got:\n%s", diff)
@@ -785,7 +786,8 @@ func TestReconcile(t *testing.T) {
785786
}}),
786787
WithParser(parser.New(metaScheme, objScheme)),
787788
WithParserBackend(parser.NewEchoBackend(string(providerBytes))),
788-
WithLinter(&MockLinter{MockLint: NewMockLintFn(errBoom)}),
789+
WithValidator(&MockLinter{MockLint: NewMockLintFn(errBoom)}),
790+
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
789791
WithCache(&xpkgfake.MockCache{
790792
MockHas: xpkgfake.NewMockCacheHasFn(false),
791793
MockStore: func(_ string, rc io.ReadCloser) error {
@@ -800,7 +802,7 @@ func TestReconcile(t *testing.T) {
800802
},
801803
},
802804
want: want{
803-
err: errors.Wrap(errBoom, errLintPackage),
805+
err: errors.Wrap(errBoom, errValidatePackage),
804806
},
805807
},
806808
"ErrCrossplaneConstraints": {
@@ -853,6 +855,7 @@ func TestReconcile(t *testing.T) {
853855
return err
854856
},
855857
}),
858+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
856859
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
857860
WithVersioner(&verfake.MockVersioner{
858861
MockInConstraints: verfake.NewMockInConstraintsFn(false, errBoom),
@@ -904,6 +907,7 @@ func TestReconcile(t *testing.T) {
904907
MockHas: xpkgfake.NewMockCacheHasFn(false),
905908
MockStore: xpkgfake.NewMockCacheStoreFn(nil),
906909
}),
910+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
907911
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
908912
WithConfigStore(&xpkgfake.MockConfigStore{
909913
MockPullSecretFor: xpkgfake.NewMockConfigStorePullSecretForFn("", "", nil),
@@ -956,6 +960,7 @@ func TestReconcile(t *testing.T) {
956960
return err
957961
},
958962
}),
963+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
959964
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
960965
WithConfigStore(&xpkgfake.MockConfigStore{
961966
MockPullSecretFor: xpkgfake.NewMockConfigStorePullSecretForFn("", "", nil),
@@ -1023,6 +1028,7 @@ func TestReconcile(t *testing.T) {
10231028
return err
10241029
},
10251030
}),
1031+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
10261032
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
10271033
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
10281034
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1035,6 +1041,76 @@ func TestReconcile(t *testing.T) {
10351041
err: errors.Wrap(errBoom, errResolveDeps),
10361042
},
10371043
},
1044+
"SuccessfulWithLintErrors": {
1045+
reason: "We should record an event but successfully install the package if linting fails.",
1046+
args: args{
1047+
mgr: &fake.Manager{},
1048+
rec: []ReconcilerOption{
1049+
WithNewPackageRevisionFn(func() v1.PackageRevision { return &v1.ProviderRevision{} }),
1050+
WithClientApplicator(resource.ClientApplicator{
1051+
Client: &test.MockClient{
1052+
MockGet: test.NewMockGetFn(nil, func(o client.Object) error {
1053+
pr := o.(*v1.ProviderRevision)
1054+
pr.SetGroupVersionKind(v1.ProviderRevisionGroupVersionKind)
1055+
pr.SetDesiredState(v1.PackageRevisionActive)
1056+
return nil
1057+
}),
1058+
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(o client.Object) error {
1059+
want := &v1.ProviderRevision{}
1060+
want.SetGroupVersionKind(v1.ProviderRevisionGroupVersionKind)
1061+
want.SetDesiredState(v1.PackageRevisionActive)
1062+
want.SetAnnotations(map[string]string{"author": "crossplane"})
1063+
want.SetConditions(v1.RevisionHealthy())
1064+
1065+
if diff := cmp.Diff(want, o); diff != "" {
1066+
t.Errorf("-want, +got:\n%s", diff)
1067+
}
1068+
return nil
1069+
}),
1070+
MockUpdate: test.NewMockUpdateFn(nil, func(o client.Object) error {
1071+
want := &v1.ProviderRevision{}
1072+
want.SetGroupVersionKind(v1.ProviderRevisionGroupVersionKind)
1073+
want.SetDesiredState(v1.PackageRevisionActive)
1074+
want.SetAnnotations(map[string]string{"author": "crossplane"})
1075+
if diff := cmp.Diff(want, o); diff != "" {
1076+
t.Errorf("-want, +got:\n%s", diff)
1077+
}
1078+
return nil
1079+
}),
1080+
1081+
MockDelete: test.NewMockDeleteFn(nil),
1082+
},
1083+
}),
1084+
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error {
1085+
return nil
1086+
}}),
1087+
WithParser(parser.New(metaScheme, objScheme)),
1088+
WithEstablisher(NewMockEstablisher()),
1089+
WithParserBackend(parser.NewEchoBackend(string(providerBytes))),
1090+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
1091+
WithLinter(&MockLinter{MockLint: NewMockLintFn(errBoom)}),
1092+
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
1093+
WithCache(&xpkgfake.MockCache{
1094+
MockHas: xpkgfake.NewMockCacheHasFn(false),
1095+
MockStore: func(_ string, rc io.ReadCloser) error {
1096+
_, err := io.ReadAll(rc)
1097+
return err
1098+
},
1099+
}),
1100+
WithConfigStore(&xpkgfake.MockConfigStore{
1101+
MockPullSecretFor: xpkgfake.NewMockConfigStorePullSecretForFn("", "", nil),
1102+
MockRewritePath: xpkgfake.NewMockRewritePathFn("", "", nil),
1103+
}),
1104+
WithRecorder(newTestRecorder(
1105+
event.Warning(reasonLint, errors.Wrap(errBoom, errLintPackage)),
1106+
event.Normal(reasonSync, "Successfully reconciled package revision"),
1107+
)),
1108+
},
1109+
},
1110+
want: want{
1111+
err: nil,
1112+
},
1113+
},
10381114
"SuccessfulActiveRevision": {
10391115
reason: "An active revision should establish control of all of its resources.",
10401116
args: args{
@@ -1088,6 +1164,7 @@ func TestReconcile(t *testing.T) {
10881164
return err
10891165
},
10901166
}),
1167+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
10911168
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
10921169
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
10931170
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1145,6 +1222,7 @@ func TestReconcile(t *testing.T) {
11451222
return err
11461223
},
11471224
}),
1225+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
11481226
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
11491227
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
11501228
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1226,6 +1304,7 @@ func TestReconcile(t *testing.T) {
12261304
return err
12271305
},
12281306
}),
1307+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
12291308
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
12301309
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
12311310
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1295,6 +1374,7 @@ func TestReconcile(t *testing.T) {
12951374
return err
12961375
},
12971376
}),
1377+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
12981378
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
12991379
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(false, nil)}),
13001380
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1361,6 +1441,7 @@ func TestReconcile(t *testing.T) {
13611441
return err
13621442
},
13631443
}),
1444+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
13641445
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
13651446
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
13661447
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1503,6 +1584,7 @@ func TestReconcile(t *testing.T) {
15031584
return err
15041585
},
15051586
}),
1587+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
15061588
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
15071589
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
15081590
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1631,6 +1713,7 @@ func TestReconcile(t *testing.T) {
16311713
return err
16321714
},
16331715
}),
1716+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
16341717
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
16351718
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
16361719
},
@@ -1693,6 +1776,7 @@ func TestReconcile(t *testing.T) {
16931776
return err
16941777
},
16951778
}),
1779+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
16961780
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
16971781
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
16981782
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1855,6 +1939,7 @@ func TestReconcile(t *testing.T) {
18551939
return err
18561940
},
18571941
}),
1942+
WithValidator(&MockLinter{MockLint: NewMockLintFn(nil)}),
18581943
WithLinter(&MockLinter{MockLint: NewMockLintFn(nil)}),
18591944
WithVersioner(&verfake.MockVersioner{MockInConstraints: verfake.NewMockInConstraintsFn(true, nil)}),
18601945
WithConfigStore(&xpkgfake.MockConfigStore{
@@ -1924,6 +2009,12 @@ func TestReconcile(t *testing.T) {
19242009
if diff := cmp.Diff(tc.want.r, got, test.EquateErrors()); diff != "" {
19252010
t.Errorf("\n%s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff)
19262011
}
2012+
2013+
if tr, ok := r.record.(*testRecorder); ok {
2014+
if diff := cmp.Diff(tr.Want, tr.Got); diff != "" {
2015+
t.Errorf("\n%s\nr.Reconcile(...): -want events, +got events:\n%s", tc.reason, diff)
2016+
}
2017+
}
19272018
})
19282019
}
19292020
}
@@ -1934,3 +2025,23 @@ func signatureVerificationEnabled() *feature.Flags {
19342025

19352026
return f
19362027
}
2028+
2029+
// testRecorder allows asserting event creation.
2030+
type testRecorder struct {
2031+
Want []event.Event
2032+
Got []event.Event
2033+
}
2034+
2035+
func (r *testRecorder) Event(_ runtime.Object, e event.Event) {
2036+
r.Got = append(r.Got, e)
2037+
}
2038+
2039+
func (r *testRecorder) WithAnnotations(_ ...string) event.Recorder {
2040+
return r
2041+
}
2042+
2043+
func newTestRecorder(expected ...event.Event) *testRecorder {
2044+
return &testRecorder{
2045+
Want: expected,
2046+
}
2047+
}

0 commit comments

Comments
 (0)