Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add conditions to FeatureTracker customer resource #16

5 changes: 3 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ linters:

issues:
exclude-rules:
- path: tests/e2e/(.+)_test\.go
- path: tests/*/(.+)_test\.go
linters:
- typecheck
- typecheck
- dupl
17 changes: 16 additions & 1 deletion apis/features/v1/features_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package v1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// FeatureTracker represents a cluster-scoped resource in the Data Science Cluster,
// specifically designed for monitoring and managing objects created via the internal Features API.
Expand All @@ -18,6 +21,16 @@ type FeatureTracker struct {
Status FeatureTrackerStatus `json:"status,omitempty"`
}

const (
ConditionPhaseFeatureCreated = "FeatureCreated"
ConditionPhasePreConditions = "FeaturePreConditions"
ConditionPhaseResourceCreation = "ResourceCreation"
ConditionPhaseLoadTemplateData = "LoadTemplateData"
ConditionPhaseProcessTemplates = "ProcessTemplates"
ConditionPhaseApplyManifests = "ApplyManifests"
ConditionPhasePostConditions = "FeaturePostConditions"
)

func (s *FeatureTracker) ToOwnerReference() metav1.OwnerReference {
return metav1.OwnerReference{
APIVersion: s.APIVersion,
Expand All @@ -33,6 +46,8 @@ type FeatureTrackerSpec struct {

// FeatureTrackerStatus defines the observed state of FeatureTracker.
type FeatureTrackerStatus struct {
// +optional
Conditions *[]conditionsv1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
14 changes: 13 additions & 1 deletion apis/features/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions config/crd/bases/features.opendatahub.io_featuretrackers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ spec:
type: object
status:
description: FeatureTrackerStatus defines the observed state of FeatureTracker.
properties:
conditions:
items:
description: Condition represents the state of the operator's reconciliation
functionality.
properties:
lastHeartbeatTime:
format: date-time
type: string
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
description: ConditionType is the state of the operator's reconciliation
functionality.
type: string
required:
- status
- type
type: object
type: array
type: object
type: object
served: true
Expand Down
2 changes: 1 addition & 1 deletion pkg/feature/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (fb *featureBuilder) Load() (*Feature, error) {
}

if feature.Enabled {
if err := feature.createResourceTracker(); err != nil {
if err := feature.createFeatureTracker(); err != nil {
return feature, err
}
}
Expand Down
75 changes: 63 additions & 12 deletions pkg/feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package feature

import (
"context"
"fmt"
"io/fs"

"github.com/hashicorp/go-multierror"
conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -45,15 +47,26 @@ type Feature struct {
// Action is a func type which can be used for different purposes while having access to Feature struct.
type Action func(feature *Feature) error

func (f *Feature) Apply() error {
func (f *Feature) Apply() (err error) {
if !f.Enabled {
log.Info("feature is disabled, skipping.", "feature", f.Name)

return nil
}

// Verify all precondition and collect errors
var multiErr *multierror.Error
var phase string
phase = featurev1.ConditionPhaseFeatureCreated
f.UpdateFeatureTrackerStatus(conditionsv1.ConditionDegraded, "False", phase, fmt.Sprintf("Applying feature %s", f.Name))
defer func() {
if err != nil {
f.UpdateFeatureTrackerStatus(conditionsv1.ConditionDegraded, "True", phase, err.Error())
} else {
f.UpdateFeatureTrackerStatus(conditionsv1.ConditionAvailable, "True", phase, fmt.Sprintf("Feature %s applied successfully", f.Name))
}
}()

phase = featurev1.ConditionPhasePreConditions
for _, precondition := range f.preconditions {
multiErr = multierror.Append(multiErr, precondition(f))
}
Expand All @@ -62,22 +75,22 @@ func (f *Feature) Apply() error {
return multiErr.ErrorOrNil()
}

// Load necessary data
phase = featurev1.ConditionPhaseLoadTemplateData
for _, loader := range f.loaders {
multiErr = multierror.Append(multiErr, loader(f))
}
if multiErr.ErrorOrNil() != nil {
return multiErr.ErrorOrNil()
}

// Create or update resources
phase = featurev1.ConditionPhaseResourceCreation
for _, resource := range f.resources {
if err := resource(f); err != nil {
return err
}
}

// Process and apply manifests
phase = featurev1.ConditionPhaseProcessTemplates
for i, m := range f.manifests {
if err := m.processTemplate(f.fsys, f.Spec); err != nil {
return errors.WithStack(err)
Expand All @@ -86,16 +99,21 @@ func (f *Feature) Apply() error {
f.manifests[i] = m
}

phase = featurev1.ConditionPhaseApplyManifests
if err := f.applyManifests(); err != nil {
return err
}

// Check all postconditions and collect errors
phase = featurev1.ConditionPhasePostConditions
for _, postcondition := range f.postconditions {
multiErr = multierror.Append(multiErr, postcondition(f))
}
if multiErr.ErrorOrNil() != nil {
return multiErr.ErrorOrNil()
}

return multiErr.ErrorOrNil()
phase = featurev1.ConditionPhaseFeatureCreated
return nil
}

func (f *Feature) Cleanup() error {
Expand Down Expand Up @@ -199,11 +217,11 @@ func (f *Feature) OwnerReference() metav1.OwnerReference {
return f.Spec.Tracker.ToOwnerReference()
}

// createResourceTracker instantiates FeatureTracker for a given Feature. All resources created when applying
// createFeatureTracker instantiates FeatureTracker for a given Feature. All resources created when applying
// it will have this object attached as an OwnerReference.
// It's a cluster-scoped resource. Once created, there's a cleanup hook added which will be invoked on deletion, resulting
// in removal of all owned resources which belong to this Feature.
func (f *Feature) createResourceTracker() error {
func (f *Feature) createFeatureTracker() error {
tracker := &featurev1.FeatureTracker{
TypeMeta: metav1.TypeMeta{
APIVersion: "features.opendatahub.io/v1",
Expand All @@ -214,7 +232,7 @@ func (f *Feature) createResourceTracker() error {
},
}

foundTracker, err := f.DynamicClient.Resource(gvr.ResourceTracker).Get(context.TODO(), tracker.Name, metav1.GetOptions{})
foundTracker, err := f.DynamicClient.Resource(gvr.FeatureTracker).Get(context.TODO(), tracker.Name, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
unstructuredTracker, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tracker)
if err != nil {
Expand All @@ -223,7 +241,7 @@ func (f *Feature) createResourceTracker() error {

u := unstructured.Unstructured{Object: unstructuredTracker}

foundTracker, err = f.DynamicClient.Resource(gvr.ResourceTracker).Create(context.TODO(), &u, metav1.CreateOptions{})
foundTracker, err = f.DynamicClient.Resource(gvr.FeatureTracker).Create(context.TODO(), &u, metav1.CreateOptions{})
if err != nil {
return err
}
Expand All @@ -238,7 +256,7 @@ func (f *Feature) createResourceTracker() error {

// Register its own cleanup
f.addCleanup(func(feature *Feature) error {
if err := f.DynamicClient.Resource(gvr.ResourceTracker).Delete(context.TODO(), f.Spec.Tracker.Name, metav1.DeleteOptions{}); err != nil && !k8serrors.IsNotFound(err) {
if err := f.DynamicClient.Resource(gvr.FeatureTracker).Delete(context.TODO(), f.Spec.Tracker.Name, metav1.DeleteOptions{}); err != nil && !k8serrors.IsNotFound(err) {
return err
}

Expand All @@ -247,3 +265,36 @@ func (f *Feature) createResourceTracker() error {

return nil
}

func (f *Feature) UpdateFeatureTrackerStatus(condType conditionsv1.ConditionType, status corev1.ConditionStatus, reason, message string) {
if f.Spec.Tracker.Status.Conditions == nil {
f.Spec.Tracker.Status.Conditions = &[]conditionsv1.Condition{}
}

conditionsv1.SetStatusCondition(f.Spec.Tracker.Status.Conditions, conditionsv1.Condition{
Type: condType,
Status: status,
Reason: reason,
Message: message,
})

modifiedTracker, err := runtime.DefaultUnstructuredConverter.ToUnstructured(f.Spec.Tracker)
if err != nil {
log.Error(err, "Error converting modified FeatureTracker to unstructured")
return
}

u := unstructured.Unstructured{Object: modifiedTracker}
updated, err := f.DynamicClient.Resource(gvr.FeatureTracker).Update(context.TODO(), &u, metav1.UpdateOptions{})
if err != nil {
log.Error(err, "Error updating FeatureTracker status")
}

var updatedTracker featurev1.FeatureTracker
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(updated.Object, &updatedTracker); err != nil {
log.Error(err, "Error converting updated unstructured object to FeatureTracker")
return
}

f.Spec.Tracker = &updatedTracker
}
2 changes: 1 addition & 1 deletion pkg/gvr/gvr.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var (
Resource: "ingresses",
}

ResourceTracker = schema.GroupVersionResource{
FeatureTracker = schema.GroupVersionResource{
Group: "features.opendatahub.io",
Version: "v1",
Resource: "featuretrackers",
Expand Down
68 changes: 68 additions & 0 deletions tests/assertions/gomega_matchers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package assertions

import (
"fmt"

"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1"
corev1 "k8s.io/api/core/v1"
)

func HaveCondition(conditionType conditionsv1.ConditionType, conditionStatus corev1.ConditionStatus, reason string) types.GomegaMatcher {
return &HaveConditionMatcher{
conditionType: conditionType,
conditionStatus: conditionStatus,
reason: reason,
}
}

type HaveConditionMatcher struct {
conditionType conditionsv1.ConditionType
conditionStatus corev1.ConditionStatus
reason string
}

func (h HaveConditionMatcher) Match(actual interface{}) (success bool, err error) {
conditions, err := asConditions(actual)
if err != nil {
return false, err
}

desiredCondition := conditionsv1.FindStatusCondition(conditions, h.conditionType)

return desiredCondition != nil && desiredCondition.Status == h.conditionStatus && desiredCondition.Reason == h.reason, nil
}

func asConditions(actual interface{}) ([]conditionsv1.Condition, error) {
var conditions []conditionsv1.Condition

switch v := actual.(type) {
case []conditionsv1.Condition:
conditions = v
case *[]conditionsv1.Condition:
if v != nil {
conditions = *v
} else {
conditions = []conditionsv1.Condition{}
}
default:
return nil, fmt.Errorf("unsupported type: %T", v)
}

return conditions, nil
}

func (h HaveConditionMatcher) FailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected %s to be:\n%s", format.Object(actual, 1), h.desiredCondition())
}

func (h HaveConditionMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected %s to not be:\n%s", format.Object(actual, 1), h.desiredCondition())
}

func (h HaveConditionMatcher) desiredCondition() interface{} {
return "Type: " + string(h.conditionType) + "\n" +
"Status: " + string(h.conditionStatus) + "\n" +
"Reason: " + h.reason
}
Loading
Loading