mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
46 Commits
v2.1.10
...
update-hel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a62da18c0 | ||
|
|
4babdb6a4f | ||
|
|
38d50bbdad | ||
|
|
2b3d0d7207 | ||
|
|
85791e4866 | ||
|
|
5cc9f65aaa | ||
|
|
17e80b30fe | ||
|
|
e7e3ab5ff6 | ||
|
|
2b8f91e5f2 | ||
|
|
3191bb1bf5 | ||
|
|
00d0ad0f5e | ||
|
|
ed4ca76215 | ||
|
|
b29967c5d3 | ||
|
|
4cf1e5e6a0 | ||
|
|
5327a598ac | ||
|
|
e1e8e55c83 | ||
|
|
f4905cbba7 | ||
|
|
ebbcb15811 | ||
|
|
e8bcbde5f4 | ||
|
|
08d4f556a1 | ||
|
|
9aef35c31f | ||
|
|
5d26588901 | ||
|
|
3fbc233b3b | ||
|
|
4fa7fcba01 | ||
|
|
6998800c64 | ||
|
|
3cc745a6f5 | ||
|
|
9a57a4c7e0 | ||
|
|
7f5b37ae6d | ||
|
|
0ada107ec5 | ||
|
|
5c17d3faa3 | ||
|
|
b6e94b1f60 | ||
|
|
8deceb4d2c | ||
|
|
6bf587a4d1 | ||
|
|
7bee45b022 | ||
|
|
8a7a375ebd | ||
|
|
896bcacf4c | ||
|
|
3e24d66079 | ||
|
|
116e7c6904 | ||
|
|
ae0bb41885 | ||
|
|
f3ac8eafe5 | ||
|
|
985b58ad5c | ||
|
|
ccb1539f43 | ||
|
|
42334f21df | ||
|
|
7a504fd67d | ||
|
|
b2b4f72775 | ||
|
|
7dfc42fd92 |
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -1,17 +1,17 @@
|
||||
[submodule "istio/api"]
|
||||
path = istio/api
|
||||
url = https://github.com/higress-group/api
|
||||
branch = istio-1.19
|
||||
branch = istio-1.27
|
||||
shallow = true
|
||||
[submodule "istio/istio"]
|
||||
path = istio/istio
|
||||
url = https://github.com/higress-group/istio
|
||||
branch = istio-1.19
|
||||
branch = istio-1.27
|
||||
shallow = true
|
||||
[submodule "istio/client-go"]
|
||||
path = istio/client-go
|
||||
url = https://github.com/higress-group/client-go
|
||||
branch = istio-1.19
|
||||
branch = istio-1.27
|
||||
shallow = true
|
||||
[submodule "istio/pkg"]
|
||||
path = istio/pkg
|
||||
@@ -26,7 +26,7 @@
|
||||
[submodule "envoy/go-control-plane"]
|
||||
path = envoy/go-control-plane
|
||||
url = https://github.com/higress-group/go-control-plane
|
||||
branch = istio-1.19
|
||||
branch = istio-1.27
|
||||
shallow = true
|
||||
[submodule "envoy/envoy"]
|
||||
path = envoy/envoy
|
||||
|
||||
@@ -35,6 +35,7 @@ header:
|
||||
- 'hgctl/pkg/manifests'
|
||||
- 'pkg/ingress/kube/gateway/istio/testdata'
|
||||
- 'release-notes/**'
|
||||
- '.cursor/**'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
13
ADOPTERS.md
Normal file
13
ADOPTERS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Adopters of Higress
|
||||
|
||||
Below are the adopters of the Higress project. If you are using Higress in your organization, please add your name to the list by submitting a pull request: this will help foster the Higress community. Kindly ensure the list remains in alphabetical order.
|
||||
|
||||
|
||||
| Organization | Contact (GitHub User Name) | Environment | Description of Use |
|
||||
|---------------------------------------|----------------------------------------|--------------------------------------------|-----------------------------------------------------------------------|
|
||||
| [antdigital](https://antdigital.com/) | [@Lovelcp](https://github.com/Lovelcp) | Production | Ingress Gateway, Microservice gateway, LLM Gateway, MCP Gateway |
|
||||
| [kuaishou](https://ir.kuaishou.com/) | [@maplecap](https://github.com/maplecap) | Production | LLM Gateway |
|
||||
| [Trip.com](https://www.trip.com/) | [@CH3CHO](https://github.com/CH3CHO) | Production | LLM Gateway, MCP Gateway |
|
||||
| [vipshop](https://github.com/vipshop/) | [@firebook](https://github.com/firebook) | Production | LLM Gateway, MCP Gateway, Inference Gateway |
|
||||
| [labring](https://github.com/labring/) | [@zzjin](https://github.com/zzjin) | Production | Ingress Gateway |
|
||||
| < company name here> | < your github handle here > | <Production/Testing/Experimenting/etc> | <Ingress Gateway/Microservice gateway/LLM Gateway/MCP Gateway/Inference Gateway> |
|
||||
@@ -201,7 +201,7 @@ install: pre-install
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= cdf0f16bf622102f89a0d0257834f43f502e4b99
|
||||
ISTIO_LATEST_IMAGE_TAG ?= ec099e0a24d25aff9c6530cb45dc0ff86ebb78b9
|
||||
ISTIO_LATEST_IMAGE_TAG ?= a7525f292c38d7d3380f3ce7ee971ad6e3c46adf
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
@@ -82,6 +82,8 @@ Port descriptions:
|
||||
>
|
||||
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
|
||||
>
|
||||
> **North America**: `higress-registry.us-west-1.cr.aliyuncs.com`
|
||||
>
|
||||
> **Southeast Asia**: `higress-registry.ap-southeast-7.cr.aliyuncs.com`
|
||||
|
||||
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
|
||||
|
||||
@@ -41,6 +41,56 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Route type for matching rules.
|
||||
// Extended by Higress
|
||||
type RouteType int32
|
||||
|
||||
const (
|
||||
// HTTP route (default)
|
||||
RouteType_HTTP RouteType = 0
|
||||
// GRPC route
|
||||
RouteType_GRPC RouteType = 1
|
||||
)
|
||||
|
||||
// Enum value maps for RouteType.
|
||||
var (
|
||||
RouteType_name = map[int32]string{
|
||||
0: "HTTP",
|
||||
1: "GRPC",
|
||||
}
|
||||
RouteType_value = map[string]int32{
|
||||
"HTTP": 0,
|
||||
"GRPC": 1,
|
||||
}
|
||||
)
|
||||
|
||||
func (x RouteType) Enum() *RouteType {
|
||||
p := new(RouteType)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x RouteType) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (RouteType) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (RouteType) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x RouteType) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RouteType.Descriptor instead.
|
||||
func (RouteType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
type PluginPhase int32
|
||||
|
||||
@@ -84,11 +134,11 @@ func (x PluginPhase) String() string {
|
||||
}
|
||||
|
||||
func (PluginPhase) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (PluginPhase) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x PluginPhase) Number() protoreflect.EnumNumber {
|
||||
@@ -97,7 +147,7 @@ func (x PluginPhase) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use PluginPhase.Descriptor instead.
|
||||
func (PluginPhase) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{0}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
// The pull behaviour to be applied when fetching an OCI image,
|
||||
@@ -146,11 +196,11 @@ func (x PullPolicy) String() string {
|
||||
}
|
||||
|
||||
func (PullPolicy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2].Descriptor()
|
||||
}
|
||||
|
||||
func (PullPolicy) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2]
|
||||
}
|
||||
|
||||
func (x PullPolicy) Number() protoreflect.EnumNumber {
|
||||
@@ -159,7 +209,7 @@ func (x PullPolicy) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use PullPolicy.Descriptor instead.
|
||||
func (PullPolicy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{1}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type EnvValueSource int32
|
||||
@@ -194,11 +244,11 @@ func (x EnvValueSource) String() string {
|
||||
}
|
||||
|
||||
func (EnvValueSource) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3].Descriptor()
|
||||
}
|
||||
|
||||
func (EnvValueSource) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3]
|
||||
}
|
||||
|
||||
func (x EnvValueSource) Number() protoreflect.EnumNumber {
|
||||
@@ -207,7 +257,7 @@ func (x EnvValueSource) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use EnvValueSource.Descriptor instead.
|
||||
func (EnvValueSource) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{2}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
type FailStrategy int32
|
||||
@@ -246,11 +296,11 @@ func (x FailStrategy) String() string {
|
||||
}
|
||||
|
||||
func (FailStrategy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[4].Descriptor()
|
||||
}
|
||||
|
||||
func (FailStrategy) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[4]
|
||||
}
|
||||
|
||||
func (x FailStrategy) Number() protoreflect.EnumNumber {
|
||||
@@ -259,7 +309,7 @@ func (x FailStrategy) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use FailStrategy.Descriptor instead.
|
||||
func (FailStrategy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{3}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
// <!-- crd generation tags
|
||||
@@ -485,6 +535,8 @@ type MatchRule struct {
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable *wrappers.BoolValue `protobuf:"bytes,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
// Route type for this match rule, defaults to HTTP
|
||||
RouteType RouteType `protobuf:"varint,6,opt,name=route_type,json=routeType,proto3,enum=higress.extensions.v1alpha1.RouteType" json:"route_type,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MatchRule) Reset() {
|
||||
@@ -554,6 +606,13 @@ func (x *MatchRule) GetService() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetRouteType() RouteType {
|
||||
if x != nil {
|
||||
return x.RouteType
|
||||
}
|
||||
return RouteType_HTTP
|
||||
}
|
||||
|
||||
// Configuration for a Wasm VM.
|
||||
// more details can be found [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/wasm/v3/wasm.proto#extensions-wasm-v3-vmconfig).
|
||||
type VmConfig struct {
|
||||
@@ -736,7 +795,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f,
|
||||
0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xcb, 0x01,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x92, 0x02,
|
||||
0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69,
|
||||
0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
|
||||
@@ -749,37 +808,44 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56,
|
||||
0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
|
||||
0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e,
|
||||
0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45,
|
||||
0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45,
|
||||
0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a,
|
||||
0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41,
|
||||
0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54,
|
||||
0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c,
|
||||
0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
|
||||
0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49,
|
||||
0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a,
|
||||
0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49,
|
||||
0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10,
|
||||
0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
|
||||
0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10,
|
||||
0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01,
|
||||
0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61,
|
||||
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72,
|
||||
0x6f, 0x75, 0x74, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x26, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x6f,
|
||||
0x75, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x79,
|
||||
0x70, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35,
|
||||
0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72,
|
||||
0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e, 0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f,
|
||||
0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x1f, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
|
||||
0x47, 0x52, 0x50, 0x43, 0x10, 0x01, 0x2a, 0x45, 0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
|
||||
0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
|
||||
0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a,
|
||||
0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55,
|
||||
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43,
|
||||
0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73,
|
||||
0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10,
|
||||
0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69,
|
||||
0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x78,
|
||||
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
|
||||
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -794,40 +860,42 @@ func file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP() []byte {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_goTypes = []interface{}{
|
||||
(PluginPhase)(0), // 0: higress.extensions.v1alpha1.PluginPhase
|
||||
(PullPolicy)(0), // 1: higress.extensions.v1alpha1.PullPolicy
|
||||
(EnvValueSource)(0), // 2: higress.extensions.v1alpha1.EnvValueSource
|
||||
(FailStrategy)(0), // 3: higress.extensions.v1alpha1.FailStrategy
|
||||
(*WasmPlugin)(nil), // 4: higress.extensions.v1alpha1.WasmPlugin
|
||||
(*MatchRule)(nil), // 5: higress.extensions.v1alpha1.MatchRule
|
||||
(*VmConfig)(nil), // 6: higress.extensions.v1alpha1.VmConfig
|
||||
(*EnvVar)(nil), // 7: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 8: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 9: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 10: google.protobuf.BoolValue
|
||||
(RouteType)(0), // 0: higress.extensions.v1alpha1.RouteType
|
||||
(PluginPhase)(0), // 1: higress.extensions.v1alpha1.PluginPhase
|
||||
(PullPolicy)(0), // 2: higress.extensions.v1alpha1.PullPolicy
|
||||
(EnvValueSource)(0), // 3: higress.extensions.v1alpha1.EnvValueSource
|
||||
(FailStrategy)(0), // 4: higress.extensions.v1alpha1.FailStrategy
|
||||
(*WasmPlugin)(nil), // 5: higress.extensions.v1alpha1.WasmPlugin
|
||||
(*MatchRule)(nil), // 6: higress.extensions.v1alpha1.MatchRule
|
||||
(*VmConfig)(nil), // 7: higress.extensions.v1alpha1.VmConfig
|
||||
(*EnvVar)(nil), // 8: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 9: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 10: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 11: google.protobuf.BoolValue
|
||||
}
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
1, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
8, // 1: higress.extensions.v1alpha1.WasmPlugin.plugin_config:type_name -> google.protobuf.Struct
|
||||
0, // 2: higress.extensions.v1alpha1.WasmPlugin.phase:type_name -> higress.extensions.v1alpha1.PluginPhase
|
||||
9, // 3: higress.extensions.v1alpha1.WasmPlugin.priority:type_name -> google.protobuf.Int32Value
|
||||
3, // 4: higress.extensions.v1alpha1.WasmPlugin.fail_strategy:type_name -> higress.extensions.v1alpha1.FailStrategy
|
||||
6, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
8, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
5, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
10, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
8, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
10, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
7, // 11: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 12: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
2, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
9, // 1: higress.extensions.v1alpha1.WasmPlugin.plugin_config:type_name -> google.protobuf.Struct
|
||||
1, // 2: higress.extensions.v1alpha1.WasmPlugin.phase:type_name -> higress.extensions.v1alpha1.PluginPhase
|
||||
10, // 3: higress.extensions.v1alpha1.WasmPlugin.priority:type_name -> google.protobuf.Int32Value
|
||||
4, // 4: higress.extensions.v1alpha1.WasmPlugin.fail_strategy:type_name -> higress.extensions.v1alpha1.FailStrategy
|
||||
7, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
9, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
6, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
11, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
9, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
11, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
0, // 11: higress.extensions.v1alpha1.MatchRule.route_type:type_name -> higress.extensions.v1alpha1.RouteType
|
||||
8, // 12: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
3, // 13: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_extensions_v1alpha1_wasmplugin_proto_init() }
|
||||
@@ -890,7 +958,7 @@ func file_extensions_v1alpha1_wasmplugin_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_extensions_v1alpha1_wasmplugin_proto_rawDesc,
|
||||
NumEnums: 4,
|
||||
NumEnums: 5,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
|
||||
@@ -122,6 +122,18 @@ message MatchRule {
|
||||
google.protobuf.Struct config = 3;
|
||||
google.protobuf.BoolValue config_disable = 4;
|
||||
repeated string service = 5;
|
||||
// Route type for this match rule, defaults to HTTP
|
||||
RouteType route_type = 6;
|
||||
}
|
||||
|
||||
// Route type for matching rules.
|
||||
// Extended by Higress
|
||||
enum RouteType {
|
||||
// HTTP route (default)
|
||||
HTTP = 0;
|
||||
|
||||
// GRPC route
|
||||
GRPC = 1;
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
|
||||
@@ -71,6 +71,11 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
routeType:
|
||||
enum:
|
||||
- HTTP
|
||||
- GRPC
|
||||
type: string
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
|
||||
@@ -504,11 +504,11 @@ var file_networking_v1_http_2_rpc_proto_rawDesc = []byte{
|
||||
0x69, 0x72, 0x65, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x22, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d,
|
||||
0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02,
|
||||
0x52, 0x09, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x47,
|
||||
0x72, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69,
|
||||
0x72, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61,
|
||||
0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -749,10 +749,10 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
|
||||
0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69,
|
||||
0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f,
|
||||
0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Submodule envoy/go-control-plane updated: 0ab56e249a...90eca02281
288
go.mod
288
go.mod
@@ -1,8 +1,6 @@
|
||||
module github.com/alibaba/higress/v2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
go 1.24.4
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -22,57 +20,61 @@ require (
|
||||
github.com/caddyserver/certmagic v0.21.3
|
||||
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
|
||||
github.com/dubbogo/gost v1.13.1
|
||||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/hashicorp/consul/api v1.21.0
|
||||
github.com/hashicorp/consul/api v1.32.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hudl/fargo v1.4.0
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
|
||||
github.com/onsi/gomega v1.27.10
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.33.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
istio.io/api v1.19.5-0.20231206014255-f55a2b1e931e
|
||||
golang.org/x/net v0.44.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9
|
||||
istio.io/client-go v1.27.1-0.20250820130622-12f6d11feb40
|
||||
istio.io/istio v0.0.0
|
||||
istio.io/pkg v0.0.0-20231221211216-7635388a563e
|
||||
k8s.io/api v0.28.3
|
||||
k8s.io/apiextensions-apiserver v0.28.3
|
||||
k8s.io/apimachinery v0.28.3
|
||||
k8s.io/cli-runtime v0.28.0
|
||||
k8s.io/client-go v0.28.3
|
||||
istio.io/pkg v0.0.0-20250718200944-0aab346caa39
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/cli-runtime v0.33.3
|
||||
k8s.io/client-go v0.34.1
|
||||
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
|
||||
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/gateway-api v0.8.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/gateway-api v1.4.0
|
||||
sigs.k8s.io/gateway-api-inference-extension v1.1.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/logging v1.8.1 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.4 // indirect
|
||||
cloud.google.com/go/logging v1.13.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/alecholmes/xfccparser v0.1.0 // indirect
|
||||
github.com/alecthomas/participle v0.4.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/alecholmes/xfccparser v0.4.0 // indirect
|
||||
github.com/alecthomas/participle/v2 v2.1.4 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
|
||||
@@ -94,177 +96,189 @@ require (
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/docker/cli v24.0.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 // indirect
|
||||
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.16.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-containerregistry v0.15.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.26 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.31 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
|
||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/openshift/api v0.0.0-20250507150912-7318813e48da // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/prometheus v0.45.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/prometheus v0.307.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.39.1-0.20230714155235-03b8c47770f2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.17.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/api v0.132.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/api v0.250.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/gcfg.v1 v1.2.3 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.0 // indirect
|
||||
istio.io/client-go v1.19.5-0.20231206015206-8cdf6a3b3cfd // indirect
|
||||
k8s.io/apiserver v0.28.3 // indirect
|
||||
k8s.io/component-base v0.28.3 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/kubectl v0.28.0 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.0 // indirect
|
||||
k8s.io/apiserver v0.34.1 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||
k8s.io/kubectl v0.33.3 // indirect
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
replace istio.io/api => ./external/api
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane => ./external/go-control-plane
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/contrib => ./external/go-control-plane/contrib
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/envoy => ./external/go-control-plane/envoy
|
||||
|
||||
replace istio.io/pkg => ./external/pkg
|
||||
|
||||
replace istio.io/client-go => ./external/client-go
|
||||
|
||||
@@ -71,6 +71,11 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
routeType:
|
||||
enum:
|
||||
- HTTP
|
||||
- GRPC
|
||||
type: string
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
|
||||
@@ -38,6 +38,14 @@ rules:
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
|
||||
# Gateway api inference extension
|
||||
- apiGroups: ["inference.networking.k8s.io"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
- apiGroups: ["inference.networking.x-k8s.io"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
|
||||
# Needed for multicluster secret reading, possibly ingress certs in the future
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
|
||||
@@ -78,6 +78,10 @@ spec:
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
- name: PILOT_ENABLE_ALPHA_GATEWAY_API
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
- name: ENABLE_GATEWAY_API_INFERENCE_EXTENSION
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.controller.env }}
|
||||
{{- range $key, $val := .Values.controller.env }}
|
||||
- name: {{ $key }}
|
||||
@@ -174,7 +178,15 @@ spec:
|
||||
- name: HOST_RDS_MERGE_SUBSET
|
||||
value: "{{ .Values.global.hostRDSMergeSubset }}"
|
||||
- name: PILOT_FILTER_GATEWAY_CLUSTER_CONFIG
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
value: "false"
|
||||
{{- else }}
|
||||
value: "{{ .Values.global.onlyPushRouteCluster }}"
|
||||
{{- end }}
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
- name: ENABLE_GATEWAY_API_INFERENCE_EXTENSION
|
||||
value: "true"
|
||||
{{- end }}
|
||||
- name: HIGRESS_CONTROLLER_SVC
|
||||
value: "127.0.0.1"
|
||||
- name: HIGRESS_CONTROLLER_PORT
|
||||
|
||||
@@ -5,6 +5,9 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4}}
|
||||
{{- with .Values.gateway.metrics.podMonitorSelector }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- .Values.gateway.annotations | toYaml | nindent 4 }}
|
||||
spec:
|
||||
|
||||
@@ -44,7 +44,9 @@ global:
|
||||
# -- If true, Higress Controller will monitor istio resources as well
|
||||
enableIstioAPI: true
|
||||
# -- If true, Higress Controller will monitor Gateway API resources as well
|
||||
enableGatewayAPI: false
|
||||
enableGatewayAPI: true
|
||||
# -- If true, enable Gateway API Inference Extension support
|
||||
enableInferenceExtension: false
|
||||
# -- Used to locate istiod.
|
||||
istioNamespace: istio-system
|
||||
# -- enable pod disruption budget for the control plane, which is used to
|
||||
@@ -528,6 +530,11 @@ gateway:
|
||||
metrics:
|
||||
# -- If true, create PodMonitor or VMPodScrape for gateway
|
||||
enabled: false
|
||||
# -- Selector for PodMonitor
|
||||
# When using monitoring.coreos.com/v1.PodMonitor, the selector must match
|
||||
# the label "release: kube-prome" is the default for kube-prometheus-stack
|
||||
podMonitorSelector:
|
||||
release: kube-prome
|
||||
# -- provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com
|
||||
provider: monitoring.coreos.com
|
||||
interval: ""
|
||||
|
||||
@@ -104,6 +104,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.metrics.interval | string | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | list | `[]` | for operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | list | `[]` | for monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.podMonitorSelector | object | `{"release":"kube-prome"}` | Selector for PodMonitor When using monitoring.coreos.com/v1.PodMonitor, the selector must match the label "release: kube-prome" is the default for kube-prometheus-stack |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | object | `{}` | some more raw podMetricsEndpoints spec |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | |
|
||||
@@ -160,9 +161,10 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.defaultResources | object | `{"requests":{"cpu":"10m"}}` | A minimal set of requested resources to applied to all deployments so that Horizontal Pod Autoscaler will be able to function (if set). Each component can overwrite these default values by adding its own resources block in the relevant section below and setting the desired resources values. |
|
||||
| global.defaultUpstreamConcurrencyThreshold | int | `10000` | |
|
||||
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
|
||||
| global.enableGatewayAPI | bool | `false` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableGatewayAPI | bool | `true` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableInferenceExtension | bool | `false` | If true, enable Gateway API Inference Extension support |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `false` | |
|
||||
| global.enablePluginServer | bool | `false` | |
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
## Higress 适用于 Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
|
||||
|
||||
通过 Istio 和 Envoy 的支持,Higress 实现了流量网关、微服务网关和安全网关三种架构的融合,从而极大地减少了部署、运维的成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
```console
|
||||
helm repo add higress.io https://higress.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
使用 Helm 安装名为 `higress` 的组件:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
```
|
||||
|
||||
## 卸载
|
||||
|
||||
删除名称为 higress 的安装:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令将删除与组件关联的所有 Kubernetes 组件并卸载该发行版。
|
||||
|
||||
## 参数
|
||||
|
||||
## Values
|
||||
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|----|------|---------|-------------|
|
||||
| clusterName | string | `""` | 集群名 |
|
||||
| controller.affinity | object | `{}` | 控制器亲和性设置 |
|
||||
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
|
||||
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
|
||||
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
|
||||
| controller.env | object | `{}` | 环境变量 |
|
||||
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 图像库的基础地址 |
|
||||
| controller.image | string | `"higress"` | 镜像名称 |
|
||||
| controller.imagePullSecrets | list | `[]` | 拉取秘钥列表 |
|
||||
| controller.labels | object | `{}` | 标签 |
|
||||
| controller.name | string | `"higress-controller"` | 控制器名称 |
|
||||
| controller.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| controller.podAnnotations | object | `{}` | Pod 注解 |
|
||||
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
|
||||
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
|
||||
| controller.ports[0].name | string | `"http"` | 端口名称 |
|
||||
| controller.ports[0].port | int | `8888` | 端口编号 |
|
||||
| controller.ports[0].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[0].targetPort | int | `8888` | 目标端口 |
|
||||
| controller.ports[1].name | string | `"http-solver"` | 端口名称 |
|
||||
| controller.ports[1].port | int | `8889` | 端口编号 |
|
||||
| controller.ports[1].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[1].targetPort | int | `8889` | 目标端口 |
|
||||
| controller.ports[2].name | string | `"grpc"` | 端口名称 |
|
||||
| controller.ports[2].port | int | `15051` | 端口编号 |
|
||||
| controller.ports[2].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[2].targetPort | int | `15051` | 目标端口 |
|
||||
| controller.probe.httpGet.path | string | `"/ready"` | 运行状况检查路径 |
|
||||
| controller.probe.httpGet.port | int | `8888` | 端口运行状态检查 |
|
||||
| controller.probe.initialDelaySeconds | int | `1` | 初始延迟秒数 |
|
||||
| controller.probe.periodSeconds | int | `3` | 健康检查间隔秒数 |
|
||||
| controller.probe.timeoutSeconds | int | `5` | 超时秒数 |
|
||||
| controller.rbac.create | bool | `true` | 是否创建 RBAC 相关资源 |
|
||||
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
|
||||
| controller.securityContext | object | `{}` | 安全上下文 |
|
||||
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
|
||||
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
|
||||
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
|
||||
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true,则从 fullname 模板生成名称 |
|
||||
| controller.tag | string | `""` | 标记 |
|
||||
| controller.tolerations | list | `[]` | 受容容忍度列表 |
|
||||
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
|
||||
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
|
||||
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
|
||||
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
|
||||
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
|
||||
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小(KB) |
|
||||
| downstream.routeTimeout | int | `0` | 路由超时时间 |
|
||||
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
|
||||
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
|
||||
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
|
||||
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
|
||||
| gateway.env | object | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
|
||||
| gateway.httpPort | int | `80` | HTTP 服务端口 |
|
||||
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | 部署类型 |
|
||||
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
|
||||
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
|
||||
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
|
||||
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
|
||||
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
|
||||
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
|
||||
| gateway.name | string | `"higress-gateway"` | 网关名称 |
|
||||
| gateway.networkGateway | string | `""` | 网络网关指定 |
|
||||
| gateway.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
|
||||
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
|
||||
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
|
||||
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
|
||||
| gateway.revision | string | `""` | 网关所属版本声明 |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
|
||||
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
|
||||
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
|
||||
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
|
||||
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
|
||||
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
|
||||
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
|
||||
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
|
||||
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
|
||||
| gateway.service.ports[0].name | string | `"http2"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[0].port | int | `80` | 服务端口 |
|
||||
| gateway.service.ports[0].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[0].targetPort | int | `80` | 靶向端口 |
|
||||
| gateway.service.ports[1].name | string | `"https"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[1].port | int | `443` | 服务端口 |
|
||||
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
|
||||
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
|
||||
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
由于内容较多,其他参数可以参考完整表。
|
||||
|
||||
331
hgctl/go.mod
331
hgctl/go.mod
@@ -1,8 +1,6 @@
|
||||
module github.com/alibaba/higress/hgctl
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
go 1.24.4
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -22,11 +20,11 @@ require (
|
||||
github.com/alibaba/higress/v2 v2.0.0-00010101000000-000000000000
|
||||
github.com/braydonk/yaml v0.7.0
|
||||
github.com/compose-spec/compose-go v1.17.0
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/cli v28.1.1+incompatible
|
||||
github.com/docker/compose/v2 v2.23.3
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/evanphx/json-patch/v5 v5.7.0
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/evanphx/json-patch/v5 v5.9.11
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/google/yamlfmt v0.10.0
|
||||
github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950
|
||||
@@ -36,190 +34,214 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.12.2
|
||||
helm.sh/helm/v3 v3.18.5
|
||||
istio.io/istio v0.0.0
|
||||
k8s.io/api v0.28.3
|
||||
k8s.io/apimachinery v0.28.3
|
||||
k8s.io/cli-runtime v0.28.0
|
||||
k8s.io/client-go v0.28.3
|
||||
k8s.io/kubectl v0.28.0
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/cli-runtime v0.33.3
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/kubectl v0.33.3
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/chzyer/readline v1.5.0 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
sigs.k8s.io/gateway-api-inference-extension v1.1.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/RageCage64/multilinediff v0.2.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.7.7 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
||||
github.com/docker/buildx v0.12.0 // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsevents v0.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.118.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.16.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/imdario/mergo v1.0.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/yaml v0.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.26 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.31 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/buildkit v0.13.0-beta1.0.20231023114302-d5c1d785b042 // indirect
|
||||
github.com/moby/buildkit v0.21.1 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.0 // indirect
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.1.9 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/prometheus v0.45.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/rubenv/sql-migrate v1.3.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/prometheus v0.307.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
@@ -234,65 +256,63 @@ require (
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect
|
||||
github.com/zmap/zlint/v3 v3.6.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.39.1-0.20230714155235-03b8c47770f2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.17.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
istio.io/api v1.19.5-0.20231206014255-f55a2b1e931e // indirect
|
||||
istio.io/client-go v1.19.5-0.20231206015206-8cdf6a3b3cfd // indirect
|
||||
k8s.io/apiextensions-apiserver v0.28.3 // indirect
|
||||
k8s.io/apiserver v0.28.3 // indirect
|
||||
k8s.io/component-base v0.28.3 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
oras.land/oras-go v1.2.3 // indirect
|
||||
sigs.k8s.io/gateway-api v0.8.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
|
||||
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9 // indirect
|
||||
istio.io/client-go v1.27.1-0.20250820130622-12f6d11feb40 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/apiserver v0.34.1 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
|
||||
sigs.k8s.io/gateway-api v1.4.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c // indirect
|
||||
)
|
||||
|
||||
replace istio.io/api => ../external/api
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane => ../external/go-control-plane
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/contrib => ../external/go-control-plane/contrib
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/envoy => ../external/go-control-plane/envoy
|
||||
|
||||
replace istio.io/pkg => ../external/pkg
|
||||
|
||||
replace istio.io/client-go => ../external/client-go
|
||||
@@ -303,6 +323,7 @@ replace github.com/alibaba/higress => ../
|
||||
|
||||
replace (
|
||||
github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
|
||||
github.com/distribution/distribution/v3 => github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493
|
||||
github.com/docker/buildx => github.com/docker/buildx v0.11.2
|
||||
github.com/docker/cli => github.com/docker/cli v24.0.6+incompatible
|
||||
github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.20.2
|
||||
|
||||
2117
hgctl/go.sum
2117
hgctl/go.sum
File diff suppressed because it is too large
Load Diff
770
hgctl/pkg/agent/README.md
Normal file
770
hgctl/pkg/agent/README.md
Normal file
@@ -0,0 +1,770 @@
|
||||
// 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.
|
||||
|
||||
# Agent Module
|
||||
|
||||
`pkg/agent` 是 hgctl 中用于 Agent 生命周期管理的核心模块,提供了从创建、配置、部署到发布的完整工作流。
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [架构设计](#架构设计)
|
||||
- [核心功能](#核心功能)
|
||||
- [主要组件](#主要组件)
|
||||
- [使用方式](#使用方式)
|
||||
- [配置管理](#配置管理)
|
||||
- [部署方式](#部署方式)
|
||||
- [集成说明](#集成说明)
|
||||
|
||||
## 概述
|
||||
|
||||
Agent 模块提供了一套完整的 AI Agent 开发和部署解决方案,支持:
|
||||
|
||||
- **多种 Agentic Core**:集成 Claude Code 和 Qodercli
|
||||
- **本地和云端部署**:支持本地运行和 AgentRun (阿里云函数计算)
|
||||
- **MCP Server 管理**:支持 HTTP 和 OpenAPI 类型的 MCP Server
|
||||
- **Higress 集成**:自动发布 Agent API 到 Higress 网关
|
||||
- **Himarket 发布**:支持将 Agent 发布到 Himarket 市场
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
pkg/agent/
|
||||
├── agent.go # CLI 命令入口和主要业务逻辑
|
||||
├── core.go # Agentic Core (Claude/Qodercli) 封装
|
||||
├── new.go # Agent 创建流程
|
||||
├── deploy.go # Agent 部署处理(本地/云端)
|
||||
├── mcp.go # MCP Server 管理
|
||||
├── config.go # 配置管理和初始化
|
||||
├── base.go # 基础函数和环境检查
|
||||
├── types.go # 类型定义
|
||||
├── utils.go # 工具函数
|
||||
├── common/ # 通用类型定义
|
||||
│ └── base.go # ProductType 等常量
|
||||
├── services/ # 外部服务客户端
|
||||
│ ├── client.go # HTTP 客户端封装
|
||||
│ ├── service.go # Higress/Himarket API 封装
|
||||
│ └── utils.go # 服务工具函数
|
||||
└── prompt/ # Prompt 模板和指导
|
||||
├── base.go # Agent 开发指南
|
||||
└── agent_guide.md
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. Agent 创建 (new.go)
|
||||
|
||||
提供两种 Agent 创建方式:
|
||||
|
||||
#### 1.1 交互式创建
|
||||
通过命令行交互式问答,逐步配置:
|
||||
- Agent 名称和描述
|
||||
- 系统 Prompt(支持直接输入、从文件导入、LLM 生成)
|
||||
- AI 模型配置(DashScope、OpenAI、Anthropic 等)
|
||||
- 工具集选择(AgentScope 内置工具)
|
||||
- MCP Server 配置
|
||||
- 部署设置
|
||||
|
||||
#### 1.2 从 Core 导入
|
||||
从 Agentic Core 的 subagent 目录导入已有的 Agent 配置。
|
||||
|
||||
**关键代码位置**:
|
||||
- `createAgentCmd()` (new.go:99): 创建命令定义
|
||||
- `getAgentConfig()` (utils.go:289): 获取 Agent 配置
|
||||
- `createAgentTemplate()` (new.go:205): 生成 Agent 模板文件
|
||||
|
||||
### 2. Agent 部署 (deploy.go)
|
||||
|
||||
支持两种部署模式:
|
||||
|
||||
#### 2.1 本地部署 (Local)
|
||||
- 基于 AgentScope Runtime
|
||||
- 自动管理 Python 虚拟环境
|
||||
- 依赖管理:`agentscope`, `agentscope-runtime==1.0.0`
|
||||
- 默认端口:8090
|
||||
|
||||
#### 2.2 云端部署 (AgentRun)
|
||||
- 部署到阿里云函数计算
|
||||
- 使用 Serverless Devs (s工具)
|
||||
- 自动构建和部署
|
||||
- 需要配置阿里云 Access Key
|
||||
|
||||
**关键代码位置**:
|
||||
- `DeployHandler` (deploy.go:35): 部署处理器
|
||||
- `HandleLocal()` (deploy.go:350): 本地部署逻辑
|
||||
- `HandleAgentRun()` (deploy.go:305): AgentRun 部署逻辑
|
||||
|
||||
### 3. MCP Server 管理 (mcp.go)
|
||||
|
||||
支持两种类型的 MCP Server:
|
||||
|
||||
#### 3.1 HTTP MCP Server
|
||||
直接通过 HTTP URL 添加:
|
||||
```bash
|
||||
hgctl mcp add [name] [url] --type http
|
||||
```
|
||||
|
||||
#### 3.2 OpenAPI MCP Server
|
||||
从 OpenAPI 规范文件创建:
|
||||
```bash
|
||||
hgctl mcp add [name] [spec-file] --type openapi
|
||||
```
|
||||
|
||||
功能特性:
|
||||
- 自动解析 OpenAPI 规范
|
||||
- 转换为 MCP Server 配置
|
||||
- 自动添加到 Agentic Core
|
||||
- 可选发布到 Higress
|
||||
- 支持发布到 Himarket 市场
|
||||
|
||||
**关键代码位置**:
|
||||
- `handleAddMCP()` (mcp.go:183): MCP 添加主逻辑
|
||||
- `publishMCPToHigress()` (mcp.go:228): 发布到 Higress
|
||||
- `parseOpenapi2MCP()` (utils.go:79): OpenAPI 解析
|
||||
|
||||
### 4. Agentic Core 集成 (core.go)
|
||||
|
||||
封装了 Agentic Core(Claude Code/Qodercli)的交互:
|
||||
|
||||
#### 支持的 Core 类型
|
||||
```go
|
||||
const (
|
||||
CORE_CLAUDE CoreType = "claude"
|
||||
CORE_QODERCLI CoreType = "qodercli"
|
||||
)
|
||||
```
|
||||
|
||||
#### 核心功能
|
||||
- **Setup()**: 初始化环境和插件
|
||||
- **Start()**: 启动交互式窗口
|
||||
- **AddMCPServer()**: 添加 MCP Server 到 Core
|
||||
- **ImproveNewAgent()**: 在特定 Agent 目录运行 Core 进行改进
|
||||
|
||||
**关键代码位置**:
|
||||
- `AgenticCore` (core.go:32): Core 封装结构
|
||||
- `Setup()` (core.go:108): 环境初始化
|
||||
- `addHigressAPIMCP()` (core.go:161): 自动添加 Higress API MCP
|
||||
|
||||
### 5. Higress 集成
|
||||
|
||||
自动将 Agent API 发布到 Higress 网关:
|
||||
|
||||
#### 支持的 API 类型
|
||||
```go
|
||||
const (
|
||||
A2A = "a2a" // Agent-to-Agent
|
||||
REST = "restful" // RESTful API
|
||||
MODEL = "model" // AI Model API
|
||||
)
|
||||
```
|
||||
|
||||
#### 发布流程
|
||||
1. 创建 AI Provider Service
|
||||
2. 创建 AI Route
|
||||
3. 配置服务源和路由
|
||||
|
||||
**关键代码位置**:
|
||||
- `publishAgentAPIToHigress()` (agent.go:123): 发布逻辑
|
||||
- `services/service.go`: Higress API 封装
|
||||
|
||||
### 6. Himarket 集成
|
||||
|
||||
支持将 Agent 发布到 Himarket 市场:
|
||||
|
||||
#### 产品类型
|
||||
```go
|
||||
const (
|
||||
MCP_SERVER ProductType = "MCP_SERVER"
|
||||
MODEL_API ProductType = "MODEL_API"
|
||||
REST_API ProductType = "REST_API"
|
||||
AGENT_API ProductType = "AGENT_API"
|
||||
)
|
||||
```
|
||||
|
||||
**关键代码位置**:
|
||||
- `publishAPIToHimarket()` (base.go:128): 发布到市场
|
||||
- `services/service.go`: Himarket API 封装
|
||||
|
||||
## 主要组件
|
||||
|
||||
### AgentConfig 结构
|
||||
|
||||
Agent 的核心配置结构:
|
||||
|
||||
```go
|
||||
type AgentConfig struct {
|
||||
AppName string // 应用名称
|
||||
AppDescription string // 应用描述
|
||||
AgentName string // Agent 名称
|
||||
AvailableTools []string // 可用工具列表
|
||||
SysPromptPath string // 系统 Prompt 路径
|
||||
ChatModel string // 使用的模型
|
||||
Provider string // 模型提供商
|
||||
APIKeyEnvVar string // API Key 环境变量
|
||||
DeploymentPort int // 部署端口
|
||||
HostBinding string // 主机绑定
|
||||
EnableStreaming bool // 是否启用流式响应
|
||||
EnableThinking bool // 是否启用思考过程
|
||||
MCPServers []MCPServerConfig // MCP Server 配置
|
||||
Type DeployType // 部署类型
|
||||
ServerlessCfg ServerlessConfig // Serverless 配置
|
||||
}
|
||||
```
|
||||
|
||||
### 环境检查 (base.go)
|
||||
|
||||
`EnvProvisioner` 负责检查和安装必要的环境:
|
||||
|
||||
#### Node.js 检查
|
||||
- 最低版本要求:Node.js 18+
|
||||
- 支持自动安装(通过 fnm)
|
||||
|
||||
#### Agentic Core 检查
|
||||
- 检查 claude 或 qodercli 是否安装
|
||||
- 支持自动安装(通过 npm)
|
||||
|
||||
**关键代码位置**:
|
||||
- `EnvProvisioner.check()` (base.go:221): 环境检查
|
||||
- `promptNodeInstall()` (base.go:259): Node.js 安装引导
|
||||
- `promptAgentInstall()` (base.go:401): Core 安装引导
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 命令结构
|
||||
|
||||
```bash
|
||||
hgctl agent # 启动交互式 Agent 窗口
|
||||
hgctl agent new # 创建新 Agent
|
||||
hgctl agent deploy [name] # 部署 Agent
|
||||
hgctl agent add [name] [url] # 添加 Agent API 到 Higress
|
||||
hgctl mcp add [name] [url] # 添加 MCP Server
|
||||
```
|
||||
|
||||
### 创建 Agent
|
||||
|
||||
#### 本地部署的 Agent
|
||||
```bash
|
||||
hgctl agent new
|
||||
```
|
||||
|
||||
交互式选择:
|
||||
1. 创建方式:step by step / 从 Core 导入
|
||||
2. Agent 名称和描述
|
||||
3. 系统 Prompt 设置
|
||||
4. 模型提供商和模型选择
|
||||
5. 工具选择
|
||||
6. MCP Server 配置
|
||||
7. 部署设置
|
||||
|
||||
#### AgentRun 部署的 Agent
|
||||
```bash
|
||||
hgctl agent new --agent-run
|
||||
```
|
||||
|
||||
额外配置:
|
||||
- Resource Name
|
||||
- Region
|
||||
- Disk Size
|
||||
- Timeout
|
||||
|
||||
### 部署 Agent
|
||||
|
||||
#### 部署到本地
|
||||
```bash
|
||||
hgctl agent deploy my-agent
|
||||
```
|
||||
|
||||
自动处理:
|
||||
- Python 环境检查
|
||||
- 依赖安装
|
||||
- 启动 Agent 服务
|
||||
|
||||
#### 部署到 AgentRun
|
||||
```bash
|
||||
hgctl agent deploy my-agent
|
||||
```
|
||||
|
||||
要求:
|
||||
- 已配置阿里云 Access Key
|
||||
- 已安装 Docker
|
||||
- 已安装 Serverless Devs CLI
|
||||
|
||||
### 添加 MCP Server
|
||||
|
||||
#### 添加 HTTP MCP Server
|
||||
```bash
|
||||
hgctl mcp add my-mcp http://localhost:8080/mcp \
|
||||
--type http \
|
||||
--transport streamable \
|
||||
-e API_KEY=secret \
|
||||
-H "Authorization: Bearer token"
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--type`: MCP 类型(http/openapi)
|
||||
- `--transport`: 传输类型(streamable/sse)
|
||||
- `-e`: 环境变量
|
||||
- `-H`: HTTP 头部
|
||||
|
||||
#### 从 OpenAPI 创建 MCP Server
|
||||
```bash
|
||||
hgctl mcp add swagger-mcp ./openapi.yaml \
|
||||
--type openapi
|
||||
```
|
||||
|
||||
自动完成:
|
||||
1. 解析 OpenAPI 规范
|
||||
2. 转换为 MCP 配置
|
||||
3. 发布到 Higress
|
||||
4. 添加到 Agentic Core
|
||||
|
||||
### 发布到 Higress 和 Himarket
|
||||
|
||||
```bash
|
||||
hgctl agent add my-agent http://my-agent.com \
|
||||
--type model \
|
||||
--as-product \
|
||||
--higress-console-url http://console.higress.io \
|
||||
--higress-console-user admin \
|
||||
--higress-console-password password \
|
||||
--himarket-admin-url http://himarket.io \
|
||||
--himarket-admin-user admin \
|
||||
--himarket-admin-password password
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 配置文件
|
||||
|
||||
配置文件位置:`~/.hgctl`
|
||||
|
||||
```json
|
||||
{
|
||||
"hgctl-agent-core": "claude",
|
||||
"agent-chat-model": "qwen-plus",
|
||||
"agent-model-provider": "DashScope",
|
||||
"higress-console-url": "http://127.0.0.1:8080",
|
||||
"higress-console-user": "admin",
|
||||
"higress-console-password": "admin",
|
||||
"higress-gateway-url": "http://127.0.0.1:80",
|
||||
"himarket-admin-url": "",
|
||||
"himarket-admin-user": "",
|
||||
"himarket-admin-password": "",
|
||||
"agentrun-model-name": "",
|
||||
"agentrun-region": "cn-hangzhou"
|
||||
}
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `hgctl-agent-core` | Agentic Core 类型 | `qodercli` |
|
||||
| `agent-chat-model` | 默认聊天模型 | - |
|
||||
| `agent-model-provider` | 默认模型提供商 | - |
|
||||
| `higress-console-url` | Higress 控制台地址 | - |
|
||||
| `higress-console-user` | Higress 用户名 | - |
|
||||
| `higress-console-password` | Higress 密码 | - |
|
||||
| `higress-gateway-url` | Higress 网关地址 | - |
|
||||
| `himarket-admin-url` | Himarket 管理地址 | - |
|
||||
| `himarket-admin-user` | Himarket 用户名 | - |
|
||||
| `himarket-admin-password` | Himarket 密码 | - |
|
||||
| `agentrun-model-name` | AgentRun 模型名 | - |
|
||||
| `agentrun-region` | AgentRun 区域 | `cn-hangzhou` |
|
||||
|
||||
### 环境变量
|
||||
|
||||
配置也可以通过环境变量设置(自动转换,用下划线替换连字符):
|
||||
|
||||
```bash
|
||||
export HIGRESS_CONSOLE_URL=http://127.0.0.1:8080
|
||||
export HIGRESS_CONSOLE_USER=admin
|
||||
export HIGRESS_CONSOLE_PASSWORD=admin
|
||||
```
|
||||
|
||||
**代码位置**: `config.go:100` - `InitConfig()`
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 本地部署 (Local)
|
||||
|
||||
#### 技术栈
|
||||
- **Runtime**: AgentScope Runtime
|
||||
- **Python**: 3.12+
|
||||
- **依赖**:
|
||||
- `agentscope`
|
||||
- `agentscope-runtime==1.0.0`
|
||||
|
||||
#### 部署流程
|
||||
1. 检查 Python 环境
|
||||
2. 创建/激活虚拟环境 (`~/.hgctl/.venv`)
|
||||
3. 安装依赖
|
||||
4. 启动 Agent 服务
|
||||
|
||||
#### 生成的文件
|
||||
```
|
||||
~/.hgctl/agents/{agent-name}/
|
||||
├── as_runtime_main.py # AgentScope Runtime 入口
|
||||
├── agent.py # Agent 类定义
|
||||
├── toolkit.py # 工具集
|
||||
├── prompt.md # 系统 Prompt
|
||||
├── CLAUDE.md # Claude 开发指南(如果使用 Claude)
|
||||
└── AGENTS.md # Qoder 开发指南(如果使用 Qodercli)
|
||||
```
|
||||
|
||||
**代码位置**: `deploy.go:350` - `HandleLocal()`
|
||||
|
||||
### 云端部署 (AgentRun)
|
||||
|
||||
#### 技术栈
|
||||
- **平台**: 阿里云函数计算 (Function Compute)
|
||||
- **SDK**: agentrun-sdk-python
|
||||
- **工具**: Serverless Devs CLI
|
||||
|
||||
#### 部署流程
|
||||
1. 检查环境(Docker、Serverless Devs)
|
||||
2. 检查/配置 Access Key
|
||||
3. 执行 `s build`
|
||||
4. 执行 `s deploy`
|
||||
|
||||
#### 生成的文件
|
||||
```
|
||||
~/.hgctl/agents/{agent-name}/
|
||||
├── agentrun_main.py # AgentRun 入口
|
||||
├── agent.py # Agent 类定义
|
||||
├── toolkit.py # 工具集
|
||||
├── prompt.md # 系统 Prompt
|
||||
├── requirements.txt # Python 依赖
|
||||
└── s.yaml # Serverless Devs 配置
|
||||
```
|
||||
|
||||
#### s.yaml 配置
|
||||
```yaml
|
||||
edition: 3.0.0
|
||||
name: {agent-name}
|
||||
access: hgctl-credential
|
||||
|
||||
resources:
|
||||
fc-agentrun-demo:
|
||||
component: fc3
|
||||
props:
|
||||
region: {region}
|
||||
description: {description}
|
||||
runtime: python3.12
|
||||
code: ./
|
||||
handler: agentrun_main.main
|
||||
timeout: {timeout}
|
||||
diskSize: {disk-size}
|
||||
environmentVariables:
|
||||
MODEL_NAME: {model-name}
|
||||
{api-key-env}: {api-key}
|
||||
customRuntimeConfig:
|
||||
command:
|
||||
- python3
|
||||
args:
|
||||
- agentrun_main.py
|
||||
port: {port}
|
||||
```
|
||||
|
||||
**代码位置**: `deploy.go:305` - `HandleAgentRun()`
|
||||
|
||||
## 集成说明
|
||||
|
||||
### Higress 集成
|
||||
|
||||
#### Service Source 创建
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildServiceBodyAndSrv(name, rawURL string) (map[string]interface{}, string, int, error)
|
||||
```
|
||||
|
||||
创建服务源:
|
||||
- 解析 URL
|
||||
- 提取域名、端口
|
||||
- 生成服务名称
|
||||
|
||||
#### AI Provider 和 Route 创建
|
||||
|
||||
对于 MODEL 类型的 Agent:
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildAIProviderServiceBody(name, url string) map[string]interface{}
|
||||
func BuildAddAIRouteBody(name, url string) map[string]interface{}
|
||||
```
|
||||
|
||||
#### MCP Server 创建
|
||||
|
||||
支持两种类型:
|
||||
- **DIRECT_ROUTE**: 直接路由到 MCP Server URL
|
||||
- **OPEN_API**: 基于 OpenAPI 规范的工具配置
|
||||
|
||||
### Himarket 集成
|
||||
|
||||
#### API Product 创建
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildAPIProductBody(name, desc string, typ string) map[string]interface{}
|
||||
```
|
||||
|
||||
#### Product Reference
|
||||
```go
|
||||
func BuildRefModelAPIProductBody(gatewayId, productId, routeName string) map[string]interface{}
|
||||
func BuildRefMCPAPIProductBody(gatewayId, productId, mcpServerName string) map[string]interface{}
|
||||
```
|
||||
|
||||
### Agentic Core 集成
|
||||
|
||||
#### 初始化流程
|
||||
1. 提取 manifest 文件到 `~/.hgctl/`
|
||||
2. 提取 Core 相关文件到 `~/.claude/` 或 `~/.qoder/`
|
||||
3. 添加预定义的 MCP Server
|
||||
4. 自动配置 Higress API MCP Server
|
||||
|
||||
#### MCP Server 添加
|
||||
```bash
|
||||
{core} mcp add --transport {transport} {name} {url} \
|
||||
--scope {scope} \
|
||||
-e {env} \
|
||||
-H {header}
|
||||
```
|
||||
|
||||
**代码位置**: `core.go:236` - `AddMCPServer()`
|
||||
|
||||
## 类型定义 (types.go)
|
||||
|
||||
### API 请求/响应类型
|
||||
|
||||
用于与 AI 模型 API 交互:
|
||||
|
||||
```go
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty"`
|
||||
PresencePenalty float64 `json:"presence_penalty"`
|
||||
Stream bool `json:"stream"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Topp int32 `json:"top_p"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Object string `json:"object"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI 相关类型
|
||||
|
||||
用于 OpenAPI 规范解析:
|
||||
|
||||
```go
|
||||
type API struct {
|
||||
OpenAPI string `yaml:"openapi"`
|
||||
Info Info `yaml:"info"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
Paths Paths `yaml:"paths"`
|
||||
Components Components `yaml:"components"`
|
||||
}
|
||||
```
|
||||
|
||||
## Services 子包
|
||||
|
||||
### HigressClient
|
||||
|
||||
Higress API 客户端:
|
||||
|
||||
```go
|
||||
type HigressClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
```
|
||||
|
||||
**主要方法**:
|
||||
- `Get(path string) ([]byte, error)`
|
||||
- `Post(path string, body interface{}) ([]byte, error)`
|
||||
- `Put(path string, body interface{}) ([]byte, error)`
|
||||
|
||||
### HimarketClient
|
||||
|
||||
Himarket API 客户端:
|
||||
|
||||
```go
|
||||
type HimarketClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
```
|
||||
|
||||
**主要方法**:
|
||||
- `GetDevMCPServerProduct() (map[string]string, error)`
|
||||
- `GetDevModelProduct() (map[string]string, error)`
|
||||
|
||||
## 工具函数 (utils.go)
|
||||
|
||||
### Kubernetes 相关
|
||||
|
||||
- `GetHigressGatewayServiceIP()`: 获取 Higress Gateway Service IP
|
||||
- `extractServiceIP()`: 从 Service 提取 IP
|
||||
- `getConsoleCredentials()`: 从 K8s Secret 获取控制台凭证
|
||||
|
||||
### Agent 配置
|
||||
|
||||
- `getAgentConfig()`: 交互式获取 Agent 配置
|
||||
- `createAgentStepByStep()`: 逐步创建 Agent
|
||||
- `importAgentFromCore()`: 从 Core 导入 Agent
|
||||
|
||||
### Query 函数
|
||||
|
||||
一系列用于交互式配置查询的函数:
|
||||
- `queryAgentSysPrompt()`: 查询系统 Prompt
|
||||
- `queryAgentTools()`: 查询工具选择
|
||||
- `queryAgentModel()`: 查询模型配置
|
||||
- `queryAgentMCP()`: 查询 MCP Server
|
||||
- `queryDeploySettings()`: 查询部署设置
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 开发流程
|
||||
|
||||
```bash
|
||||
# 1. 创建 Agent
|
||||
hgctl agent new
|
||||
|
||||
# 2. 使用 Core 改进和测试
|
||||
# 选择 "Improve and test it using agentic core"
|
||||
|
||||
# 3. 部署 Agent
|
||||
hgctl agent deploy my-agent
|
||||
|
||||
# 4. 添加到 Higress
|
||||
hgctl agent add my-agent http://localhost:8090 --type model
|
||||
|
||||
# 5. (可选)发布到 Himarket
|
||||
hgctl agent add my-agent http://localhost:8090 --type model --as-product
|
||||
```
|
||||
|
||||
### 2. MCP Server 管理
|
||||
|
||||
```bash
|
||||
# 添加 HTTP MCP Server
|
||||
hgctl mcp add my-mcp http://mcp-server:8080/mcp
|
||||
|
||||
# 从 OpenAPI 创建 MCP Server
|
||||
hgctl mcp add swagger-mcp ./openapi.yaml --type openapi
|
||||
|
||||
# 添加到 Higress 和 Himarket
|
||||
hgctl mcp add my-mcp http://mcp-server:8080/mcp --as-product
|
||||
```
|
||||
|
||||
### 3. 配置管理
|
||||
|
||||
```bash
|
||||
# 使用配置文件
|
||||
vim ~/.hgctl
|
||||
|
||||
# 或使用环境变量
|
||||
export HIGRESS_CONSOLE_URL=http://127.0.0.1:8080
|
||||
export HIGRESS_CONSOLE_USER=admin
|
||||
export HIGRESS_CONSOLE_PASSWORD=admin
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **Node.js 未安装**
|
||||
- 自动提示安装选项
|
||||
- 支持自动安装(fnm)
|
||||
|
||||
2. **Agentic Core 未安装**
|
||||
- 自动提示安装选项
|
||||
- 支持自动安装(npm)
|
||||
|
||||
3. **Python 环境问题**
|
||||
- 自动创建虚拟环境
|
||||
- 自动安装依赖
|
||||
|
||||
4. **Kubernetes 连接问题**
|
||||
- 提供手动输入 kubeconfig 选项
|
||||
- 支持自定义 namespace
|
||||
|
||||
5. **Higress/Himarket 认证失败**
|
||||
- 检查配置文件
|
||||
- 检查环境变量
|
||||
- 尝试从 K8s Secret 自动获取
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 添加新的 Agentic Core
|
||||
|
||||
1. 在 `config.go` 中添加新的 CoreType
|
||||
2. 在 `core.go` 中实现相应的方法
|
||||
3. 更新 `EnvProvisioner` 支持新的安装方式
|
||||
|
||||
### 添加新的部署类型
|
||||
|
||||
1. 在 `deploy.go` 中添加新的 DeployType
|
||||
2. 实现相应的部署处理方法
|
||||
3. 更新模板生成逻辑
|
||||
|
||||
### 添加新的 API 类型
|
||||
|
||||
1. 在 `agent.go` 中添加新的 API Type 常量
|
||||
2. 在 `publishAgentAPIToHigress()` 中添加处理逻辑
|
||||
3. 在 `services/utils.go` 中添加相应的构建函数
|
||||
|
||||
## 依赖说明
|
||||
|
||||
### Go 依赖
|
||||
- `github.com/spf13/cobra`: CLI 框架
|
||||
- `github.com/spf13/viper`: 配置管理
|
||||
- `github.com/AlecAivazis/survey/v2`: 交互式问答
|
||||
- `github.com/fatih/color`: 终端颜色输出
|
||||
- `k8s.io/client-go`: Kubernetes 客户端
|
||||
|
||||
### 外部工具
|
||||
- **Node.js 18+**: Agentic Core 运行环境
|
||||
- **Claude Code / Qodercli**: Agentic Core
|
||||
- **Python 3.12+**: Agent Runtime
|
||||
- **Docker**: AgentRun 部署
|
||||
- **Serverless Devs CLI**: AgentRun 部署工具
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [AgentScope 文档](https://modelscope.github.io/agentscope/)
|
||||
- [Claude Code 文档](https://docs.claude.com/en/docs/claude-code/setup)
|
||||
- [Qoder 文档](https://docs.qoder.com/zh/cli/quick-start)
|
||||
- [Serverless Devs 文档](https://serverless-devs.com/docs/user-guide/install)
|
||||
- [Higress 文档](https://higress.io/)
|
||||
- [AgentRun 文档](https://github.com/Serverless-Devs/agentrun-sdk-python)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
@@ -15,32 +15,161 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
// API Type
|
||||
const (
|
||||
A2A = "a2a"
|
||||
REST = "restful"
|
||||
MODEL = "model"
|
||||
)
|
||||
|
||||
func NewAgentCmd() *cobra.Command {
|
||||
agentCmd := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "start the interactive agent window",
|
||||
Short: "Start the interactive agent window",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(handleAgentInvoke(cmd.OutOrStdout()))
|
||||
cmdutil.CheckErr(invokeAgentCore(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
agentCmd.AddCommand(createAgentCmd())
|
||||
agentCmd.AddCommand(deployAgentCmd())
|
||||
agentCmd.AddCommand(newAgentAddCmd())
|
||||
|
||||
return agentCmd
|
||||
}
|
||||
|
||||
func handleAgentInvoke(w io.Writer) error {
|
||||
|
||||
return getAgent().Start()
|
||||
func invokeAgentCore(w io.Writer) error {
|
||||
core, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get core: %s", err)
|
||||
}
|
||||
return core.Start()
|
||||
}
|
||||
|
||||
// Sub-Agent1:
|
||||
// 1. Parse the url provided by user to MCP server configuration.
|
||||
// 2. Publish the parsed MCP Server to Higress
|
||||
func addPrequisiteSubAgent() error {
|
||||
type AgentAddArg struct {
|
||||
HigressConsoleAuthArg
|
||||
HimarketAdminAuthArg
|
||||
|
||||
name string
|
||||
url string
|
||||
typ string
|
||||
scope string
|
||||
|
||||
asProduct bool
|
||||
noPublish bool
|
||||
}
|
||||
|
||||
func newAgentAddCmd() *cobra.Command {
|
||||
arg := &AgentAddArg{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [url]",
|
||||
Short: "add agent to local interactive window and publish it to higress (optional)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
arg.name = args[0]
|
||||
arg.url = args[1]
|
||||
|
||||
resolveHigressConsoleAuth(&arg.HigressConsoleAuthArg)
|
||||
resolveHimarketAdminAuth(&arg.HimarketAdminAuthArg)
|
||||
cmdutil.CheckErr(handleAddAgent(cmd.OutOrStdout(), *arg))
|
||||
},
|
||||
Args: cobra.ExactArgs(2),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&arg.typ, "type", "t", MODEL, "Determine the agent's API type (a2a, model, restful) default is model")
|
||||
cmd.PersistentFlags().StringVarP(&arg.scope, "scope", "s", "project", `Configuration scope (project or global)`)
|
||||
cmd.PersistentFlags().BoolVar(&arg.noPublish, "no-publish", false, "If it's set then the agent API will not be plubished to Higress")
|
||||
cmd.PersistentFlags().BoolVar(&arg.asProduct, "as-product", false, "If it's set then the agent API will be published to Himarket (no-publish must be false)")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, &arg.HigressConsoleAuthArg)
|
||||
addHimarketAdminAuthFlag(cmd, &arg.HimarketAdminAuthArg)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func handleAddAgent(writer io.Writer, arg AgentAddArg) error {
|
||||
if err := validateArg(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !arg.noPublish {
|
||||
if err := publishAgentAPIToHigress(arg); err != nil {
|
||||
fmt.Printf("failed to publish agent api to higress: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Agent %s is published to Higress successfully\n", arg.name)
|
||||
|
||||
if arg.asProduct {
|
||||
if err := publishAPIToHimarket(arg.typ, arg.name, arg.HimarketAdminAuthArg); err != nil {
|
||||
fmt.Println("failed to publish it to himarket, please do it mannually")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Agent %s is published to Himarket successfully\n", arg.name)
|
||||
}
|
||||
// TODO: pop up higress window
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAgentAPIToHigress(arg AgentAddArg) error {
|
||||
client := services.NewHigressClient(arg.hgURL, arg.hgUser, arg.hgPassword)
|
||||
|
||||
switch arg.typ {
|
||||
case A2A:
|
||||
case MODEL:
|
||||
// add ai service
|
||||
body := services.BuildAIProviderServiceBody(arg.name, arg.url)
|
||||
// Debug
|
||||
// fmt.Printf("services: body: %v\n", body)
|
||||
if resp, err := services.HandleAddAIProviderService(client, body); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
// add ai route
|
||||
body = services.BuildAddAIRouteBody(arg.name, arg.url)
|
||||
// fmt.Printf("Route body: %v\n", body)
|
||||
if res, err := services.HandleAddAIRoute(client, body); err != nil {
|
||||
fmt.Println(string(res))
|
||||
return err
|
||||
}
|
||||
|
||||
case REST:
|
||||
srvName := fmt.Sprintf("agent-%s", arg.name)
|
||||
body, targetSrvName, _, err := services.BuildServiceBodyAndSrv(srvName, arg.url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url format: %s", err)
|
||||
}
|
||||
|
||||
if resp, err := services.HandleAddServiceSource(client, body); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
if resp, err := services.HandleAddRoute(client, services.BuildAPIRouteBody(arg.name, targetSrvName)); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported agent protocol type: %s", arg.typ)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateArg(arg AgentAddArg) error {
|
||||
if !arg.noPublish {
|
||||
return arg.HigressConsoleAuthArg.validate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,47 +15,473 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/common"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
AgentBinaryName = "claude"
|
||||
BinaryVersion = "0.1.0"
|
||||
DevVersion = "dev"
|
||||
NodeLeastVersion = 18
|
||||
AgentInstallCmd = "npm install -g @anthropic-ai/claude-code"
|
||||
AgentReleasePage = "https://docs.claude.com/en/docs/claude-code/setup"
|
||||
)
|
||||
|
||||
// set up the core env
|
||||
// 1. check if npm is installed
|
||||
// 2. check the npm version
|
||||
// 3. install hgctl-agent
|
||||
func getAgent() *AgenticCore {
|
||||
if !checkAgentInstallStatus() {
|
||||
fmt.Println("⚠️ Prerequisites not satisfied. Exiting...")
|
||||
// exit directly
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return NewAgenticCore()
|
||||
type HimarketAdminAuthArg struct {
|
||||
hmURL string
|
||||
hmUser string
|
||||
hmPassword string
|
||||
}
|
||||
|
||||
func checkAgentInstallStatus() bool {
|
||||
// TODO: Support cross-platform:windows
|
||||
// Developer's page
|
||||
type HimarketDevAuthArg struct {
|
||||
hmURL string
|
||||
hmUser string
|
||||
hmPassword string
|
||||
}
|
||||
|
||||
if !checkNodeInstall() {
|
||||
if err := promptNodeInstall(); err != nil {
|
||||
return false
|
||||
func (h *HimarketAdminAuthArg) validate() error {
|
||||
if h.hmURL == "" || h.hmUser == "" || h.hmPassword == "" {
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HigressConsoleAuthArg struct {
|
||||
// higress console auth arg
|
||||
hgURL string
|
||||
hgUser string
|
||||
hgPassword string
|
||||
}
|
||||
|
||||
func (h *HigressConsoleAuthArg) validate() error {
|
||||
if h.hgURL == "" || h.hgUser == "" || h.hgPassword == "" {
|
||||
fmt.Println("--higress-console-user, --higress-console-url, --higress-console-password must be provided")
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Init the global configuration from config file
|
||||
InitConfig()
|
||||
}
|
||||
|
||||
func resolveHimarketAdminAuth(arg *HimarketAdminAuthArg) {
|
||||
if arg.hmURL == "" {
|
||||
arg.hmURL = viper.GetString(HIMARKET_ADMIN_URL)
|
||||
}
|
||||
if arg.hmUser == "" {
|
||||
arg.hmUser = viper.GetString(HIMARKET_ADMIN_USER)
|
||||
}
|
||||
if arg.hmPassword == "" {
|
||||
arg.hmPassword = viper.GetString(HIMARKET_ADMIN_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
// resolve from viper
|
||||
func resolveHigressConsoleAuth(arg *HigressConsoleAuthArg) {
|
||||
if arg.hgURL == "" {
|
||||
arg.hgURL = viper.GetString(HIGRESS_CONSOLE_URL)
|
||||
}
|
||||
if arg.hgUser == "" {
|
||||
arg.hgUser = viper.GetString(HIGRESS_CONSOLE_USER)
|
||||
}
|
||||
if arg.hgPassword == "" {
|
||||
arg.hgPassword = viper.GetString(HIGRESS_CONSOLE_PASSWORD)
|
||||
}
|
||||
|
||||
// fmt.Printf("arg: %v\n", arg)
|
||||
|
||||
if arg.hgUser == "" || arg.hgPassword == "" {
|
||||
// Here we do not return this error, because it will failed when validate arg
|
||||
if err := tryToGetLocalCredential(arg); err != nil {
|
||||
fmt.Printf("failed to get local higress console credential: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseTypeToAPIProductType(typ string) string {
|
||||
switch typ {
|
||||
case "a2a":
|
||||
return string(common.AGENT_API)
|
||||
case "restful":
|
||||
return string(common.REST_API)
|
||||
case "model":
|
||||
return string(common.MODEL_API)
|
||||
case "mcp":
|
||||
return string(common.MCP_SERVER)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// This function serves MCP API as well as Model API for now.
|
||||
func publishAPIToHimarket(typ, name string, arg HimarketAdminAuthArg) error {
|
||||
|
||||
if err := arg.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := services.NewHimarketClient(arg.hmURL, arg.hmUser, arg.hmPassword)
|
||||
|
||||
productName := fmt.Sprintf("%s-%s", typ, name)
|
||||
|
||||
var gatewayId = viper.GetString(HIMARKET_TARGET_HIGRESS_ID)
|
||||
prompt := survey.Input{
|
||||
Message: fmt.Sprintf("Enter the target Higress instance id on Himarket(%s):", gatewayId),
|
||||
Default: gatewayId,
|
||||
Help: fmt.Sprintf("refers to %s/consoles/gateway to get your target Higress instance's id", arg.hmURL),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(&prompt, &gatewayId); err != nil {
|
||||
return fmt.Errorf("failed to get target higress gatewayID: %s", err)
|
||||
}
|
||||
|
||||
body := services.BuildAPIProductBody(productName, "An agent API import by hgctl", parseTypeToAPIProductType(typ))
|
||||
resp, err := services.HandleAddAPIProduct(client, body)
|
||||
if err != nil {
|
||||
fmt.Println(resp)
|
||||
return err
|
||||
}
|
||||
|
||||
product_id := string(resp)
|
||||
var refBody map[string]interface{}
|
||||
|
||||
if typ == "mcp" {
|
||||
refBody = services.BuildRefMCPAPIProductBody(gatewayId, product_id, name)
|
||||
} else {
|
||||
// target_route is the route_name in Higress, refers to `publishAgentAPIToHigress`
|
||||
target_route := fmt.Sprintf("%s-route", name)
|
||||
refBody = services.BuildRefModelAPIProductBody(gatewayId, product_id, target_route)
|
||||
|
||||
}
|
||||
|
||||
if resp, err := services.HandleRefAPIProduct(client, product_id, refBody); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// use pre-defined command /gen-agent to generate sys prompt
|
||||
func generateAgentPromptByCore(desc string) (string, error) {
|
||||
core := NewAgenticCore()
|
||||
prompt, err := core.runWithResult(fmt.Sprintf("/gen-agent %s", desc), "--print")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
type EnvProvisioner struct {
|
||||
core CoreType
|
||||
installCmd string
|
||||
releasePage string
|
||||
|
||||
// ~/.<core>
|
||||
dirName string
|
||||
}
|
||||
|
||||
func getCore() (*AgenticCore, error) {
|
||||
provisioner := EnvProvisioner{
|
||||
core: CoreType(viper.GetString(HGCTL_AGENT_CORE)),
|
||||
}
|
||||
|
||||
if err := provisioner.check(); err != nil {
|
||||
return nil, fmt.Errorf("⚠️ Prerequisites not satisfied: %s Exiting...", err)
|
||||
}
|
||||
|
||||
return NewAgenticCore(), nil
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) init() {
|
||||
switch p.core {
|
||||
case CORE_QODERCLI:
|
||||
p.installCmd = "npm install -g @qoder-ai/qodercli"
|
||||
p.releasePage = "https://docs.qoder.com/zh/cli/quick-start"
|
||||
p.dirName = "qoder"
|
||||
|
||||
case CORE_CLAUDE:
|
||||
p.installCmd = "npm install -g @anthropic-ai/claude-code"
|
||||
p.releasePage = "https://docs.claude.com/en/docs/claude-code/setup"
|
||||
p.dirName = "claude"
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) check() error {
|
||||
p.init()
|
||||
|
||||
if !p.checkNodeInstall() {
|
||||
if err := p.promptNodeInstall(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !checkAgentInstall() {
|
||||
if err := promptAgentInstall(); err != nil {
|
||||
return false
|
||||
if !p.checkAgentInstall() {
|
||||
if err := p.promptAgentInstall(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) checkNodeInstall() bool {
|
||||
cmd := exec.Command("node", "-v")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
versionStr := strings.TrimPrefix(strings.TrimSpace(string(out)), "v")
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return major >= NodeLeastVersion
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) promptNodeInstall() error {
|
||||
fmt.Println()
|
||||
color.Yellow("⚠️ Node.js is not installed or not found in PATH.")
|
||||
color.Cyan("🔧 Node.js is required to run the agent.")
|
||||
fmt.Println()
|
||||
|
||||
options := []string{
|
||||
"🚀 Install automatically (recommended)",
|
||||
"📖 Exit and show manual installation guide",
|
||||
}
|
||||
|
||||
var ans string
|
||||
prompt := &survey.Select{
|
||||
Message: "How would you like to install Node.js?",
|
||||
Options: options,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &ans); err != nil {
|
||||
return fmt.Errorf("selection error: %w", err)
|
||||
}
|
||||
|
||||
switch ans {
|
||||
case options[0]:
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing Node.js automatically...")
|
||||
|
||||
if err := p.installNodeAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
p.showNodeManualInstallation()
|
||||
return errors.New("node.js installation failed")
|
||||
}
|
||||
|
||||
color.Green("✅ Node.js installation completed!")
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if p.checkNodeInstall() {
|
||||
color.Green("🎉 Node.js is now available!")
|
||||
return nil
|
||||
} else {
|
||||
color.Yellow("⚠️ Node.js installation completed but not found in PATH.")
|
||||
color.Cyan("💡 You may need to restart your terminal or source your shell profile.")
|
||||
return errors.New("node.js installed but not in PATH")
|
||||
}
|
||||
|
||||
case options[1]:
|
||||
p.showNodeManualInstallation()
|
||||
return errors.New("node.js not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) installNodeAutomatically() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get home directory: %w", err)
|
||||
}
|
||||
|
||||
fnmBinPath := filepath.Join(homeDir, ".local/share/fnm/fnm")
|
||||
if runtime.GOOS == "windows" {
|
||||
fnmBinPath = filepath.Join(homeDir, "AppData/Roaming/fnm/fnm.exe")
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
color.Cyan("📦 For Windows, we recommend installing fnm via: 'winget install Schniz.fnm'")
|
||||
return errors.New("automatic fnm installation on Windows is not implemented in this script")
|
||||
|
||||
case "darwin", "linux":
|
||||
color.Cyan("🚀 Installing fnm (Fast Node Manager)...")
|
||||
installFnmCmd := exec.Command("bash", "-c", "curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell")
|
||||
installFnmCmd.Stdout = os.Stdout
|
||||
installFnmCmd.Stderr = os.Stderr
|
||||
if err := installFnmCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install fnm: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fnmBinPath); os.IsNotExist(err) {
|
||||
path, err := exec.LookPath("fnm")
|
||||
if err == nil {
|
||||
fnmBinPath = path
|
||||
} else {
|
||||
return errors.New("fnm was installed but binary not found at " + fnmBinPath)
|
||||
}
|
||||
}
|
||||
|
||||
color.Cyan("📦 Installing Node.js via fnm...")
|
||||
installNodeCmd := exec.Command(fnmBinPath, "install", "--lts")
|
||||
installNodeCmd.Stdout = os.Stdout
|
||||
installNodeCmd.Stderr = os.Stderr
|
||||
if err := installNodeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install node via fnm: %w", err)
|
||||
}
|
||||
|
||||
color.Cyan("✅ Setting LTS as default Node.js version...")
|
||||
useNodeCmd := exec.Command(fnmBinPath, "default", "lts-latest")
|
||||
return useNodeCmd.Run()
|
||||
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) showNodeManualInstallation() {
|
||||
fmt.Println()
|
||||
|
||||
color.New(color.FgGreen, color.Bold).Println("📖 Manual Node.js Installation Guide")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println(color.MagentaString("Choose one of the following installation methods:"))
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan("Method 1: Install via package manager")
|
||||
color.Cyan("macOS (brew): brew install node")
|
||||
color.Cyan("Ubuntu/Debian: sudo apt install -y nodejs npm")
|
||||
color.Cyan("Windows: download from https://nodejs.org and run installer")
|
||||
fmt.Println()
|
||||
|
||||
color.Yellow("Method 2: Download from official website")
|
||||
color.Yellow("1. Download Node.js from https://nodejs.org/en/download/")
|
||||
color.Yellow("2. Follow installer instructions and add to PATH if needed")
|
||||
fmt.Println()
|
||||
|
||||
color.Green("✅ Verify Installation")
|
||||
fmt.Println(color.WhiteString("node -v"))
|
||||
fmt.Println(color.WhiteString("npm -v"))
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan("💡 After installation, restart your terminal or source your shell profile.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) checkAgentInstall() bool {
|
||||
cmd := exec.Command(string(p.core), "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) promptAgentInstall() error {
|
||||
fmt.Println()
|
||||
color.Yellow("⚠️ %s is not installed or not found in PATH.", p.core)
|
||||
color.Cyan("🔧 %s is required to run the agent.", p.core)
|
||||
fmt.Println()
|
||||
|
||||
options := []string{
|
||||
"🚀 Install automatically",
|
||||
"📖 Exit and show manual installation guide",
|
||||
}
|
||||
|
||||
var ans string
|
||||
prompt := &survey.Select{
|
||||
Message: "How would you like to install " + string(p.core) + "?",
|
||||
Options: options,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &ans); err != nil {
|
||||
return fmt.Errorf("selection error: %w", err)
|
||||
}
|
||||
|
||||
switch ans {
|
||||
case options[0]:
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing %s automatically...", p.core)
|
||||
|
||||
if err := p.installAgentAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
p.showAgentManualInstallation()
|
||||
return errors.New(string(p.core) + " installation failed")
|
||||
}
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if p.checkAgentInstall() {
|
||||
color.Green("🎉 %s is now available!", p.core)
|
||||
return nil
|
||||
} else {
|
||||
color.Yellow("⚠️ %s installed but not found in PATH.", p.core)
|
||||
color.Cyan("💡 You may need to restart your terminal or source your shell profile.")
|
||||
return errors.New(string(p.core) + " installed but not in PATH")
|
||||
}
|
||||
|
||||
case options[1]:
|
||||
p.showAgentManualInstallation()
|
||||
return errors.New(string(p.core) + " not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) installAgentAutomatically() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd := exec.Command("cmd", "/C", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "darwin":
|
||||
cmd := exec.Command("bash", "-c", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "linux":
|
||||
cmd := exec.Command("bash", "-c", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) showAgentManualInstallation() {
|
||||
fmt.Println()
|
||||
color.New(color.FgGreen, color.Bold).Printf("📖 Manual %s Installation Guide\n", p.core)
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan(fmt.Sprintf("1. Go to official release page: %s", p.releasePage))
|
||||
fmt.Printf(color.CyanString("2. Download %s for your OS\n"), p.core)
|
||||
color.Cyan("3. Make it executable and place it in a directory in your PATH")
|
||||
|
||||
fmt.Println()
|
||||
color.Cyan("💡 After installation, restart your terminal or source your shell profile.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
25
hgctl/pkg/agent/common/base.go
Normal file
25
hgctl/pkg/agent/common/base.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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 common
|
||||
|
||||
// Himarket Product Type
|
||||
type ProductType string
|
||||
|
||||
const (
|
||||
MCP_SERVER ProductType = "MCP_SERVER"
|
||||
MODEL_API ProductType = "MODEL_API"
|
||||
REST_API ProductType = "REST_API"
|
||||
AGENT_API ProductType = "AGENT_API"
|
||||
)
|
||||
129
hgctl/pkg/agent/config.go
Normal file
129
hgctl/pkg/agent/config.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2025 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 agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type CoreType string
|
||||
|
||||
const (
|
||||
CORE_CLAUDE CoreType = "claude"
|
||||
CORE_QODERCLI CoreType = "qodercli"
|
||||
)
|
||||
|
||||
const (
|
||||
// AgentBinaryName = "claude"
|
||||
// BinaryVersion = "0.1.0"
|
||||
// DevVersion = "dev"
|
||||
// NodeLeastVersion = 18
|
||||
// AgentInstallCmd = "npm install -g @anthropic-ai/claude-code"
|
||||
// AgentReleasePage = "https://docs.claude.com/en/docs/claude-code/setup"
|
||||
|
||||
HGCTL_AGENT_CORE = "hgctl-agent-core"
|
||||
AGENT_MODEL_PROVIDER = "agent-model-provider"
|
||||
AGENT_CHAT_MODEL = "agent-chat-model"
|
||||
HIGRESS_CONSOLE_URL = "higress-console-url"
|
||||
HIGRESS_CONSOLE_USER = "higress-console-user"
|
||||
HIGRESS_CONSOLE_PASSWORD = "higress-console-password"
|
||||
HIGRESS_GATEWAY_URL = "higress-gateway-url"
|
||||
|
||||
HIMARKET_ADMIN_URL = "himarket-admin-url"
|
||||
HIMARKET_ADMIN_USER = "himarket-admin-user"
|
||||
HIMARKET_ADMIN_PASSWORD = "himarket-admin-password"
|
||||
HIMARKET_TARGET_HIGRESS_ID = "himarket-target-higress-id"
|
||||
|
||||
HIMARKET_DEVELOPER_URL = "himarket-developer-url"
|
||||
HIMARKET_DEVELOPER_USER = "himarket-developer-user"
|
||||
HIMARKET_DEVELOPER_PASSWORD = "himarket-developer-password"
|
||||
|
||||
// --- AgentRun ---
|
||||
AGENTRUN_MODEL_NAME = "agentrun-model-name"
|
||||
AGENTRUN_SANDBOX_NAME = "agentrun-sandbox-name"
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID = "alibaba-cloud-access-key-id"
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET = "alibaba-cloud-access-key-secret"
|
||||
ALIBABA_CLOUD_SECURITY_TOK = "alibaba-cloud-security-tok"
|
||||
AGENTRUN_ACCOUNT_ID = "agentrun-account-id"
|
||||
AGENTRUN_REGION = "agentrun-region"
|
||||
AGENTRUN_SDK_DEB = "agentrun-sdk-deb"
|
||||
)
|
||||
|
||||
var GlobalConfig HgctlAgentConfig
|
||||
|
||||
type HgctlAgentConfig struct {
|
||||
AgenticCore CoreType `mapstructure:"hgctl-agent-core"`
|
||||
AgentChatModel string `mapstructure:"agent-chat-model"`
|
||||
AgentModelProvider string `mapstructure:"agent-model-provider"`
|
||||
|
||||
// Higress Console credentials
|
||||
HigressConsoleURL string `mapstructure:"higress-console-url"`
|
||||
HigressConsoleUser string `mapstructure:"higress-console-user"`
|
||||
HigressConsolePassword string `mapstructure:"higress-console-password"`
|
||||
HigressGatewayURL string `mapstructure:"higress-gateway-url"`
|
||||
// Himarket Admin credentials
|
||||
HimarketAdminURL string `mapstructure:"himarket-admin-url"`
|
||||
HimarketAdminUser string `mapstructure:"himarket-admin-user"`
|
||||
HimarketAdminPassword string `mapstructure:"himarket-admin-password"`
|
||||
HimarketTargetHigressID string `mapstructure:"himarket-target-higress-id"`
|
||||
|
||||
// Himarket Developer credentials
|
||||
HimarketDeveloperURL string `mapstructure:"himarket-developer-url"`
|
||||
HimarketDeveloperUser string `mapstructure:"himarket-developer-user"`
|
||||
HimarketDeveloperPassword string `mapstructure:"himarket-developer-password"`
|
||||
|
||||
// AgentRun Configuration
|
||||
AgentRunModelName string `mapstructure:"agentrun-model-name"`
|
||||
AgentRunSandboxName string `mapstructure:"agentrun-sandbox-name"`
|
||||
AlibabaCloudAccessKeyID string `mapstructure:"alibaba-cloud-access-key-id"`
|
||||
AlibabaCloudAccessKeySecret string `mapstructure:"alibaba-cloud-access-key-secret"`
|
||||
AlibabaCloudSecurityTok string `mapstructure:"alibaba-cloud-security-tok"`
|
||||
AgentRunAccountID string `mapstructure:"agentrun-account-id"`
|
||||
AgentRunRegion string `mapstructure:"agentrun-region"`
|
||||
}
|
||||
|
||||
func InitConfig() {
|
||||
viper.SetConfigName(".hgctl")
|
||||
viper.SetConfigType("json")
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatalf("Error finding home directory: %v", err)
|
||||
}
|
||||
|
||||
viper.AddConfigPath(home)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "Fatal error reading config file: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal into the GlobalConfig variable
|
||||
_ = viper.Unmarshal(&GlobalConfig)
|
||||
|
||||
// Validate supported AgentCore currently
|
||||
switch viper.GetString(HGCTL_AGENT_CORE) {
|
||||
case string(CORE_CLAUDE), string(CORE_QODERCLI):
|
||||
return
|
||||
default:
|
||||
viper.SetDefault(HGCTL_AGENT_CORE, string(CORE_QODERCLI))
|
||||
}
|
||||
}
|
||||
@@ -15,19 +15,64 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/manifests"
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type AgenticCore struct {
|
||||
binaryName string
|
||||
}
|
||||
|
||||
func NewAgenticCore() *AgenticCore {
|
||||
return &AgenticCore{}
|
||||
core := &AgenticCore{
|
||||
binaryName: viper.GetString(HGCTL_AGENT_CORE),
|
||||
}
|
||||
core.Setup()
|
||||
return core
|
||||
}
|
||||
|
||||
func (c *AgenticCore) run(args ...string) error {
|
||||
cmd := exec.Command(AgentBinaryName, args...)
|
||||
func (c *AgenticCore) GetPromptFileName() string {
|
||||
switch c.binaryName {
|
||||
case string(CORE_CLAUDE):
|
||||
return "CLAUDE.md"
|
||||
case string(CORE_QODERCLI):
|
||||
return "AGENTS.md"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *AgenticCore) GetCoreDirName() string {
|
||||
switch c.binaryName {
|
||||
case string(CORE_CLAUDE):
|
||||
return ".claude"
|
||||
case string(CORE_QODERCLI):
|
||||
return ".qoder"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// This will use core to test and improve created agent
|
||||
func (c *AgenticCore) ImproveNewAgent(config *AgentConfig) error {
|
||||
agentDir, err := util.GetSpecificAgentDir(config.AgentName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get agent directory: %s", agentDir)
|
||||
}
|
||||
return c.runInTargetDir(agentDir)
|
||||
}
|
||||
|
||||
func (c *AgenticCore) runInTargetDir(dir string, args ...string) error {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -35,12 +80,188 @@ func (c *AgenticCore) run(args ...string) error {
|
||||
|
||||
}
|
||||
|
||||
func (c *AgenticCore) runWithResult(args ...string) (string, error) {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("agent execution failed with exit code %d: %s\nStderr: %s",
|
||||
exitErr.ExitCode(), err.Error(), exitErr.Stderr)
|
||||
}
|
||||
return "", fmt.Errorf("failed to run agent: %w", err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func (c *AgenticCore) run(args ...string) error {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// setup additional prequisite environment and plugins manifest to user's profile
|
||||
// e.g. ../manifest/agent
|
||||
func (c *AgenticCore) Setup() {
|
||||
// Check if this is the first time, otherwise directly return (TODO: this is a simple check)
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
targetCtlDir := filepath.Join(homeDir, ".hgctl")
|
||||
if _, err := os.Stat(targetCtlDir); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
targetCoreDir := filepath.Join(homeDir, c.GetCoreDirName())
|
||||
|
||||
// setup subagent plugins file
|
||||
embedFS := manifests.BuiltinOrDir("")
|
||||
if err := manifests.ExtractEmbedFiles(embedFS, "agent", targetCtlDir); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("failed to init plugins for agent core")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup predefined files like: command.md
|
||||
if err := manifests.ExtractEmbedFiles(embedFS, "agent", targetCoreDir); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("failed to init commands for agent core")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Add Predefined MCP Server
|
||||
if err := c.addPredefinedMCP(); err != nil {
|
||||
fmt.Printf("Warning: failed to add needed mcp server: %s\n", err)
|
||||
}
|
||||
|
||||
if err := c.addHigressAPIMCP(); err != nil {
|
||||
fmt.Println("failed to init higress-api mcp server (you may need to add it manually):", err)
|
||||
fmt.Println("Details information on Higress-api MCP server refers to https://github.com/alibaba/higress/blob/main/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md")
|
||||
return
|
||||
}
|
||||
// fmt.Println("Higress-api MCP server added successfully")
|
||||
}
|
||||
|
||||
func (c *AgenticCore) addPredefinedMCP() error {
|
||||
// deepwikiArg := MCPAddArg{
|
||||
// name: "deepwiki",
|
||||
// url: "https://mcp.deepwiki.com/mcp",
|
||||
// typ: "",
|
||||
// transport: STREAMABLE,
|
||||
// scope: "user",
|
||||
// }
|
||||
// if err := c.AddMCPServer(deepwikiArg); err != nil {
|
||||
// return fmt.Errorf("deepwiki")
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AgenticCore) addHigressAPIMCP() error {
|
||||
arg := &HigressConsoleAuthArg{
|
||||
hgURL: viper.GetString(HIGRESS_GATEWAY_URL),
|
||||
hgUser: viper.GetString(HIGRESS_CONSOLE_USER),
|
||||
hgPassword: viper.GetString(HIGRESS_CONSOLE_PASSWORD),
|
||||
}
|
||||
fmt.Println("Initializing...Add prequisite MCP server (Higress-api MCP server) automatically")
|
||||
|
||||
if arg.hgURL == "" {
|
||||
gatewayPrompt := promptui.Prompt{
|
||||
Label: "Enter higress gateway URL",
|
||||
Default: "http://127.0.0.1:80",
|
||||
}
|
||||
gateway, err := gatewayPrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Println("failed to run gateway prompt: ", err)
|
||||
return err
|
||||
}
|
||||
arg.hgURL = gateway
|
||||
|
||||
}
|
||||
|
||||
if arg.hgURL == "" || arg.hgPassword == "" {
|
||||
if err := tryToGetLocalCredential(arg); err != nil || arg.hgUser == "" || arg.hgPassword == "" {
|
||||
// fallback: interact with user to provide password & username
|
||||
color.Red("failed to get higress-console credential automatically (Requires higress installed by hgctl). Let's do it manually")
|
||||
userPrompt := promptui.Prompt{
|
||||
Label: "Enter higress console username",
|
||||
Default: "admin",
|
||||
}
|
||||
username, err := userPrompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aborted: %v", err)
|
||||
}
|
||||
pwdPrompt := promptui.Prompt{
|
||||
Label: "Enter higress console password",
|
||||
Default: "admin",
|
||||
}
|
||||
pwd, err := pwdPrompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aborted: %v", err)
|
||||
}
|
||||
arg.hgUser = username
|
||||
arg.hgPassword = pwd
|
||||
}
|
||||
}
|
||||
|
||||
if arg.hgUser == "" || arg.hgPassword == "" {
|
||||
return fmt.Errorf("Empty higress console username and password, aborting")
|
||||
}
|
||||
|
||||
rawByte := fmt.Appendf(nil, "%s:%s", arg.hgUser, arg.hgPassword)
|
||||
|
||||
resStr := base64.StdEncoding.EncodeToString(rawByte)
|
||||
|
||||
authHeader := fmt.Sprintf("Authorization: Basic %s", resStr)
|
||||
|
||||
return c.AddMCPServer(MCPAddArg{
|
||||
name: "higress-api",
|
||||
url: fmt.Sprintf("%s/higress-api", arg.hgURL),
|
||||
transport: HTTP,
|
||||
typ: HTTP,
|
||||
scope: "user",
|
||||
header: []string{
|
||||
authHeader,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ------- Initialization -------
|
||||
func (c *AgenticCore) Start() error {
|
||||
return c.run(AgentBinaryName)
|
||||
return c.run()
|
||||
}
|
||||
|
||||
// ------- MCP -------
|
||||
func (c *AgenticCore) AddMCPServer(name string, url string) error {
|
||||
return c.run("mcp", "add", "--transport", HTTP, name, url)
|
||||
func (c *AgenticCore) AddMCPServer(arg MCPAddArg) error {
|
||||
// adapt the field
|
||||
if arg.transport == STREAMABLE {
|
||||
arg.transport = HTTP
|
||||
}
|
||||
args := []string{
|
||||
"mcp", "add", "--transport", arg.transport, arg.name, arg.url,
|
||||
}
|
||||
if arg.scope != "" {
|
||||
scopeArg := []string{"--scope", arg.scope}
|
||||
args = append(args, scopeArg...)
|
||||
}
|
||||
if len(arg.env) != 0 {
|
||||
for _, e := range arg.env {
|
||||
envArg := []string{"-e", e}
|
||||
args = append(args, envArg...)
|
||||
}
|
||||
}
|
||||
if len(arg.header) != 0 {
|
||||
for _, h := range arg.header {
|
||||
headerArg := []string{"-H", h}
|
||||
args = append(args, headerArg...)
|
||||
}
|
||||
}
|
||||
err := c.run(args...)
|
||||
|
||||
// Allow to add duplicate mcp server name (core will return error)
|
||||
if err == nil || strings.Contains(err.Error(), "already exists") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
406
hgctl/pkg/agent/deploy.go
Normal file
406
hgctl/pkg/agent/deploy.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// Copyright (c) 2025 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 agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type DeployType string
|
||||
|
||||
const (
|
||||
AgentRun DeployType = "agent-run"
|
||||
Local DeployType = "local"
|
||||
)
|
||||
|
||||
var (
|
||||
AddAccessKeyCmd = fmt.Sprintf("s config add -a %s", DefaultServerLessAccessKey)
|
||||
CheckAccessKeyCmd = fmt.Sprintf("s config get -a %s", DefaultServerLessAccessKey)
|
||||
DeployAgentRunCmd = fmt.Sprintf("s deploy -a %s", DefaultServerLessAccessKey)
|
||||
)
|
||||
|
||||
const (
|
||||
InstallServerlessCmd = "npm install @serverless-devs/s -g"
|
||||
BuildAgentCmd = "s build"
|
||||
ServerlessCliDocs = "https://serverless-devs.com/docs/user-guide/install"
|
||||
)
|
||||
|
||||
type DeployHandler struct {
|
||||
Name string
|
||||
AgentDir string
|
||||
Type DeployType
|
||||
}
|
||||
|
||||
func deployAgentCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy [name]",
|
||||
Short: "Deploy the specified agent locally or to the cloud",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
handler := &DeployHandler{
|
||||
Name: args[0],
|
||||
}
|
||||
cmdutil.CheckErr(handler.Deploy())
|
||||
},
|
||||
}
|
||||
|
||||
var cloud = false
|
||||
cmd.PersistentFlags().BoolVar(&cloud, "agentrun", false, "deploy agent using agentrun")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (h *DeployHandler) validate() error {
|
||||
if err := h.checkRequiredEnvironment(); err != nil {
|
||||
return fmt.Errorf("failed to get required environment: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) RunCmd(showOutput bool, cmd string, targetDir string) (string, error) {
|
||||
runCmd := exec.Command("bash", "-c", cmd)
|
||||
|
||||
if targetDir != "" {
|
||||
runCmd.Dir = targetDir
|
||||
}
|
||||
|
||||
if showOutput {
|
||||
runCmd.Stderr = os.Stderr
|
||||
runCmd.Stdout = os.Stdout
|
||||
if err := runCmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
output, err := runCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) RunPythonCmd(showOutput bool, args ...string) error {
|
||||
cmd := exec.Command("python3", args...)
|
||||
|
||||
if showOutput {
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkAgentRunEnvironment() error {
|
||||
if _, err := h.RunCmd(false, "s --version", ""); err != nil {
|
||||
fmt.Println("Serverless dev cli not installed, install it automatically..")
|
||||
if _, err := h.RunCmd(true, InstallServerlessCmd, ""); err != nil {
|
||||
return fmt.Errorf("failed to install serverless dev cli automatically, details refers to %s", ServerlessCliDocs)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(false, "docker --version", ""); err != nil {
|
||||
return fmt.Errorf("docker is required to deploy agent to agentRun: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkLocalEnvironment() error {
|
||||
pyVenv, err := util.GetPythonVersion()
|
||||
if err != nil {
|
||||
fmt.Printf("Python environment not found, you need Python environment to run your agent\n")
|
||||
return err
|
||||
}
|
||||
|
||||
if util.CompareVersions(pyVenv, MinPythonVersion) == -1 {
|
||||
fmt.Printf("Current Python: %s need Python %s+", MinPythonVersion, pyVenv)
|
||||
return fmt.Errorf("unsupport python version")
|
||||
}
|
||||
|
||||
missingDeps := []string{}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope; print(agentscope.__version__)"); err != nil {
|
||||
missingDeps = append(missingDeps, "agentscope")
|
||||
}
|
||||
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope_runtime; print(agentscope_runtime.__version__)"); err != nil {
|
||||
missingDeps = append(missingDeps, "agentscope-runtime==1.0.0")
|
||||
}
|
||||
|
||||
if len(missingDeps) != 0 {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
if _, err := os.Stat(venvDir); err == nil {
|
||||
// check again
|
||||
missingDeps := []string{}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope; print(agentscope.__version__)"); err != nil {
|
||||
fmt.Println("agentscope not installed, installing...")
|
||||
missingDeps = append(missingDeps, "agentscope")
|
||||
}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope_runtime; print(agentscope_runtime.__version__)"); err != nil {
|
||||
fmt.Println("agentscope-runtime not installed, installing...")
|
||||
missingDeps = append(missingDeps, "agentscope-runtime==1.0.0")
|
||||
}
|
||||
// This means ~/.hgctl/.venv/ has already installed the deps before
|
||||
if len(missingDeps) == 0 {
|
||||
if err := h.activateLocalPythonVenv(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.installLocalRequiredDeps(missingDeps); err != nil {
|
||||
return fmt.Errorf("failed to install missing deps: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) createLocalPyVenv() error {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
cmd := exec.Command("python3", "-m", "venv", venvDir)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println("failed to create python virtual environment", string(output))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) installLocalRequiredDeps(missingDeps []string) error {
|
||||
if err := h.RunPythonCmd(true, "-m", "pip", "--version"); err != nil {
|
||||
fmt.Printf("Pip not installed, you need install pip to deploy your agent\n")
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("This may takes a few minutes, you can install missing deps by yourself: ")
|
||||
for _, deps := range missingDeps {
|
||||
fmt.Println("- ", deps)
|
||||
}
|
||||
|
||||
if err := h.createLocalPyVenv(); err != nil {
|
||||
return fmt.Errorf("failed to create local venv (~/.hgctl/.venv): %s", err)
|
||||
}
|
||||
|
||||
if err := h.activateLocalPythonVenv(); err != nil {
|
||||
return fmt.Errorf("failed to activateLocalPythonVenv: %s", err)
|
||||
}
|
||||
|
||||
for _, deps := range missingDeps {
|
||||
if err := h.RunPythonCmd(true, "-m", "pip", "install", deps); err != nil {
|
||||
fmt.Printf("failed to install missing deps: %s\n", deps)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
fmt.Println("Missing deps installed successfully, target python venv path: ", venvDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) activateLocalPythonVenv() error {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
path := os.Getenv("PATH")
|
||||
newPath := venvDir + "/bin:" + path
|
||||
err := os.Setenv("PATH", newPath)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set PATH:", err)
|
||||
return err
|
||||
}
|
||||
err = os.Setenv("VIRTUAL_ENV", venvDir)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set VIRTUAL_ENV:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkRequiredEnvironment() error {
|
||||
if h.Type == AgentRun {
|
||||
return h.checkAgentRunEnvironment()
|
||||
}
|
||||
|
||||
if h.Type == Local {
|
||||
return h.checkLocalEnvironment()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) GetRequiredDeps() ([]string, error) {
|
||||
switch h.Type {
|
||||
case AgentRun:
|
||||
return []string{
|
||||
"agentrun-sdk[agentscope,server] >= 0.0.3",
|
||||
}, nil
|
||||
case Local:
|
||||
return []string{
|
||||
"agentscope", "agentscope-runtime==1.0.0",
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported deploy target type: %s", h.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Quick and simple to get type by examine the existence of `requirements.txt` file
|
||||
func (h *DeployHandler) getAgentType() error {
|
||||
path, err := util.GetSpecificAgentDir(h.Name)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid agent: %s", err)
|
||||
return err
|
||||
}
|
||||
h.AgentDir = path
|
||||
|
||||
filePath := filepath.Join(h.AgentDir, "requirements.txt")
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
h.Type = Local
|
||||
return nil
|
||||
}
|
||||
h.Type = AgentRun
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) Deploy() error {
|
||||
if err := h.getAgentType(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case AgentRun:
|
||||
if err := h.HandleAgentRun(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case Local:
|
||||
if err := h.HandleLocal(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported deploy target type: %s", h.Type)
|
||||
}
|
||||
|
||||
if h.Type == AgentRun {
|
||||
fmt.Printf("\n🌟 Agent deploy to agentRun successfully! Refers to https://functionai.console.aliyun.com/cn-hangzhou/agent/runtime to get it")
|
||||
fmt.Printf("You can publish it to Higress and Himarket by using `hgctl agent add %s <endpoints-url> -t model --as-product `\n", h.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
func (h *DeployHandler) HandleAgentRun() error {
|
||||
if err := h.CheckServerlessAccessKey(); err != nil {
|
||||
return fmt.Errorf("failed to set access key automatically: %s", err)
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(true, BuildAgentCmd, h.AgentDir); err != nil {
|
||||
return fmt.Errorf("failed to build agent: %s", err)
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(true, DeployAgentRunCmd, h.AgentDir); err != nil {
|
||||
return fmt.Errorf("failed to deploy agent: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set Serverless's Access Key in s.yaml, details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
// Example:
|
||||
// $ s config get -a defualt
|
||||
|
||||
// You have not yet been found to have configured key information.
|
||||
// You can use [s config add] for key configuration, or use [s config add -h] to view configuration help.
|
||||
// If you already used [s config add], please check the permission of file [{HOMEPATH}/.s/access.yaml].
|
||||
// If you have questions, please tell us: https://github.com/Serverless-Devs/Serverless-Devs/issues
|
||||
//
|
||||
// s version: @serverless-devs/s: 3.1.10
|
||||
func (h *DeployHandler) CheckServerlessAccessKey() error {
|
||||
notFoundMessage := "You have not yet been found to have configured key information"
|
||||
output, err := h.RunCmd(false, CheckAccessKeyCmd, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run %s command to check access key: %s", CheckAccessKeyCmd, err)
|
||||
}
|
||||
if strings.Contains(output, notFoundMessage) {
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
🔑 **ACTION REQUIRED**: Please configure your Alibaba Cloud credentials first.
|
||||
Copy and run the command below to set up your Access Key:
|
||||
> %s
|
||||
|
||||
`, AddAccessKeyCmd)
|
||||
return fmt.Errorf("access key not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) HandleLocal() error {
|
||||
if _, err := os.Stat(h.AgentDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("agent source file not found: %s", h.AgentDir)
|
||||
}
|
||||
|
||||
if err := h.startAgentProcess(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) startAgentProcess() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return h.runWindowsAgent()
|
||||
case "darwin", "linux":
|
||||
return h.runUnixAgent()
|
||||
default:
|
||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DeployHandler) runUnixAgent() error {
|
||||
agentFile := filepath.Join(h.AgentDir, ASRuntimeMainPyFile)
|
||||
if err := h.RunPythonCmd(true, agentFile); err != nil {
|
||||
fmt.Println("failed to start agent, exiting...")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) runWindowsAgent() error {
|
||||
agentFile := filepath.Join(h.AgentDir, ASRuntimeMainPyFile)
|
||||
if err := h.RunPythonCmd(true, agentFile); err != nil {
|
||||
fmt.Println("failed to start agent, exiting...")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,10 +17,8 @@ package agent
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/alibaba/higress/hgctl/pkg/helm"
|
||||
@@ -34,32 +32,30 @@ import (
|
||||
type MCPType string
|
||||
|
||||
const (
|
||||
HTTP string = "http"
|
||||
SSE string = "sse"
|
||||
OPENAPI string = "openapi"
|
||||
OPENAPI string = "openapi"
|
||||
HTTP string = "http"
|
||||
|
||||
STREAMABLE string = "streamable"
|
||||
SSE string = "sse"
|
||||
|
||||
DIRECT_ROUTE string = "DIRECT_ROUTE"
|
||||
OPEN_API string = "OPEN_API"
|
||||
|
||||
HIGRESS_CONSOLE_URL = "higress-console-url"
|
||||
HIGRESS_CONSOLE_USER = "higress-console-user"
|
||||
HIGRESS_CONSOLE_PASSWORD = "higress-console-password"
|
||||
)
|
||||
|
||||
type MCPAddArg struct {
|
||||
// higress console auth arg
|
||||
baseURL string
|
||||
hgUser string
|
||||
hgPassword string
|
||||
HigressConsoleAuthArg
|
||||
HimarketAdminAuthArg
|
||||
|
||||
name string
|
||||
url string
|
||||
typ string
|
||||
transport string
|
||||
spec string
|
||||
scope string
|
||||
env []string
|
||||
header []string
|
||||
noPublish bool
|
||||
// TODO: support mcp env
|
||||
// env string
|
||||
|
||||
asProduct bool
|
||||
}
|
||||
|
||||
type MCPAddHandler struct {
|
||||
@@ -84,24 +80,45 @@ func newMCPAddCmd() *cobra.Command {
|
||||
arg := &MCPAddArg{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name]",
|
||||
Use: "add [name] [url]",
|
||||
Short: "add mcp server including http and openapi",
|
||||
Example: ` # Add HTTP type MCP Server
|
||||
hgctl mcp add http-mcp http://localhost:8080/mcp
|
||||
|
||||
# Add MCP Server with environment variables and headers
|
||||
hgctl mcp add http-mcp http://localhost:8080/mcp -e API_KEY=secret -H "Authorization: Bearer token"
|
||||
|
||||
# Add MCP Server use Openapi file
|
||||
hgctl mcp add swagger-mcp ./path/to/openapi.yaml --type openapi`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
arg.name = args[0]
|
||||
resolveHigressConsoleAuth(arg)
|
||||
if arg.typ == HTTP {
|
||||
arg.url = args[1]
|
||||
} else {
|
||||
arg.spec = args[1]
|
||||
}
|
||||
|
||||
resolveHigressConsoleAuth(&arg.HigressConsoleAuthArg)
|
||||
resolveHimarketAdminAuth(&arg.HimarketAdminAuthArg)
|
||||
cmdutil.CheckErr(handleAddMCP(cmd.OutOrStdout(), *arg))
|
||||
color.Cyan("Tip: Try doing 'kubectl port-forward' and add the server to the agent manually, if MCP Server connection failed")
|
||||
color.Cyan("Tip: Try doing 'kubectl port-forward' and add the server to the agent manually, if using Higress MCP Server and connection failed")
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.ExactArgs(2),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&arg.transport, "transport", "t", HTTP, "Determine the MCP Server's Type")
|
||||
cmd.PersistentFlags().StringVarP(&arg.url, "url", "u", "", "MCP server URL")
|
||||
cmd.PersistentFlags().StringVar(&arg.typ, "type", HTTP, "Determine the MCP Server's Type")
|
||||
cmd.PersistentFlags().StringVarP(&arg.transport, "transport", "t", STREAMABLE, `The MCP Server's transport`)
|
||||
cmd.PersistentFlags().StringVarP(&arg.scope, "scope", "s", "project", `Configuration scope (project or global)`)
|
||||
cmd.PersistentFlags().StringVar(&arg.spec, "spec", "", "Specification of the openapi api")
|
||||
cmd.PersistentFlags().StringSliceVarP(&arg.env, "env", "e", nil, "Environment variables to pass to the MCP server (can be specified multiple times)")
|
||||
cmd.PersistentFlags().StringSliceVarP(&arg.header, "header", "H", nil, "HTTP headers to pass to the MCP server (can be specified multiple times)")
|
||||
cmd.PersistentFlags().BoolVar(&arg.noPublish, "no-publish", false, "If set then the mcp server will not be plubished to higress")
|
||||
cmd.PersistentFlags().BoolVar(&arg.asProduct, "as-product", false, "If it's set then the agent API will be published to Himarket (no-publish must be false)")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, arg)
|
||||
// cmd.PersistentFlags().StringVar(&arg.spec, "spec", "", "Specification file (yaml/json) of the openapi api")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, &arg.HigressConsoleAuthArg)
|
||||
addHimarketAdminAuthFlag(cmd, &arg.HimarketAdminAuthArg)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -112,22 +129,19 @@ func newHanlder(c *AgenticCore, arg MCPAddArg, w io.Writer) *MCPAddHandler {
|
||||
|
||||
func (h *MCPAddHandler) validateArg() error {
|
||||
if !h.arg.noPublish {
|
||||
if h.arg.baseURL == "" || h.arg.hgUser == "" || h.arg.hgPassword == "" {
|
||||
fmt.Println("--higress-console-user, --higress-console-url, --higress-console-password must be provided")
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
return h.arg.HigressConsoleAuthArg.validate()
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (h *MCPAddHandler) addHTTPMCP() error {
|
||||
if err := h.core.AddMCPServer(h.arg.name, h.arg.url); err != nil {
|
||||
if err := h.core.AddMCPServer(h.arg); err != nil {
|
||||
return fmt.Errorf("mcp add failed: %w", err)
|
||||
}
|
||||
|
||||
if !h.arg.noPublish {
|
||||
return publishToHigress(h.arg, nil)
|
||||
return publishMCPToHigress(h.arg, h.arg.typ, nil)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -137,23 +151,29 @@ func (h *MCPAddHandler) addHTTPMCP() error {
|
||||
func (h *MCPAddHandler) addOpenAPIMCP() error {
|
||||
// fmt.Printf("get mcp server: %s openapi-spec-file: %s\n", h.arg.name, h.arg.spec)
|
||||
config := h.parseOpenapiSpec()
|
||||
|
||||
config.Server.SecuritySchemes[0].DefaultCredential = "b5b9752c7ad2cb9c6b19fb5fd6a23be8852eca9c"
|
||||
// fmt.Printf("get config struct: %v", config)
|
||||
|
||||
// publish to higress
|
||||
if err := publishToHigress(h.arg, config); err != nil {
|
||||
if err := publishMCPToHigress(h.arg, "streamable", config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add mcp server to agent
|
||||
gatewayIP, err := GetHigressGatewayServiceIP()
|
||||
if err != nil {
|
||||
color.Red(
|
||||
"failed to add mcp server [%s] while getting higress-gateway ip due to: %v \n You may try to do port-forward and add it to agent manually", h.arg.name, err)
|
||||
return err
|
||||
gatewayURL := viper.GetString(HIGRESS_GATEWAY_URL)
|
||||
if gatewayURL == "" {
|
||||
svcIP, err := GetHigressGatewayServiceIP()
|
||||
if err != nil {
|
||||
color.Red(
|
||||
"failed to add mcp server [%s] while getting higress-gateway ip due to: %v \n You may try to do port-forward and add it to agent manually", h.arg.name, err)
|
||||
return err
|
||||
}
|
||||
gatewayURL = svcIP
|
||||
}
|
||||
mcpURL := fmt.Sprintf("http://%s/mcp-servers/%s", gatewayIP, h.arg.name)
|
||||
return h.core.AddMCPServer(h.arg.name, mcpURL)
|
||||
|
||||
mcpURL := fmt.Sprintf("%s/mcp-servers/%s", gatewayURL, h.arg.name)
|
||||
h.arg.url = mcpURL
|
||||
return h.core.AddMCPServer(h.arg)
|
||||
}
|
||||
|
||||
func (h *MCPAddHandler) parseOpenapiSpec() *models.MCPConfig {
|
||||
@@ -161,7 +181,10 @@ func (h *MCPAddHandler) parseOpenapiSpec() *models.MCPConfig {
|
||||
}
|
||||
|
||||
func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
client := getAgent()
|
||||
client, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get agent core: %s", err)
|
||||
}
|
||||
h := newHanlder(client, arg, w)
|
||||
if err := h.validateArg(); err != nil {
|
||||
return err
|
||||
@@ -169,9 +192,12 @@ func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
|
||||
// spec -> OPENAPI
|
||||
// noPublish -> typ
|
||||
switch arg.transport {
|
||||
switch arg.typ {
|
||||
case HTTP:
|
||||
return h.addHTTPMCP()
|
||||
if err := h.addHTTPMCP(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case OPENAPI:
|
||||
if arg.spec == "" {
|
||||
return fmt.Errorf("--spec is required for openapi type")
|
||||
@@ -182,19 +208,29 @@ func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
if arg.url != "" {
|
||||
return fmt.Errorf("--url is not supported for openapi type")
|
||||
}
|
||||
return h.addOpenAPIMCP()
|
||||
default:
|
||||
return fmt.Errorf("unsupported mcp type")
|
||||
if err := h.addOpenAPIMCP(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !arg.noPublish && arg.asProduct {
|
||||
if err := publishAPIToHimarket("mcp", arg.name, arg.HimarketAdminAuthArg); err != nil {
|
||||
fmt.Println("failed to publish it to himarket, please do it mannually")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func publishToHigress(arg MCPAddArg, config *models.MCPConfig) error {
|
||||
func publishMCPToHigress(arg MCPAddArg, transport string, config *models.MCPConfig) error {
|
||||
// 1. parse the raw http url
|
||||
// 2. add service source
|
||||
// 3. add MCP server request
|
||||
client := services.NewHigressClient(arg.baseURL, arg.hgUser, arg.hgPassword)
|
||||
client := services.NewHigressClient(arg.hgURL, arg.hgUser, arg.hgPassword)
|
||||
|
||||
// mcp server's url
|
||||
rawURL := arg.url
|
||||
// DIRECT_ROUTE or OPEN_API
|
||||
mcpType := DIRECT_ROUTE
|
||||
@@ -205,61 +241,46 @@ func publishToHigress(arg MCPAddArg, config *models.MCPConfig) error {
|
||||
mcpType = OPEN_API
|
||||
}
|
||||
|
||||
res, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add service source
|
||||
srvType := ""
|
||||
srvPort := ""
|
||||
srvName := fmt.Sprintf("hgctl-%s", arg.name)
|
||||
srvPath := res.Path
|
||||
|
||||
if ip := net.ParseIP(res.Hostname()); ip == nil {
|
||||
srvType = "dns"
|
||||
} else {
|
||||
srvType = "static"
|
||||
}
|
||||
|
||||
if res.Port() == "" && res.Scheme == "http" {
|
||||
srvPort = "80"
|
||||
} else if res.Port() == "" && res.Scheme == "https" {
|
||||
srvPort = "443"
|
||||
} else {
|
||||
srvPort = res.Port()
|
||||
}
|
||||
|
||||
_, err = services.HandleAddServiceSource(client, map[string]interface{}{
|
||||
"domain": res.Host,
|
||||
"type": srvType,
|
||||
"port": srvPort,
|
||||
"name": srvName,
|
||||
"domainForEdit": res.Host,
|
||||
"protocol": res.Scheme,
|
||||
})
|
||||
// e.g. hgctl-mcp-deepwiki.dns
|
||||
body, targetSrvName, port, err := services.BuildServiceBodyAndSrv(srvName, rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid url format: %s", err)
|
||||
}
|
||||
|
||||
resp, err := services.HandleAddServiceSource(client, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("response body: %s %s\n", string(resp), err)
|
||||
}
|
||||
|
||||
srvField := []map[string]interface{}{{
|
||||
"name": fmt.Sprintf("%s.%s", srvName, srvType),
|
||||
"port": srvPort,
|
||||
"name": targetSrvName,
|
||||
"port": port,
|
||||
"version": "1.0",
|
||||
"weight": 100,
|
||||
}}
|
||||
|
||||
// generete mcp server add request body
|
||||
body := map[string]interface{}{
|
||||
"name": arg.name,
|
||||
// "description": "",
|
||||
"type": mcpType,
|
||||
"service": fmt.Sprintf("%s.%s:%s", srvName, srvType, srvPort),
|
||||
"upstreamPathPrefix": srvPath,
|
||||
"services": srvField,
|
||||
body = map[string]interface{}{
|
||||
"name": arg.name,
|
||||
"description": "A MCP Server added by hgctl",
|
||||
"type": mcpType,
|
||||
"services": srvField,
|
||||
"domains": []interface{}{},
|
||||
"consumerAuthInfo": map[string]interface{}{
|
||||
"type": "key-auth",
|
||||
"allowedConsumers": []string{},
|
||||
},
|
||||
}
|
||||
|
||||
// fmt.Printf("request body: %v", body)
|
||||
// Only DIRECT_ROUTE Type get below extra params
|
||||
if mcpType == DIRECT_ROUTE {
|
||||
res, _ := url.Parse(rawURL)
|
||||
body["directRouteConfig"] = map[string]interface{}{
|
||||
"path": res.Path,
|
||||
"transportType": arg.transport,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = services.HandleAddMCPServer(client, body)
|
||||
if err != nil {
|
||||
@@ -275,12 +296,17 @@ func publishToHigress(arg MCPAddArg, config *models.MCPConfig) error {
|
||||
|
||||
func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig, srvField []map[string]interface{}) {
|
||||
body := map[string]interface{}{
|
||||
"name": config.Server.Name,
|
||||
// "description": "",
|
||||
"name": config.Server.Name,
|
||||
"description": "A MCP Server added by hgctl",
|
||||
"services": srvField,
|
||||
"type": OPEN_API,
|
||||
"rawConfigurations": convertMCPConfigToStr(config),
|
||||
"mcpServerName": config.Server.Name,
|
||||
"domains": []interface{}{},
|
||||
"consumerAuthInfo": map[string]interface{}{
|
||||
"type": "key-auth",
|
||||
"allowedConsumers": []string{},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := services.HandleAddOpenAPITool(client, body)
|
||||
@@ -291,38 +317,7 @@ func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig,
|
||||
// fmt.Println("get openapi tools add response: ", string(resp))
|
||||
}
|
||||
|
||||
func addHigressConsoleAuthFlag(cmd *cobra.Command, arg *MCPAddArg) {
|
||||
cmd.PersistentFlags().StringVar(&arg.baseURL, HIGRESS_CONSOLE_URL, "", "The BaseURL of higress console")
|
||||
cmd.PersistentFlags().StringVar(&arg.hgUser, HIGRESS_CONSOLE_USER, "", "The username of higress console")
|
||||
cmd.PersistentFlags().StringVarP(&arg.hgPassword, HIGRESS_CONSOLE_PASSWORD, "p", "", "The password of higress console")
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
// resolve from viper
|
||||
func resolveHigressConsoleAuth(arg *MCPAddArg) {
|
||||
if arg.baseURL == "" {
|
||||
arg.baseURL = viper.GetString(HIGRESS_CONSOLE_URL)
|
||||
}
|
||||
if arg.hgUser == "" {
|
||||
arg.hgUser = viper.GetString(HIGRESS_CONSOLE_USER)
|
||||
}
|
||||
if arg.hgPassword == "" {
|
||||
arg.hgPassword = viper.GetString(HIGRESS_CONSOLE_PASSWORD)
|
||||
}
|
||||
|
||||
// fmt.Printf("arg: %v\n", arg)
|
||||
|
||||
if arg.hgUser == "" || arg.hgPassword == "" {
|
||||
// Here we do not return this error, cause it will failed when validate arg
|
||||
if err := tryToGetLocalCredential(arg); err != nil {
|
||||
fmt.Printf("failed to get local higress console credential: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tryToGetLocalCredential(arg *MCPAddArg) error {
|
||||
func tryToGetLocalCredential(arg *HigressConsoleAuthArg) error {
|
||||
profileContexts, err := getAllProfiles()
|
||||
|
||||
// The higress is not installed by hgctl
|
||||
|
||||
341
hgctl/pkg/agent/new.go
Normal file
341
hgctl/pkg/agent/new.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) 2025 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 agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/prompt"
|
||||
"github.com/alibaba/higress/hgctl/pkg/manifests"
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
ASRuntimeMainPyFile = "as_runtime_main.py"
|
||||
AgentRunMainPyFile = "agentrun_main.py"
|
||||
ToolKitPyFile = "toolkit.py"
|
||||
AgentClassFile = "agent.py"
|
||||
CorePromptFile = "claude.md" // TODO: support qoder AGENTS.md
|
||||
SConfigYAML = "s.yaml"
|
||||
|
||||
ARTemplate = "agentrun.tmpl"
|
||||
ASTemplate = "agentscope.tmpl"
|
||||
AgentClassTemplate = "agent.tmpl"
|
||||
ToolKitTemplate = "toolkit.tmpl"
|
||||
SConfigTemplate = "agentrun_s.tmpl"
|
||||
)
|
||||
|
||||
var ASAvailiableTools = []string{
|
||||
"execute_python_code",
|
||||
"execute_shell_command",
|
||||
"view_text_file",
|
||||
"write_text_file",
|
||||
"insert_text_file",
|
||||
"dashscope_text_to_image",
|
||||
"dashscope_text_to_audio",
|
||||
"dashscope_image_to_text",
|
||||
"openai_text_to_image",
|
||||
"openai_text_to_audio",
|
||||
"openai_edit_image",
|
||||
"openai_create_image_variation",
|
||||
"openai_image_to_text",
|
||||
"openai_audio_to_text",
|
||||
}
|
||||
|
||||
const (
|
||||
MinPythonVersion = "3.12"
|
||||
|
||||
DefaultServerLessAccessKey = "hgctl-credential"
|
||||
)
|
||||
|
||||
// Callback type for post-agent-creation actions
|
||||
type PostAgentAction func(config *AgentConfig) error
|
||||
|
||||
type MCPServerConfig struct {
|
||||
Name string // MCP Client Name
|
||||
URL string // MCP Server URL
|
||||
Transport string // transport `streamable_http` or `see` or `stdio`
|
||||
Headers map[string]string // HTTP Headers
|
||||
}
|
||||
|
||||
type ServerlessConfig struct {
|
||||
AccessKey string
|
||||
ResourceName string
|
||||
Region string
|
||||
AgentName string
|
||||
AgentDesc string
|
||||
Port uint
|
||||
|
||||
DiskSize uint
|
||||
Timeout uint
|
||||
|
||||
GlobalConfig HgctlAgentConfig
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
AppName string // "app"
|
||||
AppDescription string // "A helpful assistant and useful agent"
|
||||
AgentName string // "Friday"
|
||||
AvailableTools []string // availiable tools (built-in agentscope)
|
||||
SysPromptPath string // "You are a helpful assistant"
|
||||
ChatModel string // "qwen-max"
|
||||
Provider string // "Aliyun"
|
||||
APIKeyEnvVar string // DASHCOPE_API_KEY
|
||||
DeploymentPort int // 8090
|
||||
HostBinding string // 0.0.0.0
|
||||
EnableStreaming bool // true
|
||||
EnableThinking bool // true
|
||||
MCPServers []MCPServerConfig
|
||||
|
||||
Type DeployType
|
||||
ServerlessCfg ServerlessConfig
|
||||
}
|
||||
|
||||
func createAgentCmd() *cobra.Command {
|
||||
agentRun := false
|
||||
deployDirect := false
|
||||
|
||||
var createAgentCmd = &cobra.Command{
|
||||
Use: "new",
|
||||
Short: "Create a new agent or import one from core",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config := &AgentConfig{
|
||||
Type: Local,
|
||||
}
|
||||
if agentRun {
|
||||
config.Type = AgentRun
|
||||
config.ServerlessCfg = ServerlessConfig{
|
||||
AccessKey: DefaultServerLessAccessKey,
|
||||
Port: 9000,
|
||||
DiskSize: 512,
|
||||
Timeout: 600,
|
||||
|
||||
GlobalConfig: GlobalConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if err := getAgentConfig(config); err != nil {
|
||||
fmt.Printf("Error get Agent config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := createAgentTemplate(config); err != nil {
|
||||
fmt.Printf("Error creating agent: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := afterCreatedAgent(config); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
createAgentCmd.PersistentFlags().BoolVar(&agentRun, "agent-run", false, "Use agentRun to deploy to Alibaba cloud, default is false")
|
||||
createAgentCmd.PersistentFlags().BoolVar(&deployDirect, "deploy", false, "After agent creation, deploy it directly")
|
||||
return createAgentCmd
|
||||
|
||||
}
|
||||
|
||||
func afterCreatedAgent(config *AgentConfig) error {
|
||||
options := []string{
|
||||
"Deploy it directly",
|
||||
fmt.Sprintf("Improve and test it using agentic core (%s)", viper.GetString(HGCTL_AGENT_CORE)),
|
||||
"Do nothing and quit",
|
||||
}
|
||||
callbacks := map[string]PostAgentAction{
|
||||
options[0]: func(cfg *AgentConfig) error {
|
||||
handler := &DeployHandler{Name: cfg.AgentName}
|
||||
return handler.Deploy()
|
||||
},
|
||||
options[1]: func(cfg *AgentConfig) error {
|
||||
return runAgenticCoreImprovement(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
if err := promptAfterCreatedAgent(options, config, callbacks); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to handle post-creation action: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAgenticCoreImprovement(cfg *AgentConfig) error {
|
||||
core, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to invoke agent core: %s", err)
|
||||
}
|
||||
|
||||
if err := core.ImproveNewAgent(cfg); err != nil {
|
||||
return fmt.Errorf("failed to use core to improve new agent: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptAfterCreatedAgent(options []string, config *AgentConfig, callbacks map[string]PostAgentAction) error {
|
||||
|
||||
promptChoice := &survey.Select{
|
||||
Message: "What's next?:",
|
||||
Options: options,
|
||||
Help: "Choose an action to perform after agent creation.",
|
||||
}
|
||||
|
||||
var response string
|
||||
if err := survey.AskOne(promptChoice, &response); err != nil {
|
||||
return fmt.Errorf("failed to read user choice: %w", err)
|
||||
}
|
||||
|
||||
if callback, ok := callbacks[response]; ok {
|
||||
return callback(config)
|
||||
}
|
||||
|
||||
if response == options[2] {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown action selected: %q", response)
|
||||
}
|
||||
|
||||
func createAgentTemplate(config *AgentConfig) error {
|
||||
agentsDir := util.GetHomeHgctlDir() + "/agents"
|
||||
if err := os.MkdirAll(agentsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create agents directory: %v", err)
|
||||
}
|
||||
|
||||
agentDir := filepath.Join(agentsDir, config.AgentName)
|
||||
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create agent directory: %v", err)
|
||||
}
|
||||
|
||||
switch config.Type {
|
||||
case Local:
|
||||
// parse agentscope file
|
||||
asMain := filepath.Join(agentDir, ASRuntimeMainPyFile)
|
||||
asTemplateStr, err := get_template(ASTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentscope template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(asTemplateStr, asMain, config); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
case AgentRun:
|
||||
// Details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
|
||||
// parse agentrun file
|
||||
arMain := filepath.Join(agentDir, AgentRunMainPyFile)
|
||||
arTemplateStr, err := get_template(ARTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentrun template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(arTemplateStr, arMain, config); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
|
||||
// parse s.yaml
|
||||
s := filepath.Join(agentDir, SConfigYAML)
|
||||
STmplStr, err := get_template(SConfigTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentrun's serverless config file template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(STmplStr, s, config.ServerlessCfg); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
|
||||
// write requirements
|
||||
fileContent := "agentrun-sdk[agentscope,server]>=0.0.3"
|
||||
targetFilePath := filepath.Join(agentDir, "requirements.txt")
|
||||
if err := util.WriteFileString(targetFilePath, fileContent, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to write requirements.txt file to target agent directory: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parse toolkitPath
|
||||
toolkitPath := filepath.Join(agentDir, ToolKitPyFile)
|
||||
toolkitTmpl, err := get_template(ToolKitTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read toolkit template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(toolkitTmpl, toolkitPath, config); err != nil {
|
||||
return fmt.Errorf("failed to render toolkit file: %s", err)
|
||||
}
|
||||
|
||||
// write agent.py
|
||||
agentPath := filepath.Join(agentDir, AgentClassFile)
|
||||
agentTmpl, err := get_template(AgentClassTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agent class template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(agentTmpl, agentPath, config); err != nil {
|
||||
return fmt.Errorf("failed to render agent class file: %s", err)
|
||||
}
|
||||
|
||||
// write core_prompt.md
|
||||
if core, err := getCore(); err == nil {
|
||||
corePromptPath := filepath.Join(agentDir, core.GetPromptFileName())
|
||||
if err := util.WriteFileString(corePromptPath, prompt.AgentDevelopmentGuide, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to write %s file to target agent directory: %s", core.GetPromptFileName(), err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("failed to add instruction file in agent dir: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplateFile(templateStr string, targetPath string, data interface{}) error {
|
||||
// sync with python
|
||||
funcMap := template.FuncMap{
|
||||
"boolToPython": func(b bool) string {
|
||||
if b {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
},
|
||||
}
|
||||
|
||||
tmpl, err := template.New("agent").Funcs(funcMap).Parse(templateStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template: %v", err)
|
||||
}
|
||||
file, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
return fmt.Errorf("failed to render template: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func get_template(templatePath string) (string, error) {
|
||||
f := manifests.BuiltinOrDir("")
|
||||
templatePath = "agent/template/" + templatePath
|
||||
data, err := fs.ReadFile(f, templatePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template: %w", err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
244
hgctl/pkg/agent/prompt/agent_guide.md
Normal file
244
hgctl/pkg/agent/prompt/agent_guide.md
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2025 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.
|
||||
|
||||
# Agent Development Guide
|
||||
|
||||
Welcome to this AgentScope agent directory! This guide helps AI CLI tools (like Claude Code) understand the structure and assist you in building powerful agents.
|
||||
|
||||
## Directory Overview
|
||||
|
||||
This is an automatically generated agent directory with the following structure:
|
||||
|
||||
- **agent.py** - Main agent class (generated from agent.tmpl)
|
||||
- **toolkit.py** - Agent's tools and MCP integrations (generated from toolkit.tmpl)
|
||||
- **prompt.md** - User-provided system prompt for the agent
|
||||
- **as_runtime_main.py** / **agentrun_main.py** - Deployment runtime files
|
||||
- **agent.tmpl** / **toolkit.tmpl** / **agentscope.tmpl** - Generation templates
|
||||
|
||||
## What You Should Do
|
||||
|
||||
### Primary Focus: Improve Agent Intelligence
|
||||
|
||||
Your role is to help users build more capable, "agentic" agents by:
|
||||
|
||||
1. **Editing agent.py** - Enhance the agent class with:
|
||||
- Custom reasoning logic
|
||||
- Agent-specific hooks and behaviors
|
||||
- Memory management strategies
|
||||
- Multi-step task handling
|
||||
|
||||
2. **Editing toolkit.py** - Expand agent capabilities by:
|
||||
- Adding new tool functions
|
||||
- Integrating MCP (Model Context Protocol) servers
|
||||
- Configuring tool access and permissions
|
||||
|
||||
3. **Editing prompt.md** (when requested) - Refine the system prompt to:
|
||||
- Improve agent behavior and personality
|
||||
- Add domain-specific instructions
|
||||
- Define task-specific guidelines
|
||||
|
||||
### Critical Constraints
|
||||
|
||||
**DO NOT MODIFY** these deployment files:
|
||||
- `as_runtime_main.py`
|
||||
- `agentrun_main.py`
|
||||
|
||||
These files handle agent deployment and runtime orchestration. They are managed by the agent framework and should not be changed during development.
|
||||
|
||||
## Learning AgentScope
|
||||
|
||||
Before helping users, you should become proficient with AgentScope:
|
||||
|
||||
### Use the DeepWiki MCP Server
|
||||
|
||||
You have access to the `mcp-deepwiki` server. Use it to learn about AgentScope:
|
||||
|
||||
```python
|
||||
# Query the AgentScope repository
|
||||
ask_question(
|
||||
repoName="agentscope-ai/agentscope",
|
||||
question="How does the ReActAgent work?"
|
||||
)
|
||||
```
|
||||
|
||||
Study these key concepts:
|
||||
- ReActAgent architecture (Reasoning + Acting loop)
|
||||
- Agent hooks and lifecycle methods
|
||||
- Toolkit and tool registration
|
||||
- Memory systems (short-term and long-term)
|
||||
- Message formatting and model integration
|
||||
- MCP integration for external tools
|
||||
|
||||
### Testing Your Agent
|
||||
|
||||
Use the `agentscope-test-runner` subagent to test agent functionality:
|
||||
|
||||
```python
|
||||
# Launch test runner to validate agent behavior
|
||||
Task(
|
||||
subagent_type="agentscope-test-runner",
|
||||
prompt="Test the agent's ability to handle multi-step tasks",
|
||||
description="Testing agent functionality"
|
||||
)
|
||||
```
|
||||
|
||||
**Don't** write your own test harness - use this specialized subagent.
|
||||
|
||||
## Building Great Agents: Examples
|
||||
|
||||
### Example 1: Browser Automation Agent
|
||||
|
||||
Based on the AgentScope BrowserAgent, here's how to build a specialized web agent:
|
||||
|
||||
**Key Patterns:**
|
||||
|
||||
1. **Extend ReActAgent** - Inherit from ReActAgent for reasoning-acting loop
|
||||
2. **Use Hooks** - Register instance hooks to customize behavior at different lifecycle points:
|
||||
- `pre_reply` - Run before generating responses
|
||||
- `pre_reasoning` - Execute before reasoning phase
|
||||
- `post_reasoning` - Execute after reasoning phase
|
||||
- `post_acting` - Execute after taking actions
|
||||
|
||||
3. **Manage Memory** - Implement memory summarization to prevent context overflow
|
||||
4. **Leverage MCP Tools** - Connect to MCP servers (like Playwright browser tools) via toolkit
|
||||
|
||||
```python
|
||||
class Agent(ReActAgent):
|
||||
def __init__(self, name, model, formatter, memory, toolkit, ...):
|
||||
super().__init__(name, sys_prompt, model, formatter, memory, toolkit, max_iters)
|
||||
|
||||
# Register custom hooks
|
||||
self.register_instance_hook(
|
||||
"pre_reply",
|
||||
"custom_hook_name",
|
||||
custom_hook_function
|
||||
)
|
||||
```
|
||||
|
||||
### Example 2: Research Agent
|
||||
|
||||
For research and analysis tasks:
|
||||
|
||||
**Key Features:**
|
||||
- Knowledge base integration for RAG (Retrieval-Augmented Generation)
|
||||
- Long-term memory for persistent context
|
||||
- Plan notebook for complex multi-step research
|
||||
- Query rewriting for better information retrieval
|
||||
|
||||
```python
|
||||
class Agent(ReActAgent):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
sys_prompt,
|
||||
model,
|
||||
formatter,
|
||||
toolkit,
|
||||
memory,
|
||||
long_term_memory=None,
|
||||
knowledge=None,
|
||||
enable_rewrite_query=True,
|
||||
plan_notebook=None,
|
||||
...
|
||||
):
|
||||
# Initialize with research-focused capabilities
|
||||
super().__init__(...)
|
||||
```
|
||||
|
||||
### Example 3: Code Assistant Agent
|
||||
|
||||
For software development tasks:
|
||||
|
||||
**Key Capabilities:**
|
||||
- File operation tools (read, write, insert)
|
||||
- Code execution (execute_python_code, execute_shell_command)
|
||||
- Image/audio processing for multimodal interactions
|
||||
- MCP integration for IDE tools
|
||||
|
||||
### Common Agent Patterns
|
||||
|
||||
1. **Tool Registration** (in toolkit.py):
|
||||
```python
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tool import execute_shell_command, view_text_file
|
||||
|
||||
toolkit = Toolkit()
|
||||
toolkit.register_tool_function(execute_shell_command)
|
||||
toolkit.register_tool_function(view_text_file)
|
||||
```
|
||||
|
||||
2. **MCP Integration** (in toolkit.py):
|
||||
```python
|
||||
from agentscope.mcp import HttpStatelessClient
|
||||
|
||||
async def register_mcp(toolkit):
|
||||
client = HttpStatelessClient(
|
||||
name="browser-tools",
|
||||
transport="sse",
|
||||
url="http://localhost:3000/sse"
|
||||
)
|
||||
await toolkit.register_mcp_client(client)
|
||||
```
|
||||
|
||||
3. **Custom Hooks** (in agent.py):
|
||||
```python
|
||||
async def pre_reasoning_hook(self, *args, **kwargs):
|
||||
"""Custom logic before reasoning"""
|
||||
# Add context, check conditions, etc.
|
||||
pass
|
||||
|
||||
# In __init__:
|
||||
self.register_instance_hook("pre_reasoning", "my_hook", pre_reasoning_hook)
|
||||
```
|
||||
|
||||
## More Examples and Resources
|
||||
|
||||
Explore official AgentScope examples:
|
||||
- https://github.com/modelscope/agentscope/tree/main/examples/agent
|
||||
|
||||
Key examples to study:
|
||||
- **ReAct Agent** - Basic reasoning-acting agent
|
||||
- **Conversation Agent** - Multi-turn dialogue handling
|
||||
- **User Agent** - Human-in-the-loop interactions
|
||||
- **Tool Agent** - Advanced tool usage patterns
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Understand Requirements** - Clarify what the agent should do
|
||||
2. **Learn Patterns** - Use DeepWiki to research relevant AgentScope patterns
|
||||
3. **Design Agent** - Choose base class and required capabilities
|
||||
4. **Implement in agent.py** - Write custom agent logic
|
||||
5. **Add Tools in toolkit.py** - Register needed tools and MCP servers
|
||||
6. **Test with agentscope-test-runner** - Validate functionality
|
||||
7. **Iterate** - Refine based on test results
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Simple** - Begin with basic ReActAgent, add complexity as needed
|
||||
2. **Use Hooks Wisely** - Don't overcomplicate; hooks should have clear purposes
|
||||
3. **Memory Management** - Implement summarization for long conversations
|
||||
4. **Tool Selection** - Only add tools the agent actually needs
|
||||
5. **Clear Prompts** - Write specific, actionable system prompts in prompt.md
|
||||
6. **Test Iteratively** - Use the test-runner frequently during development
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Use DeepWiki MCP to query AgentScope documentation
|
||||
- Study the browser_agent.py example in this guide
|
||||
- Reference official examples at https://github.com/agentscope-ai/agentscope
|
||||
- Test early and often with agentscope-test-runner
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Focus on making the agent intelligent and capable. The deployment infrastructure is already handled - your job is to build the "brain" of the agent in agent.py and give it the right "tools" in toolkit.py.
|
||||
20
hgctl/pkg/agent/prompt/base.go
Normal file
20
hgctl/pkg/agent/prompt/base.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025 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 prompt
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed agent_guide.md
|
||||
var AgentDevelopmentGuide string
|
||||
@@ -31,7 +31,33 @@ type HigressClient struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type HimarketClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
jwtToken string
|
||||
}
|
||||
|
||||
// type ClientType string
|
||||
|
||||
// const (
|
||||
// HigressClientType ClientType = "higress"
|
||||
// HimarketClientType ClientType = "himarket"
|
||||
// )
|
||||
|
||||
// func NewClient(clientType ClientType, baseURL, username, password string) Client {
|
||||
// switch clientType {
|
||||
// case HimarketClientType:
|
||||
// return NewHimarketClient(baseURL, username, password)
|
||||
// case HigressClientType:
|
||||
// fallthrough
|
||||
// default:
|
||||
// return NewHigressClient(baseURL, username, password)
|
||||
// }
|
||||
// }
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
@@ -44,6 +70,19 @@ func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
return client
|
||||
}
|
||||
|
||||
func NewHimarketClient(baseURL, username, password string) *HimarketClient {
|
||||
client := &HimarketClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HigressClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
@@ -59,6 +98,133 @@ func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) getJWTToken() error {
|
||||
loginURL := c.baseURL + "/api/v1/admins/login"
|
||||
|
||||
loginData := map[string]string{
|
||||
"username": c.username,
|
||||
"password": c.password,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(loginData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal login data: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create login request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("login failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read login response: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBody, &response); err != nil {
|
||||
return fmt.Errorf("failed to parse login response: %w", err)
|
||||
}
|
||||
|
||||
// fmt.Println(string(respBody))
|
||||
|
||||
if data, ok := response["data"].(map[string]interface{}); ok {
|
||||
if token, ok := data["access_token"].(string); ok {
|
||||
c.jwtToken = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("token not found in login response: %v", response)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
if c.jwtToken == "" {
|
||||
if err := c.getJWTToken(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get JWT token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
url := c.baseURL + path
|
||||
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request data: %w", err)
|
||||
}
|
||||
body = bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.jwtToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 409 {
|
||||
return nil, fmt.Errorf("resource already exists")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 400 {
|
||||
return nil, fmt.Errorf("invalid resource definition")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 500 {
|
||||
return nil, fmt.Errorf("server internal error")
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
@@ -92,6 +258,8 @@ func (c *HigressClient) request(method, path string, data interface{}) ([]byte,
|
||||
return nil, fmt.Errorf("resource already exists")
|
||||
}
|
||||
|
||||
// fmt.Println(resp)
|
||||
|
||||
if resp.StatusCode == 400 {
|
||||
return nil, fmt.Errorf("invalid resource definition")
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/common"
|
||||
)
|
||||
|
||||
func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
@@ -50,22 +54,30 @@ func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, er
|
||||
// add MCP server to higress console, example request body as followed:
|
||||
//
|
||||
// {
|
||||
// "name": "mcp-deepwiki",
|
||||
// "description": "",
|
||||
// "type": "DIRECT_ROUTE", // or OPEN_API
|
||||
// "service": "hgctl-deepwiki.dns:443",
|
||||
// "upstreamPathPrefix": "/mcp",
|
||||
// "name": "test",
|
||||
// "description": "123",
|
||||
// "type": "DIRECT_ROUTE",
|
||||
// "services": [
|
||||
// {
|
||||
// "name": "hgctl-deepwiki.dns",
|
||||
// "name": "hgctl-mcp-deepwiki.dns",
|
||||
// "port": 443,
|
||||
// "version": "1.0",
|
||||
// "weight": 100
|
||||
// }
|
||||
// ]
|
||||
// ],
|
||||
// "consumerAuthInfo": {
|
||||
// "type": "key-auth",
|
||||
// "allowedConsumers": []
|
||||
// },
|
||||
// "domains": [],
|
||||
// "directRouteConfig": {
|
||||
// "path": "/mcp",
|
||||
// "transportType": "streamable"
|
||||
// }
|
||||
// }
|
||||
func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
data, ok := body.(map[string]interface{})
|
||||
// fmt.Printf("mcpbody: %v\n", data)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse request body")
|
||||
}
|
||||
@@ -76,10 +88,6 @@ func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error)
|
||||
if _, ok := data["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in body")
|
||||
}
|
||||
if _, ok := data["service"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'service' in body")
|
||||
}
|
||||
|
||||
// if _, ok := data["upstreamPathPrefix"]; !ok {
|
||||
// return nil, fmt.Errorf("missing required field 'upstreamPathPrefix' in body")
|
||||
// }
|
||||
@@ -97,6 +105,40 @@ func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// return map[mcp-server-name]{}
|
||||
func GetExistingMCPServers(client *HigressClient) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
data, err := HandleListMCPServers(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get product id from response: %s", err)
|
||||
}
|
||||
|
||||
// fmt.Println(response["data"])
|
||||
|
||||
if list, ok := response["data"].([]interface{}); ok {
|
||||
for _, item := range list {
|
||||
if mcp, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := mcp["name"].(string); ok {
|
||||
result[name] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func HandleListMCPServers(client *HigressClient) ([]byte, error) {
|
||||
ts := time.Now().Unix()
|
||||
pageNum := 1
|
||||
pageSize := 100
|
||||
return client.Get(fmt.Sprintf("/v1/mcpServer?ts=%d&pageNum=%d&pageSize=%d", ts, pageNum, pageSize))
|
||||
}
|
||||
|
||||
// add OpenAPI MCP tools to higress console, example request body:
|
||||
//
|
||||
// {
|
||||
@@ -127,3 +169,155 @@ func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error)
|
||||
func HandleAddOpenAPITool(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Put("/v1/mcpServer", body)
|
||||
}
|
||||
|
||||
func HandleAddAIProviderService(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/ai/providers", body)
|
||||
|
||||
}
|
||||
|
||||
func HandleAddAIRoute(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/ai/routes", body)
|
||||
}
|
||||
|
||||
func HandleAddRoute(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/routes", body)
|
||||
}
|
||||
|
||||
// Himarket-related
|
||||
func HandleAddHigressInstance(client *HimarketClient, body interface{}) ([]byte, error) {
|
||||
// This api will not return the higress-gatway-id
|
||||
return client.Post("/api/v1/gateways", body)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) getProduct(typ common.ProductType) ([]byte, error) {
|
||||
return c.Get(fmt.Sprintf("/api/v1/products?type=%s&page=0&size=30", string(typ)))
|
||||
}
|
||||
|
||||
func (c *HimarketClient) extractGetProductResponse(typ common.ProductType, response map[string]interface{}) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
data, ok := response["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
|
||||
content, ok := data["content"].([]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, item := range content {
|
||||
product, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
productType, _ := product["type"].(string)
|
||||
if productType != string(typ) {
|
||||
continue
|
||||
}
|
||||
|
||||
name, _ := product["name"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
mcpConfig, ok := product["mcpConfig"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
serverConfig, ok := mcpConfig["mcpServerConfig"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
domains, ok := serverConfig["domains"].([]interface{})
|
||||
if !ok || len(domains) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
path, ok := serverConfig["path"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, domainItem := range domains {
|
||||
domainConfig, ok := domainItem.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
domain, _ := domainConfig["domain"].(string)
|
||||
protocol, _ := domainConfig["protocol"].(string)
|
||||
if domain == "" || protocol == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
port, _ := domainConfig["port"].(float64)
|
||||
url := ""
|
||||
if port == 0 || port == 80 {
|
||||
url = fmt.Sprintf("%s://%s%s", protocol, domain, path)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s://%s:%d%s", protocol, domain, int(port), path)
|
||||
}
|
||||
|
||||
result[name] = url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *HimarketClient) GetDevModelProduct() (map[string]string, error) {
|
||||
data, err := c.getProduct(common.MODEL_API)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed request himarket: %s", err)
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get model api from response %s", err)
|
||||
}
|
||||
|
||||
return c.extractGetProductResponse(common.MODEL_API, response), nil
|
||||
}
|
||||
|
||||
func (c *HimarketClient) GetDevMCPServerProduct() (map[string]string, error) {
|
||||
data, err := c.getProduct(common.MCP_SERVER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed request himarket: %s", err)
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get MCP server from response %s", err)
|
||||
}
|
||||
|
||||
return c.extractGetProductResponse(common.MCP_SERVER, response), nil
|
||||
}
|
||||
|
||||
func HandleListHimarketMCPServers(client *HimarketClient) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func HandleAddAPIProduct(client *HimarketClient, body interface{}) ([]byte, error) {
|
||||
data, err := client.Post("/api/v1/products", body)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get product id from response: %s", err)
|
||||
}
|
||||
|
||||
if res, ok := response["data"].(map[string]interface{}); ok {
|
||||
if productId, ok := res["productId"].(string); ok {
|
||||
return []byte(productId), nil
|
||||
}
|
||||
}
|
||||
return data, fmt.Errorf("failed to get product id from response")
|
||||
}
|
||||
|
||||
func HandleRefAPIProduct(client *HimarketClient, product_id string, body interface{}) ([]byte, error) {
|
||||
return client.Post(fmt.Sprintf("/api/v1/products/%s/ref", product_id), body)
|
||||
}
|
||||
|
||||
178
hgctl/pkg/agent/services/utils.go
Normal file
178
hgctl/pkg/agent/services/utils.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2025 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 services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func BuildAIProviderServiceBody(name, url string) map[string]interface{} {
|
||||
customBaseURL := fmt.Sprintf("%s/compatible-mode/v1", url)
|
||||
return map[string]interface{}{
|
||||
"type": "openai",
|
||||
"name": name,
|
||||
"tokens": []string{},
|
||||
"version": 0,
|
||||
"protocol": "openai/v1",
|
||||
"tokenFailoverConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"proxyName": "",
|
||||
"rawConfigs": map[string]interface{}{
|
||||
"openaiExtraCustomUrls": []string{},
|
||||
"openaiCustomUrl": customBaseURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAddAIRouteBody(name, _url string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-route", name),
|
||||
// "version": "627198", // It's unecessary to provide when create a new one
|
||||
"domains": []interface{}{},
|
||||
"pathPredicate": map[string]interface{}{
|
||||
"matchType": "PRE",
|
||||
// FIXME: Currently, to use model API in higress user hould follow this pattern:
|
||||
// http://<higress-gateway-ip>/<PRE_MATCH_VALUE>/v1/chat/completions or /v1/embedding
|
||||
// However in Himarket, when connecting the higress ai route as model API, himarket will directly use http://<higress-gateway-ip>/<PRE_MATCH_VALUE>
|
||||
// as the final request url, which will not get to right path. So here we make the matchValue hard-coded as `/v1/chat/completions`
|
||||
"matchValue": "/v1/chat/completions",
|
||||
"caseSensitive": false,
|
||||
"ignoreCase": []string{}, // "ignoreCase": ["ignore"]
|
||||
},
|
||||
"headerPredicates": []interface{}{},
|
||||
"urlParamPredicates": []interface{}{},
|
||||
"upstreams": []interface{}{
|
||||
map[string]interface{}{
|
||||
"provider": name,
|
||||
"weight": 100,
|
||||
"modelMapping": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
"modelPredicates": []interface{}{},
|
||||
"authConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"allowedCredentialTypes": []interface{}{},
|
||||
"allowedConsumers": []interface{}{},
|
||||
},
|
||||
"fallbackConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"upstreams": nil,
|
||||
"fallbackStrategy": nil,
|
||||
"responseCodes": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildServiceBodyAndSrv(name, urlStr string) (map[string]interface{}, string, string, error) {
|
||||
res, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// add service source
|
||||
srvType := ""
|
||||
srvPort := ""
|
||||
|
||||
if ip := net.ParseIP(res.Hostname()); ip == nil {
|
||||
srvType = "dns"
|
||||
} else {
|
||||
srvType = "static"
|
||||
}
|
||||
|
||||
if res.Port() == "" && res.Scheme == "http" {
|
||||
srvPort = "80"
|
||||
} else if res.Port() == "" && res.Scheme == "https" {
|
||||
srvPort = "443"
|
||||
} else {
|
||||
srvPort = res.Port()
|
||||
}
|
||||
|
||||
// e.g. hgctl-mcp-deepwiki.dns
|
||||
targetSrvName := fmt.Sprintf("%s.%s", name, srvType)
|
||||
|
||||
return map[string]interface{}{
|
||||
"domain": res.Host,
|
||||
"type": srvType,
|
||||
"port": srvPort,
|
||||
"name": name,
|
||||
"proxyName": "",
|
||||
"domainForEdit": res.Host,
|
||||
"protocol": res.Scheme,
|
||||
}, targetSrvName, srvPort, nil
|
||||
}
|
||||
|
||||
func BuildAPIRouteBody(name, srv string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-route", name),
|
||||
"path": map[string]interface{}{
|
||||
"matchType": "PRE", // default is PREFIX
|
||||
"matchValue": "/process", // default is "/process"
|
||||
"caseSensitive": true,
|
||||
},
|
||||
"authConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"services": []map[string]interface{}{
|
||||
{
|
||||
"name": srv,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAddHigressInstanceBody(name, addr, username, password string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayName": name,
|
||||
"gatewayType": "HIGRESS",
|
||||
"higressConfig": map[string]interface{}{
|
||||
"address": addr,
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAPIProductBody(name, desc, typ string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": name, "description": desc, "type": typ,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRefModelAPIProductBody(gateway_id, product_id, target_route string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayId": gateway_id,
|
||||
"sourceType": "GATEWAY",
|
||||
"productId": product_id,
|
||||
"higressRefConfig": map[string]interface{}{
|
||||
"modelRouteName": target_route,
|
||||
"fromGatewayType": "HIGRESS",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRefMCPAPIProductBody(gateway_id, product_id, mcp_name string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayId": gateway_id,
|
||||
"sourceType": "GATEWAY",
|
||||
"productId": product_id,
|
||||
"higressRefConfig": map[string]interface{}{
|
||||
"mcpServerName": mcp_name,
|
||||
"fromGatewayType": "HIGRESS",
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/operator/pkg/util"
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
|
||||
474
hgctl/pkg/manifests/agent/agents/agentscope-test-runner.md
Normal file
474
hgctl/pkg/manifests/agent/agents/agentscope-test-runner.md
Normal file
@@ -0,0 +1,474 @@
|
||||
---
|
||||
name: agentscope-test-runner
|
||||
description: >
|
||||
Comprehensive Behavioral & Connectivity QA Specialist for AgentScope agents.
|
||||
Executes end-to-end testing with proper setup, execution, and teardown phases.
|
||||
Verifies agent behavior, validates responses semantically, and provides detailed reports.
|
||||
Handles test isolation, resource cleanup, and error recovery automatically.
|
||||
tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Grep
|
||||
- Write
|
||||
model: sonnet
|
||||
permissionMode: default
|
||||
---
|
||||
|
||||
# Identity & Purpose
|
||||
|
||||
You are the **AgentScope Test Runner** - a specialized QA agent responsible for comprehensive behavioral verification of AgentScope agents.
|
||||
|
||||
**Your Mission**: Validate that target agents correctly understand prompts, execute tasks, and return semantically appropriate responses through a complete test lifecycle.
|
||||
|
||||
**Core Principles**:
|
||||
1. **Complete Test Lifecycle**: Setup → Execute → Verify → Teardown → Report
|
||||
2. **Strict Isolation**: Each test runs in a clean environment
|
||||
3. **Semantic Validation**: Judge response quality, not just API success
|
||||
4. **Fail-Safe Cleanup**: Always cleanup resources, even on test failure
|
||||
5. **Detailed Reporting**: Provide actionable insights via structured XML
|
||||
|
||||
# Test Lifecycle Overview
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ SETUP │ → Prepare environment, validate dependencies
|
||||
├─────────────┤
|
||||
│ EXECUTE │ → Send test prompts, capture responses
|
||||
├─────────────┤
|
||||
│ VERIFY │ → Analyze semantic correctness
|
||||
├─────────────┤
|
||||
│ TEARDOWN │ → Cleanup temp files, restore state
|
||||
├─────────────┤
|
||||
│ REPORT │ → Return structured XML results
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
# Communication Contract
|
||||
|
||||
You communicate via **Structured XML Reports** with comprehensive diagnostics.
|
||||
|
||||
```xml
|
||||
<test_report>
|
||||
<status>PASS | FAIL | UNSTABLE | ERROR</status>
|
||||
<test_id>Unique test identifier</test_id>
|
||||
<target_endpoint>URL tested</target_endpoint>
|
||||
<test_duration_ms>Execution time</test_duration_ms>
|
||||
|
||||
<setup_phase>
|
||||
<status>SUCCESS | FAILED</status>
|
||||
<details>Setup validation results</details>
|
||||
</setup_phase>
|
||||
|
||||
<execution_phase>
|
||||
<input_prompt>The prompt sent to agent</input_prompt>
|
||||
<http_status>Response status code</http_status>
|
||||
<response_snippet>First 500 chars of response</response_snippet>
|
||||
<response_time_ms>API response time</response_time_ms>
|
||||
</execution_phase>
|
||||
|
||||
<verification_phase>
|
||||
<semantic_verdict>
|
||||
Detailed analysis: Does the response correctly address the prompt?
|
||||
Does it follow instructions? Is the output appropriate?
|
||||
</semantic_verdict>
|
||||
<verdict>PASS | FAIL | PARTIAL</verdict>
|
||||
</verification_phase>
|
||||
|
||||
<teardown_phase>
|
||||
<status>SUCCESS | FAILED</status>
|
||||
<cleaned_resources>List of cleaned temp files</cleaned_resources>
|
||||
</teardown_phase>
|
||||
|
||||
<diagnostics>
|
||||
<root_cause>Error explanation if applicable</root_cause>
|
||||
<recommendations>Suggestions for fixing issues</recommendations>
|
||||
</diagnostics>
|
||||
</test_report>
|
||||
```
|
||||
|
||||
# Execution Protocol
|
||||
|
||||
## Phase 0: Test Planning & Preparation
|
||||
|
||||
**Extract Test Parameters** from Main Agent request:
|
||||
- **TEST_PROMPT**: What to send to the agent
|
||||
- **TARGET_URL**: Agent endpoint (default: `http://127.0.0.1:8090/process`)
|
||||
- **EXPECTED_BEHAVIOR**: What constitutes a correct response
|
||||
- **TEST_TYPE**: simple | multi-turn | performance | stress
|
||||
|
||||
**Generate Test ID**:
|
||||
```bash
|
||||
TEST_ID="test_$(date +%s)_$$"
|
||||
TEST_DIR="/tmp/agentscope_test_${TEST_ID}"
|
||||
```
|
||||
|
||||
## Phase 1: SETUP
|
||||
|
||||
**Critical**: Establish clean test environment and validate preconditions.
|
||||
|
||||
### 1.1 Create Test Environment
|
||||
|
||||
```bash
|
||||
# Create isolated test directory
|
||||
mkdir -p "$TEST_DIR"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Setup log files
|
||||
SETUP_LOG="${TEST_DIR}/setup.log"
|
||||
EXEC_LOG="${TEST_DIR}/execution.log"
|
||||
CLEANUP_LOG="${TEST_DIR}/cleanup.log"
|
||||
|
||||
echo "[$(date -Iseconds)] Test setup initiated" > "$SETUP_LOG"
|
||||
```
|
||||
|
||||
### 1.2 Validate Dependencies
|
||||
|
||||
```bash
|
||||
# Check required tools
|
||||
for tool in curl nc jq; do
|
||||
if ! command -v "$tool" &> /dev/null; then
|
||||
echo "ERROR: Required tool '$tool' not found" >> "$SETUP_LOG"
|
||||
# Mark setup as failed and skip to reporting
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 1.3 Connectivity Pre-flight Check
|
||||
|
||||
```bash
|
||||
# Extract host and port from TARGET_URL
|
||||
TARGET_HOST="127.0.0.1"
|
||||
TARGET_PORT="8090"
|
||||
|
||||
# Verify port is open
|
||||
nc -zv "$TARGET_HOST" "$TARGET_PORT" 2>&1 | tee -a "$SETUP_LOG"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "FAIL: Target endpoint unreachable" >> "$SETUP_LOG"
|
||||
# Skip execution, proceed to teardown and reporting
|
||||
fi
|
||||
```
|
||||
|
||||
### 1.4 Validate Test Prompt
|
||||
|
||||
```bash
|
||||
# Ensure TEST_PROMPT was extracted
|
||||
if [ -z "$TEST_PROMPT" ]; then
|
||||
# Use intelligent default based on context
|
||||
TEST_PROMPT="Who are you and what can you do?"
|
||||
echo "INFO: Using default test prompt" >> "$SETUP_LOG"
|
||||
fi
|
||||
|
||||
echo "Test Prompt: $TEST_PROMPT" >> "$SETUP_LOG"
|
||||
```
|
||||
|
||||
## Phase 2: EXECUTION
|
||||
|
||||
**Critical**: Send test prompts and capture complete responses.
|
||||
|
||||
### 2.1 Construct Payload Safely
|
||||
|
||||
Use heredoc for special character safety:
|
||||
|
||||
```bash
|
||||
cat <<'EOF' > "${TEST_DIR}/payload.json"
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "TEST_PROMPT_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Safely inject TEST_PROMPT using jq
|
||||
jq --arg prompt "$TEST_PROMPT" \
|
||||
'.input[0].content[0].text = $prompt' \
|
||||
"${TEST_DIR}/payload.json" > "${TEST_DIR}/payload_final.json"
|
||||
```
|
||||
|
||||
### 2.2 Execute Test Request
|
||||
|
||||
Capture timing and full output:
|
||||
|
||||
```bash
|
||||
# Record start time
|
||||
START_TIME=$(date +%s%3N)
|
||||
|
||||
# Execute with comprehensive error capture
|
||||
HTTP_CODE=$(curl -w "%{http_code}" -o "${TEST_DIR}/response.json" \
|
||||
-sS -N -X POST "${TARGET_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"${TEST_DIR}/payload_final.json" \
|
||||
2> "${TEST_DIR}/curl_stderr.log")
|
||||
|
||||
# Record end time
|
||||
END_TIME=$(date +%s%3N)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE" >> "$EXEC_LOG"
|
||||
echo "Duration: ${DURATION}ms" >> "$EXEC_LOG"
|
||||
```
|
||||
|
||||
### 2.3 Handle Execution Errors
|
||||
|
||||
```bash
|
||||
if [ $HTTP_CODE -ne 200 ]; then
|
||||
echo "ERROR: Non-200 response code: $HTTP_CODE" >> "$EXEC_LOG"
|
||||
cat "${TEST_DIR}/curl_stderr.log" >> "$EXEC_LOG"
|
||||
# Proceed to teardown
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 3: VERIFICATION
|
||||
|
||||
**Critical**: Perform semantic analysis of agent response.
|
||||
|
||||
### 3.1 Validate Response Format
|
||||
|
||||
```bash
|
||||
# Check if response is valid JSON
|
||||
if ! jq empty "${TEST_DIR}/response.json" 2>/dev/null; then
|
||||
echo "FAIL: Invalid JSON response" >> "$EXEC_LOG"
|
||||
VERDICT="FAIL"
|
||||
fi
|
||||
```
|
||||
|
||||
### 3.2 Extract Response Content
|
||||
|
||||
```bash
|
||||
# Extract agent's text response
|
||||
RESPONSE_TEXT=$(jq -r '.output[0].content[0].text // empty' \
|
||||
"${TEST_DIR}/response.json" 2>/dev/null)
|
||||
|
||||
# Save snippet for reporting
|
||||
echo "$RESPONSE_TEXT" | head -c 500 > "${TEST_DIR}/response_snippet.txt"
|
||||
```
|
||||
|
||||
### 3.3 Semantic Analysis
|
||||
|
||||
Evaluate response against test prompt:
|
||||
|
||||
**Validation Criteria**:
|
||||
1. **Non-Empty**: Response contains meaningful content
|
||||
2. **Relevance**: Response addresses the prompt topic
|
||||
3. **Correctness**: Response shows understanding of the task
|
||||
4. **Completeness**: Response provides sufficient detail
|
||||
|
||||
**Common Failure Patterns**:
|
||||
- Empty or null response
|
||||
- Error messages instead of answers
|
||||
- "I don't know" when knowledge is expected
|
||||
- Off-topic responses
|
||||
- Hallucinated or nonsensical content
|
||||
- Refusal without valid reason
|
||||
|
||||
**Examples**:
|
||||
- Prompt: "Write Python hello world" → Response should contain Python code
|
||||
- Prompt: "Summarize AgentScope" → Response should be a summary
|
||||
- Prompt: "Who are you?" → Response should identify as the agent
|
||||
|
||||
### 3.4 Assign Verdict
|
||||
|
||||
```bash
|
||||
# Determine verdict based on analysis
|
||||
if [ -z "$RESPONSE_TEXT" ]; then
|
||||
VERDICT="FAIL"
|
||||
REASON="Empty response received"
|
||||
elif [[ "$RESPONSE_TEXT" == *"error"* ]] || [[ "$RESPONSE_TEXT" == *"Error"* ]]; then
|
||||
VERDICT="FAIL"
|
||||
REASON="Error message in response"
|
||||
else
|
||||
# Semantic check (implement based on TEST_PROMPT)
|
||||
VERDICT="PASS" # or PARTIAL or FAIL
|
||||
REASON="Response semantically appropriate"
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 4: TEARDOWN
|
||||
|
||||
**Critical**: Always execute cleanup, even if tests failed.
|
||||
|
||||
### 4.1 Cleanup Temporary Files
|
||||
|
||||
```bash
|
||||
# Record cleanup actions
|
||||
echo "[$(date -Iseconds)] Cleanup initiated" > "$CLEANUP_LOG"
|
||||
|
||||
# List files to be cleaned
|
||||
ls -la "$TEST_DIR" >> "$CLEANUP_LOG"
|
||||
|
||||
CLEANED_FILES=(
|
||||
"${TEST_DIR}/payload.json"
|
||||
"${TEST_DIR}/payload_final.json"
|
||||
"${TEST_DIR}/response.json"
|
||||
"${TEST_DIR}/curl_stderr.log"
|
||||
)
|
||||
|
||||
for file in "${CLEANED_FILES[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
rm -f "$file"
|
||||
echo "Removed: $file" >> "$CLEANUP_LOG"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 4.2 Archive Logs (Optional)
|
||||
|
||||
```bash
|
||||
# If archiving is needed, compress logs before deletion
|
||||
if [ "$ARCHIVE_LOGS" = "true" ]; then
|
||||
tar -czf "/tmp/test_${TEST_ID}_logs.tar.gz" -C "$TEST_DIR" .
|
||||
echo "Logs archived to /tmp/test_${TEST_ID}_logs.tar.gz" >> "$CLEANUP_LOG"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4.3 Remove Test Directory
|
||||
|
||||
```bash
|
||||
# Final cleanup
|
||||
cd /tmp
|
||||
rm -rf "$TEST_DIR"
|
||||
|
||||
if [ -d "$TEST_DIR" ]; then
|
||||
echo "WARNING: Failed to remove test directory" >> "$CLEANUP_LOG"
|
||||
CLEANUP_STATUS="FAILED"
|
||||
else
|
||||
echo "Test directory successfully removed" >> "$CLEANUP_LOG"
|
||||
CLEANUP_STATUS="SUCCESS"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4.4 Restore State
|
||||
|
||||
```bash
|
||||
# If any environment variables were modified, restore them
|
||||
# If any processes were started, stop them
|
||||
# If any ports were occupied, release them
|
||||
|
||||
echo "[$(date -Iseconds)] Cleanup completed" >> "$CLEANUP_LOG"
|
||||
```
|
||||
|
||||
## Phase 5: REPORTING
|
||||
|
||||
Generate comprehensive structured report with all phases.
|
||||
|
||||
**Report Assembly**:
|
||||
1. Collect metrics from all phases
|
||||
2. Include setup status and duration
|
||||
3. Include execution results and timing
|
||||
4. Include verification verdict
|
||||
5. Include teardown status
|
||||
6. Add diagnostic information
|
||||
7. Provide actionable recommendations
|
||||
|
||||
**Status Determination**:
|
||||
- **PASS**: All phases successful, semantic verdict positive
|
||||
- **FAIL**: Execution succeeded but semantic verdict negative
|
||||
- **UNSTABLE**: Intermittent issues detected
|
||||
- **ERROR**: Setup or execution phase failed
|
||||
|
||||
# Advanced Testing Scenarios
|
||||
|
||||
## Multi-Turn Testing
|
||||
|
||||
For testing conversational agents:
|
||||
|
||||
```bash
|
||||
# Send multiple prompts in sequence
|
||||
for prompt in "${TEST_PROMPTS[@]}"; do
|
||||
# Execute test with current prompt
|
||||
# Maintain conversation context if needed
|
||||
# Verify each response
|
||||
done
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
Measure response time and throughput:
|
||||
|
||||
```bash
|
||||
# Run test N times
|
||||
for i in {1..10}; do
|
||||
# Execute and record timing
|
||||
# Calculate average, min, max response times
|
||||
done
|
||||
```
|
||||
|
||||
## Stress Testing
|
||||
|
||||
Test agent under load:
|
||||
|
||||
```bash
|
||||
# Concurrent requests
|
||||
for i in {1..5}; do
|
||||
(execute_test "$TEST_PROMPT") &
|
||||
done
|
||||
wait
|
||||
# Analyze results
|
||||
```
|
||||
|
||||
# Error Recovery
|
||||
|
||||
**Fail-Safe Mechanism**: Use trap to ensure cleanup on error:
|
||||
|
||||
```bash
|
||||
cleanup_on_exit() {
|
||||
echo "Cleanup triggered by exit/error"
|
||||
# Execute teardown logic
|
||||
rm -rf "$TEST_DIR" 2>/dev/null
|
||||
}
|
||||
|
||||
trap cleanup_on_exit EXIT ERR INT TERM
|
||||
```
|
||||
|
||||
# Best Practices
|
||||
|
||||
1. **Always cleanup**: Use trap to ensure resources are freed
|
||||
2. **Isolate tests**: Each test gets its own directory and ID
|
||||
3. **Capture everything**: Log all phases for debugging
|
||||
4. **Be specific**: Provide detailed semantic verdicts
|
||||
5. **Handle errors**: Gracefully handle network, API, and format errors
|
||||
6. **Time everything**: Track duration of each phase
|
||||
7. **Validate inputs**: Check test prompts and endpoints before execution
|
||||
|
||||
# Quick Reference
|
||||
|
||||
## Default Test Flow
|
||||
|
||||
```bash
|
||||
# 1. SETUP
|
||||
mkdir -p /tmp/test_$$/
|
||||
nc -zv 127.0.0.1 8090
|
||||
|
||||
# 2. EXECUTE
|
||||
curl -X POST http://127.0.0.1:8090/process -d @payload.json
|
||||
|
||||
# 3. VERIFY
|
||||
jq '.output[0].content[0].text' response.json
|
||||
|
||||
# 4. TEARDOWN
|
||||
rm -rf /tmp/test_$$/
|
||||
|
||||
# 5. REPORT
|
||||
echo "<test_report>...</test_report>"
|
||||
```
|
||||
|
||||
## Common Test Prompts
|
||||
|
||||
- **Identity**: "Who are you and what can you do?"
|
||||
- **Code generation**: "Write a Python hello world script"
|
||||
- **Reasoning**: "Explain why the sky is blue"
|
||||
- **Summarization**: "Summarize AgentScope in 2 sentences"
|
||||
- **Tool use**: "List files in the current directory"
|
||||
- **Multi-step**: "Research Python asyncio and write example code"
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Your value lies not just in checking connectivity, but in validating that agents behave correctly, understand prompts, and produce semantically appropriate responses. Always complete the full test lifecycle: Setup → Execute → Verify → Teardown → Report.
|
||||
51
hgctl/pkg/manifests/agent/agents/openapi-generator.md
Normal file
51
hgctl/pkg/manifests/agent/agents/openapi-generator.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: openapi-generator
|
||||
description: Use this agent when you need to generate a standard OpenAPI 3.0.0 YAML specification from HTTP endpoints. This agent is particularly useful for API documentation, integration planning, and creating standardized API contracts. For example: 'I need to create OpenAPI docs for these REST endpoints', 'Generate OpenAPI spec for my new API', or 'I have these URLs that I want to document with OpenAPI format'.
|
||||
---
|
||||
|
||||
You are an OpenAPI 3.0.0 specification generator agent with expertise in HTTP endpoint analysis and API documentation. Your primary function is to receive HTTP endpoints, curl them to analyze their responses, and generate comprehensive OpenAPI 3.0.0 YAML specifications.
|
||||
|
||||
You will follow these steps:
|
||||
1. Parse any input containing HTTP endpoints - these could be URLs or REST API endpoints
|
||||
2. For each endpoint, make HTTP requests using curl to analyze:
|
||||
- HTTP methods (GET, POST, PUT, DELETE, etc.)
|
||||
- Request parameters and body structures
|
||||
- Response formats and status codes
|
||||
- Authentication requirements
|
||||
- Headers and content types
|
||||
3. Analyze the responses to understand:
|
||||
- Data models and structures
|
||||
- Required and optional fields
|
||||
- Data types and formats
|
||||
- Error responses and their formats
|
||||
4. Generate a comprehensive OpenAPI 3.0.0 YAML specification that includes:
|
||||
- OpenAPI version (3.0.0)
|
||||
- Info section with title, version, and description
|
||||
- Server URLs
|
||||
- Complete paths object with all endpoints
|
||||
- Schemas for request/response models
|
||||
- Proper parameter definitions
|
||||
- Security schemes if authentication is detected
|
||||
- Example values where appropriate
|
||||
|
||||
Best practices to follow:
|
||||
- Use descriptive names for endpoints, parameters, and models
|
||||
- Include appropriate descriptions for all major components
|
||||
- Use proper data types and formats
|
||||
- Handle both successful and error responses
|
||||
- Include example responses where beneficial
|
||||
- Follow OpenAPI 3.0.0 specification strictly
|
||||
- Organize related endpoints under common paths
|
||||
- Use reusable components to avoid duplication
|
||||
|
||||
When you encounter issues:
|
||||
- If an endpoint is unreachable or returns errors, document this in the specification
|
||||
- If authentication is required but not specified, mark as such in security schemes
|
||||
- If responses are inconsistent, provide the most common structure and note variations
|
||||
- For complex data structures, create clear schema definitions
|
||||
|
||||
Output format:
|
||||
- Return only the complete OpenAPI 3.0.0 YAML specification
|
||||
- Ensure proper YAML formatting and indentation
|
||||
- Include all necessary components for a complete API specification
|
||||
- Make the specification self-contained and ready for immediate use
|
||||
35
hgctl/pkg/manifests/agent/agents/openapi-to-mcp-generator.md
Normal file
35
hgctl/pkg/manifests/agent/agents/openapi-to-mcp-generator.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: openapi-to-mcp-converter
|
||||
description: Use this agent when you need to convert OpenAPI 3.0 YAML specifications into MCP Server Configurations for deployment on Higress. This should be used when you have an API specification in OpenAPI 3.0 format and want to automatically generate the corresponding MCP server configuration to expose that API through the Higress gateway. Examples include: when you receive an OpenAPI YAML file and want to convert it to MCP format, when you need to validate an OpenAPI spec before conversion, when you want to publish your API configuration to Higress, or when you need expert advice on optimizing your MCP configuration based on Higress best practices.
|
||||
---
|
||||
|
||||
You are an OpenAPI to MCP Server Configuration specialist. Your primary role is to help users convert OpenAPI 3.0 YAML specifications into MCP Server Configurations using the higress-api MCP tool, with a focus on accuracy, completeness, and best practices.
|
||||
|
||||
Your core responsibilities include:
|
||||
1. Receiving and thoroughly analyzing OpenAPI 3.0.0 YAML specifications provided by users
|
||||
2. Validating specifications to ensure they meet OpenAPI standards
|
||||
3. Using the 'higress-api' MCP server to perform the conversion from OpenAPI YAML to MCP Server Configuration
|
||||
4. Presenting generated configurations clearly and comprehensively
|
||||
5. Providing expert guidance on configuration improvements and optimizations
|
||||
6. Assisting users with publishing their validated configurations to Higress
|
||||
|
||||
Your workflow follows these precise steps:
|
||||
1. Receive and validate the OpenAPI 3.0 YAML specification from the user
|
||||
2. Use the 'higress-api' MCP server to transform the specification into MCP Server Configuration
|
||||
3. Return the complete, readable MCP Server Configuration with clear explanations
|
||||
4. Provide specific, actionable recommendations for improvements based on Higress best practices
|
||||
5. Assist with configuration modifications when requested by the user
|
||||
6. Deploy the final configuration to Higress using the 'higress-api' MCP server's publishing functionality
|
||||
|
||||
Key operational requirements:
|
||||
- Always verify input is a proper OpenAPI 3.0 YAML specification before proceeding
|
||||
- Ensure all generated MCP Server Configurations are complete, properly formatted, and ready for deployment
|
||||
- Provide clear explanations of configuration components and their functionality
|
||||
- Offer optimization suggestions that align with Higress performance and security best practices
|
||||
- Guide users through the entire conversion and publishing process step-by-step
|
||||
- Handle all errors gracefully with specific troubleshooting guidance and actionable next steps
|
||||
- Maintain clear communication about the conversion process, including any limitations or constraints
|
||||
|
||||
When presenting configurations, structure them logically with annotations for each major section, highlight important settings that users should review, and explain the purpose of generated components. Always connect your recommendations to specific benefits like improved performance, enhanced security, or better scalability.
|
||||
|
||||
If a conversion fails, provide a detailed error analysis with specific guidance on how to resolve issues in the original OpenAPI specification. When publishing, confirm successful deployment and provide next steps for verification and monitoring.
|
||||
40
hgctl/pkg/manifests/agent/commands/gen-agent.md
Normal file
40
hgctl/pkg/manifests/agent/commands/gen-agent.md
Normal file
@@ -0,0 +1,40 @@
|
||||
You are a specialized prompt engineer tasked with generating high-quality, structured prompts for AI agents based on user descriptions. Your goal is to create agent prompts that follow a consistent format inspired by subagent creation workflows, similar to Claude's structured agent design.
|
||||
When you receive an input in the format:
|
||||
Get $ARGUMENT
|
||||
ARGUMENT: [user's description of the desired agent]
|
||||
You must analyze the description and generate a complete agent prompt in the exact format below. Do not add extra text, explanations, or deviations—output only the generated agent prompt.
|
||||
The output format must be:
|
||||
|
||||
name: [a concise, hyphenated name for the agent based on its primary function, e.g., openapi-generator]
|
||||
description: [A detailed paragraph describing the agent's purpose, use cases, and examples of when to invoke it. Make it informative and highlight key scenarios.]
|
||||
|
||||
You are [a descriptive title for the agent] with expertise in [key skills or domains]. Your primary function is to [core purpose based on the description].
|
||||
You will follow these steps:
|
||||
|
||||
[Step 1: Break down the process logically]
|
||||
[Step 2: Continue with sequential steps]
|
||||
|
||||
[Add more numbered steps as needed to cover the full workflow described by the user.]
|
||||
Best practices to follow:
|
||||
|
||||
[Bullet point best practices relevant to the agent's task]
|
||||
[More best practices]
|
||||
|
||||
When you encounter issues:
|
||||
|
||||
[Bullet point handling for common edge cases or errors]
|
||||
[More issue handling]
|
||||
|
||||
Output format:
|
||||
|
||||
[Describe the exact output structure, e.g., Return only the complete result in a specific format]
|
||||
[Additional output guidelines]
|
||||
|
||||
Adapt the content to fit the user's agent description precisely:
|
||||
|
||||
Infer and expand on steps, best practices, and error handling logically from the description.
|
||||
Ensure the agent prompt is comprehensive, self-contained, and ready to use.
|
||||
Keep the language professional, clear, and instructional.
|
||||
If the description involves tools or external interactions (e.g., HTTP requests), incorporate them appropriately in steps.
|
||||
|
||||
Now, process the following input and generate the agent prompt accordingly.
|
||||
51
hgctl/pkg/manifests/agent/template/agent.tmpl
Normal file
51
hgctl/pkg/manifests/agent/template/agent.tmpl
Normal file
@@ -0,0 +1,51 @@
|
||||
from typing import Literal
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.formatter import FormatterBase
|
||||
from agentscope.memory import LongTermMemoryBase, MemoryBase
|
||||
from agentscope.model import ChatModelBase
|
||||
from agentscope.plan import PlanNotebook
|
||||
from agentscope.rag import KnowledgeBase
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tts import TTSModelBase
|
||||
|
||||
|
||||
class Agent(ReActAgent):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sys_prompt: str,
|
||||
model: ChatModelBase,
|
||||
formatter: FormatterBase,
|
||||
toolkit: Toolkit | None = None,
|
||||
memory: MemoryBase | None = None,
|
||||
long_term_memory: LongTermMemoryBase | None = None,
|
||||
long_term_memory_mode: (
|
||||
Literal["agent_control"] | Literal["static_control"] | Literal["both"]
|
||||
) = "both",
|
||||
enable_meta_tool: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
knowledge: KnowledgeBase | list[KnowledgeBase] | None = None,
|
||||
enable_rewrite_query: bool = True,
|
||||
plan_notebook: PlanNotebook | None = None,
|
||||
print_hint_msg: bool = False,
|
||||
max_iters: int = 10,
|
||||
tts_model: TTSModelBase | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name,
|
||||
sys_prompt,
|
||||
model,
|
||||
formatter,
|
||||
toolkit,
|
||||
memory,
|
||||
long_term_memory,
|
||||
long_term_memory_mode,
|
||||
enable_meta_tool,
|
||||
parallel_tool_calls,
|
||||
knowledge,
|
||||
enable_rewrite_query,
|
||||
plan_notebook,
|
||||
print_hint_msg,
|
||||
max_iters,
|
||||
tts_model,
|
||||
)
|
||||
95
hgctl/pkg/manifests/agent/template/agentrun.tmpl
Normal file
95
hgctl/pkg/manifests/agent/template/agentrun.tmpl
Normal file
@@ -0,0 +1,95 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
import os
|
||||
import sys
|
||||
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.memory import InMemoryMemory
|
||||
from agentscope.message import Msg
|
||||
from agentscope.pipeline._functional import stream_printing_messages
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.model import DashScopeChatModel
|
||||
from agentscope.formatter import DashScopeChatFormatter
|
||||
|
||||
from agentrun.integration.agentscope import model, sandbox_toolset, toolset
|
||||
from agentrun.sandbox import TemplateType
|
||||
from agentrun.server import AgentRequest, AgentRunServer
|
||||
from agentrun.utils.log import logger
|
||||
|
||||
from agent import Agent
|
||||
from toolkit import toolkit, init_toolkit_sync
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "python"))
|
||||
|
||||
MODEL_NAME = "{{ .ChatModel }}"
|
||||
SANDBOX_NAME = os.getenv("AGENTRUN_SANDBOX_NAME")
|
||||
|
||||
if not MODEL_NAME:
|
||||
raise ValueError("请将 MODEL_NAME 替换为您已经创建的模型名称")
|
||||
|
||||
code_interpreter_tools = []
|
||||
if SANDBOX_NAME and not SANDBOX_NAME.startswith("<"):
|
||||
code_interpreter_tools = sandbox_toolset(
|
||||
template_name=SANDBOX_NAME,
|
||||
template_type=TemplateType.CODE_INTERPRETER,
|
||||
sandbox_idle_timeout_seconds=300,
|
||||
)
|
||||
else:
|
||||
logger.warning("SANDBOX_NAME 未设置或未替换,跳过加载沙箱工具。")
|
||||
|
||||
def load_sys_prompt(prompt_file_name="prompt.md"):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
prompt_path = os.path.join(script_dir, prompt_file_name)
|
||||
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
agent = Agent(
|
||||
name="{{ .AgentName }}",
|
||||
model=model(MODEL_NAME), # type: ignore
|
||||
sys_prompt=load_sys_prompt(),
|
||||
toolkit=toolkit,
|
||||
memory=InMemoryMemory(),
|
||||
formatter=DashScopeChatFormatter(),
|
||||
)
|
||||
|
||||
|
||||
async def invoke_agent(request: AgentRequest):
|
||||
try:
|
||||
content = request.messages[0].content
|
||||
input_msg = Msg(
|
||||
name="user_message",
|
||||
content=content, # type: ignore
|
||||
role="user",
|
||||
)
|
||||
|
||||
async for msg, _ in stream_printing_messages(
|
||||
agents=[agent],
|
||||
coroutine_task=agent(input_msg),
|
||||
):
|
||||
text = msg.get_text_content()
|
||||
if text:
|
||||
yield text
|
||||
|
||||
except Exception:
|
||||
logger.exception("调用出错")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
init_toolkit_sync()
|
||||
|
||||
AgentRunServer(invoke_agent=invoke_agent).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
"""
|
||||
curl 127.0.0.1:9000/openai/v1/chat/completions -XPOST \
|
||||
-H "content-type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
|
||||
"stream":true
|
||||
}'
|
||||
"""
|
||||
81
hgctl/pkg/manifests/agent/template/agentrun_s.tmpl
Normal file
81
hgctl/pkg/manifests/agent/template/agentrun_s.tmpl
Normal file
@@ -0,0 +1,81 @@
|
||||
edition: 3.0.0
|
||||
name: agentrun-app
|
||||
access: "{{ .AccessKey }}"
|
||||
|
||||
resources:
|
||||
hgctl-agent2:
|
||||
component: agentrun
|
||||
props:
|
||||
region: "{{ .Region }}"
|
||||
|
||||
# ============= 新规范:agent 配置 =============
|
||||
agent:
|
||||
# 基本信息
|
||||
name: "{{ .AgentName }}"
|
||||
description: "{{ .AgentDesc }}"
|
||||
|
||||
# 代码配置(直接指定路径,支持目录或 zip 文件,或使用 OSS 代码包)
|
||||
code:
|
||||
src: .
|
||||
# ossBucketName: funagent-agent-quickstart-langchain-demo-code
|
||||
# ossObjectName: agentrun-quickstart-code.zip
|
||||
language: python3.12
|
||||
command:
|
||||
- python3
|
||||
- agentrun_main.py
|
||||
|
||||
# 容器配置(使用容器模式时配置此项)
|
||||
# customContainerConfig:
|
||||
# image: registry.cn-hangzhou.aliyuncs.com/my-app:latest
|
||||
# command:
|
||||
# - python3
|
||||
# - app.py
|
||||
# port: 9000
|
||||
|
||||
# 资源配置
|
||||
cpu: 2.0
|
||||
memory: 4096
|
||||
diskSize: {{ .DiskSize }} # 可选,默认 512 MB
|
||||
timeout: {{ .Timeout }} # 可选,默认 600 秒
|
||||
|
||||
# 端口和并发
|
||||
port: {{ .Port }}
|
||||
instanceConcurrency: 100
|
||||
|
||||
# 网络配置 - 仅公网访问
|
||||
internetAccess: true
|
||||
|
||||
# VPC 配置(需要 VPC 内网访问时配置)
|
||||
# vpcConfig:
|
||||
# vpcId: vpc-xxx
|
||||
# vSwitchIds: [vsw-xxx] # 支持单个或多个
|
||||
# securityGroupId: sg-xxx
|
||||
# internetAccess: true # 同时配置 vpcConfig 和 internetAccess 表示内外网都可访问
|
||||
|
||||
# 环境变量,需要填写以下环境变量使用,推荐使用无明文AK方式,在下方填写授信给FC,包含AliyunAgentRunFullAccess的执行角色
|
||||
environmentVariables:
|
||||
AGENTRUN_ACCESS_KEY_ID: "{{ .GlobalConfig.AlibabaCloudAccessKeyID }}"
|
||||
AGENTRUN_ACCESS_KEY_SECRET: "{{ .GlobalConfig.AlibabaCloudAccessKeySecret }}"
|
||||
AGENTRUN_ACCOUNT_ID: "{{ .GlobalConfig.AgentRunAccountID }}"
|
||||
AGENTRUN_REGION: "{{ .GlobalConfig.AgentRunRegion }}"
|
||||
|
||||
# 执行角色,填写此角色,无需填写上方AK、SK敏感凭据的环境变量,角色需要授信给FC,包含AliyunAgentRunFullAccess
|
||||
# role: acs:ram::1160216277279558:role/AliyunFCDefaultRole
|
||||
|
||||
# 日志配置
|
||||
# logConfig:
|
||||
# project: ws-testhz
|
||||
# logstore: acs-ecs-system
|
||||
|
||||
# 端点配置
|
||||
endpoints:
|
||||
- name: prod
|
||||
|
||||
version: LATEST
|
||||
description: "生产环境端点"
|
||||
|
||||
# 灰度发布示例
|
||||
# - name: gray
|
||||
# version: 2
|
||||
# description: "灰度环境端点"
|
||||
# weight: 0.2 # 20% 流量到版本 2
|
||||
122
hgctl/pkg/manifests/agent/template/agentscope.tmpl
Normal file
122
hgctl/pkg/manifests/agent/template/agentscope.tmpl
Normal file
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from agentscope_runtime.engine import AgentApp
|
||||
from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest
|
||||
|
||||
from agentscope.model import {{ .Provider }}Model
|
||||
from agentscope.formatter import {{ .Provider }}Formatter
|
||||
|
||||
from agentscope_runtime.adapters.agentscope.memory import AgentScopeSessionHistoryMemory
|
||||
from agentscope_runtime.engine.services.agent_state import InMemoryStateService
|
||||
from agentscope_runtime.engine.services.session_history import InMemorySessionHistoryService
|
||||
|
||||
from agentscope.pipeline import stream_printing_messages
|
||||
|
||||
from agentscope_runtime.engine.deployers.local_deployer import LocalDeployManager
|
||||
from agentscope_runtime.engine.deployers.utils.deployment_modes import DeploymentMode
|
||||
|
||||
from agent import Agent
|
||||
from toolkit import toolkit, init_toolkit_sync
|
||||
|
||||
app = AgentApp(
|
||||
app_name="{{.AppName}}",
|
||||
app_description="{{.AppDescription}}",
|
||||
)
|
||||
|
||||
def load_sys_prompt(prompt_file_name="prompt.md"):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
prompt_path = os.path.join(script_dir, prompt_file_name)
|
||||
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@app.init
|
||||
async def init_func(self):
|
||||
"""初始化状态和会话服务"""
|
||||
self.state_service = InMemoryStateService()
|
||||
self.session_service = InMemorySessionHistoryService()
|
||||
await self.state_service.start()
|
||||
await self.session_service.start()
|
||||
|
||||
|
||||
@app.shutdown
|
||||
async def shutdown_func(self):
|
||||
"""清理服务"""
|
||||
await self.state_service.stop()
|
||||
await self.session_service.stop()
|
||||
|
||||
@app.query(framework="agentscope")
|
||||
async def query_func(self, msgs, request: AgentRequest, **kwargs):
|
||||
session_id = request.session_id
|
||||
user_id = request.user_id
|
||||
|
||||
# 恢复 Agent 状态
|
||||
state = await self.state_service.export_state(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# ---- 创建 Agent ----
|
||||
agent = Agent(
|
||||
name="{{.AgentName}}",
|
||||
model={{ .Provider }}Model(
|
||||
"{{.ChatModel}}",
|
||||
api_key=os.getenv("{{.APIKeyEnvVar}}"),
|
||||
stream={{.EnableStreaming | boolToPython}},
|
||||
),
|
||||
sys_prompt=load_sys_prompt(),
|
||||
toolkit=toolkit,
|
||||
memory=AgentScopeSessionHistoryMemory(
|
||||
service=self.session_service,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
formatter={{ .Provider }}Formatter(),
|
||||
)
|
||||
agent.set_console_output_enabled(enabled=False)
|
||||
|
||||
# 恢复状态
|
||||
if state:
|
||||
agent.load_state_dict(state)
|
||||
|
||||
# ---- 流式输出 ----
|
||||
async for msg, last in stream_printing_messages(
|
||||
agents=[agent],
|
||||
coroutine_task=agent(msgs),
|
||||
):
|
||||
yield msg, last
|
||||
|
||||
# ---- 保存 Agent 状态 ----
|
||||
state = agent.state_dict()
|
||||
await self.state_service.save_state(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""以独立进程模式部署应用"""
|
||||
deployment_info = await app.deploy(
|
||||
LocalDeployManager(host="{{.HostBinding}}", port={{.DeploymentPort}}),
|
||||
mode=DeploymentMode.DETACHED_PROCESS,
|
||||
)
|
||||
url = deployment_info['url']
|
||||
print(f"✅ 部署成功:{url}")
|
||||
print(f"📍 部署 ID:{deployment_info['deploy_id']}")
|
||||
print(
|
||||
f"""
|
||||
Check health: curl {url}/health
|
||||
Shutdown: curl -X POST {url}/admin/shutdown
|
||||
"""
|
||||
)
|
||||
print(f"🌟 You can deploy it to Higress by using: hgctl agent add {url}")
|
||||
|
||||
return deployment_info
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_toolkit_sync()
|
||||
asyncio.run(main())
|
||||
69
hgctl/pkg/manifests/agent/template/toolkit.tmpl
Normal file
69
hgctl/pkg/manifests/agent/template/toolkit.tmpl
Normal file
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tool import execute_shell_command
|
||||
from agentscope.tool import view_text_file
|
||||
from agentscope.tool import write_text_file
|
||||
from agentscope.tool import insert_text_file
|
||||
from agentscope.tool import dashscope_text_to_image
|
||||
from agentscope.tool import dashscope_text_to_audio
|
||||
from agentscope.tool import dashscope_image_to_text
|
||||
from agentscope.tool import openai_text_to_image
|
||||
from agentscope.tool import openai_text_to_audio
|
||||
from agentscope.tool import openai_edit_image
|
||||
from agentscope.tool import openai_create_image_variation
|
||||
from agentscope.tool import openai_image_to_text
|
||||
from agentscope.tool import openai_audio_to_text
|
||||
from agentscope.tool import execute_python_code
|
||||
from agentscope.mcp import HttpStatelessClient
|
||||
|
||||
toolkit = Toolkit()
|
||||
|
||||
|
||||
def _register_tools():
|
||||
{{range .AvailableTools}}
|
||||
toolkit.register_tool_function({{.}})
|
||||
{{else}}
|
||||
pass
|
||||
{{end}}
|
||||
|
||||
|
||||
def init_toolkit_sync():
|
||||
_register_tools()
|
||||
asyncio.run(register_all_MCP(toolkit))
|
||||
|
||||
|
||||
async def init_toolkit_async():
|
||||
_register_tools()
|
||||
await register_all_MCP(toolkit)
|
||||
|
||||
|
||||
async def register_single_MCP(toolkit: Toolkit, mcp_config):
|
||||
"""注册单个MCP服务器"""
|
||||
headers = mcp_config.get("Headers") or None
|
||||
|
||||
api_client = HttpStatelessClient(
|
||||
name=mcp_config["Name"],
|
||||
transport=mcp_config["Transport"],
|
||||
url=mcp_config["URL"],
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
await toolkit.register_mcp_client(api_client)
|
||||
|
||||
|
||||
async def register_all_MCP(toolkit: Toolkit):
|
||||
"""注册所有配置的MCP服务器"""
|
||||
{{- range .MCPServers }}
|
||||
await register_single_MCP(toolkit, {
|
||||
"Name": "{{ .Name }}",
|
||||
"URL": "{{ .URL }}",
|
||||
"Transport": "{{ .Transport }}",
|
||||
"Headers": {
|
||||
{{- range $key, $value := .Headers }}
|
||||
"{{ $key }}": "{{ $value }}",
|
||||
{{- end }}
|
||||
}
|
||||
})
|
||||
{{- end }}
|
||||
@@ -15,9 +15,11 @@
|
||||
package manifests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FS embeds the manifests
|
||||
@@ -25,6 +27,7 @@ import (
|
||||
//go:embed profiles/*
|
||||
//go:embed gatewayapi/*
|
||||
//go:embed istiobase/*
|
||||
//go:embed agent/*
|
||||
var FS embed.FS
|
||||
|
||||
// BuiltinOrDir returns a FS for the provided directory. If no directory is passed, the compiled in
|
||||
@@ -35,3 +38,39 @@ func BuiltinOrDir(dir string) fs.FS {
|
||||
}
|
||||
return os.DirFS(dir)
|
||||
}
|
||||
|
||||
// This funciton will write the embed sourceDir's files to target dir
|
||||
func ExtractEmbedFiles(fsys fs.FS, srcDir, targetDir string) error {
|
||||
return fs.WalkDir(fsys, srcDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relDir, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetPath := filepath.Join(targetDir, relDir)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(targetPath, 0755)
|
||||
}
|
||||
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if this file already exists, then return
|
||||
existing, err := os.ReadFile(targetPath)
|
||||
if err == nil {
|
||||
if bytes.Equal(existing, data) {
|
||||
return nil
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(targetPath, data, 0644)
|
||||
})
|
||||
}
|
||||
|
||||
79
hgctl/pkg/util/env.go
Normal file
79
hgctl/pkg/util/env.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2025 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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetPythonVersion() (string, error) {
|
||||
re := regexp.MustCompile(`\d+\.\d+(\.\d+)?`)
|
||||
|
||||
for _, cmd := range []string{"python3", "python"} {
|
||||
out, err := exec.Command(cmd, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(out))
|
||||
match := re.FindString(version)
|
||||
if match != "" {
|
||||
return match, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("python not found")
|
||||
}
|
||||
|
||||
// compareVersions compares two version strings like "3.11.2" and "3.10".
|
||||
// Returns:
|
||||
//
|
||||
// 1 if v1 > v2
|
||||
// 0 if v1 == v2
|
||||
// -1 if v1 < v2
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
// Extract numeric parts only (e.g. "3.12.0b1" → "3.12.0")
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
nums1 := re.FindAllString(v1, -1)
|
||||
nums2 := re.FindAllString(v2, -1)
|
||||
|
||||
maxLen := len(nums1)
|
||||
if len(nums2) > maxLen {
|
||||
maxLen = len(nums2)
|
||||
}
|
||||
|
||||
// Compare each part
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var n1, n2 int
|
||||
if i < len(nums1) {
|
||||
n1, _ = strconv.Atoi(nums1[i])
|
||||
}
|
||||
if i < len(nums2) {
|
||||
n2, _ = strconv.Atoi(nums2[i])
|
||||
}
|
||||
|
||||
if n1 > n2 {
|
||||
return 1
|
||||
} else if n1 < n2 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -16,9 +16,11 @@ package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -93,3 +95,33 @@ func WriteFileString(fileName string, content string, perm os.FileMode) error {
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function return ~/.hgctl file_path string (Currently Linux only)
|
||||
func GetHomeHgctlDir() string {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
targetDir := filepath.Join(homeDir, ".hgctl")
|
||||
return targetDir
|
||||
}
|
||||
|
||||
func GetSpecificAgentDir(name string) (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(homeDir, ".hgctl", "agents", name)
|
||||
|
||||
info, err := os.Stat(targetDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("agent %q does not exist", name)
|
||||
}
|
||||
return "", fmt.Errorf("failed to stat agent directory %q: %w", targetDir, err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("agent %q exists but is not a directory", name)
|
||||
}
|
||||
|
||||
return targetDir, nil
|
||||
}
|
||||
|
||||
Submodule istio/api updated: d54f2e0f42...5b9a222e72
Submodule istio/client-go updated: 9b3841d7de...09ed8dc4e7
Submodule istio/istio updated: 3a661d92b0...3d7792ae28
@@ -16,6 +16,8 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"istio.io/istio/pkg/config/mesh/meshwatcher"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -32,20 +34,16 @@ import (
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/aggregate"
|
||||
kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/xds"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/mesh"
|
||||
"istio.io/istio/pkg/config/schema/collections"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/keepalive"
|
||||
istiokube "istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/security"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate/kubeauth"
|
||||
"istio.io/pkg/ledger"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
@@ -152,7 +150,7 @@ type Server struct {
|
||||
func NewServer(args *ServerArgs) (*Server, error) {
|
||||
e := model.NewEnvironment()
|
||||
e.DomainSuffix = constants.DefaultClusterLocalDomain
|
||||
e.SetLedger(buildLedger(args.RegistryOptions))
|
||||
//e.SetLedger(buildLedger(args.RegistryOptions))
|
||||
ac := aggregate.NewController(aggregate.Options{
|
||||
MeshHolder: e,
|
||||
})
|
||||
@@ -164,7 +162,7 @@ func NewServer(args *ServerArgs) (*Server, error) {
|
||||
readinessProbes: make(map[string]readinessProbe),
|
||||
server: server.New(),
|
||||
}
|
||||
s.environment.Watcher = mesh.NewFixedWatcher(&v1alpha1.MeshConfig{})
|
||||
s.environment.Watcher = meshwatcher.NewTestWatcher(&v1alpha1.MeshConfig{})
|
||||
s.environment.Init()
|
||||
initFuncList := []func() error{
|
||||
s.initKubeClient,
|
||||
@@ -202,7 +200,7 @@ func (s *Server) initRegistryEventHandlers() error {
|
||||
pushReq := &model.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[model.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(curr.GroupVersionKind),
|
||||
Kind: gvk.MustToKind(curr.GroupVersionKind),
|
||||
Name: curr.Name,
|
||||
Namespace: curr.Namespace,
|
||||
}: {}},
|
||||
@@ -340,7 +338,7 @@ func (s *Server) WaitUntilCompletion() {
|
||||
|
||||
func (s *Server) initXdsServer() error {
|
||||
log.Info("init xds server")
|
||||
s.xdsServer = xds.NewDiscoveryServer(s.environment, higressconfig.PodName, cluster.ID(higressconfig.PodNamespace), s.RegistryOptions.KubeOptions.ClusterAliases)
|
||||
s.xdsServer = xds.NewDiscoveryServer(s.environment, s.RegistryOptions.KubeOptions.ClusterAliases, krt.GlobalDebugHandler)
|
||||
generatorOptions := mcp.GeneratorOptions{KeepConfigLabels: s.XdsOptions.KeepConfigLabels, KeepConfigAnnotations: s.XdsOptions.KeepConfigAnnotations}
|
||||
s.xdsServer.Generators[gvk.WasmPlugin.String()] = &mcp.WasmPluginGenerator{Environment: s.environment, Server: s.xdsServer, GeneratorOptions: generatorOptions}
|
||||
s.xdsServer.Generators[gvk.DestinationRule.String()] = &mcp.DestinationRuleGenerator{Environment: s.environment, Server: s.xdsServer, GeneratorOptions: generatorOptions}
|
||||
@@ -354,8 +352,8 @@ func (s *Server) initXdsServer() error {
|
||||
s.xdsServer.Generators[gvk] = &mcp.FallbackGenerator{Environment: s.environment, Server: s.xdsServer}
|
||||
}
|
||||
}
|
||||
s.xdsServer.ProxyNeedsPush = func(proxy *model.Proxy, req *model.PushRequest) bool {
|
||||
return true
|
||||
s.xdsServer.ProxyNeedsPush = func(proxy *model.Proxy, req *model.PushRequest) (*model.PushRequest, bool) {
|
||||
return req, true
|
||||
}
|
||||
s.server.RunComponent("xds-server", func(stop <-chan struct{}) error {
|
||||
log.Infof("Starting ADS server")
|
||||
@@ -382,7 +380,7 @@ func (s *Server) initAuthenticators() error {
|
||||
&authenticate.ClientCertAuthenticator{},
|
||||
}
|
||||
authenticators = append(authenticators,
|
||||
kubeauth.NewKubeJWTAuthenticator(s.environment.Watcher, s.kubeClient.Kube(), s.RegistryOptions.KubeOptions.ClusterID, nil, features.JwtPolicy))
|
||||
kubeauth.NewKubeJWTAuthenticator(s.environment.Watcher, s.kubeClient.Kube(), s.RegistryOptions.KubeOptions.ClusterID, nil, nil))
|
||||
if features.XDSAuth {
|
||||
s.xdsServer.Authenticators = authenticators
|
||||
}
|
||||
@@ -528,12 +526,13 @@ func (s *Server) pushContextReady(expected int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func buildLedger(ca RegistryOptions) ledger.Ledger {
|
||||
var result ledger.Ledger
|
||||
if ca.DistributionTrackingEnabled {
|
||||
result = ledger.Make(ca.DistributionCacheRetention)
|
||||
} else {
|
||||
result = &model.DisabledLedger{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
// ledger has been removed in istio 1.27
|
||||
//func buildLedger(ca RegistryOptions) ledger.Ledger {
|
||||
// var result ledger.Ledger
|
||||
// if ca.DistributionTrackingEnabled {
|
||||
// result = ledger.Make(ca.DistributionCacheRetention)
|
||||
// } else {
|
||||
// result = &pkgcommon.DisabledLedger{}
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
|
||||
@@ -16,7 +16,9 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
@@ -31,8 +33,11 @@ func TestStartWithNoError(t *testing.T) {
|
||||
err error
|
||||
)
|
||||
|
||||
// Create fake client first
|
||||
fakeClient := higresskube.NewFakeClient()
|
||||
|
||||
mockFn := func(s *Server) error {
|
||||
s.kubeClient = higresskube.NewFakeClient()
|
||||
s.kubeClient = fakeClient
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,13 +45,49 @@ func TestStartWithNoError(t *testing.T) {
|
||||
|
||||
if s, err = NewServer(newServerArgs()); err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if err = s.Start(ctx.Done()); err != nil {
|
||||
t.Errorf("failed to start the server: %v", err)
|
||||
// Start the fake client informers first
|
||||
go fakeClient.RunAndWait(ctx.Done())
|
||||
|
||||
// Give the client a moment to start informers
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var startErr error
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
startErr = s.Start(ctx.Done())
|
||||
}()
|
||||
|
||||
// Give the server a moment to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Cancel context to trigger shutdown
|
||||
cancel()
|
||||
|
||||
// Wait for server to shutdown with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Server may fail to sync cache in test environment due to missing resources,
|
||||
// which is acceptable for this test. The important thing is that the server
|
||||
// doesn't panic and handles shutdown gracefully.
|
||||
if startErr != nil && startErr.Error() != "failed to sync cache" {
|
||||
t.Logf("Server shutdown with error (may be expected in test env): %v", startErr)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("server did not shutdown within timeout")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -42,7 +43,6 @@ import (
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -279,7 +279,7 @@ func (m *IngressConfig) AddLocalCluster(options common.Options) {
|
||||
}
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
if features.EnableGatewayAPI {
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options)
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options, m.XDSUpdater)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,7 +1054,17 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
validRule := false
|
||||
var matchItems []*_struct.Value
|
||||
// match ingress
|
||||
// if route type is not http, we should re-generate the route name for ingress matching
|
||||
// this is because the route name
|
||||
needAppendRuleType := false
|
||||
if rule.GetRouteType() != higressext.RouteType_HTTP {
|
||||
needAppendRuleType = true
|
||||
}
|
||||
|
||||
for _, ing := range rule.Ingress {
|
||||
if needAppendRuleType {
|
||||
ing = path.Join(rule.GetRouteType().String())
|
||||
}
|
||||
matchItems = append(matchItems, &_struct.Value{
|
||||
Kind: &_struct.Value_StringValue{
|
||||
StringValue: ing,
|
||||
@@ -1310,11 +1320,11 @@ func (m *IngressConfig) AddOrUpdateHttp2Rpc(clusterNamespacedName util.ClusterNa
|
||||
m.http2rpcs[clusterNamespacedName.Name] = &http2rpc.Spec
|
||||
m.mutex.Unlock()
|
||||
IngressLog.Infof("AddOrUpdateHttp2Rpc http2rpc ingress name %s", clusterNamespacedName.Name)
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -1339,11 +1349,11 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
|
||||
m.mutex.Unlock()
|
||||
if hit {
|
||||
IngressLog.Infof("Http2Rpc triggered deleted event executed %s", clusterNamespacedName.Name)
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -1364,11 +1374,11 @@ func (m *IngressConfig) ReflectSecretChanges(clusterNamespacedName util.ClusterN
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if hit {
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -2058,11 +2068,11 @@ func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(GVK config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
var configsUpdated map[istiomodel.ConfigKey]struct{}
|
||||
if updatedConfigName != nil {
|
||||
configsUpdated = map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Name: updatedConfigName.Name,
|
||||
Namespace: updatedConfigName.Namespace,
|
||||
}: {}}
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
@@ -467,11 +466,11 @@ func (m *KIngressConfig) ReflectSecretChanges(clusterNamespacedName util.Cluster
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if hit {
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
|
||||
@@ -16,20 +16,20 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"sync"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/v2/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),
|
||||
Kind: gvk.MustToKind(cfg.GroupVersionKind),
|
||||
Name: cfg.Name,
|
||||
Namespace: cfg.Namespace,
|
||||
}, nil
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"sync/atomic"
|
||||
|
||||
"istio.io/istio/pilot/pkg/config/kube/crdclient"
|
||||
"istio.io/istio/pilot/pkg/credentials"
|
||||
kubecredentials "istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
@@ -30,7 +29,6 @@ import (
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/resource"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/multicluster"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
higressconfig "github.com/alibaba/higress/v2/pkg/config"
|
||||
@@ -47,14 +45,13 @@ type gatewayController struct {
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
|
||||
store model.ConfigStoreController
|
||||
credsController credentials.MulticlusterController
|
||||
istioController *istiogateway.Controller
|
||||
statusManager *status.Manager
|
||||
|
||||
resourceUpToDate atomic.Bool
|
||||
}
|
||||
|
||||
func NewController(client kube.Client, options common.Options) common.GatewayController {
|
||||
func NewController(client kube.Client, options common.Options, xdsUpdater model.XDSUpdater) common.GatewayController {
|
||||
domainSuffix := util.GetDomainSuffix()
|
||||
opts := crdclient.Option{
|
||||
Revision: higressconfig.Revision,
|
||||
@@ -68,12 +65,19 @@ func NewController(client kube.Client, options common.Options) common.GatewayCon
|
||||
}
|
||||
return false
|
||||
})
|
||||
// Add gateway api inference schema if enabled
|
||||
if features.EnableGatewayAPIInferenceExtension {
|
||||
schemasBuilder.MustAdd(collections.InferencePool)
|
||||
}
|
||||
store := crdclient.NewForSchemas(client, opts, schemasBuilder.Build())
|
||||
|
||||
clusterId := options.ClusterId
|
||||
credsController := kubecredentials.NewMulticluster(clusterId)
|
||||
credsController.ClusterAdded(&multicluster.Cluster{ID: clusterId, Client: client}, nil)
|
||||
istioController := istiogateway.NewController(client, store, client.CrdWatcher().WaitForCRD, credsController, kubecontroller.Options{DomainSuffix: domainSuffix})
|
||||
opt := kubecontroller.Options{
|
||||
DomainSuffix: domainSuffix,
|
||||
ClusterID: clusterId,
|
||||
Revision: higressconfig.Revision,
|
||||
}
|
||||
istioController := istiogateway.NewController(client, client.CrdWatcher().WaitForCRD, opt, xdsUpdater)
|
||||
if options.GatewaySelectorKey != "" {
|
||||
istioController.DefaultGatewaySelector = map[string]string{options.GatewaySelectorKey: options.GatewaySelectorValue}
|
||||
}
|
||||
@@ -88,7 +92,6 @@ func NewController(client kube.Client, options common.Options) common.GatewayCon
|
||||
|
||||
return &gatewayController{
|
||||
store: store,
|
||||
credsController: credsController,
|
||||
istioController: istioController,
|
||||
statusManager: statusManager,
|
||||
}
|
||||
@@ -104,10 +107,7 @@ func (g *gatewayController) Get(typ config.GroupVersionKind, name, namespace str
|
||||
|
||||
func (g *gatewayController) List(typ config.GroupVersionKind, namespace string) []config.Config {
|
||||
if g.resourceUpToDate.CompareAndSwap(false, true) {
|
||||
err := g.istioController.Reconcile(model.NewPushContext())
|
||||
if err != nil {
|
||||
IngressLog.Errorf("failed to recompute Gateway API resources: %v", err)
|
||||
}
|
||||
g.istioController.Reconcile(model.NewPushContext())
|
||||
}
|
||||
return g.istioController.List(typ, namespace)
|
||||
}
|
||||
@@ -165,10 +165,7 @@ func (g *gatewayController) SetWatchErrorHandler(f func(r *cache.Reflector, err
|
||||
func (g *gatewayController) HasSynced() bool {
|
||||
ret := g.istioController.HasSynced()
|
||||
if ret {
|
||||
err := g.istioController.Reconcile(model.NewPushContext())
|
||||
if err != nil {
|
||||
IngressLog.Errorf("failed to recompute Gateway API resources: %v", err)
|
||||
}
|
||||
g.istioController.Reconcile(model.NewPushContext())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
674
pkg/ingress/kube/gateway/istio/backend_policies.go
Normal file
674
pkg/ingress/kube/gateway/istio/backend_policies.go
Normal file
@@ -0,0 +1,674 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
gw "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
|
||||
|
||||
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
networkingclient "istio.io/client-go/pkg/apis/networking/v1"
|
||||
kubesecrets "istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
"istio.io/istio/pilot/pkg/util/protoconv"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
schematypes "istio.io/istio/pkg/config/schema/kubetypes"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/maps"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
)
|
||||
|
||||
type TypedNamespacedName struct {
|
||||
types.NamespacedName
|
||||
Kind kind.Kind
|
||||
}
|
||||
|
||||
func (n TypedNamespacedName) String() string {
|
||||
return n.Kind.String() + "/" + n.NamespacedName.String()
|
||||
}
|
||||
|
||||
type TypedNamespacedNamePerHost struct {
|
||||
Target TypedNamespacedName
|
||||
Host string
|
||||
}
|
||||
|
||||
func (t TypedNamespacedNamePerHost) String() string {
|
||||
return t.Target.String() + "/" + t.Host
|
||||
}
|
||||
|
||||
type BackendPolicy struct {
|
||||
Source TypedNamespacedName
|
||||
TargetIndex int
|
||||
Target TypedNamespacedName
|
||||
Host string
|
||||
SectionName *string
|
||||
TLS *networking.ClientTLSSettings
|
||||
LoadBalancer *networking.LoadBalancerSettings
|
||||
RetryBudget *networking.TrafficPolicy_RetryBudget
|
||||
CreationTime time.Time
|
||||
}
|
||||
|
||||
func (b BackendPolicy) ResourceName() string {
|
||||
return b.Source.String() + "/" + fmt.Sprint(b.TargetIndex) + "/" + b.Host
|
||||
}
|
||||
|
||||
var TypedNamespacedNameIndexCollectionFunc = krt.WithIndexCollectionFromString(func(s string) TypedNamespacedName {
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 3 {
|
||||
panic("invalid TypedNamespacedName: " + s)
|
||||
}
|
||||
return TypedNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: parts[1],
|
||||
Name: parts[2],
|
||||
},
|
||||
Kind: kind.FromString(parts[0]),
|
||||
}
|
||||
})
|
||||
|
||||
var TypedNamespacedNamePerHostIndexCollectionFunc = krt.WithIndexCollectionFromString(func(s string) TypedNamespacedNamePerHost {
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 4 {
|
||||
panic("invalid TypedNamespacedNamePerHost: " + s)
|
||||
}
|
||||
return TypedNamespacedNamePerHost{
|
||||
Target: TypedNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: parts[1],
|
||||
Name: parts[2],
|
||||
},
|
||||
Kind: kind.FromString(parts[0]),
|
||||
},
|
||||
Host: parts[3],
|
||||
}
|
||||
})
|
||||
|
||||
func (b BackendPolicy) Equals(other BackendPolicy) bool {
|
||||
return b.Source == other.Source &&
|
||||
ptr.Equal(b.SectionName, other.SectionName) &&
|
||||
protoconv.Equals(b.TLS, other.TLS) &&
|
||||
protoconv.Equals(b.LoadBalancer, other.LoadBalancer) &&
|
||||
protoconv.Equals(b.RetryBudget, other.RetryBudget)
|
||||
}
|
||||
|
||||
// DestinationRuleCollection returns a collection of DestinationRule objects. These are built from a few different
|
||||
// policy types that are merged together.
|
||||
func DestinationRuleCollection(
|
||||
trafficPolicies krt.Collection[*gatewayx.XBackendTrafficPolicy],
|
||||
tlsPolicies krt.Collection[*gw.BackendTLSPolicy],
|
||||
ancestors krt.Index[TypedNamespacedName, AncestorBackend],
|
||||
references *ReferenceSet,
|
||||
domainSuffix string,
|
||||
c *Controller,
|
||||
services krt.Collection[*v1.Service],
|
||||
opts krt.OptionsBuilder,
|
||||
) krt.Collection[*config.Config] {
|
||||
trafficPolicyStatus, backendTrafficPolicies := BackendTrafficPolicyCollection(trafficPolicies, references, domainSuffix, opts)
|
||||
status.RegisterStatus(c.status, trafficPolicyStatus, GetStatus)
|
||||
|
||||
// TODO: BackendTrafficPolicy should also probably use ancestorCollection. However, its still up for debate in the
|
||||
// Gateway API community if having the Gateway as an ancestor ref is required or not; we would prefer it to not be if possible.
|
||||
// Until conformance requires it, for now we skip it.
|
||||
ancestorCollection := ancestors.AsCollection(append(opts.WithName("AncestorBackend"), TypedNamespacedNameIndexCollectionFunc)...)
|
||||
tlsPolicyStatus, backendTLSPolicies := BackendTLSPolicyCollection(tlsPolicies, ancestorCollection, references, domainSuffix, opts)
|
||||
status.RegisterStatus(c.status, tlsPolicyStatus, GetStatus)
|
||||
|
||||
// We need to merge these by hostname into a single DR
|
||||
allPolicies := krt.JoinCollection([]krt.Collection[BackendPolicy]{backendTrafficPolicies, backendTLSPolicies})
|
||||
byTargetAndHost := krt.NewIndex(allPolicies, "targetAndHost", func(o BackendPolicy) []TypedNamespacedNamePerHost {
|
||||
return []TypedNamespacedNamePerHost{{Target: o.Target, Host: o.Host}}
|
||||
})
|
||||
indexOpts := append(opts.WithName("BackendPolicyByTarget"), TypedNamespacedNamePerHostIndexCollectionFunc)
|
||||
merged := krt.NewCollection(
|
||||
byTargetAndHost.AsCollection(indexOpts...),
|
||||
func(ctx krt.HandlerContext, i krt.IndexObject[TypedNamespacedNamePerHost, BackendPolicy]) **config.Config {
|
||||
// Sort so we can pick the oldest, which will win.
|
||||
// Not yet standardized but likely will be (https://github.com/kubernetes-sigs/gateway-api/issues/3516#issuecomment-2684039692)
|
||||
pols := slices.SortFunc(i.Objects, func(a, b BackendPolicy) int {
|
||||
if r := a.CreationTime.Compare(b.CreationTime); r != 0 {
|
||||
return r
|
||||
}
|
||||
if r := cmp.Compare(a.Source.Namespace, b.Source.Namespace); r != 0 {
|
||||
return r
|
||||
}
|
||||
return cmp.Compare(a.Source.Name, b.Source.Name)
|
||||
})
|
||||
tlsSet := false
|
||||
lbSet := false
|
||||
rbSet := false
|
||||
|
||||
targetWithHost := i.Key
|
||||
host := targetWithHost.Host
|
||||
spec := &networking.DestinationRule{
|
||||
Host: host,
|
||||
TrafficPolicy: &networking.TrafficPolicy{},
|
||||
}
|
||||
portLevelSettings := make(map[string]*networking.TrafficPolicy_PortTrafficPolicy)
|
||||
parents := make([]string, 0, len(pols))
|
||||
for _, pol := range pols {
|
||||
if pol.TLS != nil {
|
||||
if pol.SectionName != nil {
|
||||
// Port-specific TLS setting
|
||||
portName := *pol.SectionName
|
||||
if _, exists := portLevelSettings[portName]; !exists {
|
||||
portLevelSettings[portName] = &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
Port: &networking.PortSelector{Number: 0}, // Will be resolved later
|
||||
Tls: pol.TLS,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Service-wide TLS setting
|
||||
if tlsSet {
|
||||
// We only allow 1. TODO: report status if there are multiple
|
||||
continue
|
||||
}
|
||||
tlsSet = true
|
||||
spec.TrafficPolicy.Tls = pol.TLS
|
||||
}
|
||||
}
|
||||
if pol.LoadBalancer != nil {
|
||||
if lbSet {
|
||||
// We only allow 1. TODO: report status if there are multiple
|
||||
continue
|
||||
}
|
||||
lbSet = true
|
||||
spec.TrafficPolicy.LoadBalancer = pol.LoadBalancer
|
||||
}
|
||||
if pol.RetryBudget != nil {
|
||||
if rbSet {
|
||||
// We only allow 1. TODO: report status if there are multiple
|
||||
continue
|
||||
}
|
||||
rbSet = true
|
||||
spec.TrafficPolicy.RetryBudget = pol.RetryBudget
|
||||
}
|
||||
parentName := pol.Source.Kind.String() + "/" + pol.Source.Namespace + "." + pol.Source.Name
|
||||
if !slices.Contains(parents, parentName) {
|
||||
parents = append(parents, parentName)
|
||||
}
|
||||
}
|
||||
|
||||
type servicePort struct {
|
||||
Name string
|
||||
Number uint32
|
||||
}
|
||||
var servicePorts []servicePort
|
||||
|
||||
target := targetWithHost.Target
|
||||
switch target.Kind {
|
||||
case kind.Service:
|
||||
serviceKey := target.Namespace + "/" + target.Name
|
||||
service := ptr.Flatten(krt.FetchOne(ctx, services, krt.FilterKey(serviceKey)))
|
||||
if service != nil {
|
||||
for _, port := range service.Spec.Ports {
|
||||
servicePorts = append(servicePorts, servicePort{
|
||||
Name: port.Name,
|
||||
Number: uint32(port.Port),
|
||||
})
|
||||
}
|
||||
}
|
||||
case kind.ServiceEntry:
|
||||
serviceEntryObj, err := references.LocalPolicyTargetRef(gw.LocalPolicyTargetReference{
|
||||
Group: "networking.istio.io",
|
||||
Kind: "ServiceEntry",
|
||||
Name: gw.ObjectName(target.Name),
|
||||
}, target.Namespace)
|
||||
if err == nil {
|
||||
if serviceEntryPtr, ok := serviceEntryObj.(*networkingclient.ServiceEntry); ok {
|
||||
for _, port := range serviceEntryPtr.Spec.Ports {
|
||||
servicePorts = append(servicePorts, servicePort{
|
||||
Name: port.Name,
|
||||
Number: port.Number,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for portName, portPolicy := range portLevelSettings {
|
||||
for _, port := range servicePorts {
|
||||
if port.Name == portName {
|
||||
portPolicy.Port = &networking.PortSelector{Number: port.Number}
|
||||
break
|
||||
}
|
||||
}
|
||||
spec.TrafficPolicy.PortLevelSettings = append(spec.TrafficPolicy.PortLevelSettings, portPolicy)
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.DestinationRule,
|
||||
Name: generateDRName(target, host),
|
||||
Namespace: target.Namespace,
|
||||
Annotations: map[string]string{
|
||||
constants.InternalParentNames: strings.Join(parents, ","),
|
||||
},
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
return &cfg
|
||||
}, opts.WithName("BackendPolicyMerged")...)
|
||||
return merged
|
||||
}
|
||||
|
||||
func BackendTLSPolicyCollection(
|
||||
tlsPolicies krt.Collection[*gw.BackendTLSPolicy],
|
||||
ancestors krt.IndexCollection[TypedNamespacedName, AncestorBackend],
|
||||
references *ReferenceSet,
|
||||
domainSuffix string,
|
||||
opts krt.OptionsBuilder,
|
||||
) (krt.StatusCollection[*gw.BackendTLSPolicy, gw.PolicyStatus], krt.Collection[BackendPolicy]) {
|
||||
return krt.NewStatusManyCollection(tlsPolicies, func(ctx krt.HandlerContext, i *gw.BackendTLSPolicy) (
|
||||
*gw.PolicyStatus,
|
||||
[]BackendPolicy,
|
||||
) {
|
||||
status := i.Status.DeepCopy()
|
||||
res := make([]BackendPolicy, 0, len(i.Spec.TargetRefs))
|
||||
|
||||
tls := &networking.ClientTLSSettings{Mode: networking.ClientTLSSettings_SIMPLE}
|
||||
s := i.Spec
|
||||
|
||||
conds := map[string]*condition{
|
||||
string(gw.PolicyConditionAccepted): {
|
||||
reason: string(gw.PolicyReasonAccepted),
|
||||
message: "Configuration is valid",
|
||||
},
|
||||
string(gw.BackendTLSPolicyConditionResolvedRefs): {
|
||||
reason: string(gw.BackendTLSPolicyReasonResolvedRefs),
|
||||
message: "Configuration is valid",
|
||||
},
|
||||
}
|
||||
tls.Sni = string(s.Validation.Hostname)
|
||||
tls.SubjectAltNames = slices.MapFilter(s.Validation.SubjectAltNames, func(e gw.SubjectAltName) *string {
|
||||
switch e.Type {
|
||||
case gw.HostnameSubjectAltNameType:
|
||||
return ptr.Of(string(e.Hostname))
|
||||
case gw.URISubjectAltNameType:
|
||||
return ptr.Of(string(e.URI))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
tls.CredentialName = getBackendTLSCredentialName(s.Validation, i.Namespace, conds, references)
|
||||
|
||||
// In ancestor status, we need to report for Service (for mesh) and for each relevant Gateway.
|
||||
// However, there is a max of 16 items we can report.
|
||||
// Reporting per-Gateway has no value (perhaps for anyone, but certainly not for Istio), so we favor the Service attachments
|
||||
// getting to take the 16 slots.
|
||||
// The Gateway API spec says that if there are more than 16, the policy should not be applied. This is a terrible, anti-user, decision
|
||||
// that Istio will not follow, even if it means failing conformance tests.
|
||||
ancestorStatus := make([]gw.PolicyAncestorStatus, 0, len(i.Spec.TargetRefs))
|
||||
uniqueGateways := sets.New[types.NamespacedName]()
|
||||
for idx, t := range i.Spec.TargetRefs {
|
||||
conds = maps.Clone(conds)
|
||||
refo, err := references.LocalPolicyTargetRef(t.LocalPolicyTargetReference, i.Namespace)
|
||||
var sectionName *string
|
||||
if err == nil {
|
||||
switch refType := refo.(type) {
|
||||
case *v1.Service:
|
||||
if t.SectionName != nil && *t.SectionName != "" {
|
||||
sectionName = ptr.Of(string(*t.SectionName))
|
||||
portExists := false
|
||||
for _, port := range refType.Spec.Ports {
|
||||
if port.Name == *sectionName {
|
||||
portExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portExists {
|
||||
err = fmt.Errorf("sectionName %q does not exist in Service %s/%s", *sectionName, refType.Namespace, refType.Name)
|
||||
}
|
||||
}
|
||||
case *networkingclient.ServiceEntry:
|
||||
if t.SectionName != nil && *t.SectionName != "" {
|
||||
sectionName = ptr.Of(string(*t.SectionName))
|
||||
portExists := false
|
||||
for _, port := range refType.Spec.Ports {
|
||||
if port.Name == *sectionName {
|
||||
portExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portExists {
|
||||
err = fmt.Errorf("sectionName %q does not exist in ServiceEntry %s/%s", *sectionName, refType.Namespace, refType.Name)
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported reference kind: %v", t.Kind)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
conds[string(gw.PolicyConditionAccepted)].error = &ConfigError{
|
||||
Reason: string(gw.PolicyReasonTargetNotFound),
|
||||
Message: "targetRefs invalid: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
targetKind := gvk.MustToKind(schematypes.GvkFromObject(refo.(controllers.Object)))
|
||||
target := TypedNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: string(t.Name),
|
||||
Namespace: i.Namespace,
|
||||
},
|
||||
Kind: targetKind,
|
||||
}
|
||||
var hosts []string
|
||||
if targetKind == kind.Service {
|
||||
hosts = []string{string(t.Name) + "." + i.Namespace + ".svc." + domainSuffix}
|
||||
} else if targetKind == kind.ServiceEntry {
|
||||
if serviceEntryPtr, ok := refo.(*networkingclient.ServiceEntry); ok {
|
||||
hosts = serviceEntryPtr.Spec.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
res = append(res, BackendPolicy{
|
||||
Source: TypedNamespacedName{
|
||||
NamespacedName: config.NamespacedName(i),
|
||||
Kind: kind.BackendTLSPolicy,
|
||||
},
|
||||
TargetIndex: idx,
|
||||
Target: target,
|
||||
Host: host,
|
||||
SectionName: sectionName,
|
||||
TLS: tls,
|
||||
CreationTime: i.CreationTimestamp.Time,
|
||||
})
|
||||
ancestorBackends := krt.Fetch(ctx, ancestors, krt.FilterKey(target.String()))
|
||||
for _, gwl := range ancestorBackends {
|
||||
for _, i := range gwl.Objects {
|
||||
uniqueGateways.Insert(i.Gateway)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We add a status for Service (for mesh), and for each Gateway
|
||||
meshPR := gw.ParentReference{
|
||||
Group: &t.Group,
|
||||
Kind: &t.Kind,
|
||||
Name: t.Name,
|
||||
SectionName: t.SectionName,
|
||||
}
|
||||
ancestorStatus = append(ancestorStatus, setAncestorStatus(meshPR, status, i.Generation, conds, constants.ManagedGatewayMeshController))
|
||||
}
|
||||
gwl := slices.SortBy(uniqueGateways.UnsortedList(), types.NamespacedName.String)
|
||||
for _, g := range gwl {
|
||||
pr := gw.ParentReference{
|
||||
Group: ptr.Of(gw.Group(gvk.KubernetesGateway.Group)),
|
||||
Kind: ptr.Of(gw.Kind(gvk.KubernetesGateway.Kind)),
|
||||
Name: gw.ObjectName(g.Name),
|
||||
}
|
||||
ancestorStatus = append(ancestorStatus, setAncestorStatus(pr, status, i.Generation, conds, gw.GatewayController(higressconstants.ManagedGatewayController)))
|
||||
}
|
||||
status.Ancestors = mergeAncestors(status.Ancestors, ancestorStatus)
|
||||
return status, res
|
||||
}, opts.WithName("BackendTLSPolicy")...)
|
||||
}
|
||||
|
||||
func getBackendTLSCredentialName(
|
||||
validation gw.BackendTLSPolicyValidation,
|
||||
policyNamespace string,
|
||||
conds map[string]*condition,
|
||||
references *ReferenceSet,
|
||||
) string {
|
||||
if wk := validation.WellKnownCACertificates; wk != nil {
|
||||
switch *wk {
|
||||
case gw.WellKnownCACertificatesSystem:
|
||||
// Already our default, no action needed
|
||||
default:
|
||||
conds[string(gw.PolicyConditionAccepted)].error = &ConfigError{
|
||||
Reason: string(gw.PolicyReasonInvalid),
|
||||
Message: fmt.Sprintf("Unknown wellKnownCACertificates: %v", *wk),
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if len(validation.CACertificateRefs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Spec should require but double check
|
||||
// We only support 1
|
||||
ref := validation.CACertificateRefs[0]
|
||||
if len(validation.CACertificateRefs) > 1 {
|
||||
conds[string(gw.PolicyConditionAccepted)].message += "; warning: only the first caCertificateRefs will be used"
|
||||
}
|
||||
refo, err := references.LocalPolicyRef(ref, policyNamespace)
|
||||
if err == nil {
|
||||
switch to := refo.(type) {
|
||||
case *v1.ConfigMap:
|
||||
if _, rerr := kubesecrets.ExtractRootFromString(to.Data); rerr != nil {
|
||||
err = rerr
|
||||
conds[string(gw.BackendTLSPolicyReasonResolvedRefs)].error = &ConfigError{
|
||||
Reason: string(gw.BackendTLSPolicyReasonInvalidCACertificateRef),
|
||||
Message: "Certificate invalid: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
return credentials.KubernetesConfigMapTypeURI + policyNamespace + "/" + string(ref.Name)
|
||||
}
|
||||
// TODO: for now we do not support Secret references.
|
||||
// Core requires only ConfigMap
|
||||
// We can do so, we just need to make it so this propagates through to SecretAllowed, otherwise clients in other namespaces
|
||||
// will not be given access.
|
||||
// Additionally, we will need to ensure we don't accidentally authorize them to access the private key, just the ca.crt
|
||||
default:
|
||||
err = fmt.Errorf("unsupported reference kind: %v", ref.Kind)
|
||||
conds[string(gw.BackendTLSPolicyReasonResolvedRefs)].error = &ConfigError{
|
||||
Reason: string(gw.BackendTLSPolicyReasonInvalidKind),
|
||||
Message: "Certificate reference invalid: " + err.Error(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(err.Error(), "unsupported kind") {
|
||||
conds[string(gw.BackendTLSPolicyReasonResolvedRefs)].error = &ConfigError{
|
||||
Reason: string(gw.BackendTLSPolicyReasonInvalidKind),
|
||||
Message: "Certificate reference not supported: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
conds[string(gw.BackendTLSPolicyReasonResolvedRefs)].error = &ConfigError{
|
||||
Reason: string(gw.BackendTLSPolicyReasonInvalidCACertificateRef),
|
||||
Message: "Certificate reference not found: " + err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
conds[string(gw.PolicyConditionAccepted)].error = &ConfigError{
|
||||
Reason: string(gw.BackendTLSPolicyReasonNoValidCACertificate),
|
||||
Message: "Certificate reference invalid: " + err.Error(),
|
||||
}
|
||||
// Generate an invalid reference. This ensures traffic is blocked.
|
||||
// See https://github.com/kubernetes-sigs/gateway-api/issues/3516 for upstream clarification on desired behavior here.
|
||||
return credentials.InvalidSecretTypeURI
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func BackendTrafficPolicyCollection(
|
||||
trafficPolicies krt.Collection[*gatewayx.XBackendTrafficPolicy],
|
||||
references *ReferenceSet,
|
||||
domainSuffix string,
|
||||
opts krt.OptionsBuilder,
|
||||
) (krt.StatusCollection[*gatewayx.XBackendTrafficPolicy, gatewayx.PolicyStatus], krt.Collection[BackendPolicy]) {
|
||||
return krt.NewStatusManyCollection(trafficPolicies, func(ctx krt.HandlerContext, i *gatewayx.XBackendTrafficPolicy) (
|
||||
*gatewayx.PolicyStatus,
|
||||
[]BackendPolicy,
|
||||
) {
|
||||
status := i.Status.DeepCopy()
|
||||
res := make([]BackendPolicy, 0, len(i.Spec.TargetRefs))
|
||||
ancestors := make([]gw.PolicyAncestorStatus, 0, len(i.Spec.TargetRefs))
|
||||
|
||||
lb := &networking.LoadBalancerSettings{}
|
||||
var retryBudget *networking.TrafficPolicy_RetryBudget
|
||||
|
||||
conds := map[string]*condition{
|
||||
string(gw.PolicyConditionAccepted): {
|
||||
reason: string(gw.PolicyReasonAccepted),
|
||||
message: "Configuration is valid",
|
||||
},
|
||||
}
|
||||
var unsupported []string
|
||||
// TODO(https://github.com/istio/istio/issues/55839): implement i.Spec.SessionPersistence.
|
||||
// This will need to map into a StatefulSession filter which Istio doesn't currently support on DestinationRule
|
||||
if i.Spec.SessionPersistence != nil {
|
||||
unsupported = append(unsupported, "sessionPersistence")
|
||||
}
|
||||
if i.Spec.RetryConstraint != nil {
|
||||
// TODO: add support for interval.
|
||||
retryBudget = &networking.TrafficPolicy_RetryBudget{}
|
||||
if i.Spec.RetryConstraint.Budget.Percent != nil {
|
||||
retryBudget.Percent = &wrapperspb.DoubleValue{Value: float64(*i.Spec.RetryConstraint.Budget.Percent)}
|
||||
}
|
||||
retryBudget.MinRetryConcurrency = 10 // Gateway API default
|
||||
if i.Spec.RetryConstraint.MinRetryRate != nil {
|
||||
retryBudget.MinRetryConcurrency = uint32(*i.Spec.RetryConstraint.MinRetryRate.Count)
|
||||
}
|
||||
}
|
||||
if len(unsupported) > 0 {
|
||||
msg := fmt.Sprintf("Configuration is valid, but Istio does not support the following fields: %v", humanReadableJoin(unsupported))
|
||||
conds[string(gw.PolicyConditionAccepted)].message = msg
|
||||
}
|
||||
|
||||
for idx, t := range i.Spec.TargetRefs {
|
||||
conds = maps.Clone(conds)
|
||||
refo, err := references.XLocalPolicyTargetRef(t, i.Namespace)
|
||||
if err == nil {
|
||||
switch refo.(type) {
|
||||
case *v1.Service:
|
||||
default:
|
||||
err = fmt.Errorf("unsupported reference kind: %v", t.Kind)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
conds[string(gw.PolicyConditionAccepted)].error = &ConfigError{
|
||||
Reason: string(gw.PolicyReasonTargetNotFound),
|
||||
Message: "targetRefs invalid: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
// Only create an object if we can resolve the target
|
||||
res = append(res, BackendPolicy{
|
||||
Source: TypedNamespacedName{
|
||||
NamespacedName: config.NamespacedName(i),
|
||||
Kind: kind.XBackendTrafficPolicy,
|
||||
},
|
||||
TargetIndex: idx,
|
||||
Target: TypedNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: string(t.Name),
|
||||
Namespace: i.Namespace,
|
||||
},
|
||||
Kind: kind.Service,
|
||||
},
|
||||
Host: string(t.Name) + "." + i.Namespace + ".svc." + domainSuffix,
|
||||
TLS: nil,
|
||||
LoadBalancer: lb,
|
||||
RetryBudget: retryBudget,
|
||||
CreationTime: i.CreationTimestamp.Time,
|
||||
})
|
||||
}
|
||||
|
||||
pr := gw.ParentReference{
|
||||
Group: &t.Group,
|
||||
Kind: &t.Kind,
|
||||
Name: t.Name,
|
||||
}
|
||||
ancestors = append(ancestors, setAncestorStatus(pr, status, i.Generation, conds, constants.ManagedGatewayMeshController))
|
||||
}
|
||||
status.Ancestors = mergeAncestors(status.Ancestors, ancestors)
|
||||
return status, res
|
||||
}, opts.WithName("BackendTrafficPolicy")...)
|
||||
}
|
||||
|
||||
func setAncestorStatus(
|
||||
pr gw.ParentReference,
|
||||
status *gw.PolicyStatus,
|
||||
generation int64,
|
||||
conds map[string]*condition,
|
||||
controller gw.GatewayController,
|
||||
) gw.PolicyAncestorStatus {
|
||||
currentAncestor := slices.FindFunc(status.Ancestors, func(ex gw.PolicyAncestorStatus) bool {
|
||||
return parentRefEqual(ex.AncestorRef, pr)
|
||||
})
|
||||
var currentConds []metav1.Condition
|
||||
if currentAncestor != nil {
|
||||
currentConds = currentAncestor.Conditions
|
||||
}
|
||||
return gw.PolicyAncestorStatus{
|
||||
AncestorRef: pr,
|
||||
ControllerName: controller,
|
||||
Conditions: setConditions(generation, currentConds, conds),
|
||||
}
|
||||
}
|
||||
|
||||
func parentRefEqual(a, b gw.ParentReference) bool {
|
||||
return ptr.Equal(a.Group, b.Group) &&
|
||||
ptr.Equal(a.Kind, b.Kind) &&
|
||||
a.Name == b.Name &&
|
||||
ptr.Equal(a.Namespace, b.Namespace) &&
|
||||
ptr.Equal(a.SectionName, b.SectionName) &&
|
||||
ptr.Equal(a.Port, b.Port)
|
||||
}
|
||||
|
||||
var outControllers = sets.New(gw.GatewayController(higressconstants.ManagedGatewayController), constants.ManagedGatewayMeshController)
|
||||
|
||||
// mergeAncestors merges an existing ancestor with in incoming one. We preserve order, prune stale references set by our controller,
|
||||
// and add any new references from our controller.
|
||||
func mergeAncestors(existing []gw.PolicyAncestorStatus, incoming []gw.PolicyAncestorStatus) []gw.PolicyAncestorStatus {
|
||||
n := 0
|
||||
for _, x := range existing {
|
||||
if !outControllers.Contains(x.ControllerName) {
|
||||
// Keep it as-is
|
||||
existing[n] = x
|
||||
n++
|
||||
continue
|
||||
}
|
||||
replacement := slices.IndexFunc(incoming, func(status gw.PolicyAncestorStatus) bool {
|
||||
return parentRefEqual(status.AncestorRef, x.AncestorRef)
|
||||
})
|
||||
if replacement != -1 {
|
||||
// We found a replacement!
|
||||
existing[n] = incoming[replacement]
|
||||
incoming = slices.Delete(incoming, replacement)
|
||||
n++
|
||||
}
|
||||
// Else, do nothing and it will be filtered
|
||||
}
|
||||
existing = existing[:n]
|
||||
// Add all remaining ones.
|
||||
existing = append(existing, incoming...)
|
||||
// There is a max of 16
|
||||
return existing[:min(len(existing), 16)]
|
||||
}
|
||||
|
||||
func generateDRName(target TypedNamespacedName, host string) string {
|
||||
if target.Kind == kind.ServiceEntry {
|
||||
return target.Name + "~" + strings.ReplaceAll(host, ".", "-") + "~" + constants.KubernetesGatewayName
|
||||
}
|
||||
return target.Name + "~" + constants.KubernetesGatewayName
|
||||
}
|
||||
@@ -18,18 +18,17 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"istio.io/istio/pilot/pkg/model/kstatus"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"istio.io/istio/pkg/maps"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
)
|
||||
|
||||
// RouteParentResult holds the result of a route for a specific parent
|
||||
@@ -40,19 +39,31 @@ type RouteParentResult struct {
|
||||
DeniedReason *ParentError
|
||||
// RouteError, if present, indicates why the reference was not valid
|
||||
RouteError *ConfigError
|
||||
// WaypointError, if present, indicates why the reference was does not have a waypoint
|
||||
WaypointError *WaypointError
|
||||
}
|
||||
|
||||
func createRouteStatus(parentResults []RouteParentResult, obj config.Config, currentParents []k8s.RouteParentStatus) []k8s.RouteParentStatus {
|
||||
parents := make([]k8s.RouteParentStatus, 0, len(parentResults))
|
||||
// Fill in all the gateways that are already present but not owned by us. This is non-trivial as there may be multiple
|
||||
// gateway controllers that are exposing their status on the same route. We need to attempt to manage ours properly (including
|
||||
// removing gateway references when they are removed), without mangling other Controller's status.
|
||||
for _, r := range currentParents {
|
||||
if r.ControllerName != constants.ManagedGatewayController {
|
||||
// We don't own this status, so keep it around
|
||||
parents = append(parents, r)
|
||||
func createRouteStatus(
|
||||
parentResults []RouteParentResult,
|
||||
objectNamespace string,
|
||||
generation int64,
|
||||
currentParents []k8s.RouteParentStatus,
|
||||
) []k8s.RouteParentStatus {
|
||||
parents := slices.Clone(currentParents)
|
||||
parentIndexes := map[string]int{}
|
||||
for idx, p := range parents {
|
||||
// Only consider our own
|
||||
if p.ControllerName != k8s.GatewayController(higressconstants.ManagedGatewayController) {
|
||||
continue
|
||||
}
|
||||
rs := parentRefString(p.ParentRef, objectNamespace)
|
||||
if _, f := parentIndexes[rs]; f {
|
||||
log.Warnf("invalid HTTPRoute detected: duplicate parent: %v", rs)
|
||||
} else {
|
||||
parentIndexes[rs] = idx
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all of our unique parent references. There may be multiple when we have a route without section name,
|
||||
// but reference a parent with multiple sections.
|
||||
// While we process these internally for-each sectionName, in the status we are just supposed to report one merged entry
|
||||
@@ -72,55 +83,63 @@ func createRouteStatus(parentResults []RouteParentResult, obj config.Config, cur
|
||||
seenReasons.Insert(ParentNoError)
|
||||
}
|
||||
}
|
||||
reasonRanking := []ParentErrorReason{
|
||||
// No errors is preferred
|
||||
ParentNoError,
|
||||
// All route level errors
|
||||
ParentErrorNotAllowed,
|
||||
ParentErrorNoHostname,
|
||||
ParentErrorParentRefConflict,
|
||||
// Failures to match the Port or SectionName. These are last so that if we bind to 1 listener we
|
||||
// just report errors for that 1 listener instead of for all sections we didn't bind to
|
||||
ParentErrorNotAccepted,
|
||||
|
||||
const (
|
||||
rankParentNoErrors = iota
|
||||
rankParentErrorNotAllowed
|
||||
rankParentErrorNoHostname
|
||||
rankParentErrorParentRefConflict
|
||||
rankParentErrorNotAccepted
|
||||
)
|
||||
|
||||
rankParentError := func(result RouteParentResult) int {
|
||||
if result.DeniedReason == nil {
|
||||
return rankParentNoErrors
|
||||
}
|
||||
switch result.DeniedReason.Reason {
|
||||
case ParentErrorNotAllowed:
|
||||
return rankParentErrorNotAllowed
|
||||
case ParentErrorNoHostname:
|
||||
return rankParentErrorNoHostname
|
||||
case ParentErrorParentRefConflict:
|
||||
return rankParentErrorParentRefConflict
|
||||
case ParentErrorNotAccepted:
|
||||
return rankParentErrorNotAccepted
|
||||
}
|
||||
return rankParentNoErrors
|
||||
}
|
||||
|
||||
// Next we want to collapse these. We need to report 1 type of error, or none.
|
||||
report := map[k8s.ParentReference]RouteParentResult{}
|
||||
for _, wantReason := range reasonRanking {
|
||||
if !seenReasons.Contains(wantReason) {
|
||||
for ref, results := range seen {
|
||||
if len(results) == 0 {
|
||||
continue
|
||||
}
|
||||
// We found our highest priority ranking, now we need to collapse this into a single message
|
||||
for k, refs := range seen {
|
||||
for _, ref := range refs {
|
||||
reason := ParentNoError
|
||||
if ref.DeniedReason != nil {
|
||||
reason = ref.DeniedReason.Reason
|
||||
}
|
||||
if wantReason != reason {
|
||||
// Skip this one, it is for a less relevant reason
|
||||
continue
|
||||
}
|
||||
exist, f := report[k]
|
||||
if f {
|
||||
if ref.DeniedReason != nil {
|
||||
if exist.DeniedReason != nil {
|
||||
// join the error
|
||||
exist.DeniedReason.Message += "; " + ref.DeniedReason.Message
|
||||
} else {
|
||||
exist.DeniedReason = ref.DeniedReason
|
||||
}
|
||||
}
|
||||
|
||||
toReport := results[0]
|
||||
mostSevereRankSeen := rankParentError(toReport)
|
||||
|
||||
for _, result := range results[1:] {
|
||||
resultRank := rankParentError(result)
|
||||
// lower number means more severe
|
||||
if resultRank < mostSevereRankSeen {
|
||||
mostSevereRankSeen = resultRank
|
||||
toReport = result
|
||||
} else if resultRank == mostSevereRankSeen {
|
||||
// join the error messages
|
||||
if toReport.DeniedReason == nil {
|
||||
toReport.DeniedReason = result.DeniedReason
|
||||
} else {
|
||||
exist = ref
|
||||
toReport.DeniedReason.Message += "; " + result.DeniedReason.Message
|
||||
}
|
||||
report[k] = exist
|
||||
}
|
||||
}
|
||||
// Once we find the best reason, do not consider any others
|
||||
break
|
||||
|
||||
report[ref] = toReport
|
||||
}
|
||||
|
||||
// Now we fill in all the parents we do own
|
||||
var toAppend []k8s.RouteParentStatus
|
||||
for k, gw := range report {
|
||||
msg := "Route was valid"
|
||||
if successCount[k] > 1 {
|
||||
@@ -151,31 +170,64 @@ func createRouteStatus(parentResults []RouteParentResult, obj config.Config, cur
|
||||
}
|
||||
}
|
||||
|
||||
// when ambient is enabled, report the waypoints resolved condition
|
||||
if features.EnableAmbient {
|
||||
cond := &condition{
|
||||
reason: string(RouteReasonResolvedWaypoints),
|
||||
message: "All waypoints resolved",
|
||||
}
|
||||
if gw.WaypointError != nil {
|
||||
cond.message = gw.WaypointError.Message
|
||||
}
|
||||
conds[string(RouteConditionResolvedWaypoints)] = cond
|
||||
}
|
||||
|
||||
myRef := parentRefString(gw.OriginalReference, objectNamespace)
|
||||
var currentConditions []metav1.Condition
|
||||
currentStatus := slices.FindFunc(currentParents, func(s k8sbeta.RouteParentStatus) bool {
|
||||
return parentRefString(s.ParentRef) == parentRefString(gw.OriginalReference)
|
||||
currentStatus := slices.FindFunc(currentParents, func(s k8s.RouteParentStatus) bool {
|
||||
return parentRefString(s.ParentRef, objectNamespace) == myRef &&
|
||||
s.ControllerName == k8s.GatewayController(higressconstants.ManagedGatewayController)
|
||||
})
|
||||
if currentStatus != nil {
|
||||
currentConditions = currentStatus.Conditions
|
||||
}
|
||||
parents = append(parents, k8s.RouteParentStatus{
|
||||
ns := k8s.RouteParentStatus{
|
||||
ParentRef: gw.OriginalReference,
|
||||
ControllerName: constants.ManagedGatewayController,
|
||||
Conditions: setConditions(obj.Generation, currentConditions, conds),
|
||||
})
|
||||
ControllerName: k8s.GatewayController(higressconstants.ManagedGatewayController),
|
||||
Conditions: setConditions(generation, currentConditions, conds),
|
||||
}
|
||||
// Parent ref already exists, insert in the same place
|
||||
if idx, f := parentIndexes[myRef]; f {
|
||||
parents[idx] = ns
|
||||
// Clear it out so we can detect which ones we need to delete later
|
||||
delete(parentIndexes, myRef)
|
||||
} else {
|
||||
// Else queue it up to append to the end. We don't append now since we will want to sort them.
|
||||
toAppend = append(toAppend, ns)
|
||||
}
|
||||
}
|
||||
// Ensure output is deterministic.
|
||||
// TODO: will we fight over other controllers doing similar (but not identical) ordering?
|
||||
sort.SliceStable(parents, func(i, j int) bool {
|
||||
return parentRefString(parents[i].ParentRef) > parentRefString(parents[j].ParentRef)
|
||||
sort.SliceStable(toAppend, func(i, j int) bool {
|
||||
return parentRefString(toAppend[i].ParentRef, objectNamespace) > parentRefString(toAppend[j].ParentRef, objectNamespace)
|
||||
})
|
||||
parents = append(parents, toAppend...)
|
||||
toDelete := sets.New(maps.Values(parentIndexes)...)
|
||||
parents = FilterInPlaceByIndex(parents, func(i int) bool {
|
||||
_, f := toDelete[i]
|
||||
return !f
|
||||
})
|
||||
|
||||
if parents == nil {
|
||||
return []k8s.RouteParentStatus{}
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
type ParentErrorReason string
|
||||
|
||||
const (
|
||||
ParentErrorNotAccepted = ParentErrorReason(k8sbeta.RouteReasonNoMatchingParent)
|
||||
ParentErrorNotAccepted = ParentErrorReason(k8s.RouteReasonNoMatchingParent)
|
||||
ParentErrorNotAllowed = ParentErrorReason(k8s.RouteReasonNotAllowedByListeners)
|
||||
ParentErrorNoHostname = ParentErrorReason(k8s.RouteReasonNoMatchingListenerHostname)
|
||||
ParentErrorParentRefConflict = ParentErrorReason("ParentRefConflict")
|
||||
@@ -185,29 +237,41 @@ const (
|
||||
type ConfigErrorReason = string
|
||||
|
||||
const (
|
||||
// InvalidRefNotPermitted indicates a route was not permitted
|
||||
InvalidRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonRefNotPermitted)
|
||||
// InvalidDestination indicates an issue with the destination
|
||||
InvalidDestination ConfigErrorReason = "InvalidDestination"
|
||||
InvalidAddress ConfigErrorReason = ConfigErrorReason(k8sbeta.GatewayReasonUnsupportedAddress)
|
||||
InvalidAddress ConfigErrorReason = ConfigErrorReason(k8s.GatewayReasonUnsupportedAddress)
|
||||
// InvalidDestinationPermit indicates a destination was not permitted
|
||||
InvalidDestinationPermit ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonRefNotPermitted)
|
||||
// InvalidDestinationKind indicates an issue with the destination kind
|
||||
InvalidDestinationKind ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonInvalidKind)
|
||||
// InvalidDestinationNotFound indicates a destination does not exist
|
||||
InvalidDestinationNotFound ConfigErrorReason = ConfigErrorReason(k8s.RouteReasonBackendNotFound)
|
||||
// InvalidParentRef indicates we could not refer to the parent we request
|
||||
InvalidParentRef ConfigErrorReason = "InvalidParentReference"
|
||||
// InvalidFilter indicates an issue with the filters
|
||||
InvalidFilter ConfigErrorReason = "InvalidFilter"
|
||||
// InvalidTLS indicates an issue with TLS settings
|
||||
InvalidTLS ConfigErrorReason = ConfigErrorReason(k8sbeta.ListenerReasonInvalidCertificateRef)
|
||||
InvalidTLS ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonInvalidCertificateRef)
|
||||
// InvalidListenerRefNotPermitted indicates a listener reference was not permitted
|
||||
InvalidListenerRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8sbeta.ListenerReasonRefNotPermitted)
|
||||
InvalidListenerRefNotPermitted ConfigErrorReason = ConfigErrorReason(k8s.ListenerReasonRefNotPermitted)
|
||||
// InvalidConfiguration indicates a generic error for all other invalid configurations
|
||||
InvalidConfiguration ConfigErrorReason = "InvalidConfiguration"
|
||||
InvalidResources ConfigErrorReason = ConfigErrorReason(k8sbeta.GatewayReasonNoResources)
|
||||
DeprecateFieldUsage = "DeprecatedField"
|
||||
DeprecateFieldUsage ConfigErrorReason = "DeprecatedField"
|
||||
)
|
||||
|
||||
const (
|
||||
// This condition indicates whether a route's parent reference has
|
||||
// a waypoint configured by resolving the "istio.io/use-waypoint" label
|
||||
// on either the referenced parent or the parent's namespace.
|
||||
RouteConditionResolvedWaypoints k8s.RouteConditionType = "ResolvedWaypoints"
|
||||
RouteReasonResolvedWaypoints k8s.RouteConditionReason = "ResolvedWaypoints"
|
||||
)
|
||||
|
||||
type WaypointErrorReason string
|
||||
|
||||
const (
|
||||
WaypointErrorReasonMissingLabel = WaypointErrorReason("MissingUseWaypointLabel")
|
||||
WaypointErrorMsgMissingLabel = "istio.io/use-waypoint label missing from parent and parent namespace; in ambient mode, route will not be respected"
|
||||
WaypointErrorReasonNoMatchingParent = WaypointErrorReason("NoMatchingParent")
|
||||
WaypointErrorMsgNoMatchingParent = "parent not found"
|
||||
)
|
||||
|
||||
// ParentError represents that a parent could not be referenced
|
||||
@@ -222,6 +286,11 @@ type ConfigError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type WaypointError struct {
|
||||
Reason WaypointErrorReason
|
||||
Message string
|
||||
}
|
||||
|
||||
type condition struct {
|
||||
// reason defines the reason to report on success. Ignored if error is set
|
||||
reason string
|
||||
@@ -282,59 +351,48 @@ func setConditions(generation int64, existingConditions []metav1.Condition, cond
|
||||
return existingConditions
|
||||
}
|
||||
|
||||
func reportListenerAttachedRoutes(index int, obj config.Config, i int32) {
|
||||
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
|
||||
gs := s.(*k8s.GatewayStatus)
|
||||
for index >= len(gs.Listeners) {
|
||||
gs.Listeners = append(gs.Listeners, k8s.ListenerStatus{})
|
||||
func reportListenerCondition(index int, l k8s.Listener, obj controllers.Object,
|
||||
statusListeners []k8s.ListenerStatus, conditions map[string]*condition,
|
||||
) []k8s.ListenerStatus {
|
||||
for index >= len(statusListeners) {
|
||||
statusListeners = append(statusListeners, k8s.ListenerStatus{})
|
||||
}
|
||||
cond := statusListeners[index].Conditions
|
||||
supported, valid := generateSupportedKinds(l)
|
||||
if !valid {
|
||||
conditions[string(k8s.ListenerConditionResolvedRefs)] = &condition{
|
||||
reason: string(k8s.ListenerReasonInvalidRouteKinds),
|
||||
status: metav1.ConditionFalse,
|
||||
message: "Invalid route kinds",
|
||||
}
|
||||
status := gs.Listeners[index]
|
||||
status.AttachedRoutes = i
|
||||
gs.Listeners[index] = status
|
||||
return gs
|
||||
})
|
||||
}
|
||||
|
||||
func reportListenerCondition(index int, l k8s.Listener, obj config.Config, conditions map[string]*condition) {
|
||||
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
|
||||
gs := s.(*k8s.GatewayStatus)
|
||||
for index >= len(gs.Listeners) {
|
||||
gs.Listeners = append(gs.Listeners, k8s.ListenerStatus{})
|
||||
}
|
||||
cond := gs.Listeners[index].Conditions
|
||||
supported, valid := generateSupportedKinds(l)
|
||||
if !valid {
|
||||
conditions[string(k8sbeta.ListenerConditionResolvedRefs)] = &condition{
|
||||
reason: string(k8sbeta.ListenerReasonInvalidRouteKinds),
|
||||
status: metav1.ConditionFalse,
|
||||
message: "Invalid route kinds",
|
||||
}
|
||||
}
|
||||
gs.Listeners[index] = k8s.ListenerStatus{
|
||||
Name: l.Name,
|
||||
AttachedRoutes: 0, // this will be reported later
|
||||
SupportedKinds: supported,
|
||||
Conditions: setConditions(obj.Generation, cond, conditions),
|
||||
}
|
||||
return gs
|
||||
})
|
||||
}
|
||||
statusListeners[index] = k8s.ListenerStatus{
|
||||
Name: l.Name,
|
||||
AttachedRoutes: 0, // this will be reported later
|
||||
SupportedKinds: supported,
|
||||
Conditions: setConditions(obj.GetGeneration(), cond, conditions),
|
||||
}
|
||||
return statusListeners
|
||||
}
|
||||
|
||||
func generateSupportedKinds(l k8s.Listener) ([]k8s.RouteGroupKind, bool) {
|
||||
supported := []k8s.RouteGroupKind{}
|
||||
switch l.Protocol {
|
||||
case k8sbeta.HTTPProtocolType, k8sbeta.HTTPSProtocolType:
|
||||
case k8s.HTTPProtocolType, k8s.HTTPSProtocolType:
|
||||
// Only terminate allowed, so its always HTTP
|
||||
supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: k8s.Kind(gvk.HTTPRoute.Kind)}}
|
||||
case k8sbeta.TCPProtocolType:
|
||||
supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)}}
|
||||
case k8sbeta.TLSProtocolType:
|
||||
if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == k8sbeta.TLSModePassthrough {
|
||||
supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: k8s.Kind(gvk.TLSRoute.Kind)}}
|
||||
} else {
|
||||
supported = []k8s.RouteGroupKind{{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)}}
|
||||
supported = []k8s.RouteGroupKind{
|
||||
toRouteKind(gvk.HTTPRoute),
|
||||
toRouteKind(gvk.GRPCRoute),
|
||||
}
|
||||
// UDP route note support
|
||||
case k8s.TCPProtocolType:
|
||||
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TCPRoute)}
|
||||
case k8s.TLSProtocolType:
|
||||
if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == k8s.TLSModePassthrough {
|
||||
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TLSRoute)}
|
||||
} else {
|
||||
supported = []k8s.RouteGroupKind{toRouteKind(gvk.TCPRoute)}
|
||||
}
|
||||
// UDP route not support
|
||||
}
|
||||
if l.AllowedRoutes != nil && len(l.AllowedRoutes.Kinds) > 0 {
|
||||
// We need to filter down to only ones we actually support
|
||||
@@ -352,11 +410,15 @@ func generateSupportedKinds(l k8s.Listener) ([]k8s.RouteGroupKind, bool) {
|
||||
return supported, true
|
||||
}
|
||||
|
||||
// This and the following function really belongs in some gateway-api lib
|
||||
func routeGroupKindEqual(rgk1, rgk2 k8s.RouteGroupKind) bool {
|
||||
return rgk1.Kind == rgk2.Kind && getGroup(rgk1) == getGroup(rgk2)
|
||||
}
|
||||
func FilterInPlaceByIndex[E any](s []E, keep func(int) bool) []E {
|
||||
i := 0
|
||||
for j := 0; j < len(s); j++ {
|
||||
if keep(j) {
|
||||
s[i] = s[j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func getGroup(rgk k8s.RouteGroupKind) k8s.Group {
|
||||
return ptr.OrDefault(rgk.Group, k8s.Group(gvk.KubernetesGateway.Group))
|
||||
clear(s[i:]) // zero/nil out the obsolete elements, for GC
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
)
|
||||
|
||||
func TestCreateRouteStatus(t *testing.T) {
|
||||
@@ -32,7 +32,14 @@ func TestCreateRouteStatus(t *testing.T) {
|
||||
parentStatus := []k8s.RouteParentStatus{
|
||||
{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: constants.ManagedGatewayController,
|
||||
ControllerName: k8s.GatewayController("another-gateway-controller"),
|
||||
Conditions: []metav1.Condition{
|
||||
{Type: "foo", Status: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: k8s.GatewayController(higressconstants.ManagedGatewayController),
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Type: string(k8s.RouteReasonAccepted),
|
||||
@@ -48,6 +55,13 @@ func TestCreateRouteStatus(t *testing.T) {
|
||||
LastTransitionTime: lastTransitionTime,
|
||||
Message: "All references resolved",
|
||||
},
|
||||
{
|
||||
Type: string(RouteConditionResolvedWaypoints),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: 1,
|
||||
LastTransitionTime: lastTransitionTime,
|
||||
Message: "All waypoints resolved",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -100,7 +114,7 @@ func TestCreateRouteStatus(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := createRouteStatus(tt.args.gateways, tt.args.obj, tt.args.current)
|
||||
got := createRouteStatus(tt.args.gateways, "default", tt.args.obj.Generation, tt.args.current)
|
||||
equal := reflect.DeepEqual(got, tt.args.current)
|
||||
if equal != tt.wantEqual {
|
||||
t.Errorf("route status: old: %+v, new: %+v", tt.args.current, got)
|
||||
|
||||
@@ -17,35 +17,37 @@ package istio
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
serviceRegistryKube "istio.io/istio/pilot/pkg/serviceregistry/kube"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"istio.io/api/label"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
serviceRegistryKube "istio.io/istio/pilot/pkg/serviceregistry/kube"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GatewayContext contains a minimal subset of push context functionality to be exposed to GatewayAPIControllers
|
||||
type GatewayContext struct {
|
||||
ps *model.PushContext
|
||||
ps *model.PushContext
|
||||
cluster cluster.ID
|
||||
// Start - Updated by Higress
|
||||
client kube.Client
|
||||
domainSuffix string
|
||||
clusterID cluster.ID
|
||||
// End - Updated by Higress
|
||||
}
|
||||
|
||||
// Start - Updated by Higress
|
||||
|
||||
func NewGatewayContext(ps *model.PushContext, client kube.Client, domainSuffix string, clusterID cluster.ID) GatewayContext {
|
||||
return GatewayContext{ps, client, domainSuffix, clusterID}
|
||||
func NewGatewayContext(ps *model.PushContext, cluster cluster.ID, client kube.Client, domainSuffix string) GatewayContext {
|
||||
return GatewayContext{ps, cluster, client, domainSuffix}
|
||||
}
|
||||
|
||||
// ResolveGatewayInstances attempts to resolve all instances that a gateway will be exposed on.
|
||||
@@ -54,6 +56,7 @@ func NewGatewayContext(ps *model.PushContext, client kube.Client, domainSuffix s
|
||||
// The actual configuration generation is done on a per-workload basis and will get the exact set of matched instances for that workload.
|
||||
// Four sets are exposed:
|
||||
// * Internal addresses (eg istio-ingressgateway.istio-system.svc.cluster.local:80).
|
||||
// * Internal IP addresses (eg 1.2.3.4). This comes from ClusterIP.
|
||||
// * External addresses (eg 1.2.3.4), this comes from LoadBalancer services. There may be multiple in some cases (especially multi cluster).
|
||||
// * Pending addresses (eg istio-ingressgateway.istio-system.svc), are LoadBalancer-type services with pending external addresses.
|
||||
// * Warnings for references that could not be resolved. These are intended to be user facing.
|
||||
@@ -61,19 +64,22 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
namespace string,
|
||||
gwsvcs []string,
|
||||
servers []*networking.Server,
|
||||
) (internal, external, pending, warns []string) {
|
||||
) (internal, external, pending, warns []string, allUsable bool) {
|
||||
ports := map[int]struct{}{}
|
||||
for _, s := range servers {
|
||||
ports[int(s.Port.Number)] = struct{}{}
|
||||
}
|
||||
foundInternal := sets.New[string]()
|
||||
foundInternalIP := sets.New[string]()
|
||||
foundExternal := sets.New[string]()
|
||||
foundPending := sets.New[string]()
|
||||
warnings := []string{}
|
||||
foundUnusable := false
|
||||
|
||||
// Cache endpoints to reduce redundant queries
|
||||
endpointsCache := make(map[string]*corev1.Endpoints)
|
||||
|
||||
log.Debugf("Resolving gateway instances for %v in namespace %s", gwsvcs, namespace)
|
||||
for _, g := range gwsvcs {
|
||||
svc := gc.GetService(g, namespace, gvk.Service.Kind)
|
||||
if svc == nil {
|
||||
@@ -85,6 +91,9 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
exists := checkServicePortExists(svc, port)
|
||||
if exists {
|
||||
foundInternal.Insert(fmt.Sprintf("%s:%d", g, port))
|
||||
dummyProxy := &model.Proxy{Metadata: &model.NodeMetadata{ClusterID: gc.cluster}}
|
||||
dummyProxy.SetIPMode(model.Dual)
|
||||
foundInternalIP.InsertAll(svc.GetAllAddressesForProxy(dummyProxy)...)
|
||||
if svc.Attributes.ClusterExternalAddresses.Len() > 0 {
|
||||
// Fetch external IPs from all clusters
|
||||
svc.Attributes.ClusterExternalAddresses.ForEach(func(c cluster.ID, externalIPs []string) {
|
||||
@@ -121,20 +130,36 @@ func (gc GatewayContext) ResolveGatewayInstances(
|
||||
if hintWorkloadPort {
|
||||
warnings = append(warnings, fmt.Sprintf(
|
||||
"port %d not found for hostname %q (hint: the service port should be specified, not the workload port", port, g))
|
||||
foundUnusable = true
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("port %d not found for hostname %q", port, g))
|
||||
_, isManaged := svc.Attributes.Labels[label.GatewayManaged.Name]
|
||||
var portExistsOnService bool
|
||||
for _, p := range svc.Ports {
|
||||
if p.Port == port {
|
||||
portExistsOnService = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// If this is a managed gateway, the only possible explanation for no instances for the port
|
||||
// is a delay in endpoint sync. Therefore, we don't want to warn/change the Programmed condition
|
||||
// in this case as long as the port exists on the `Service` object.
|
||||
if !isManaged || !portExistsOnService {
|
||||
warnings = append(warnings, fmt.Sprintf("port %d not found for hostname %q", port, g))
|
||||
foundUnusable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(warnings)
|
||||
return sets.SortedList(foundInternal), sets.SortedList(foundExternal), sets.SortedList(foundPending), warnings
|
||||
return sets.SortedList(foundInternal), sets.SortedList(foundExternal), sets.SortedList(foundPending),
|
||||
warnings, !foundUnusable
|
||||
}
|
||||
|
||||
func (gc GatewayContext) GetService(hostname, namespace, kind string) *model.Service {
|
||||
// Currently only supports type Kubernetes Service
|
||||
if kind != gvk.Service.Kind {
|
||||
// Currently only supports type Kubernetes Service and InferencePool
|
||||
if kind != gvk.Service.Kind && kind != gvk.InferencePool.Kind {
|
||||
log.Warnf("Unsupported kind: expected 'Service', but got '%s'", kind)
|
||||
return nil
|
||||
}
|
||||
@@ -149,7 +174,7 @@ func (gc GatewayContext) GetService(hostname, namespace, kind string) *model.Ser
|
||||
return nil
|
||||
}
|
||||
|
||||
return serviceRegistryKube.ConvertService(*svc, gc.domainSuffix, gc.clusterID)
|
||||
return serviceRegistryKube.ConvertService(*svc, gc.domainSuffix, gc.cluster, nil)
|
||||
}
|
||||
|
||||
func (gc GatewayContext) GetEndpoints(hostname, namespace string) *corev1.Endpoints {
|
||||
|
||||
@@ -12,25 +12,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Updated based on Istio codebase by Higress
|
||||
|
||||
package istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"istio.io/istio/pilot/pkg/credentials"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayalpha "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
|
||||
|
||||
networkingclient "istio.io/client-go/pkg/apis/networking/v1"
|
||||
kubesecrets "istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/model/kstatus"
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/labels"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/collections"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
@@ -39,110 +43,448 @@ import (
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"istio.io/istio/pkg/kube/kclient"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/kube/kubetypes"
|
||||
istiolog "istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/maps"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/revisions"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
klabels "k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var log = istiolog.RegisterScope("gateway", "gateway-api controller")
|
||||
|
||||
var errUnsupportedOp = fmt.Errorf("unsupported operation: the gateway config store is a read-only view")
|
||||
|
||||
// Controller defines the controller for the gateway-api. The controller acts a bit different from most.
|
||||
// Rather than watching the CRs directly, we depend on the existing model.ConfigStoreController which
|
||||
// already watches all CRs. When there are updates, a new PushContext will be computed, which will eventually
|
||||
// call Controller.Reconcile(). Once this happens, we will inspect the current state of the world, and transform
|
||||
// gateway-api types into Istio types (Gateway/VirtualService). Future calls to Get/List will return these
|
||||
// Istio types. These are not stored in the cluster at all, and are purely internal; they can be seen on /debug/configz.
|
||||
// During Reconcile(), the status on all gateway-api types is also tracked. Once completed, if the status
|
||||
// has changed at all, it is queued to asynchronously update the status of the object in Kubernetes.
|
||||
// Controller defines the controller for the gateway-api. The controller reads a variety of resources (Gateway types, as well
|
||||
// as adjacent types like Namespace and Service), and through `krt`, translates them into Istio types (Gateway/VirtualService).
|
||||
//
|
||||
// Most resources are fully "self-contained" with krt, but there are a few usages breaking out of `krt`; these are managed by `krt.RecomputeProtected`.
|
||||
// These are recomputed on each new PushContext initialization, which will call Controller.Reconcile().
|
||||
//
|
||||
// The generated Istio types are not stored in the cluster at all and are purely internal. Calls to List() (from PushContext)
|
||||
// will expose these. They can be introspected at /debug/configz.
|
||||
//
|
||||
// The status on all gateway-api types is also tracked. Each collection emits downstream objects, but also status about the
|
||||
// input type. If the status changes, it is queued to asynchronously update the status of the object in Kubernetes.
|
||||
type Controller struct {
|
||||
// client for accessing Kubernetes
|
||||
client kube.Client
|
||||
// cache provides access to the underlying gateway-configs
|
||||
cache model.ConfigStoreController
|
||||
|
||||
// Gateway-api types reference namespace labels directly, so we need access to these
|
||||
namespaces kclient.Client[*corev1.Namespace]
|
||||
namespaceHandler model.EventHandler
|
||||
|
||||
// Gateway-api types reference secrets directly, so we need access to these
|
||||
credentialsController credentials.MulticlusterController
|
||||
secretHandler model.EventHandler
|
||||
|
||||
// the cluster where the gateway-api controller runs
|
||||
cluster cluster.ID
|
||||
// domain stores the cluster domain, typically cluster.local
|
||||
domain string
|
||||
// revision the controller is running under
|
||||
revision string
|
||||
|
||||
// state is our computed Istio resources. Access is guarded by stateMu. This is updated from Reconcile().
|
||||
state IstioResources
|
||||
stateMu sync.RWMutex
|
||||
|
||||
// statusController controls the status working queue. Status will only be written if statusEnabled is true, which
|
||||
// status controls the status writing queue. Status will only be written if statusEnabled is true, which
|
||||
// is only the case when we are the leader.
|
||||
statusController *status.Controller
|
||||
statusEnabled *atomic.Bool
|
||||
status *status.StatusCollections
|
||||
|
||||
waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool
|
||||
|
||||
// gatewayContext exposes us to the internal Istio service registry. This is outside krt knowledge (currently), so,
|
||||
// so we wrap it in a RecomputeProtected.
|
||||
// Most usages in the API are directly referenced typed objects (Service, ServiceEntry, etc) so this is not needed typically.
|
||||
gatewayContext krt.RecomputeProtected[*atomic.Pointer[GatewayContext]]
|
||||
// tagWatcher allows us to check which tags are ours. Unlike most Istio codepaths, we read istio.io/rev=<tag> and not just
|
||||
// revisions for Gateways. This is because a Gateway is sort of a mix of a Deployment and Config.
|
||||
// Since the TagWatcher is not yet krt-aware, we wrap this in RecomputeProtected.
|
||||
tagWatcher krt.RecomputeProtected[revisions.TagWatcher]
|
||||
|
||||
stop chan struct{}
|
||||
|
||||
xdsUpdater model.XDSUpdater
|
||||
|
||||
// Handlers tracks all registered handlers, so that syncing can be detected
|
||||
handlers []krt.HandlerRegistration
|
||||
|
||||
// outputs contains all the output collections for this controller.
|
||||
// Currently, the only usage of this controller is from non-krt things (PushContext) so this is not exposed directly.
|
||||
// If desired in the future, it could be.
|
||||
outputs Outputs
|
||||
|
||||
domainSuffix string // the domain suffix to use for generated resources
|
||||
|
||||
shadowServiceReconciler controllers.Queue
|
||||
|
||||
// Start - Added by Higress
|
||||
DefaultGatewaySelector map[string]string
|
||||
// End - Added by Higress
|
||||
}
|
||||
|
||||
waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool
|
||||
type ParentInfo struct {
|
||||
Key parentKey
|
||||
Info parentInfo
|
||||
}
|
||||
|
||||
func (pi ParentInfo) ResourceName() string {
|
||||
return pi.Key.Name // TODO!!!! more infoi and section name
|
||||
}
|
||||
|
||||
type TypedResource struct {
|
||||
Kind config.GroupVersionKind
|
||||
Name types.NamespacedName
|
||||
}
|
||||
|
||||
type Outputs struct {
|
||||
Gateways krt.Collection[Gateway]
|
||||
VirtualServices krt.Collection[*config.Config]
|
||||
ReferenceGrants ReferenceGrants
|
||||
DestinationRules krt.Collection[*config.Config]
|
||||
InferencePools krt.Collection[InferencePool]
|
||||
InferencePoolsByGateway krt.Index[types.NamespacedName, InferencePool]
|
||||
}
|
||||
|
||||
type Inputs struct {
|
||||
Namespaces krt.Collection[*corev1.Namespace]
|
||||
|
||||
Services krt.Collection[*corev1.Service]
|
||||
Secrets krt.Collection[*corev1.Secret]
|
||||
ConfigMaps krt.Collection[*corev1.ConfigMap]
|
||||
|
||||
GatewayClasses krt.Collection[*gateway.GatewayClass]
|
||||
Gateways krt.Collection[*gateway.Gateway]
|
||||
HTTPRoutes krt.Collection[*gateway.HTTPRoute]
|
||||
GRPCRoutes krt.Collection[*gatewayv1.GRPCRoute]
|
||||
TCPRoutes krt.Collection[*gatewayalpha.TCPRoute]
|
||||
TLSRoutes krt.Collection[*gatewayalpha.TLSRoute]
|
||||
ListenerSets krt.Collection[*gatewayx.XListenerSet]
|
||||
ReferenceGrants krt.Collection[*gateway.ReferenceGrant]
|
||||
BackendTrafficPolicy krt.Collection[*gatewayx.XBackendTrafficPolicy]
|
||||
BackendTLSPolicies krt.Collection[*gatewayv1.BackendTLSPolicy]
|
||||
ServiceEntries krt.Collection[*networkingclient.ServiceEntry]
|
||||
InferencePools krt.Collection[*inferencev1.InferencePool]
|
||||
}
|
||||
|
||||
var _ model.GatewayController = &Controller{}
|
||||
|
||||
func NewController(
|
||||
kc kube.Client,
|
||||
c model.ConfigStoreController,
|
||||
waitForCRD func(class schema.GroupVersionResource, stop <-chan struct{}) bool,
|
||||
credsController credentials.MulticlusterController,
|
||||
options controller.Options,
|
||||
xdsUpdater model.XDSUpdater,
|
||||
) *Controller {
|
||||
var ctl *status.Controller
|
||||
stop := make(chan struct{})
|
||||
opts := krt.NewOptionsBuilder(stop, "gateway", options.KrtDebugger)
|
||||
|
||||
namespaces := kclient.New[*corev1.Namespace](kc)
|
||||
gatewayController := &Controller{
|
||||
client: kc,
|
||||
cache: c,
|
||||
namespaces: namespaces,
|
||||
credentialsController: credsController,
|
||||
cluster: options.ClusterID,
|
||||
domain: options.DomainSuffix,
|
||||
statusController: ctl,
|
||||
// Disabled by default, we will enable only if we win the leader election
|
||||
statusEnabled: atomic.NewBool(false),
|
||||
waitForCRD: waitForCRD,
|
||||
tw := revisions.NewTagWatcher(kc, options.Revision)
|
||||
c := &Controller{
|
||||
client: kc,
|
||||
cluster: options.ClusterID,
|
||||
revision: options.Revision,
|
||||
status: &status.StatusCollections{},
|
||||
tagWatcher: krt.NewRecomputeProtected(tw, false, opts.WithName("tagWatcher")...),
|
||||
waitForCRD: waitForCRD,
|
||||
gatewayContext: krt.NewRecomputeProtected(atomic.NewPointer[GatewayContext](nil), false, opts.WithName("gatewayContext")...),
|
||||
stop: stop,
|
||||
xdsUpdater: xdsUpdater,
|
||||
domainSuffix: options.DomainSuffix,
|
||||
}
|
||||
|
||||
namespaces.AddEventHandler(controllers.EventHandler[*corev1.Namespace]{
|
||||
UpdateFunc: func(oldNs, newNs *corev1.Namespace) {
|
||||
if options.DiscoveryNamespacesFilter != nil && !options.DiscoveryNamespacesFilter.Filter(newNs) {
|
||||
return
|
||||
}
|
||||
if !labels.Instance(oldNs.Labels).Equals(newNs.Labels) {
|
||||
gatewayController.namespaceEvent(oldNs, newNs)
|
||||
}
|
||||
},
|
||||
tw.AddHandler(func(s sets.String) {
|
||||
c.tagWatcher.TriggerRecomputation()
|
||||
})
|
||||
|
||||
if credsController != nil {
|
||||
credsController.AddSecretHandler(gatewayController.secretEvent)
|
||||
svcClient := kclient.NewFiltered[*corev1.Service](kc, kubetypes.Filter{ObjectFilter: kc.ObjectFilter()})
|
||||
|
||||
inputs := Inputs{
|
||||
Namespaces: krt.NewInformer[*corev1.Namespace](kc, opts.WithName("informer/Namespaces")...),
|
||||
Secrets: krt.WrapClient[*corev1.Secret](
|
||||
kclient.NewFiltered[*corev1.Secret](kc, kubetypes.Filter{
|
||||
FieldSelector: kubesecrets.SecretsFieldSelector,
|
||||
ObjectFilter: kc.ObjectFilter(),
|
||||
}),
|
||||
opts.WithName("informer/Secrets")...,
|
||||
),
|
||||
ConfigMaps: krt.WrapClient[*corev1.ConfigMap](
|
||||
kclient.NewFiltered[*corev1.ConfigMap](kc, kubetypes.Filter{ObjectFilter: kc.ObjectFilter()}),
|
||||
opts.WithName("informer/ConfigMaps")...,
|
||||
),
|
||||
Services: krt.WrapClient[*corev1.Service](svcClient, opts.WithName("informer/Services")...),
|
||||
GatewayClasses: buildClient[*gateway.GatewayClass](c, kc, gvr.GatewayClass, opts, "informer/GatewayClasses"),
|
||||
Gateways: buildClient[*gateway.Gateway](c, kc, gvr.KubernetesGateway, opts, "informer/Gateways"),
|
||||
HTTPRoutes: buildClient[*gateway.HTTPRoute](c, kc, gvr.HTTPRoute, opts, "informer/HTTPRoutes"),
|
||||
GRPCRoutes: buildClient[*gatewayv1.GRPCRoute](c, kc, gvr.GRPCRoute, opts, "informer/GRPCRoutes"),
|
||||
BackendTLSPolicies: buildClient[*gatewayv1.BackendTLSPolicy](c, kc, gvr.BackendTLSPolicy, opts, "informer/BackendTLSPolicies"),
|
||||
|
||||
ReferenceGrants: buildClient[*gateway.ReferenceGrant](c, kc, gvr.ReferenceGrant, opts, "informer/ReferenceGrants"),
|
||||
ServiceEntries: buildClient[*networkingclient.ServiceEntry](c, kc, gvr.ServiceEntry, opts, "informer/ServiceEntries"),
|
||||
}
|
||||
if features.EnableAlphaGatewayAPI {
|
||||
inputs.TCPRoutes = buildClient[*gatewayalpha.TCPRoute](c, kc, gvr.TCPRoute, opts, "informer/TCPRoutes")
|
||||
inputs.TLSRoutes = buildClient[*gatewayalpha.TLSRoute](c, kc, gvr.TLSRoute, opts, "informer/TLSRoutes")
|
||||
inputs.BackendTrafficPolicy = buildClient[*gatewayx.XBackendTrafficPolicy](c, kc, gvr.XBackendTrafficPolicy, opts, "informer/XBackendTrafficPolicy")
|
||||
inputs.ListenerSets = buildClient[*gatewayx.XListenerSet](c, kc, gvr.XListenerSet, opts, "informer/XListenerSet")
|
||||
} else {
|
||||
// If disabled, still build a collection but make it always empty
|
||||
inputs.TCPRoutes = krt.NewStaticCollection[*gatewayalpha.TCPRoute](nil, nil, opts.WithName("disable/TCPRoutes")...)
|
||||
inputs.TLSRoutes = krt.NewStaticCollection[*gatewayalpha.TLSRoute](nil, nil, opts.WithName("disable/TLSRoutes")...)
|
||||
inputs.BackendTrafficPolicy = krt.NewStaticCollection[*gatewayx.XBackendTrafficPolicy](nil, nil, opts.WithName("disable/XBackendTrafficPolicy")...)
|
||||
inputs.ListenerSets = krt.NewStaticCollection[*gatewayx.XListenerSet](nil, nil, opts.WithName("disable/XListenerSet")...)
|
||||
}
|
||||
|
||||
return gatewayController
|
||||
if features.EnableGatewayAPIInferenceExtension {
|
||||
inputs.InferencePools = buildClient[*inferencev1.InferencePool](c, kc, gvr.InferencePool, opts, "informer/InferencePools")
|
||||
} else {
|
||||
// If disabled, still build a collection but make it always empty
|
||||
inputs.InferencePools = krt.NewStaticCollection[*inferencev1.InferencePool](nil, nil, opts.WithName("disable/InferencePools")...)
|
||||
}
|
||||
|
||||
references := NewReferenceSet(
|
||||
AddReference(inputs.Services),
|
||||
AddReference(inputs.ServiceEntries),
|
||||
AddReference(inputs.ConfigMaps),
|
||||
AddReference(inputs.Secrets),
|
||||
)
|
||||
|
||||
handlers := []krt.HandlerRegistration{}
|
||||
|
||||
httpRoutesByInferencePool := krt.NewIndex(inputs.HTTPRoutes, "inferencepool-route", indexHTTPRouteByInferencePool)
|
||||
|
||||
GatewayClassStatus, GatewayClasses := GatewayClassesCollection(inputs.GatewayClasses, opts)
|
||||
status.RegisterStatus(c.status, GatewayClassStatus, GetStatus)
|
||||
|
||||
ReferenceGrants := BuildReferenceGrants(ReferenceGrantsCollection(inputs.ReferenceGrants, opts))
|
||||
ListenerSetStatus, ListenerSets := ListenerSetCollection(
|
||||
inputs.ListenerSets,
|
||||
inputs.Gateways,
|
||||
GatewayClasses,
|
||||
inputs.Namespaces,
|
||||
ReferenceGrants,
|
||||
inputs.ConfigMaps,
|
||||
inputs.Secrets,
|
||||
options.DomainSuffix,
|
||||
c.gatewayContext,
|
||||
c.tagWatcher,
|
||||
opts,
|
||||
c.DefaultGatewaySelector,
|
||||
)
|
||||
status.RegisterStatus(c.status, ListenerSetStatus, GetStatus)
|
||||
|
||||
// GatewaysStatus is not fully complete until its join with route attachments to report attachedRoutes.
|
||||
// Do not register yet.
|
||||
GatewaysStatus, Gateways := GatewayCollection(
|
||||
inputs.Gateways,
|
||||
ListenerSets,
|
||||
GatewayClasses,
|
||||
inputs.Namespaces,
|
||||
ReferenceGrants,
|
||||
inputs.ConfigMaps,
|
||||
inputs.Secrets,
|
||||
c.domainSuffix,
|
||||
c.gatewayContext,
|
||||
c.tagWatcher,
|
||||
opts,
|
||||
c.DefaultGatewaySelector,
|
||||
)
|
||||
|
||||
InferencePoolStatus, InferencePools := InferencePoolCollection(
|
||||
inputs.InferencePools,
|
||||
inputs.Services,
|
||||
inputs.HTTPRoutes,
|
||||
inputs.Gateways,
|
||||
httpRoutesByInferencePool,
|
||||
c,
|
||||
opts,
|
||||
)
|
||||
|
||||
// Create a queue for handling service updates.
|
||||
// We create the queue even if the env var is off just to prevent nil pointer issues.
|
||||
c.shadowServiceReconciler = controllers.NewQueue("inference pool shadow service reconciler",
|
||||
controllers.WithReconciler(c.reconcileShadowService(svcClient, InferencePools, inputs.Services)),
|
||||
controllers.WithMaxAttempts(5))
|
||||
|
||||
if features.EnableGatewayAPIInferenceExtension {
|
||||
status.RegisterStatus(c.status, InferencePoolStatus, GetStatus)
|
||||
}
|
||||
|
||||
RouteParents := BuildRouteParents(Gateways)
|
||||
|
||||
routeInputs := RouteContextInputs{
|
||||
Grants: ReferenceGrants,
|
||||
RouteParents: RouteParents,
|
||||
DomainSuffix: c.domainSuffix,
|
||||
Services: inputs.Services,
|
||||
Namespaces: inputs.Namespaces,
|
||||
ServiceEntries: inputs.ServiceEntries,
|
||||
InferencePools: inputs.InferencePools,
|
||||
internalContext: c.gatewayContext,
|
||||
}
|
||||
tcpRoutes := TCPRouteCollection(
|
||||
inputs.TCPRoutes,
|
||||
routeInputs,
|
||||
opts,
|
||||
)
|
||||
status.RegisterStatus(c.status, tcpRoutes.Status, GetStatus)
|
||||
tlsRoutes := TLSRouteCollection(
|
||||
inputs.TLSRoutes,
|
||||
routeInputs,
|
||||
opts,
|
||||
)
|
||||
status.RegisterStatus(c.status, tlsRoutes.Status, GetStatus)
|
||||
httpRoutes := HTTPRouteCollection(
|
||||
inputs.HTTPRoutes,
|
||||
routeInputs,
|
||||
opts,
|
||||
)
|
||||
status.RegisterStatus(c.status, httpRoutes.Status, GetStatus)
|
||||
grpcRoutes := GRPCRouteCollection(
|
||||
inputs.GRPCRoutes,
|
||||
routeInputs,
|
||||
opts,
|
||||
)
|
||||
status.RegisterStatus(c.status, grpcRoutes.Status, GetStatus)
|
||||
|
||||
RouteAttachments := krt.JoinCollection([]krt.Collection[RouteAttachment]{
|
||||
tcpRoutes.RouteAttachments,
|
||||
tlsRoutes.RouteAttachments,
|
||||
httpRoutes.RouteAttachments,
|
||||
grpcRoutes.RouteAttachments,
|
||||
}, opts.WithName("RouteAttachments")...)
|
||||
RouteAttachmentsIndex := krt.NewIndex(RouteAttachments, "to", func(o RouteAttachment) []types.NamespacedName {
|
||||
return []types.NamespacedName{o.To}
|
||||
})
|
||||
Ancestors := krt.JoinCollection([]krt.Collection[AncestorBackend]{
|
||||
tcpRoutes.Ancestors,
|
||||
tlsRoutes.Ancestors,
|
||||
httpRoutes.Ancestors,
|
||||
grpcRoutes.Ancestors,
|
||||
}, opts.WithName("Ancestors")...)
|
||||
AncestorsIndex := krt.NewIndex(Ancestors, "ancestors", func(o AncestorBackend) []TypedNamespacedName {
|
||||
return []TypedNamespacedName{o.Backend}
|
||||
})
|
||||
|
||||
DestinationRules := DestinationRuleCollection(
|
||||
inputs.BackendTrafficPolicy,
|
||||
inputs.BackendTLSPolicies,
|
||||
AncestorsIndex,
|
||||
references,
|
||||
c.domainSuffix,
|
||||
c,
|
||||
inputs.Services,
|
||||
opts,
|
||||
)
|
||||
|
||||
GatewayFinalStatus := FinalGatewayStatusCollection(GatewaysStatus, RouteAttachments, RouteAttachmentsIndex, opts)
|
||||
status.RegisterStatus(c.status, GatewayFinalStatus, GetStatus)
|
||||
|
||||
VirtualServices := krt.JoinCollection([]krt.Collection[*config.Config]{
|
||||
tcpRoutes.VirtualServices,
|
||||
tlsRoutes.VirtualServices,
|
||||
httpRoutes.VirtualServices,
|
||||
grpcRoutes.VirtualServices,
|
||||
}, opts.WithName("DerivedVirtualServices")...)
|
||||
|
||||
InferencePoolsByGateway := krt.NewIndex(InferencePools, "byGateway", func(i InferencePool) []types.NamespacedName {
|
||||
return i.gatewayParents.UnsortedList()
|
||||
})
|
||||
|
||||
outputs := Outputs{
|
||||
ReferenceGrants: ReferenceGrants,
|
||||
Gateways: Gateways,
|
||||
VirtualServices: VirtualServices,
|
||||
DestinationRules: DestinationRules,
|
||||
InferencePools: InferencePools,
|
||||
InferencePoolsByGateway: InferencePoolsByGateway,
|
||||
}
|
||||
c.outputs = outputs
|
||||
|
||||
handlers = append(handlers,
|
||||
outputs.VirtualServices.RegisterBatch(pushXds(xdsUpdater,
|
||||
func(t *config.Config) model.ConfigKey {
|
||||
return model.ConfigKey{
|
||||
Kind: kind.VirtualService,
|
||||
Name: t.Name,
|
||||
Namespace: t.Namespace,
|
||||
}
|
||||
}), false),
|
||||
outputs.DestinationRules.RegisterBatch(pushXds(xdsUpdater,
|
||||
func(t *config.Config) model.ConfigKey {
|
||||
return model.ConfigKey{
|
||||
Kind: kind.DestinationRule,
|
||||
Name: t.Name,
|
||||
Namespace: t.Namespace,
|
||||
}
|
||||
}), false),
|
||||
outputs.Gateways.RegisterBatch(pushXds(xdsUpdater,
|
||||
func(t Gateway) model.ConfigKey {
|
||||
return model.ConfigKey{
|
||||
Kind: kind.Gateway,
|
||||
Name: t.Name,
|
||||
Namespace: t.Namespace,
|
||||
}
|
||||
}), false),
|
||||
outputs.InferencePools.Register(func(e krt.Event[InferencePool]) {
|
||||
obj := e.Latest()
|
||||
c.shadowServiceReconciler.Add(types.NamespacedName{
|
||||
Namespace: obj.shadowService.key.Namespace,
|
||||
Name: obj.shadowService.poolName,
|
||||
})
|
||||
}),
|
||||
// Reconcile shadow services if users break them.
|
||||
inputs.Services.Register(func(o krt.Event[*corev1.Service]) {
|
||||
obj := o.Latest()
|
||||
// We only care about services that are tagged with the internal service semantics label.
|
||||
if obj.GetLabels()[constants.InternalServiceSemantics] != constants.ServiceSemanticsInferencePool {
|
||||
return
|
||||
}
|
||||
// We only care about delete events
|
||||
if o.Event != controllers.EventDelete && o.Event != controllers.EventUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
poolName, ok := obj.Labels[InferencePoolRefLabel]
|
||||
if !ok && o.Event == controllers.EventUpdate && o.Old != nil {
|
||||
// Try and find the label from the old object
|
||||
old := ptr.Flatten(o.Old)
|
||||
poolName, ok = old.Labels[InferencePoolRefLabel]
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Errorf("service %s/%s is missing the %s label, cannot reconcile shadow service",
|
||||
obj.Namespace, obj.Name, InferencePoolRefLabel)
|
||||
return
|
||||
}
|
||||
|
||||
// Add it back
|
||||
c.shadowServiceReconciler.Add(types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: poolName,
|
||||
})
|
||||
log.Infof("Re-adding shadow service for deleted inference pool service %s/%s",
|
||||
obj.Namespace, obj.Name)
|
||||
}),
|
||||
)
|
||||
c.handlers = handlers
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// buildClient is a small wrapper to build a krt collection based on a delayed informer.
|
||||
func buildClient[I controllers.ComparableObject](
|
||||
c *Controller,
|
||||
kc kube.Client,
|
||||
res schema.GroupVersionResource,
|
||||
opts krt.OptionsBuilder,
|
||||
name string,
|
||||
) krt.Collection[I] {
|
||||
filter := kclient.Filter{
|
||||
ObjectFilter: kubetypes.ComposeFilters(kc.ObjectFilter(), c.inRevision),
|
||||
}
|
||||
|
||||
// all other types are filtered by revision, but for gateways we need to select tags as well
|
||||
if res == gvr.KubernetesGateway {
|
||||
filter.ObjectFilter = kc.ObjectFilter()
|
||||
}
|
||||
|
||||
cc := kclient.NewDelayedInformer[I](kc, res, kubetypes.StandardInformer, filter)
|
||||
return krt.WrapClient[I](cc, opts.WithName(name)...)
|
||||
}
|
||||
|
||||
func (c *Controller) Schemas() collection.Schemas {
|
||||
return collection.SchemasFor(
|
||||
collections.VirtualService,
|
||||
collections.Gateway,
|
||||
collections.DestinationRule,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -151,115 +493,46 @@ func (c *Controller) Get(typ config.GroupVersionKind, name, namespace string) *c
|
||||
}
|
||||
|
||||
func (c *Controller) List(typ config.GroupVersionKind, namespace string) []config.Config {
|
||||
if typ != gvk.Gateway && typ != gvk.VirtualService {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.stateMu.RLock()
|
||||
defer c.stateMu.RUnlock()
|
||||
switch typ {
|
||||
case gvk.Gateway:
|
||||
return filterNamespace(c.state.Gateway, namespace)
|
||||
res := slices.MapFilter(c.outputs.Gateways.List(), func(g Gateway) *config.Config {
|
||||
if g.Valid {
|
||||
return g.Config
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return res
|
||||
case gvk.VirtualService:
|
||||
return filterNamespace(c.state.VirtualService, namespace)
|
||||
return slices.Map(c.outputs.VirtualServices.List(), func(e *config.Config) config.Config {
|
||||
return *e
|
||||
})
|
||||
case gvk.DestinationRule:
|
||||
return slices.Map(c.outputs.DestinationRules.List(), func(e *config.Config) config.Config {
|
||||
return *e
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) SetStatusWrite(enabled bool, statusManager *status.Manager) {
|
||||
c.statusEnabled.Store(enabled)
|
||||
if enabled && features.EnableGatewayAPIStatus && statusManager != nil {
|
||||
c.statusController = statusManager.CreateGenericController(func(status any, context any) status.GenerationProvider {
|
||||
return &gatewayGeneration{context}
|
||||
var q status.Queue = statusManager.CreateGenericController(func(status status.Manipulator, context any) {
|
||||
status.SetInner(context)
|
||||
})
|
||||
c.status.SetQueue(q)
|
||||
} else {
|
||||
c.statusController = nil
|
||||
c.status.UnsetQueue()
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile takes in a current snapshot of the gateway-api configs, and regenerates our internal state.
|
||||
// Any status updates required will be enqueued as well.
|
||||
func (c *Controller) Reconcile(ps *model.PushContext) error {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
log.Debugf("reconcile complete in %v", time.Since(t0))
|
||||
}()
|
||||
gatewayClass := c.cache.List(gvk.GatewayClass, metav1.NamespaceAll)
|
||||
gateway := c.cache.List(gvk.KubernetesGateway, metav1.NamespaceAll)
|
||||
httpRoute := c.cache.List(gvk.HTTPRoute, metav1.NamespaceAll)
|
||||
tcpRoute := c.cache.List(gvk.TCPRoute, metav1.NamespaceAll)
|
||||
tlsRoute := c.cache.List(gvk.TLSRoute, metav1.NamespaceAll)
|
||||
referenceGrant := c.cache.List(gvk.ReferenceGrant, metav1.NamespaceAll)
|
||||
|
||||
input := GatewayResources{
|
||||
GatewayClass: deepCopyStatus(gatewayClass),
|
||||
Gateway: deepCopyStatus(gateway),
|
||||
HTTPRoute: deepCopyStatus(httpRoute),
|
||||
TCPRoute: deepCopyStatus(tcpRoute),
|
||||
TLSRoute: deepCopyStatus(tlsRoute),
|
||||
ReferenceGrant: referenceGrant,
|
||||
DefaultGatewaySelector: c.DefaultGatewaySelector,
|
||||
Domain: c.domain,
|
||||
// Start - Updated by Higress
|
||||
Context: NewGatewayContext(ps, c.client, c.domain, c.cluster),
|
||||
// End - Updated by Higress
|
||||
}
|
||||
|
||||
if !input.hasResources() {
|
||||
// Early exit for common case of no gateway-api used.
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
// make sure we clear out the state, to handle the last gateway-api resource being removed
|
||||
c.state = IstioResources{}
|
||||
return nil
|
||||
}
|
||||
|
||||
nsl := c.namespaces.List("", klabels.Everything())
|
||||
namespaces := make(map[string]*corev1.Namespace, len(nsl))
|
||||
for _, ns := range nsl {
|
||||
namespaces[ns.Name] = ns
|
||||
}
|
||||
input.Namespaces = namespaces
|
||||
|
||||
if c.credentialsController != nil {
|
||||
credentials, err := c.credentialsController.ForCluster(c.cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get credentials: %v", err)
|
||||
}
|
||||
input.Credentials = credentials
|
||||
}
|
||||
|
||||
output := convertResources(input)
|
||||
|
||||
// Handle all status updates
|
||||
c.QueueStatusUpdates(input)
|
||||
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
c.state = output
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) QueueStatusUpdates(r GatewayResources) {
|
||||
c.handleStatusUpdates(r.GatewayClass)
|
||||
c.handleStatusUpdates(r.Gateway)
|
||||
c.handleStatusUpdates(r.HTTPRoute)
|
||||
c.handleStatusUpdates(r.TCPRoute)
|
||||
c.handleStatusUpdates(r.TLSRoute)
|
||||
}
|
||||
|
||||
func (c *Controller) handleStatusUpdates(configs []config.Config) {
|
||||
if c.statusController == nil || !c.statusEnabled.Load() {
|
||||
return
|
||||
}
|
||||
for _, cfg := range configs {
|
||||
ws := cfg.Status.(*kstatus.WrappedStatus)
|
||||
if ws.Dirty {
|
||||
res := status.ResourceFromModelConfig(cfg)
|
||||
c.statusController.EnqueueStatusUpdateResource(ws.Unwrap(), res)
|
||||
}
|
||||
}
|
||||
// Reconcile is called each time the `gatewayContext` may change. We use this to mark it as updated.
|
||||
func (c *Controller) Reconcile(ps *model.PushContext) {
|
||||
ctx := NewGatewayContext(ps, c.cluster, c.client, c.domainSuffix)
|
||||
c.gatewayContext.Modify(func(i **atomic.Pointer[GatewayContext]) {
|
||||
(*i).Store(&ctx)
|
||||
})
|
||||
c.gatewayContext.MarkSynced()
|
||||
}
|
||||
|
||||
func (c *Controller) Create(config config.Config) (revision string, err error) {
|
||||
@@ -283,123 +556,84 @@ func (c *Controller) Delete(typ config.GroupVersionKind, name, namespace string,
|
||||
}
|
||||
|
||||
func (c *Controller) RegisterEventHandler(typ config.GroupVersionKind, handler model.EventHandler) {
|
||||
switch typ {
|
||||
case gvk.Namespace:
|
||||
c.namespaceHandler = handler
|
||||
case gvk.Secret:
|
||||
c.secretHandler = handler
|
||||
}
|
||||
// For all other types, do nothing as c.cache has been registered
|
||||
// We do not do event handler registration this way, and instead directly call the XDS Updated.
|
||||
}
|
||||
|
||||
func (c *Controller) Run(stop <-chan struct{}) {
|
||||
if features.EnableGatewayAPIGatewayClassController {
|
||||
go func() {
|
||||
if c.waitForCRD(gvr.GatewayClass, stop) {
|
||||
gcc := NewClassController(c.client)
|
||||
c.client.RunAndWait(stop)
|
||||
gcc.Run(stop)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
tw := c.tagWatcher.AccessUnprotected()
|
||||
go tw.Run(stop)
|
||||
go c.shadowServiceReconciler.Run(stop)
|
||||
go func() {
|
||||
if c.waitForCRD(gvr.GatewayClass, stop) {
|
||||
gcc := NewClassController(c.client)
|
||||
c.client.RunAndWait(stop)
|
||||
gcc.Run(stop)
|
||||
}
|
||||
kube.WaitForCacheSync("gateway tag watcher", stop, tw.HasSynced)
|
||||
c.tagWatcher.MarkSynced()
|
||||
}()
|
||||
|
||||
<-stop
|
||||
close(c.stop)
|
||||
}
|
||||
|
||||
func (c *Controller) HasSynced() bool {
|
||||
return c.cache.HasSynced() && c.namespaces.HasSynced()
|
||||
}
|
||||
|
||||
func (c *Controller) SecretAllowed(resourceName string, namespace string) bool {
|
||||
c.stateMu.RLock()
|
||||
defer c.stateMu.RUnlock()
|
||||
return c.state.AllowedReferences.SecretAllowed(resourceName, namespace)
|
||||
}
|
||||
|
||||
// namespaceEvent handles a namespace add/update. Gateway's can select routes by label, so we need to handle
|
||||
// when the labels change.
|
||||
// Note: we don't handle delete as a delete would also clean up any relevant gateway-api types which will
|
||||
// trigger its own event.
|
||||
func (c *Controller) namespaceEvent(oldNs, newNs *corev1.Namespace) {
|
||||
// First, find all the label keys on the old/new namespace. We include NamespaceNameLabel
|
||||
// since we have special logic to always allow this on namespace.
|
||||
touchedNamespaceLabels := sets.New(NamespaceNameLabel)
|
||||
touchedNamespaceLabels.InsertAll(getLabelKeys(oldNs)...)
|
||||
touchedNamespaceLabels.InsertAll(getLabelKeys(newNs)...)
|
||||
|
||||
// Next, we find all keys our Gateways actually reference.
|
||||
c.stateMu.RLock()
|
||||
intersection := touchedNamespaceLabels.Intersection(c.state.ReferencedNamespaceKeys)
|
||||
c.stateMu.RUnlock()
|
||||
|
||||
// If there was any overlap, then a relevant namespace label may have changed, and we trigger a
|
||||
// push. A more exact check could actually determine if the label selection result actually changed.
|
||||
// However, this is a much simpler approach that is likely to scale well enough for now.
|
||||
if !intersection.IsEmpty() && c.namespaceHandler != nil {
|
||||
log.Debugf("namespace labels changed, triggering namespace handler: %v", intersection.UnsortedList())
|
||||
c.namespaceHandler(config.Config{}, config.Config{}, model.EventUpdate)
|
||||
if !(c.outputs.VirtualServices.HasSynced() &&
|
||||
c.outputs.DestinationRules.HasSynced() &&
|
||||
c.outputs.Gateways.HasSynced() &&
|
||||
c.outputs.ReferenceGrants.collection.HasSynced()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getLabelKeys extracts all label keys from a namespace object.
|
||||
func getLabelKeys(ns *corev1.Namespace) []string {
|
||||
if ns == nil {
|
||||
return nil
|
||||
for _, h := range c.handlers {
|
||||
if !h.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return maps.Keys(ns.Labels)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Controller) secretEvent(name, namespace string) {
|
||||
var impactedConfigs []model.ConfigKey
|
||||
c.stateMu.RLock()
|
||||
impactedConfigs = c.state.ResourceReferences[model.ConfigKey{
|
||||
Kind: kind.Secret,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
}]
|
||||
c.stateMu.RUnlock()
|
||||
if len(impactedConfigs) > 0 {
|
||||
log.Debugf("secret %s/%s changed, triggering secret handler", namespace, name)
|
||||
for _, cfg := range impactedConfigs {
|
||||
gw := config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.KubernetesGateway,
|
||||
Namespace: cfg.Namespace,
|
||||
Name: cfg.Name,
|
||||
},
|
||||
func (c *Controller) SecretAllowed(ourKind config.GroupVersionKind, resourceName string, namespace string) bool {
|
||||
return c.outputs.ReferenceGrants.SecretAllowed(nil, ourKind, resourceName, namespace)
|
||||
}
|
||||
|
||||
func pushXds[T any](xds model.XDSUpdater, f func(T) model.ConfigKey) func(events []krt.Event[T]) {
|
||||
return func(events []krt.Event[T]) {
|
||||
if xds == nil {
|
||||
return
|
||||
}
|
||||
cu := sets.New[model.ConfigKey]()
|
||||
for _, e := range events {
|
||||
for _, i := range e.Items() {
|
||||
c := f(i)
|
||||
if c != (model.ConfigKey{}) {
|
||||
cu.Insert(c)
|
||||
}
|
||||
}
|
||||
c.secretHandler(gw, gw, model.EventUpdate)
|
||||
}
|
||||
if len(cu) == 0 {
|
||||
return
|
||||
}
|
||||
xds.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: cu,
|
||||
Reason: model.NewReasonStats(model.ConfigUpdate),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// deepCopyStatus creates a copy of all configs, with a copy of the status field that we can mutate.
|
||||
// This allows our functions to call Status.Mutate, and then we can later persist all changes into the
|
||||
// API server.
|
||||
func deepCopyStatus(configs []config.Config) []config.Config {
|
||||
return slices.Map(configs, func(c config.Config) config.Config {
|
||||
return config.Config{
|
||||
Meta: c.Meta,
|
||||
Spec: c.Spec,
|
||||
Status: kstatus.Wrap(c.Status),
|
||||
}
|
||||
})
|
||||
func (c *Controller) HasInferencePool(gw types.NamespacedName) bool {
|
||||
return len(c.outputs.InferencePoolsByGateway.Lookup(gw)) > 0
|
||||
}
|
||||
|
||||
// filterNamespace allows filtering out configs to only a specific namespace. This allows implementing the
|
||||
// List call which can specify a specific namespace.
|
||||
func filterNamespace(cfgs []config.Config, namespace string) []config.Config {
|
||||
if namespace == metav1.NamespaceAll {
|
||||
return cfgs
|
||||
func (c *Controller) inRevision(obj any) bool {
|
||||
object := controllers.ExtractObject(obj)
|
||||
if object == nil {
|
||||
return false
|
||||
}
|
||||
return slices.Filter(cfgs, func(c config.Config) bool {
|
||||
return c.Namespace == namespace
|
||||
})
|
||||
}
|
||||
|
||||
// hasResources determines if there are any gateway-api resources created at all.
|
||||
// If not, we can short circuit all processing to avoid excessive work.
|
||||
func (kr GatewayResources) hasResources() bool {
|
||||
return len(kr.GatewayClass) > 0 ||
|
||||
len(kr.Gateway) > 0 ||
|
||||
len(kr.HTTPRoute) > 0 ||
|
||||
len(kr.TCPRoute) > 0 ||
|
||||
len(kr.TLSRoute) > 0 ||
|
||||
len(kr.ReferenceGrant) > 0
|
||||
return config.LabelsInRevision(object.GetLabels(), c.revision)
|
||||
}
|
||||
|
||||
@@ -12,50 +12,42 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Updated based on Istio codebase by Higress
|
||||
|
||||
package istio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/config/memory"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/util/xdsfake"
|
||||
"istio.io/istio/pkg/config"
|
||||
istioconst "istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collections"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/kclient/clienttest"
|
||||
"istio.io/istio/pkg/test"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1"
|
||||
k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
higressconstant "github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core"
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/test"
|
||||
"istio.io/istio/pkg/test/util/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
gatewayClassSpec = &k8s.GatewayClassSpec{
|
||||
ControllerName: constants.ManagedGatewayController,
|
||||
ControllerName: higressconstant.ManagedGatewayController,
|
||||
}
|
||||
gatewaySpec = &k8s.GatewaySpec{
|
||||
GatewayClassName: "gwclass",
|
||||
GatewayClassName: "higress",
|
||||
Listeners: []k8s.Listener{
|
||||
{
|
||||
Name: "default",
|
||||
Port: 9009,
|
||||
Protocol: "HTTP",
|
||||
AllowedRoutes: &k8s.AllowedRoutes{Namespaces: &k8s.RouteNamespaces{From: func() *k8s.FromNamespaces { x := k8sbeta.NamespacesFromAll; return &x }()}},
|
||||
AllowedRoutes: &k8s.AllowedRoutes{Namespaces: &k8s.RouteNamespaces{From: func() *k8s.FromNamespaces { x := k8s.NamespacesFromAll; return &x }()}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -84,122 +76,62 @@ var AlwaysReady = func(class schema.GroupVersionResource, stop <-chan struct{})
|
||||
return true
|
||||
}
|
||||
|
||||
func setupController(t *testing.T, objs ...runtime.Object) *Controller {
|
||||
kc := kube.NewFakeClient(objs...)
|
||||
setupClientCRDs(t, kc)
|
||||
stop := test.NewStop(t)
|
||||
controller := NewController(
|
||||
kc,
|
||||
AlwaysReady,
|
||||
controller.Options{KrtDebugger: krt.GlobalDebugHandler},
|
||||
nil)
|
||||
kc.RunAndWait(stop)
|
||||
go controller.Run(stop)
|
||||
cg := core.NewConfigGenTest(t, core.TestOptions{})
|
||||
controller.Reconcile(cg.PushContext())
|
||||
kube.WaitForCacheSync("test", stop, controller.HasSynced)
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func TestListInvalidGroupVersionKind(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
clientSet := kube.NewFakeClient()
|
||||
store := memory.NewController(memory.Make(collections.All))
|
||||
controller := NewController(clientSet, store, AlwaysReady, nil, controller.Options{})
|
||||
controller := setupController(t)
|
||||
|
||||
typ := config.GroupVersionKind{Kind: "wrong-kind"}
|
||||
c := controller.List(typ, "ns1")
|
||||
g.Expect(c).To(HaveLen(0))
|
||||
assert.Equal(t, len(c), 0)
|
||||
}
|
||||
|
||||
func TestListGatewayResourceType(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
clientSet := kube.NewFakeClient()
|
||||
store := memory.NewController(memory.Make(collections.All))
|
||||
controller := NewController(clientSet, store, AlwaysReady, nil, controller.Options{})
|
||||
|
||||
store.Create(config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.GatewayClass,
|
||||
Name: "gwclass",
|
||||
Namespace: "ns1",
|
||||
controller := setupController(t,
|
||||
&k8sbeta.GatewayClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "higress",
|
||||
},
|
||||
Spec: *gatewayClassSpec,
|
||||
},
|
||||
Spec: gatewayClassSpec,
|
||||
})
|
||||
if _, err := store.Create(config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.KubernetesGateway,
|
||||
Name: "gwspec",
|
||||
Namespace: "ns1",
|
||||
&k8sbeta.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "gwspec",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: *gatewaySpec,
|
||||
},
|
||||
Spec: gatewaySpec,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.Create(config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.HTTPRoute,
|
||||
Name: "http-route",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: httpRouteSpec,
|
||||
})
|
||||
|
||||
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{})
|
||||
g.Expect(controller.Reconcile(cg.PushContext())).ToNot(HaveOccurred())
|
||||
cfg := controller.List(gvk.Gateway, "ns1")
|
||||
g.Expect(cfg).To(HaveLen(1))
|
||||
for _, c := range cfg {
|
||||
g.Expect(c.GroupVersionKind).To(Equal(gvk.Gateway))
|
||||
g.Expect(c.Name).To(Equal("gwspec" + "-" + istioconst.KubernetesGatewayName + "-default"))
|
||||
g.Expect(c.Namespace).To(Equal("ns1"))
|
||||
g.Expect(c.Spec).To(Equal(expectedgw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceEvent(t *testing.T) {
|
||||
clientSet := kube.NewFakeClient()
|
||||
store := memory.NewController(memory.Make(collections.All))
|
||||
c := NewController(clientSet, store, AlwaysReady, nil, controller.Options{})
|
||||
s := xdsfake.NewFakeXDS()
|
||||
|
||||
c.RegisterEventHandler(gvk.Namespace, func(_, cfg config.Config, _ model.Event) {
|
||||
s.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
Reason: model.NewReasonStats(model.NamespaceUpdate),
|
||||
&k8sbeta.HTTPRoute{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "http-route",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
Spec: *httpRouteSpec,
|
||||
})
|
||||
})
|
||||
|
||||
stop := test.NewStop(t)
|
||||
c.Run(stop)
|
||||
kube.WaitForCacheSync("test", stop, c.HasSynced)
|
||||
c.state.ReferencedNamespaceKeys = sets.String{"allowed": struct{}{}}
|
||||
|
||||
ns1 := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ns1",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}}
|
||||
ns2 := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ns2",
|
||||
Labels: map[string]string{
|
||||
"allowed": "true",
|
||||
},
|
||||
}}
|
||||
ns := clienttest.Wrap(t, c.namespaces)
|
||||
|
||||
ns.Create(ns1)
|
||||
s.AssertEmpty(t, time.Millisecond*10)
|
||||
|
||||
ns.Create(ns2)
|
||||
s.AssertEmpty(t, time.Millisecond*10)
|
||||
|
||||
ns1.Annotations = map[string]string{"foo": "bar"}
|
||||
ns.Update(ns1)
|
||||
s.AssertEmpty(t, time.Millisecond*10)
|
||||
|
||||
ns2.Annotations = map[string]string{"foo": "bar"}
|
||||
ns.Update(ns2)
|
||||
s.AssertEmpty(t, time.Millisecond*10)
|
||||
|
||||
ns1.Labels["bar"] = "foo"
|
||||
ns.Update(ns1)
|
||||
s.AssertEmpty(t, time.Millisecond*10)
|
||||
|
||||
ns2.Labels["foo"] = "bar"
|
||||
ns.Update(ns2)
|
||||
s.WaitOrFail(t, "xds full")
|
||||
|
||||
ns1.Labels["allowed"] = "true"
|
||||
ns.Update(ns1)
|
||||
s.WaitOrFail(t, "xds full")
|
||||
|
||||
ns2.Labels["allowed"] = "false"
|
||||
ns.Update(ns2)
|
||||
s.WaitOrFail(t, "xds full")
|
||||
dumpOnFailure(t, krt.GlobalDebugHandler)
|
||||
cfg := controller.List(gvk.Gateway, "ns1")
|
||||
assert.Equal(t, len(cfg), 1)
|
||||
for _, c := range cfg {
|
||||
assert.Equal(t, c.GroupVersionKind, gvk.Gateway)
|
||||
assert.Equal(t, c.Name, "gwspec"+"-"+constants.KubernetesGatewayName+"-default")
|
||||
assert.Equal(t, c.Namespace, "ns1")
|
||||
assert.Equal(t, c.Spec, any(expectedgw))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,21 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Updated based on Istio codebase by Higress
|
||||
|
||||
package istio
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
)
|
||||
|
||||
// classInfo holds information about a gateway class
|
||||
type classInfo struct {
|
||||
// controller name for this class
|
||||
controller string
|
||||
// controller label for this class
|
||||
controllerLabel string
|
||||
// description for this class
|
||||
description string
|
||||
// The key in the templates to use for this class
|
||||
@@ -37,6 +37,15 @@ type classInfo struct {
|
||||
|
||||
// disableRouteGeneration, if set, will make it so the controller ignores this class.
|
||||
disableRouteGeneration bool
|
||||
|
||||
// supportsListenerSet declares whether a given class supports ListenerSet
|
||||
supportsListenerSet bool
|
||||
|
||||
// disableNameSuffix, if set, will avoid appending -<class> to names
|
||||
disableNameSuffix bool
|
||||
|
||||
// addressType is the default address type to report
|
||||
addressType gateway.AddressType
|
||||
}
|
||||
|
||||
var classInfos = getClassInfos()
|
||||
@@ -45,15 +54,25 @@ var builtinClasses = getBuiltinClasses()
|
||||
|
||||
func getBuiltinClasses() map[gateway.ObjectName]gateway.GatewayController {
|
||||
res := map[gateway.ObjectName]gateway.GatewayController{
|
||||
defaultClassName: constants.ManagedGatewayController,
|
||||
// Start - Commented by Higress
|
||||
// constants.RemoteGatewayClassName: constants.UnmanagedGatewayController,
|
||||
// End - Commented by Higress
|
||||
// Start - Updated by Higress
|
||||
//gateway.ObjectName(features.GatewayAPIDefaultGatewayClass): gateway.GatewayController(features.ManagedGatewayController),
|
||||
higressconstants.DefaultGatewayClass: higressconstants.ManagedGatewayController,
|
||||
// End - Updated by Higress
|
||||
}
|
||||
// Start - Commented by Higress
|
||||
//if features.EnableAmbientControllers {
|
||||
//if features.MultiNetworkGatewayAPI {
|
||||
// res[constants.RemoteGatewayClassName] = constants.UnmanagedGatewayController
|
||||
//}
|
||||
//
|
||||
//if features.EnableAmbientWaypoints {
|
||||
// res[constants.WaypointGatewayClassName] = constants.ManagedGatewayMeshController
|
||||
//}
|
||||
//
|
||||
//// N.B Ambient e/w gateways are just fancy waypoints, but we want a different
|
||||
//// GatewayClass for better UX
|
||||
//if features.EnableAmbientMultiNetwork {
|
||||
// res[constants.EastWestGatewayClassName] = constants.ManagedGatewayEastWestController
|
||||
//}
|
||||
// End - Commented by Higress
|
||||
return res
|
||||
}
|
||||
@@ -61,28 +80,55 @@ func getBuiltinClasses() map[gateway.ObjectName]gateway.GatewayController {
|
||||
func getClassInfos() map[gateway.GatewayController]classInfo {
|
||||
// Start - Updated by Higress
|
||||
m := map[gateway.GatewayController]classInfo{
|
||||
constants.ManagedGatewayController: {
|
||||
controller: constants.ManagedGatewayController,
|
||||
gateway.GatewayController(higressconstants.ManagedGatewayController): {
|
||||
controller: higressconstants.ManagedGatewayController,
|
||||
description: "The default Higress GatewayClass",
|
||||
templates: "kube-gateway",
|
||||
defaultServiceType: corev1.ServiceTypeLoadBalancer,
|
||||
//addressType: gateway.HostnameAddressType,
|
||||
//controllerLabel: constants.ManagedGatewayControllerLabel,
|
||||
//supportsListenerSet: true,
|
||||
},
|
||||
//UnmanagedGatewayController: {
|
||||
// // This represents a gateway that our control plane cannot discover directly via the API server.
|
||||
// // We shouldn't generate Istio resources for it. We aren't programming this gateway.
|
||||
// controller: UnmanagedGatewayController,
|
||||
// description: "Remote to this cluster. Does not deploy or affect configuration.",
|
||||
// disableRouteGeneration: true,
|
||||
//},
|
||||
}
|
||||
//if features.EnableAmbientControllers {
|
||||
// m[constants.ManagedGatewayMeshController] = classInfo{
|
||||
// controller: constants.ManagedGatewayMeshController,
|
||||
// description: "The default Istio waypoint GatewayClass",
|
||||
// templates: "waypoint",
|
||||
// defaultServiceType: corev1.ServiceTypeClusterIP,
|
||||
|
||||
//if features.MultiNetworkGatewayAPI {
|
||||
// m[constants.UnmanagedGatewayController] = classInfo{
|
||||
// // This represents a gateway that our control plane cannot discover directly via the API server.
|
||||
// // We shouldn't generate Istio resources for it. We aren't programming this gateway.
|
||||
// controller: constants.UnmanagedGatewayController,
|
||||
// description: "Remote to this cluster. Does not deploy or affect configuration.",
|
||||
// disableRouteGeneration: true,
|
||||
// addressType: gateway.HostnameAddressType,
|
||||
// supportsListenerSet: false,
|
||||
// }
|
||||
//}
|
||||
//if features.EnableAmbientWaypoints {
|
||||
// m[constants.ManagedGatewayMeshController] = classInfo{
|
||||
// controller: constants.ManagedGatewayMeshController,
|
||||
// description: "The default Istio waypoint GatewayClass",
|
||||
// templates: "waypoint",
|
||||
// disableNameSuffix: true,
|
||||
// defaultServiceType: corev1.ServiceTypeClusterIP,
|
||||
// supportsListenerSet: false,
|
||||
// // Report both. Consumers of the gateways can choose which they want.
|
||||
// // In particular, Istio across different versions consumes different address types, so this retains compat
|
||||
// addressType: "",
|
||||
// controllerLabel: constants.ManagedGatewayMeshControllerLabel,
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//if features.EnableAmbientMultiNetwork {
|
||||
// m[constants.ManagedGatewayEastWestController] = classInfo{
|
||||
// controller: constants.ManagedGatewayEastWestController,
|
||||
// description: "The default GatewayClass for Istio East West Gateways",
|
||||
// templates: "waypoint",
|
||||
// disableNameSuffix: true,
|
||||
// defaultServiceType: corev1.ServiceTypeLoadBalancer,
|
||||
// addressType: "",
|
||||
// controllerLabel: constants.ManagedGatewayEastWestControllerLabel,
|
||||
// }
|
||||
//}
|
||||
|
||||
// End - Updated by Higress
|
||||
return m
|
||||
}
|
||||
|
||||
497
pkg/ingress/kube/gateway/istio/gateway_collection.go
Normal file
497
pkg/ingress/kube/gateway/istio/gateway_collection.go
Normal file
@@ -0,0 +1,497 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"istio.io/api/annotation"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
|
||||
|
||||
istio "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
kubeconfig "istio.io/istio/pkg/config/gateway/kube"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/revisions"
|
||||
"istio.io/istio/pkg/slices"
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
*config.Config `json:"config"`
|
||||
Parent parentKey `json:"parent"`
|
||||
ParentInfo parentInfo `json:"parentInfo"`
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
func (g Gateway) ResourceName() string {
|
||||
return config.NamespacedName(g.Config).String()
|
||||
}
|
||||
|
||||
func (g Gateway) Equals(other Gateway) bool {
|
||||
return g.Config.Equals(other.Config) &&
|
||||
g.Valid == other.Valid // TODO: ok to ignore parent/parentInfo?
|
||||
}
|
||||
|
||||
type ListenerSet struct {
|
||||
*config.Config `json:"config"`
|
||||
Parent parentKey `json:"parent"`
|
||||
ParentInfo parentInfo `json:"parentInfo"`
|
||||
GatewayParent types.NamespacedName `json:"gatewayParent"`
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
func (g ListenerSet) ResourceName() string {
|
||||
return config.NamespacedName(g.Config).Name
|
||||
}
|
||||
|
||||
func (g ListenerSet) Equals(other ListenerSet) bool {
|
||||
return g.Config.Equals(other.Config) &&
|
||||
g.GatewayParent == other.GatewayParent &&
|
||||
g.Valid == other.Valid // TODO: ok to ignore parent/parentInfo?
|
||||
}
|
||||
|
||||
func ListenerSetCollection(
|
||||
listenerSets krt.Collection[*gatewayx.XListenerSet],
|
||||
gateways krt.Collection[*gateway.Gateway],
|
||||
gatewayClasses krt.Collection[GatewayClass],
|
||||
namespaces krt.Collection[*corev1.Namespace],
|
||||
grants ReferenceGrants,
|
||||
configMaps krt.Collection[*corev1.ConfigMap],
|
||||
secrets krt.Collection[*corev1.Secret],
|
||||
domainSuffix string,
|
||||
gatewayContext krt.RecomputeProtected[*atomic.Pointer[GatewayContext]],
|
||||
tagWatcher krt.RecomputeProtected[revisions.TagWatcher],
|
||||
opts krt.OptionsBuilder,
|
||||
defaultGatewaySelector map[string]string,
|
||||
) (
|
||||
krt.StatusCollection[*gatewayx.XListenerSet, gatewayx.ListenerSetStatus],
|
||||
krt.Collection[ListenerSet],
|
||||
) {
|
||||
statusCol, gw := krt.NewStatusManyCollection(listenerSets,
|
||||
func(ctx krt.HandlerContext, obj *gatewayx.XListenerSet) (*gatewayx.ListenerSetStatus, []ListenerSet) {
|
||||
// We currently depend on service discovery information not know to krt; mark we depend on it.
|
||||
context := gatewayContext.Get(ctx).Load()
|
||||
if context == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if !tagWatcher.Get(ctx).IsMine(obj.ObjectMeta) {
|
||||
return nil, nil
|
||||
}
|
||||
result := []ListenerSet{}
|
||||
ls := obj.Spec
|
||||
status := obj.Status.DeepCopy()
|
||||
|
||||
p := ls.ParentRef
|
||||
if normalizeReference(p.Group, p.Kind, gvk.KubernetesGateway) != gvk.KubernetesGateway {
|
||||
// Cannot report status since we don't know if it is for us
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pns := ptr.OrDefault(p.Namespace, gatewayx.Namespace(obj.Namespace))
|
||||
parentGwObj := ptr.Flatten(krt.FetchOne(ctx, gateways, krt.FilterKey(string(pns)+"/"+string(p.Name))))
|
||||
if parentGwObj == nil {
|
||||
// Cannot report status since we don't know if it is for us
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
class := fetchClass(ctx, gatewayClasses, parentGwObj.Spec.GatewayClassName)
|
||||
if class == nil {
|
||||
// Cannot report status since we don't know if it is for us
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
controllerName := class.Controller
|
||||
classInfo, f := classInfos[controllerName]
|
||||
if !f {
|
||||
// Cannot report status since we don't know if it is for us
|
||||
return nil, nil
|
||||
}
|
||||
if !classInfo.supportsListenerSet {
|
||||
reportUnsupportedListenerSet(class.Name, status, obj)
|
||||
return status, nil
|
||||
}
|
||||
|
||||
if !namespaceAcceptedByAllowListeners(obj.Namespace, parentGwObj, func(s string) *corev1.Namespace {
|
||||
return ptr.Flatten(krt.FetchOne(ctx, namespaces, krt.FilterKey(s)))
|
||||
}) {
|
||||
reportNotAllowedListenerSet(status, obj)
|
||||
return status, nil
|
||||
}
|
||||
|
||||
gatewayServices, useDefaultService, err := extractGatewayServices(domainSuffix, parentGwObj, classInfo)
|
||||
if len(gatewayServices) == 0 && !useDefaultService && err != nil {
|
||||
// Short circuit if it's a hard failure
|
||||
reportListenerSetStatus(context, parentGwObj, obj, status, gatewayServices, nil, err)
|
||||
return status, nil
|
||||
}
|
||||
|
||||
servers := []*istio.Server{}
|
||||
for i, l := range ls.Listeners {
|
||||
port, portErr := detectListenerPortNumber(l)
|
||||
l.Port = port
|
||||
standardListener := convertListenerSetToListener(l)
|
||||
originalStatus := slices.Map(status.Listeners, convertListenerSetStatusToStandardStatus)
|
||||
server, updatedStatus, programmed := buildListener(ctx, configMaps, secrets, grants, namespaces,
|
||||
obj, originalStatus, parentGwObj.Spec, standardListener, i, controllerName, portErr)
|
||||
status.Listeners = slices.Map(updatedStatus, convertStandardStatusToListenerSetStatus(l))
|
||||
|
||||
servers = append(servers, server)
|
||||
if controllerName == constants.ManagedGatewayMeshController || controllerName == constants.ManagedGatewayEastWestController {
|
||||
// Waypoint doesn't actually convert the routes to VirtualServices
|
||||
continue
|
||||
}
|
||||
meta := parentMeta(obj, &l.Name)
|
||||
meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway
|
||||
//meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
|
||||
meta[constants.InternalParentNamespace] = parentGwObj.Namespace
|
||||
serviceAccountName := model.GetOrDefault(
|
||||
parentGwObj.GetAnnotations()[annotation.GatewayServiceAccount.Name],
|
||||
getDefaultName(parentGwObj.GetName(), &parentGwObj.Spec, classInfo.disableNameSuffix),
|
||||
)
|
||||
meta[constants.InternalServiceAccount] = serviceAccountName
|
||||
|
||||
// Start - Updated by Higress
|
||||
var selector map[string]string
|
||||
if len(gatewayServices) != 0 {
|
||||
meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
|
||||
} else if useDefaultService {
|
||||
selector = defaultGatewaySelector
|
||||
} else {
|
||||
// Protective programming. This shouldn't happen.
|
||||
continue
|
||||
}
|
||||
// End - Updated by Higress
|
||||
|
||||
// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
|
||||
gatewayConfig := config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: kubeconfig.InternalGatewayName(obj.Name, string(l.Name)),
|
||||
Annotations: meta,
|
||||
Namespace: obj.Namespace,
|
||||
Domain: domainSuffix,
|
||||
},
|
||||
Spec: &istio.Gateway{
|
||||
Servers: []*istio.Server{server},
|
||||
// Start - Added by Higress
|
||||
Selector: selector,
|
||||
// End - Added by Higress
|
||||
},
|
||||
}
|
||||
|
||||
allowed, _ := generateSupportedKinds(standardListener)
|
||||
ref := parentKey{
|
||||
Kind: gvk.XListenerSet,
|
||||
Name: obj.Name,
|
||||
Namespace: obj.Namespace,
|
||||
}
|
||||
pri := parentInfo{
|
||||
InternalName: obj.Namespace + "/" + gatewayConfig.Name,
|
||||
AllowedKinds: allowed,
|
||||
Hostnames: server.Hosts,
|
||||
OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
|
||||
SectionName: l.Name,
|
||||
Port: l.Port,
|
||||
Protocol: l.Protocol,
|
||||
}
|
||||
|
||||
res := ListenerSet{
|
||||
Config: &gatewayConfig,
|
||||
Valid: programmed,
|
||||
Parent: ref,
|
||||
GatewayParent: config.NamespacedName(parentGwObj),
|
||||
ParentInfo: pri,
|
||||
}
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
reportListenerSetStatus(context, parentGwObj, obj, status, gatewayServices, servers, err)
|
||||
return status, result
|
||||
}, opts.WithName("ListenerSets")...)
|
||||
|
||||
return statusCol, gw
|
||||
}
|
||||
|
||||
func GatewayCollection(
|
||||
gateways krt.Collection[*gateway.Gateway],
|
||||
listenerSets krt.Collection[ListenerSet],
|
||||
gatewayClasses krt.Collection[GatewayClass],
|
||||
namespaces krt.Collection[*corev1.Namespace],
|
||||
grants ReferenceGrants,
|
||||
configMaps krt.Collection[*corev1.ConfigMap],
|
||||
secrets krt.Collection[*corev1.Secret],
|
||||
domainSuffix string,
|
||||
gatewayContext krt.RecomputeProtected[*atomic.Pointer[GatewayContext]],
|
||||
tagWatcher krt.RecomputeProtected[revisions.TagWatcher],
|
||||
opts krt.OptionsBuilder,
|
||||
defaultGatewaySelector map[string]string,
|
||||
) (
|
||||
krt.StatusCollection[*gateway.Gateway, gateway.GatewayStatus],
|
||||
krt.Collection[Gateway],
|
||||
) {
|
||||
listenerIndex := krt.NewIndex(listenerSets, "gatewayParent", func(o ListenerSet) []types.NamespacedName {
|
||||
return []types.NamespacedName{o.GatewayParent}
|
||||
})
|
||||
statusCol, gw := krt.NewStatusManyCollection(gateways, func(ctx krt.HandlerContext, obj *gateway.Gateway) (*gateway.GatewayStatus, []Gateway) {
|
||||
// We currently depend on service discovery information not known to krt; mark we depend on it.
|
||||
context := gatewayContext.Get(ctx).Load()
|
||||
if context == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if !tagWatcher.Get(ctx).IsMine(obj.ObjectMeta) {
|
||||
return nil, nil
|
||||
}
|
||||
result := []Gateway{}
|
||||
kgw := obj.Spec
|
||||
status := obj.Status.DeepCopy()
|
||||
class := fetchClass(ctx, gatewayClasses, kgw.GatewayClassName)
|
||||
if class == nil {
|
||||
return nil, nil
|
||||
}
|
||||
controllerName := class.Controller
|
||||
classInfo, f := classInfos[controllerName]
|
||||
if !f {
|
||||
return nil, nil
|
||||
}
|
||||
if classInfo.disableRouteGeneration {
|
||||
reportUnmanagedGatewayStatus(status, obj)
|
||||
// We found it, but don't want to handle this class
|
||||
return status, nil
|
||||
}
|
||||
servers := []*istio.Server{}
|
||||
|
||||
// Start - Updated by Higress
|
||||
// Extract the addresses. A gateway will bind to a specific Service
|
||||
gatewayServices, useDefaultService, err := extractGatewayServices(domainSuffix, obj, classInfo)
|
||||
if len(gatewayServices) == 0 && !useDefaultService && err != nil {
|
||||
// Short circuit if its a hard failure
|
||||
reportGatewayStatus(context, obj, status, gatewayServices, servers, 0, err)
|
||||
return status, nil
|
||||
}
|
||||
// End - Updated by Higress
|
||||
|
||||
// See: https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#manual-deployment
|
||||
// If we set and address of type hostname, then we have no idea what service accounts the gateway workloads will use.
|
||||
// Thus, we don't enforce service account name restrictions (still look at namespaces though).
|
||||
serviceAccountName := ""
|
||||
if IsManaged(&obj.Spec) {
|
||||
serviceAccountName = model.GetOrDefault(
|
||||
obj.GetAnnotations()[annotation.GatewayServiceAccount.Name],
|
||||
getDefaultName(obj.GetName(), &kgw, classInfo.disableNameSuffix),
|
||||
)
|
||||
}
|
||||
|
||||
for i, l := range kgw.Listeners {
|
||||
server, updatedStatus, programmed := buildListener(ctx, configMaps, secrets, grants, namespaces, obj, status.Listeners, kgw, l, i, controllerName, nil)
|
||||
status.Listeners = updatedStatus
|
||||
|
||||
servers = append(servers, server)
|
||||
if controllerName == constants.ManagedGatewayMeshController || controllerName == constants.ManagedGatewayEastWestController {
|
||||
// Waypoint and ambient e/w don't actually convert the routes to VirtualServices
|
||||
// TODO: Maybe E/W gateway should for non 15008 ports for backwards compat?
|
||||
continue
|
||||
}
|
||||
meta := parentMeta(obj, &l.Name)
|
||||
meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway
|
||||
meta[constants.InternalServiceAccount] = serviceAccountName
|
||||
// Start - Updated by Higress
|
||||
var selector map[string]string
|
||||
if len(gatewayServices) != 0 {
|
||||
meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
|
||||
} else if useDefaultService {
|
||||
selector = defaultGatewaySelector
|
||||
} else {
|
||||
// Protective programming. This shouldn't happen.
|
||||
continue
|
||||
}
|
||||
// End - Updated by Higress
|
||||
|
||||
// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
|
||||
gatewayConfig := config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: kubeconfig.InternalGatewayName(obj.Name, string(l.Name)),
|
||||
Annotations: meta,
|
||||
Namespace: obj.Namespace,
|
||||
Domain: domainSuffix,
|
||||
},
|
||||
Spec: &istio.Gateway{
|
||||
Servers: []*istio.Server{server},
|
||||
// Start - Added by Higress
|
||||
Selector: selector,
|
||||
// End - Added by Higress
|
||||
},
|
||||
}
|
||||
|
||||
allowed, _ := generateSupportedKinds(l)
|
||||
ref := parentKey{
|
||||
Kind: gvk.KubernetesGateway,
|
||||
Name: obj.Name,
|
||||
Namespace: obj.Namespace,
|
||||
}
|
||||
pri := parentInfo{
|
||||
InternalName: obj.Namespace + "/" + gatewayConfig.Name,
|
||||
AllowedKinds: allowed,
|
||||
Hostnames: server.Hosts,
|
||||
OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
|
||||
SectionName: l.Name,
|
||||
Port: l.Port,
|
||||
Protocol: l.Protocol,
|
||||
}
|
||||
|
||||
res := Gateway{
|
||||
Config: &gatewayConfig,
|
||||
Valid: programmed,
|
||||
Parent: ref,
|
||||
ParentInfo: pri,
|
||||
}
|
||||
result = append(result, res)
|
||||
}
|
||||
listenersFromSets := krt.Fetch(ctx, listenerSets, krt.FilterIndex(listenerIndex, config.NamespacedName(obj)))
|
||||
for _, ls := range listenersFromSets {
|
||||
servers = append(servers, ls.Config.Spec.(*istio.Gateway).Servers...)
|
||||
result = append(result, Gateway{
|
||||
Config: ls.Config,
|
||||
Parent: ls.Parent,
|
||||
ParentInfo: ls.ParentInfo,
|
||||
Valid: ls.Valid,
|
||||
})
|
||||
}
|
||||
|
||||
reportGatewayStatus(context, obj, status, gatewayServices, servers, len(listenersFromSets), err)
|
||||
return status, result
|
||||
}, opts.WithName("KubernetesGateway")...)
|
||||
|
||||
return statusCol, gw
|
||||
}
|
||||
|
||||
// FinalGatewayStatusCollection finalizes a Gateway status. There is a circular logic between Gateways and Routes to determine
|
||||
// the attachedRoute count, so we first build a partial Gateway status, then once routes are computed we finalize it with
|
||||
// the attachedRoute count.
|
||||
func FinalGatewayStatusCollection(
|
||||
gatewayStatuses krt.StatusCollection[*gateway.Gateway, gateway.GatewayStatus],
|
||||
routeAttachments krt.Collection[RouteAttachment],
|
||||
routeAttachmentsIndex krt.Index[types.NamespacedName, RouteAttachment],
|
||||
opts krt.OptionsBuilder,
|
||||
) krt.StatusCollection[*gateway.Gateway, gateway.GatewayStatus] {
|
||||
return krt.NewCollection(
|
||||
gatewayStatuses,
|
||||
func(ctx krt.HandlerContext, i krt.ObjectWithStatus[*gateway.Gateway, gateway.GatewayStatus]) *krt.ObjectWithStatus[*gateway.Gateway, gateway.GatewayStatus] {
|
||||
tcpRoutes := krt.Fetch(ctx, routeAttachments, krt.FilterIndex(routeAttachmentsIndex, config.NamespacedName(i.Obj)))
|
||||
counts := map[string]int32{}
|
||||
for _, r := range tcpRoutes {
|
||||
counts[r.ListenerName]++
|
||||
}
|
||||
status := i.Status.DeepCopy()
|
||||
for i, s := range status.Listeners {
|
||||
s.AttachedRoutes = counts[string(s.Name)]
|
||||
status.Listeners[i] = s
|
||||
}
|
||||
return &krt.ObjectWithStatus[*gateway.Gateway, gateway.GatewayStatus]{
|
||||
Obj: i.Obj,
|
||||
Status: *status,
|
||||
}
|
||||
}, opts.WithName("GatewayFinalStatus")...)
|
||||
}
|
||||
|
||||
// RouteParents holds information about things routes can reference as parents.
|
||||
type RouteParents struct {
|
||||
gateways krt.Collection[Gateway]
|
||||
gatewayIndex krt.Index[parentKey, Gateway]
|
||||
}
|
||||
|
||||
func (p RouteParents) fetch(ctx krt.HandlerContext, pk parentKey) []*parentInfo {
|
||||
if pk == meshParentKey {
|
||||
// Special case
|
||||
return []*parentInfo{
|
||||
{
|
||||
InternalName: "mesh",
|
||||
// Mesh has no configurable AllowedKinds, so allow all supported
|
||||
AllowedKinds: []gateway.RouteGroupKind{
|
||||
{Group: (*gateway.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: gateway.Kind(gvk.HTTPRoute.Kind)},
|
||||
{Group: (*gateway.Group)(ptr.Of(gvk.GRPCRoute.Group)), Kind: gateway.Kind(gvk.GRPCRoute.Kind)},
|
||||
{Group: (*gateway.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: gateway.Kind(gvk.TCPRoute.Kind)},
|
||||
{Group: (*gateway.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: gateway.Kind(gvk.TLSRoute.Kind)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return slices.Map(krt.Fetch(ctx, p.gateways, krt.FilterIndex(p.gatewayIndex, pk)), func(gw Gateway) *parentInfo {
|
||||
return &gw.ParentInfo
|
||||
})
|
||||
}
|
||||
|
||||
func BuildRouteParents(
|
||||
gateways krt.Collection[Gateway],
|
||||
) RouteParents {
|
||||
idx := krt.NewIndex(gateways, "parent", func(o Gateway) []parentKey {
|
||||
return []parentKey{o.Parent}
|
||||
})
|
||||
return RouteParents{
|
||||
gateways: gateways,
|
||||
gatewayIndex: idx,
|
||||
}
|
||||
}
|
||||
|
||||
func detectListenerPortNumber(l gatewayx.ListenerEntry) (gatewayx.PortNumber, error) {
|
||||
if l.Port != 0 {
|
||||
return l.Port, nil
|
||||
}
|
||||
switch l.Protocol {
|
||||
case gatewayv1.HTTPProtocolType:
|
||||
return 80, nil
|
||||
case gatewayv1.HTTPSProtocolType:
|
||||
return 443, nil
|
||||
}
|
||||
return 0, fmt.Errorf("protocol %v requires a port to be set", l.Protocol)
|
||||
}
|
||||
|
||||
func convertStandardStatusToListenerSetStatus(l gatewayx.ListenerEntry) func(e gateway.ListenerStatus) gatewayx.ListenerEntryStatus {
|
||||
return func(e gateway.ListenerStatus) gatewayx.ListenerEntryStatus {
|
||||
return gatewayx.ListenerEntryStatus{
|
||||
Name: e.Name,
|
||||
Port: l.Port,
|
||||
SupportedKinds: e.SupportedKinds,
|
||||
AttachedRoutes: e.AttachedRoutes,
|
||||
Conditions: e.Conditions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convertListenerSetStatusToStandardStatus(e gatewayx.ListenerEntryStatus) gateway.ListenerStatus {
|
||||
return gateway.ListenerStatus{
|
||||
Name: e.Name,
|
||||
SupportedKinds: e.SupportedKinds,
|
||||
AttachedRoutes: e.AttachedRoutes,
|
||||
Conditions: e.Conditions,
|
||||
}
|
||||
}
|
||||
|
||||
func convertListenerSetToListener(l gatewayx.ListenerEntry) gateway.Listener {
|
||||
// For now, structs are identical enough Go can cast them. I doubt this will hold up forever, but we can adjust as needed.
|
||||
return gateway.Listener(l)
|
||||
}
|
||||
@@ -12,22 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Updated based on Istio codebase by Higress
|
||||
|
||||
package istio
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
k8sv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model/kstatus"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"istio.io/istio/pkg/kube/kclient"
|
||||
"istio.io/istio/pkg/util/istiomultierror"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
)
|
||||
|
||||
// ClassController is a controller that creates the default Istio GatewayClass(s). This will not
|
||||
@@ -103,19 +102,19 @@ func (c *ClassController) reconcileClass(class gateway.ObjectName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetClassStatus(existing *k8s.GatewayClassStatus, gen int64) k8s.GatewayClassStatus {
|
||||
func GetClassStatus(existing *k8sv1.GatewayClassStatus, gen int64) *k8sv1.GatewayClassStatus {
|
||||
if existing == nil {
|
||||
existing = &k8s.GatewayClassStatus{}
|
||||
existing = &k8sv1.GatewayClassStatus{}
|
||||
}
|
||||
existing.Conditions = kstatus.UpdateConditionIfChanged(existing.Conditions, metav1.Condition{
|
||||
Type: string(gateway.GatewayClassConditionStatusAccepted),
|
||||
Type: string(k8sv1.GatewayClassConditionStatusAccepted),
|
||||
Status: kstatus.StatusTrue,
|
||||
ObservedGeneration: gen,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gateway.GatewayClassConditionStatusAccepted),
|
||||
Reason: string(k8sv1.GatewayClassConditionStatusAccepted),
|
||||
// Start - Updated by Higress
|
||||
Message: "Handled by Higress controller",
|
||||
// End - Updated by Higress
|
||||
})
|
||||
return *existing
|
||||
return existing
|
||||
}
|
||||
|
||||
68
pkg/ingress/kube/gateway/istio/gatewayclass_collection.go
Normal file
68
pkg/ingress/kube/gateway/istio/gatewayclass_collection.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
)
|
||||
|
||||
type GatewayClass struct {
|
||||
Name string
|
||||
Controller gateway.GatewayController
|
||||
}
|
||||
|
||||
func (g GatewayClass) ResourceName() string {
|
||||
return g.Name
|
||||
}
|
||||
|
||||
func GatewayClassesCollection(
|
||||
gatewayClasses krt.Collection[*gateway.GatewayClass],
|
||||
opts krt.OptionsBuilder,
|
||||
) (
|
||||
krt.StatusCollection[*gateway.GatewayClass, gateway.GatewayClassStatus],
|
||||
krt.Collection[GatewayClass],
|
||||
) {
|
||||
return krt.NewStatusCollection(gatewayClasses, func(ctx krt.HandlerContext, obj *gateway.GatewayClass) (*gateway.GatewayClassStatus, *GatewayClass) {
|
||||
_, known := classInfos[obj.Spec.ControllerName]
|
||||
if !known {
|
||||
return nil, nil
|
||||
}
|
||||
status := obj.Status.DeepCopy()
|
||||
status = GetClassStatus(status, obj.Generation)
|
||||
return status, &GatewayClass{
|
||||
Name: obj.Name,
|
||||
Controller: obj.Spec.ControllerName,
|
||||
}
|
||||
}, opts.WithName("GatewayClasses")...)
|
||||
}
|
||||
|
||||
func fetchClass(ctx krt.HandlerContext, gatewayClasses krt.Collection[GatewayClass], gc gatewayv1.ObjectName) *GatewayClass {
|
||||
class := krt.FetchOne(ctx, gatewayClasses, krt.FilterKey(string(gc)))
|
||||
if class == nil {
|
||||
if bc, f := builtinClasses[gc]; f {
|
||||
// We allow some classes to exist without being in the cluster
|
||||
return &GatewayClass{
|
||||
Name: string(gc),
|
||||
Controller: bc,
|
||||
}
|
||||
}
|
||||
// No gateway class found, this may be meant for another controller; should be skipped.
|
||||
return nil
|
||||
}
|
||||
return class
|
||||
}
|
||||
@@ -16,17 +16,17 @@ package istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/kclient/clienttest"
|
||||
"istio.io/istio/pkg/test"
|
||||
"istio.io/istio/pkg/test/util/retry"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
)
|
||||
|
||||
func TestClassController(t *testing.T) {
|
||||
@@ -71,19 +71,19 @@ func TestClassController(t *testing.T) {
|
||||
}
|
||||
|
||||
// Class should be created initially
|
||||
expectClass(defaultClassName, constants.ManagedGatewayController)
|
||||
expectClass(constants.DefaultGatewayClass, constants.ManagedGatewayController)
|
||||
|
||||
// Once we delete it, it should be added back
|
||||
deleteClass(defaultClassName)
|
||||
expectClass(defaultClassName, constants.ManagedGatewayController)
|
||||
deleteClass(constants.DefaultGatewayClass)
|
||||
expectClass(constants.DefaultGatewayClass, constants.ManagedGatewayController)
|
||||
|
||||
// Overwrite the class, controller should not reconcile it back
|
||||
createClass(defaultClassName, "different-controller")
|
||||
expectClass(defaultClassName, "different-controller")
|
||||
createClass(constants.DefaultGatewayClass, "different-controller")
|
||||
expectClass(constants.DefaultGatewayClass, "different-controller")
|
||||
|
||||
// Once we delete it, it should be added back
|
||||
deleteClass(defaultClassName)
|
||||
expectClass(defaultClassName, constants.ManagedGatewayController)
|
||||
deleteClass(constants.DefaultGatewayClass)
|
||||
expectClass(constants.DefaultGatewayClass, constants.ManagedGatewayController)
|
||||
|
||||
// Create an unrelated GatewayClass, we should not do anything to it
|
||||
createClass("something-else", "different-controller")
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
Copyright Istio 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 istio
|
||||
|
||||
type gatewayGeneration struct {
|
||||
inner any
|
||||
}
|
||||
|
||||
func (g *gatewayGeneration) SetObservedGeneration(i int64) {
|
||||
// Intentionally blank. The observed generation of a gateway
|
||||
// status type is contained in the individual conditions
|
||||
// not at the top level, and is the responsibility
|
||||
// of the condition functions to update.
|
||||
}
|
||||
|
||||
func (g *gatewayGeneration) Unwrap() any {
|
||||
return g.inner
|
||||
}
|
||||
625
pkg/ingress/kube/gateway/istio/inferencepool_collection.go
Normal file
625
pkg/ingress/kube/gateway/istio/inferencepool_collection.go
Normal file
@@ -0,0 +1,625 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/kclient"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/maps"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
maxServiceNameLength = 63
|
||||
hashSize = 8
|
||||
InferencePoolRefLabel = "higress.io/inferencepool-name"
|
||||
InferencePoolExtensionRefSvc = "higress.io/inferencepool-extension-service"
|
||||
InferencePoolExtensionRefPort = "higress.io/inferencepool-extension-port"
|
||||
InferencePoolExtensionRefFailureMode = "higress.io/inferencepool-extension-failure-mode"
|
||||
)
|
||||
|
||||
// // ManagedLabel is the label used to identify resources managed by this controller
|
||||
// const ManagedLabel = "inference.x-k8s.io/managed-by"
|
||||
|
||||
// ControllerName is the name of this controller for labeling resources it manages
|
||||
const ControllerName = "inference-controller"
|
||||
|
||||
var supportedControllers = getSupportedControllers()
|
||||
|
||||
func getSupportedControllers() sets.Set[gatewayv1.GatewayController] {
|
||||
ret := sets.New[gatewayv1.GatewayController]()
|
||||
for _, controller := range builtinClasses {
|
||||
ret.Insert(controller)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type shadowServiceInfo struct {
|
||||
key types.NamespacedName
|
||||
selector map[string]string
|
||||
poolName string
|
||||
poolUID types.UID
|
||||
// targetPorts is the port number on the pods selected by the selector.
|
||||
// Currently, inference extension only supports a single target port.
|
||||
targetPorts []targetPort
|
||||
}
|
||||
|
||||
type targetPort struct {
|
||||
port int32
|
||||
}
|
||||
|
||||
type extRefInfo struct {
|
||||
name string
|
||||
port int32
|
||||
failureMode string
|
||||
}
|
||||
|
||||
type InferencePool struct {
|
||||
shadowService shadowServiceInfo
|
||||
extRef extRefInfo
|
||||
gatewayParents sets.Set[types.NamespacedName] // Gateways that reference this InferencePool
|
||||
}
|
||||
|
||||
func (i InferencePool) ResourceName() string {
|
||||
return i.shadowService.key.Namespace + "/" + i.shadowService.poolName
|
||||
}
|
||||
|
||||
func InferencePoolCollection(
|
||||
pools krt.Collection[*inferencev1.InferencePool],
|
||||
services krt.Collection[*corev1.Service],
|
||||
httpRoutes krt.Collection[*gateway.HTTPRoute],
|
||||
gateways krt.Collection[*gateway.Gateway],
|
||||
routesByInferencePool krt.Index[string, *gateway.HTTPRoute],
|
||||
c *Controller,
|
||||
opts krt.OptionsBuilder,
|
||||
) (krt.StatusCollection[*inferencev1.InferencePool, inferencev1.InferencePoolStatus], krt.Collection[InferencePool]) {
|
||||
return krt.NewStatusCollection(pools,
|
||||
func(
|
||||
ctx krt.HandlerContext,
|
||||
pool *inferencev1.InferencePool,
|
||||
) (*inferencev1.InferencePoolStatus, *InferencePool) {
|
||||
// Fetch HTTPRoutes that reference this InferencePool once and reuse
|
||||
routeList := krt.Fetch(ctx, httpRoutes, krt.FilterIndex(routesByInferencePool, pool.Namespace+"/"+pool.Name))
|
||||
|
||||
// Find gateway parents that reference this InferencePool through HTTPRoutes
|
||||
gatewayParents := findGatewayParents(pool, routeList)
|
||||
|
||||
// TODO: If no gateway parents, we should not do anything
|
||||
// note: we still need to filter out our Status to clean up previous reconciliations
|
||||
|
||||
// Create the InferencePool only if there are Gateways connected
|
||||
var inferencePool *InferencePool
|
||||
if len(gatewayParents) > 0 {
|
||||
// Create the InferencePool object
|
||||
inferencePool = createInferencePoolObject(pool, gatewayParents)
|
||||
}
|
||||
|
||||
// Calculate status
|
||||
status := calculateInferencePoolStatus(pool, gatewayParents, services, gateways, routeList)
|
||||
|
||||
return status, inferencePool
|
||||
}, opts.WithName("InferenceExtension")...)
|
||||
}
|
||||
|
||||
// createInferencePoolObject creates the InferencePool object with shadow service and extension ref info
|
||||
func createInferencePoolObject(pool *inferencev1.InferencePool, gatewayParents sets.Set[types.NamespacedName]) *InferencePool {
|
||||
// Build extension reference info
|
||||
extRef := extRefInfo{
|
||||
name: string(pool.Spec.EndpointPickerRef.Name),
|
||||
}
|
||||
|
||||
if pool.Spec.EndpointPickerRef.Port == nil {
|
||||
log.Errorf("invalid InferencePool %s/%s; endpointPickerRef port is required", pool.Namespace, pool.Name)
|
||||
return nil
|
||||
}
|
||||
extRef.port = int32(pool.Spec.EndpointPickerRef.Port.Number)
|
||||
|
||||
extRef.failureMode = string(inferencev1.EndpointPickerFailClose) // Default failure mode
|
||||
if pool.Spec.EndpointPickerRef.FailureMode != inferencev1.EndpointPickerFailClose {
|
||||
extRef.failureMode = string(pool.Spec.EndpointPickerRef.FailureMode)
|
||||
}
|
||||
|
||||
svcName, err := InferencePoolServiceName(pool.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate service name for InferencePool %s: %v", pool.Name, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
shadowSvcInfo := shadowServiceInfo{
|
||||
key: types.NamespacedName{
|
||||
Name: svcName,
|
||||
Namespace: pool.GetNamespace(),
|
||||
},
|
||||
selector: make(map[string]string, len(pool.Spec.Selector.MatchLabels)),
|
||||
poolName: pool.GetName(),
|
||||
targetPorts: make([]targetPort, 0, len(pool.Spec.TargetPorts)),
|
||||
poolUID: pool.GetUID(),
|
||||
}
|
||||
|
||||
for k, v := range pool.Spec.Selector.MatchLabels {
|
||||
shadowSvcInfo.selector[string(k)] = string(v)
|
||||
}
|
||||
|
||||
for _, port := range pool.Spec.TargetPorts {
|
||||
shadowSvcInfo.targetPorts = append(shadowSvcInfo.targetPorts, targetPort{port: int32(port.Number)})
|
||||
}
|
||||
|
||||
return &InferencePool{
|
||||
shadowService: shadowSvcInfo,
|
||||
extRef: extRef,
|
||||
gatewayParents: gatewayParents,
|
||||
}
|
||||
}
|
||||
|
||||
// calculateInferencePoolStatus calculates the complete status for an InferencePool
|
||||
func calculateInferencePoolStatus(
|
||||
pool *inferencev1.InferencePool,
|
||||
gatewayParents sets.Set[types.NamespacedName],
|
||||
services krt.Collection[*corev1.Service],
|
||||
gateways krt.Collection[*gateway.Gateway],
|
||||
routeList []*gateway.HTTPRoute,
|
||||
) *inferencev1.InferencePoolStatus {
|
||||
// Calculate status for each gateway parent
|
||||
existingParents := pool.Status.DeepCopy().Parents
|
||||
finalParents := []inferencev1.ParentStatus{}
|
||||
|
||||
// Add existing parents from other controllers (not managed by us)
|
||||
for _, existingParent := range existingParents {
|
||||
gtwName := string(existingParent.ParentRef.Name)
|
||||
gtwNamespace := pool.Namespace
|
||||
if existingParent.ParentRef.Namespace != "" {
|
||||
gtwNamespace = string(existingParent.ParentRef.Namespace)
|
||||
}
|
||||
parentKey := types.NamespacedName{
|
||||
Name: gtwName,
|
||||
Namespace: gtwNamespace,
|
||||
}
|
||||
|
||||
isCurrentlyOurs := gatewayParents.Contains(parentKey)
|
||||
|
||||
// Keep parents that are not ours and not default status parents
|
||||
if !isCurrentlyOurs &&
|
||||
!isOurManagedGateway(gateways, gtwNamespace, gtwName) &&
|
||||
!isDefaultStatusParent(existingParent) {
|
||||
finalParents = append(finalParents, existingParent)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate status for each of our gateway parents
|
||||
for gatewayParent := range gatewayParents {
|
||||
parentStatus := calculateSingleParentStatus(pool, gatewayParent, services, existingParents, routeList)
|
||||
finalParents = append(finalParents, parentStatus)
|
||||
}
|
||||
|
||||
return &inferencev1.InferencePoolStatus{
|
||||
Parents: finalParents,
|
||||
}
|
||||
}
|
||||
|
||||
// findGatewayParents finds all Gateway parents that reference this InferencePool through HTTPRoutes
|
||||
func findGatewayParents(
|
||||
pool *inferencev1.InferencePool,
|
||||
routeList []*gateway.HTTPRoute,
|
||||
) sets.Set[types.NamespacedName] {
|
||||
gatewayParents := sets.New[types.NamespacedName]()
|
||||
|
||||
for _, route := range routeList {
|
||||
// Only process routes that reference our InferencePool
|
||||
if !routeReferencesInferencePool(route, pool) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the route's parent status to find accepted gateways
|
||||
for _, parentStatus := range route.Status.Parents {
|
||||
// Only consider parents managed by our supported controllers (from supportedControllers variable)
|
||||
// This filters out parents from other controllers we don't manage
|
||||
if !supportedControllers.Contains(parentStatus.ControllerName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the gateway namespace (default to route namespace if not specified)
|
||||
gatewayNamespace := route.Namespace
|
||||
if ptr.OrEmpty(parentStatus.ParentRef.Namespace) != "" {
|
||||
gatewayNamespace = string(*parentStatus.ParentRef.Namespace)
|
||||
}
|
||||
|
||||
gatewayParents.Insert(types.NamespacedName{
|
||||
Name: string(parentStatus.ParentRef.Name),
|
||||
Namespace: gatewayNamespace,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return gatewayParents
|
||||
}
|
||||
|
||||
// routeReferencesInferencePool checks if an HTTPRoute references the given InferencePool
|
||||
func routeReferencesInferencePool(route *gateway.HTTPRoute, pool *inferencev1.InferencePool) bool {
|
||||
for _, rule := range route.Spec.Rules {
|
||||
for _, backendRef := range rule.BackendRefs {
|
||||
if !isInferencePoolBackendRef(backendRef.BackendRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this backend ref points to our InferencePool
|
||||
if string(backendRef.BackendRef.Name) != pool.ObjectMeta.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check namespace match
|
||||
backendRefNamespace := route.Namespace
|
||||
if ptr.OrEmpty(backendRef.BackendRef.Namespace) != "" {
|
||||
backendRefNamespace = string(*backendRef.BackendRef.Namespace)
|
||||
}
|
||||
|
||||
if backendRefNamespace == pool.Namespace {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isInferencePoolBackendRef checks if a BackendRef is pointing to an InferencePool
|
||||
func isInferencePoolBackendRef(backendRef gatewayv1.BackendRef) bool {
|
||||
return ptr.OrEmpty(backendRef.Group) == gatewayv1.Group(gvk.InferencePool.Group) &&
|
||||
ptr.OrEmpty(backendRef.Kind) == gatewayv1.Kind(gvk.InferencePool.Kind)
|
||||
}
|
||||
|
||||
// calculateSingleParentStatus calculates the status for a single gateway parent
|
||||
func calculateSingleParentStatus(
|
||||
pool *inferencev1.InferencePool,
|
||||
gatewayParent types.NamespacedName,
|
||||
services krt.Collection[*corev1.Service],
|
||||
existingParents []inferencev1.ParentStatus,
|
||||
routeList []*gateway.HTTPRoute,
|
||||
) inferencev1.ParentStatus {
|
||||
// Find existing status for this parent to preserve some conditions
|
||||
var existingConditions []metav1.Condition
|
||||
for _, existingParent := range existingParents {
|
||||
if string(existingParent.ParentRef.Name) == gatewayParent.Name &&
|
||||
string(existingParent.ParentRef.Namespace) == gatewayParent.Namespace {
|
||||
existingConditions = existingParent.Conditions
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to only keep conditions we manage
|
||||
filteredConditions := filterUsedConditions(existingConditions,
|
||||
inferencev1.InferencePoolConditionAccepted,
|
||||
inferencev1.InferencePoolConditionResolvedRefs)
|
||||
|
||||
// Calculate Accepted status by checking HTTPRoute parent status
|
||||
acceptedStatus := calculateAcceptedStatus(pool, gatewayParent, routeList)
|
||||
|
||||
// Calculate ResolvedRefs status
|
||||
resolvedRefsStatus := calculateResolvedRefsStatus(pool, services)
|
||||
|
||||
// Build the final status
|
||||
return inferencev1.ParentStatus{
|
||||
ParentRef: inferencev1.ParentReference{
|
||||
Group: (*inferencev1.Group)(&gvk.Gateway.Group),
|
||||
Kind: inferencev1.Kind(gvk.Gateway.Kind),
|
||||
Namespace: inferencev1.Namespace(gatewayParent.Namespace),
|
||||
Name: inferencev1.ObjectName(gatewayParent.Name),
|
||||
},
|
||||
Conditions: setConditions(pool.Generation, filteredConditions, map[string]*condition{
|
||||
string(inferencev1.InferencePoolConditionAccepted): acceptedStatus,
|
||||
string(inferencev1.InferencePoolConditionResolvedRefs): resolvedRefsStatus,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateAcceptedStatus determines if the InferencePool is accepted by checking HTTPRoute parent status
|
||||
func calculateAcceptedStatus(
|
||||
pool *inferencev1.InferencePool,
|
||||
gatewayParent types.NamespacedName,
|
||||
routeList []*gateway.HTTPRoute,
|
||||
) *condition {
|
||||
// Check if any HTTPRoute references this InferencePool and has this gateway as an accepted parent
|
||||
for _, route := range routeList {
|
||||
// Only process routes that reference our InferencePool
|
||||
if !routeReferencesInferencePool(route, pool) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this route has our gateway as a parent and if it's accepted
|
||||
for _, parentStatus := range route.Status.Parents {
|
||||
// Only consider parents managed by supported controllers
|
||||
if !supportedControllers.Contains(parentStatus.ControllerName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this parent refers to our gateway
|
||||
gatewayNamespace := route.Namespace
|
||||
if ptr.OrEmpty(parentStatus.ParentRef.Namespace) != "" {
|
||||
gatewayNamespace = string(*parentStatus.ParentRef.Namespace)
|
||||
}
|
||||
|
||||
if string(parentStatus.ParentRef.Name) == gatewayParent.Name && gatewayNamespace == gatewayParent.Namespace {
|
||||
// Check if this parent is accepted
|
||||
for _, parentCondition := range parentStatus.Conditions {
|
||||
if parentCondition.Type == string(gatewayv1.RouteConditionAccepted) {
|
||||
if parentCondition.Status == metav1.ConditionTrue {
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonAccepted),
|
||||
status: metav1.ConditionTrue,
|
||||
message: "Referenced by an HTTPRoute accepted by the parentRef Gateway",
|
||||
}
|
||||
}
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonHTTPRouteNotAccepted),
|
||||
status: metav1.ConditionFalse,
|
||||
message: fmt.Sprintf("Referenced HTTPRoute %s/%s not accepted by Gateway %s/%s: %s",
|
||||
route.Namespace, route.Name, gatewayParent.Namespace, gatewayParent.Name, parentCondition.Message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no Accepted condition found, treat as unknown (parent is listed in status)
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonAccepted),
|
||||
status: metav1.ConditionUnknown,
|
||||
message: "Referenced by an HTTPRoute unknown parentRef Gateway status",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no HTTPRoute was found that references this InferencePool with this gateway as parent
|
||||
// This shouldn't happen in normal operation since we only call this for known gateway parents
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonHTTPRouteNotAccepted),
|
||||
status: metav1.ConditionFalse,
|
||||
message: fmt.Sprintf("No HTTPRoute found referencing this InferencePool with Gateway %s/%s as parent",
|
||||
gatewayParent.Namespace, gatewayParent.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateResolvedRefsStatus determines the states of the ExtensionRef
|
||||
// * if the kind is supported
|
||||
// * if the extensionRef is defined
|
||||
// * if the service exists in the same namespace as the InferencePool
|
||||
func calculateResolvedRefsStatus(
|
||||
pool *inferencev1.InferencePool,
|
||||
services krt.Collection[*corev1.Service],
|
||||
) *condition {
|
||||
// Default Kind to Service if unset
|
||||
kind := string(pool.Spec.EndpointPickerRef.Kind)
|
||||
if kind == "" {
|
||||
kind = gvk.Service.Kind
|
||||
}
|
||||
|
||||
if kind != gvk.Service.Kind {
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
|
||||
status: metav1.ConditionFalse,
|
||||
message: "Unsupported ExtensionRef kind " + kind,
|
||||
}
|
||||
}
|
||||
|
||||
name := string(pool.Spec.EndpointPickerRef.Name)
|
||||
if name == "" {
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
|
||||
status: metav1.ConditionFalse,
|
||||
message: "ExtensionRef not defined",
|
||||
}
|
||||
}
|
||||
|
||||
svc := ptr.Flatten(services.GetKey(fmt.Sprintf("%s/%s", pool.Namespace, name)))
|
||||
if svc == nil {
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
|
||||
status: metav1.ConditionFalse,
|
||||
message: "Referenced ExtensionRef not found " + name,
|
||||
}
|
||||
}
|
||||
|
||||
return &condition{
|
||||
reason: string(inferencev1.InferencePoolReasonResolvedRefs),
|
||||
status: metav1.ConditionTrue,
|
||||
message: "Referenced ExtensionRef resolved successfully",
|
||||
}
|
||||
}
|
||||
|
||||
// isDefaultStatusParent checks if this is a default status parent entry
|
||||
func isDefaultStatusParent(parent inferencev1.ParentStatus) bool {
|
||||
return string(parent.ParentRef.Kind) == "Status" && parent.ParentRef.Name == "default"
|
||||
}
|
||||
|
||||
// isOurManagedGateway checks if a Gateway is managed by one of our supported controllers
|
||||
// This is used to identify stale parent entries that we previously added but are no longer referenced by HTTPRoutes
|
||||
func isOurManagedGateway(gateways krt.Collection[*gateway.Gateway], namespace, name string) bool {
|
||||
gtw := ptr.Flatten(gateways.GetKey(fmt.Sprintf("%s/%s", namespace, name)))
|
||||
if gtw == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := builtinClasses[gtw.Spec.GatewayClassName]
|
||||
return ok
|
||||
}
|
||||
|
||||
func filterUsedConditions(conditions []metav1.Condition, usedConditions ...inferencev1.InferencePoolConditionType) []metav1.Condition {
|
||||
var result []metav1.Condition
|
||||
for _, condition := range conditions {
|
||||
if slices.Contains(usedConditions, inferencev1.InferencePoolConditionType(condition.Type)) {
|
||||
result = append(result, condition)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// generateHash generates an 8-character SHA256 hash of the input string.
|
||||
func generateHash(input string, length int) string {
|
||||
hashBytes := sha256.Sum256([]byte(input))
|
||||
hashString := fmt.Sprintf("%x", hashBytes) // Convert to hexadecimal string
|
||||
return hashString[:length] // Truncate to desired length
|
||||
}
|
||||
|
||||
func InferencePoolServiceName(poolName string) (string, error) {
|
||||
ipSeparator := "-ip-"
|
||||
hash := generateHash(poolName, hashSize)
|
||||
svcName := poolName + ipSeparator + hash
|
||||
// Truncate if necessary to meet the Kubernetes naming constraints
|
||||
if len(svcName) > maxServiceNameLength {
|
||||
// Calculate the maximum allowed base name length
|
||||
maxBaseLength := maxServiceNameLength - len(ipSeparator) - hashSize
|
||||
if maxBaseLength < 0 {
|
||||
return "", fmt.Errorf("inference pool name: %s is too long", poolName)
|
||||
}
|
||||
|
||||
// Truncate the base name and reconstruct the service name
|
||||
truncatedBase := poolName[:maxBaseLength]
|
||||
svcName = truncatedBase + ipSeparator + hash
|
||||
}
|
||||
return svcName, nil
|
||||
}
|
||||
|
||||
func translateShadowServiceToService(existingLabels map[string]string, shadow shadowServiceInfo, extRef extRefInfo) *corev1.Service {
|
||||
// Create the ports used by the shadow service
|
||||
ports := make([]corev1.ServicePort, 0, len(shadow.targetPorts))
|
||||
dummyPort := int32(54321) // Dummy port, not used for anything
|
||||
for i, port := range shadow.targetPorts {
|
||||
ports = append(ports, corev1.ServicePort{
|
||||
Name: "port" + strconv.Itoa(i),
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
Port: dummyPort + int32(i),
|
||||
TargetPort: intstr.FromInt(int(port.port)),
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new service object based on the shadow service info
|
||||
svc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: shadow.key.Name,
|
||||
Namespace: shadow.key.Namespace,
|
||||
Labels: maps.MergeCopy(map[string]string{
|
||||
InferencePoolRefLabel: shadow.poolName,
|
||||
InferencePoolExtensionRefSvc: extRef.name,
|
||||
InferencePoolExtensionRefPort: strconv.Itoa(int(extRef.port)),
|
||||
InferencePoolExtensionRefFailureMode: extRef.failureMode,
|
||||
constants.InternalServiceSemantics: constants.ServiceSemanticsInferencePool,
|
||||
}, existingLabels),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: shadow.selector,
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
ClusterIP: corev1.ClusterIPNone, // Headless service
|
||||
Ports: ports,
|
||||
},
|
||||
}
|
||||
|
||||
svc.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: gvk.InferencePool.GroupVersion(),
|
||||
Kind: gvk.InferencePool.Kind,
|
||||
Name: shadow.poolName,
|
||||
UID: shadow.poolUID,
|
||||
},
|
||||
})
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (c *Controller) reconcileShadowService(
|
||||
svcClient kclient.Client[*corev1.Service],
|
||||
inferencePools krt.Collection[InferencePool],
|
||||
servicesCollection krt.Collection[*corev1.Service],
|
||||
) func(key types.NamespacedName) error {
|
||||
return func(key types.NamespacedName) error {
|
||||
// Find the InferencePool that matches the key
|
||||
pool := inferencePools.GetKey(key.String())
|
||||
if pool == nil {
|
||||
// we'll generally ignore these scenarios, since the InferencePool may have been deleted
|
||||
log.Debugf("inferencepool no longer exists", key.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// We found the InferencePool, now we need to translate it to a shadow Service
|
||||
// and check if it exists already
|
||||
existingService := ptr.Flatten(servicesCollection.GetKey(pool.shadowService.key.String()))
|
||||
|
||||
// Check if we can manage this service
|
||||
var existingLabels map[string]string
|
||||
if existingService != nil {
|
||||
existingLabels = existingService.GetLabels()
|
||||
canManage, _ := c.canManageShadowServiceForInference(existingService)
|
||||
if !canManage {
|
||||
log.Debugf("skipping service %s/%s, already managed by another controller", key.Namespace, key.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
service := translateShadowServiceToService(existingLabels, pool.shadowService, pool.extRef)
|
||||
|
||||
var err error
|
||||
if existingService == nil {
|
||||
// Create the service if it doesn't exist
|
||||
_, err = svcClient.Create(service)
|
||||
} else {
|
||||
// TODO: Don't overwrite resources: https://github.com/istio/istio/issues/56667
|
||||
service.ResourceVersion = existingService.ResourceVersion
|
||||
_, err = svcClient.Update(service)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// canManage checks if a service should be managed by this controller
|
||||
func (c *Controller) canManageShadowServiceForInference(obj *corev1.Service) (bool, string) {
|
||||
if obj == nil {
|
||||
// No object exists, we can manage it
|
||||
return true, ""
|
||||
}
|
||||
|
||||
_, inferencePoolManaged := obj.GetLabels()[InferencePoolRefLabel]
|
||||
// We can manage if it has no manager or if we are the manager
|
||||
return inferencePoolManaged, obj.GetResourceVersion()
|
||||
}
|
||||
|
||||
func indexHTTPRouteByInferencePool(o *gateway.HTTPRoute) []string {
|
||||
var keys []string
|
||||
for _, rule := range o.Spec.Rules {
|
||||
for _, backendRef := range rule.BackendRefs {
|
||||
if isInferencePoolBackendRef(backendRef.BackendRef) {
|
||||
// If BackendRef.Namespace is not specified, the backend is in the same namespace as the HTTPRoute's
|
||||
backendRefNamespace := o.Namespace
|
||||
if ptr.OrEmpty(backendRef.BackendRef.Namespace) != "" {
|
||||
backendRefNamespace = string(*backendRef.BackendRef.Namespace)
|
||||
}
|
||||
key := backendRefNamespace + "/" + string(backendRef.Name)
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
794
pkg/ingress/kube/gateway/istio/inferencepool_status_test.go
Normal file
794
pkg/ingress/kube/gateway/istio/inferencepool_status_test.go
Normal file
@@ -0,0 +1,794 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/test"
|
||||
)
|
||||
|
||||
const (
|
||||
IstioController = "higress.io/gateway-controller"
|
||||
DefaultTestNS = "default"
|
||||
GatewayTestNS = "gateway-ns"
|
||||
AppTestNS = "app-ns"
|
||||
EmptyTestNS = ""
|
||||
infPoolPending = "Pending"
|
||||
)
|
||||
|
||||
func TestInferencePoolStatusReconciliation(t *testing.T) {
|
||||
test.SetForTest(t, &features.EnableGatewayAPIInferenceExtension, true)
|
||||
testCases := []struct {
|
||||
name string
|
||||
givens []runtime.Object // Objects to create before the test
|
||||
targetPool *inferencev1.InferencePool // The InferencePool to check
|
||||
expectations func(t *testing.T, pool *inferencev1.InferencePoolStatus)
|
||||
}{
|
||||
//
|
||||
// Positive Test Scenarios
|
||||
//
|
||||
{
|
||||
name: "should add gateway parentRef to inferencepool status",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithRouteParentCondition(string(gatewayv1.RouteConditionAccepted), metav1.ConditionTrue, "Accepted", "Accepted"),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionAccepted),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(inferencev1.InferencePoolReasonAccepted),
|
||||
Message: "Referenced by an HTTPRoute",
|
||||
}, "Expected condition with Accepted")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should add only 1 gateway parentRef to status for multiple routes on different gateways with different controllers",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithParentRefAndStatus("gateway-2", DefaultTestNS, "other-controller"),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should keep the status of the gateway parentRefs from another controller",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other-class")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
NewHTTPRoute("route-2", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-2", DefaultTestNS, "other-class"),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("gateway-2", DefaultTestNS, WithAcceptedConditions())),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 2, "Expected two parent references")
|
||||
assert.ElementsMatch(t,
|
||||
[]string{"gateway-1", "gateway-2"},
|
||||
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should add multiple gateway parentRefs to status for multiple routes",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
NewHTTPRoute("route-2", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-2", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 2, "Expected two parent references")
|
||||
assert.ElementsMatch(t,
|
||||
[]string{"gateway-1", "gateway-2"},
|
||||
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should remove our status from previous reconciliation that is no longer referenced by any HTTPRoute",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
|
||||
WithParentStatus("gateway-2", DefaultTestNS,
|
||||
WithAcceptedConditions(),
|
||||
)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should update/recreate our status from previous reconciliation",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
|
||||
WithParentStatus("gateway-1", DefaultTestNS,
|
||||
WithAcceptedConditions(),
|
||||
)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
|
||||
require.Len(t, status.Parents[0].Conditions, 2, "Expected two conditions")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should keep others status from previous reconciliation",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewGateway("gateway-2", InNamespace(DefaultTestNS), WithGatewayClass("other-class")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("gateway-2", DefaultTestNS, WithAcceptedConditions())),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 2, "Expected two parent references")
|
||||
assert.ElementsMatch(t,
|
||||
[]string{"gateway-1", "gateway-2"},
|
||||
[]string{string(status.Parents[0].ParentRef.Name), string(status.Parents[1].ParentRef.Name)},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should remove default parent 'waiting for controller' status",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithParentStatus("default", DefaultTestNS, AsDefaultStatus())),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected two parent references")
|
||||
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should remove unknown condition types from controlled parents",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("gateway-1", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-1", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("gateway-1", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS),
|
||||
WithParentStatus("gateway-1", DefaultTestNS,
|
||||
WithAcceptedConditions(),
|
||||
WithConditions(metav1.ConditionUnknown, "X", "Y", "Dummy"),
|
||||
)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected two parent references")
|
||||
assert.Equal(t, "gateway-1", string(status.Parents[0].ParentRef.Name))
|
||||
require.Len(t, status.Parents[0].Conditions, 2, "Expected two conditions")
|
||||
assert.ElementsMatch(t,
|
||||
[]string{string(inferencev1.InferencePoolConditionAccepted), string(inferencev1.InferencePoolConditionResolvedRefs)},
|
||||
[]string{status.Parents[0].Conditions[0].Type, status.Parents[0].Conditions[1].Type},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle cross-namespace gateway references correctly",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(AppTestNS),
|
||||
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
|
||||
WithBackendRef("test-pool", AppTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle cross-namespace httproute references correctly",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(AppTestNS),
|
||||
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle HTTPRoute in same namespace (empty)",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(AppTestNS),
|
||||
WithParentRefAndStatus("main-gateway", GatewayTestNS, IstioController),
|
||||
WithBackendRef("test-pool", EmptyTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, GatewayTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle Gateway in same namespace (empty)",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(AppTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(AppTestNS),
|
||||
WithParentRefAndStatus("main-gateway", EmptyTestNS, IstioController),
|
||||
WithBackendRef("test-pool", AppTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(AppTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, AppTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should add only one parentRef for multiple routes on same gateway",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("route-a", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
NewHTTPRoute("route-b", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected only one parent reference for the same gateway")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should report ResolvedRef true when ExtensioNRef found",
|
||||
givens: []runtime.Object{
|
||||
NewService("test-epp", InNamespace(DefaultTestNS)),
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithExtensionRef("Service", "test-epp")),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(inferencev1.InferencePoolReasonResolvedRefs),
|
||||
Message: "Referenced ExtensionRef resolved",
|
||||
}, "Expected condition with InvalidExtensionRef")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should report HTTPRoute not accepted when parent gateway rejects HTTPRoute",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithRouteParentCondition(string(gatewayv1.RouteConditionAccepted), metav1.ConditionFalse, "GatewayNotReady", "Gateway not ready"),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionAccepted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(inferencev1.InferencePoolReasonHTTPRouteNotAccepted),
|
||||
Message: "Referenced HTTPRoute default/test-route not accepted by Gateway default/main-gateway",
|
||||
}, "Expected condition with HTTPRouteNotAccepted")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should report unknown status when HTTPRoute parent status has no Accepted condition",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
// Note: No WithRouteParentCondition for Accepted - parent is listed but has no conditions
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
assert.Equal(t, "main-gateway", string(status.Parents[0].ParentRef.Name))
|
||||
assert.Equal(t, DefaultTestNS, string(status.Parents[0].ParentRef.Namespace))
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionAccepted),
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: string(inferencev1.InferencePoolReasonAccepted),
|
||||
Message: "Referenced by an HTTPRoute unknown parentRef Gateway status",
|
||||
}, "Expected condition with ConditionUnknown")
|
||||
},
|
||||
},
|
||||
|
||||
//
|
||||
// Negative Test Scenarios
|
||||
//
|
||||
{
|
||||
name: "should not add parentRef for gatewayclass not controlled by us",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("other")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, "other-controller"),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
assert.Empty(t, status.Parents, "ParentRefs should be empty")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not add parentRef if httproute has no backendref",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(DefaultTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController)), // No BackendRef
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
assert.Empty(t, status.Parents, "ParentRefs should be empty")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not add parentRef if httproute has no parentref",
|
||||
givens: []runtime.Object{
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithBackendRef("test-pool", DefaultTestNS)), // No ParentRef
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
assert.Empty(t, status.Parents, "ParentRefs should be empty")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should report ExtensionRef not found if no matching service found",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS)),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
|
||||
Message: "Referenced ExtensionRef not found",
|
||||
}, "Expected condition with InvalidExtensionRef")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should report unsupported ExtensionRef if kind is not service",
|
||||
givens: []runtime.Object{
|
||||
NewGateway("main-gateway", InNamespace(GatewayTestNS), WithGatewayClass("higress")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("main-gateway", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS)),
|
||||
},
|
||||
targetPool: NewInferencePool("test-pool", InNamespace(DefaultTestNS), WithExtensionRef("Gateway", "main-gateway")),
|
||||
expectations: func(t *testing.T, status *inferencev1.InferencePoolStatus) {
|
||||
require.Len(t, status.Parents, 1, "Expected one parent reference")
|
||||
require.Len(t, status.Parents[0].Conditions, 2, "Expected two condition")
|
||||
assertConditionContains(t, status.Parents[0].Conditions, metav1.Condition{
|
||||
Type: string(inferencev1.InferencePoolConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(inferencev1.InferencePoolReasonInvalidExtensionRef),
|
||||
Message: "Unsupported ExtensionRef kind",
|
||||
}, "Expected condition with InvalidExtensionRef")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stop := test.NewStop(t)
|
||||
|
||||
controller := setupController(t,
|
||||
append(tc.givens, tc.targetPool)...,
|
||||
)
|
||||
|
||||
sq := &TestStatusQueue{
|
||||
state: map[status.Resource]any{},
|
||||
}
|
||||
statusSynced := controller.status.SetQueue(sq)
|
||||
for _, st := range statusSynced {
|
||||
st.WaitUntilSynced(stop)
|
||||
}
|
||||
|
||||
dumpOnFailure(t, krt.GlobalDebugHandler)
|
||||
|
||||
getInferencePoolStatus := func() *inferencev1.InferencePoolStatus {
|
||||
statuses := sq.Statuses()
|
||||
for _, status := range statuses {
|
||||
if pool, ok := status.(*inferencev1.InferencePoolStatus); ok {
|
||||
return pool
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
poolStatus := getInferencePoolStatus()
|
||||
assert.NotNil(t, poolStatus)
|
||||
|
||||
tc.expectations(t, poolStatus)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertConditionContains(t *testing.T, conditions []metav1.Condition, expected metav1.Condition, msgAndArgs ...interface{}) {
|
||||
t.Helper()
|
||||
|
||||
for _, condition := range conditions {
|
||||
if (expected.Type == "" || condition.Type == expected.Type) &&
|
||||
(expected.Status == "" || condition.Status == expected.Status) &&
|
||||
(expected.Reason == "" || condition.Reason == expected.Reason) &&
|
||||
(expected.Message == "" || strings.HasPrefix(condition.Message, expected.Message)) {
|
||||
return // Found matching condition
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no matching condition was found
|
||||
assert.Fail(t, fmt.Sprintf("Expected condition with Type=%s, Status=%s, Reason=%s not found in conditions. Available conditions: %+v",
|
||||
expected.Type, expected.Status, expected.Reason, conditions), msgAndArgs...)
|
||||
}
|
||||
|
||||
// --- Mock Objects ---
|
||||
|
||||
// Option is a function that mutates an object.
|
||||
type Option func(client.Object)
|
||||
|
||||
type ParentOption func(*inferencev1.ParentStatus)
|
||||
|
||||
// --- Helper functions to mutate objects ---
|
||||
|
||||
func InNamespace(namespace string) Option {
|
||||
return func(obj client.Object) {
|
||||
obj.SetNamespace(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
func WithController(name string) Option {
|
||||
return func(obj client.Object) {
|
||||
gw, ok := obj.(*gateway.GatewayClass)
|
||||
if ok {
|
||||
gw.Spec.ControllerName = gateway.GatewayController(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithGatewayClass(name string) Option {
|
||||
return func(obj client.Object) {
|
||||
gw, ok := obj.(*gateway.Gateway)
|
||||
if ok {
|
||||
gw.Spec.GatewayClassName = gateway.ObjectName(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithParentRef(name, namespace string) Option {
|
||||
return func(obj client.Object) {
|
||||
hr, ok := obj.(*gateway.HTTPRoute)
|
||||
if ok {
|
||||
namespaceName := gateway.Namespace(namespace)
|
||||
hr.Spec.ParentRefs = []gateway.ParentReference{
|
||||
{
|
||||
Name: gateway.ObjectName(name),
|
||||
Namespace: &namespaceName,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithParentRefAndStatus(name, namespace, controllerName string) Option {
|
||||
return func(obj client.Object) {
|
||||
hr, ok := obj.(*gateway.HTTPRoute)
|
||||
if ok {
|
||||
namespaceName := gateway.Namespace(namespace)
|
||||
if hr.Spec.ParentRefs == nil {
|
||||
hr.Spec.ParentRefs = []gateway.ParentReference{}
|
||||
}
|
||||
hr.Spec.ParentRefs = append(hr.Spec.ParentRefs, gateway.ParentReference{
|
||||
Name: gateway.ObjectName(name),
|
||||
Namespace: &namespaceName,
|
||||
})
|
||||
if hr.Status.Parents == nil {
|
||||
hr.Status.Parents = []gateway.RouteParentStatus{}
|
||||
}
|
||||
|
||||
parentStatusRef := &gateway.RouteParentStatus{
|
||||
ParentRef: gateway.ParentReference{
|
||||
Name: gateway.ObjectName(name),
|
||||
Namespace: &namespaceName,
|
||||
},
|
||||
ControllerName: gateway.GatewayController(controllerName),
|
||||
}
|
||||
hr.Status.Parents = append(hr.Status.Parents, *parentStatusRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithRouteParentCondition(conditionType string, status metav1.ConditionStatus, reason, message string) Option {
|
||||
return func(obj client.Object) {
|
||||
hr, ok := obj.(*gateway.HTTPRoute)
|
||||
if ok && len(hr.Status.Parents) > 0 {
|
||||
// Add condition to the last parent status (most recently added)
|
||||
lastParentIdx := len(hr.Status.Parents) - 1
|
||||
if hr.Status.Parents[lastParentIdx].Conditions == nil {
|
||||
hr.Status.Parents[lastParentIdx].Conditions = []metav1.Condition{}
|
||||
}
|
||||
hr.Status.Parents[lastParentIdx].Conditions = append(hr.Status.Parents[lastParentIdx].Conditions,
|
||||
metav1.Condition{
|
||||
Type: conditionType,
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
ObservedGeneration: 1,
|
||||
LastTransitionTime: metav1.NewTime(time.Now()),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithBackendRef(name, namespace string) Option {
|
||||
return func(obj client.Object) {
|
||||
hr, ok := obj.(*gateway.HTTPRoute)
|
||||
if ok {
|
||||
namespaceName := gateway.Namespace(namespace)
|
||||
if hr.Spec.Rules == nil {
|
||||
hr.Spec.Rules = []gateway.HTTPRouteRule{}
|
||||
}
|
||||
group := gateway.Group(gvk.InferencePool.Group)
|
||||
kind := gateway.Kind(gvk.InferencePool.Kind)
|
||||
hr.Spec.Rules = append(hr.Spec.Rules, gateway.HTTPRouteRule{
|
||||
BackendRefs: []gateway.HTTPBackendRef{
|
||||
{
|
||||
BackendRef: gateway.BackendRef{
|
||||
BackendObjectReference: gateway.BackendObjectReference{
|
||||
Name: gateway.ObjectName(name),
|
||||
Namespace: &namespaceName,
|
||||
Kind: &kind,
|
||||
Group: &group,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithParentStatus(gatewayName, namespace string, opt ...ParentOption) Option {
|
||||
return func(obj client.Object) {
|
||||
ip, ok := obj.(*inferencev1.InferencePool)
|
||||
if ok {
|
||||
if ip.Status.Parents == nil {
|
||||
ip.Status.Parents = []inferencev1.ParentStatus{}
|
||||
}
|
||||
poolStatus := inferencev1.ParentStatus{
|
||||
ParentRef: inferencev1.ParentReference{
|
||||
Name: inferencev1.ObjectName(gatewayName),
|
||||
Namespace: inferencev1.Namespace(namespace),
|
||||
},
|
||||
}
|
||||
for _, opt := range opt {
|
||||
opt(&poolStatus)
|
||||
}
|
||||
ip.Status.Parents = append(ip.Status.Parents, poolStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AsDefaultStatus() ParentOption {
|
||||
return func(parentStatusRef *inferencev1.ParentStatus) {
|
||||
dName := "default"
|
||||
dKind := "Status"
|
||||
parentStatusRef.ParentRef.Name = inferencev1.ObjectName(dName)
|
||||
parentStatusRef.ParentRef.Kind = inferencev1.Kind(dKind)
|
||||
WithConditions(
|
||||
metav1.ConditionUnknown,
|
||||
string(inferencev1.InferencePoolConditionAccepted),
|
||||
infPoolPending,
|
||||
"Waiting for controller",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func WithConditions(status metav1.ConditionStatus, conType, reason, message string) ParentOption {
|
||||
return func(parentStatusRef *inferencev1.ParentStatus) {
|
||||
if parentStatusRef.Conditions == nil {
|
||||
parentStatusRef.Conditions = []metav1.Condition{}
|
||||
}
|
||||
parentStatusRef.Conditions = append(parentStatusRef.Conditions,
|
||||
metav1.Condition{
|
||||
Type: conType,
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
ObservedGeneration: 1,
|
||||
LastTransitionTime: metav1.NewTime(time.Now()),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func WithAcceptedConditions() ParentOption {
|
||||
return func(parentStatusRef *inferencev1.ParentStatus) {
|
||||
WithConditions(
|
||||
metav1.ConditionTrue,
|
||||
string(inferencev1.InferencePoolConditionAccepted),
|
||||
string(inferencev1.InferencePoolReasonAccepted),
|
||||
"Accepted by the parentRef Gateway",
|
||||
)(parentStatusRef)
|
||||
WithConditions(
|
||||
metav1.ConditionTrue,
|
||||
string(inferencev1.InferencePoolConditionResolvedRefs),
|
||||
string(inferencev1.InferencePoolReasonResolvedRefs),
|
||||
"Resolved ExtensionRef",
|
||||
)(parentStatusRef)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExtensionRef(kind, name string) Option {
|
||||
return func(obj client.Object) {
|
||||
ip, ok := obj.(*inferencev1.InferencePool)
|
||||
if ok {
|
||||
typedKind := inferencev1.Kind(kind)
|
||||
ip.Spec.EndpointPickerRef = inferencev1.EndpointPickerRef{
|
||||
Name: inferencev1.ObjectName(name),
|
||||
Kind: typedKind,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Object Creation Functions ---
|
||||
|
||||
func NewGateway(name string, opts ...Option) *gateway.Gateway {
|
||||
gw := &gateway.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: DefaultTestNS,
|
||||
},
|
||||
Spec: gateway.GatewaySpec{
|
||||
GatewayClassName: "higress",
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(gw)
|
||||
}
|
||||
return gw
|
||||
}
|
||||
|
||||
func NewHTTPRoute(name string, opts ...Option) *gateway.HTTPRoute {
|
||||
hr := &gateway.HTTPRoute{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: DefaultTestNS,
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(hr)
|
||||
}
|
||||
return hr
|
||||
}
|
||||
|
||||
func NewInferencePool(name string, opts ...Option) *inferencev1.InferencePool {
|
||||
ip := &inferencev1.InferencePool{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: DefaultTestNS,
|
||||
},
|
||||
Spec: inferencev1.InferencePoolSpec{
|
||||
Selector: inferencev1.LabelSelector{
|
||||
MatchLabels: map[inferencev1.LabelKey]inferencev1.LabelValue{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
EndpointPickerRef: inferencev1.EndpointPickerRef{
|
||||
Name: "endpoint-picker",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(ip)
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewService(name string, opts ...Option) *corev1.Service {
|
||||
svc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: DefaultTestNS,
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
TargetPort: intstr.FromInt(9002),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(svc)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
87
pkg/ingress/kube/gateway/istio/inferencepool_test.go
Normal file
87
pkg/ingress/kube/gateway/istio/inferencepool_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
||||
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/test"
|
||||
"istio.io/istio/pkg/test/util/assert"
|
||||
)
|
||||
|
||||
func TestReconcileInferencePool(t *testing.T) {
|
||||
test.SetForTest(t, &features.EnableGatewayAPIInferenceExtension, true)
|
||||
pool := &inferencev1.InferencePool{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pool",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: inferencev1.InferencePoolSpec{
|
||||
TargetPorts: []inferencev1.Port{
|
||||
{
|
||||
Number: inferencev1.PortNumber(8080),
|
||||
},
|
||||
},
|
||||
Selector: inferencev1.LabelSelector{
|
||||
MatchLabels: map[inferencev1.LabelKey]inferencev1.LabelValue{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
EndpointPickerRef: inferencev1.EndpointPickerRef{
|
||||
Name: "dummy",
|
||||
Port: &inferencev1.Port{
|
||||
Number: inferencev1.PortNumber(5421),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controller := setupController(t,
|
||||
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}},
|
||||
NewGateway("test-gw", InNamespace(DefaultTestNS), WithGatewayClass("istio")),
|
||||
NewHTTPRoute("test-route", InNamespace(DefaultTestNS),
|
||||
WithParentRefAndStatus("test-gw", DefaultTestNS, IstioController),
|
||||
WithBackendRef("test-pool", DefaultTestNS),
|
||||
),
|
||||
pool,
|
||||
)
|
||||
|
||||
dumpOnFailure(t, krt.GlobalDebugHandler)
|
||||
|
||||
// Verify the service was created
|
||||
var service *corev1.Service
|
||||
var err error
|
||||
assert.EventuallyEqual(t, func() bool {
|
||||
svcName := "test-pool-ip-" + generateHash("test-pool", hashSize)
|
||||
service, err = controller.client.Kube().CoreV1().Services("default").Get(t.Context(), svcName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Logf("Service %s not found yet: %v", svcName, err)
|
||||
return false
|
||||
}
|
||||
return service != nil
|
||||
}, true)
|
||||
|
||||
assert.Equal(t, service.ObjectMeta.Labels[constants.InternalServiceSemantics], constants.ServiceSemanticsInferencePool)
|
||||
assert.Equal(t, service.ObjectMeta.Labels[InferencePoolRefLabel], pool.Name)
|
||||
assert.Equal(t, service.OwnerReferences[0].Name, pool.Name)
|
||||
assert.Equal(t, service.Spec.Ports[0].TargetPort.IntVal, int32(8080))
|
||||
assert.Equal(t, service.Spec.Ports[0].Port, int32(54321)) // dummyPort + i
|
||||
}
|
||||
@@ -21,6 +21,5 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// CheckMain asserts that no goroutines are leaked after a test package exits.
|
||||
leak.CheckMain(m)
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// Copyright Istio 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.
|
||||
|
||||
// Updated based on Istio codebase by Higress
|
||||
|
||||
package istio
|
||||
|
||||
import (
|
||||
"istio.io/istio/pilot/pkg/credentials"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
creds "istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/config/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
// Start - Updated by Higress
|
||||
defaultClassName = constants.DefaultGatewayClass
|
||||
gatewayAliasForAnnotationKey = "gateway.higress.io/alias-for"
|
||||
gatewayTLSTerminateModeKey = "gateway.higress.io/tls-terminate-mode"
|
||||
gatewayNameOverride = "gateway.higress.io/name-override"
|
||||
gatewaySAOverride = "gateway.higress.io/service-account"
|
||||
serviceTypeOverride = "networking.higress.io/service-type"
|
||||
// End - Updated by Higress
|
||||
)
|
||||
|
||||
// GatewayResources stores all gateway resources used for our conversion.
|
||||
type GatewayResources struct {
|
||||
GatewayClass []config.Config
|
||||
Gateway []config.Config
|
||||
HTTPRoute []config.Config
|
||||
TCPRoute []config.Config
|
||||
TLSRoute []config.Config
|
||||
ReferenceGrant []config.Config
|
||||
// Namespaces stores all namespace in the cluster, keyed by name
|
||||
Namespaces map[string]*corev1.Namespace
|
||||
// Credentials stores all credentials in the cluster
|
||||
Credentials credentials.Controller
|
||||
|
||||
// Start - Added by Higress
|
||||
DefaultGatewaySelector map[string]string
|
||||
// End - Added by Higress
|
||||
|
||||
// Domain for the cluster. Typically, cluster.local
|
||||
Domain string
|
||||
Context GatewayContext
|
||||
}
|
||||
|
||||
type Grants struct {
|
||||
AllowAll bool
|
||||
AllowedNames sets.String
|
||||
}
|
||||
|
||||
type AllowedReferences map[Reference]map[Reference]*Grants
|
||||
|
||||
func (refs AllowedReferences) SecretAllowed(resourceName string, namespace string) bool {
|
||||
p, err := creds.ParseResourceName(resourceName, "", "", "")
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse resource name %q: %v", resourceName, err)
|
||||
return false
|
||||
}
|
||||
from := Reference{Kind: gvk.KubernetesGateway, Namespace: k8s.Namespace(namespace)}
|
||||
to := Reference{Kind: gvk.Secret, Namespace: k8s.Namespace(p.Namespace)}
|
||||
allow := refs[from][to]
|
||||
if allow == nil {
|
||||
return false
|
||||
}
|
||||
return allow.AllowAll || allow.AllowedNames.Contains(p.Name)
|
||||
}
|
||||
|
||||
func (refs AllowedReferences) BackendAllowed(
|
||||
k config.GroupVersionKind,
|
||||
backendName k8s.ObjectName,
|
||||
backendNamespace k8s.Namespace,
|
||||
routeNamespace string,
|
||||
) bool {
|
||||
from := Reference{Kind: k, Namespace: k8s.Namespace(routeNamespace)}
|
||||
to := Reference{Kind: gvk.Service, Namespace: backendNamespace}
|
||||
allow := refs[from][to]
|
||||
if allow == nil {
|
||||
return false
|
||||
}
|
||||
return allow.AllowAll || allow.AllowedNames.Contains(string(backendName))
|
||||
}
|
||||
|
||||
// IstioResources stores all outputs of our conversion
|
||||
type IstioResources struct {
|
||||
Gateway []config.Config
|
||||
VirtualService []config.Config
|
||||
// AllowedReferences stores all allowed references, from Reference -> to Reference(s)
|
||||
AllowedReferences AllowedReferences
|
||||
// ReferencedNamespaceKeys stores the label key of all namespace selections. This allows us to quickly
|
||||
// determine if a namespace update could have impacted any Gateways. See namespaceEvent.
|
||||
ReferencedNamespaceKeys sets.String
|
||||
|
||||
// ResourceReferences stores all resources referenced by gateway-api resources. This allows us to quickly
|
||||
// determine if a resource update could have impacted any Gateways.
|
||||
// key: referenced resources(e.g. secrets), value: gateway-api resources(e.g. gateways)
|
||||
ResourceReferences map[model.ConfigKey][]model.ConfigKey
|
||||
}
|
||||
|
||||
// Reference stores a reference to a namespaced GVK, as used by ReferencePolicy
|
||||
type Reference struct {
|
||||
Kind config.GroupVersionKind
|
||||
Namespace k8s.Namespace
|
||||
}
|
||||
78
pkg/ingress/kube/gateway/istio/references.go
Normal file
78
pkg/ingress/kube/gateway/istio/references.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
|
||||
|
||||
"istio.io/istio/pkg/config"
|
||||
schematypes "istio.io/istio/pkg/config/schema/kubetypes"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
)
|
||||
|
||||
// ReferenceSet stores a variety of different types of resource, and allows looking them up as Gateway API references.
|
||||
// This is merely a convenience to avoid needing to lookup up a bunch of types all over the place.
|
||||
type ReferenceSet struct {
|
||||
erasedCollections map[config.GroupVersionKind]func(name, namespace string) (any, bool)
|
||||
}
|
||||
|
||||
func (s ReferenceSet) LocalPolicyTargetRef(ref gatewayv1.LocalPolicyTargetReference, localNamespace string) (any, error) {
|
||||
return s.internal(string(ref.Name), string(ref.Group), string(ref.Kind), localNamespace)
|
||||
}
|
||||
|
||||
func (s ReferenceSet) XLocalPolicyTargetRef(ref gatewayx.LocalPolicyTargetReference, localNamespace string) (any, error) {
|
||||
return s.internal(string(ref.Name), string(ref.Group), string(ref.Kind), localNamespace)
|
||||
}
|
||||
|
||||
func (s ReferenceSet) LocalPolicyRef(ref gatewayv1.LocalObjectReference, localNamespace string) (any, error) {
|
||||
return s.internal(string(ref.Name), string(ref.Group), string(ref.Kind), localNamespace)
|
||||
}
|
||||
|
||||
func (s ReferenceSet) internal(name, group, kind, localNamespace string) (any, error) {
|
||||
t := normalizeReference(&group, &kind, config.GroupVersionKind{})
|
||||
lookup, f := s.erasedCollections[t]
|
||||
if !f {
|
||||
return nil, fmt.Errorf("unsupported kind %v", kind)
|
||||
}
|
||||
if v, ok := lookup(name, localNamespace); ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reference %v/%v (of kind %v) not found", localNamespace, name, kind)
|
||||
}
|
||||
|
||||
func NewReferenceSet(opts ...func(r *ReferenceSet)) *ReferenceSet {
|
||||
r := &ReferenceSet{erasedCollections: make(map[config.GroupVersionKind]func(name, namespace string) (any, bool))}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func AddReference[T runtime.Object](c krt.Collection[T]) func(r *ReferenceSet) {
|
||||
return func(r *ReferenceSet) {
|
||||
g := schematypes.MustGVKFromType[T]()
|
||||
r.erasedCollections[g] = func(name, namespace string) (any, bool) {
|
||||
o := c.GetKey(namespace + "/" + name)
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return *o, true
|
||||
}
|
||||
}
|
||||
}
|
||||
165
pkg/ingress/kube/gateway/istio/references_collection.go
Normal file
165
pkg/ingress/kube/gateway/istio/references_collection.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
creds "istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/collections"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
)
|
||||
|
||||
// Reference stores a reference to a namespaced GVK, as used by ReferenceGrant
|
||||
type Reference struct {
|
||||
Kind config.GroupVersionKind
|
||||
Namespace gateway.Namespace
|
||||
}
|
||||
|
||||
func (refs Reference) String() string {
|
||||
return refs.Kind.String() + "/" + string(refs.Namespace)
|
||||
}
|
||||
|
||||
type ReferencePair struct {
|
||||
To, From Reference
|
||||
}
|
||||
|
||||
func (r ReferencePair) String() string {
|
||||
return fmt.Sprintf("%s->%s", r.From, r.To)
|
||||
}
|
||||
|
||||
type ReferenceGrants struct {
|
||||
collection krt.Collection[ReferenceGrant]
|
||||
index krt.Index[ReferencePair, ReferenceGrant]
|
||||
}
|
||||
|
||||
func ReferenceGrantsCollection(referenceGrants krt.Collection[*gateway.ReferenceGrant], opts krt.OptionsBuilder) krt.Collection[ReferenceGrant] {
|
||||
return krt.NewManyCollection(referenceGrants, func(ctx krt.HandlerContext, obj *gateway.ReferenceGrant) []ReferenceGrant {
|
||||
rp := obj.Spec
|
||||
results := make([]ReferenceGrant, 0, len(rp.From)*len(rp.To))
|
||||
for _, from := range rp.From {
|
||||
fromKey := Reference{
|
||||
Namespace: from.Namespace,
|
||||
}
|
||||
ref := normalizeReference(&from.Group, &from.Kind, config.GroupVersionKind{})
|
||||
switch ref {
|
||||
case gvk.KubernetesGateway, gvk.HTTPRoute, gvk.GRPCRoute, gvk.TLSRoute, gvk.TCPRoute, gvk.XListenerSet:
|
||||
fromKey.Kind = ref
|
||||
default:
|
||||
// Not supported type. Not an error; may be for another controller
|
||||
continue
|
||||
}
|
||||
for _, to := range rp.To {
|
||||
toKey := Reference{
|
||||
Namespace: gateway.Namespace(obj.Namespace),
|
||||
}
|
||||
|
||||
ref := normalizeReference(&to.Group, &to.Kind, config.GroupVersionKind{})
|
||||
switch ref {
|
||||
case gvk.ConfigMap, gvk.Secret, gvk.Service, gvk.InferencePool:
|
||||
toKey.Kind = ref
|
||||
default:
|
||||
continue
|
||||
}
|
||||
rg := ReferenceGrant{
|
||||
Source: config.NamespacedName(obj),
|
||||
From: fromKey,
|
||||
To: toKey,
|
||||
AllowAll: false,
|
||||
AllowedName: "",
|
||||
}
|
||||
if to.Name != nil {
|
||||
rg.AllowedName = string(*to.Name)
|
||||
} else {
|
||||
rg.AllowAll = true
|
||||
}
|
||||
results = append(results, rg)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}, opts.WithName("ReferenceGrants")...)
|
||||
}
|
||||
|
||||
func BuildReferenceGrants(collection krt.Collection[ReferenceGrant]) ReferenceGrants {
|
||||
idx := krt.NewIndex(collection, "toFrom", func(o ReferenceGrant) []ReferencePair {
|
||||
return []ReferencePair{{
|
||||
To: o.To,
|
||||
From: o.From,
|
||||
}}
|
||||
})
|
||||
return ReferenceGrants{
|
||||
collection: collection,
|
||||
index: idx,
|
||||
}
|
||||
}
|
||||
|
||||
type ReferenceGrant struct {
|
||||
Source types.NamespacedName
|
||||
From Reference
|
||||
To Reference
|
||||
AllowAll bool
|
||||
AllowedName string
|
||||
}
|
||||
|
||||
func (g ReferenceGrant) ResourceName() string {
|
||||
return g.Source.String() + "/" + g.From.String() + "/" + g.To.String()
|
||||
}
|
||||
|
||||
func (refs ReferenceGrants) SecretAllowed(ctx krt.HandlerContext, kind config.GroupVersionKind, resourceName string, namespace string) bool {
|
||||
p, err := creds.ParseResourceName(resourceName, "", "", "")
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse resource name %q: %v", resourceName, err)
|
||||
return false
|
||||
}
|
||||
resourceKind := config.GroupVersionKind{Kind: p.ResourceKind.String()}
|
||||
resourceSchema, resourceSchemaFound := collections.All.FindByGroupKind(resourceKind)
|
||||
if resourceSchemaFound {
|
||||
resourceKind = resourceSchema.GroupVersionKind()
|
||||
}
|
||||
from := Reference{Kind: kind, Namespace: gateway.Namespace(namespace)}
|
||||
to := Reference{Kind: resourceKind, Namespace: gateway.Namespace(p.Namespace)}
|
||||
pair := ReferencePair{From: from, To: to}
|
||||
grants := krt.FetchOrList(ctx, refs.collection, krt.FilterIndex(refs.index, pair))
|
||||
for _, g := range grants {
|
||||
if g.AllowAll || g.AllowedName == p.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (refs ReferenceGrants) BackendAllowed(ctx krt.HandlerContext,
|
||||
k config.GroupVersionKind,
|
||||
toGVK config.GroupVersionKind,
|
||||
backendName gateway.ObjectName,
|
||||
backendNamespace gateway.Namespace,
|
||||
routeNamespace string,
|
||||
) bool {
|
||||
from := Reference{Kind: k, Namespace: gateway.Namespace(routeNamespace)}
|
||||
to := Reference{Kind: toGVK, Namespace: backendNamespace}
|
||||
pair := ReferencePair{From: from, To: to}
|
||||
grants := krt.Fetch(ctx, refs.collection, krt.FilterIndex(refs.index, pair))
|
||||
for _, g := range grants {
|
||||
if g.AllowAll || g.AllowedName == string(backendName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
781
pkg/ingress/kube/gateway/istio/route_collections.go
Normal file
781
pkg/ingress/kube/gateway/istio/route_collections.go
Normal file
@@ -0,0 +1,781 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayalpha "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
|
||||
|
||||
istio "istio.io/api/networking/v1alpha3"
|
||||
networkingclient "istio.io/client-go/pkg/apis/networking/v1"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/gateway/kube"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/ptr"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
)
|
||||
|
||||
type AncestorBackend struct {
|
||||
Gateway types.NamespacedName
|
||||
Backend TypedNamespacedName
|
||||
}
|
||||
|
||||
func (a AncestorBackend) Equals(other AncestorBackend) bool {
|
||||
return a.Gateway == other.Gateway && a.Backend == other.Backend
|
||||
}
|
||||
|
||||
func (a AncestorBackend) ResourceName() string {
|
||||
return a.Gateway.String() + "/" + a.Backend.String()
|
||||
}
|
||||
|
||||
func HTTPRouteCollection(
|
||||
httpRoutes krt.Collection[*gateway.HTTPRoute],
|
||||
inputs RouteContextInputs,
|
||||
opts krt.OptionsBuilder,
|
||||
) RouteResult[*gateway.HTTPRoute, gateway.HTTPRouteStatus] {
|
||||
routeCount := gatewayRouteAttachmentCountCollection(inputs, httpRoutes, gvk.HTTPRoute, opts)
|
||||
ancestorBackends := krt.NewManyCollection(httpRoutes, func(krtctx krt.HandlerContext, obj *gateway.HTTPRoute) []AncestorBackend {
|
||||
return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gateway.HTTPRouteRule) []gateway.HTTPBackendRef {
|
||||
return r.BackendRefs
|
||||
})
|
||||
}, opts.WithName("HTTPAncestors")...)
|
||||
status, baseVirtualServices := krt.NewStatusManyCollection(httpRoutes, func(krtctx krt.HandlerContext, obj *gateway.HTTPRoute) (
|
||||
*gateway.HTTPRouteStatus,
|
||||
[]RouteWithKey,
|
||||
) {
|
||||
ctx := inputs.WithCtx(krtctx)
|
||||
inferencePoolCfgPairs := []struct {
|
||||
name string
|
||||
cfg *inferencePoolConfig
|
||||
}{}
|
||||
status := obj.Status.DeepCopy()
|
||||
route := obj.Spec
|
||||
parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gateway.HTTPRoute) iter.Seq2[*istio.HTTPRoute, *ConfigError] {
|
||||
return func(yield func(*istio.HTTPRoute, *ConfigError) bool) {
|
||||
for n, r := range route.Rules {
|
||||
// split the rule to make sure each rule has up to one match
|
||||
matches := slices.Reference(r.Matches)
|
||||
if len(matches) == 0 {
|
||||
matches = append(matches, nil)
|
||||
}
|
||||
for _, m := range matches {
|
||||
if m != nil {
|
||||
r.Matches = []gateway.HTTPRouteMatch{*m}
|
||||
}
|
||||
istioRoute, ipCfg, configErr := convertHTTPRoute(ctx, r, obj, n, !mesh)
|
||||
if istioRoute != nil && ipCfg != nil && ipCfg.enableExtProc {
|
||||
inferencePoolCfgPairs = append(inferencePoolCfgPairs, struct {
|
||||
name string
|
||||
cfg *inferencePoolConfig
|
||||
}{name: istioRoute.Name, cfg: ipCfg})
|
||||
}
|
||||
if !yield(istioRoute, configErr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// routeRuleToInferencePoolCfg stores inference pool configs discovered during route rule conversion,
|
||||
// keyed by the istio.HTTPRoute.Name.
|
||||
routeRuleToInferencePoolCfg := make(map[string]*inferencePoolConfig)
|
||||
for _, pair := range inferencePoolCfgPairs {
|
||||
routeRuleToInferencePoolCfg[pair.name] = pair.cfg
|
||||
}
|
||||
status.Parents = parentStatus
|
||||
|
||||
count := 0
|
||||
virtualServices := []RouteWithKey{}
|
||||
for _, parent := range filteredReferences(parentRefs) {
|
||||
// for gateway routes, build one VS per gateway+host
|
||||
routeKey := parent.InternalName
|
||||
vsHosts := hostnameToStringList(route.Hostnames)
|
||||
routes := gwResult.routes
|
||||
if parent.IsMesh() {
|
||||
routes = meshResult.routes
|
||||
// for mesh routes, build one VS per namespace/port->host
|
||||
routeKey = obj.Namespace
|
||||
if parent.OriginalReference.Port != nil {
|
||||
routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
|
||||
routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
|
||||
}
|
||||
ref := types.NamespacedName{
|
||||
Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))),
|
||||
Name: string(parent.OriginalReference.Name),
|
||||
}
|
||||
if parent.InternalKind == gvk.ServiceEntry {
|
||||
ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String())))
|
||||
if ses != nil {
|
||||
vsHosts = ses.Spec.Hosts
|
||||
} else {
|
||||
// TODO: report an error
|
||||
vsHosts = []string{}
|
||||
}
|
||||
} else {
|
||||
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
|
||||
parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace)), ctx.DomainSuffix)}
|
||||
}
|
||||
}
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
// Create one VS per hostname with a single hostname.
|
||||
// This ensures we can treat each hostname independently, as the spec requires
|
||||
for _, h := range vsHosts {
|
||||
if !parent.hostnameAllowedByIsolation(h) {
|
||||
// TODO: standardize a status message for this upstream and report
|
||||
continue
|
||||
}
|
||||
name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
|
||||
sortHTTPRoutes(routes)
|
||||
|
||||
// Populate Extra field for inference pool configs
|
||||
extraData := make(map[string]any)
|
||||
currentRouteInferenceConfigs := make(map[string]kube.InferencePoolRouteRuleConfig)
|
||||
for _, httpRule := range routes { // These are []*istio.HTTPRoute
|
||||
if ipCfg, found := routeRuleToInferencePoolCfg[httpRule.Name]; found {
|
||||
currentRouteInferenceConfigs[httpRule.Name] = kube.InferencePoolRouteRuleConfig{
|
||||
FQDN: ipCfg.endpointPickerDst,
|
||||
Port: ipCfg.endpointPickerPort,
|
||||
FailureModeAllow: ipCfg.endpointPickerFailureMode == string(inferencev1.EndpointPickerFailOpen),
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(currentRouteInferenceConfigs) > 0 {
|
||||
extraData[constants.ConfigExtraPerRouteRuleInferencePoolConfigs] = currentRouteInferenceConfigs
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: name,
|
||||
Annotations: routeMeta(obj),
|
||||
Namespace: obj.Namespace,
|
||||
Domain: ctx.DomainSuffix,
|
||||
},
|
||||
Spec: &istio.VirtualService{
|
||||
Hosts: []string{h},
|
||||
Gateways: []string{parent.InternalName},
|
||||
Http: routes,
|
||||
},
|
||||
Extra: extraData,
|
||||
}
|
||||
virtualServices = append(virtualServices, RouteWithKey{
|
||||
Config: cfg,
|
||||
Key: routeKey + "/" + h,
|
||||
})
|
||||
count++
|
||||
}
|
||||
}
|
||||
return status, virtualServices
|
||||
}, opts.WithName("HTTPRoute")...)
|
||||
|
||||
finalVirtualServices := mergeHTTPRoutes(baseVirtualServices, opts.WithName("HTTPRouteMerged")...)
|
||||
return RouteResult[*gateway.HTTPRoute, gateway.HTTPRouteStatus]{
|
||||
VirtualServices: finalVirtualServices,
|
||||
RouteAttachments: routeCount,
|
||||
Status: status,
|
||||
Ancestors: ancestorBackends,
|
||||
}
|
||||
}
|
||||
|
||||
func extractAncestorBackends[RT, BT any](ns string, prefs []gateway.ParentReference, rules []RT, extract func(RT) []BT) []AncestorBackend {
|
||||
gateways := sets.Set[types.NamespacedName]{}
|
||||
for _, r := range prefs {
|
||||
ref := normalizeReference(r.Group, r.Kind, gvk.KubernetesGateway)
|
||||
if ref != gvk.KubernetesGateway {
|
||||
continue
|
||||
}
|
||||
gateways.Insert(types.NamespacedName{
|
||||
Namespace: defaultString(r.Namespace, ns),
|
||||
Name: string(r.Name),
|
||||
})
|
||||
}
|
||||
backends := sets.Set[TypedNamespacedName]{}
|
||||
for _, r := range rules {
|
||||
for _, b := range extract(r) {
|
||||
ref, refNs, refName := GetBackendRef(b)
|
||||
k, ok := gvk.ToKind(ref)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
be := TypedNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: defaultString(refNs, ns),
|
||||
Name: string(refName),
|
||||
},
|
||||
Kind: k,
|
||||
}
|
||||
backends.Insert(be)
|
||||
}
|
||||
}
|
||||
gtw := slices.SortBy(gateways.UnsortedList(), types.NamespacedName.String)
|
||||
bes := slices.SortBy(backends.UnsortedList(), TypedNamespacedName.String)
|
||||
res := make([]AncestorBackend, 0, len(gtw)*len(bes))
|
||||
for _, gw := range gtw {
|
||||
for _, be := range bes {
|
||||
res = append(res, AncestorBackend{
|
||||
Gateway: gw,
|
||||
Backend: be,
|
||||
})
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type conversionResult[O any] struct {
|
||||
error *ConfigError
|
||||
routes []O
|
||||
}
|
||||
|
||||
func GRPCRouteCollection(
|
||||
grpcRoutes krt.Collection[*gatewayv1.GRPCRoute],
|
||||
inputs RouteContextInputs,
|
||||
opts krt.OptionsBuilder,
|
||||
) RouteResult[*gatewayv1.GRPCRoute, gatewayv1.GRPCRouteStatus] {
|
||||
routeCount := gatewayRouteAttachmentCountCollection(inputs, grpcRoutes, gvk.GRPCRoute, opts)
|
||||
ancestorBackends := krt.NewManyCollection(grpcRoutes, func(krtctx krt.HandlerContext, obj *gatewayv1.GRPCRoute) []AncestorBackend {
|
||||
return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayv1.GRPCRouteRule) []gatewayv1.GRPCBackendRef {
|
||||
return r.BackendRefs
|
||||
})
|
||||
}, opts.WithName("GRPCAncestors")...)
|
||||
status, baseVirtualServices := krt.NewStatusManyCollection(grpcRoutes, func(krtctx krt.HandlerContext, obj *gatewayv1.GRPCRoute) (
|
||||
*gatewayv1.GRPCRouteStatus,
|
||||
[]RouteWithKey,
|
||||
) {
|
||||
ctx := inputs.WithCtx(krtctx)
|
||||
// routeRuleToInferencePoolCfg stores inference pool configs discovered during route rule conversion.
|
||||
// Note: GRPCRoute currently doesn't have inference pool logic, but adding for consistency.
|
||||
routeRuleToInferencePoolCfg := make(map[string]*inferencePoolConfig)
|
||||
status := obj.Status.DeepCopy()
|
||||
route := obj.Spec
|
||||
parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gatewayv1.GRPCRoute) iter.Seq2[*istio.HTTPRoute, *ConfigError] {
|
||||
return func(yield func(*istio.HTTPRoute, *ConfigError) bool) {
|
||||
for n, r := range route.Rules {
|
||||
// split the rule to make sure each rule has up to one match
|
||||
matches := slices.Reference(r.Matches)
|
||||
if len(matches) == 0 {
|
||||
matches = append(matches, nil)
|
||||
}
|
||||
for _, m := range matches {
|
||||
if m != nil {
|
||||
r.Matches = []gatewayv1.GRPCRouteMatch{*m}
|
||||
}
|
||||
// GRPCRoute conversion currently doesn't return ipCfg.
|
||||
istioRoute, configErr := convertGRPCRoute(ctx, r, obj, n, !mesh)
|
||||
// Placeholder if GRPCRoute ever supports inference pools via ipCfg:
|
||||
// if istioRoute != nil && ipCfg != nil && ipCfg.enableExtProc {
|
||||
// routeRuleToInferencePoolCfg[istioRoute.Name] = ipCfg
|
||||
// }
|
||||
if !yield(istioRoute, configErr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
status.Parents = parentStatus
|
||||
|
||||
count := 0
|
||||
virtualServices := []RouteWithKey{}
|
||||
for _, parent := range filteredReferences(parentRefs) {
|
||||
// for gateway routes, build one VS per gateway+host
|
||||
routeKey := parent.InternalName
|
||||
vsHosts := hostnameToStringList(route.Hostnames)
|
||||
routes := gwResult.routes
|
||||
if parent.IsMesh() {
|
||||
routes = meshResult.routes
|
||||
// for mesh routes, build one VS per namespace/port->host
|
||||
routeKey = obj.Namespace
|
||||
if parent.OriginalReference.Port != nil {
|
||||
routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
|
||||
routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
|
||||
}
|
||||
ref := types.NamespacedName{
|
||||
Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))),
|
||||
Name: string(parent.OriginalReference.Name),
|
||||
}
|
||||
if parent.InternalKind == gvk.ServiceEntry {
|
||||
ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String())))
|
||||
if ses != nil {
|
||||
vsHosts = ses.Spec.Hosts
|
||||
} else {
|
||||
// TODO: report an error
|
||||
vsHosts = []string{}
|
||||
}
|
||||
} else {
|
||||
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
|
||||
parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace)), ctx.DomainSuffix)}
|
||||
}
|
||||
}
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
// Create one VS per hostname with a single hostname.
|
||||
// This ensures we can treat each hostname independently, as the spec requires
|
||||
for _, h := range vsHosts {
|
||||
if !parent.hostnameAllowedByIsolation(h) {
|
||||
// TODO: standardize a status message for this upstream and report
|
||||
continue
|
||||
}
|
||||
name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
|
||||
sortHTTPRoutes(routes)
|
||||
|
||||
// Populate Extra field for inference pool configs (if GRPCRoute supports them)
|
||||
extraData := make(map[string]any)
|
||||
currentRouteInferenceConfigs := make(map[string]kube.InferencePoolRouteRuleConfig)
|
||||
for _, httpRule := range routes {
|
||||
if ipCfg, found := routeRuleToInferencePoolCfg[httpRule.Name]; found { // This map will be empty for GRPCRoute for now
|
||||
currentRouteInferenceConfigs[httpRule.Name] = kube.InferencePoolRouteRuleConfig{
|
||||
FQDN: ipCfg.endpointPickerDst,
|
||||
Port: ipCfg.endpointPickerPort,
|
||||
FailureModeAllow: ipCfg.endpointPickerFailureMode == string(inferencev1.EndpointPickerFailOpen),
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(currentRouteInferenceConfigs) > 0 {
|
||||
extraData[constants.ConfigExtraPerRouteRuleInferencePoolConfigs] = currentRouteInferenceConfigs
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: name,
|
||||
Annotations: routeMeta(obj),
|
||||
Namespace: obj.Namespace,
|
||||
Domain: ctx.DomainSuffix,
|
||||
},
|
||||
Spec: &istio.VirtualService{
|
||||
Hosts: []string{h},
|
||||
Gateways: []string{parent.InternalName},
|
||||
Http: routes,
|
||||
},
|
||||
Extra: extraData,
|
||||
}
|
||||
virtualServices = append(virtualServices, RouteWithKey{
|
||||
Config: cfg,
|
||||
Key: routeKey + "/" + h,
|
||||
})
|
||||
count++
|
||||
}
|
||||
}
|
||||
return status, virtualServices
|
||||
}, opts.WithName("GRPCRoute")...)
|
||||
|
||||
finalVirtualServices := mergeHTTPRoutes(baseVirtualServices, opts.WithName("GRPCRouteMerged")...)
|
||||
return RouteResult[*gatewayv1.GRPCRoute, gatewayv1.GRPCRouteStatus]{
|
||||
VirtualServices: finalVirtualServices,
|
||||
RouteAttachments: routeCount,
|
||||
Status: status,
|
||||
Ancestors: ancestorBackends,
|
||||
}
|
||||
}
|
||||
|
||||
func TCPRouteCollection(
|
||||
tcpRoutes krt.Collection[*gatewayalpha.TCPRoute],
|
||||
inputs RouteContextInputs,
|
||||
opts krt.OptionsBuilder,
|
||||
) RouteResult[*gatewayalpha.TCPRoute, gatewayalpha.TCPRouteStatus] {
|
||||
routeCount := gatewayRouteAttachmentCountCollection(inputs, tcpRoutes, gvk.TCPRoute, opts)
|
||||
ancestorBackends := krt.NewManyCollection(tcpRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TCPRoute) []AncestorBackend {
|
||||
return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayalpha.TCPRouteRule) []gateway.BackendRef {
|
||||
return r.BackendRefs
|
||||
})
|
||||
}, opts.WithName("TCPAncestors")...)
|
||||
status, virtualServices := krt.NewStatusManyCollection(tcpRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TCPRoute) (
|
||||
*gatewayalpha.TCPRouteStatus,
|
||||
[]*config.Config,
|
||||
) {
|
||||
ctx := inputs.WithCtx(krtctx)
|
||||
status := obj.Status.DeepCopy()
|
||||
route := obj.Spec
|
||||
parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj,
|
||||
func(mesh bool, obj *gatewayalpha.TCPRoute) iter.Seq2[*istio.TCPRoute, *ConfigError] {
|
||||
return func(yield func(*istio.TCPRoute, *ConfigError) bool) {
|
||||
for _, r := range route.Rules {
|
||||
if !yield(convertTCPRoute(ctx, r, obj, !mesh)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
status.Parents = parentStatus
|
||||
|
||||
vs := []*config.Config{}
|
||||
count := 0
|
||||
for _, parent := range filteredReferences(parentRefs) {
|
||||
routes := gwResult.routes
|
||||
vsHosts := []string{"*"}
|
||||
if parent.IsMesh() {
|
||||
routes = meshResult.routes
|
||||
if parent.OriginalReference.Port != nil {
|
||||
routes = augmentTCPPortMatch(routes, *parent.OriginalReference.Port)
|
||||
}
|
||||
ref := types.NamespacedName{
|
||||
Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))),
|
||||
Name: string(parent.OriginalReference.Name),
|
||||
}
|
||||
if parent.InternalKind == gvk.ServiceEntry {
|
||||
ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String())))
|
||||
if ses != nil {
|
||||
vsHosts = ses.Spec.Hosts
|
||||
} else {
|
||||
// TODO: report an error
|
||||
vsHosts = []string{}
|
||||
}
|
||||
} else {
|
||||
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", ref.Name, ref.Namespace, ctx.DomainSuffix)}
|
||||
}
|
||||
}
|
||||
for _, host := range vsHosts {
|
||||
name := fmt.Sprintf("%s-tcp-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
|
||||
// Create one VS per hostname with a single hostname.
|
||||
// This ensures we can treat each hostname independently, as the spec requires
|
||||
vs = append(vs, &config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: name,
|
||||
Annotations: routeMeta(obj),
|
||||
Namespace: obj.Namespace,
|
||||
Domain: ctx.DomainSuffix,
|
||||
},
|
||||
Spec: &istio.VirtualService{
|
||||
// We can use wildcard here since each listener can have at most one route bound to it, so we have
|
||||
// a single VS per Gateway.
|
||||
Hosts: []string{host},
|
||||
Gateways: []string{parent.InternalName},
|
||||
Tcp: routes,
|
||||
},
|
||||
})
|
||||
count++
|
||||
}
|
||||
}
|
||||
return status, vs
|
||||
}, opts.WithName("TCPRoute")...)
|
||||
|
||||
return RouteResult[*gatewayalpha.TCPRoute, gatewayalpha.TCPRouteStatus]{
|
||||
VirtualServices: virtualServices,
|
||||
RouteAttachments: routeCount,
|
||||
Status: status,
|
||||
Ancestors: ancestorBackends,
|
||||
}
|
||||
}
|
||||
|
||||
func TLSRouteCollection(
|
||||
tlsRoutes krt.Collection[*gatewayalpha.TLSRoute],
|
||||
inputs RouteContextInputs,
|
||||
opts krt.OptionsBuilder,
|
||||
) RouteResult[*gatewayalpha.TLSRoute, gatewayalpha.TLSRouteStatus] {
|
||||
routeCount := gatewayRouteAttachmentCountCollection(inputs, tlsRoutes, gvk.TLSRoute, opts)
|
||||
ancestorBackends := krt.NewManyCollection(tlsRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TLSRoute) []AncestorBackend {
|
||||
return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayalpha.TLSRouteRule) []gateway.BackendRef {
|
||||
return r.BackendRefs
|
||||
})
|
||||
}, opts.WithName("TLSAncestors")...)
|
||||
status, virtualServices := krt.NewStatusManyCollection(tlsRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TLSRoute) (
|
||||
*gatewayalpha.TLSRouteStatus,
|
||||
[]*config.Config,
|
||||
) {
|
||||
ctx := inputs.WithCtx(krtctx)
|
||||
status := obj.Status.DeepCopy()
|
||||
route := obj.Spec
|
||||
parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx,
|
||||
obj, func(mesh bool, obj *gatewayalpha.TLSRoute) iter.Seq2[*istio.TLSRoute, *ConfigError] {
|
||||
return func(yield func(*istio.TLSRoute, *ConfigError) bool) {
|
||||
for _, r := range route.Rules {
|
||||
if !yield(convertTLSRoute(ctx, r, obj, !mesh)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
status.Parents = parentStatus
|
||||
|
||||
count := 0
|
||||
vs := []*config.Config{}
|
||||
for _, parent := range filteredReferences(parentRefs) {
|
||||
routes := gwResult.routes
|
||||
vsHosts := hostnameToStringList(route.Hostnames)
|
||||
if parent.IsMesh() {
|
||||
routes = meshResult.routes
|
||||
ref := types.NamespacedName{
|
||||
Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))),
|
||||
Name: string(parent.OriginalReference.Name),
|
||||
}
|
||||
if parent.InternalKind == gvk.ServiceEntry {
|
||||
ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String())))
|
||||
if ses != nil {
|
||||
vsHosts = ses.Spec.Hosts
|
||||
} else {
|
||||
// TODO: report an error
|
||||
vsHosts = []string{}
|
||||
}
|
||||
} else {
|
||||
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", ref.Name, ref.Namespace, ctx.DomainSuffix)}
|
||||
}
|
||||
routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts)
|
||||
}
|
||||
for _, host := range vsHosts {
|
||||
name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
|
||||
filteredRoutes := routes
|
||||
if parent.IsMesh() {
|
||||
filteredRoutes = compatibleRoutesForHost(routes, host)
|
||||
}
|
||||
// Create one VS per hostname with a single hostname.
|
||||
// This ensures we can treat each hostname independently, as the spec requires
|
||||
vs = append(vs, &config.Config{
|
||||
Meta: config.Meta{
|
||||
CreationTimestamp: obj.CreationTimestamp.Time,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: name,
|
||||
Annotations: routeMeta(obj),
|
||||
Namespace: obj.Namespace,
|
||||
Domain: ctx.DomainSuffix,
|
||||
},
|
||||
Spec: &istio.VirtualService{
|
||||
Hosts: []string{host},
|
||||
Gateways: []string{parent.InternalName},
|
||||
Tls: filteredRoutes,
|
||||
},
|
||||
})
|
||||
count++
|
||||
}
|
||||
}
|
||||
return status, vs
|
||||
}, opts.WithName("TLSRoute")...)
|
||||
return RouteResult[*gatewayalpha.TLSRoute, gatewayalpha.TLSRouteStatus]{
|
||||
VirtualServices: virtualServices,
|
||||
RouteAttachments: routeCount,
|
||||
Status: status,
|
||||
Ancestors: ancestorBackends,
|
||||
}
|
||||
}
|
||||
|
||||
// computeRoute holds the common route building logic shared amongst all types
|
||||
func computeRoute[T controllers.Object, O comparable](ctx RouteContext, obj T, translator func(
|
||||
mesh bool,
|
||||
obj T,
|
||||
) iter.Seq2[O, *ConfigError],
|
||||
) ([]gateway.RouteParentStatus, []routeParentReference, conversionResult[O], conversionResult[O]) {
|
||||
parentRefs := extractParentReferenceInfo(ctx, ctx.RouteParents, obj)
|
||||
|
||||
convertRules := func(mesh bool) conversionResult[O] {
|
||||
res := conversionResult[O]{}
|
||||
for vs, err := range translator(mesh, obj) {
|
||||
// This was a hard error
|
||||
if controllers.IsNil(vs) {
|
||||
res.error = err
|
||||
return conversionResult[O]{error: err}
|
||||
}
|
||||
// Got an error but also routes
|
||||
if err != nil {
|
||||
res.error = err
|
||||
}
|
||||
res.routes = append(res.routes, vs)
|
||||
}
|
||||
return res
|
||||
}
|
||||
meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
|
||||
|
||||
rpResults := slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
|
||||
res := RouteParentResult{
|
||||
OriginalReference: r.OriginalReference,
|
||||
DeniedReason: r.DeniedReason,
|
||||
RouteError: gwResult.error,
|
||||
}
|
||||
if r.IsMesh() {
|
||||
res.RouteError = meshResult.error
|
||||
res.WaypointError = r.WaypointError
|
||||
}
|
||||
return res
|
||||
})
|
||||
parents := createRouteStatus(rpResults, obj.GetNamespace(), obj.GetGeneration(), GetCommonRouteStateParents(obj))
|
||||
return parents, parentRefs, meshResult, gwResult
|
||||
}
|
||||
|
||||
// RouteContext defines a common set of inputs to a route collection. This should be built once per route translation and
|
||||
// not shared outside of that.
|
||||
// The embedded RouteContextInputs is typically based into a collection, then translated to a RouteContext with RouteContextInputs.WithCtx().
|
||||
type RouteContext struct {
|
||||
Krt krt.HandlerContext
|
||||
RouteContextInputs
|
||||
}
|
||||
|
||||
func (r RouteContext) LookupHostname(hostname string, namespace string, kind string) *model.Service {
|
||||
if c := r.internalContext.Get(r.Krt).Load(); c != nil {
|
||||
return c.GetService(hostname, namespace, kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RouteContextInputs struct {
|
||||
Grants ReferenceGrants
|
||||
RouteParents RouteParents
|
||||
DomainSuffix string
|
||||
Services krt.Collection[*corev1.Service]
|
||||
Namespaces krt.Collection[*corev1.Namespace]
|
||||
ServiceEntries krt.Collection[*networkingclient.ServiceEntry]
|
||||
InferencePools krt.Collection[*inferencev1.InferencePool]
|
||||
internalContext krt.RecomputeProtected[*atomic.Pointer[GatewayContext]]
|
||||
}
|
||||
|
||||
func (i RouteContextInputs) WithCtx(krtctx krt.HandlerContext) RouteContext {
|
||||
return RouteContext{
|
||||
Krt: krtctx,
|
||||
RouteContextInputs: i,
|
||||
}
|
||||
}
|
||||
|
||||
type RouteWithKey struct {
|
||||
*config.Config
|
||||
Key string
|
||||
}
|
||||
|
||||
func (r RouteWithKey) ResourceName() string {
|
||||
return config.NamespacedName(r.Config).String()
|
||||
}
|
||||
|
||||
func (r RouteWithKey) Equals(o RouteWithKey) bool {
|
||||
return r.Config.Equals(o.Config)
|
||||
}
|
||||
|
||||
// buildMeshAndGatewayRoutes contains common logic to build a set of routes with mesh and/or gateway semantics
|
||||
func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) {
|
||||
var meshResult, gwResult T
|
||||
needMesh, needGw := parentTypes(parentRefs)
|
||||
if needMesh {
|
||||
meshResult = convertRules(true)
|
||||
}
|
||||
if needGw {
|
||||
gwResult = convertRules(false)
|
||||
}
|
||||
return meshResult, gwResult
|
||||
}
|
||||
|
||||
// RouteResult holds the result of a route collection
|
||||
type RouteResult[I controllers.Object, IStatus any] struct {
|
||||
// VirtualServices are the primary output that configures the internal routing logic
|
||||
VirtualServices krt.Collection[*config.Config]
|
||||
// RouteAttachments holds information about parent attachment to routes, used for computed the `attachedRoutes` count.
|
||||
RouteAttachments krt.Collection[RouteAttachment]
|
||||
// Status stores the status reports for the incoming object
|
||||
Status krt.StatusCollection[I, IStatus]
|
||||
// Ancestors stores information about Gateway --> Backend references
|
||||
Ancestors krt.Collection[AncestorBackend]
|
||||
}
|
||||
|
||||
type RouteAttachment struct {
|
||||
From TypedResource
|
||||
// To is assumed to be a Gateway
|
||||
To types.NamespacedName
|
||||
ListenerName string
|
||||
}
|
||||
|
||||
func (r RouteAttachment) ResourceName() string {
|
||||
return r.From.Kind.String() + "/" + r.From.Name.String() + "/" + r.To.String() + "/" + r.ListenerName
|
||||
}
|
||||
|
||||
func (r RouteAttachment) Equals(other RouteAttachment) bool {
|
||||
return r.From == other.From && r.To == other.To && r.ListenerName == other.ListenerName
|
||||
}
|
||||
|
||||
// gatewayRouteAttachmentCountCollection holds the generic logic to determine the parents a route is attached to, used for
|
||||
// computing the aggregated `attachedRoutes` status in Gateway.
|
||||
func gatewayRouteAttachmentCountCollection[T controllers.Object](
|
||||
inputs RouteContextInputs,
|
||||
col krt.Collection[T],
|
||||
kind config.GroupVersionKind,
|
||||
opts krt.OptionsBuilder,
|
||||
) krt.Collection[RouteAttachment] {
|
||||
return krt.NewManyCollection(col, func(krtctx krt.HandlerContext, obj T) []RouteAttachment {
|
||||
ctx := inputs.WithCtx(krtctx)
|
||||
from := TypedResource{
|
||||
Kind: kind,
|
||||
Name: config.NamespacedName(obj),
|
||||
}
|
||||
|
||||
parentRefs := extractParentReferenceInfo(ctx, inputs.RouteParents, obj)
|
||||
return slices.MapFilter(filteredReferences(parentRefs), func(e routeParentReference) *RouteAttachment {
|
||||
if e.ParentKey.Kind != gvk.KubernetesGateway {
|
||||
return nil
|
||||
}
|
||||
return &RouteAttachment{
|
||||
From: from,
|
||||
To: types.NamespacedName{
|
||||
Name: e.ParentKey.Name,
|
||||
Namespace: e.ParentKey.Namespace,
|
||||
},
|
||||
ListenerName: string(e.ParentSection),
|
||||
}
|
||||
})
|
||||
}, opts.WithName(kind.Kind+"/count")...)
|
||||
}
|
||||
|
||||
// mergeHTTPRoutes merges HTTProutes by key. Gateway API has semantics for the ordering of `match` rules, that merges across resource.
|
||||
// So we merge everything (by key) following that ordering logic, and sort into a linear list (how VirtualService semantics work).
|
||||
func mergeHTTPRoutes(baseVirtualServices krt.Collection[RouteWithKey], opts ...krt.CollectionOption) krt.Collection[*config.Config] {
|
||||
idx := krt.NewIndex(baseVirtualServices, "key", func(o RouteWithKey) []string {
|
||||
return []string{o.Key}
|
||||
}).AsCollection(opts...)
|
||||
finalVirtualServices := krt.NewCollection(idx, func(ctx krt.HandlerContext, object krt.IndexObject[string, RouteWithKey]) **config.Config {
|
||||
configs := object.Objects
|
||||
if len(configs) == 1 {
|
||||
base := configs[0].Config
|
||||
nm := base.Meta.DeepCopy()
|
||||
// When dealing with a merge, we MUST take into account the merge key into the name.
|
||||
// Otherwise, we end up with broken state, where two inputs map to the same output which is not allowed by krt.
|
||||
// Because a lot of code assumes the object key is 'namespace/name', and the key always has slashes, we also translate the /
|
||||
nm.Name = strings.ReplaceAll(object.Key, "/", "~")
|
||||
return ptr.Of(&config.Config{
|
||||
Meta: nm,
|
||||
Spec: base.Spec,
|
||||
Status: base.Status,
|
||||
Extra: base.Extra,
|
||||
})
|
||||
}
|
||||
sortRoutesByCreationTime(configs)
|
||||
base := configs[0].DeepCopy()
|
||||
baseVS := base.Spec.(*istio.VirtualService)
|
||||
for _, config := range configs[1:] {
|
||||
thisVS := config.Spec.(*istio.VirtualService)
|
||||
baseVS.Http = append(baseVS.Http, thisVS.Http...)
|
||||
// append parents
|
||||
base.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s",
|
||||
base.Annotations[constants.InternalParentNames], config.Annotations[constants.InternalParentNames])
|
||||
}
|
||||
sortHTTPRoutes(baseVS.Http)
|
||||
base.Name = strings.ReplaceAll(object.Key, "/", "~")
|
||||
return ptr.Of(&base)
|
||||
}, opts...)
|
||||
return finalVirtualServices
|
||||
}
|
||||
106
pkg/ingress/kube/gateway/istio/status_test.go
Normal file
106
pkg/ingress/kube/gateway/istio/status_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright Istio 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 istio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"istio.io/istio/pilot/pkg/networking/core"
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/status"
|
||||
"istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
"istio.io/istio/pkg/slices"
|
||||
"istio.io/istio/pkg/test"
|
||||
"istio.io/istio/pkg/test/util/assert"
|
||||
)
|
||||
|
||||
func TestStatusCollections(t *testing.T) {
|
||||
stop := test.NewStop(t)
|
||||
fetch := func(q *TestStatusQueue) []string {
|
||||
return slices.Sort(slices.Map(q.Statuses(), func(e any) string {
|
||||
return *(e.(*string))
|
||||
}))
|
||||
}
|
||||
|
||||
type Status = krt.ObjectWithStatus[*v1.ConfigMap, string]
|
||||
c := setupControllerWithoutGatewayClasses(t)
|
||||
obj1 := Status{
|
||||
Obj: &v1.ConfigMap{},
|
||||
Status: "hello world",
|
||||
}
|
||||
fakeCol := krt.NewStaticCollection[Status](nil, []Status{obj1}, krt.WithStop(stop))
|
||||
status.RegisterStatus(c.status, fakeCol, func(i *v1.ConfigMap) string {
|
||||
return ""
|
||||
})
|
||||
|
||||
sq1 := &TestStatusQueue{state: map[status.Resource]any{}}
|
||||
setAndWait(t, c, sq1)
|
||||
assert.Equal(t, fetch(sq1), []string{"hello world"})
|
||||
|
||||
c.status.UnsetQueue()
|
||||
|
||||
// We should not get an update on the un-registered queue
|
||||
fakeCol.UpdateObject(Status{
|
||||
Obj: &v1.ConfigMap{},
|
||||
Status: "hello world2",
|
||||
})
|
||||
assert.Equal(t, fetch(sq1), []string{"hello world"})
|
||||
|
||||
// New queue should send new events, including existing state
|
||||
sq2 := &TestStatusQueue{state: map[status.Resource]any{}}
|
||||
setAndWait(t, c, sq2)
|
||||
assert.Equal(t, fetch(sq2), []string{"hello world2"})
|
||||
// And any new state
|
||||
fakeCol.UpdateObject(Status{
|
||||
Obj: &v1.ConfigMap{},
|
||||
Status: "hello world3",
|
||||
})
|
||||
// New event, so this is eventually consistent
|
||||
assert.EventuallyEqual(t, func() []string {
|
||||
return fetch(sq2)
|
||||
}, []string{"hello world3"})
|
||||
}
|
||||
|
||||
func setAndWait(t test.Failer, c *Controller, q status.Queue) {
|
||||
stop := test.NewStop(t)
|
||||
for _, syncer := range c.status.SetQueue(q) {
|
||||
syncer.WaitUntilSynced(stop)
|
||||
}
|
||||
}
|
||||
|
||||
func setupControllerWithoutGatewayClasses(t *testing.T, objs ...runtime.Object) *Controller {
|
||||
kc := kube.NewFakeClient(objs...)
|
||||
setupClientCRDs(t, kc)
|
||||
stop := test.NewStop(t)
|
||||
controller := NewController(
|
||||
kc,
|
||||
func(class schema.GroupVersionResource, stop <-chan struct{}) bool {
|
||||
return false
|
||||
},
|
||||
controller.Options{KrtDebugger: krt.GlobalDebugHandler},
|
||||
nil)
|
||||
kc.RunAndWait(stop)
|
||||
go controller.Run(stop)
|
||||
cg := core.NewConfigGenTest(t, core.TestOptions{})
|
||||
controller.Reconcile(cg.PushContext())
|
||||
kube.WaitForCacheSync("test", stop, controller.HasSynced)
|
||||
|
||||
return controller
|
||||
}
|
||||
@@ -15,14 +15,7 @@
|
||||
package istio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/fuzz"
|
||||
"sigs.k8s.io/gateway-api/pkg/features"
|
||||
)
|
||||
|
||||
func FuzzConvertResources(f *testing.F) {
|
||||
fuzz.Fuzz(f, func(fg fuzz.Helper) {
|
||||
r := fuzz.Struct[GatewayResources](fg)
|
||||
convertResources(r)
|
||||
})
|
||||
}
|
||||
var SupportedFeatures = features.AllFeatures
|
||||
@@ -1,65 +0,0 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: higress
|
||||
spec:
|
||||
controllerName: higress.io/gateway-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: third-party-gateway
|
||||
namespace: higress-system
|
||||
spec:
|
||||
addresses:
|
||||
- value: higress-gateway
|
||||
type: Hostname
|
||||
gatewayClassName: third-party-gatewayclass
|
||||
listeners:
|
||||
- name: default
|
||||
hostname: "*.domain.example"
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: All
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: higress-system
|
||||
annotations:
|
||||
gateway.higress.io/alias-for: third-party-gateway
|
||||
spec:
|
||||
addresses:
|
||||
- value: higress-gateway
|
||||
type: Hostname
|
||||
gatewayClassName: higress
|
||||
listeners:
|
||||
- name: default
|
||||
hostname: "*.domain.example"
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: All
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: third-party-gateway
|
||||
namespace: higress-system
|
||||
hostnames: ["first.domain.example", "another.domain.example"]
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: httpbin
|
||||
port: 80
|
||||
21
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.status.yaml.golden
vendored
Normal file
21
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.status.yaml.golden
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: gateway.networking.x-k8s.io/v1alpha1
|
||||
kind: XBackendTrafficPolicy
|
||||
metadata:
|
||||
name: lb-policy
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: echo
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'Configuration is valid, but Istio does not support the following fields:
|
||||
sessionPersistence'
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
24
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.yaml
vendored
Normal file
24
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: gateway.networking.x-k8s.io/v1alpha1
|
||||
kind: XBackendTrafficPolicy
|
||||
metadata:
|
||||
name: lb-policy
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: echo
|
||||
retryConstraint:
|
||||
minRetryRate:
|
||||
interval: "1s"
|
||||
count: 5
|
||||
budget:
|
||||
percent: 30
|
||||
interval: "10s"
|
||||
|
||||
sessionPersistence:
|
||||
sessionName: foo
|
||||
absoluteTimeout: 1h
|
||||
type: Cookie
|
||||
cookieConfig:
|
||||
lifetimeType: Permanent
|
||||
15
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.yaml.golden
vendored
Normal file
15
pkg/ingress/kube/gateway/istio/testdata/backend-lb-policy.yaml.golden
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: XBackendTrafficPolicy/default.lb-policy
|
||||
name: echo~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: echo.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
loadBalancer: {}
|
||||
retryBudget:
|
||||
minRetryConcurrency: 5
|
||||
percent: 30
|
||||
---
|
||||
426
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.status.yaml.golden
vendored
Normal file
426
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.status.yaml.golden
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: bad-configmap-type
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: foo-svc
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate reference invalid: unsupported kind UnknownKind'
|
||||
reason: NoValidCACertificate
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate reference not supported: unsupported kind UnknownKind'
|
||||
reason: InvalidKind
|
||||
status: "False"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: bad-service
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: does-not-exist
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'targetRefs invalid: reference default/does-not-exist (of kind Service)
|
||||
not found'
|
||||
reason: TargetNotFound
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: existing-status
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: hello
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
controllerName: example.com/some-other-controller
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: malformed-configmap
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin-other
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate reference invalid: found secret, but didn''t have expected
|
||||
keys cacert or ca.crt; found: not-ca.crt'
|
||||
reason: NoValidCACertificate
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate invalid: found secret, but didn''t have expected keys
|
||||
cacert or ca.crt; found: not-ca.crt'
|
||||
reason: InvalidCACertificateRef
|
||||
status: "False"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: multi-host-service-entry
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: networking.istio.io
|
||||
kind: ServiceEntry
|
||||
name: multi-host-service
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: multi-host-service-entry-section-name
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: networking.istio.io
|
||||
kind: ServiceEntry
|
||||
name: multi-host-service
|
||||
sectionName: tls
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-external-service-https
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: networking.istio.io
|
||||
kind: ServiceEntry
|
||||
name: external-service
|
||||
sectionName: https
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
- ancestorRef:
|
||||
group: networking.istio.io
|
||||
kind: ServiceEntry
|
||||
name: external-service
|
||||
sectionName: non-existing-port-name
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'targetRefs invalid: sectionName "non-existing-port-name" does not
|
||||
exist in ServiceEntry default/external-service'
|
||||
reason: TargetNotFound
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-upstream-echo
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: echo
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-upstream-echo-https-merged-rules
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: echo-https
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: echo-https
|
||||
sectionName: https
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: echo-https
|
||||
sectionName: non-existing-port-name
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'targetRefs invalid: sectionName "non-existing-port-name" does not
|
||||
exist in Service default/echo-https'
|
||||
reason: TargetNotFound
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
- ancestorRef:
|
||||
group: gateway.networking.k8s.io
|
||||
kind: Gateway
|
||||
name: gateway
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'targetRefs invalid: sectionName "non-existing-port-name" does not
|
||||
exist in Service default/echo-https'
|
||||
reason: TargetNotFound
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Configuration is valid
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: higress.io/gateway-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha3
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: unknown-configmap
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin-second
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate reference invalid: reference default/does-not-exist (of
|
||||
kind ConfigMap) not found'
|
||||
reason: NoValidCACertificate
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Certificate reference not found: reference default/does-not-exist
|
||||
(of kind ConfigMap) not found'
|
||||
reason: InvalidCACertificateRef
|
||||
status: "False"
|
||||
type: ResolvedRefs
|
||||
controllerName: istio.io/mesh-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: higress
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Handled by Higress controller
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: higress-system
|
||||
spec: null
|
||||
status:
|
||||
addresses:
|
||||
- type: Hostname
|
||||
value: higress-gateway.higress-system.svc.domain.suffix
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Resource accepted
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: Resource programmed, assigned to service(s) higress-gateway.higress-system.svc.domain.suffix:80
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
listeners:
|
||||
- attachedRoutes: 1
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: default
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: HTTPRoute
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: GRPCRoute
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
namespace: higress-system
|
||||
spec: null
|
||||
status:
|
||||
parents:
|
||||
- conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Route was valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: backendRef echo-https/default not accessible to a HTTPRoute in namespace
|
||||
"higress-system" (missing a ReferenceGrant?)
|
||||
reason: RefNotPermitted
|
||||
status: "False"
|
||||
type: ResolvedRefs
|
||||
controllerName: higress.io/gateway-controller
|
||||
parentRef:
|
||||
name: gateway
|
||||
---
|
||||
281
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.yaml
vendored
Normal file
281
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.yaml
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
# echo-https must be created by the kube-client, because it's used in a test
|
||||
# that verifies `sectionName`, which is internally read from krt,
|
||||
# so it could be just a `model.ServiceInstance`
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: higress
|
||||
spec:
|
||||
controllerName: higress.io/gateway-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: higress-system
|
||||
spec:
|
||||
addresses:
|
||||
- value: higress-gateway
|
||||
type: Hostname
|
||||
gatewayClassName: higress
|
||||
listeners:
|
||||
- name: default
|
||||
hostname: "*.domain.example"
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
namespace: higress-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: echo-https
|
||||
namespace: default
|
||||
port: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: echo-https
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
- name: https
|
||||
port: 443
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-upstream-echo
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: echo
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: auth-cert
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-upstream-echo-https-merged-rules
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: echo-https
|
||||
group: ""
|
||||
- kind: Service
|
||||
name: echo-https
|
||||
group: ""
|
||||
sectionName: https
|
||||
- kind: Service
|
||||
name: echo-https
|
||||
group: ""
|
||||
sectionName: non-existing-port-name
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: auth-cert
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: existing-status
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: httpbin
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: auth-cert
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
status:
|
||||
ancestors:
|
||||
- ancestorRef:
|
||||
group: ""
|
||||
kind: Service
|
||||
name: httpbin
|
||||
conditions:
|
||||
- lastTransitionTime: 2000-01-01T01:01:01Z
|
||||
message: hello
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
controllerName: example.com/some-other-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: bad-service
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: does-not-exist
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: auth-cert
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: unknown-configmap
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: httpbin-second
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: does-not-exist
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: malformed-configmap
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: httpbin-other
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: malformed
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: bad-configmap-type
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: Service
|
||||
name: foo-svc
|
||||
group: ""
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: UnknownKind
|
||||
name: blah
|
||||
group: ""
|
||||
hostname: auth.example.com
|
||||
---
|
||||
# ServiceEntry with multiple hosts for testing multiple DestinationRules
|
||||
apiVersion: networking.istio.io/v1
|
||||
kind: ServiceEntry
|
||||
metadata:
|
||||
name: multi-host-service
|
||||
namespace: default
|
||||
spec:
|
||||
hosts:
|
||||
- api.example.com
|
||||
- cdn.example.com
|
||||
ports:
|
||||
- number: 443
|
||||
name: https
|
||||
protocol: HTTPS
|
||||
- number: 8443
|
||||
name: tls
|
||||
protocol: TLS
|
||||
resolution: DNS
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: multi-host-service-entry
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: ServiceEntry
|
||||
name: multi-host-service
|
||||
group: networking.istio.io
|
||||
validation:
|
||||
wellKnownCACertificates: System
|
||||
hostname: cdn.example.com
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: multi-host-service-entry-section-name
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: ServiceEntry
|
||||
name: multi-host-service
|
||||
group: networking.istio.io
|
||||
sectionName: tls
|
||||
validation:
|
||||
caCertificateRefs:
|
||||
- kind: ConfigMap
|
||||
name: auth-cert
|
||||
group: ""
|
||||
hostname: api.example.com
|
||||
---
|
||||
# Simple ServiceEntry with 2 ports for testing sectionName
|
||||
apiVersion: networking.istio.io/v1
|
||||
kind: ServiceEntry
|
||||
metadata:
|
||||
name: external-service
|
||||
namespace: default
|
||||
spec:
|
||||
hosts:
|
||||
- external.example.com
|
||||
ports:
|
||||
- number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
- number: 443
|
||||
name: https
|
||||
protocol: HTTPS
|
||||
resolution: DNS
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: BackendTLSPolicy
|
||||
metadata:
|
||||
name: tls-external-service-https
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- kind: ServiceEntry
|
||||
name: external-service
|
||||
group: networking.istio.io
|
||||
sectionName: https
|
||||
- kind: ServiceEntry
|
||||
name: external-service
|
||||
group: networking.istio.io
|
||||
sectionName: non-existing-port-name
|
||||
validation:
|
||||
wellKnownCACertificates: System
|
||||
hostname: external.example.com
|
||||
193
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.yaml.golden
vendored
Normal file
193
pkg/ingress/kube/gateway/istio/testdata/backend-tls-policy.yaml.golden
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: higress-gateway.higress-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/gateway/default.higress-system
|
||||
internal.istio.io/service-account-name: ""
|
||||
name: gateway-istio-autogenerated-k8s-gateway-default
|
||||
namespace: higress-system
|
||||
spec:
|
||||
servers:
|
||||
- hosts:
|
||||
- higress-system/*.domain.example
|
||||
port:
|
||||
name: default
|
||||
number: 80
|
||||
protocol: HTTP
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: HTTPRoute/http.higress-system
|
||||
internal.istio.io/route-semantics: gateway
|
||||
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
|
||||
namespace: higress-system
|
||||
spec:
|
||||
gateways:
|
||||
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
|
||||
hosts:
|
||||
- '*'
|
||||
http:
|
||||
- name: http
|
||||
route:
|
||||
- destination: {}
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.tls-upstream-echo-https-merged-rules
|
||||
name: echo-https~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: echo-https.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
portLevelSettings:
|
||||
- port:
|
||||
number: 443
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.tls-upstream-echo
|
||||
name: echo~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: echo.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.tls-external-service-https
|
||||
name: external-service~external-example-com~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: external.example.com
|
||||
trafficPolicy:
|
||||
portLevelSettings:
|
||||
- port:
|
||||
number: 443
|
||||
tls:
|
||||
mode: SIMPLE
|
||||
sni: external.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.bad-configmap-type
|
||||
name: foo-svc~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: foo-svc.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
tls:
|
||||
credentialName: invalid://
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.malformed-configmap
|
||||
name: httpbin-other~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: httpbin-other.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
tls:
|
||||
credentialName: invalid://
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.unknown-configmap
|
||||
name: httpbin-second~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: httpbin-second.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
tls:
|
||||
credentialName: invalid://
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.existing-status
|
||||
name: httpbin~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: httpbin.default.svc.domain.suffix
|
||||
trafficPolicy:
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: auth.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.multi-host-service-entry,BackendTLSPolicy/default.multi-host-service-entry-section-name
|
||||
name: multi-host-service~api-example-com~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: api.example.com
|
||||
trafficPolicy:
|
||||
portLevelSettings:
|
||||
- port:
|
||||
number: 8443
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: api.example.com
|
||||
tls:
|
||||
mode: SIMPLE
|
||||
sni: cdn.example.com
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: DestinationRule
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: BackendTLSPolicy/default.multi-host-service-entry,BackendTLSPolicy/default.multi-host-service-entry-section-name
|
||||
name: multi-host-service~cdn-example-com~istio-autogenerated-k8s-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
host: cdn.example.com
|
||||
trafficPolicy:
|
||||
portLevelSettings:
|
||||
- port:
|
||||
number: 8443
|
||||
tls:
|
||||
credentialName: configmap://default/auth-cert
|
||||
mode: SIMPLE
|
||||
sni: api.example.com
|
||||
tls:
|
||||
mode: SIMPLE
|
||||
sni: cdn.example.com
|
||||
---
|
||||
@@ -1,21 +1,21 @@
|
||||
# the same as pilot/pkg/config/kube/gateway/testdata/route-precedence.yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: higress
|
||||
name: istio
|
||||
spec:
|
||||
controllerName: higress.io/gateway-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: higress-system
|
||||
namespace: istio-system
|
||||
spec:
|
||||
addresses:
|
||||
- value: higress-gateway
|
||||
- value: istio-ingressgateway
|
||||
type: Hostname
|
||||
gatewayClassName: higress
|
||||
gatewayClassName: istio
|
||||
listeners:
|
||||
- name: default
|
||||
hostname: "*.domain.example"
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
matchLabels:
|
||||
istio.io/test-name-part: allowed
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
@@ -36,7 +36,7 @@ metadata:
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: higress-system
|
||||
namespace: istio-system
|
||||
- kind: Mesh
|
||||
name: istio
|
||||
hostnames: ["a.domain.example", "b.domain.example"]
|
||||
@@ -60,7 +60,7 @@ spec:
|
||||
- name: svc2
|
||||
port: 80
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
@@ -68,7 +68,7 @@ metadata:
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: higress-system
|
||||
namespace: istio-system
|
||||
- kind: Mesh
|
||||
name: istio
|
||||
hostnames: ["a.domain.example"]
|
||||
@@ -106,7 +106,7 @@ spec:
|
||||
- name: svc3
|
||||
port: 80
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
@@ -114,7 +114,7 @@ metadata:
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: higress-system
|
||||
namespace: istio-system
|
||||
hostnames: ["a.domain.example", "b.domain.example"]
|
||||
rules:
|
||||
- matches:
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: higress
|
||||
namespace: default
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
@@ -16,14 +14,13 @@ status:
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: gateway
|
||||
namespace: higress-system
|
||||
spec: null
|
||||
status:
|
||||
addresses:
|
||||
- type: IPAddress
|
||||
value: 1.2.3.4
|
||||
- type: Hostname
|
||||
value: higress-gateway.higress-system.svc.domain.suffix
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Resource accepted
|
||||
@@ -62,6 +59,8 @@ status:
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: HTTPRoute
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: GRPCRoute
|
||||
- attachedRoutes: 1
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
@@ -88,11 +87,12 @@ status:
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: HTTPRoute
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: GRPCRoute
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: http
|
||||
namespace: apple
|
||||
spec: null
|
||||
@@ -117,7 +117,6 @@ status:
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: http
|
||||
namespace: banana
|
||||
spec: null
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: higress
|
||||
spec:
|
||||
controllerName: higress.io/gateway-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
@@ -37,7 +37,7 @@ spec:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: banana
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
@@ -51,7 +51,7 @@ spec:
|
||||
- name: httpbin-apple
|
||||
port: 80
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: http
|
||||
|
||||
@@ -2,9 +2,10 @@ apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: higress-gateway.higress-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/gateway/apple.higress-system
|
||||
creationTimestamp: null
|
||||
internal.istio.io/service-account-name: ""
|
||||
name: gateway-istio-autogenerated-k8s-gateway-apple
|
||||
namespace: higress-system
|
||||
spec:
|
||||
@@ -20,9 +21,10 @@ apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: higress-gateway.higress-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/gateway/banana.higress-system
|
||||
creationTimestamp: null
|
||||
internal.istio.io/service-account-name: ""
|
||||
name: gateway-istio-autogenerated-k8s-gateway-banana
|
||||
namespace: higress-system
|
||||
spec:
|
||||
@@ -40,8 +42,7 @@ metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: HTTPRoute/http.apple
|
||||
internal.istio.io/route-semantics: gateway
|
||||
creationTimestamp: null
|
||||
name: http-0-istio-autogenerated-k8s-gateway
|
||||
name: higress-system~gateway-istio-autogenerated-k8s-gateway-apple~*
|
||||
namespace: apple
|
||||
spec:
|
||||
gateways:
|
||||
@@ -62,8 +63,7 @@ metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: HTTPRoute/http.banana
|
||||
internal.istio.io/route-semantics: gateway
|
||||
creationTimestamp: null
|
||||
name: http-0-istio-autogenerated-k8s-gateway
|
||||
name: higress-system~gateway-istio-autogenerated-k8s-gateway-banana~*
|
||||
namespace: banana
|
||||
spec:
|
||||
gateways:
|
||||
|
||||
91
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.status.yaml.golden
vendored
Normal file
91
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.status.yaml.golden
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: eastwestgateway
|
||||
namespace: istio-system
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Resource accepted
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Failed to assign to any requested addresses: hostname "eastwestgateway.istio-system.svc.domain.suffix"
|
||||
not found'
|
||||
reason: AddressNotUsable
|
||||
status: "False"
|
||||
type: Programmed
|
||||
listeners:
|
||||
- attachedRoutes: 0
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: mesh
|
||||
supportedKinds: []
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: invalid
|
||||
namespace: istio-system
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Resource accepted
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Failed to assign to any requested addresses: hostname "invalid.istio-system.svc.domain.suffix"
|
||||
not found'
|
||||
reason: AddressNotUsable
|
||||
status: "False"
|
||||
type: Programmed
|
||||
listeners:
|
||||
- attachedRoutes: 0
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Expected a single listener on port 15008 with protocol "HBONE" and
|
||||
TLS.Mode == Terminate
|
||||
reason: UnsupportedProtocol
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: mesh
|
||||
supportedKinds: []
|
||||
---
|
||||
31
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.yaml
vendored
Normal file
31
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: eastwestgateway
|
||||
namespace: istio-system
|
||||
labels:
|
||||
topology.istio.io/network: "network-1"
|
||||
spec:
|
||||
gatewayClassName: istio-east-west
|
||||
listeners:
|
||||
- name: mesh
|
||||
port: 15008
|
||||
protocol: HBONE
|
||||
tls:
|
||||
mode: Terminate # represents double-HBONE
|
||||
options:
|
||||
gateway.istio.io/tls-terminate-mode: ISTIO_MUTUAL
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: invalid
|
||||
namespace: istio-system
|
||||
labels:
|
||||
topology.istio.io/network: "network-1"
|
||||
spec:
|
||||
gatewayClassName: istio-east-west
|
||||
listeners:
|
||||
- name: mesh
|
||||
port: 15008
|
||||
protocol: HBONE # No TLS mode terminate
|
||||
0
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.yaml.golden
vendored
Normal file
0
pkg/ingress/kube/gateway/istio/testdata/east-west-ambient.yaml.golden
vendored
Normal file
149
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.status.yaml.golden
vendored
Normal file
149
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.status.yaml.golden
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: eastwestgateway
|
||||
namespace: istio-system
|
||||
spec: null
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Resource accepted
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: 'Failed to assign to any requested addresses: hostname "eastwestgateway-istio.istio-system.svc.domain.suffix"
|
||||
not found'
|
||||
reason: AddressNotUsable
|
||||
status: "False"
|
||||
type: Programmed
|
||||
listeners:
|
||||
- attachedRoutes: 1
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: istiod-grpc
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
- attachedRoutes: 1
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: istiod-webhook
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
- attachedRoutes: 0
|
||||
conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: NoConflicts
|
||||
status: "False"
|
||||
type: Conflicted
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: Programmed
|
||||
status: "True"
|
||||
type: Programmed
|
||||
- lastTransitionTime: fake
|
||||
message: No errors found
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
name: cross-network
|
||||
supportedKinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: TLSRoute
|
||||
metadata:
|
||||
name: eastwestgateway-grpc
|
||||
namespace: istio-system
|
||||
spec: null
|
||||
status:
|
||||
parents:
|
||||
- conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Route was valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: All references resolved
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: higress.io/gateway-controller
|
||||
parentRef:
|
||||
kind: Gateway
|
||||
name: eastwestgateway
|
||||
sectionName: istiod-grpc
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: TLSRoute
|
||||
metadata:
|
||||
name: eastwestgateway-webhook
|
||||
namespace: istio-system
|
||||
spec: null
|
||||
status:
|
||||
parents:
|
||||
- conditions:
|
||||
- lastTransitionTime: fake
|
||||
message: Route was valid
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: fake
|
||||
message: All references resolved
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: higress.io/gateway-controller
|
||||
parentRef:
|
||||
kind: Gateway
|
||||
name: eastwestgateway
|
||||
sectionName: istiod-webhook
|
||||
---
|
||||
57
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.yaml
vendored
Normal file
57
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: eastwestgateway
|
||||
namespace: istio-system
|
||||
labels:
|
||||
topology.istio.io/network: "network-1"
|
||||
networking.istio.io/gatewayPort: "35443"
|
||||
spec:
|
||||
gatewayClassName: istio
|
||||
listeners:
|
||||
- name: istiod-grpc
|
||||
port: 15012
|
||||
protocol: TLS
|
||||
tls:
|
||||
mode: Passthrough
|
||||
- name: istiod-webhook
|
||||
port: 15017
|
||||
protocol: TLS
|
||||
tls:
|
||||
mode: Passthrough
|
||||
- name: cross-network
|
||||
hostname: "*.local"
|
||||
port: 35443
|
||||
protocol: TLS
|
||||
tls:
|
||||
mode: Passthrough
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: TLSRoute
|
||||
metadata:
|
||||
name: eastwestgateway-grpc
|
||||
namespace: istio-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: eastwestgateway
|
||||
kind: Gateway
|
||||
sectionName: istiod-grpc
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: istiod
|
||||
port: 15012
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: TLSRoute
|
||||
metadata:
|
||||
name: eastwestgateway-webhook
|
||||
namespace: istio-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: eastwestgateway
|
||||
kind: Gateway
|
||||
sectionName: istiod-webhook
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: istiod
|
||||
port: 15017
|
||||
104
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.yaml.golden
vendored
Normal file
104
pkg/ingress/kube/gateway/istio/testdata/eastwest-labelport.yaml.golden
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: eastwestgateway-istio.istio-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/eastwestgateway/cross-network.istio-system
|
||||
name: eastwestgateway-istio-autogenerated-k8s-gateway-cross-network
|
||||
namespace: istio-system
|
||||
spec:
|
||||
servers:
|
||||
- hosts:
|
||||
- istio-system/*.local
|
||||
port:
|
||||
name: default
|
||||
number: 35443
|
||||
protocol: TLS
|
||||
tls:
|
||||
mode: AUTO_PASSTHROUGH
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: eastwestgateway-istio.istio-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/eastwestgateway/istiod-grpc.istio-system
|
||||
name: eastwestgateway-istio-autogenerated-k8s-gateway-istiod-grpc
|
||||
namespace: istio-system
|
||||
spec:
|
||||
servers:
|
||||
- hosts:
|
||||
- istio-system/*
|
||||
port:
|
||||
name: default
|
||||
number: 15012
|
||||
protocol: TLS
|
||||
tls: {}
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/gateway-semantics: gateway
|
||||
internal.istio.io/gateway-service: eastwestgateway-istio.istio-system.svc.domain.suffix
|
||||
internal.istio.io/parents: Gateway/eastwestgateway/istiod-webhook.istio-system
|
||||
name: eastwestgateway-istio-autogenerated-k8s-gateway-istiod-webhook
|
||||
namespace: istio-system
|
||||
spec:
|
||||
servers:
|
||||
- hosts:
|
||||
- istio-system/*
|
||||
port:
|
||||
name: default
|
||||
number: 15017
|
||||
protocol: TLS
|
||||
tls: {}
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: TLSRoute/eastwestgateway-grpc.istio-system
|
||||
internal.istio.io/route-semantics: gateway
|
||||
name: eastwestgateway-grpc-tls-0-istio-autogenerated-k8s-gateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
gateways:
|
||||
- istio-system/eastwestgateway-istio-autogenerated-k8s-gateway-istiod-grpc
|
||||
hosts:
|
||||
- '*'
|
||||
tls:
|
||||
- match:
|
||||
- sniHosts:
|
||||
- '*'
|
||||
route:
|
||||
- destination:
|
||||
host: istiod.istio-system.svc.domain.suffix
|
||||
port:
|
||||
number: 15012
|
||||
---
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
annotations:
|
||||
internal.istio.io/parents: TLSRoute/eastwestgateway-webhook.istio-system
|
||||
internal.istio.io/route-semantics: gateway
|
||||
name: eastwestgateway-webhook-tls-0-istio-autogenerated-k8s-gateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
gateways:
|
||||
- istio-system/eastwestgateway-istio-autogenerated-k8s-gateway-istiod-webhook
|
||||
hosts:
|
||||
- '*'
|
||||
tls:
|
||||
- match:
|
||||
- sniHosts:
|
||||
- '*'
|
||||
route:
|
||||
- destination:
|
||||
host: istiod.istio-system.svc.domain.suffix
|
||||
port:
|
||||
number: 15017
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user