Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ includes:
excludes: [] # put task names in here which are overwritten in this file
vars:
NESTED_MODULES: api
API_DIRS: '{{.ROOT_DIR}}/api/provider/v1alpha1/... {{.ROOT_DIR}}/api/clusters/v1alpha1/...'
API_DIRS: '{{.ROOT_DIR}}/api/...'
MANIFEST_OUT: '{{.ROOT_DIR}}/api/crds/manifests'
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/provider/v1alpha1/... {{.ROOT_DIR}}/api/clusters/v1alpha1/...'
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/...'
COMPONENTS: 'openmcp-operator'
REPO_URL: 'https://github.com/openmcp-project/openmcp-operator'
GENERATE_DOCS_INDEX: "true"
Expand Down
4 changes: 2 additions & 2 deletions api/clusters/v1alpha1/accessrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import (

type AccessRequestSpec struct {
// ClusterRef is the reference to the Cluster for which access is requested.
// Exactly one of clusterRef or requestRef must be set.
// If set, requestRef will be ignored.
// This value is immutable.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="clusterRef is immutable"
// +optional
ClusterRef *NamespacedObjectReference `json:"clusterRef,omitempty"`

// RequestRef is the reference to the ClusterRequest for whose Cluster access is requested.
// Exactly one of clusterRef or requestRef must be set.
// Is ignored if clusterRef is set.
// This value is immutable.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="requestRef is immutable"
// +optional
Expand Down
8 changes: 5 additions & 3 deletions api/clusters/v1alpha1/clusterrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type ClusterRequestSpec struct {
Purpose string `json:"purpose"`
}

// +kubebuilder:validation:XValidation:rule="!has(oldSelf.clusterRef) || has(self.clusterRef)", message="clusterRef may not be removed once set"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.cluster) || has(self.cluster)", message="cluster may not be removed once set"
type ClusterRequestStatus struct {
CommonStatus `json:",inline"`

Expand All @@ -23,8 +23,8 @@ type ClusterRequestStatus struct {
// Cluster is the reference to the Cluster that was returned as a result of a granted request.
// Note that this information needs to be recoverable in case this status is lost, e.g. by adding a back reference in form of a finalizer to the Cluster resource.
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="clusterRef is immutable"
Cluster *NamespacedObjectReference `json:"clusterRef,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="cluster is immutable"
Cluster *NamespacedObjectReference `json:"cluster,omitempty"`
}

type RequestPhase string
Expand All @@ -49,6 +49,8 @@ func (p RequestPhase) IsPending() bool {
// +kubebuilder:selectablefield:JSONPath=".status.phase"
// +kubebuilder:printcolumn:JSONPath=".spec.purpose",name="Purpose",type=string
// +kubebuilder:printcolumn:JSONPath=".status.phase",name="Phase",type=string
// +kubebuilder:printcolumn:JSONPath=".status.cluster.name",name="Cluster",type=string
// +kubebuilder:printcolumn:JSONPath=".status.cluster.namespace",name="Cluster-NS",type=string

// ClusterRequest is the Schema for the clusters API
type ClusterRequest struct {
Expand Down
25 changes: 10 additions & 15 deletions api/clusters/v1alpha1/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,24 @@ const (
)

const (
// ClusterLabel can be used on CRDs to indicate onto which cluster they should be deployed.
ClusterLabel = "openmcp.cloud/cluster"
// OperationAnnotation is used to trigger specific operations on resources.
OperationAnnotation = "openmcp.cloud/operation"
// OperationAnnotationValueIgnore is used to ignore the resource.
OperationAnnotationValueIgnore = "ignore"
// OperationAnnotationValueReconcile is used to trigger a reconcile on the resource.
OperationAnnotationValueReconcile = "reconcile"

// K8sVersionAnnotation can be used to display the k8s version of the cluster.
K8sVersionAnnotation = "clusters.openmcp.cloud/k8sversion"
K8sVersionAnnotation = GroupName + "/k8sversion"
// ProviderInfoAnnotation can be used to display provider-specific information about the cluster.
ProviderInfoAnnotation = "clusters.openmcp.cloud/providerinfo"
ProviderInfoAnnotation = GroupName + "/providerinfo"
// ProfileNameAnnotation can be used to display the actual name (not the hash) of the cluster profile.
ProfileNameAnnotation = "clusters.openmcp.cloud/profile"
ProfileNameAnnotation = GroupName + "/profile"
// EnvironmentAnnotation can be used to display the environment of the cluster.
EnvironmentAnnotation = "clusters.openmcp.cloud/environment"
EnvironmentAnnotation = GroupName + "/environment"
// ProviderAnnotation can be used to display the provider of the cluster.
ProviderAnnotation = "clusters.openmcp.cloud/provider"
ProviderAnnotation = GroupName + "/provider"

// DeleteWithoutRequestsLabel marks that the corresponding cluster can be deleted if the scheduler removes the last request pointing to it.
// Its value must be "true" for the label to take effect.
DeleteWithoutRequestsLabel = "clusters.openmcp.cloud/delete-without-requests"
DeleteWithoutRequestsLabel = GroupName + "/delete-without-requests"
// ProviderLabel is used to indicate the provider that is responsible for an AccessRequest.
ProviderLabel = "provider." + GroupName
// ProfileLabel is used to make the profile information easily accessible for the ClusterProviders.
ProfileLabel = "profile." + GroupName
)

const (
Expand Down
2 changes: 2 additions & 0 deletions api/clusters/v1alpha1/constants/reasons.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const (
ReasonOnboardingClusterInteractionProblem = "OnboardingClusterInteractionProblem"
// ReasonPlatformClusterInteractionProblem is used when the platform cluster cannot be reached.
ReasonPlatformClusterInteractionProblem = "PlatformClusterInteractionProblem"
// ReasonInvalidReference means that a reference points to a non-existing or otherwise invalid object.
ReasonInvalidReference = "InvalidReference"
// ReasonConfigurationProblem indicates that something is configured incorrectly.
ReasonConfigurationProblem = "ConfigurationProblem"
// ReasonInternalError indicates that something went wrong internally.
Expand Down
4 changes: 3 additions & 1 deletion api/clusters/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"

apiconst "github.com/openmcp-project/openmcp-operator/api/constants"
)

const GroupName = "clusters.openmcp.cloud"
const GroupName = "clusters." + apiconst.OpenMCPGroupName

var (
// GroupVersion is group version used to register these objects
Expand Down
16 changes: 16 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package constants

const (
// OpenMCPGroupName is the base API group name for OpenMCP.
OpenMCPGroupName = "openmcp.cloud"

// ClusterLabel can be used on CRDs to indicate onto which cluster they should be deployed.
ClusterLabel = OpenMCPGroupName + "/cluster"

// OperationAnnotation is used to trigger specific operations on resources.
OperationAnnotation = OpenMCPGroupName + "/operation"
// OperationAnnotationValueIgnore is used to ignore the resource.
OperationAnnotationValueIgnore = "ignore"
// OperationAnnotationValueReconcile is used to trigger a reconcile on the resource.
OperationAnnotationValueReconcile = "reconcile"
)
4 changes: 2 additions & 2 deletions api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ spec:
clusterRef:
description: |-
ClusterRef is the reference to the Cluster for which access is requested.
Exactly one of clusterRef or requestRef must be set.
If set, requestRef will be ignored.
This value is immutable.
properties:
name:
Expand Down Expand Up @@ -135,7 +135,7 @@ spec:
requestRef:
description: |-
RequestRef is the reference to the ClusterRequest for whose Cluster access is requested.
Exactly one of clusterRef or requestRef must be set.
Is ignored if clusterRef is set.
This value is immutable.
properties:
name:
Expand Down
14 changes: 10 additions & 4 deletions api/crds/manifests/clusters.openmcp.cloud_clusterrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ spec:
- jsonPath: .status.phase
name: Phase
type: string
- jsonPath: .status.cluster.name
name: Cluster
type: string
- jsonPath: .status.cluster.namespace
name: Cluster-NS
type: string
name: v1alpha1
schema:
openAPIV3Schema:
Expand Down Expand Up @@ -62,7 +68,7 @@ spec:
rule: self == oldSelf
status:
properties:
clusterRef:
cluster:
description: |-
Cluster is the reference to the Cluster that was returned as a result of a granted request.
Note that this information needs to be recoverable in case this status is lost, e.g. by adding a back reference in form of a finalizer to the Cluster resource.
Expand All @@ -79,7 +85,7 @@ spec:
- namespace
type: object
x-kubernetes-validations:
- message: clusterRef is immutable
- message: cluster is immutable
rule: self == oldSelf
conditions:
description: Conditions contains the conditions.
Expand Down Expand Up @@ -147,8 +153,8 @@ spec:
- phase
type: object
x-kubernetes-validations:
- message: clusterRef may not be removed once set
rule: '!has(oldSelf.clusterRef) || has(self.clusterRef)'
- message: cluster may not be removed once set
rule: '!has(oldSelf.cluster) || has(self.cluster)'
type: object
selectableFields:
- jsonPath: .spec.purpose
Expand Down
5 changes: 3 additions & 2 deletions cmd/openmcp-operator/app/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sigs.k8s.io/yaml"

clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1"
apiconst "github.com/openmcp-project/openmcp-operator/api/constants"
"github.com/openmcp-project/openmcp-operator/api/crds"
"github.com/openmcp-project/openmcp-operator/api/install"
)
Expand Down Expand Up @@ -75,12 +76,12 @@ func (o *InitOptions) Run(ctx context.Context) error {
log.Info("Environment", "value", o.Environment)

// apply CRDs
crdManager := crdutil.NewCRDManager(clustersv1alpha1.ClusterLabel, crds.CRDs)
crdManager := crdutil.NewCRDManager(apiconst.ClusterLabel, crds.CRDs)

crdManager.AddCRDLabelToClusterMapping(clustersv1alpha1.PURPOSE_ONBOARDING, o.Clusters.Onboarding)
crdManager.AddCRDLabelToClusterMapping(clustersv1alpha1.PURPOSE_PLATFORM, o.Clusters.Platform)

if err := crdManager.CreateOrUpdateCRDs(ctx, nil); err != nil {
if err := crdManager.CreateOrUpdateCRDs(ctx, &log); err != nil {
return fmt.Errorf("error creating/updating CRDs: %w", err)
}

Expand Down
9 changes: 9 additions & 0 deletions cmd/openmcp-operator/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/openmcp-project/openmcp-operator/api/install"
"github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1"
"github.com/openmcp-project/openmcp-operator/internal/controllers/accessrequest"
"github.com/openmcp-project/openmcp-operator/internal/controllers/provider"
"github.com/openmcp-project/openmcp-operator/internal/controllers/scheduler"
)
Expand All @@ -33,6 +34,7 @@ var setupLog logging.Logger
var allControllers = []string{
strings.ToLower(scheduler.ControllerName),
strings.ToLower(provider.ControllerName),
strings.ToLower(accessrequest.ControllerName),
}

func NewRunCommand(so *SharedOptions) *cobra.Command {
Expand Down Expand Up @@ -299,6 +301,13 @@ func (o *RunOptions) Run(ctx context.Context) error {
}
}

// setup accessrequest controller
if slices.Contains(o.Controllers, strings.ToLower(accessrequest.ControllerName)) {
if err := accessrequest.NewAccessRequestReconciler(o.Clusters.Platform, o.Config.AccessRequest).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to setup accessrequest controller: %w", err)
}
}

// setup deployment controller
if slices.Contains(o.Controllers, strings.ToLower(provider.ControllerName)) {
utilruntime.Must(clientgoscheme.AddToScheme(mgr.GetScheme()))
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

## Controller

- [AccessRequest Controller](controller/accessrequest.md)
- [Cluster Scheduler](controller/scheduler.md)

## Resources
Expand Down
37 changes: 37 additions & 0 deletions docs/controller/accessrequest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# AccessRequest Controller

The _AccessRequest Controller_ is responsible for labelling `AccessRequest` resources with the name of the ClusterProvider that is responsible for them. It also adds a label for the corresponding `ClusterProfile` and adds the cluster reference to the spec, if only the request reference is specified.

This is needed because the information, which ClusterProvider is responsible for answering the `AccessRequest` is contained in the referenced `ClusterProfile`. Depending on `AccessRequest`'s spec, a `Cluster` and potentially also a `ClusterRequest` must be fetched before the `ClusterProfile` is known, which then has to be fetched too. If multiple ClusterProviders are running in the cluster, all of them would need to fetch these resources, only for all but one of them to notice that they are not responsible and don't have to do anything.

To increase performance and simplify reconciliation logic in the individual ClusterProviders, this central AccessRequest controller takes over the task of figuring out the ClusterProfile and the responsible ClusterProvider and it adds these as labels to the `AccessRequest` resource. It reacts only on resources which do not yet have both of these labels set, so it should reconcile each `AccessRequest` only once (excluding repeated reconciliations due to errors).

The added labels are:
```yaml
provider.clusters.openmcp.cloud: <provider-name>
profile.clusters.openmcp.cloud: <profile-name>
```

ClusterProviders should only reconcile `AccessRequest` resources where both labels are set and the value of the provider label matches their own provider name. Resources where either label is missing or the value of the provider label does not match the own provider name must be ignored.

Note that if a reconciled `AccessRequest` already has one of the labels set, but its value differs from the expected one, the controller will log an error, but not update the resource in any way, to not accidentally move the responsibility from one provider to another. This also means that `AccessRequest` resources that have only one of the labels set, and that one to a wrong value, will not be handled - this controller won't update the resource and the ClusterProvider should not pick it up because one of the labels is missing. It is therefore strongly recommended to not set the labels when creating a new `AccessRequest` resource.

In addition to the labels, the controller also sets `spec.clusterRef`, if only `spec.requestRef` is specified.

After an `AccessRequest` has been prepared this way, the ClusterProviders can easily infer which one is responsible, which `Cluster` resource this request belongs to, and which `ClusterProfile` is used by the `Cluster`, directly from the labels and spec of the `AccessRequest` resource.

## Configuration

The AccessRequest controller is run as long as `accessrequest` is included in the `--controllers` flag. It is included by default.

The entire configuration for the AccessRequest controller is optional.
```yaml
accessRequest: # optional
selector: # optional
matchLabels: <...> # optional
matchExpressions: <...> # optional
```

The following fields can be specified inside the `accessRequest` node:
- `selector` _(optional)_
- A standard k8s label selector, as it is also used in Deployments, for example. If specified, only `AccessRequest` resources matching the selector are reconciled by the controller. This can be used to distribute resources between multiple instances of the AccessRequest controller watching the same cluster.
3 changes: 3 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ type Config struct {

// Scheduler is the configuration for the cluster scheduler.
Scheduler *SchedulerConfig `json:"scheduler,omitempty"`

// AccessRequest is the configuration for the access request controller.
AccessRequest *AccessRequestConfig `json:"accessRequest,omitempty"`
}

// Dump is used for logging and debugging purposes.
Expand Down
24 changes: 24 additions & 0 deletions internal/config/config_accessrequest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package config

import (
"fmt"

"k8s.io/apimachinery/pkg/util/validation/field"
)

type AccessRequestConfig struct {
// If set, only AccessRequests that match the selector will be reconciled.
Selector *Selector `json:"selector,omitempty"`
}

func (c *AccessRequestConfig) Validate(fldPath *field.Path) error {
return c.Selector.Validate(fldPath.Child("selector"))
}

func (c *AccessRequestConfig) Complete(fldPath *field.Path) error {
if err := c.Selector.Complete(fldPath.Child("selector")); err != nil {
return fmt.Errorf("error completing selector: %w", err)
}

return nil
}
Loading