Skip to content

Commit c267427

Browse files
committed
feat: add GVK existence check for OLM supported resources by querying discovery in
case of 404 not found errors when applying installplan steps.
1 parent b4a2199 commit c267427

File tree

5 files changed

+181
-10
lines changed

5 files changed

+181
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package catalog
2+
3+
import (
4+
"fmt"
5+
6+
"k8s.io/apimachinery/pkg/api/errors"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"k8s.io/client-go/discovery"
9+
10+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
)
12+
13+
// gvkNotFoundError is returned from installplan execution when a step contains a GVK that is not found on cluster.
14+
type gvkNotFoundError struct {
15+
gvk schema.GroupVersionKind
16+
name string
17+
}
18+
19+
func (g gvkNotFoundError) Error() string {
20+
return fmt.Sprintf("api-server resource not found installing %s %s: GroupVersionKind %s not found on the cluster. %s", g.gvk.Kind, g.name, g.gvk,
21+
"This API may have been deprecated and removed, see https://kubernetes.io/docs/reference/using-api/deprecation-guide/ for more information.")
22+
}
23+
24+
type DiscoveryQuerier interface {
25+
QueryForGVK() error
26+
}
27+
28+
type DiscoveryQuerierFunc func() error
29+
30+
func (d DiscoveryQuerierFunc) QueryForGVK() error {
31+
return d()
32+
}
33+
34+
type discoveryQuerier struct {
35+
client discovery.DiscoveryInterface
36+
}
37+
38+
func newDiscoveryQuerier(client discovery.DiscoveryInterface) *discoveryQuerier {
39+
return &discoveryQuerier{client: client}
40+
}
41+
42+
// WithStepResource returns a DiscoveryQuerier which uses discovery to query for supported APIs on the server based on the provided step's GVK.
43+
func (d *discoveryQuerier) WithStepResource(stepResource operatorsv1alpha1.StepResource) DiscoveryQuerier {
44+
var f DiscoveryQuerierFunc = func() error {
45+
gvk := schema.GroupVersionKind{Group: stepResource.Group, Version: stepResource.Version, Kind: stepResource.Kind}
46+
47+
resourceList, err := d.client.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
48+
if err != nil {
49+
if errors.IsNotFound(err) {
50+
return gvkNotFoundError{gvk: gvk, name: stepResource.Name}
51+
}
52+
return err
53+
}
54+
55+
if resourceList == nil {
56+
return gvkNotFoundError{gvk: gvk, name: stepResource.Name}
57+
}
58+
59+
for _, resource := range resourceList.APIResources {
60+
if resource.Kind == stepResource.Kind {
61+
// this kind is supported for this particular GroupVersion
62+
return nil
63+
}
64+
}
65+
66+
return gvkNotFoundError{gvk: gvk, name: stepResource.Name}
67+
}
68+
return f
69+
}

pkg/controller/operators/catalog/operator.go

+12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"sync"
1212
"time"
1313

14+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
15+
1416
errorwrap "github.com/pkg/errors"
1517
"github.com/sirupsen/logrus"
1618
"google.golang.org/grpc/connectivity"
@@ -1774,6 +1776,8 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
17741776
ensurer := newStepEnsurer(kubeclient, crclient, dynamicClient)
17751777
r := newManifestResolver(plan.GetNamespace(), o.lister.CoreV1().ConfigMapLister(), o.logger)
17761778

1779+
discoveryQuerier := newDiscoveryQuerier(o.opClient.KubernetesInterface().Discovery())
1780+
17771781
// CRDs should be installed via the default OLM (cluster-admin) client and not the scoped client specified by the AttenuatedServiceAccount
17781782
// the StepBuilder is currently only implemented for CRD types
17791783
// TODO give the StepBuilder both OLM and scoped clients when it supports new scoped types
@@ -2173,6 +2177,14 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
21732177
}
21742178
return nil
21752179
}(i, step); err != nil {
2180+
if k8serrors.IsNotFound(err) {
2181+
// Check for APIVersions present in the installplan steps that are not available on the server.
2182+
// The check is made via discovery per step in the plan. Transient communication failures to the api-server are handled by the plan retry logic.
2183+
notFoundErr := discoveryQuerier.WithStepResource(step.Resource).QueryForGVK()
2184+
if notFoundErr != nil {
2185+
return notFoundErr
2186+
}
2187+
}
21762188
return err
21772189
}
21782190
}

test/e2e/deprecated_e2e_test.go

+86-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,89 @@
11
package e2e
22

3-
// TODO
3+
import (
4+
"context"
5+
"time"
46

5-
// v1beta1 CRD in installplan fails
6-
// v1beta1 RBAC in an installplan fails
7+
"github.com/blang/semver/v4"
8+
. "github.com/onsi/ginkgo"
9+
"github.com/onsi/ginkgo/extensions/table"
10+
. "github.com/onsi/gomega"
11+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
15+
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx"
16+
)
17+
18+
var missingAPI = `{"apiVersion":"verticalpodautoscalers.autoscaling.k8s.io/v1","kind":"VerticalPodAutoscaler","metadata":{"name":"my.thing","namespace":"foo"}}`
19+
20+
var _ = Describe("Not found APIs", func() {
21+
BeforeEach(func() {
22+
csv := newCSV("test-csv", testNamespace, "", semver.Version{}, nil, nil, nil)
23+
Expect(ctx.Ctx().Client().Create(context.TODO(), &csv)).To(Succeed())
24+
})
25+
AfterEach(func() {
26+
TearDown(testNamespace)
27+
})
28+
29+
When("objects with APIs that are not on-cluster are created in the installplan", func() {
30+
// each entry is an installplan with a deprecated resource
31+
type payload struct {
32+
name string
33+
ip *operatorsv1alpha1.InstallPlan
34+
errMessage string
35+
}
36+
37+
var tableEntries []table.TableEntry
38+
tableEntries = []table.TableEntry{
39+
table.Entry("contains an entry with a missing API not found on cluster ", payload{
40+
name: "installplan contains a missing API",
41+
ip: &operatorsv1alpha1.InstallPlan{
42+
ObjectMeta: metav1.ObjectMeta{
43+
Namespace: *namespace, // this is necessary due to ginkgo table semantics, see https://github.com/onsi/ginkgo/issues/378
44+
Name: "test-plan-api",
45+
},
46+
Spec: operatorsv1alpha1.InstallPlanSpec{
47+
Approval: operatorsv1alpha1.ApprovalAutomatic,
48+
Approved: true,
49+
ClusterServiceVersionNames: []string{},
50+
},
51+
Status: operatorsv1alpha1.InstallPlanStatus{
52+
Phase: operatorsv1alpha1.InstallPlanPhaseInstalling,
53+
CatalogSources: []string{},
54+
Plan: []*operatorsv1alpha1.Step{
55+
{
56+
Resolving: "test-csv",
57+
Status: operatorsv1alpha1.StepStatusUnknown,
58+
Resource: operatorsv1alpha1.StepResource{
59+
Name: "my.thing",
60+
Group: "verticalpodautoscalers.autoscaling.k8s.io",
61+
Version: "v1",
62+
Kind: "VerticalPodAutoscaler",
63+
Manifest: missingAPI,
64+
},
65+
},
66+
},
67+
},
68+
},
69+
errMessage: "api-server resource not found installing VerticalPodAutoscaler my.thing: GroupVersionKind " +
70+
"verticalpodautoscalers.autoscaling.k8s.io/v1, Kind=VerticalPodAutoscaler not found on the cluster",
71+
}),
72+
}
73+
74+
table.DescribeTable("the ip enters a failed state with a helpful error message", func(tt payload) {
75+
Expect(ctx.Ctx().Client().Create(context.Background(), tt.ip)).To(Succeed())
76+
Expect(ctx.Ctx().Client().Status().Update(context.Background(), tt.ip)).To(Succeed())
77+
78+
// The IP sits in the Installing phase with the GVK missing error
79+
Eventually(func() (*operatorsv1alpha1.InstallPlan, error) {
80+
return tt.ip, ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(tt.ip), tt.ip)
81+
}).Should(And(HavePhase(operatorsv1alpha1.InstallPlanPhaseInstalling)), HaveMessage(tt.errMessage))
82+
83+
// Eventually the IP fails with the GVK missing error, after installplan retries, which is by default 1 minute.
84+
Eventually(func() (*operatorsv1alpha1.InstallPlan, error) {
85+
return tt.ip, ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(tt.ip), tt.ip)
86+
}, 2*time.Minute).Should(And(HavePhase(operatorsv1alpha1.InstallPlanPhaseFailed)), HaveMessage(tt.errMessage))
87+
}, tableEntries...)
88+
})
89+
})

test/e2e/installplan_e2e_test.go

-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"github.com/onsi/ginkgo/extensions/table"
1717
. "github.com/onsi/gomega"
1818
. "github.com/onsi/gomega/gstruct"
19-
"github.com/onsi/gomega/types"
2019
"github.com/stretchr/testify/assert"
2120
"github.com/stretchr/testify/require"
2221
appsv1 "k8s.io/api/apps/v1"
@@ -53,12 +52,6 @@ import (
5352
)
5453

5554
var _ = Describe("Install Plan", func() {
56-
HavePhase := func(goal operatorsv1alpha1.InstallPlanPhase) types.GomegaMatcher {
57-
return WithTransform(func(plan *operatorsv1alpha1.InstallPlan) operatorsv1alpha1.InstallPlanPhase {
58-
return plan.Status.Phase
59-
}, Equal(goal))
60-
}
61-
6255
AfterEach(func() {
6356
TearDown(testNamespace)
6457
})

test/e2e/util_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import (
3535
"k8s.io/client-go/rest"
3636
k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client"
3737

38+
gtypes "github.com/onsi/gomega/types"
3839
"github.com/operator-framework/api/pkg/operators/v1alpha1"
40+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
3941
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
4042
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
4143
controllerclient "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/controller-runtime/client"
@@ -897,3 +899,15 @@ func deploymentReplicas(replicas int32) predicateFunc {
897899
func Apply(obj controllerclient.Object, changeFunc interface{}) func() error {
898900
return ctx.Ctx().SSAClient().Apply(context.Background(), obj, changeFunc)
899901
}
902+
903+
func HavePhase(goal operatorsv1alpha1.InstallPlanPhase) gtypes.GomegaMatcher {
904+
return WithTransform(func(plan *operatorsv1alpha1.InstallPlan) operatorsv1alpha1.InstallPlanPhase {
905+
return plan.Status.Phase
906+
}, Equal(goal))
907+
}
908+
909+
func HaveMessage(goal string) gtypes.GomegaMatcher {
910+
return WithTransform(func(plan *operatorsv1alpha1.InstallPlan) string {
911+
return plan.Status.Message
912+
}, ContainSubstring(goal))
913+
}

0 commit comments

Comments
 (0)