diff --git a/api/clusters/v1alpha1/accessrequest_types.go b/api/clusters/v1alpha1/accessrequest_types.go index 86d4798..7243547 100644 --- a/api/clusters/v1alpha1/accessrequest_types.go +++ b/api/clusters/v1alpha1/accessrequest_types.go @@ -32,7 +32,16 @@ type AccessRequestSpec struct { RequestRef *commonapi.ObjectReference `json:"requestRef,omitempty"` // Permissions are the requested permissions. - Permissions []PermissionsRequest `json:"permissions"` + // If not empty, corresponding Roles and ClusterRoles will be created in the target cluster, potentially also creating namespaces for Roles. + // For token-based access, the serviceaccount will be bound to the created Roles and ClusterRoles. + // +optional + Permissions []PermissionsRequest `json:"permissions,omitempty"` + + // OIDCProvider is a configuration for an OIDC provider that should be used for authentication and associated role bindings. + // If set, the handling ClusterProvider will create an OIDC-based access for the AccessRequest, if supported. + // Otherwise, a serviceaccount with a token will be created and bound to the requested permissions. + // +optional + OIDCProvider *commonapi.OIDCProviderConfig `json:"oidcProvider,omitempty"` } type PermissionsRequest struct { diff --git a/api/clusters/v1alpha1/constants.go b/api/clusters/v1alpha1/constants.go index 80ab1d5..ddde8f1 100644 --- a/api/clusters/v1alpha1/constants.go +++ b/api/clusters/v1alpha1/constants.go @@ -74,3 +74,14 @@ const ( // RequestFinalizerOnClusterPrefix is the prefix for the finalizers that mark a Cluster as being referenced by a ClusterRequest. RequestFinalizerOnClusterPrefix = "request." + GroupName + "/" ) + +const ( + // SecretKeyKubeconfig is the name of the key in the AccessRequest secret that contains the kubeconfig. + SecretKeyKubeconfig = "kubeconfig" + // SecretKeyExpirationTimestamp is the name of the key in the AccessRequest secret that contains the expiration timestamp. + // This value is optional and must not be set for non-expiring authentication methods. + SecretKeyExpirationTimestamp = "expirationTimestamp" + // SecretKeyCreationTimestamp is the name of the key in the AccessRequest secret that contains the creation timestamp. + // This value is optional and must not be set for non-expiring authentication methods. + SecretKeyCreationTimestamp = "creationTimestamp" +) diff --git a/api/clusters/v1alpha1/zz_generated.deepcopy.go b/api/clusters/v1alpha1/zz_generated.deepcopy.go index f076a30..b7af543 100644 --- a/api/clusters/v1alpha1/zz_generated.deepcopy.go +++ b/api/clusters/v1alpha1/zz_generated.deepcopy.go @@ -90,6 +90,11 @@ func (in *AccessRequestSpec) DeepCopyInto(out *AccessRequestSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.OIDCProvider != nil { + in, out := &in.OIDCProvider, &out.OIDCProvider + *out = new(common.OIDCProviderConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRequestSpec. diff --git a/api/common/doc.go b/api/common/doc.go new file mode 100644 index 0000000..cb37cef --- /dev/null +++ b/api/common/doc.go @@ -0,0 +1,2 @@ +// +kubebuilder:object:generate=true +package common diff --git a/api/common/oidc_types.go b/api/common/oidc_types.go new file mode 100644 index 0000000..9abc717 --- /dev/null +++ b/api/common/oidc_types.go @@ -0,0 +1,99 @@ +package common + +import ( + "strings" + + rbacv1 "k8s.io/api/rbac/v1" +) + +type OIDCProviderConfig struct { + // Name is the name of the OIDC provider. + // May be used in k8s resources, therefore has to be a valid k8s name. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*` + Name string `json:"name"` + + // Issuer is the issuer URL of the OIDC provider. + Issuer string `json:"issuer"` + + // ClientID is the client ID to use for the OIDC provider. + ClientID string `json:"clientID"` + + // GroupsClaim is the claim in the OIDC token that contains the groups. + // If empty, the default claim "groups" will be used. + // +kubebuilder:default="groups" + // +optional + GroupsClaim string `json:"groupsClaim"` + + // GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. + // This is required to avoid conflicts with Kubernetes built-in groups. + // If the prefix does not end with a colon (:), it will be added automatically. + // +kubebuilder:validation:MinLength=1 + GroupsPrefix string `json:"groupsPrefix"` + + // UsernameClaim is the claim in the OIDC token that contains the username. + // If empty, the default claim "sub" will be used. + // +kubebuilder:default="sub" + // +optional + UsernameClaim string `json:"usernameClaim"` + + // UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. + // This is required to avoid conflicts with Kubernetes built-in users. + // If the prefix does not end with a colon (:), it will be added automatically. + // +kubebuilder:validation:MinLength=1 + UsernamePrefix string `json:"usernamePrefix"` + + // RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + // Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + RoleBindings []RoleBindings `json:"roleBindings"` +} + +type RoleBindings struct { + // Subjects is a list of subjects that should be bound to the specified roles. + // The subjects' names will be prefixed with the username prefix of the OIDC provider. + Subjects []rbacv1.Subject `json:"subjects"` + + // RoleRefs is a list of (cluster) role references that the subjects should be bound to. + // Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + RoleRefs []RoleRef `json:"roleRefs"` +} + +// +kubebuilder:validation:XValidation:rule="self.kind == 'Role' && has(self.namespace) && self.namespace != ”", message="namespace must be set if kind is 'Role'" +// +kubebuilder:validation:XValidation:rule="self.kind == 'ClusterRole' && (!has(self.namespace) || self.namespace == ”)", message="namespace must not be set if kind is 'ClusterRole'" +type RoleRef struct { + // Name is the name of the role or cluster role to bind to the subjects. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Namespace is the namespace of the role to bind to the subjects. + // It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + // +optional + Namespace string `json:"namespace,omitempty"` + + // Kind is the kind of the role to bind to the subjects. + // It must be 'Role' or 'ClusterRole'. + // +kubebuilder:validation:Enum=Role;ClusterRole + Kind string `json:"kind"` +} + +// Default sets default values for the OIDCProviderConfig. +// Modifies in-place and returns the receiver for chaining. +func (o *OIDCProviderConfig) Default() *OIDCProviderConfig { + if o == nil { + return nil + } + if o.GroupsClaim == "" { + o.GroupsClaim = "groups" + } + if !strings.HasSuffix(o.GroupsPrefix, ":") { + o.GroupsPrefix += ":" + } + if o.UsernameClaim == "" { + o.UsernameClaim = "sub" + } + if !strings.HasSuffix(o.UsernamePrefix, ":") { + o.UsernamePrefix += ":" + } + return o +} diff --git a/api/common/status_types.go b/api/common/status_types.go index 48020c6..dad5733 100644 --- a/api/common/status_types.go +++ b/api/common/status_types.go @@ -11,8 +11,6 @@ const ( StatusPhaseTerminating = "Terminating" ) -// +kubebuilder:object:generate=true - // Status represents the status of an openMCP resource. type Status struct { // ObservedGeneration is the generation of this resource that was last reconciled by the controller. diff --git a/api/common/zz_generated.deepcopy.go b/api/common/zz_generated.deepcopy.go index 919a9d7..89d22cf 100644 --- a/api/common/zz_generated.deepcopy.go +++ b/api/common/zz_generated.deepcopy.go @@ -5,15 +5,140 @@ package common import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference. +func (in *LocalObjectReference) DeepCopy() *LocalObjectReference { + if in == nil { + return nil + } + out := new(LocalObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) { + *out = *in + out.LocalObjectReference = in.LocalObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference. +func (in *LocalSecretReference) DeepCopy() *LocalSecretReference { + if in == nil { + return nil + } + out := new(LocalSecretReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCProviderConfig) DeepCopyInto(out *OIDCProviderConfig) { + *out = *in + if in.RoleBindings != nil { + in, out := &in.RoleBindings, &out.RoleBindings + *out = make([]RoleBindings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProviderConfig. +func (in *OIDCProviderConfig) DeepCopy() *OIDCProviderConfig { + if in == nil { + return nil + } + out := new(OIDCProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. +func (in *ObjectReference) DeepCopy() *ObjectReference { + if in == nil { + return nil + } + out := new(ObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleBindings) DeepCopyInto(out *RoleBindings) { + *out = *in + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]v1.Subject, len(*in)) + copy(*out, *in) + } + if in.RoleRefs != nil { + in, out := &in.RoleRefs, &out.RoleRefs + *out = make([]RoleRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleBindings. +func (in *RoleBindings) DeepCopy() *RoleBindings { + if in == nil { + return nil + } + out := new(RoleBindings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleRef) DeepCopyInto(out *RoleRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleRef. +func (in *RoleRef) DeepCopy() *RoleRef { + if in == nil { + return nil + } + out := new(RoleRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretReference) DeepCopyInto(out *SecretReference) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. +func (in *SecretReference) DeepCopy() *SecretReference { + if in == nil { + return nil + } + out := new(SecretReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Status) DeepCopyInto(out *Status) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/api/core/v2alpha1/constants.go b/api/core/v2alpha1/constants.go new file mode 100644 index 0000000..c4e254f --- /dev/null +++ b/api/core/v2alpha1/constants.go @@ -0,0 +1,6 @@ +package v2alpha1 + +const ( + // DefaultOIDCProviderName is the identifier for the default OIDC provider. + DefaultOIDCProviderName = "default" +) diff --git a/api/core/v2alpha1/groupversion_info.go b/api/core/v2alpha1/groupversion_info.go new file mode 100644 index 0000000..81565da --- /dev/null +++ b/api/core/v2alpha1/groupversion_info.go @@ -0,0 +1,19 @@ +// +kubebuilder:object:generate=true +// +groupName=core.openmcp.cloud +package v2alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "core.openmcp.cloud", Version: "v2alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/core/v2alpha1/managedcontrolplane_types.go b/api/core/v2alpha1/managedcontrolplane_types.go new file mode 100644 index 0000000..1437254 --- /dev/null +++ b/api/core/v2alpha1/managedcontrolplane_types.go @@ -0,0 +1,62 @@ +package v2alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + commonapi "github.com/openmcp-project/openmcp-operator/api/common" +) + +type ManagedControlPlaneSpec struct { + // IAM contains the access management configuration for the ManagedControlPlane. + IAM IAMConfig `json:"iam"` +} + +type ManagedControlPlaneStatus struct { + commonapi.Status `json:",inline"` + + // Access is a mapping from OIDC provider names to secret references. + // Each referenced secret is expected to contain a 'kubeconfig' key with the kubeconfig that was generated for the respective OIDC provider for the ManagedControlPlane. + // The default OIDC provider, if configured, uses the name "default" in this mapping. + // The "default" key is also used if the ClusterProvider does not support OIDC-based access and created a serviceaccount with a token instead. + Access map[string]commonapi.LocalObjectReference `json:"access"` +} + +type IAMConfig struct { + // RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + // These bindings refer to the standard OIDC provider. If empty, the standard OIDC provider is disabled. + // Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + // +optional + RoleBindings []commonapi.RoleBindings `json:"roleBindings,omitempty"` + + // OIDCProviders is a list of OIDC providers that should be configured for the ManagedControlPlane. + // They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. + // +kubebuilder:validation:items:XValidation:rule="self.name != 'default'", message="OIDC provider name must not be 'default' as this is reserved for the standard OIDC provider" + // +optional + OIDCProviders []*commonapi.OIDCProviderConfig `json:"oidcProviders,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=mcp +// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding" +// +kubebuilder:selectablefield:JSONPath=".status.phase" +// +kubebuilder:printcolumn:JSONPath=".status.phase",name="Phase",type=string + +type ManagedControlPlane struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ManagedControlPlaneSpec `json:"spec,omitempty"` + Status ManagedControlPlaneStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +type ManagedControlPlaneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ManagedControlPlane `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ManagedControlPlane{}, &ManagedControlPlaneList{}) +} diff --git a/api/core/v2alpha1/zz_generated.deepcopy.go b/api/core/v2alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..3a0fe17 --- /dev/null +++ b/api/core/v2alpha1/zz_generated.deepcopy.go @@ -0,0 +1,142 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + + "github.com/openmcp-project/openmcp-operator/api/common" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IAMConfig) DeepCopyInto(out *IAMConfig) { + *out = *in + if in.RoleBindings != nil { + in, out := &in.RoleBindings, &out.RoleBindings + *out = make([]common.RoleBindings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.OIDCProviders != nil { + in, out := &in.OIDCProviders, &out.OIDCProviders + *out = make([]*common.OIDCProviderConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(common.OIDCProviderConfig) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IAMConfig. +func (in *IAMConfig) DeepCopy() *IAMConfig { + if in == nil { + return nil + } + out := new(IAMConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlane) DeepCopyInto(out *ManagedControlPlane) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlane. +func (in *ManagedControlPlane) DeepCopy() *ManagedControlPlane { + if in == nil { + return nil + } + out := new(ManagedControlPlane) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedControlPlane) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneList) DeepCopyInto(out *ManagedControlPlaneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagedControlPlane, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneList. +func (in *ManagedControlPlaneList) DeepCopy() *ManagedControlPlaneList { + if in == nil { + return nil + } + out := new(ManagedControlPlaneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedControlPlaneList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneSpec) DeepCopyInto(out *ManagedControlPlaneSpec) { + *out = *in + in.IAM.DeepCopyInto(&out.IAM) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneSpec. +func (in *ManagedControlPlaneSpec) DeepCopy() *ManagedControlPlaneSpec { + if in == nil { + return nil + } + out := new(ManagedControlPlaneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneStatus) DeepCopyInto(out *ManagedControlPlaneStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + if in.Access != nil { + in, out := &in.Access, &out.Access + *out = make(map[string]common.LocalObjectReference, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneStatus. +func (in *ManagedControlPlaneStatus) DeepCopy() *ManagedControlPlaneStatus { + if in == nil { + return nil + } + out := new(ManagedControlPlaneStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml index 8aa3b65..6a688ba 100644 --- a/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml +++ b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml @@ -66,8 +66,146 @@ spec: x-kubernetes-validations: - message: clusterRef is immutable rule: self == oldSelf + oidcProvider: + description: |- + OIDCProvider is a configuration for an OIDC provider that should be used for authentication and associated role bindings. + If set, the handling ClusterProvider will create an OIDC-based access for the AccessRequest, if supported. + Otherwise, a serviceaccount with a token will be created and bound to the requested permissions. + properties: + clientID: + description: ClientID is the client ID to use for the OIDC provider. + type: string + groupsClaim: + default: groups + description: |- + GroupsClaim is the claim in the OIDC token that contains the groups. + If empty, the default claim "groups" will be used. + type: string + groupsPrefix: + description: |- + GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. + This is required to avoid conflicts with Kubernetes built-in groups. + If the prefix does not end with a colon (:), it will be added automatically. + minLength: 1 + type: string + issuer: + description: Issuer is the issuer URL of the OIDC provider. + type: string + name: + description: |- + Name is the name of the OIDC provider. + May be used in k8s resources, therefore has to be a valid k8s name. + maxLength: 253 + minLength: 1 + pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' + type: string + roleBindings: + description: |- + RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + items: + properties: + roleRefs: + description: |- + RoleRefs is a list of (cluster) role references that the subjects should be bound to. + Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + items: + properties: + kind: + description: |- + Kind is the kind of the role to bind to the subjects. + It must be 'Role' or 'ClusterRole'. + enum: + - Role + - ClusterRole + type: string + name: + description: Name is the name of the role or cluster + role to bind to the subjects. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the role to bind to the subjects. + It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + type: string + required: + - kind + - name + type: object + x-kubernetes-validations: + - message: namespace must be set if kind is 'Role' + rule: self.kind == 'Role' && has(self.namespace) && + self.namespace != ” + - message: namespace must not be set if kind is 'ClusterRole' + rule: self.kind == 'ClusterRole' && (!has(self.namespace) + || self.namespace == ”) + type: array + subjects: + description: |- + Subjects is a list of subjects that should be bound to the specified roles. + The subjects' names will be prefixed with the username prefix of the OIDC provider. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + required: + - roleRefs + - subjects + type: object + type: array + usernameClaim: + default: sub + description: |- + UsernameClaim is the claim in the OIDC token that contains the username. + If empty, the default claim "sub" will be used. + type: string + usernamePrefix: + description: |- + UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. + This is required to avoid conflicts with Kubernetes built-in users. + If the prefix does not end with a colon (:), it will be added automatically. + minLength: 1 + type: string + required: + - clientID + - groupsPrefix + - issuer + - name + - roleBindings + - usernamePrefix + type: object permissions: - description: Permissions are the requested permissions. + description: |- + Permissions are the requested permissions. + If not empty, corresponding Roles and ClusterRoles will be created in the target cluster, potentially also creating namespaces for Roles. + For token-based access, the serviceaccount will be bound to the created Roles and ClusterRoles. items: properties: namespace: @@ -150,8 +288,6 @@ spec: x-kubernetes-validations: - message: requestRef is immutable rule: self == oldSelf - required: - - permissions type: object x-kubernetes-validations: - message: clusterRef may not be removed once set diff --git a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml new file mode 100644 index 0000000..d706659 --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml @@ -0,0 +1,380 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + openmcp.cloud/cluster: onboarding + name: managedcontrolplanes.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ManagedControlPlane + listKind: ManagedControlPlaneList + plural: managedcontrolplanes + shortNames: + - mcp + singular: managedcontrolplane + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + name: v2alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + iam: + description: IAM contains the access management configuration for + the ManagedControlPlane. + properties: + oidcProviders: + description: |- + OIDCProviders is a list of OIDC providers that should be configured for the ManagedControlPlane. + They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. + items: + properties: + clientID: + description: ClientID is the client ID to use for the OIDC + provider. + type: string + groupsClaim: + default: groups + description: |- + GroupsClaim is the claim in the OIDC token that contains the groups. + If empty, the default claim "groups" will be used. + type: string + groupsPrefix: + description: |- + GroupsPrefix is a prefix that will be added to all group names when referenced in RBAC rules. + This is required to avoid conflicts with Kubernetes built-in groups. + If the prefix does not end with a colon (:), it will be added automatically. + minLength: 1 + type: string + issuer: + description: Issuer is the issuer URL of the OIDC provider. + type: string + name: + description: |- + Name is the name of the OIDC provider. + May be used in k8s resources, therefore has to be a valid k8s name. + maxLength: 253 + minLength: 1 + pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' + type: string + roleBindings: + description: |- + RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + items: + properties: + roleRefs: + description: |- + RoleRefs is a list of (cluster) role references that the subjects should be bound to. + Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + items: + properties: + kind: + description: |- + Kind is the kind of the role to bind to the subjects. + It must be 'Role' or 'ClusterRole'. + enum: + - Role + - ClusterRole + type: string + name: + description: Name is the name of the role or + cluster role to bind to the subjects. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the role to bind to the subjects. + It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + type: string + required: + - kind + - name + type: object + x-kubernetes-validations: + - message: namespace must be set if kind is 'Role' + rule: self.kind == 'Role' && has(self.namespace) + && self.namespace != ” + - message: namespace must not be set if kind is + 'ClusterRole' + rule: self.kind == 'ClusterRole' && (!has(self.namespace) + || self.namespace == ”) + type: array + subjects: + description: |- + Subjects is a list of subjects that should be bound to the specified roles. + The subjects' names will be prefixed with the username prefix of the OIDC provider. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + required: + - roleRefs + - subjects + type: object + type: array + usernameClaim: + default: sub + description: |- + UsernameClaim is the claim in the OIDC token that contains the username. + If empty, the default claim "sub" will be used. + type: string + usernamePrefix: + description: |- + UsernamePrefix is a prefix that will be added to all usernames when referenced in RBAC rules. + This is required to avoid conflicts with Kubernetes built-in users. + If the prefix does not end with a colon (:), it will be added automatically. + minLength: 1 + type: string + required: + - clientID + - groupsPrefix + - issuer + - name + - roleBindings + - usernamePrefix + type: object + x-kubernetes-validations: + - message: OIDC provider name must not be 'default' as this + is reserved for the standard OIDC provider + rule: self.name != 'default' + type: array + roleBindings: + description: |- + RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + These bindings refer to the standard OIDC provider. If empty, the standard OIDC provider is disabled. + Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + items: + properties: + roleRefs: + description: |- + RoleRefs is a list of (cluster) role references that the subjects should be bound to. + Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + items: + properties: + kind: + description: |- + Kind is the kind of the role to bind to the subjects. + It must be 'Role' or 'ClusterRole'. + enum: + - Role + - ClusterRole + type: string + name: + description: Name is the name of the role or cluster + role to bind to the subjects. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the role to bind to the subjects. + It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + type: string + required: + - kind + - name + type: object + x-kubernetes-validations: + - message: namespace must be set if kind is 'Role' + rule: self.kind == 'Role' && has(self.namespace) && + self.namespace != ” + - message: namespace must not be set if kind is 'ClusterRole' + rule: self.kind == 'ClusterRole' && (!has(self.namespace) + || self.namespace == ”) + type: array + subjects: + description: |- + Subjects is a list of subjects that should be bound to the specified roles. + The subjects' names will be prefixed with the username prefix of the OIDC provider. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + required: + - roleRefs + - subjects + type: object + type: array + type: object + required: + - iam + type: object + status: + properties: + access: + additionalProperties: + description: LocalObjectReference is a reference to an object in + the same namespace as the resource referencing it. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + description: |- + Access is a mapping from OIDC provider names to secret references. + Each referenced secret is expected to contain a 'kubeconfig' key with the kubeconfig that was generated for the respective OIDC provider for the ManagedControlPlane. + The default OIDC provider, if configured, uses the name "default" in this mapping. + The "default" key is also used if the ClusterProvider does not support OIDC-based access and created a serviceaccount with a token instead. + type: object + conditions: + description: Conditions contains the conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the generation of this resource + that was last reconciled by the controller. + format: int64 + type: integer + phase: + description: Phase is the current phase of the resource. + type: string + required: + - access + - observedGeneration + - phase + type: object + type: object + selectableFields: + - jsonPath: .status.phase + served: true + storage: true + subresources: + status: {} diff --git a/api/install/install.go b/api/install/install.go index 0126953..fce1acf 100644 --- a/api/install/install.go +++ b/api/install/install.go @@ -7,6 +7,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" + corev2alpha1 "github.com/openmcp-project/openmcp-operator/api/core/v2alpha1" providerv1alpha1 "github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1" ) @@ -23,6 +24,7 @@ func InstallOperatorAPIs(scheme *runtime.Scheme) *runtime.Scheme { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(providerv1alpha1.AddToScheme(scheme)) utilruntime.Must(clustersv1alpha1.AddToScheme(scheme)) + utilruntime.Must(corev2alpha1.AddToScheme(scheme)) return scheme }