Skip to content

Commit 4ddebe0

Browse files
authored
fix empty action config client and update catalog creation unit test (#226)
Also adding some nil value checks for printing clusterCatalogs and clusterExtensions. Signed-off-by: Ankita Thomas <ankithom@redhat.com>
1 parent 7794db4 commit 4ddebe0

File tree

8 files changed

+152
-78
lines changed

8 files changed

+152
-78
lines changed

internal/cmd/internal/olmv1/catalog_create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
// NewCatalogCreateCmd allows creating a new catalog
1515
func NewCatalogCreateCmd(cfg *action.Configuration) *cobra.Command {
16-
i := v1action.NewCatalogCreate(cfg.Client)
16+
i := v1action.NewCatalogCreate(cfg)
1717
i.Logf = log.Printf
1818

1919
cmd := &cobra.Command{

internal/cmd/internal/olmv1/printing.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@ func printFormattedOperators(extensions ...olmv1.ClusterExtension) {
2121

2222
sortOperators(extensions)
2323
for _, ext := range extensions {
24+
var bundleName, bundleVersion string
25+
if ext.Status.Install != nil {
26+
bundleName = ext.Status.Install.Bundle.Name
27+
bundleVersion = ext.Status.Install.Bundle.Version
28+
}
2429
age := time.Since(ext.CreationTimestamp.Time)
2530
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
2631
ext.Name,
27-
ext.Status.Install.Bundle.Name,
28-
ext.Status.Install.Bundle.Version,
32+
bundleName,
33+
bundleVersion,
2934
ext.Spec.Source.SourceType,
3035
status(ext.Status.Conditions, olmv1.TypeInstalled),
3136
status(ext.Status.Conditions, olmv1.TypeProgressing),
@@ -41,13 +46,16 @@ func printFormattedCatalogs(catalogs ...olmv1.ClusterCatalog) {
4146

4247
sortCatalogs(catalogs)
4348
for _, cat := range catalogs {
49+
var lastUnpacked string
50+
if cat.Status.LastUnpacked != nil {
51+
duration.HumanDuration(time.Since(cat.Status.LastUnpacked.Time))
52+
}
4453
age := time.Since(cat.CreationTimestamp.Time)
45-
lastUnpacked := time.Since(cat.Status.LastUnpacked.Time)
4654
_, _ = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\t%s\t%s\n",
4755
cat.Name,
4856
string(cat.Spec.AvailabilityMode),
4957
cat.Spec.Priority,
50-
duration.HumanDuration(lastUnpacked),
58+
lastUnpacked,
5159
status(cat.Status.Conditions, olmv1.TypeServing),
5260
duration.HumanDuration(age),
5361
)
@@ -59,6 +67,9 @@ func printFormattedCatalogs(catalogs ...olmv1.ClusterCatalog) {
5967
// name (asc), version (desc)
6068
func sortOperators(extensions []olmv1.ClusterExtension) {
6169
slices.SortFunc(extensions, func(a, b olmv1.ClusterExtension) int {
70+
if a.Status.Install == nil || b.Status.Install == nil {
71+
return cmp.Compare(a.Name, b.Name)
72+
}
6273
return cmp.Or(
6374
cmp.Compare(a.Name, b.Name),
6475
-semver.MustParse(a.Status.Install.Bundle.Version).Compare(semver.MustParse(b.Status.Install.Bundle.Version)),

internal/pkg/v1/action/action_suite_test.go

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,85 @@ import (
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
"k8s.io/apimachinery/pkg/types"
1414
"sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
16+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
1517

1618
olmv1 "github.com/operator-framework/operator-controller/api/v1"
19+
20+
"github.com/operator-framework/kubectl-operator/pkg/action"
21+
)
22+
23+
const (
24+
verbCreate = "create"
1725
)
1826

1927
func TestCommand(t *testing.T) {
2028
RegisterFailHandler(Fail)
2129
RunSpecs(t, "Internal v1 action Suite")
2230
}
2331

24-
type mockCreator struct {
25-
createErr error
26-
createCalled int
27-
}
28-
29-
func (mc *mockCreator) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
30-
mc.createCalled++
31-
return mc.createErr
32-
}
32+
type fakeClient struct {
33+
// Expected errors for create/delete/get.
34+
createErr error
35+
deleteErr error
36+
getErr error
3337

34-
type mockDeleter struct {
35-
deleteErr error
38+
// counters for number of create/delete/get calls seen.
39+
createCalled int
3640
deleteCalled int
37-
}
41+
getCalled int
3842

39-
func (md *mockDeleter) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
40-
md.deleteCalled++
41-
return md.deleteErr
43+
// transformer functions for applying changes to an object
44+
// matching the objectKey prior to an operation of the
45+
// type `verb` (get/create/delete), where the operation is
46+
// not set to error fail with a corresponding error (getErr/createErr/deleteErr).
47+
transformers []objectTransformer
48+
client.Client
4249
}
4350

44-
type mockGetter struct {
45-
getErr error
46-
getCalled int
51+
type objectTransformer struct {
52+
verb string
53+
objectKey client.ObjectKey
54+
transformFunc func(obj *client.Object)
4755
}
4856

49-
func (mg *mockGetter) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
50-
mg.getCalled++
51-
return mg.getErr
57+
func (c *fakeClient) Initialize() error {
58+
scheme, err := action.NewScheme()
59+
if err != nil {
60+
return err
61+
}
62+
clientBuilder := fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
63+
Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
64+
c.createCalled++
65+
if c.createErr != nil {
66+
return c.createErr
67+
}
68+
objKey := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
69+
for _, t := range c.transformers {
70+
if t.verb == verbCreate && objKey == t.objectKey && t.transformFunc != nil {
71+
t.transformFunc(&obj)
72+
}
73+
}
74+
// make sure to plumb request through to underlying client
75+
return client.Create(ctx, obj, opts...)
76+
},
77+
Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error {
78+
c.deleteCalled++
79+
if c.deleteErr != nil {
80+
return c.deleteErr
81+
}
82+
return client.Delete(ctx, obj, opts...)
83+
},
84+
Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
85+
c.getCalled++
86+
if c.getErr != nil {
87+
return c.getErr
88+
}
89+
return client.Get(ctx, key, obj, opts...)
90+
},
91+
}).WithScheme(scheme)
92+
c.Client = clientBuilder.Build()
93+
return nil
5294
}
5395

5496
func setupTestCatalogs(n int) []client.Object {

internal/pkg/v1/action/catalog_create.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@ import (
77
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
88

99
olmv1 "github.com/operator-framework/operator-controller/api/v1"
10-
)
1110

12-
type createClient interface {
13-
creator
14-
deleter
15-
getter
16-
}
11+
"github.com/operator-framework/kubectl-operator/pkg/action"
12+
)
1713

1814
type CatalogCreate struct {
19-
client createClient
15+
config *action.Configuration
2016
CatalogName string
2117
ImageSourceRef string
2218

@@ -29,28 +25,28 @@ type CatalogCreate struct {
2925
Logf func(string, ...interface{})
3026
}
3127

32-
func NewCatalogCreate(client createClient) *CatalogCreate {
28+
func NewCatalogCreate(config *action.Configuration) *CatalogCreate {
3329
return &CatalogCreate{
34-
client: client,
30+
config: config,
3531
Logf: func(string, ...interface{}) {},
3632
}
3733
}
3834

3935
func (i *CatalogCreate) Run(ctx context.Context) error {
4036
catalog := i.buildCatalog()
41-
if err := i.client.Create(ctx, &catalog); err != nil {
37+
if err := i.config.Client.Create(ctx, &catalog); err != nil {
4238
return err
4339
}
4440

4541
var err error
4642
if i.Available {
47-
err = waitUntilCatalogStatusCondition(ctx, i.client, &catalog, olmv1.TypeServing, metav1.ConditionTrue)
43+
err = waitUntilCatalogStatusCondition(ctx, i.config.Client, &catalog, olmv1.TypeServing, metav1.ConditionTrue)
4844
} else {
49-
err = waitUntilCatalogStatusCondition(ctx, i.client, &catalog, olmv1.TypeServing, metav1.ConditionFalse)
45+
err = waitUntilCatalogStatusCondition(ctx, i.config.Client, &catalog, olmv1.TypeServing, metav1.ConditionFalse)
5046
}
5147

5248
if err != nil {
53-
if cleanupErr := deleteWithTimeout(i.client, &catalog, i.CleanupTimeout); cleanupErr != nil {
49+
if cleanupErr := deleteWithTimeout(i.config.Client, &catalog, i.CleanupTimeout); cleanupErr != nil {
5450
i.Logf("cleaning up failed catalog: %v", cleanupErr)
5551
}
5652
return err

internal/pkg/v1/action/catalog_create_test.go

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,21 @@ import (
88
. "github.com/onsi/gomega"
99

1010
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/types"
1112
"sigs.k8s.io/controller-runtime/pkg/client"
1213

1314
olmv1 "github.com/operator-framework/operator-controller/api/v1"
1415

1516
internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
17+
"github.com/operator-framework/kubectl-operator/pkg/action"
1618
)
1719

18-
type mockCreateClient struct {
19-
*mockCreator
20-
*mockGetter
21-
*mockDeleter
22-
createCatalog *olmv1.ClusterCatalog
23-
}
24-
25-
func (mcc *mockCreateClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
26-
mcc.createCatalog = obj.(*olmv1.ClusterCatalog)
27-
return mcc.mockCreator.Create(ctx, obj, opts...)
28-
}
29-
3020
var _ = Describe("CatalogCreate", func() {
21+
catalogName := "testcatalog"
3122
pollInterval := 20
3223
expectedCatalog := olmv1.ClusterCatalog{
3324
ObjectMeta: metav1.ObjectMeta{
34-
Name: "testcatalog",
25+
Name: catalogName,
3526
Labels: map[string]string{"a": "b"},
3627
},
3728
Spec: olmv1.ClusterCatalogSpec{
@@ -49,9 +40,10 @@ var _ = Describe("CatalogCreate", func() {
4940

5041
It("fails creating catalog", func() {
5142
expectedErr := errors.New("create failed")
52-
mockClient := &mockCreateClient{&mockCreator{createErr: expectedErr}, nil, nil, &expectedCatalog}
43+
testClient := fakeClient{createErr: expectedErr}
44+
Expect(testClient.Initialize()).To(Succeed())
5345

54-
creator := internalaction.NewCatalogCreate(mockClient)
46+
creator := internalaction.NewCatalogCreate(&action.Configuration{Client: testClient})
5547
creator.Available = true
5648
creator.CatalogName = expectedCatalog.Name
5749
creator.ImageSourceRef = expectedCatalog.Spec.Source.Image.Ref
@@ -62,41 +54,78 @@ var _ = Describe("CatalogCreate", func() {
6254

6355
Expect(err).NotTo(BeNil())
6456
Expect(err).To(MatchError(expectedErr))
65-
Expect(mockClient.createCalled).To(Equal(1))
66-
67-
// there is no way of testing a happy path in unit tests because we have no way to
68-
// set/mock the catalog status condition we're waiting for in waitUntilCatalogStatusCondition
69-
// but we can still at least verify that CR would have been created with expected attribute values
70-
validateCreateCatalog(mockClient.createCatalog, &expectedCatalog)
57+
Expect(testClient.createCalled).To(Equal(1))
7158
})
7259

7360
It("fails waiting for created catalog status, successfully cleans up", func() {
7461
expectedErr := errors.New("get failed")
75-
mockClient := &mockCreateClient{&mockCreator{}, &mockGetter{getErr: expectedErr}, &mockDeleter{}, nil}
62+
testClient := fakeClient{getErr: expectedErr}
63+
Expect(testClient.Initialize()).To(Succeed())
7664

77-
creator := internalaction.NewCatalogCreate(mockClient)
65+
creator := internalaction.NewCatalogCreate(&action.Configuration{Client: testClient})
66+
// fakeClient requires at least the catalogName to be set to run
67+
creator.CatalogName = expectedCatalog.Name
7868
err := creator.Run(context.TODO())
7969

8070
Expect(err).NotTo(BeNil())
8171
Expect(err).To(MatchError(expectedErr))
82-
Expect(mockClient.createCalled).To(Equal(1))
83-
Expect(mockClient.getCalled).To(Equal(1))
84-
Expect(mockClient.deleteCalled).To(Equal(1))
72+
Expect(testClient.createCalled).To(Equal(1))
73+
Expect(testClient.getCalled).To(Equal(1))
74+
Expect(testClient.deleteCalled).To(Equal(1))
8575
})
8676

8777
It("fails waiting for created catalog status, fails clean up", func() {
8878
getErr := errors.New("get failed")
8979
deleteErr := errors.New("delete failed")
90-
mockClient := &mockCreateClient{&mockCreator{}, &mockGetter{getErr: getErr}, &mockDeleter{deleteErr: deleteErr}, nil}
80+
testClient := fakeClient{deleteErr: deleteErr, getErr: getErr}
81+
Expect(testClient.Initialize()).To(Succeed())
9182

92-
creator := internalaction.NewCatalogCreate(mockClient)
83+
creator := internalaction.NewCatalogCreate(&action.Configuration{Client: testClient})
84+
// fakeClient requires at least the catalogName to be set to run
85+
creator.CatalogName = expectedCatalog.Name
9386
err := creator.Run(context.TODO())
9487

9588
Expect(err).NotTo(BeNil())
9689
Expect(err).To(MatchError(getErr))
97-
Expect(mockClient.createCalled).To(Equal(1))
98-
Expect(mockClient.getCalled).To(Equal(1))
99-
Expect(mockClient.deleteCalled).To(Equal(1))
90+
Expect(testClient.createCalled).To(Equal(1))
91+
Expect(testClient.getCalled).To(Equal(1))
92+
Expect(testClient.deleteCalled).To(Equal(1))
93+
})
94+
It("succeeds creating catalog", func() {
95+
testClient := fakeClient{
96+
transformers: []objectTransformer{
97+
{
98+
verb: verbCreate,
99+
objectKey: types.NamespacedName{Name: catalogName},
100+
transformFunc: func(obj *client.Object) {
101+
if obj == nil {
102+
return
103+
}
104+
catalogObj, ok := (*obj).(*olmv1.ClusterCatalog)
105+
if !ok {
106+
return
107+
}
108+
catalogObj.Status.Conditions = []metav1.Condition{{Type: olmv1.TypeServing, Status: metav1.ConditionTrue}}
109+
},
110+
},
111+
},
112+
}
113+
Expect(testClient.Initialize()).To(Succeed())
114+
115+
creator := internalaction.NewCatalogCreate(&action.Configuration{Client: testClient})
116+
creator.Available = true
117+
creator.CatalogName = expectedCatalog.Name
118+
creator.ImageSourceRef = expectedCatalog.Spec.Source.Image.Ref
119+
creator.Priority = expectedCatalog.Spec.Priority
120+
creator.Labels = expectedCatalog.Labels
121+
creator.PollIntervalMinutes = *expectedCatalog.Spec.Source.Image.PollIntervalMinutes
122+
Expect(creator.Run(context.TODO())).To(Succeed())
123+
124+
Expect(testClient.createCalled).To(Equal(1))
125+
126+
actualCatalog := &olmv1.ClusterCatalog{TypeMeta: metav1.TypeMeta{Kind: "ClusterCatalog", APIVersion: "olm.operatorframework.io/v1"}}
127+
Expect(testClient.Client.Get(context.TODO(), types.NamespacedName{Name: catalogName}, actualCatalog)).To(Succeed())
128+
validateCreateCatalog(actualCatalog, &expectedCatalog)
100129
})
101130
})
102131

internal/pkg/v1/action/helpers.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,8 @@ func deleteWithTimeout(cl deleter, obj client.Object, timeout time.Duration) err
8080
return nil
8181
}
8282

83-
func waitForDeletion(ctx context.Context, cl client.Client, objs ...client.Object) error {
83+
func waitForDeletion(ctx context.Context, cl getter, objs ...client.Object) error {
8484
for _, obj := range objs {
85-
obj := obj
8685
lowerKind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
8786
key := objectKeyForObject(obj)
8887
if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {

internal/pkg/v1/action/interfaces.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import (
66
"sigs.k8s.io/controller-runtime/pkg/client"
77
)
88

9-
type creator interface {
10-
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
11-
}
12-
139
type deleter interface {
1410
Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error
1511
}

internal/pkg/v1/action/operator_update_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,9 @@ var _ = Describe("OperatorUpdate", func() {
145145
cfg := setupEnv(testExt, buildExtension("test2"), buildExtension("test3"))
146146

147147
go func() {
148-
Eventually(updateOperatorConditionStatus("test", cfg.Client, olmv1.TypeInstalled, metav1.ConditionTrue)).
149-
WithTimeout(5 * time.Second).WithPolling(200 * time.Second).
148+
Eventually(updateOperatorConditionStatus).
149+
WithArguments("test", cfg.Client, olmv1.TypeInstalled, metav1.ConditionTrue).
150+
WithTimeout(5 * time.Second).WithPolling(200 * time.Millisecond).
150151
Should(Succeed())
151152
}()
152153

0 commit comments

Comments
 (0)