Files
kubevela/pkg/webhook/core.oam.dev/v1beta1/application/validation.go
2025-09-03 05:53:56 +08:00

419 lines
15 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package application
import (
"context"
"fmt"
"reflect"
"time"
"github.com/kubevela/pkg/controller/sharding"
"github.com/kubevela/pkg/util/singleton"
authv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
// ValidateWorkflow validates the Application workflow
func (h *ValidatingHandler) ValidateWorkflow(_ context.Context, app *v1beta1.Application) field.ErrorList {
var errs field.ErrorList
if app.Spec.Workflow != nil {
stepName := make(map[string]interface{})
for _, step := range app.Spec.Workflow.Steps {
if _, ok := stepName[step.Name]; ok {
errs = append(errs, field.Invalid(field.NewPath("spec", "workflow", "steps"), step.Name, "duplicated step name"))
}
stepName[step.Name] = nil
if step.Timeout != "" {
errs = append(errs, h.ValidateTimeout(step.Name, step.Timeout)...)
}
for _, sub := range step.SubSteps {
if _, ok := stepName[sub.Name]; ok {
errs = append(errs, field.Invalid(field.NewPath("spec", "workflow", "steps", "subSteps"), sub.Name, "duplicated step name"))
}
stepName[sub.Name] = nil
if step.Timeout != "" {
errs = append(errs, h.ValidateTimeout(step.Name, step.Timeout)...)
}
}
}
}
return errs
}
// ValidateTimeout validates the timeout of steps
func (h *ValidatingHandler) ValidateTimeout(name, timeout string) field.ErrorList {
var errs field.ErrorList
_, err := time.ParseDuration(timeout)
if err != nil {
errs = append(errs, field.Invalid(field.NewPath("spec", "workflow", "steps", "timeout"), name, "invalid timeout, please use the format of timeout like 1s, 1m, 1h or 1d"))
}
return errs
}
// appRevBypassCacheClient
type appRevBypassCacheClient struct {
client.Client
}
// Get retrieve appRev directly from request if sharding enabled
func (in *appRevBypassCacheClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
if _, ok := obj.(*v1beta1.ApplicationRevision); ok && sharding.EnableSharding {
return singleton.KubeClient.Get().Get(ctx, key, obj)
}
return in.Client.Get(ctx, key, obj)
}
// ValidateComponents validates the Application components
func (h *ValidatingHandler) ValidateComponents(ctx context.Context, app *v1beta1.Application) field.ErrorList {
if sharding.EnableSharding && !utilfeature.DefaultMutableFeatureGate.Enabled(features.ValidateComponentWhenSharding) {
return nil
}
var componentErrs field.ErrorList
// try to generate an app file
cli := &appRevBypassCacheClient{Client: h.Client}
appParser := appfile.NewApplicationParser(cli)
af, err := appParser.GenerateAppFile(ctx, app)
if err != nil {
componentErrs = append(componentErrs, field.Invalid(field.NewPath("spec"), app, err.Error()))
// cannot generate appfile, no need to validate further
return componentErrs
}
if i, err := appParser.ValidateComponentNames(app); err != nil {
componentErrs = append(componentErrs, field.Invalid(field.NewPath(fmt.Sprintf("components[%d].name", i)), app, err.Error()))
}
if err := appParser.ValidateCUESchematicAppfile(af); err != nil {
componentErrs = append(componentErrs, field.Invalid(field.NewPath("schematic"), app, err.Error()))
}
return componentErrs
}
// checkDefinitionPermission checks if user has permission to access a definition in either system namespace or app namespace
func (h *ValidatingHandler) checkDefinitionPermission(ctx context.Context, req admission.Request, resource, definitionType, appNamespace string) (bool, error) {
// Check permission in system namespace (vela-system) first since most definitions are there
// This optimizes for the common case and reduces API calls
systemNsSar := &authv1.SubjectAccessReview{
Spec: authv1.SubjectAccessReviewSpec{
User: req.UserInfo.Username,
Groups: req.UserInfo.Groups,
ResourceAttributes: &authv1.ResourceAttributes{
Verb: "get",
Group: "core.oam.dev",
Version: "v1beta1",
Resource: resource,
Namespace: oam.SystemDefinitionNamespace,
Name: definitionType,
},
},
}
if err := h.Client.Create(ctx, systemNsSar); err != nil {
return false, fmt.Errorf("failed to check %s permission in system namespace: %w", resource, err)
}
if systemNsSar.Status.Allowed {
// User has permission in system namespace - no need to check app namespace
return true, nil
}
// If not in system namespace and app namespace is different, check app namespace
if appNamespace != oam.SystemDefinitionNamespace {
appNsSar := &authv1.SubjectAccessReview{
Spec: authv1.SubjectAccessReviewSpec{
User: req.UserInfo.Username,
Groups: req.UserInfo.Groups,
ResourceAttributes: &authv1.ResourceAttributes{
Verb: "get",
Group: "core.oam.dev",
Version: "v1beta1",
Resource: resource,
Namespace: appNamespace,
Name: definitionType,
},
},
}
if err := h.Client.Create(ctx, appNsSar); err != nil {
return false, fmt.Errorf("failed to check %s permission in namespace %s: %w", resource, appNamespace, err)
}
if appNsSar.Status.Allowed {
// User has permission in app namespace
return true, nil
}
}
// User doesn't have permission in either namespace
return false, nil
}
// workflowStepLocation represents the location of a workflow step
type workflowStepLocation struct {
StepIndex int
SubStepIndex int // -1 if not a sub-step
IsSubStep bool
}
// definitionUsage tracks where each definition type is used in the application
type definitionUsage struct {
componentTypes map[string][]int
traitTypes map[string][][2]int
policyTypes map[string][]int
workflowStepTypes map[string][]workflowStepLocation
}
// collectDefinitionUsage collects all unique definition types and their locations in the application
func collectDefinitionUsage(app *v1beta1.Application) *definitionUsage {
usage := &definitionUsage{
componentTypes: make(map[string][]int),
traitTypes: make(map[string][][2]int),
policyTypes: make(map[string][]int),
workflowStepTypes: make(map[string][]workflowStepLocation),
}
// Collect component and trait types
for i, comp := range app.Spec.Components {
usage.componentTypes[comp.Type] = append(usage.componentTypes[comp.Type], i)
for j, trait := range comp.Traits {
usage.traitTypes[trait.Type] = append(usage.traitTypes[trait.Type], [2]int{i, j})
}
}
// Collect policy types
for i, policy := range app.Spec.Policies {
usage.policyTypes[policy.Type] = append(usage.policyTypes[policy.Type], i)
}
// Collect workflow step types (including sub-steps)
if app.Spec.Workflow != nil {
for i, step := range app.Spec.Workflow.Steps {
location := workflowStepLocation{
StepIndex: i,
SubStepIndex: -1,
IsSubStep: false,
}
usage.workflowStepTypes[step.Type] = append(usage.workflowStepTypes[step.Type], location)
// Also check sub-steps
for j, subStep := range step.SubSteps {
subLocation := workflowStepLocation{
StepIndex: i,
SubStepIndex: j,
IsSubStep: true,
}
usage.workflowStepTypes[subStep.Type] = append(usage.workflowStepTypes[subStep.Type], subLocation)
}
}
}
return usage
}
// processDefinitionPermissionCheck handles the common logic for processing permission check results
func (h *ValidatingHandler) processDefinitionPermissionCheck(
allowed bool,
err error,
req admission.Request,
definitionKind string,
definitionType string,
appNamespace string,
fieldPaths []*field.Path,
) field.ErrorList {
var errs field.ErrorList
if err != nil {
klog.Errorf("Failed to check %s permission for user %s: %v", definitionKind, req.UserInfo.Username, err)
for _, fieldPath := range fieldPaths {
errs = append(errs, field.Forbidden(fieldPath,
fmt.Sprintf("unable to verify permissions for %s %q: %v", definitionKind, definitionType, err)))
}
return errs
}
if !allowed {
klog.Infof("User %q does not have permission to access %s %q in namespace %q or %q",
req.UserInfo.Username, definitionKind, definitionType, appNamespace, oam.SystemDefinitionNamespace)
for _, fieldPath := range fieldPaths {
errs = append(errs, field.Forbidden(fieldPath,
fmt.Sprintf("user %q cannot get %s %q in namespace %q or %q",
req.UserInfo.Username, definitionKind, definitionType, appNamespace, oam.SystemDefinitionNamespace)))
}
}
return errs
}
// fieldPathBuilder is a function that builds field paths for a given definition usage
type fieldPathBuilder func(interface{}) []*field.Path
// validateDefinitions is a generic function to validate any definition type
func (h *ValidatingHandler) validateDefinitions(
ctx context.Context,
req admission.Request,
appNamespace string,
definitionType reflect.Type,
usageMap interface{},
buildFieldPaths fieldPathBuilder,
) field.ErrorList {
var errs field.ErrorList
// Get the definition info for the given type
defInfo, ok := v1beta1.DefinitionTypeMap[definitionType]
if !ok {
klog.Errorf("Unknown definition type: %v", definitionType)
return errs
}
switch typedMap := usageMap.(type) {
case map[string][]int:
for defType, indices := range typedMap {
allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
fieldPaths := buildFieldPaths(indices)
errs = append(errs, h.processDefinitionPermissionCheck(
allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
}
case map[string][][2]int:
for defType, locations := range typedMap {
allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
fieldPaths := buildFieldPaths(locations)
errs = append(errs, h.processDefinitionPermissionCheck(
allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
}
case map[string][]workflowStepLocation:
for defType, locations := range typedMap {
allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
fieldPaths := buildFieldPaths(locations)
errs = append(errs, h.processDefinitionPermissionCheck(
allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
}
}
return errs
}
// ValidateDefinitionPermissions validates that the user has permissions to access all definition types
func (h *ValidatingHandler) ValidateDefinitionPermissions(ctx context.Context, app *v1beta1.Application, req admission.Request) field.ErrorList {
// Requires both authentication and definition validation enabled
if !auth.AuthenticationWithUser || !utilfeature.DefaultMutableFeatureGate.Enabled(features.ValidateDefinitionPermissions) {
return nil
}
var errs field.ErrorList
usage := collectDefinitionUsage(app)
// Validate ComponentDefinitions
errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
reflect.TypeOf(v1beta1.ComponentDefinition{}), usage.componentTypes,
func(indices interface{}) []*field.Path {
var paths []*field.Path
for _, idx := range indices.([]int) {
paths = append(paths, field.NewPath("spec", "components").Index(idx).Child("type"))
}
return paths
})...)
// Validate TraitDefinitions
errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
reflect.TypeOf(v1beta1.TraitDefinition{}), usage.traitTypes,
func(locations interface{}) []*field.Path {
var paths []*field.Path
for _, loc := range locations.([][2]int) {
paths = append(paths,
field.NewPath("spec", "components").Index(loc[0]).Child("traits").Index(loc[1]).Child("type"))
}
return paths
})...)
// Validate PolicyDefinitions
errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
reflect.TypeOf(v1beta1.PolicyDefinition{}), usage.policyTypes,
func(indices interface{}) []*field.Path {
var paths []*field.Path
for _, idx := range indices.([]int) {
paths = append(paths, field.NewPath("spec", "policies").Index(idx).Child("type"))
}
return paths
})...)
// Validate WorkflowStepDefinitions
errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
reflect.TypeOf(v1beta1.WorkflowStepDefinition{}), usage.workflowStepTypes,
func(locations interface{}) []*field.Path {
var paths []*field.Path
for _, loc := range locations.([]workflowStepLocation) {
paths = append(paths, getWorkflowStepFieldPath(loc))
}
return paths
})...)
return errs
}
// getWorkflowStepFieldPath constructs the field path for a workflow step or sub-step
func getWorkflowStepFieldPath(loc workflowStepLocation) *field.Path {
if !loc.IsSubStep {
// Regular step
return field.NewPath("spec", "workflow", "steps").Index(loc.StepIndex).Child("type")
}
// Sub-step
return field.NewPath("spec", "workflow", "steps").Index(loc.StepIndex).Child("subSteps").Index(loc.SubStepIndex).Child("type")
}
// ValidateAnnotations validates whether the application has both autoupdate and publish version annotations
func (h *ValidatingHandler) ValidateAnnotations(_ context.Context, app *v1beta1.Application) field.ErrorList {
var annotationsErrs field.ErrorList
hasPublishVersion := app.Annotations[oam.AnnotationPublishVersion]
hasAutoUpdate := app.Annotations[oam.AnnotationAutoUpdate]
if hasAutoUpdate == "true" && hasPublishVersion != "" {
annotationsErrs = append(annotationsErrs, field.Invalid(field.NewPath("metadata", "annotations"), app,
"Application has both autoUpdate and publishVersion annotations. Only one can be present"))
}
return annotationsErrs
}
// ValidateCreate validates the Application on creation
func (h *ValidatingHandler) ValidateCreate(ctx context.Context, app *v1beta1.Application, req admission.Request) field.ErrorList {
var errs field.ErrorList
errs = append(errs, h.ValidateAnnotations(ctx, app)...)
errs = append(errs, h.ValidateDefinitionPermissions(ctx, app, req)...)
errs = append(errs, h.ValidateWorkflow(ctx, app)...)
errs = append(errs, h.ValidateComponents(ctx, app)...)
return errs
}
// ValidateUpdate validates the Application on update
func (h *ValidatingHandler) ValidateUpdate(ctx context.Context, newApp, _ *v1beta1.Application, req admission.Request) field.ErrorList {
// check if the newApp is valid
errs := h.ValidateCreate(ctx, newApp, req)
// TODO: add more validating
return errs
}