Skip to content

Commit 697d879

Browse files
authored
feat: make PWO v2-compatible (#132)
* make additional roles configurable * turn config into k8s crd * make pwo runnable as platformservice * fetch managed resources from service providers * fix issues * fix webhook * add gateway configuration * fix logging issue * fix scheme issue * fix webhook configuration * fix validating webhook url port * fix webhook certs * exclude pwo identity from validation rules * fix rbac permissions * add permissions to check for deletion-blocking resources * webhook TLS secret mounting logic has changed * bump controller-utils dependency * add crds to component descriptor * bump deps
1 parent 94a1ca9 commit 697d879

36 files changed

+2165
-333
lines changed

Taskfile.yaml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,36 @@ includes:
1818
NESTED_MODULES: api
1919
API_DIRS: '{{.ROOT_DIR}}/api/core/v1alpha1/...'
2020
MANIFEST_OUT: '{{.ROOT_DIR}}/api/crds/manifests'
21-
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/test/... {{.ROOT_DIR}}/api/core/v1alpha1/...'
21+
CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/core/v1alpha1/...'
2222
COMPONENTS: 'project-workspace-operator'
23+
CRDS_COMPONENTS: 'project-workspace-operator'
2324
REPO_URL: 'https://github.com/openmcp-project/project-workspace-operator'
2425
ENVTEST_REQUIRED: "true"
26+
27+
tasks:
28+
platformservice:
29+
desc: " Generates a PlatformService manifest for the current version. Set the VERBOSITY env var to overwrite the default verbosity level (INFO)."
30+
requires:
31+
vars:
32+
- VERSION
33+
vars:
34+
VERBOSITY:
35+
sh: echo "${VERBOSITY:-INFO}"
36+
cmds:
37+
- cmd: |
38+
cat << EOF
39+
apiVersion: openmcp.cloud/v1alpha1
40+
kind: PlatformService
41+
metadata:
42+
name: project-workspace
43+
spec:
44+
image: ghcr.io/openmcp-project/images/project-workspace-operator:{{.VERSION}}
45+
verbosity: {{.VERBOSITY}}
46+
initCommand:
47+
- platformservice
48+
- init
49+
runCommand:
50+
- platformservice
51+
- run
52+
EOF
53+
silent: true

api/core/v1alpha1/common_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func (s Subject) RbacV1() rbacv1.Subject {
4747
// MemberOverrides is a resource used to Manage admin access to the Project/Workspace operator resources.
4848
// +kubebuilder:object:root=true
4949
// +kubebuilder:resource:scope=Cluster
50+
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
5051
type MemberOverrides struct {
5152
metav1.TypeMeta `json:",inline"`
5253
metav1.ObjectMeta `json:"metadata,omitempty"`

api/core/v1alpha1/common_webhook.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package v1alpha1
33
import (
44
"context"
55
"fmt"
6-
"os"
7-
"strings"
86

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

62-
func isOwnServiceAccount(userinfo authv1.UserInfo) bool {
63-
svcAccUsername := fmt.Sprintf("system:serviceaccount:%s:%s", os.Getenv("POD_NAMESPACE"), os.Getenv("POD_SERVICE_ACCOUNT"))
64-
return strings.HasSuffix(userinfo.Username, svcAccUsername)
65-
}
66-
6760
// userInfoFromContext extracts the authv1.UserInfo from the admission.Request available in the context. Returns an error if the request can't be found.
6861
func userInfoFromContext(ctx context.Context) (authv1.UserInfo, error) {
6962
req, err := admission.RequestFromContext(ctx)

api/core/v1alpha1/groupversion_info.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"sigs.k8s.io/controller-runtime/pkg/scheme"
99
)
1010

11+
const GroupName = "core.openmcp.cloud"
12+
1113
var (
1214
// GroupVersion is group version used to register these objects
13-
GroupVersion = schema.GroupVersion{Group: "core.openmcp.cloud", Version: "v1alpha1"}
15+
GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
1416

1517
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
1618
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

api/core/v1alpha1/project_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type ProjectStatus struct {
7474
// +kubebuilder:printcolumn:name="Resulting Namespace",type="string",JSONPath=".status.namespace"
7575
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
7676
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 25",message="Name must not be longer than 25 characters"
77+
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
7778
type Project struct {
7879
metav1.TypeMeta `json:",inline"`
7980
metav1.ObjectMeta `json:"metadata,omitempty"`

api/core/v1alpha1/project_webhook.go

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,39 @@ import (
1313
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
1414
)
1515

16+
// +kubebuilder:object:generate=false
17+
type ProjectWebhook struct {
18+
client.Client
19+
20+
// Identity is the name of the entity (usually a service account) the project-workspace-operator uses to access the onboarding cluster.
21+
// It is required to exclude the operator's own identity from validation checks.
22+
Identity string
23+
OverrideName string
24+
}
25+
1626
// log is for logging in this package.
1727
var projectlog = logf.Log.WithName("project-resource")
1828

19-
func (p *Project) SetupWebhookWithManager(mgr ctrl.Manager, memberOverridesName string) error {
29+
func (p *Project) SetupWebhookWithManager(ctx context.Context, mgr ctrl.Manager, memberOverridesName, identity string) error {
30+
pwh := &ProjectWebhook{
31+
Client: mgr.GetClient(),
32+
OverrideName: memberOverridesName,
33+
Identity: identity,
34+
}
2035

2136
return ctrl.NewWebhookManagedBy(mgr).
22-
For(&Project{}).
23-
WithDefaulter(&Project{}).
24-
WithValidator(&projectValidator{
25-
Client: mgr.GetClient(),
26-
overrideName: memberOverridesName,
27-
}).
37+
For(p).
38+
WithDefaulter(pwh).
39+
WithValidator(pwh).
2840
Complete()
2941
}
3042

31-
// +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
43+
// +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
3244

33-
var _ webhook.CustomDefaulter = &Project{}
45+
var _ webhook.CustomDefaulter = &ProjectWebhook{}
3446

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

53-
// 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"
54-
var _ webhook.CustomValidator = &Project{}
55-
56-
func (p *Project) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
57-
return
58-
}
59-
func (p *Project) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings admission.Warnings, err error) {
60-
return
61-
}
62-
func (p *Project) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
63-
return
64-
}
65-
66-
// +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
67-
68-
var _ webhook.CustomValidator = &projectValidator{}
65+
// +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
6966

70-
type projectValidator struct {
71-
client.Client
72-
overrideName string
73-
}
67+
var _ webhook.CustomValidator = &ProjectWebhook{}
7468

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

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

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

163-
func (v *projectValidator) ensureValidRole(ctx context.Context, project *Project) (bool, error) {
157+
func (v *ProjectWebhook) ensureValidRole(ctx context.Context, project *Project) (bool, error) {
164158
userInfo, err := userInfoFromContext(ctx)
165159
if err != nil {
166160
return false, fmt.Errorf("failed to get userInfo")
167161
}
168-
if project.UserInfoHasRole(userInfo, ProjectRoleAdmin) || isOwnServiceAccount(userInfo) {
162+
if project.UserInfoHasRole(userInfo, ProjectRoleAdmin) || userInfo.Username == v.Identity {
169163
return true, nil
170164
}
171165

172-
if v.overrideName == "" {
166+
if v.OverrideName == "" {
173167
return false, nil
174168
}
175169

176170
overrides := &MemberOverrides{}
177-
if err := v.Get(ctx, types.NamespacedName{Name: v.overrideName}, overrides); err != nil {
171+
if err := v.Get(ctx, types.NamespacedName{Name: v.OverrideName}, overrides); err != nil {
178172
return false, err
179173
}
180174
if overrides.HasAdminOverrideForResource(&userInfo, project.Name, project.Kind) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package v1alpha1
2+
3+
import (
4+
rbacv1 "k8s.io/api/rbac/v1"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
)
7+
8+
// ProjectWorkspaceConfigSpec defines the desired state of ProjectWorkspaceConfig
9+
type ProjectWorkspaceConfigSpec struct {
10+
// +optional
11+
Project ProjectConfig `json:"project"`
12+
// +optional
13+
Workspace WorkspaceConfig `json:"workspace"`
14+
// MemberOverridesName is the name of the MemberOverrides resource that should be used to manage admin access to the projects and workspaces.
15+
// Leave empty to disable.
16+
// +optional
17+
MemberOverridesName string `json:"memberOverridesName,omitempty"`
18+
// Webhook contains the configuration for the webhooks.
19+
// +optional
20+
Webhook WebhookConfig `json:"webhook"`
21+
}
22+
23+
// ProjectWorkspaceConfig is the Schema for the ProjectWorkspaceConfigs API
24+
// +kubebuilder:object:root=true
25+
// +kubebuilder:resource:scope=Cluster,shortName=pwcfg
26+
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=platform"
27+
type ProjectWorkspaceConfig struct {
28+
metav1.TypeMeta `json:",inline"`
29+
metav1.ObjectMeta `json:"metadata"`
30+
31+
Spec ProjectWorkspaceConfigSpec `json:"spec"`
32+
}
33+
34+
// ProjectConfig contains the configuration for projects.
35+
type ProjectConfig struct {
36+
// +optional
37+
ResourcesBlockingDeletion []metav1.GroupVersionKind `json:"resourcesBlockingDeletion,omitempty"`
38+
// AdditionalPermissions defines additional permissions users should have in a project, depending on their role.
39+
// +optional
40+
AdditionalPermissions map[ProjectMemberRole][]rbacv1.PolicyRule `json:"additionalPermissions,omitempty"`
41+
}
42+
43+
// WorkspaceConfig contains the configuration for workspaces.
44+
type WorkspaceConfig struct {
45+
// +optional
46+
ResourcesBlockingDeletion []metav1.GroupVersionKind `json:"resourcesBlockingDeletion,omitempty"`
47+
// AdditionalPermissions defines additional permissions users should have in a workspace, depending on their role.
48+
// +optional
49+
AdditionalPermissions map[WorkspaceMemberRole][]rbacv1.PolicyRule `json:"additionalPermissions,omitempty"`
50+
}
51+
52+
type WebhookConfig struct {
53+
// Disabled specifies whether the webhooks should be disabled.
54+
// +optional
55+
Disabled bool `json:"disabled"`
56+
}
57+
58+
// +kubebuilder:object:root=true
59+
60+
// ProjectWorkspaceConfigList contains a list of ProjectWorkspaceConfig
61+
type ProjectWorkspaceConfigList struct {
62+
metav1.TypeMeta `json:",inline"`
63+
metav1.ListMeta `json:"metadata"`
64+
Items []ProjectWorkspaceConfig `json:"items"`
65+
}
66+
67+
func init() {
68+
SchemeBuilder.Register(&ProjectWorkspaceConfig{}, &ProjectWorkspaceConfigList{})
69+
}
70+
71+
// SetDefaults sets the default values for the project workspace configuration when not set.
72+
func (pwc *ProjectWorkspaceConfig) SetDefaults() {}
73+
74+
// Validate validates the project workspace configuration.
75+
func (pwc *ProjectWorkspaceConfig) Validate() error {
76+
return nil
77+
}

api/core/v1alpha1/webhook_suite_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ var _ = BeforeSuite(func() {
9292
Expect(err).NotTo(HaveOccurred())
9393
Expect(k8sClient).NotTo(BeNil())
9494

95+
identity := "nobody"
96+
9597
// start webhook server using Manager
9698
webhookInstallOptions := &testEnv.WebhookInstallOptions
9799
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
@@ -106,10 +108,10 @@ var _ = BeforeSuite(func() {
106108
})
107109
Expect(err).NotTo(HaveOccurred())
108110

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

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

115117
// +kubebuilder:scaffold:webhook

api/core/v1alpha1/workspace_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type WorkspaceStatus struct {
7474
// +kubebuilder:printcolumn:name="Resulting Namespace",type="string",JSONPath=".status.namespace"
7575
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
7676
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 25",message="Name must not be longer than 25 characters"
77+
// +kubebuilder:metadata:labels="openmcp.cloud/cluster=onboarding"
7778
type Workspace struct {
7879
metav1.TypeMeta `json:",inline"`
7980
metav1.ObjectMeta `json:"metadata,omitempty"`

0 commit comments

Comments
 (0)