Skip to content

Commit aa66b29

Browse files
authored
Merge pull request #119 from phisco/readiness_fromobject
feat: deriving readiness from object
2 parents d966cc7 + e30839e commit aa66b29

File tree

7 files changed

+289
-14
lines changed

7 files changed

+289
-14
lines changed

.golangci.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@ linters-settings:
3535
# simplify code: gofmt with `-s` option, true by default
3636
simplify: true
3737

38-
goimports:
39-
# put imports beginning with prefix after 3rd-party packages;
40-
# it's a comma-separated list of prefixes
41-
local-prefixes: github.com/crossplane-contrib/provider-kubernetes
38+
gci:
39+
custom-order: true
40+
sections:
41+
- standard
42+
- default
43+
- prefix(github.com/crossplane)
44+
- prefix(github.com/crossplane-contrib/provider-kubernetes)
45+
- blank
46+
- dot
4247

4348
gocyclo:
4449
# minimal code complexity to report, 30 by default (but we recommend 10-20)
@@ -110,7 +115,7 @@ linters:
110115
- gocritic
111116
- interfacer
112117
- goconst
113-
- goimports
118+
- gci
114119
- gofmt # We enable this as well as goimports for its simplify mode.
115120
- prealloc
116121
- golint

apis/object/v1alpha1/types.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,30 @@ type ObjectSpec struct {
123123
// +kubebuilder:default=Default
124124
ManagementPolicy `json:"managementPolicy,omitempty"`
125125
References []Reference `json:"references,omitempty"`
126+
Readiness Readiness `json:"readiness,omitempty"`
127+
}
128+
129+
// ReadinessPolicy defines how the Object's readiness condition should be computed.
130+
type ReadinessPolicy string
131+
132+
const (
133+
// ReadinessPolicySuccessfulCreate means the object is marked as ready when the
134+
// underlying external resource is successfully created.
135+
ReadinessPolicySuccessfulCreate ReadinessPolicy = "SuccessfulCreate"
136+
// ReadinessPolicyDeriveFromObject means the object is marked as ready if and only if the underlying
137+
// external resource is considered ready.
138+
ReadinessPolicyDeriveFromObject ReadinessPolicy = "DeriveFromObject"
139+
)
140+
141+
// Readiness defines how the object's readiness condition should be computed,
142+
// if not specified it will be considered ready as soon as the underlying external
143+
// resource is considered up-to-date.
144+
type Readiness struct {
145+
// Policy defines how the Object's readiness condition should be computed.
146+
// +optional
147+
// +kubebuilder:validation:Enum=SuccessfulCreate;DeriveFromObject
148+
// +kubebuilder:default=SuccessfulCreate
149+
Policy ReadinessPolicy `json:"policy,omitempty"`
126150
}
127151

128152
// ConnectionDetail represents an entry in the connection secret for an Object

apis/object/v1alpha1/zz_generated.deepcopy.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/provider/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ import (
2121
"path/filepath"
2222
"time"
2323

24-
"github.com/crossplane/crossplane-runtime/pkg/controller"
25-
"github.com/crossplane/crossplane-runtime/pkg/feature"
2624
"go.uber.org/zap/zapcore"
27-
2825
"gopkg.in/alecthomas/kingpin.v2"
29-
_ "k8s.io/client-go/plugin/pkg/client/auth"
3026
"k8s.io/client-go/tools/leaderelection/resourcelock"
3127
ctrl "sigs.k8s.io/controller-runtime"
3228
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3329

30+
"github.com/crossplane/crossplane-runtime/pkg/controller"
31+
"github.com/crossplane/crossplane-runtime/pkg/feature"
3432
"github.com/crossplane/crossplane-runtime/pkg/logging"
3533
"github.com/crossplane/crossplane-runtime/pkg/ratelimiter"
3634

3735
"github.com/crossplane-contrib/provider-kubernetes/apis"
3836
object "github.com/crossplane-contrib/provider-kubernetes/internal/controller"
37+
38+
_ "k8s.io/client-go/plugin/pkg/client/auth"
3939
)
4040

4141
func main() {

internal/controller/object/object.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
233233
return managed.ExternalObservation{}, errors.Wrap(err, errGetObject)
234234
}
235235

236-
if err = setObserved(cr, observed); err != nil {
236+
if err = c.setObserved(cr, observed); err != nil {
237237
return managed.ExternalObservation{}, err
238238
}
239239

@@ -270,7 +270,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext
270270
return managed.ExternalCreation{}, errors.Wrap(err, errCreateObject)
271271
}
272272

273-
return managed.ExternalCreation{}, setObserved(cr, obj)
273+
return managed.ExternalCreation{}, c.setObserved(cr, obj)
274274
}
275275

276276
func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
@@ -299,7 +299,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
299299
return managed.ExternalUpdate{}, errors.Wrap(err, errApplyObject)
300300
}
301301

302-
return managed.ExternalUpdate{}, setObserved(cr, obj)
302+
return managed.ExternalUpdate{}, c.setObserved(cr, obj)
303303
}
304304

305305
func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
@@ -353,11 +353,41 @@ func getLastApplied(obj *v1alpha1.Object, observed *unstructured.Unstructured) (
353353
return last, nil
354354
}
355355

356-
func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) error {
356+
func (c *external) setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) error {
357357
var err error
358358
if obj.Status.AtProvider.Manifest.Raw, err = observed.MarshalJSON(); err != nil {
359359
return errors.Wrap(err, errFailedToMarshalExisting)
360360
}
361+
362+
if err := c.updateConditionFromObserved(obj, observed); err != nil {
363+
return err
364+
}
365+
return nil
366+
}
367+
368+
func (c *external) updateConditionFromObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) error {
369+
switch obj.Spec.Readiness.Policy {
370+
case v1alpha1.ReadinessPolicyDeriveFromObject:
371+
conditioned := xpv1.ConditionedStatus{}
372+
err := fieldpath.Pave(observed.Object).GetValueInto("status", &conditioned)
373+
if err != nil {
374+
c.logger.Debug("Got error while getting conditions from observed object, setting it as Unavailable", "error", err, "observed", observed)
375+
obj.SetConditions(xpv1.Unavailable())
376+
return nil
377+
}
378+
if status := conditioned.GetCondition(xpv1.TypeReady).Status; status != v1.ConditionTrue {
379+
c.logger.Debug("Observed object is not ready, setting it as Unavailable", "status", status, "observed", observed)
380+
obj.SetConditions(xpv1.Unavailable())
381+
return nil
382+
}
383+
obj.SetConditions(xpv1.Available())
384+
case v1alpha1.ReadinessPolicySuccessfulCreate, "":
385+
// do nothing, will be handled by c.handleLastApplied method
386+
// "" should never happen, but just in case we will treat it as SuccessfulCreate for backward compatibility
387+
default:
388+
// should never happen
389+
return errors.Errorf("unknown readiness policy %q", obj.Spec.Readiness.Policy)
390+
}
361391
return nil
362392
}
363393

@@ -453,7 +483,9 @@ func (c *external) handleLastApplied(ctx context.Context, obj *v1alpha1.Object,
453483
if isUpToDate {
454484
c.logger.Debug("Up to date!")
455485

456-
obj.Status.SetConditions(xpv1.Available())
486+
if p := obj.Spec.Readiness.Policy; p == v1alpha1.ReadinessPolicySuccessfulCreate || p == "" {
487+
obj.Status.SetConditions(xpv1.Available())
488+
}
457489

458490
cd, err := connectionDetails(ctx, c.client, obj.Spec.ConnectionDetails)
459491
if err != nil {

internal/controller/object/object_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,3 +1643,187 @@ func Test_connectionDetails(t *testing.T) {
16431643
})
16441644
}
16451645
}
1646+
1647+
func Test_updateConditionFromObserved(t *testing.T) {
1648+
type args struct {
1649+
obj *v1alpha1.Object
1650+
observed *unstructured.Unstructured
1651+
}
1652+
type want struct {
1653+
err error
1654+
conditions []xpv1.Condition
1655+
}
1656+
cases := map[string]struct {
1657+
args
1658+
want
1659+
}{
1660+
"NoopIfNoPolicyDefined": {
1661+
args: args{
1662+
obj: &v1alpha1.Object{},
1663+
observed: &unstructured.Unstructured{
1664+
Object: map[string]interface{}{
1665+
"status": xpv1.ConditionedStatus{},
1666+
},
1667+
},
1668+
},
1669+
want: want{
1670+
err: nil,
1671+
conditions: nil,
1672+
},
1673+
},
1674+
"NoopIfSuccessfulCreatePolicyDefined": {
1675+
args: args{
1676+
obj: &v1alpha1.Object{
1677+
Spec: v1alpha1.ObjectSpec{
1678+
Readiness: v1alpha1.Readiness{
1679+
Policy: v1alpha1.ReadinessPolicySuccessfulCreate,
1680+
},
1681+
},
1682+
},
1683+
observed: &unstructured.Unstructured{
1684+
Object: map[string]interface{}{
1685+
"status": xpv1.ConditionedStatus{},
1686+
},
1687+
},
1688+
},
1689+
want: want{
1690+
err: nil,
1691+
conditions: nil,
1692+
},
1693+
},
1694+
"UnavailableIfDeriveFromObjectAndNotReady": {
1695+
args: args{
1696+
obj: &v1alpha1.Object{
1697+
Spec: v1alpha1.ObjectSpec{
1698+
Readiness: v1alpha1.Readiness{
1699+
Policy: v1alpha1.ReadinessPolicyDeriveFromObject,
1700+
},
1701+
},
1702+
},
1703+
observed: &unstructured.Unstructured{
1704+
Object: map[string]interface{}{
1705+
"status": xpv1.ConditionedStatus{
1706+
Conditions: []xpv1.Condition{
1707+
{
1708+
Type: xpv1.TypeReady,
1709+
Status: corev1.ConditionFalse,
1710+
},
1711+
},
1712+
},
1713+
},
1714+
},
1715+
},
1716+
want: want{
1717+
err: nil,
1718+
conditions: []xpv1.Condition{
1719+
{
1720+
Type: xpv1.TypeReady,
1721+
Status: corev1.ConditionFalse,
1722+
Reason: xpv1.ReasonUnavailable,
1723+
},
1724+
},
1725+
},
1726+
},
1727+
"UnavailableIfDerivedFromObjectAndNoCondition": {
1728+
args: args{
1729+
obj: &v1alpha1.Object{
1730+
Spec: v1alpha1.ObjectSpec{
1731+
Readiness: v1alpha1.Readiness{
1732+
Policy: v1alpha1.ReadinessPolicyDeriveFromObject,
1733+
},
1734+
},
1735+
},
1736+
observed: &unstructured.Unstructured{
1737+
Object: map[string]interface{}{
1738+
"status": xpv1.ConditionedStatus{},
1739+
},
1740+
},
1741+
},
1742+
want: want{
1743+
err: nil,
1744+
conditions: []xpv1.Condition{
1745+
{
1746+
Type: xpv1.TypeReady,
1747+
Status: corev1.ConditionFalse,
1748+
Reason: xpv1.ReasonUnavailable,
1749+
},
1750+
},
1751+
},
1752+
},
1753+
"AvailableIfDeriveFromObjectAndReady": {
1754+
args: args{
1755+
obj: &v1alpha1.Object{
1756+
Spec: v1alpha1.ObjectSpec{
1757+
Readiness: v1alpha1.Readiness{
1758+
Policy: v1alpha1.ReadinessPolicyDeriveFromObject,
1759+
},
1760+
},
1761+
},
1762+
observed: &unstructured.Unstructured{
1763+
Object: map[string]interface{}{
1764+
"status": xpv1.ConditionedStatus{
1765+
Conditions: []xpv1.Condition{
1766+
{
1767+
Type: xpv1.TypeReady,
1768+
Status: corev1.ConditionTrue,
1769+
},
1770+
},
1771+
},
1772+
},
1773+
},
1774+
},
1775+
want: want{
1776+
err: nil,
1777+
conditions: []xpv1.Condition{
1778+
{
1779+
Type: xpv1.TypeReady,
1780+
Status: corev1.ConditionTrue,
1781+
Reason: xpv1.ReasonAvailable,
1782+
},
1783+
},
1784+
},
1785+
},
1786+
"UnavailableIfDerivedFromObjectAndCantParse": {
1787+
args: args{
1788+
obj: &v1alpha1.Object{
1789+
Spec: v1alpha1.ObjectSpec{
1790+
Readiness: v1alpha1.Readiness{
1791+
Policy: v1alpha1.ReadinessPolicyDeriveFromObject,
1792+
},
1793+
},
1794+
},
1795+
observed: &unstructured.Unstructured{
1796+
Object: map[string]interface{}{
1797+
"status": "not a conditioned status",
1798+
},
1799+
},
1800+
},
1801+
want: want{
1802+
err: nil,
1803+
conditions: []xpv1.Condition{
1804+
{
1805+
Type: xpv1.TypeReady,
1806+
Status: corev1.ConditionFalse,
1807+
Reason: xpv1.ReasonUnavailable,
1808+
},
1809+
},
1810+
},
1811+
},
1812+
}
1813+
for name, tc := range cases {
1814+
t.Run(name, func(t *testing.T) {
1815+
e := &external{
1816+
logger: logging.NewNopLogger(),
1817+
}
1818+
gotErr := e.updateConditionFromObserved(tc.args.obj, tc.args.observed)
1819+
if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
1820+
t.Fatalf("updateConditionFromObserved(...): -want error, +got error: %s", diff)
1821+
}
1822+
if diff := cmp.Diff(tc.want.conditions, tc.args.obj.Status.Conditions, cmpopts.SortSlices(func(a, b xpv1.Condition) bool {
1823+
return a.Type < b.Type
1824+
}), cmpopts.IgnoreFields(xpv1.Condition{}, "LastTransitionTime")); diff != "" {
1825+
t.Errorf("updateConditionFromObserved(...): -want result, +got result: %s", diff)
1826+
}
1827+
})
1828+
}
1829+
}

0 commit comments

Comments
 (0)