Skip to content

Commit

Permalink
dynamicconfig: implement control plane (1/n) (#707)
Browse files Browse the repository at this point in the history
Signed-off-by: spacewander <[email protected]>
  • Loading branch information
spacewander authored Sep 6, 2024
1 parent 07ff9e6 commit 3dbd5a2
Show file tree
Hide file tree
Showing 25 changed files with 1,274 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ jobs:
uses: "pascalgn/[email protected]"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
IGNORED: "**/*.pb.*\n*.sum\ntypes/pkg/client\n**/zz_generated.deepcopy.go"
IGNORED: "**/*.pb.*\n*.sum\ntypes/pkg/client/**\n**/zz_generated.deepcopy.go"
# use same size labels as kubernetes: https://github.com/kubernetes/kubernetes/labels?q=size
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
}

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

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)
}

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
}
}
if !dynamicConfig.IsValid() {
continue
}

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))
} 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
}
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})
}
}
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 3dbd5a2

Please sign in to comment.