feature

package
v2.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 12, 2024 License: Apache-2.0 Imports: 22 Imported by: 0

README

Feature DSL

Feature struct encapsulates a set of resources to be created and actions to be performed in the cluster in order to enable desired functionality. For example, this could involve deploying a service or configuring the cluster infrastructure necessary for a component to function properly.

Goals

  • Abstraction for self-contained units of desired changes in the cluster.
  • Granular control of resource management.
  • Intuitive way of defining desired cluster changes in the controller code.
  • Support both programmatic and declarative means of changing cluster state.
  • Detailed status reporting enables quick diagnosis of issues.

Overview

---
title: Feature Domain Model
---
classDiagram
    direction LR
    
    namespace Controller {
        class Feature {
            <<struct>>
            Precondtions <1>
            Manifests <2>
            Resources <3>
            PostConditions <4>
            CleanupHooks <5>
            Data <6>
        }


        class FeatureTracker {
            <<struct>>
        }    
    }
    
    namespace Cluster {
        class Resource {
            <<k8s>>
        }
    }
    
    Feature -- FeatureTracker: is associated with
    FeatureTracker --> "1..*" Resource: owns
    
    note for FeatureTracker "Internal Custom Resource (not part of user-facing API)"
  • <1> Preconditions which are needed for the feature to be successfully applied (e.g. existence of particular CRDs)

[!NOTE] Although not strictly required due to the declarative nature of Kubernetes, it can improve error reporting and prevent the application of resources that would lead to subsequent errors.

  • <2> Manifests (i.e. YAML files) to be applied on the cluster. These can be arbitrary files or Go templates.
  • <3> Programmatic resource creation required by the feature.
  • <4> Post-creation checks, for example waiting for pods to be ready.
  • <5> Cleanup hooks (i.e. undoing patch changes)
  • <6> Data store needed by templates and actions (functions) performed during feature's lifecycle.

Creating a Feature

Getting started

The easiest way to define a feature is by using the Feature Builder (builder.go), which provides a guided flow through a fluent API, making the construction process straightforward and intuitive.

smcp, errSMCPCreate := feature.Define("mesh-control-plane-creation").
	TargetNamespace("opendatahub").
	ManifestsLocation(Templates.Location).
	Manifests(
		path.Join(Templates.ServiceMeshDir),
	).
	WithData(
		servicemesh.FeatureData.ControlPlane.Create(&dsci.Spec).AsAction(),
	).
	PreConditions(
		servicemesh.EnsureServiceMeshOperatorInstalled,
		feature.CreateNamespaceIfNotExists(serviceMeshSpec.ControlPlane.Namespace),
	).
	PostConditions(
		feature.WaitForPodsToBeReady(serviceMeshSpec.ControlPlane.Namespace),
	).
	Create()

For more examples have a look at integration/feature tests.

Using dynamic values

It may be necessary to supply data that is only available at runtime, such as cluster configuration details. For this purpose, a DataProviderFunc can be utilized.

WithData(
    feature.Entry("Domain", func (ctx context.Context, c client.Client) (string, error) {
		//... fetch the data somehow
		return domain, nil
    }),
)

Default values can be applied when an expected field in the struct is "empty" by using utilities from the provider package, for example:

WithData(
    feature.Entry("Secret", provider.ValueOf(spec.SecretName).OrElse(DefaultCertificateSecretName),
)

For more on how to further simplify re-use of Feature's context data see a dedicated section about conventions.

Execution flow

The diagram below depicts the flow when Feature is applied.

flowchart TD
    Data[Resolve Feature Data]
    PC[Pre-condition check]
    R[Create Resources]
    M[Apply manifests]
    P[Post-condition check]
    E(((Error)))
    S(((Feature applied)))
    
    Data --> OK1{OK} 
    OK1 -->|No| E
    OK1 -->|Yes|PC
    PC --> OK2{OK}
    OK2 -->|No| E
    OK2 -->|Yes| R
    R --> M
    M --> OK3{OK}
    OK3 -->|No| E
    OK3 -->|Yes| P
    P --> OK4{OK}
    OK4 -->|Yes| S
    OK4 -->|No| E

Feature Tracker

FeatureTracker is an internal CRD, not intended to be used in user-facing API. Its primary goal is to establish ownership of all resources that are part of the given feature. This way we can transparently garbage collect them when feature is no longer needed.

Additionally, it updates the .status field with detailed information about the Feature's lifecycle operations. This can be useful for troubleshooting, as it indicates which part of the feature application process is failing.

Managing Features with FeaturesHandler

The FeaturesHandler (handler.go) provides a structured way to manage and coordinate the creation, application, and deletion of features needed in particular Data Science Cluster configuration such as cluster setup or component configuration.

When creating a FeaturesHandler, developers can provide a FeaturesProvider implementations. This allows for the straightforward registration of a list of features that the handler will manage.

Conventions

Templates

Golang templating can be used to create (or patch) resources in the cluster which are part of the Feature. Following rules are applied:

  • Any file which has .tmpl. in its name will be treated as a template for the target resource.
  • Any file which has .patch. in its name will be treated a patch operation for the target resource.

By convention, these files can be stored in the resources folder next to the Feature setup code, so they can be embedded as an embedded filesystem when defining a feature, for example, by using the Builder.

Anonymous struct can be used on per feature set basis to organize resource access easier:

//go:embed resources
var resourcesFS embed.FS

const baseDir = "resources"

var Resources = struct {
	// InstallDir is the path to the Serving install templates.
	InstallDir string
	// Location specifies the file system that contains the templates to be used.
	Location fs.FS
	// BaseDir is the path to the base of the embedded FS
	BaseDir string
}{
	InstallDir:     path.Join(baseDir, "installation"),
	Location:       resourcesFS,
	BaseDir:        baseDir,
}
Feature context re-use

The FeatureData anonymous struct convention provides a clear and consistent way to manage data for features.

By defining data and extraction functions, it simplifies handling of feature-related data in both templates and functions where this data is required.

const (
	servingKey  = "Serving" // <1>
)

var FeatureData = struct {
	Serving       feature.DataDefinition[infrav1.ServingSpec, infrav1.ServingSpec] // <2>
}{
	Serving: feature.DataDefinition[infrav1.ServingSpec, infrav1.ServingSpec]{
		Define /*<3>*/: func(source *infrav1.ServingSpec) feature.DataEntry[infrav1.ServingSpec] {
			return feature.DataEntry[infrav1.ServingSpec]{
				Key:   servingKey,
				Value: provider.ValueOf(*source).Get,
			}
		},
		Extract /*<4>*/: feature.ExtractEntry[infrav1.ServingSpec](servingKey),
	},
}
  • <1> Key used to store defined data entry in the Feature's context. This can be later used in templates as well as golang functions.
  • <2> Generic struct used to define how context entry is created. Parametrized types hold information about the source and target types. The source type represents the origin from which data is taken, defining the type of data being input or extracted from the source. The target type determines the type of the value that will be stored in the key-value store (feature context data).
  • <3> Defines how an entry (key-valaue) is created and stored in Feature's context.
  • <4> Defines how to extract the value from the context (typed access instead of relying on string keys).

Example below illustrates both storing data in the feature and accessing it from Feature's action function:

// Define the feature using builder
// ...
WithData(serverless.FeatureData.Serving.Define(servingSpec).AsAction())
// ...

// To extract in the action function
func DoSomethingWithServingData(f *feature.Feature) (string, error) {
    serving, err := FeatureData.Serving.Extract(f);
    if err != nil {
        return "", err
    }
    // do something with serving data - create a resource, etc.
    // ....
}

[!NOTE] Calling .AsAction() is a workaround due to limitation of generic on receiver functions

Documentation

Index

Constants

This section is empty.

Variables

View Source
var EmptyFeaturesHandler = &FeaturesHandler{
	features:          []*Feature{},
	featuresProviders: []FeaturesProvider{},
}

EmptyFeaturesHandler is noop handler so that we can avoid nil checks in the code and safely call Apply/Delete methods.

Functions

func DefaultMetaOptions added in v2.15.0

func DefaultMetaOptions(f *Feature) []cluster.MetaOptions

func Define added in v2.15.0

func Define(featureName string) *featureBuilder

Define creates a new feature builder with the given name.

func ExtractEntry added in v2.15.0

func ExtractEntry[T any](key string) func(f *Feature) (T, error)

ExtractEntry is a convenient way to define how to extract a value from the given Feature's data using defined key.

func Get added in v2.15.0

func Get[T any](f *Feature, key string) (T, error)

Get allows to retrieve arbitrary value from the Feature's data container.

func OwnedBy added in v2.10.0

func OwnedBy(f *Feature) cluster.MetaOptions

OwnedBy returns a cluster.MetaOptions that sets the owner reference to the FeatureTracker resource.

Types

type Action

type Action func(ctx context.Context, cli client.Client, f *Feature) error

Action is a func type which can be used for different purposes during Feature's lifecycle while having access to Feature struct.

func CreateNamespaceIfNotExists added in v2.7.0

func CreateNamespaceIfNotExists(namespace string) Action

CreateNamespaceIfNotExists will create a namespace with the given name if it does not exist yet. It does not set ownership nor apply extra metadata to the existing namespace.

func EnsureOperatorIsInstalled added in v2.9.0

func EnsureOperatorIsInstalled(operatorName string) Action

func Entry added in v2.15.0

func Entry[T any](key string, providerFunc provider.DataProviderFunc[T]) Action

Entry allows to define association between a name under which the data is stored in the Feature and a data provider defining the logic for fetching. Provider is a function allowing to fetch a value for a given key dynamically by interacting with Kubernetes client.

If the value is static, consider using provider.ValueOf(variable).Get as passed provider function.

func WaitForPodsToBeReady

func WaitForPodsToBeReady(namespace string) Action

func WaitForResourceToBeCreated

func WaitForResourceToBeCreated(namespace string, gvk schema.GroupVersionKind) Action

type CleanupFunc added in v2.17.0

type CleanupFunc func(ctx context.Context, cli client.Client) error

CleanupFunc defines how to clean up resources associated with a feature. By default, all resources created by the feature are deleted when the feature is, so there is no need to explicitly add cleanup hooks for them. This is useful when you need to perform some additional cleanup actions such as removing effects of a patch operation.

type DataDefinition added in v2.15.0

type DataDefinition[S, T any] struct {
	// Define is a factory function to create a Feature's DataEntry from the given source.
	Define func(source *S) DataEntry[T]
	// Extract allows to fetch data from the Feature.
	Extract func(f *Feature) (T, error)
}

DataDefinition defines how the data is created and fetched from the Feature's data context. S is a source type from which the data is created. T is a type of the data stored in the Feature.

type DataEntry added in v2.15.0

type DataEntry[T any] struct {
	Key   string
	Value provider.DataProviderFunc[T]
}

DataEntry associates data provider with a key under which the data is stored in the Feature.

func (DataEntry[T]) AsAction added in v2.15.0

func (d DataEntry[T]) AsAction() Action

AsAction converts DataEntry to an Action which is the used to populate defined key-value in the feature itself.

type EnabledFunc added in v2.15.0

type EnabledFunc func(ctx context.Context, cli client.Client, feature *Feature) (bool, error)

EnabledFunc is a func type used to determine if a feature should be enabled.

type Feature

type Feature struct {
	Name            string
	TargetNamespace string
	Enabled         EnabledFunc
	Managed         bool

	Log logr.Logger
	// contains filtered or unexported fields
}

Feature is a high-level abstraction that represents a collection of resources and actions that are applied to the cluster to enable a specific feature.

Features can be either managed or unmanaged. Managed features are reconciled to their desired state based on defined manifests.

In addition to creating resources using manifest files or through Golang functions, a Feature allows defining preconditions and postconditions. These conditions are checked to ensure the cluster is in the desired state for the feature to be applied successfully.

When a Feature is applied, an associated resource called FeatureTracker is created. This resource establishes ownership for related resources, allowing for easy cleanup of all resources associated with the feature when it is about to be removed during reconciliation.

Each Feature can have a list of cleanup functions. These functions can be particularly useful when the cleanup involves actions other than the removal of resources, such as reverting a patch operation.

To create a Feature, use the provided FeatureBuilder. This builder guides through the process using a fluent API.

func (*Feature) Apply

func (f *Feature) Apply(ctx context.Context, cli client.Client) error

Apply applies the feature to the cluster. It creates a FeatureTracker resource to establish ownership and reports the result of the operation as a condition.

func (*Feature) AsOwnerReference added in v2.7.0

func (f *Feature) AsOwnerReference() metav1.OwnerReference

AsOwnerReference returns an OwnerReference for the FeatureTracker resource.

func (*Feature) Cleanup

func (f *Feature) Cleanup(ctx context.Context, cli client.Client) error

func (*Feature) Set added in v2.15.0

func (f *Feature) Set(key string, data any) error

Set allows to store a value under given key in the Feature's data container.

type FeaturesHandler added in v2.7.0

type FeaturesHandler struct {
	// contains filtered or unexported fields
}

FeaturesHandler provides a structured way to manage and coordinate the creation, application, and deletion of features needed in particular Data Science Cluster configuration.

func ClusterFeaturesHandler added in v2.7.0

func ClusterFeaturesHandler(dsci *dsciv1.DSCInitialization, def ...FeaturesProvider) *FeaturesHandler

func ComponentFeaturesHandler added in v2.7.0

func ComponentFeaturesHandler(owner metav1.Object, componentName, targetNamespace string, def ...FeaturesProvider) *FeaturesHandler

func (*FeaturesHandler) Add added in v2.15.0

func (fh *FeaturesHandler) Add(builders ...*featureBuilder) error

Add loads features defined by passed builders and adds to internal list which is then used to Apply on the cluster. It also makes sure that both TargetNamespace and Source are added to the feature before it's `Create()`ed.

func (*FeaturesHandler) Apply added in v2.7.0

func (fh *FeaturesHandler) Apply(ctx context.Context, cli client.Client) error

func (*FeaturesHandler) Delete added in v2.7.0

func (fh *FeaturesHandler) Delete(ctx context.Context, cli client.Client) error

Delete executes registered clean-up tasks for handled Features in the opposite order they were initiated. This approach assumes that Features are either instantiated in the correct sequence or are self-contained.

type FeaturesProvider added in v2.7.0

type FeaturesProvider func(registry FeaturesRegistry) error

FeaturesProvider is a function which allow to define list of features and add them to the handler's registry.

type FeaturesRegistry added in v2.15.0

type FeaturesRegistry interface {
	Add(builders ...*featureBuilder) error
}

type HandlerWithReporter added in v2.10.1

type HandlerWithReporter[T client.Object] struct {
	// contains filtered or unexported fields
}

HandlerWithReporter is a wrapper around FeaturesHandler and status.Reporter It is intended apply features related to a given resource capabilities and report its status using custom reporter.

func NewHandlerWithReporter added in v2.10.1

func NewHandlerWithReporter[T client.Object](handler *FeaturesHandler, reporter *status.Reporter[T]) *HandlerWithReporter[T]

func (HandlerWithReporter[T]) Apply added in v2.10.1

func (h HandlerWithReporter[T]) Apply(ctx context.Context, cli client.Client) error

func (HandlerWithReporter[T]) Delete added in v2.10.1

func (h HandlerWithReporter[T]) Delete(ctx context.Context, cli client.Client) error

type MissingOperatorError added in v2.10.1

type MissingOperatorError struct {
	// contains filtered or unexported fields
}

func NewMissingOperatorError added in v2.10.1

func NewMissingOperatorError(operatorName string, err error) *MissingOperatorError

func (*MissingOperatorError) Error added in v2.10.1

func (e *MissingOperatorError) Error() string

func (*MissingOperatorError) Unwrap added in v2.10.1

func (e *MissingOperatorError) Unwrap() error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL