Skip to content

Commit ed7b70c

Browse files
authored
feat: Implement v2 bridge (#103)
* implement v2 path for APIServer controller * fix apiserver conditions * implement v1-v2 bridge for APIServer component * add documentation for the v2 bridge * implement review feedback: replace architecture config with general config * implement review feedback: handle ValidatingAdmissionPolicy(Binding) lifecycle * use StableRequestNamespace function for generating the ClusterRequest namespace * fix linting issue and broken test * remove wrong log message * add architecture configuration to mcp-operator helm chart
1 parent 944a6e9 commit ed7b70c

File tree

28 files changed

+1457
-26
lines changed

28 files changed

+1457
-26
lines changed

api/constants/conditions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@ package constants
33
const (
44
// ConditionMCPSuccessful is an aggregated condition showing whether all component resources could be reconciled successfully.
55
ConditionMCPSuccessful = "MCPSuccessful"
6+
7+
ConditionClusterRequestGranted = "ClusterRequestGranted"
8+
ConditionClusterReady = "ClusterReady"
9+
ConditionAccessRequestGranted = "AccessRequestGranted"
10+
ConditionAccessRequestDeleted = "AccessRequestDeleted"
11+
ConditionClusterRequestDeleted = "ClusterRequestDeleted"
612
)

api/constants/reasons.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,11 @@ const (
9797
// ReasonNotAllComponentsReconciledSuccessfully indicates that not all components have been reconciled successfully.
9898
ReasonNotAllComponentsReconciledSuccessfully = "NotAllComponentsReconciledSuccessfully"
9999
)
100+
101+
const (
102+
ReasonClusterRequestNotGranted = "ClusterRequestNotGranted"
103+
ReasonClusterNotReady = "ClusterNotReady"
104+
ReasonAccessRequestNotGranted = "AccessRequestNotGranted"
105+
ReasonAccessRequestNotDeleted = "AccessRequestNotDeleted"
106+
ReasonClusterRequestNotDeleted = "ClusterRequestNotDeleted"
107+
)

api/core/v1alpha1/component_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,15 @@ func (ct ComponentType) ReconciliationCondition() string {
139139
func (ct ComponentType) HealthyCondition() string {
140140
return fmt.Sprintf("%sHealthy", string(ct))
141141
}
142+
143+
// ArchitectureLabelPrefix returns the component-specific architecture label prefix.
144+
// Note that this label is only used on the MCP resource itself, on the component resources, the static ArchitectureLabelPrefix is used.
145+
func (ct ComponentType) ArchitectureLabelPrefix() string {
146+
return fmt.Sprintf("%s.%s", strings.ToLower(string(ct)), ArchitectureLabelPrefix)
147+
}
148+
149+
// ArchitectureVersionLabel returns the component-specific architecture version label.
150+
// Note that this label is only used on the MCP resource itself, on the component resources, the static ArchitectureVersionLabel is used.
151+
func (ct ComponentType) ArchitectureVersionLabel() string {
152+
return fmt.Sprintf("%s%s", ct.ArchitectureLabelPrefix(), "version")
153+
}

api/core/v1alpha1/constants.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ const (
3939
// ManagedByLabel is added to resources created by the operator.
4040
ManagedByLabel = BaseDomain + "/managed-by"
4141

42+
// ManagedPurposeLabel holds the purpose of a managed resource.
43+
ManagedPurposeLabel = "managed." + BaseDomain + "/purpose"
44+
// ManagedPurposeArchitectureImmutability is the value of the managed purpose label for resources that are used to enforce architecture immutability.
45+
ManagedPurposeArchitectureImmutability = "architecture-immutability"
46+
4247
CreatedByAnnotation = BaseDomain + "/created-by"
4348

4449
DisplayNameAnnotation = BaseDomain + "/display-name"
@@ -76,4 +81,12 @@ const (
7681
APIServerDomain = "apiserver." + BaseDomain
7782

7883
ManagedByAPIServerLabel = APIServerDomain + "/managed"
84+
85+
// Architecture Switch Labels
86+
ArchitectureLabelPrefix = "architecture." + BaseDomain + "/"
87+
ArchitectureVersionLabel = ArchitectureLabelPrefix + "version"
88+
ArchitectureV1 = "v1"
89+
ArchitectureV2 = "v2"
90+
V1MCPReferenceLabelName = "v1." + BaseDomain + "/mcp-name"
91+
V1MCPReferenceLabelNamespace = "v1." + BaseDomain + "/mcp-namespace"
7992
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
apiVersion: v1
3+
kind: ConfigMap
4+
metadata:
5+
name: mcp-operator-config
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "mcp-operator.labels" . | nindent 4 }}
9+
data:
10+
config.yaml: |
11+
architecture:
12+
{{- if and .Values.architecture .Values.architecture.immutability }}
13+
immutability:
14+
{{- .Values.architecture.immutability | toYaml | nindent 8 }}
15+
{{- end }}
16+
{{- if and .Values.apiServer .Values.apiServer.architecture }}
17+
apiServer:
18+
version: {{ .Values.apiServer.architecture.version | default "v1" }}
19+
allowOverride: {{ .Values.apiServer.architecture.allowOverride | default false }}
20+
{{- end }}

charts/mcp-operator/templates/deployment.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ spec:
3030
checksum/co-clusters: {{ include (print $.Template.BasePath "/secrets-cloudorchestrator-clusters.yaml") . | sha256sum }}
3131
checksum/auth-config: {{ include (print $.Template.BasePath "/secret-auth-config.yaml") . | sha256sum }}
3232
checksum/authz-config: {{ include (print $.Template.BasePath "/secret-authz-config.yaml") . | sha256sum }}
33+
checksum/mcp-operator-config: {{ include (print $.Template.BasePath "/configmap-mcp-operator-config.yaml") . | sha256sum }}
3334
{{- with .Values.podAnnotations }}
3435
{{- toYaml . | nindent 8 }}
3536
{{- end }}
@@ -50,6 +51,7 @@ spec:
5051
command:
5152
- /mcp-operator
5253
- --controllers={{ include "mcp-operator.activeControllersString" .Values }}
54+
- --config=/etc/config/mcp-operator/config.yaml
5355
{{- if .Values.deployment.leaderElection.enabled }}
5456
- --leader-elect
5557
- --lease-namespace={{ .Values.deployment.leaderElection.leaseNamespace }}
@@ -140,6 +142,9 @@ spec:
140142
mountPath: /tmp/k8s-webhook-server/serving-certs/
141143
readOnly: true
142144
{{- end }}
145+
- name: mcp-operator-config
146+
mountPath: /etc/config/mcp-operator
147+
readOnly: true
143148
- name: common
144149
mountPath: /etc/config/common
145150
readOnly: true
@@ -247,6 +252,11 @@ spec:
247252
{{- end }}
248253
{{- end }}
249254
{{- end }}
255+
- name: mcp-operator-config
256+
projected:
257+
sources:
258+
- configMap:
259+
name: mcp-operator-config
250260
- name: common
251261
projected:
252262
sources:

charts/mcp-operator/values.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ clusters:
3232
# caData: ...
3333
# caConfigMapName: ...
3434

35-
35+
# architecture: # architecture configuration
36+
# immutability:
37+
# policyName: mcp-architecture-immutability # name of the ValidatingAdmissionPolicy to enforce architecture immutability
38+
# disabled: false # whether architecture immutability should be enforced (strongly recommended to leave this enabled)
3639

3740
crds:
3841
manage: true
@@ -53,6 +56,9 @@ managedcontrolplane:
5356

5457
apiserver:
5558
disabled: false
59+
# architecture:
60+
# version: v1
61+
# allowOverride: false
5662
worker:
5763
maxWorkers: 10
5864
intervalTime: 10s

cmd/mcp-operator/app/app.go

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/openmcp-project/mcp-operator/internal/releasechannel"
1010
"github.com/openmcp-project/mcp-operator/internal/utils/apiserver"
1111

12+
mcpocfg "github.com/openmcp-project/mcp-operator/internal/config"
1213
apiservercontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver"
1314
authenticationcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication"
1415
authorizationcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization"
@@ -17,12 +18,16 @@ import (
1718
landscapercontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper"
1819
mcpcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/managedcontrolplane"
1920

21+
admissionv1 "k8s.io/api/admissionregistration/v1"
2022
"sigs.k8s.io/controller-runtime/pkg/cluster"
2123

24+
v2install "github.com/openmcp-project/openmcp-operator/api/install"
25+
2226
laasinstall "github.com/gardener/landscaper-service/pkg/apis/core/install"
2327
cocorev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1"
2428
"github.com/openmcp-project/controller-utils/pkg/init/webhooks"
2529
"github.com/openmcp-project/controller-utils/pkg/logging"
30+
"github.com/openmcp-project/controller-utils/pkg/resources"
2631
"github.com/spf13/cobra"
2732
"k8s.io/apimachinery/pkg/runtime"
2833
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -45,6 +50,8 @@ import (
4550
openmcpinstall "github.com/openmcp-project/mcp-operator/api/install"
4651
)
4752

53+
const OperatorName = "ManagedControlPlaneOperator"
54+
4855
func NewMCPOperatorCommand(ctx context.Context) *cobra.Command {
4956
options := NewOptions()
5057

@@ -143,6 +150,125 @@ func (o *Options) runInit(ctx context.Context) error {
143150
}
144151
}
145152

153+
// manage architecture immutability
154+
labelSelector := client.MatchingLabels{
155+
openmcpv1alpha1.ManagedByLabel: OperatorName,
156+
openmcpv1alpha1.ManagedPurposeLabel: openmcpv1alpha1.ManagedPurposeArchitectureImmutability,
157+
}
158+
evapbs := &admissionv1.ValidatingAdmissionPolicyBindingList{}
159+
if err := crateClient.List(ctx, evapbs, labelSelector); err != nil {
160+
return fmt.Errorf("error listing ValidatingAdmissionPolicyBindings: %w", err)
161+
}
162+
for _, evapb := range evapbs.Items {
163+
if mcpocfg.Config.Architecture.Immutability.Disabled || evapb.Name != mcpocfg.Config.Architecture.Immutability.PolicyName {
164+
setupLog.Info("Deleting existing ValidatingAdmissionPolicyBinding with architecture immutability purpose", "name", evapb.Name)
165+
if err := crateClient.Delete(ctx, &evapb); client.IgnoreNotFound(err) != nil {
166+
return fmt.Errorf("error deleting ValidatingAdmissionPolicyBinding '%s': %w", evapb.Name, err)
167+
}
168+
}
169+
}
170+
evaps := &admissionv1.ValidatingAdmissionPolicyList{}
171+
if err := crateClient.List(ctx, evaps, labelSelector); err != nil {
172+
return fmt.Errorf("error listing ValidatingAdmissionPolicies: %w", err)
173+
}
174+
for _, evap := range evaps.Items {
175+
if mcpocfg.Config.Architecture.Immutability.Disabled || evap.Name != mcpocfg.Config.Architecture.Immutability.PolicyName {
176+
setupLog.Info("Deleting existing ValidatingAdmissionPolicy with architecture immutability purpose", "name", evap.Name)
177+
if err := crateClient.Delete(ctx, &evap); client.IgnoreNotFound(err) != nil {
178+
return fmt.Errorf("error deleting ValidatingAdmissionPolicy '%s': %w", evap.Name, err)
179+
}
180+
}
181+
}
182+
if !mcpocfg.Config.Architecture.Immutability.Disabled {
183+
setupLog.Info("Architecture immutability validation enabled, creating/updating ValidatingAdmissionPolicies ...")
184+
vapm := resources.NewValidatingAdmissionPolicyMutator(mcpocfg.Config.Architecture.Immutability.PolicyName, admissionv1.ValidatingAdmissionPolicySpec{
185+
FailurePolicy: ptr.To(admissionv1.Fail),
186+
MatchConstraints: &admissionv1.MatchResources{
187+
ResourceRules: []admissionv1.NamedRuleWithOperations{
188+
{
189+
RuleWithOperations: admissionv1.RuleWithOperations{
190+
Operations: []admissionv1.OperationType{
191+
admissionv1.Create,
192+
admissionv1.Update,
193+
},
194+
Rule: admissionv1.Rule{ // match all resources, actual restriction happens in the binding
195+
APIGroups: []string{"*"},
196+
APIVersions: []string{"*"},
197+
Resources: []string{"*"},
198+
},
199+
},
200+
},
201+
},
202+
},
203+
Variables: []admissionv1.Variable{
204+
{
205+
Name: "archLabel",
206+
Expression: fmt.Sprintf(`(has(object.metadata.labels) && "%s" in object.metadata.labels) ? object.metadata.labels["%s"] : ""`, openmcpv1alpha1.ArchitectureVersionLabel, openmcpv1alpha1.ArchitectureVersionLabel),
207+
},
208+
{
209+
Name: "oldArchLabel",
210+
Expression: fmt.Sprintf(`(oldObject != null && has(oldObject.metadata.labels) && "%s" in oldObject.metadata.labels) ? oldObject.metadata.labels["%s"] : ""`, openmcpv1alpha1.ArchitectureVersionLabel, openmcpv1alpha1.ArchitectureVersionLabel),
211+
},
212+
},
213+
Validations: []admissionv1.Validation{
214+
{
215+
Expression: fmt.Sprintf(`variables.archLabel == "%s" || variables.archLabel == "%s"`, openmcpv1alpha1.ArchitectureV1, openmcpv1alpha1.ArchitectureV2),
216+
Message: fmt.Sprintf(`The label "%s" must be set and its value must be either "%s" or "%s".`, openmcpv1alpha1.ArchitectureVersionLabel, openmcpv1alpha1.ArchitectureV1, openmcpv1alpha1.ArchitectureV2),
217+
},
218+
{
219+
Expression: fmt.Sprintf(`request.operation == "CREATE" || (variables.oldArchLabel == "" && variables.archLabel == "%s") || (variables.oldArchLabel == variables.archLabel)`, openmcpv1alpha1.ArchitectureV1),
220+
Message: fmt.Sprintf(`The label "%s" is immutable, it may not be changed or removed once set. Adding it to existing resources is only allowed with "%s" as value.`, openmcpv1alpha1.ArchitectureVersionLabel, openmcpv1alpha1.ArchitectureV1),
221+
},
222+
},
223+
})
224+
vapm.MetadataMutator().WithLabels(map[string]string{
225+
openmcpv1alpha1.ManagedByLabel: OperatorName,
226+
openmcpv1alpha1.ManagedPurposeLabel: openmcpv1alpha1.ManagedPurposeArchitectureImmutability,
227+
})
228+
if err := resources.CreateOrUpdateResource(ctx, crateClient, vapm); err != nil {
229+
return fmt.Errorf("error creating/updating ValidatingAdmissionPolicy for architecture immutability: %w", err)
230+
}
231+
232+
vapbm := resources.NewValidatingAdmissionPolicyBindingMutator(mcpocfg.Config.Architecture.Immutability.PolicyName, admissionv1.ValidatingAdmissionPolicyBindingSpec{
233+
PolicyName: mcpocfg.Config.Architecture.Immutability.PolicyName,
234+
ValidationActions: []admissionv1.ValidationAction{
235+
admissionv1.Deny,
236+
},
237+
MatchResources: &admissionv1.MatchResources{
238+
ResourceRules: []admissionv1.NamedRuleWithOperations{
239+
{
240+
RuleWithOperations: admissionv1.RuleWithOperations{
241+
Operations: []admissionv1.OperationType{
242+
admissionv1.Create,
243+
admissionv1.Update,
244+
},
245+
Rule: admissionv1.Rule{
246+
APIGroups: []string{openmcpv1alpha1.GroupVersion.Group},
247+
APIVersions: []string{openmcpv1alpha1.GroupVersion.Version},
248+
Resources: []string{
249+
"apiservers",
250+
"landscapers",
251+
"cloudorchestrators",
252+
"authentications",
253+
"authorizations",
254+
},
255+
},
256+
},
257+
},
258+
},
259+
},
260+
})
261+
vapbm.MetadataMutator().WithLabels(map[string]string{
262+
openmcpv1alpha1.ManagedByLabel: OperatorName,
263+
openmcpv1alpha1.ManagedPurposeLabel: openmcpv1alpha1.ManagedPurposeArchitectureImmutability,
264+
})
265+
if err := resources.CreateOrUpdateResource(ctx, crateClient, vapbm); err != nil {
266+
return fmt.Errorf("error creating/updating ValidatingAdmissionPolicyBinding for architecture immutability: %w", err)
267+
}
268+
269+
setupLog.Info("ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding for architecture immutability created/updated")
270+
}
271+
146272
return nil
147273
}
148274

@@ -219,7 +345,15 @@ func (o *Options) run(ctx context.Context) error {
219345

220346
if o.ActiveControllers.Has(ControllerIDAPIServer) {
221347
// APIServer controller
222-
apiServerProvider, err := apiservercontroller.NewAPIServerProvider(ctx, mgr.GetClient(), o.APIServerConfig)
348+
// build platform cluster client for v2 path
349+
v2scheme := v2install.InstallOperatorAPIs(runtime.NewScheme())
350+
platformClient, err := client.New(o.LaaSClusterConfig, client.Options{
351+
Scheme: v2scheme,
352+
})
353+
if err != nil {
354+
return fmt.Errorf("error creating platform cluster client: %w", err)
355+
}
356+
apiServerProvider, err := apiservercontroller.NewAPIServerProvider(ctx, mgr.GetClient(), platformClient, o.APIServerConfig)
223357
if err != nil {
224358
return fmt.Errorf("error creating %s: %w", apiservercontroller.ControllerName, err)
225359
}

cmd/mcp-operator/app/options.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/openmcp-project/mcp-operator/internal/components"
1212

13+
mcpocfg "github.com/openmcp-project/mcp-operator/internal/config"
1314
"github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config"
1415
configauthn "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config"
1516
configauthz "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config"
@@ -56,6 +57,7 @@ type rawOptions struct {
5657

5758
// raw options that need to be evaluated
5859
APIServerConfigPath string `json:"apiServerConfigPath"`
60+
MCPOConfigPath string `json:"configPath"`
5961
LaaSClusterPath string `json:"laasClusterConfigPath"`
6062
CrateClusterPath string `json:"crateClusterConfigPath"`
6163
CloudOrchestratorClusterPath string `json:"cloudOrchestratorClusterConfigPath"`
@@ -131,6 +133,9 @@ func (o *Options) String(includeHeader bool, includeRawOptions bool) (string, er
131133
// API server config
132134
opts["apiServerConfig"] = o.APIServerConfig
133135

136+
// architecture config
137+
opts["config"] = mcpocfg.Config
138+
134139
// clusters
135140
opts["crateClusterHost"] = nil
136141
if o.CrateClusterConfig != nil {
@@ -219,6 +224,7 @@ func (o *Options) AddFlags(fs *flag.FlagSet) {
219224
fs.StringVar(&o.AuthzConfigPath, "authz-config", "", "Path to the authorization config file.")
220225

221226
// common
227+
fs.StringVar(&o.MCPOConfigPath, "config", "", "Path to the MCP operator config file.")
222228
fs.BoolVar(&o.DryRun, "dry-run", false, "If true, the CLI args are evaluated as usual, but the program exits before the controllers are started.")
223229
fs.StringVar(&o.CrateClusterPath, "crate-cluster", "", "Path to the crate cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.")
224230
fs.StringVar(&o.ControllerList, "controllers", strings.Join([]string{ControllerIDManagedControlPlane, ControllerIDAPIServer, ControllerIDLandscaper, ControllerIDCloudOrchestrator}, ","), "Comma-separated list of controllers that should be active.")
@@ -337,6 +343,19 @@ func (o *Options) Complete() error {
337343
}
338344
}
339345

346+
// load config
347+
if o.MCPOConfigPath != "" {
348+
cfg, err := mcpocfg.LoadConfig(o.MCPOConfigPath)
349+
if err != nil {
350+
return err
351+
}
352+
err = cfg.Validate().ToAggregate()
353+
if err != nil {
354+
return fmt.Errorf("invalid config: %w", err)
355+
}
356+
mcpocfg.Config = *cfg
357+
}
358+
340359
// print options
341360
optsString, err := o.String(true, false)
342361
if err != nil {

docs/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
<!-- Do not edit this file, as it is auto-generated!-->
22
# Documentation Index
33

4+
## v2 Architecture
5+
6+
- [v2 Architecture Bridge](architecture-v2/bridge.md)
7+
8+
## Configuration
9+
10+
- [Configuration](config/config.md)
11+
412
## Controllers
513

614
- [APIServer Controller](controllers/apiserver.md)

0 commit comments

Comments
 (0)