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
31 changes: 30 additions & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,36 @@ includes:
NESTED_MODULES: api
API_DIRS: '{{.ROOT_DIR}}/api/core/v1alpha1/...'
MANIFEST_OUT: '{{.ROOT_DIR}}/api/crds/manifests'
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/test/... {{.ROOT_DIR}}/api/core/v1alpha1/...'
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/core/v1alpha1/...'
COMPONENTS: 'project-workspace-operator'
CRDS_COMPONENTS: 'project-workspace-operator'
REPO_URL: 'https://github.com/openmcp-project/project-workspace-operator'
ENVTEST_REQUIRED: "true"

tasks:
platformservice:
desc: " Generates a PlatformService manifest for the current version. Set the VERBOSITY env var to overwrite the default verbosity level (INFO)."
requires:
vars:
- VERSION
vars:
VERBOSITY:
sh: echo "${VERBOSITY:-INFO}"
cmds:
- cmd: |
cat << EOF
apiVersion: openmcp.cloud/v1alpha1
kind: PlatformService
metadata:
name: project-workspace
spec:
image: ghcr.io/openmcp-project/images/project-workspace-operator:{{.VERSION}}
verbosity: {{.VERBOSITY}}
initCommand:
- platformservice
- init
runCommand:
- platformservice
- run
EOF
silent: true
1 change: 1 addition & 0 deletions api/core/v1alpha1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (s Subject) RbacV1() rbacv1.Subject {
// MemberOverrides is a resource used to Manage admin access to the Project/Workspace operator resources.
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
type MemberOverrides struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
7 changes: 0 additions & 7 deletions api/core/v1alpha1/common_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package v1alpha1
import (
"context"
"fmt"
"os"
"strings"

admissionv1 "k8s.io/api/admission/v1"
authv1 "k8s.io/api/authentication/v1"
Expand Down Expand Up @@ -59,11 +57,6 @@ func setMetaDataAnnotation(meta metav1.Object, key, value string) { // TODO move
meta.SetAnnotations(labels)
}

func isOwnServiceAccount(userinfo authv1.UserInfo) bool {
svcAccUsername := fmt.Sprintf("system:serviceaccount:%s:%s", os.Getenv("POD_NAMESPACE"), os.Getenv("POD_SERVICE_ACCOUNT"))
return strings.HasSuffix(userinfo.Username, svcAccUsername)
}

// userInfoFromContext extracts the authv1.UserInfo from the admission.Request available in the context. Returns an error if the request can't be found.
func userInfoFromContext(ctx context.Context) (authv1.UserInfo, error) {
req, err := admission.RequestFromContext(ctx)
Expand Down
4 changes: 3 additions & 1 deletion api/core/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

const GroupName = "core.openmcp.cloud"

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "core.openmcp.cloud", Version: "v1alpha1"}
GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
Expand Down
1 change: 1 addition & 0 deletions api/core/v1alpha1/project_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type ProjectStatus struct {
// +kubebuilder:printcolumn:name="Resulting Namespace",type="string",JSONPath=".status.namespace"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 25",message="Name must not be longer than 25 characters"
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
type Project struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
68 changes: 31 additions & 37 deletions api/core/v1alpha1/project_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,39 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// +kubebuilder:object:generate=false
type ProjectWebhook struct {
client.Client

// Identity is the name of the entity (usually a service account) the project-workspace-operator uses to access the onboarding cluster.
// It is required to exclude the operator's own identity from validation checks.
Identity string
OverrideName string
}

// log is for logging in this package.
var projectlog = logf.Log.WithName("project-resource")

func (p *Project) SetupWebhookWithManager(mgr ctrl.Manager, memberOverridesName string) error {
func (p *Project) SetupWebhookWithManager(ctx context.Context, mgr ctrl.Manager, memberOverridesName, identity string) error {
pwh := &ProjectWebhook{
Client: mgr.GetClient(),
OverrideName: memberOverridesName,
Identity: identity,
}

return ctrl.NewWebhookManagedBy(mgr).
For(&Project{}).
WithDefaulter(&Project{}).
WithValidator(&projectValidator{
Client: mgr.GetClient(),
overrideName: memberOverridesName,
}).
For(p).
WithDefaulter(pwh).
WithValidator(pwh).
Complete()
}

// +kubebuilder:webhook:path=/mutate-core-openmcp-cloud-v1alpha1-project,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=projects,verbs=create;update,versions=v1alpha1,name=mproject.kb.io,admissionReviewVersions=v1
// +kubebuilder:webhook:path=/mutate-core-openmcp-cloud-v1alpha1-project,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=projects,verbs=create;update,versions=v1alpha1,name=mproject.openmcp.cloud,admissionReviewVersions=v1

var _ webhook.CustomDefaulter = &Project{}
var _ webhook.CustomDefaulter = &ProjectWebhook{}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
func (p *Project) Default(ctx context.Context, obj runtime.Object) error {
func (p *ProjectWebhook) Default(ctx context.Context, obj runtime.Object) error {
project, err := expectProject(obj)
if err != nil {
return err
Expand All @@ -50,30 +62,12 @@ func (p *Project) Default(ctx context.Context, obj runtime.Object) error {
return nil
}

// Project must implement webhook.CustomValidator in order for it's Mutating/Validating Webhook configuration to be generated by "github.com/openmcp-project/controller-utils/pkg/init/webhooks"
var _ webhook.CustomValidator = &Project{}

func (p *Project) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
return
}
func (p *Project) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings admission.Warnings, err error) {
return
}
func (p *Project) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
return
}

// +kubebuilder:webhook:path=/validate-core-openmcp-cloud-v1alpha1-project,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=projects,verbs=create;update;delete,versions=v1alpha1,name=vproject.kb.io,admissionReviewVersions=v1

var _ webhook.CustomValidator = &projectValidator{}
// +kubebuilder:webhook:path=/validate-core-openmcp-cloud-v1alpha1-project,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=projects,verbs=create;update;delete,versions=v1alpha1,name=vproject.openmcp.cloud,admissionReviewVersions=v1

type projectValidator struct {
client.Client
overrideName string
}
var _ webhook.CustomValidator = &ProjectWebhook{}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
func (v *projectValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
func (v *ProjectWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
project, err := expectProject(obj)
if err != nil {
return
Expand All @@ -97,7 +91,7 @@ func (v *projectValidator) ValidateCreate(ctx context.Context, obj runtime.Objec
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
func (v *projectValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings admission.Warnings, err error) {
func (v *ProjectWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings admission.Warnings, err error) {
oldProject, err := expectProject(oldObj)
if err != nil {
return
Expand Down Expand Up @@ -138,7 +132,7 @@ func (v *projectValidator) ValidateUpdate(ctx context.Context, oldObj, newObj ru
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
func (v *projectValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
func (v *ProjectWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
project, err := expectProject(obj)
if err != nil {
return
Expand All @@ -160,21 +154,21 @@ func expectProject(obj runtime.Object) (*Project, error) {
return project, nil
}

func (v *projectValidator) ensureValidRole(ctx context.Context, project *Project) (bool, error) {
func (v *ProjectWebhook) ensureValidRole(ctx context.Context, project *Project) (bool, error) {
userInfo, err := userInfoFromContext(ctx)
if err != nil {
return false, fmt.Errorf("failed to get userInfo")
}
if project.UserInfoHasRole(userInfo, ProjectRoleAdmin) || isOwnServiceAccount(userInfo) {
if project.UserInfoHasRole(userInfo, ProjectRoleAdmin) || userInfo.Username == v.Identity {
return true, nil
}

if v.overrideName == "" {
if v.OverrideName == "" {
return false, nil
}

overrides := &MemberOverrides{}
if err := v.Get(ctx, types.NamespacedName{Name: v.overrideName}, overrides); err != nil {
if err := v.Get(ctx, types.NamespacedName{Name: v.OverrideName}, overrides); err != nil {
return false, err
}
if overrides.HasAdminOverrideForResource(&userInfo, project.Name, project.Kind) {
Expand Down
77 changes: 77 additions & 0 deletions api/core/v1alpha1/pwconfig_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package v1alpha1

import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ProjectWorkspaceConfigSpec defines the desired state of ProjectWorkspaceConfig
type ProjectWorkspaceConfigSpec struct {
// +optional
Project ProjectConfig `json:"project"`
// +optional
Workspace WorkspaceConfig `json:"workspace"`
// MemberOverridesName is the name of the MemberOverrides resource that should be used to manage admin access to the projects and workspaces.
// Leave empty to disable.
// +optional
MemberOverridesName string `json:"memberOverridesName,omitempty"`
// Webhook contains the configuration for the webhooks.
// +optional
Webhook WebhookConfig `json:"webhook"`
}

// ProjectWorkspaceConfig is the Schema for the ProjectWorkspaceConfigs API
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName=pwcfg
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=platform"
type ProjectWorkspaceConfig struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`

Spec ProjectWorkspaceConfigSpec `json:"spec"`
}

// ProjectConfig contains the configuration for projects.
type ProjectConfig struct {
// +optional
ResourcesBlockingDeletion []metav1.GroupVersionKind `json:"resourcesBlockingDeletion,omitempty"`
// AdditionalPermissions defines additional permissions users should have in a project, depending on their role.
// +optional
AdditionalPermissions map[ProjectMemberRole][]rbacv1.PolicyRule `json:"additionalPermissions,omitempty"`
}

// WorkspaceConfig contains the configuration for workspaces.
type WorkspaceConfig struct {
// +optional
ResourcesBlockingDeletion []metav1.GroupVersionKind `json:"resourcesBlockingDeletion,omitempty"`
// AdditionalPermissions defines additional permissions users should have in a workspace, depending on their role.
// +optional
AdditionalPermissions map[WorkspaceMemberRole][]rbacv1.PolicyRule `json:"additionalPermissions,omitempty"`
}

type WebhookConfig struct {
// Disabled specifies whether the webhooks should be disabled.
// +optional
Disabled bool `json:"disabled"`
}

// +kubebuilder:object:root=true

// ProjectWorkspaceConfigList contains a list of ProjectWorkspaceConfig
type ProjectWorkspaceConfigList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []ProjectWorkspaceConfig `json:"items"`
}

func init() {
SchemeBuilder.Register(&ProjectWorkspaceConfig{}, &ProjectWorkspaceConfigList{})
}

// SetDefaults sets the default values for the project workspace configuration when not set.
func (pwc *ProjectWorkspaceConfig) SetDefaults() {}

// Validate validates the project workspace configuration.
func (pwc *ProjectWorkspaceConfig) Validate() error {
return nil
}
6 changes: 4 additions & 2 deletions api/core/v1alpha1/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ var _ = BeforeSuite(func() {
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())

identity := "nobody"

// start webhook server using Manager
webhookInstallOptions := &testEnv.WebhookInstallOptions
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Expand All @@ -106,10 +108,10 @@ var _ = BeforeSuite(func() {
})
Expect(err).NotTo(HaveOccurred())

err = (&Project{}).SetupWebhookWithManager(mgr, "test-override")
err = (&Project{}).SetupWebhookWithManager(ctx, mgr, "test-override", identity)
Expect(err).NotTo(HaveOccurred())

err = (&Workspace{}).SetupWebhookWithManager(mgr, "test-override")
err = (&Workspace{}).SetupWebhookWithManager(ctx, mgr, "test-override", identity)
Expect(err).NotTo(HaveOccurred())

// +kubebuilder:scaffold:webhook
Expand Down
1 change: 1 addition & 0 deletions api/core/v1alpha1/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type WorkspaceStatus struct {
// +kubebuilder:printcolumn:name="Resulting Namespace",type="string",JSONPath=".status.namespace"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 25",message="Name must not be longer than 25 characters"
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
type Workspace struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
Loading