Skip to content

Commit c737224

Browse files
authored
Add custom StatusReader for Config Connector resources (#2626)
1 parent bbaef48 commit c737224

File tree

9 files changed

+406
-6
lines changed

9 files changed

+406
-6
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca
1515
golang.org/x/mod v0.5.1
1616
gotest.tools v2.2.0+incompatible
17-
k8s.io/api v0.22.3 // indirect
17+
k8s.io/api v0.22.3
1818
k8s.io/apiextensions-apiserver v0.22.2
1919
k8s.io/apimachinery v0.22.3
2020
k8s.io/cli-runtime v0.22.2
@@ -23,6 +23,7 @@ require (
2323
k8s.io/kube-openapi v0.0.0-20211109043139-026bd182f079 // indirect
2424
k8s.io/kubectl v0.22.2
2525
sigs.k8s.io/cli-utils v0.27.0
26+
sigs.k8s.io/controller-runtime v0.10.1
2627
sigs.k8s.io/kustomize/api v0.8.11
2728
sigs.k8s.io/kustomize/kyaml v0.13.1-0.20211203194734-cd2c6a1ad117
2829
)

internal/cmdapply/cmdapply.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/GoogleContainerTools/kpt/internal/util/argutil"
2626
"github.com/GoogleContainerTools/kpt/internal/util/strings"
2727
"github.com/GoogleContainerTools/kpt/pkg/live"
28+
"github.com/GoogleContainerTools/kpt/pkg/status"
2829
"github.com/spf13/cobra"
2930
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -214,10 +215,20 @@ func runApply(r *Runner, invInfo inventory.InventoryInfo, objs []*unstructured.U
214215
if err != nil {
215216
return err
216217
}
218+
219+
statusPoller, err := status.NewStatusPoller(r.factory)
220+
if err != nil {
221+
return err
222+
}
223+
217224
applier, err := apply.NewApplier(r.factory, invClient)
218225
if err != nil {
219226
return err
220227
}
228+
// TODO(mortent): See if we can improve this. Having to change the Applier after it has been
229+
// created feels a bit awkward.
230+
applier.StatusPoller = statusPoller
231+
221232
ch := applier.Run(r.ctx, invInfo, objs, apply.Options{
222233
ServerSideOptions: r.serverSideOptions,
223234
PollInterval: r.period,

internal/cmddestroy/cmddestroy.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/GoogleContainerTools/kpt/internal/util/argutil"
2424
"github.com/GoogleContainerTools/kpt/internal/util/strings"
2525
"github.com/GoogleContainerTools/kpt/pkg/live"
26+
"github.com/GoogleContainerTools/kpt/pkg/status"
2627
"github.com/spf13/cobra"
2728
"k8s.io/cli-runtime/pkg/genericclioptions"
2829
"k8s.io/kubectl/pkg/cmd/util"
@@ -160,10 +161,18 @@ func runDestroy(r *Runner, inv inventory.InventoryInfo, dryRunStrategy common.Dr
160161
if err != nil {
161162
return err
162163
}
164+
165+
statusPoller, err := status.NewStatusPoller(r.factory)
166+
if err != nil {
167+
return err
168+
}
169+
163170
destroyer, err := apply.NewDestroyer(r.factory, invClient)
164171
if err != nil {
165172
return err
166173
}
174+
destroyer.StatusPoller = statusPoller
175+
167176
options := apply.DestroyerOptions{
168177
InventoryPolicy: r.inventoryPolicy,
169178
DryRunStrategy: dryRunStrategy,

pkg/live/inventoryrg.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"strings"
2121
"time"
2222

23+
"github.com/GoogleContainerTools/kpt/pkg/status"
2324
apierrors "k8s.io/apimachinery/pkg/api/errors"
2425
"k8s.io/apimachinery/pkg/api/meta"
2526
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -31,8 +32,6 @@ import (
3132
"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
3233
"sigs.k8s.io/cli-utils/pkg/common"
3334
"sigs.k8s.io/cli-utils/pkg/inventory"
34-
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
35-
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
3635
"sigs.k8s.io/cli-utils/pkg/object"
3736
"sigs.k8s.io/kustomize/kyaml/yaml"
3837
)
@@ -280,7 +279,7 @@ func InstallResourceGroupCRD(factory cmdutil.Factory) error {
280279
for _, t := range tasks {
281280
taskQueue <- t
282281
}
283-
statusPoller, err := polling.NewStatusPollerFromFactory(factory, []engine.StatusReader{})
282+
statusPoller, err := status.NewStatusPoller(factory)
284283
if err != nil {
285284
handleError(eventChannel, err)
286285
return

pkg/status/configconnector.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package status
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"strings"
21+
22+
v1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/meta"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"k8s.io/apimachinery/pkg/types"
27+
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
28+
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
29+
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
30+
"sigs.k8s.io/cli-utils/pkg/object"
31+
)
32+
33+
// ConfigConnectorStatusReader can compute reconcile status for Config Connector
34+
// resources. It leverages information in the `Reason` field of the `Ready` condition.
35+
// TODO(mortent): Make more of the convencience functions and types from cli-utils
36+
// exported so we can simplify this.
37+
type ConfigConnectorStatusReader struct {
38+
Mapper meta.RESTMapper
39+
}
40+
41+
// Supports returns true for all Config Connector resources.
42+
func (c *ConfigConnectorStatusReader) Supports(gk schema.GroupKind) bool {
43+
return strings.HasSuffix(gk.Group, "cnrm.cloud.google.com")
44+
}
45+
46+
func (c *ConfigConnectorStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, id object.ObjMetadata) *event.ResourceStatus {
47+
gvk, err := toGVK(id.GroupKind, c.Mapper)
48+
if err != nil {
49+
return newUnknownResourceStatus(id, nil, err)
50+
}
51+
52+
key := types.NamespacedName{
53+
Name: id.Name,
54+
Namespace: id.Namespace,
55+
}
56+
57+
var u unstructured.Unstructured
58+
u.SetGroupVersionKind(gvk)
59+
err = reader.Get(ctx, key, &u)
60+
if err != nil {
61+
return newUnknownResourceStatus(id, nil, err)
62+
}
63+
64+
return c.ReadStatusForObject(ctx, reader, &u)
65+
}
66+
67+
func (c *ConfigConnectorStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, u *unstructured.Unstructured) *event.ResourceStatus {
68+
id := object.UnstructuredToObjMetadata(u)
69+
70+
// First check if the resource is in the process of being deleted.
71+
deletionTimestamp, found, err := unstructured.NestedString(u.Object, "metadata", "deletionTimestamp")
72+
if err != nil {
73+
return newUnknownResourceStatus(id, u, err)
74+
}
75+
if found && deletionTimestamp != "" {
76+
return newResourceStatus(id, status.TerminatingStatus, u, "Resource scheduled for deletion")
77+
}
78+
79+
// ensure that the meta generation is observed
80+
generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
81+
if err != nil {
82+
e := fmt.Errorf("looking up metadata.generation from resource: %w", err)
83+
return newUnknownResourceStatus(id, u, e)
84+
}
85+
if !found {
86+
e := fmt.Errorf("metadata.generation not found")
87+
return newUnknownResourceStatus(id, u, e)
88+
}
89+
90+
observedGeneration, found, err := unstructured.NestedInt64(u.Object, "status", "observedGeneration")
91+
if err != nil {
92+
e := fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
93+
return newUnknownResourceStatus(id, u, e)
94+
}
95+
if !found {
96+
// We know that Config Connector resources uses the ObservedGeneration pattern, so consider it
97+
// an error if it is not found.
98+
e := fmt.Errorf("status.ObservedGeneration not found")
99+
return newUnknownResourceStatus(id, u, e)
100+
}
101+
if generation != observedGeneration {
102+
msg := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
103+
return newResourceStatus(id, status.InProgressStatus, u, msg)
104+
}
105+
106+
obj, err := status.GetObjectWithConditions(u.Object)
107+
if err != nil {
108+
return newUnknownResourceStatus(id, u, err)
109+
}
110+
111+
var readyCond status.BasicCondition
112+
foundCond := false
113+
for i := range obj.Status.Conditions {
114+
if obj.Status.Conditions[i].Type == "Ready" {
115+
readyCond = obj.Status.Conditions[i]
116+
foundCond = true
117+
}
118+
}
119+
120+
if !foundCond {
121+
return newResourceStatus(id, status.InProgressStatus, u, "Ready condition not set")
122+
}
123+
124+
if readyCond.Status == v1.ConditionTrue {
125+
return newResourceStatus(id, status.CurrentStatus, u, "Resource is Current")
126+
}
127+
128+
switch readyCond.Reason {
129+
case "ManagementConflict", "UpdateFailed", "DeleteFailed", "DependencyInvalid":
130+
return newResourceStatus(id, status.FailedStatus, u, readyCond.Message)
131+
}
132+
133+
return newResourceStatus(id, status.InProgressStatus, u, readyCond.Message)
134+
}
135+
136+
func toGVK(gk schema.GroupKind, mapper meta.RESTMapper) (schema.GroupVersionKind, error) {
137+
mapping, err := mapper.RESTMapping(gk)
138+
if err != nil {
139+
return schema.GroupVersionKind{}, err
140+
}
141+
return mapping.GroupVersionKind, nil
142+
}
143+
144+
func newResourceStatus(id object.ObjMetadata, s status.Status, u *unstructured.Unstructured, msg string) *event.ResourceStatus {
145+
return &event.ResourceStatus{
146+
Identifier: id,
147+
Status: s,
148+
Resource: u,
149+
Message: msg,
150+
}
151+
}
152+
153+
func newUnknownResourceStatus(id object.ObjMetadata, u *unstructured.Unstructured, err error) *event.ResourceStatus {
154+
return &event.ResourceStatus{
155+
Identifier: id,
156+
Status: status.UnknownStatus,
157+
Error: err,
158+
Resource: u,
159+
}
160+
}

pkg/status/configconnector_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package status
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"k8s.io/apimachinery/pkg/runtime/schema"
9+
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
10+
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
11+
"sigs.k8s.io/cli-utils/pkg/object"
12+
fakemapper "sigs.k8s.io/cli-utils/pkg/testutil"
13+
)
14+
15+
func TestSupports(t *testing.T) {
16+
testCases := map[string]struct {
17+
gk schema.GroupKind
18+
supports bool
19+
}{
20+
"matches config connector group": {
21+
gk: schema.GroupKind{
22+
Group: "sql.cnrm.cloud.google.com",
23+
Kind: "SQLDatabase",
24+
},
25+
supports: true,
26+
},
27+
"doesn't match other resources": {
28+
gk: schema.GroupKind{
29+
Group: "apps",
30+
Kind: "StatefulSet",
31+
},
32+
supports: false,
33+
},
34+
}
35+
36+
for tn, tc := range testCases {
37+
t.Run(tn, func(t *testing.T) {
38+
fakeMapper := fakemapper.NewFakeRESTMapper()
39+
40+
statusReader := &ConfigConnectorStatusReader{
41+
Mapper: fakeMapper,
42+
}
43+
44+
supports := statusReader.Supports(tc.gk)
45+
46+
assert.Equal(t, tc.supports, supports)
47+
})
48+
}
49+
}
50+
51+
func TestReadStatus(t *testing.T) {
52+
testCases := map[string]struct {
53+
resource string
54+
gvk schema.GroupVersionKind
55+
expectedStatus status.Status
56+
}{
57+
"Resource with deletionTimestap is Terminating": {
58+
resource: `
59+
apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1
60+
kind: Service
61+
metadata:
62+
name: pubsub.googleapis.com
63+
namespace: cnrm
64+
generation: 42
65+
deletionTimestamp: "2020-01-09T20:56:25Z"
66+
`,
67+
gvk: schema.GroupVersionKind{
68+
Group: "serviceusage.cnrm.cloud.google.com",
69+
Version: "v1beta1",
70+
Kind: "Service",
71+
},
72+
expectedStatus: status.TerminatingStatus,
73+
},
74+
"Resource where observedGeneration doesn't match generation is InProgress": {
75+
resource: `
76+
apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1
77+
kind: Service
78+
metadata:
79+
name: pubsub.googleapis.com
80+
namespace: cnrm
81+
generation: 42
82+
status:
83+
observedGeneration: 41
84+
conditions:
85+
- type: Ready
86+
status: "False"
87+
reason: UpdateFailed
88+
message: "Resource couldn't be updated"
89+
`,
90+
gvk: schema.GroupVersionKind{
91+
Group: "serviceusage.cnrm.cloud.google.com",
92+
Version: "v1beta1",
93+
Kind: "Service",
94+
},
95+
expectedStatus: status.InProgressStatus,
96+
},
97+
"Resource with reason UpdateFailed is Failed": {
98+
resource: `
99+
apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1
100+
kind: Service
101+
metadata:
102+
name: pubsub.googleapis.com
103+
namespace: cnrm
104+
generation: 42
105+
status:
106+
observedGeneration: 42
107+
conditions:
108+
- type: Ready
109+
status: "False"
110+
reason: UpdateFailed
111+
message: "Resource couldn't be updated"
112+
`,
113+
gvk: schema.GroupVersionKind{
114+
Group: "serviceusage.cnrm.cloud.google.com",
115+
Version: "v1beta1",
116+
Kind: "Service",
117+
},
118+
expectedStatus: status.FailedStatus,
119+
},
120+
}
121+
122+
for tn, tc := range testCases {
123+
t.Run(tn, func(t *testing.T) {
124+
obj := testutil.YamlToUnstructured(t, tc.resource)
125+
126+
fakeClusterReader := &fakeClusterReader{
127+
getResource: obj,
128+
}
129+
fakeMapper := fakemapper.NewFakeRESTMapper(tc.gvk)
130+
statusReader := &ConfigConnectorStatusReader{
131+
Mapper: fakeMapper,
132+
}
133+
134+
res := statusReader.ReadStatus(context.Background(), fakeClusterReader, object.UnstructuredToObjMetadata(obj))
135+
assert.Equal(t, tc.expectedStatus, res.Status)
136+
})
137+
}
138+
}

0 commit comments

Comments
 (0)