Skip to content

Commit

Permalink
dynamicconfig: implement control plane (1/n)
Browse files Browse the repository at this point in the history
Signed-off-by: spacewander <[email protected]>
  • Loading branch information
spacewander committed Sep 3, 2024
1 parent c0c4192 commit a6ac74f
Show file tree
Hide file tree
Showing 24 changed files with 1,273 additions and 6 deletions.
18 changes: 15 additions & 3 deletions controller/internal/controller/component/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,23 @@ func NewK8sOutput(c client.Client) component.Output {
}

func (o *k8sOutput) FromFilterPolicy(ctx context.Context, generatedEnvoyFilters map[component.EnvoyFilterKey]*istiov1a3.EnvoyFilter) error {
return o.diffGeneratedEnvoyFilters(ctx, "FilterPolicy", generatedEnvoyFilters)
}

func (o *k8sOutput) FromConsumer(ctx context.Context, ef *istiov1a3.EnvoyFilter) error {
return o.diffGeneratedEnvoyFilter(ctx, "Consumer", ef)
}

func (o *k8sOutput) FromDynamicConfig(ctx context.Context, efs map[component.EnvoyFilterKey]*istiov1a3.EnvoyFilter) error {
return o.diffGeneratedEnvoyFilters(ctx, "DynamicConfig", efs)
}

func (o *k8sOutput) diffGeneratedEnvoyFilters(ctx context.Context, creator string, generatedEnvoyFilters map[component.EnvoyFilterKey]*istiov1a3.EnvoyFilter) error {
logger := o.logger

var envoyfilters istiov1a3.EnvoyFilterList
if err := o.List(ctx, &envoyfilters,
client.MatchingLabels{constant.LabelCreatedBy: "FilterPolicy"},
client.MatchingLabels{constant.LabelCreatedBy: creator},
); err != nil {
return fmt.Errorf("failed to list EnvoyFilter: %w", err)
}
Expand Down Expand Up @@ -101,12 +113,12 @@ func (o *k8sOutput) FromFilterPolicy(ctx context.Context, generatedEnvoyFilters
return nil
}

func (o *k8sOutput) FromConsumer(ctx context.Context, ef *istiov1a3.EnvoyFilter) error {
func (o *k8sOutput) diffGeneratedEnvoyFilter(ctx context.Context, creator string, ef *istiov1a3.EnvoyFilter) error {
logger := o.logger

nsName := types.NamespacedName{Name: ef.Name, Namespace: ef.Namespace}
var envoyfilters istiov1a3.EnvoyFilterList
if err := o.List(ctx, &envoyfilters, client.MatchingLabels{constant.LabelCreatedBy: "Consumer"}); err != nil {
if err := o.List(ctx, &envoyfilters, client.MatchingLabels{constant.LabelCreatedBy: creator}); err != nil {
return fmt.Errorf("failed to list EnvoyFilter: %w", err)
}

Expand Down
159 changes: 159 additions & 0 deletions controller/internal/controller/dynamicconfig_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
Copyright The HTNN 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 controller

import (
"context"
"fmt"
"time"

"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"mosn.io/htnn/controller/internal/istio"
"mosn.io/htnn/controller/internal/log"
"mosn.io/htnn/controller/internal/metrics"
"mosn.io/htnn/controller/pkg/component"
mosniov1 "mosn.io/htnn/types/apis/v1"
)

// DynamicConfigReconciler reconciles a DynamicConfig object
type DynamicConfigReconciler struct {
component.ResourceManager
Output component.Output
}

//+kubebuilder:rbac:groups=htnn.mosn.io,resources=dynamicconfigs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=htnn.mosn.io,resources=dynamicconfigs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=htnn.mosn.io,resources=dynamicconfigs/finalizers,verbs=update

func (r *DynamicConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reconcilationStart := time.Now()
defer func() {
reconcilationDuration := time.Since(reconcilationStart).Seconds()
metrics.DynamicConfigReconcileDurationDistribution.Record(reconcilationDuration)
}()

log.Info("Reconcile DynamicConfig")

var dynamicConfigs mosniov1.DynamicConfigList
state, err := r.dynamicconfigsToState(ctx, &dynamicConfigs)
if err != nil {
return ctrl.Result{}, err

Check warning on line 61 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L61

Added line #L61 was not covered by tests
}

err = r.generateCustomResource(ctx, state)
if err != nil {
return ctrl.Result{}, err

Check warning on line 66 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L66

Added line #L66 was not covered by tests
}

err = r.updateDynamicConfigs(ctx, &dynamicConfigs)
return ctrl.Result{}, err
}

type dynamicConfigReconcileState struct {
namespaceToDynamicConfigs map[string]map[string]*mosniov1.DynamicConfig
}

func (r *DynamicConfigReconciler) dynamicconfigsToState(ctx context.Context,
dynamicConfigs *mosniov1.DynamicConfigList) (*dynamicConfigReconcileState, error) {

if err := r.List(ctx, dynamicConfigs); err != nil {
return nil, fmt.Errorf("failed to list DynamicConfig: %w", err)

Check warning on line 81 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L81

Added line #L81 was not covered by tests
}

namespaceToDynamicConfigs := make(map[string]map[string]*mosniov1.DynamicConfig)
for i := range dynamicConfigs.Items {
dynamicConfig := &dynamicConfigs.Items[i]

// defensive code in case the webhook doesn't work
if dynamicConfig.IsSpecChanged() {
err := mosniov1.ValidateDynamicConfig(dynamicConfig)
if err != nil {
log.Errorf("invalid DynamicConfig, err: %v, name: %s, namespace: %s", dynamicConfig.Name, dynamicConfig.Namespace)
dynamicConfig.SetAccepted(mosniov1.ReasonInvalid, err.Error())
continue

Check warning on line 94 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L92-L94

Added lines #L92 - L94 were not covered by tests
}
}
if !dynamicConfig.IsValid() {
continue

Check warning on line 98 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L98

Added line #L98 was not covered by tests
}

namespace := dynamicConfig.Namespace
if namespaceToDynamicConfigs[namespace] == nil {
namespaceToDynamicConfigs[namespace] = make(map[string]*mosniov1.DynamicConfig)
}

name := dynamicConfig.Spec.Type
if namespaceToDynamicConfigs[namespace][name] != nil {
log.Errorf("duplicate DynamicConfig %s/%s, k8s name %s takes effect, k8s name %s ignored", namespace, name,
namespaceToDynamicConfigs[namespace][name].Name, dynamicConfig.Name)
dynamicConfig.SetAccepted(mosniov1.ReasonInvalid,
fmt.Sprintf("duplicate with another DynamicConfig %s/%s, k8s name %s", namespace, name, dynamicConfig.Name))

Check warning on line 111 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L108-L111

Added lines #L108 - L111 were not covered by tests
} else {
namespaceToDynamicConfigs[namespace][name] = dynamicConfig
dynamicConfig.SetAccepted(mosniov1.ReasonAccepted)
}
}

state := &dynamicConfigReconcileState{
namespaceToDynamicConfigs: namespaceToDynamicConfigs,
}
return state, nil
}

func (r *DynamicConfigReconciler) generateCustomResource(ctx context.Context, state *dynamicConfigReconcileState) error {
efs := istio.GenerateDynamicConfigs(state.namespaceToDynamicConfigs)
return r.Output.FromDynamicConfig(ctx, efs)
}

func (r *DynamicConfigReconciler) updateDynamicConfigs(ctx context.Context, dynamicConfigs *mosniov1.DynamicConfigList) error {
for i := range dynamicConfigs.Items {
dynamicConfig := &dynamicConfigs.Items[i]
if !dynamicConfig.Status.IsChanged() {
continue

Check warning on line 133 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L133

Added line #L133 was not covered by tests
}
dynamicConfig.Status.Reset()
if err := r.UpdateStatus(ctx, dynamicConfig, &dynamicConfig.Status); err != nil {
return fmt.Errorf("failed to update DynamicConfig status: %w, namespacedName: %v",
err,
types.NamespacedName{Name: dynamicConfig.Name, Namespace: dynamicConfig.Namespace})

Check warning on line 139 in controller/internal/controller/dynamicconfig_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/internal/controller/dynamicconfig_controller.go#L137-L139

Added lines #L137 - L139 were not covered by tests
}
}
return nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *DynamicConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
controller := ctrl.NewControllerManagedBy(mgr).
Named("dynamicConfig").
Watches(
&mosniov1.DynamicConfig{},
handler.EnqueueRequestsFromMapFunc(func(_ context.Context, _ client.Object) []reconcile.Request {
return triggerReconciliation()
}),
builder.WithPredicates(
predicate.GenerationChangedPredicate{},
),
)
return controller.Complete(r)
}
123 changes: 121 additions & 2 deletions controller/internal/istio/envoyfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package istio

import (
"encoding/json"
"fmt"
"sort"

Expand All @@ -29,6 +30,7 @@ import (
"mosn.io/htnn/controller/internal/model"
"mosn.io/htnn/controller/pkg/component"
"mosn.io/htnn/controller/pkg/constant"
mosniov1 "mosn.io/htnn/types/apis/v1"
)

func MustNewStruct(fields map[string]interface{}) *structpb.Struct {
Expand All @@ -41,8 +43,9 @@ func MustNewStruct(fields map[string]interface{}) *structpb.Struct {
}

const (
DefaultHTTPFilter = "htnn-http-filter"
ECDSConsumerName = "htnn-consumer"
DefaultHTTPFilter = "htnn-http-filter"
ECDSConsumerName = "htnn-consumer"
DynamicConfigEnvoyFilterName = "htnn-dynamic-config"
)

type configWrapper struct {
Expand Down Expand Up @@ -465,3 +468,119 @@ func GenerateConsumers(consumers map[string]interface{}) *istiov1a3.EnvoyFilter
},
}
}

func GenerateDynamicConfigs(namespacedDynamicConfigs map[string]map[string]*mosniov1.DynamicConfig) map[component.EnvoyFilterKey]*istiov1a3.EnvoyFilter {
efs := map[component.EnvoyFilterKey]*istiov1a3.EnvoyFilter{}
for ns, dynamicConfigs := range namespacedDynamicConfigs {
ef := &istiov1a3.EnvoyFilter{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: DynamicConfigEnvoyFilterName,
Labels: map[string]string{
constant.LabelCreatedBy: "DynamicConfig",
},
},
Spec: istioapi.EnvoyFilter{
ConfigPatches: []*istioapi.EnvoyFilter_EnvoyConfigObjectPatch{},
},
}
// Each DynamicConfig is smaller than 1.5MB, which is the limit applied by the k8s API server (the value may be different by configured).
// In prod, we generate the EnvoyFilter inside the istio, so the size of EnvoyFilter doesn't matter.

configs := make([]*mosniov1.DynamicConfig, 0, len(dynamicConfigs))
for _, dynamicConfig := range dynamicConfigs {
configs = append(configs, dynamicConfig)
}
sort.Slice(configs, func(i, j int) bool {
return configs[i].Spec.Type < configs[j].Spec.Type
})

httpFilters := []interface{}{}
for _, cfg := range configs {
var dispatchedConfig interface{}
_ = json.Unmarshal(cfg.Spec.Config.Raw, &dispatchedConfig)

ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{
ApplyTo: istioapi.EnvoyFilter_EXTENSION_CONFIG,
Patch: &istioapi.EnvoyFilter_Patch{
Operation: istioapi.EnvoyFilter_Patch_ADD,
Value: MustNewStruct(map[string]interface{}{
"name": fmt.Sprintf("htnn-DynamicConfig-%s", cfg.Spec.Type),
"typed_config": map[string]interface{}{
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"library_id": "dc",
"library_path": ctrlcfg.GoSoPath(),
"plugin_name": "dc",
"plugin_config": map[string]interface{}{
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": map[string]interface{}{
"name": cfg.Spec.Type,
"config": dispatchedConfig,
},
},
},
}),
},
})
httpFilters = append(httpFilters, map[string]interface{}{
"name": fmt.Sprintf("htnn-DynamicConfig-%s", cfg.Spec.Type),
"config_discovery": map[string]interface{}{
"config_source": map[string]interface{}{
"ads": map[string]interface{}{},
},
"type_urls": []interface{}{
"type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
},
},
})
}

httpFilters = append(httpFilters, map[string]interface{}{
"name": "envoy.filters.http.router",
"typed_config": map[string]interface{}{
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
},
})
listener := &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{
ApplyTo: istioapi.EnvoyFilter_LISTENER,
Patch: &istioapi.EnvoyFilter_Patch{
Operation: istioapi.EnvoyFilter_Patch_ADD,
Value: MustNewStruct(map[string]interface{}{
"name": "htnn_dynamic_config",
"internal_listener": map[string]interface{}{},
"filter_chains": []interface{}{
map[string]interface{}{
"filters": []interface{}{
map[string]interface{}{
"name": "envoy.filters.network.http_connection_manager",
"typed_config": map[string]interface{}{
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"stat_prefix": "htnn_dynamic_config",
"http_filters": httpFilters,
"route_config": map[string]interface{}{
"name": "htnn_dynamic_config",
"virtual_hosts": []interface{}{
map[string]interface{}{
"name": "htnn_dynamic_config",
"domains": []interface{}{"*"},
},
},
},
},
},
},
},
},
},
),
},
}
ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, listener)
efs[component.EnvoyFilterKey{
Namespace: ns,
Name: ef.Name,
}] = ef
}

return efs
}
Loading

0 comments on commit a6ac74f

Please sign in to comment.