Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions gateway/configs/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,17 @@ reconnect_max = "5m"
# Polling interval for periodic sync
polling_interval = "15m"
# Skip TLS certificate verification for control plane connections
insecure_skip_verify = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_INSECURE_SKIP_VERIFY" "false" }}'
insecure_skip_verify = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_INSECURE_SKIP_VERIFY" "true" }}'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Enable two-way artifact/deployment sync with the control plane (default: true)
deployment_sync_enabled = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_DEPLOYMENT_SYNC_ENABLED" "true" }}'
# APIM OAuth2 Client ID
# Option 1: Client Credentials Flow (OAuth2) — APIM OAuth2 Client ID
apim_oauth2_client_id = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_APIM_OAUTH2_CLIENT_ID" "" }}'
# APIM OAuth2 Client secret
apim_oauth2_client_secret = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_APIM_OAUTH2_CLIENT_SECRET" "" }}'
# Option 2: Resource Owner Password Credentials Flow (OAuth2). Set these instead of
# the client id/secret pair above; both default to empty.
# apim_oauth2_username = "your-username"
# apim_oauth2_password = "your-password"
# Gateway name
gateway_name = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_GATEWAY_NAME" "default" }}'

Expand Down Expand Up @@ -140,7 +144,7 @@ level = '{{ env "APIP_GW_CONTROLLER_LOGGING_LEVEL" "info" }}'
format = "text"

[controller.metrics]
enabled = true
enabled = false
port = '{{ env "APIP_GW_CONTROLLER_METRICS_PORT" "9091" }}'

[controller.event_hub]
Expand Down Expand Up @@ -385,7 +389,7 @@ max_header_limit = 8192
# ANALYTICS CONFIGURATION (consumer — enabling it activates the collector)
# =============================================================================
[analytics]
enabled = false
enabled = true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Deprecated: allow_payloads is preserved for backward compatibility. When true
# and both send_request_body and send_response_body are false, both directions
# are enabled (bool fields cannot distinguish "unset" from explicitly false).
Expand All @@ -400,7 +404,7 @@ send_response_body = false
enabled_publishers = ["moesif"]

[analytics.publishers.moesif]
application_id = '{{ env "APIP_GW_ANALYTICS_PUBLISHERS_MOESIF_APPLICATION_ID" "" }}'
application_id = '{{ env "APIP_GW_ANALYTICS_PUBLISHERS_MOESIF_APPLICATION_ID" "<your-moesif-application-id>" }}'
moesif_base_url = "https://api.moesif.net"
publish_interval = 5
event_queue_size = 10000
Expand Down
36 changes: 0 additions & 36 deletions gateway/configs/config.toml
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
[collector]
request_body = false
response_body = false
request_headers = false
response_headers = false

[analytics]
enabled = true
enabled_publishers = ["moesif"]

[analytics.publishers.moesif]
application_id = '{{ env "APIP_GW_ANALYTICS_PUBLISHERS_MOESIF_APPLICATION_ID" "<your-moesif-application-id>" }}'

[traffic_logging]
enabled = false
masked_headers = ["authorization", "x-api-key", "x-jwt-assertion"]
max_payload_size = 2048
request_headers = false
response_headers = false
request_body = false
response_body = false
exclude_fields = []

[router]
gateway_host = "*"

Expand All @@ -36,31 +20,19 @@ type = '{{ env "APIP_GW_CONTROLLER_STORAGE_TYPE" "sqlite" }}'
[controller.storage.sqlite]
path = '{{ env "APIP_GW_CONTROLLER_STORAGE_SQLITE_PATH" "./data/gateway.db" }}'

[controller.storage.database]
dsn = '{{ env "APIP_GW_CONTROLLER_STORAGE_DATABASE_DSN" "" }}'

[policy_engine.logging]
level = "info"

[controller.logging]
level = '{{ env "APIP_GW_CONTROLLER_LOGGING_LEVEL" "info" }}'

[controller.metrics]
port = '{{ env "APIP_GW_CONTROLLER_METRICS_PORT" "9091" }}'

[controller.controlplane]
host = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_HOST" "" }}'
token = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_TOKEN" "" }}'
insecure_skip_verify = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_INSECURE_SKIP_VERIFY" "true" }}'
gateway_name = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_GATEWAY_NAME" "default" }}'
apim_oauth2_client_id = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_APIM_OAUTH2_CLIENT_ID" "" }}'
apim_oauth2_client_secret = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_APIM_OAUTH2_CLIENT_SECRET" "" }}'
# Option 2: Resource Owner Password Credentials Flow (OAuth2)
# apim_oauth2_username = "your-username"
# apim_oauth2_password = "your-password"

[controller.policies]
definitions_path = '{{ env "APIP_GW_CONTROLLER_POLICIES_DEFINITIONS_PATH" "./default-policies" }}'

[controller.auth.basic]
enabled = true
Expand All @@ -70,11 +42,3 @@ username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}'
password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}'
password_hashed = true
roles = ["admin"]

[policy_configurations.llm_cost_v1]
pricing_file = "/etc/policy-engine/llm-pricing/model_prices.json"

[immutable_gateway]
enabled = false
artifacts_dir = "/etc/api-platform-gateway/immutable_gateway/artifacts"

2 changes: 1 addition & 1 deletion gateway/gateway-controller/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ func defaultConfig() *Config {
ReconnectInitial: 1 * time.Second,
ReconnectMax: 5 * time.Minute,
PollingInterval: 15 * time.Minute,
InsecureSkipVerify: false,
InsecureSkipVerify: true,
DeploymentSyncEnabled: true,
SyncBatchSize: 50,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ func Load(configPaths ...string) (*Config, error) {
// unmarshal and Validate.
k := koanf.New(".")

if err := k.Load(confmap.Provider(defaultResolvableConfig(), "."), nil); err != nil {
return nil, fmt.Errorf("failed to seed resolvable config defaults: %w", err)
}

// Load each config file in order. Successive loads deep-merge maps and replace
// arrays, giving last-wins precedence for keys set in more than one file.
for _, configPath := range configPaths {
Expand Down Expand Up @@ -442,6 +446,22 @@ func interpolate(k *koanf.Koanf) (*koanf.Koanf, error) {
return out, nil
}

// DefaultLLMCostPricingFile is the model-pricing file the llm-cost policy fallback
const DefaultLLMCostPricingFile = "/etc/policy-engine/llm-pricing/model_prices.json"

// defaultResolvableConfig returns defaults for config keys that policy definitions
// reference via ${config...} system-parameter markers
func defaultResolvableConfig() map[string]interface{} {
return map[string]interface{}{
"policy_configurations.llm_cost_v1.pricing_file": DefaultLLMCostPricingFile,
}
}

// defaultMaskedHeaders returns the header names whose values traffic logging redacts
func defaultMaskedHeaders() []string {
return []string{"authorization", "x-api-key", "x-jwt-assertion"}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// defaultAccessLogsServiceConfig returns the default policy-engine ALS receiver tuning.
// Shared by the collector (canonical) and the deprecated [analytics].access_logs_service
// alias so a partial alias override migrates cleanly.
Expand Down Expand Up @@ -515,7 +535,7 @@ func defaultConfig() *Config {
},
TrafficLogging: TrafficLoggingConfig{
Enabled: false,
MaskedHeaders: []string{},
MaskedHeaders: defaultMaskedHeaders(),
MaxPayloadSize: 0,
RequestHeaders: false,
RequestBody: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you 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 (
"os"
"path/filepath"
"testing"
)

// Verifies the two new defaults reach a config that never mentions them:
// traffic_logging.masked_headers, and the ${config}-resolvable pricing_file
// (which must land in RawConfig, not just the struct).
func TestNewDefaultsReachBareConfig(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "bare.toml")
if err := os.WriteFile(p, []byte("[policy_engine.logging]\nlevel = \"info\"\n"), 0o600); err != nil {
t.Fatal(err)
}
cfg, err := Load(p)
if err != nil {
t.Fatalf("load: %v", err)
}

t.Logf("masked_headers = %v", cfg.TrafficLogging.MaskedHeaders)
if len(cfg.TrafficLogging.MaskedHeaders) != 3 {
t.Errorf("want 3 masked headers, got %v", cfg.TrafficLogging.MaskedHeaders)
}

pc, _ := cfg.PolicyConfigurations["llm_cost_v1"].(map[string]interface{})
t.Logf("struct pricing_file = %v", pc["pricing_file"])

// The resolver evaluates against RawConfig — this is the path that matters.
raw, _ := cfg.PolicyEngine.RawConfig["policy_configurations"].(map[string]interface{})
inner, _ := raw["llm_cost_v1"].(map[string]interface{})
got, _ := inner["pricing_file"].(string)
t.Logf("RawConfig pricing_file = %q", got)
if got != DefaultLLMCostPricingFile {
t.Errorf("RawConfig pricing_file = %q, want %q", got, DefaultLLMCostPricingFile)
}
}

// An operator-set value must still win over the seeded default.
func TestOperatorOverridesPricingFile(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "over.toml")
body := "[policy_configurations.llm_cost_v1]\npricing_file = \"/custom/prices.json\"\n" +
"[traffic_logging]\nmasked_headers = [\"cookie\"]\n"
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
cfg, err := Load(p)
if err != nil {
t.Fatalf("load: %v", err)
}
raw, _ := cfg.PolicyEngine.RawConfig["policy_configurations"].(map[string]interface{})
inner, _ := raw["llm_cost_v1"].(map[string]interface{})
if got, _ := inner["pricing_file"].(string); got != "/custom/prices.json" {
t.Errorf("override lost: got %q", got)
}
if got := cfg.TrafficLogging.MaskedHeaders; len(got) != 1 || got[0] != "cookie" {
t.Errorf("masked_headers override lost: got %v", got)
}
}
Loading