Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: predefined configurableInterpreter #2768

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 105 additions & 0 deletions pkg/resourceinterpreter/default/thirdparty/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package thirdparty

import (
"bytes"
"io"
"io/fs"
"path/filepath"
"sort"
"strings"
"sync/atomic"

"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/klog/v2"

configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
"github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/declarative/configmanager"
"github.com/karmada-io/karmada/pkg/resourceinterpreter/default/thirdparty/resourcecustomizations"
)

const configurableInterpreterFile = "customizations.yaml"

// configManager collects the thirdparty resource interpreter customization.
type configManager struct {
configuration atomic.Value
}

func (t *configManager) HasSynced() bool {
return true
}

func (t *configManager) LoadConfig(customizations []*configv1alpha1.ResourceInterpreterCustomization) {
sort.Slice(customizations, func(i, j int) bool {
return customizations[i].Name < customizations[j].Name
})

accessors := make(map[schema.GroupVersionKind]configmanager.CustomAccessor)
for _, config := range customizations {
key := schema.FromAPIVersionAndKind(config.Spec.Target.APIVersion, config.Spec.Target.Kind)
var ac configmanager.CustomAccessor
var ok bool
if ac, ok = accessors[key]; !ok {
ac = configmanager.NewResourceCustomAccessor()
}
ac.Merge(config.Spec.Customizations)
accessors[key] = ac
}

t.configuration.Store(accessors)
}

// CustomAccessors returns all cached configurations.
func (t *configManager) CustomAccessors() map[schema.GroupVersionKind]configmanager.CustomAccessor {
return t.configuration.Load().(map[schema.GroupVersionKind]configmanager.CustomAccessor)
}

// NewThirdPartyConfigManager load third party resource in the cache.
func NewThirdPartyConfigManager() configmanager.ConfigManager {
manager := &configManager{}
manager.configuration.Store(make(map[schema.GroupVersionKind]configmanager.CustomAccessor))
manager.loadThirdPartyConfig()
return manager
}

func (t *configManager) loadThirdPartyConfig() {
var configs []*configv1alpha1.ResourceInterpreterCustomization
if err := fs.WalkDir(resourcecustomizations.Embedded, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
// cannot happen
return err
}
if d.IsDir() {
return nil
}
if strings.Contains(d.Name(), "testdata") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this going to happen?
Should we add a new document to describe how to use this feature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to filter out test files.

return nil
}

if filepath.Base(d.Name()) != configurableInterpreterFile {
return nil
}
data, err := fs.ReadFile(resourcecustomizations.Embedded, path)
if err != nil {
// cannot happen
return err
}
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 4096)
for {
config := &configv1alpha1.ResourceInterpreterCustomization{}
err = decoder.Decode(config)
if err != nil {
break
}
configs = append(configs, config)
}
if err != io.EOF {
return err
}
return nil
}); err != nil {
klog.Warning(err, "failed to load third party resource")
return
}
t.LoadConfig(configs)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package resourcecustomizations

import (
"embed"
)

// Embedded contains embedded resource customization
//
//go:embed *
var Embedded embed.FS
220 changes: 220 additions & 0 deletions pkg/resourceinterpreter/default/thirdparty/thirdparty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package thirdparty

import (
"sort"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"

configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/declarative/configmanager"
"github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/declarative/luavm"
)

// ConfigurableInterpreter interprets resources with third party resource interpreter.
type ConfigurableInterpreter struct {
configManager configmanager.ConfigManager
luaVM *luavm.VM
}

// HookEnabled tells if any hook exist for specific resource gvk and operation type.
func (p *ConfigurableInterpreter) HookEnabled(kind schema.GroupVersionKind, operationType configv1alpha1.InterpreterOperation) bool {
customAccessor, exist := p.getCustomAccessor(kind)
if !exist {
return exist
}
if operationType == configv1alpha1.InterpreterOperationInterpretDependency {
scripts := customAccessor.GetDependencyInterpretationLuaScripts()
return scripts != nil
}
var script string
switch operationType {
case configv1alpha1.InterpreterOperationAggregateStatus:
script = customAccessor.GetStatusAggregationLuaScript()
case configv1alpha1.InterpreterOperationInterpretHealth:
script = customAccessor.GetHealthInterpretationLuaScript()
case configv1alpha1.InterpreterOperationInterpretReplica:
script = customAccessor.GetReplicaResourceLuaScript()
case configv1alpha1.InterpreterOperationInterpretStatus:
script = customAccessor.GetStatusReflectionLuaScript()
case configv1alpha1.InterpreterOperationRetain:
script = customAccessor.GetRetentionLuaScript()
case configv1alpha1.InterpreterOperationReviseReplica:
script = customAccessor.GetReplicaRevisionLuaScript()
}
return len(script) > 0
}

// GetReplicas returns the desired replicas of the object as well as the requirements of each replica.
func (p *ConfigurableInterpreter) GetReplicas(object *unstructured.Unstructured) (replicas int32, requires *workv1alpha2.ReplicaRequirements, enabled bool, err error) {
klog.V(4).Infof("Get replicas for object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())

customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetReplicaResourceLuaScript()
if len(script) == 0 {
enabled = false
return
}

replicas, requires, err = p.luaVM.GetReplicas(object, script)
return
}

// ReviseReplica revises the replica of the given object.
func (p *ConfigurableInterpreter) ReviseReplica(object *unstructured.Unstructured, replica int64) (revised *unstructured.Unstructured, enabled bool, err error) {
klog.V(4).Infof("Revise replicas for object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())

customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetReplicaRevisionLuaScript()
if len(script) == 0 {
enabled = false
return
}

revised, err = p.luaVM.ReviseReplica(object, replica, script)
return
}

// Retain returns the objects that based on the "desired" object but with values retained from the "observed" object.
func (p *ConfigurableInterpreter) Retain(desired *unstructured.Unstructured, observed *unstructured.Unstructured) (retained *unstructured.Unstructured, enabled bool, err error) {
klog.V(4).Infof("Retain object: %v %s/%s with thirdparty configurable interpreter.", desired.GroupVersionKind(), desired.GetNamespace(), desired.GetName())

customAccessor, enabled := p.getCustomAccessor(desired.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetRetentionLuaScript()
if len(script) == 0 {
enabled = false
return
}

retained, err = p.luaVM.Retain(desired, observed, script)
return
}

// AggregateStatus returns the objects that based on the 'object' but with status aggregated.
func (p *ConfigurableInterpreter) AggregateStatus(object *unstructured.Unstructured, aggregatedStatusItems []workv1alpha2.AggregatedStatusItem) (status *unstructured.Unstructured, enabled bool, err error) {
klog.V(4).Infof("Aggregate status of object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())
customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetStatusAggregationLuaScript()
if len(script) == 0 {
enabled = false
return
}

status, err = p.luaVM.AggregateStatus(object, aggregatedStatusItems, script)
return
}

// GetDependencies returns the dependent resources of the given object.
func (p *ConfigurableInterpreter) GetDependencies(object *unstructured.Unstructured) (dependencies []configv1alpha1.DependentObjectReference, enabled bool, err error) {
klog.V(4).Infof("Get dependencies of object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())

customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

scripts := customAccessor.GetDependencyInterpretationLuaScripts()
if scripts == nil {
enabled = false
return
}

refs := sets.New[configv1alpha1.DependentObjectReference]()
for _, luaScript := range scripts {
var references []configv1alpha1.DependentObjectReference
references, err = p.luaVM.GetDependencies(object, luaScript)
if err != nil {
klog.Errorf("Failed to get DependentObjectReferences from object: %v %s/%s, error: %v",
object.GroupVersionKind(), object.GetNamespace(), object.GetName(), err)
return
}
refs.Insert(references...)
}
dependencies = refs.UnsortedList()

// keep returned items in the same order between each call.
sort.Slice(dependencies, func(i, j int) bool {
if dependencies[i].APIVersion != dependencies[j].APIVersion {
return dependencies[i].APIVersion < dependencies[j].APIVersion
}
if dependencies[i].Kind != dependencies[j].Kind {
return dependencies[i].Kind < dependencies[j].Kind
}
if dependencies[i].Namespace != dependencies[j].Namespace {
return dependencies[i].Namespace < dependencies[j].Namespace
}
return dependencies[i].Name < dependencies[j].Name
})
return
}

// ReflectStatus returns the status of the object.
func (p *ConfigurableInterpreter) ReflectStatus(object *unstructured.Unstructured) (status *runtime.RawExtension, enabled bool, err error) {
klog.V(4).Infof("Reflect status of object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())

customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetStatusReflectionLuaScript()
if len(script) == 0 {
enabled = false
return
}

status, err = p.luaVM.ReflectStatus(object, script)
return
}

// InterpretHealth returns the health state of the object.
func (p *ConfigurableInterpreter) InterpretHealth(object *unstructured.Unstructured) (health bool, enabled bool, err error) {
klog.V(4).Infof("Get health status of object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())

customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
if !enabled {
return
}

script := customAccessor.GetHealthInterpretationLuaScript()
if len(script) == 0 {
enabled = false
return
}

health, err = p.luaVM.InterpretHealth(object, script)
return
}

func (p *ConfigurableInterpreter) getCustomAccessor(kind schema.GroupVersionKind) (configmanager.CustomAccessor, bool) {
customAccessor, exist := p.configManager.CustomAccessors()[kind]
return customAccessor, exist
}

// NewConfigurableInterpreter return a new ConfigurableInterpreter.
func NewConfigurableInterpreter() *ConfigurableInterpreter {
return &ConfigurableInterpreter{
configManager: NewThirdPartyConfigManager(),
luaVM: luavm.New(false, 10),
}
}