Skip to content

Commit 081c4b0

Browse files
authored
Add Conditions to status (#91)
* Add Conditions to status * Adds an initial Condition: "Ready" with a "False" status. * Fixed #73 * Include unit-tests Signed-off-by: Todd Short <[email protected]>
1 parent 47485a7 commit 081c4b0

File tree

6 files changed

+238
-4
lines changed

6 files changed

+238
-4
lines changed

.github/workflows/unit-test.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: unit-test
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches:
8+
- main
9+
10+
jobs:
11+
unit-test-basic:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v1
15+
- uses: actions/setup-go@v3
16+
with:
17+
go-version-file: "go.mod"
18+
- name: Run basic unit tests
19+
run: |
20+
make test

api/v1alpha1/operator_types.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,22 @@ type OperatorSpec struct {
2727
PackageName string `json:"packageName"`
2828
}
2929

30+
const (
31+
// TODO(user): add more Types
32+
TypeReady = "Ready"
33+
34+
// TODO(user): add more Reasons
35+
ReasonNotImplemented = "NotImplemented"
36+
)
37+
3038
// OperatorStatus defines the observed state of Operator
31-
type OperatorStatus struct{}
39+
type OperatorStatus struct {
40+
// +patchMergeKey=type
41+
// +patchStrategy=merge
42+
// +listType=map
43+
// +listMapKey=type
44+
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
45+
}
3246

3347
//+kubebuilder:object:root=true
3448
//+kubebuilder:resource:scope=Cluster

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/operators.operatorframework.io_operators.yaml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,78 @@ spec:
4444
type: object
4545
status:
4646
description: OperatorStatus defines the observed state of Operator
47+
properties:
48+
conditions:
49+
items:
50+
description: "Condition contains details for one aspect of the current
51+
state of this API Resource. --- This struct is intended for direct
52+
use as an array at the field path .status.conditions. For example,
53+
\n type FooStatus struct{ // Represents the observations of a
54+
foo's current state. // Known .status.conditions.type are: \"Available\",
55+
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
56+
// +listType=map // +listMapKey=type Conditions []metav1.Condition
57+
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
58+
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
59+
properties:
60+
lastTransitionTime:
61+
description: lastTransitionTime is the last time the condition
62+
transitioned from one status to another. This should be when
63+
the underlying condition changed. If that is not known, then
64+
using the time when the API field changed is acceptable.
65+
format: date-time
66+
type: string
67+
message:
68+
description: message is a human readable message indicating
69+
details about the transition. This may be an empty string.
70+
maxLength: 32768
71+
type: string
72+
observedGeneration:
73+
description: observedGeneration represents the .metadata.generation
74+
that the condition was set based upon. For instance, if .metadata.generation
75+
is currently 12, but the .status.conditions[x].observedGeneration
76+
is 9, the condition is out of date with respect to the current
77+
state of the instance.
78+
format: int64
79+
minimum: 0
80+
type: integer
81+
reason:
82+
description: reason contains a programmatic identifier indicating
83+
the reason for the condition's last transition. Producers
84+
of specific condition types may define expected values and
85+
meanings for this field, and whether the values are considered
86+
a guaranteed API. The value should be a CamelCase string.
87+
This field may not be empty.
88+
maxLength: 1024
89+
minLength: 1
90+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
91+
type: string
92+
status:
93+
description: status of the condition, one of True, False, Unknown.
94+
enum:
95+
- "True"
96+
- "False"
97+
- Unknown
98+
type: string
99+
type:
100+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
101+
--- Many .condition.type values are consistent across resources
102+
like Available, but because arbitrary conditions can be useful
103+
(see .node.status.conditions), the ability to deconflict is
104+
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
105+
maxLength: 316
106+
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
107+
type: string
108+
required:
109+
- lastTransitionTime
110+
- message
111+
- reason
112+
- status
113+
- type
114+
type: object
115+
type: array
116+
x-kubernetes-list-map-keys:
117+
- type
118+
x-kubernetes-list-type: map
47119
type: object
48120
type: object
49121
served: true

controllers/operator_controller.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ package controllers
1919
import (
2020
"context"
2121

22+
"k8s.io/apimachinery/pkg/api/equality"
23+
apimeta "k8s.io/apimachinery/pkg/api/meta"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2225
"k8s.io/apimachinery/pkg/runtime"
26+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2327
ctrl "sigs.k8s.io/controller-runtime"
2428
"sigs.k8s.io/controller-runtime/pkg/client"
2529
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -47,7 +51,59 @@ type OperatorReconciler struct {
4751
// For more details, check Reconcile and its Result here:
4852
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
4953
func (r *OperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
50-
_ = log.FromContext(ctx)
54+
l := log.FromContext(ctx).WithName("reconcile")
55+
l.V(1).Info("starting")
56+
defer l.V(1).Info("ending")
57+
58+
var existingOp = &operatorsv1alpha1.Operator{}
59+
if err := r.Get(ctx, req.NamespacedName, existingOp); err != nil {
60+
return ctrl.Result{}, client.IgnoreNotFound(err)
61+
}
62+
63+
reconciledOp := existingOp.DeepCopy()
64+
res, reconcileErr := r.reconcile(ctx, reconciledOp)
65+
66+
// Do checks before any Update()s, as Update() may modify the resource structure!
67+
updateStatus := !equality.Semantic.DeepEqual(existingOp.Status, reconciledOp.Status)
68+
updateFinalizers := !equality.Semantic.DeepEqual(existingOp.Finalizers, reconciledOp.Finalizers)
69+
70+
// Compare resources - ignoring status & metadata.finalizers
71+
compareOp := reconciledOp.DeepCopy()
72+
existingOp.Status, compareOp.Status = operatorsv1alpha1.OperatorStatus{}, operatorsv1alpha1.OperatorStatus{}
73+
existingOp.Finalizers, compareOp.Finalizers = []string{}, []string{}
74+
specDiffers := !equality.Semantic.DeepEqual(existingOp, compareOp)
75+
76+
if updateStatus {
77+
if updateErr := r.Status().Update(ctx, reconciledOp); updateErr != nil {
78+
return res, utilerrors.NewAggregate([]error{reconcileErr, updateErr})
79+
}
80+
}
81+
82+
if specDiffers {
83+
panic("spec or metadata changed by reconciler")
84+
}
85+
86+
if updateFinalizers {
87+
if updateErr := r.Update(ctx, reconciledOp); updateErr != nil {
88+
return res, utilerrors.NewAggregate([]error{reconcileErr, updateErr})
89+
}
90+
}
91+
92+
return res, reconcileErr
93+
}
94+
95+
// Helper function to do the actual reconcile
96+
func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha1.Operator) (ctrl.Result, error) {
97+
98+
// TODO(user): change ReasonNotImplemented when functionality added
99+
readyCondition := metav1.Condition{
100+
Type: operatorsv1alpha1.TypeReady,
101+
Status: metav1.ConditionFalse,
102+
Reason: operatorsv1alpha1.ReasonNotImplemented,
103+
Message: "The Reconcile operation is not implemented",
104+
ObservedGeneration: op.GetGeneration(),
105+
}
106+
apimeta.SetStatusCondition(&op.Status.Conditions, readyCondition)
51107

52108
// TODO(user): your logic here
53109

controllers/suite_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,30 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package controllers
17+
package controllers_test
1818

1919
import (
20+
"context"
21+
"fmt"
2022
"path/filepath"
2123
"testing"
2224

2325
. "github.com/onsi/ginkgo/v2"
2426
. "github.com/onsi/gomega"
2527

28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/apimachinery/pkg/util/rand"
2631
"k8s.io/client-go/kubernetes/scheme"
2732
"k8s.io/client-go/rest"
2833
"sigs.k8s.io/controller-runtime/pkg/client"
2934
"sigs.k8s.io/controller-runtime/pkg/envtest"
3035
logf "sigs.k8s.io/controller-runtime/pkg/log"
3136
"sigs.k8s.io/controller-runtime/pkg/log/zap"
37+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3238

3339
operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
40+
"github.com/operator-framework/operator-controller/controllers"
3441
//+kubebuilder:scaffold:imports
3542
)
3643

@@ -78,3 +85,60 @@ var _ = AfterSuite(func() {
7885
err := testEnv.Stop()
7986
Expect(err).NotTo(HaveOccurred())
8087
})
88+
89+
var _ = Describe("Reconcile Test", func() {
90+
When("an Operator is created", func() {
91+
var (
92+
operator *operatorsv1alpha1.Operator
93+
ctx context.Context
94+
opName string
95+
pkgName string
96+
err error
97+
)
98+
BeforeEach(func() {
99+
ctx = context.Background()
100+
101+
opName = fmt.Sprintf("operator-test-%s", rand.String(8))
102+
pkgName = fmt.Sprintf("package-test-%s", rand.String(8))
103+
104+
operator = &operatorsv1alpha1.Operator{
105+
ObjectMeta: metav1.ObjectMeta{
106+
Name: opName,
107+
},
108+
Spec: operatorsv1alpha1.OperatorSpec{
109+
PackageName: pkgName,
110+
},
111+
}
112+
err = k8sClient.Create(ctx, operator)
113+
Expect(err).To(Not(HaveOccurred()))
114+
115+
or := controllers.OperatorReconciler{
116+
k8sClient,
117+
scheme.Scheme,
118+
}
119+
_, err = or.Reconcile(ctx, reconcile.Request{
120+
NamespacedName: types.NamespacedName{
121+
Name: opName,
122+
},
123+
})
124+
Expect(err).To(Not(HaveOccurred()))
125+
})
126+
AfterEach(func() {
127+
err = k8sClient.Delete(ctx, operator)
128+
Expect(err).To(Not(HaveOccurred()))
129+
})
130+
It("has a Condition created", func() {
131+
getOperator := &operatorsv1alpha1.Operator{}
132+
133+
err = k8sClient.Get(ctx, client.ObjectKey{
134+
Name: opName,
135+
}, getOperator)
136+
Expect(err).To(Not(HaveOccurred()))
137+
138+
// There should always be a "Ready" condition, regardless of Status.
139+
conds := getOperator.Status.Conditions
140+
Expect(conds).To(Not(BeEmpty()))
141+
Expect(conds).To(ContainElement(HaveField("Type", operatorsv1alpha1.TypeReady)))
142+
})
143+
})
144+
})

0 commit comments

Comments
 (0)