mirror of
https://github.com/alibaba/higress.git
synced 2026-03-20 11:08:03 +08:00
add variable from secret when applying istio cr (#1877)
This commit is contained in:
@@ -151,6 +151,33 @@ type IngressConfig struct {
|
||||
clusterId cluster.ID
|
||||
|
||||
httpsConfigMgr *cert.ConfigMgr
|
||||
|
||||
// templateProcessor processes template variables in config
|
||||
templateProcessor *TemplateProcessor
|
||||
|
||||
// secretConfigMgr manages secret dependencies
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
}
|
||||
|
||||
// getSecretValue implements the getValue function for secret references
|
||||
func (m *IngressConfig) getSecretValue(valueType, namespace, name, key string) (string, error) {
|
||||
if valueType != "secret" {
|
||||
return "", fmt.Errorf("unsupported value type: %s", valueType)
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, controller := range m.remoteIngressControllers {
|
||||
secret, err := controller.SecretLister().Secrets(namespace).Get(name)
|
||||
if err == nil {
|
||||
if value, exists := secret.Data[key]; exists {
|
||||
return string(value), nil
|
||||
}
|
||||
return "", fmt.Errorf("key %s not found in secret %s/%s", key, namespace, name)
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("secret %s/%s not found", namespace, name)
|
||||
}
|
||||
|
||||
func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *IngressConfig {
|
||||
@@ -171,6 +198,13 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
|
||||
wasmPlugins: make(map[string]*extensions.WasmPlugin),
|
||||
http2rpcs: make(map[string]*higressv1.Http2Rpc),
|
||||
}
|
||||
|
||||
// Initialize secret config manager
|
||||
config.secretConfigMgr = NewSecretConfigMgr(xdsUpdater)
|
||||
|
||||
// Initialize template processor with value getter function
|
||||
config.templateProcessor = NewTemplateProcessor(config.getSecretValue, namespace, config.secretConfigMgr)
|
||||
|
||||
mcpbridgeController := mcpbridge.NewController(localKubeClient, options)
|
||||
mcpbridgeController.AddEventHandler(config.AddOrUpdateMcpBridge, config.DeleteMcpBridge)
|
||||
config.mcpbridgeController = mcpbridgeController
|
||||
@@ -228,6 +262,7 @@ func (m *IngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f ist
|
||||
func (m *IngressConfig) AddLocalCluster(options common.Options) {
|
||||
secretController := secret.NewController(m.localKubeClient, options)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
secretController.AddEventHandler(m.secretConfigMgr.HandleSecretChange)
|
||||
|
||||
var ingressController common.IngressController
|
||||
v1 := common.V1Available(m.localKubeClient)
|
||||
@@ -254,10 +289,24 @@ func (m *IngressConfig) List(typ config.GroupVersionKind, namespace string) []co
|
||||
var configs = make([]config.Config, 0)
|
||||
|
||||
if configsFromIngress := m.listFromIngressControllers(typ, namespace); configsFromIngress != nil {
|
||||
// Process templates for ingress configs
|
||||
for i := range configsFromIngress {
|
||||
if err := m.templateProcessor.ProcessConfig(&configsFromIngress[i]); err != nil {
|
||||
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
|
||||
configsFromIngress[i].Namespace, configsFromIngress[i].Name, err)
|
||||
}
|
||||
}
|
||||
configs = append(configs, configsFromIngress...)
|
||||
}
|
||||
|
||||
if configsFromGateway := m.listFromGatewayControllers(typ, namespace); configsFromGateway != nil {
|
||||
// Process templates for gateway configs
|
||||
for i := range configsFromGateway {
|
||||
if err := m.templateProcessor.ProcessConfig(&configsFromGateway[i]); err != nil {
|
||||
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
|
||||
configsFromGateway[i].Namespace, configsFromGateway[i].Name, err)
|
||||
}
|
||||
}
|
||||
configs = append(configs, configsFromGateway...)
|
||||
}
|
||||
|
||||
@@ -987,7 +1036,6 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
func isBoolValueTrue(b *wrappers.BoolValue) bool {
|
||||
|
||||
119
pkg/ingress/config/ingress_template.go
Normal file
119
pkg/ingress/config/ingress_template.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
// TemplateProcessor handles template substitution in configs
|
||||
type TemplateProcessor struct {
|
||||
// getValue is a function that retrieves values by type, namespace, name and key
|
||||
getValue func(valueType, namespace, name, key string) (string, error)
|
||||
namespace string
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
}
|
||||
|
||||
// NewTemplateProcessor creates a new TemplateProcessor with the given value getter function
|
||||
func NewTemplateProcessor(getValue func(valueType, namespace, name, key string) (string, error), namespace string, secretConfigMgr *SecretConfigMgr) *TemplateProcessor {
|
||||
return &TemplateProcessor{
|
||||
getValue: getValue,
|
||||
namespace: namespace,
|
||||
secretConfigMgr: secretConfigMgr,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessConfig processes a config and substitutes any template variables
|
||||
func (p *TemplateProcessor) ProcessConfig(cfg *config.Config) error {
|
||||
// Convert spec to JSON string to process substitutions
|
||||
jsonBytes, err := json.Marshal(cfg.Spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config spec: %v", err)
|
||||
}
|
||||
|
||||
configStr := string(jsonBytes)
|
||||
// Find all value references in format:
|
||||
// ${type.name.key} or ${type.namespace/name.key}
|
||||
valueRegex := regexp.MustCompile(`\$\{([^.}]+)\.(?:([^/]+)/)?([^.}]+)\.([^}]+)\}`)
|
||||
matches := valueRegex.FindAllStringSubmatch(configStr, -1)
|
||||
// If there are no value references, return immediately
|
||||
if len(matches) == 0 {
|
||||
if p.secretConfigMgr != nil {
|
||||
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
|
||||
IngressLog.Errorf("failed to delete secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
foundSecretSource := false
|
||||
IngressLog.Infof("start to apply config %s/%s with %d variables", cfg.Namespace, cfg.Name, len(matches))
|
||||
for _, match := range matches {
|
||||
valueType := match[1]
|
||||
var namespace, name, key string
|
||||
if match[2] != "" {
|
||||
// Format: ${type.namespace/name.key}
|
||||
namespace = match[2]
|
||||
} else {
|
||||
// Format: ${type.name.key} - use default namespace
|
||||
namespace = p.namespace
|
||||
}
|
||||
name = match[3]
|
||||
key = match[4]
|
||||
|
||||
// Get value using the provided getter function
|
||||
value, err := p.getValue(valueType, namespace, name, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s value for %s/%s.%s: %v", valueType, namespace, name, key, err)
|
||||
}
|
||||
|
||||
// Add secret dependency if this is a secret reference
|
||||
if valueType == "secret" && p.secretConfigMgr != nil {
|
||||
foundSecretSource = true
|
||||
secretKey := fmt.Sprintf("%s/%s", namespace, name)
|
||||
if err := p.secretConfigMgr.AddConfig(secretKey, cfg); err != nil {
|
||||
IngressLog.Errorf("failed to add secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
// Replace placeholder with actual value
|
||||
configStr = strings.Replace(configStr, match[0], value, 1)
|
||||
}
|
||||
|
||||
// Create a new instance of the same type as cfg.Spec
|
||||
newSpec := proto.Clone(cfg.Spec.(proto.Message))
|
||||
if err := json.Unmarshal([]byte(configStr), newSpec); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal substituted config: %v", err)
|
||||
}
|
||||
cfg.Spec = newSpec
|
||||
|
||||
// Delete secret dependency if no secret reference is found
|
||||
if !foundSecretSource {
|
||||
if p.secretConfigMgr != nil {
|
||||
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
|
||||
IngressLog.Errorf("failed to delete secret dependency: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IngressLog.Infof("end to process config %s/%s", cfg.Namespace, cfg.Name)
|
||||
return nil
|
||||
}
|
||||
166
pkg/ingress/config/ingress_template_test.go
Normal file
166
pkg/ingress/config/ingress_template_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
)
|
||||
|
||||
func TestTemplateProcessor_ProcessConfig(t *testing.T) {
|
||||
// Create test values map
|
||||
values := map[string]string{
|
||||
"secret.default/test-secret.api_key": "test-api-key",
|
||||
"secret.default/test-secret.plugin_conf.timeout": "5000",
|
||||
"secret.default/test-secret.plugin_conf.max_retries": "3",
|
||||
"secret.higress-system/auth-secret.auth_config.type": "basic",
|
||||
"secret.higress-system/auth-secret.auth_config.credentials": "base64-encoded",
|
||||
}
|
||||
|
||||
// Mock value getter function
|
||||
getValue := func(valueType, namespace, name, key string) (string, error) {
|
||||
fullKey := fmt.Sprintf("%s.%s/%s.%s", valueType, namespace, name, key)
|
||||
fmt.Printf("Getting value for %s", fullKey)
|
||||
if value, exists := values[fullKey]; exists {
|
||||
return value, nil
|
||||
}
|
||||
return "", fmt.Errorf("value not found for %s", fullKey)
|
||||
}
|
||||
|
||||
// Create template processor
|
||||
processor := NewTemplateProcessor(getValue, "higress-system", nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wasmPlugin *extensions.WasmPlugin
|
||||
expected *extensions.WasmPlugin
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple api key reference",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "${secret.default/test-secret.api_key}",
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "test-api-key",
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "config with multiple fields",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"timeout": "${secret.default/test-secret.plugin_conf.timeout}",
|
||||
"max_retries": "${secret.default/test-secret.plugin_conf.max_retries}",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"timeout": "5000",
|
||||
"max_retries": "3",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "auth config with default namespace",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"auth": map[string]interface{}{
|
||||
"type": "${secret.auth-secret.auth_config.type}",
|
||||
"credentials": "${secret.auth-secret.auth_config.credentials}",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expected: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"auth": map[string]interface{}{
|
||||
"type": "basic",
|
||||
"credentials": "base64-encoded",
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent secret",
|
||||
wasmPlugin: &extensions.WasmPlugin{
|
||||
PluginName: "test-plugin",
|
||||
PluginConfig: makeStructValue(t, map[string]interface{}{
|
||||
"api_key": "${secret.default/non-existent.api_key}",
|
||||
}),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: tt.wasmPlugin,
|
||||
}
|
||||
|
||||
err := processor.ProcessConfig(cfg)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
processedPlugin := cfg.Spec.(*extensions.WasmPlugin)
|
||||
|
||||
// Compare plugin name
|
||||
assert.Equal(t, tt.expected.PluginName, processedPlugin.PluginName)
|
||||
|
||||
// Compare plugin configs
|
||||
if tt.expected.PluginConfig != nil {
|
||||
assert.NotNil(t, processedPlugin.PluginConfig)
|
||||
assert.Equal(t, tt.expected.PluginConfig.AsMap(), processedPlugin.PluginConfig.AsMap())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create structpb.Struct from map
|
||||
func makeStructValue(t *testing.T, m map[string]interface{}) *structpb.Struct {
|
||||
s, err := structpb.NewStruct(m)
|
||||
assert.NoError(t, err, "Failed to create struct value")
|
||||
return s
|
||||
}
|
||||
157
pkg/ingress/config/secret_config_mgr.go
Normal file
157
pkg/ingress/config/secret_config_mgr.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
)
|
||||
|
||||
// toConfigKey converts config.Config to istiomodel.ConfigKey
|
||||
func toConfigKey(cfg *config.Config) (istiomodel.ConfigKey, error) {
|
||||
return istiomodel.ConfigKey{
|
||||
Kind: kind.MustFromGVK(cfg.GroupVersionKind),
|
||||
Name: cfg.Name,
|
||||
Namespace: cfg.Namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SecretConfigMgr maintains the mapping between secrets and configs
|
||||
type SecretConfigMgr struct {
|
||||
mutex sync.RWMutex
|
||||
|
||||
// configSet tracks all configs that have been added
|
||||
// key format: namespace/name
|
||||
configSet sets.Set[string]
|
||||
|
||||
// secretToConfigs maps secret key to dependent configs
|
||||
// key format: namespace/name
|
||||
secretToConfigs map[string]sets.Set[istiomodel.ConfigKey]
|
||||
|
||||
// watchedSecrets tracks which secrets are being watched
|
||||
watchedSecrets sets.Set[string]
|
||||
|
||||
// xdsUpdater is used to push config updates
|
||||
xdsUpdater istiomodel.XDSUpdater
|
||||
}
|
||||
|
||||
// NewSecretConfigMgr creates a new SecretConfigMgr
|
||||
func NewSecretConfigMgr(xdsUpdater istiomodel.XDSUpdater) *SecretConfigMgr {
|
||||
return &SecretConfigMgr{
|
||||
secretToConfigs: make(map[string]sets.Set[istiomodel.ConfigKey]),
|
||||
watchedSecrets: sets.New[string](),
|
||||
configSet: sets.New[string](),
|
||||
xdsUpdater: xdsUpdater,
|
||||
}
|
||||
}
|
||||
|
||||
// AddConfig adds a config and its secret dependencies
|
||||
func (m *SecretConfigMgr) AddConfig(secretKey string, cfg *config.Config) error {
|
||||
configKey, _ := toConfigKey(cfg)
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
|
||||
m.configSet.Insert(configId)
|
||||
|
||||
if configs, exists := m.secretToConfigs[secretKey]; exists {
|
||||
configs.Insert(configKey)
|
||||
} else {
|
||||
m.secretToConfigs[secretKey] = sets.New(configKey)
|
||||
}
|
||||
|
||||
// Add to watched secrets
|
||||
m.watchedSecrets.Insert(secretKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteConfig removes a config from all secret dependencies
|
||||
func (m *SecretConfigMgr) DeleteConfig(cfg *config.Config) error {
|
||||
configKey, _ := toConfigKey(cfg)
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
|
||||
if !m.configSet.Contains(configId) {
|
||||
return nil
|
||||
}
|
||||
|
||||
removeKeys := make([]string, 0)
|
||||
// Find and remove the config from all secrets
|
||||
for secretKey, configs := range m.secretToConfigs {
|
||||
if configs.Contains(configKey) {
|
||||
configs.Delete(configKey)
|
||||
// If no more configs depend on this secret, remove it
|
||||
if configs.Len() == 0 {
|
||||
removeKeys = append(removeKeys, secretKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the secrets from the secretToConfigs map
|
||||
for _, secretKey := range removeKeys {
|
||||
delete(m.secretToConfigs, secretKey)
|
||||
m.watchedSecrets.Delete(secretKey)
|
||||
}
|
||||
// Remove the config from the config set
|
||||
m.configSet.Delete(configId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigsForSecret returns all configs that depend on the given secret
|
||||
func (m *SecretConfigMgr) GetConfigsForSecret(secretKey string) []istiomodel.ConfigKey {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if configs, exists := m.secretToConfigs[secretKey]; exists {
|
||||
return configs.UnsortedList()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSecretWatched checks if a secret is being watched
|
||||
func (m *SecretConfigMgr) IsSecretWatched(secretKey string) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.watchedSecrets.Contains(secretKey)
|
||||
}
|
||||
|
||||
// HandleSecretChange handles secret changes and updates affected configs
|
||||
func (m *SecretConfigMgr) HandleSecretChange(name util.ClusterNamespacedName) {
|
||||
secretKey := fmt.Sprintf("%s/%s", name.Namespace, name.Name)
|
||||
// Check if this secret is being watched
|
||||
if !m.IsSecretWatched(secretKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get affected configs
|
||||
configKeys := m.GetConfigsForSecret(secretKey)
|
||||
if len(configKeys) == 0 {
|
||||
return
|
||||
}
|
||||
IngressLog.Infof("SecretConfigMgr Secret %s changed, updating %d dependent configs and push", secretKey, len(configKeys))
|
||||
m.xdsUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
Reason: istiomodel.NewReasonStats(istiomodel.SecretTrigger),
|
||||
})
|
||||
}
|
||||
155
pkg/ingress/config/secret_config_mgr_test.go
Normal file
155
pkg/ingress/config/secret_config_mgr_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
istiomodel "istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
)
|
||||
|
||||
type mockXdsUpdater struct {
|
||||
lastPushRequest *istiomodel.PushRequest
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) EDSUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) EDSCacheUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) SvcUpdate(shard istiomodel.ShardKey, hostname string, namespace string, event istiomodel.Event) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) ProxyUpdate(clusterID cluster.ID, ip string) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) RemoveShard(shardKey istiomodel.ShardKey) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockXdsUpdater) ConfigUpdate(req *istiomodel.PushRequest) {
|
||||
m.lastPushRequest = req
|
||||
}
|
||||
|
||||
func TestSecretConfigMgr(t *testing.T) {
|
||||
updater := &mockXdsUpdater{}
|
||||
mgr := NewSecretConfigMgr(updater)
|
||||
|
||||
// Test AddConfig
|
||||
t.Run("AddConfig", func(t *testing.T) {
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
err := mgr.AddConfig("default/test-secret", wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, mgr.IsSecretWatched("default/test-secret"))
|
||||
|
||||
configs := mgr.GetConfigsForSecret("default/test-secret")
|
||||
assert.Len(t, configs, 1)
|
||||
assert.Equal(t, kind.WasmPlugin, configs[0].Kind)
|
||||
assert.Equal(t, "test-plugin", configs[0].Name)
|
||||
assert.Equal(t, "default", configs[0].Namespace)
|
||||
})
|
||||
|
||||
// Test DeleteConfig
|
||||
t.Run("DeleteConfig", func(t *testing.T) {
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
err := mgr.DeleteConfig(wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, mgr.IsSecretWatched("default/test-secret"))
|
||||
assert.Empty(t, mgr.GetConfigsForSecret("default/test-secret"))
|
||||
})
|
||||
|
||||
// Test HandleSecretChange
|
||||
t.Run("HandleSecretChange", func(t *testing.T) {
|
||||
// Add a config first
|
||||
wasmPlugin := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "test-plugin",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := mgr.AddConfig("default/test-secret", wasmPlugin)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test secret change
|
||||
secretName := util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mgr.HandleSecretChange(secretName)
|
||||
assert.NotNil(t, updater.lastPushRequest)
|
||||
assert.True(t, updater.lastPushRequest.Full)
|
||||
})
|
||||
|
||||
// Test full push for secret update
|
||||
t.Run("FullPushForSecretUpdate", func(t *testing.T) {
|
||||
// Add a secret config
|
||||
secretConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Secret,
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := mgr.AddConfig("default/test-secret", secretConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update the secret
|
||||
secretName := util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mgr.HandleSecretChange(secretName)
|
||||
assert.NotNil(t, updater.lastPushRequest)
|
||||
assert.True(t, updater.lastPushRequest.Full)
|
||||
})
|
||||
}
|
||||
192
test/e2e/conformance/tests/go-wasm-basic-auth-template.go
Normal file
192
test/e2e/conformance/tests/go-wasm-basic-auth-template.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/kubernetes"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(WasmPluginsBasicAuthTemplate)
|
||||
}
|
||||
|
||||
var WasmPluginsBasicAuthTemplate = suite.ConformanceTest{
|
||||
ShortName: "WasmPluginsBasicAuthTemplate",
|
||||
Description: "The Ingress in the higress-conformance-infra namespace test the basic-auth WASM plugin.",
|
||||
Manifests: []string{"tests/go-wasm-basic-auth-template.yaml"},
|
||||
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
testcases := []http.Assertion{
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 1: Successful authentication",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"Authorization": "Basic YWRtaW46MTIzNDU2"}, // base64("admin:123456")
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"X-Mse-Consumer": "consumer1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 2: No Basic Authentication information found",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 401,
|
||||
},
|
||||
AdditionalResponseHeaders: map[string]string{
|
||||
"WWW-Authenticate": "Basic realm=MSE Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 3: Invalid username and/or password",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"Authorization": "Basic YWRtaW46cXdlcg=="}, // base64("admin:qwer")
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 401,
|
||||
},
|
||||
AdditionalResponseHeaders: map[string]string{
|
||||
"WWW-Authenticate": "Basic realm=MSE Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 4: Unauthorized consumer",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"Authorization": "Basic Z3Vlc3Q6YWJj"}, // base64("guest:abc")
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 403,
|
||||
},
|
||||
AdditionalResponseHeaders: map[string]string{
|
||||
"WWW-Authenticate": "Basic realm=MSE Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testcases2 := []http.Assertion{
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 5: Invalid username and/or password",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"Authorization": "Basic YWRtaW46MTIzNDU2"}, // base64("admin:123456")
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"X-Mse-Consumer": "consumer1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 401,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 6: Successful authentication",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Headers: map[string]string{"Authorization": "Basic YWRtaW46cXdlcg=="}, // base64("admin:qwer")
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
AdditionalResponseHeaders: map[string]string{
|
||||
"WWW-Authenticate": "Basic realm=MSE Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("WasmPlugins basic-auth", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
err := kubernetes.ApplySecret(t, suite.Client, "higress-conformance-infra", "auth-secret", "auth.credential1", "admin:qwer")
|
||||
if err != nil {
|
||||
t.Fatalf("can't apply secret %s in namespace %s for data key %s", "auth-secret", "higress-conformance-infra", "auth.credential1")
|
||||
}
|
||||
for _, testcase := range testcases2 {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
77
test/e2e/conformance/tests/go-wasm-basic-auth-template.yaml
Normal file
77
test/e2e/conformance/tests/go-wasm-basic-auth-template.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: auth-secret
|
||||
namespace: higress-conformance-infra
|
||||
type: Opaque
|
||||
stringData:
|
||||
auth.credential1: "admin:123456"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: auth-secret
|
||||
namespace: higress-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
auth.credential2: "guest:abc"
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-basic-auth
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "foo.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/foo"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: basic-auth
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
consumers:
|
||||
- credential: ${secret.higress-conformance-infra/auth-secret.auth.credential1}
|
||||
name: consumer1
|
||||
- credential: ${secret.auth-secret.auth.credential2}
|
||||
name: consumer2
|
||||
global_auth: false
|
||||
defaultConfigDisable: false
|
||||
matchRules:
|
||||
- config:
|
||||
allow:
|
||||
- consumer1
|
||||
configDisable: false
|
||||
ingress:
|
||||
- higress-conformance-infra/wasmplugin-basic-auth
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/go-basic-auth:1.0.0
|
||||
@@ -144,3 +144,15 @@ func ApplyConfigmapDataWithYaml(t *testing.T, c client.Client, namespace string,
|
||||
t.Logf("🏗 Updating %s %s", name, namespace)
|
||||
return c.Update(ctx, cm)
|
||||
}
|
||||
|
||||
func ApplySecret(t *testing.T, c client.Client, namespace string, name string, key string, val string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cm := &v1.Secret{}
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
cm.Data[key] = []byte(val)
|
||||
t.Logf("🏗 Updating Secret %s %s", name, namespace)
|
||||
return c.Update(ctx, cm)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user