Compare commits
3
Commits
882eeade1e
...
v0.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cfde8b702 | ||
|
|
deea8629e0 | ||
|
|
6cf9465fea |
@@ -2,6 +2,17 @@
|
||||
|
||||
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)(版本段不记日期),版本号遵循语义化版本。
|
||||
|
||||
## [0.7.3]
|
||||
|
||||
### Changed
|
||||
|
||||
- `LaunchInstance` 移出回传关键事件清单(Connector 过滤条件与「云端事件」告警白名单共用):创建以面板自身(手动 / 抢机反复尝试)发起为主,回传即噪声(高频抢机可在一两天内刷掉 2 万条存量上限);终止与电源操作等外部风险信号保留。链路幂等创建新增过滤条件对账,存量链路在租户详情点「一键创建」即原地更新条件,无需拆除重建
|
||||
- 实例规格清单(ListShapes)透传 OCI `quotaNames`(与 compute limits 配额名同名),供前端数据驱动配额与可用域可用性判定
|
||||
|
||||
### Fixed
|
||||
|
||||
- 修复「云端事件」实例生命周期通知从未生效:Audit v2 计算类事件类型带 `.begin` / `.end` 阶段后缀(如 `…ComputeApi.LaunchInstance.end`),事件短名取末段得到 `begin` / `end`,不在关键事件集合内而被跳过。现剥离阶段后缀判定,成对事件只推 `.begin`(携带操作者 / IP / 成败;`.end` 无操作者且信息重复);`X failed with response 'Err'` 形态消息判为失败并提取引号内错误码作告警补充说明;告警资源名在 `resourceName` 缺失时回退外层 `source`(实例名)
|
||||
|
||||
## [0.7.2]
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v0.7.2
|
||||
v0.7.3
|
||||
|
||||
@@ -8654,6 +8654,13 @@ const docTemplate = `{
|
||||
},
|
||||
"processorDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"quotaNames": {
|
||||
"description": "QuotaNames 是该 shape 对应的配额名(与 Limits 服务 compute limit name 同名),\n前端据此结合 limits 行判定配额与 AD 可用性。",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8647,6 +8647,13 @@
|
||||
},
|
||||
"processorDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"quotaNames": {
|
||||
"description": "QuotaNames 是该 shape 对应的配额名(与 Limits 服务 compute limit name 同名),\n前端据此结合 limits 行判定配额与 AD 可用性。",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1754,6 +1754,13 @@ definitions:
|
||||
type: number
|
||||
processorDescription:
|
||||
type: string
|
||||
quotaNames:
|
||||
description: |-
|
||||
QuotaNames 是该 shape 对应的配额名(与 Limits 服务 compute limit name 同名),
|
||||
前端据此结合 limits 行判定配额与 AD 可用性。
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
oci-portal_internal_oci.ConsoleConnection:
|
||||
properties:
|
||||
|
||||
@@ -234,8 +234,12 @@ func (c *RealClient) EnsureRelayConnector(ctx context.Context, cred Credentials,
|
||||
if err != nil {
|
||||
return RelayResource{}, err
|
||||
}
|
||||
if res, ok, err := findRelayConnector(ctx, sc, cred.TenancyOCID); err != nil || ok {
|
||||
return res, err
|
||||
res, ok, err := findRelayConnector(ctx, sc, cred.TenancyOCID)
|
||||
if err != nil {
|
||||
return RelayResource{}, err
|
||||
}
|
||||
if ok {
|
||||
return res, reconcileRelayCondition(ctx, sc, res.ID, condition)
|
||||
}
|
||||
details := sch.CreateServiceConnectorDetails{
|
||||
DisplayName: common.String(relayConnectorName),
|
||||
@@ -257,6 +261,45 @@ func (c *RealClient) EnsureRelayConnector(ctx context.Context, cred Credentials,
|
||||
return waitRelayConnector(ctx, sc, cred.TenancyOCID)
|
||||
}
|
||||
|
||||
// reconcileRelayCondition 对齐存量 Connector 的过滤条件:关键事件清单变更
|
||||
// (如去除 LaunchInstance)后,已建链路经「一键创建」幂等调用原地更新,无需拆除重建。
|
||||
func reconcileRelayCondition(ctx context.Context, sc sch.ServiceConnectorClient, id, condition string) error {
|
||||
got, err := sc.GetServiceConnector(ctx, sch.GetServiceConnectorRequest{ServiceConnectorId: &id})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get service connector: %w", err)
|
||||
}
|
||||
if !relayConditionDiffers(got.Tasks, condition) {
|
||||
return nil
|
||||
}
|
||||
details := sch.UpdateServiceConnectorDetails{Tasks: []sch.TaskDetails{}}
|
||||
if condition != "" {
|
||||
details.Tasks = []sch.TaskDetails{sch.LogRuleTaskDetails{Condition: &condition}}
|
||||
}
|
||||
_, err = sc.UpdateServiceConnector(ctx, sch.UpdateServiceConnectorRequest{
|
||||
ServiceConnectorId: &id, UpdateServiceConnectorDetails: details,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update service connector condition: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// relayConditionDiffers 判断现有任务集与期望条件是否不一致:
|
||||
// 期望形态是单条 LogRule 条件;任务数、类型或条件文本不同均视为漂移。
|
||||
func relayConditionDiffers(tasks []sch.TaskDetailsResponse, want string) bool {
|
||||
if want == "" {
|
||||
return len(tasks) > 0
|
||||
}
|
||||
if len(tasks) != 1 {
|
||||
return true
|
||||
}
|
||||
rule, ok := tasks[0].(sch.LogRuleTaskDetailsResponse)
|
||||
if !ok || rule.Condition == nil {
|
||||
return true
|
||||
}
|
||||
return *rule.Condition != want
|
||||
}
|
||||
|
||||
// findRelayConnector 按名查找存活 Connector。
|
||||
func findRelayConnector(ctx context.Context, sc sch.ServiceConnectorClient, tenancy string) (RelayResource, bool, error) {
|
||||
list, err := sc.ListServiceConnectors(ctx, sch.ListServiceConnectorsRequest{
|
||||
|
||||
@@ -3,6 +3,8 @@ package oci
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/sch"
|
||||
)
|
||||
|
||||
func TestRelayEventCondition(t *testing.T) {
|
||||
@@ -35,3 +37,31 @@ func TestRelayEventConditionQuoting(t *testing.T) {
|
||||
t.Errorf("单引号数 = %d, want 2 (%s)", strings.Count(cond, "'"), cond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelayConditionDiffers(t *testing.T) {
|
||||
cond := "data.eventName='TerminateInstance'"
|
||||
rule := func(c string) sch.TaskDetailsResponse {
|
||||
return sch.LogRuleTaskDetailsResponse{Condition: &c}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
tasks []sch.TaskDetailsResponse
|
||||
want string
|
||||
diff bool
|
||||
}{
|
||||
{name: "条件一致不漂移", tasks: []sch.TaskDetailsResponse{rule(cond)}, want: cond, diff: false},
|
||||
{name: "条件文本不同漂移", tasks: []sch.TaskDetailsResponse{rule("data.eventName='LaunchInstance'")}, want: cond, diff: true},
|
||||
{name: "无任务但期望条件漂移", tasks: nil, want: cond, diff: true},
|
||||
{name: "多任务漂移", tasks: []sch.TaskDetailsResponse{rule(cond), rule(cond)}, want: cond, diff: true},
|
||||
{name: "期望空且无任务不漂移", tasks: nil, want: "", diff: false},
|
||||
{name: "期望空但有任务漂移", tasks: []sch.TaskDetailsResponse{rule(cond)}, want: "", diff: true},
|
||||
{name: "条件缺失漂移", tasks: []sch.TaskDetailsResponse{sch.LogRuleTaskDetailsResponse{}}, want: cond, diff: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := relayConditionDiffers(tt.tasks, tt.want); got != tt.diff {
|
||||
t.Errorf("differs = %v, want %v", got, tt.diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ type ComputeShape struct {
|
||||
MemoryMaxGBs float32 `json:"memoryMaxGBs,omitempty"`
|
||||
Gpus int `json:"gpus,omitempty"`
|
||||
ProcessorDescription string `json:"processorDescription,omitempty"`
|
||||
// QuotaNames 是该 shape 对应的配额名(与 Limits 服务 compute limit name 同名),
|
||||
// 前端据此结合 limits 行判定配额与 AD 可用性。
|
||||
QuotaNames []string `json:"quotaNames,omitempty"`
|
||||
}
|
||||
|
||||
// shapeCacheEntry 是一份 shape 清单的缓存条目。
|
||||
@@ -92,6 +95,7 @@ func toComputeShape(s core.Shape) ComputeShape {
|
||||
BillingType: string(s.BillingType),
|
||||
IsFlexible: s.OcpuOptions != nil,
|
||||
ProcessorDescription: deref(s.ProcessorDescription),
|
||||
QuotaNames: s.QuotaNames,
|
||||
}
|
||||
out.Ocpus, out.MemoryInGBs = deref32(s.Ocpus), deref32(s.MemoryInGBs)
|
||||
if s.Gpus != nil {
|
||||
|
||||
@@ -554,6 +554,8 @@ func envActor(env onsEnvelope) string {
|
||||
|
||||
// envOutcome 判读事件成败与补充说明:登录事件解析 auditEventMapValue;其余
|
||||
// Audit 事件看 message 后缀,补充说明取 stateChange.current.description(策略描述)。
|
||||
// 计算类失败形如 "LaunchInstance failed with response 'NotAuthorizedOrNotFound'",
|
||||
// 判失败并以引号内错误码作补充说明(无其他说明时)。
|
||||
func envOutcome(env onsEnvelope) (outcome, detail string) {
|
||||
if env.StateChange != nil {
|
||||
detail = env.StateChange.Current.Description
|
||||
@@ -566,10 +568,24 @@ func envOutcome(env onsEnvelope) (outcome, detail string) {
|
||||
outcome = "成功"
|
||||
case strings.HasSuffix(env.Message, " failed"):
|
||||
outcome = "失败"
|
||||
case strings.Contains(env.Message, " failed with response "):
|
||||
outcome = "失败"
|
||||
if detail == "" {
|
||||
detail = failedResponse(env.Message)
|
||||
}
|
||||
}
|
||||
return outcome, detail
|
||||
}
|
||||
|
||||
// failedResponse 提取 "X failed with response 'Err'" 中引号内的错误码。
|
||||
func failedResponse(msg string) string {
|
||||
_, after, ok := strings.Cut(msg, " failed with response ")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.Trim(strings.TrimSpace(after), "'")
|
||||
}
|
||||
|
||||
// ssoOutcome 解析 IDCS 审计负载(JSON 字符串):eventId 含 success/failure 定成败,
|
||||
// 失败时以其 message 作为原因说明。
|
||||
func ssoOutcome(raw, detail string) (string, string) {
|
||||
|
||||
@@ -308,6 +308,14 @@ func TestParseLogEvent(t *testing.T) {
|
||||
wantType: "x",
|
||||
wantOutcome: "失败",
|
||||
},
|
||||
{
|
||||
name: "failed with response 判失败并提取错误码",
|
||||
payload: `{"type":"com.oraclecloud.computeApi.LaunchInstance.begin","data":{
|
||||
"message":"LaunchInstance failed with response 'NotAuthorizedOrNotFound'"}}`,
|
||||
wantType: "com.oraclecloud.computeApi.LaunchInstance.begin",
|
||||
wantOutcome: "失败",
|
||||
wantDetail: "NotAuthorizedOrNotFound",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -23,9 +24,11 @@ const (
|
||||
)
|
||||
|
||||
// RelayCriticalEvents 是回传的关键事件清单(Connector Log Filter 与 P2 告警共用):
|
||||
// 实例生命周期、用户与凭据、区域订阅、策略变更、控制台登录。
|
||||
// 实例终止与电源操作、用户与凭据、区域订阅、策略变更、控制台登录。
|
||||
// 不含 LaunchInstance:创建以面板自身(手动/抢机反复尝试)发起为主,回传即噪声
|
||||
// (10 个 1min 抢机任务一天即可刷掉 2 万条上限),终止与电源操作才是外部风险信号。
|
||||
var RelayCriticalEvents = []string{
|
||||
"LaunchInstance", "TerminateInstance", "InstanceAction",
|
||||
"TerminateInstance", "InstanceAction",
|
||||
"CreateUser", "DeleteUser", "UpdateUser",
|
||||
"CreateApiKey", "DeleteApiKey", "UpdateUserCapabilities",
|
||||
"CreateRegionSubscription",
|
||||
@@ -46,7 +49,7 @@ var relayCriticalSet = func() map[string]bool {
|
||||
// 通知管理「云端事件」按此细分开关。
|
||||
func relayEventClass(name string) string {
|
||||
switch name {
|
||||
case "LaunchInstance", "TerminateInstance", "InstanceAction":
|
||||
case "TerminateInstance", "InstanceAction":
|
||||
return "instance"
|
||||
case "CreateUser", "DeleteUser", "UpdateUser",
|
||||
"CreateApiKey", "DeleteApiKey", "UpdateUserCapabilities":
|
||||
@@ -313,24 +316,30 @@ func (s *LogEventService) teardownResources(ctx context.Context, cred oci.Creden
|
||||
|
||||
// ---- P2 告警联动:解析出关键事件后经 Notifier 推送 ----
|
||||
|
||||
// relayEventShortName 取 CloudEvents type 末段(com.oraclecloud.ComputeApi.LaunchInstance → LaunchInstance)。
|
||||
// relayEventShortName 取 CloudEvents type 中的事件短名:Audit v2 计算类事件带
|
||||
// .begin/.end 阶段后缀(com.oraclecloud.ComputeApi.LaunchInstance.end),先剥阶段再取末段。
|
||||
func relayEventShortName(eventType string) string {
|
||||
if i := strings.LastIndex(eventType, "."); i >= 0 {
|
||||
return eventType[i+1:]
|
||||
t := strings.TrimSuffix(strings.TrimSuffix(eventType, ".begin"), ".end")
|
||||
if i := strings.LastIndex(t, "."); i >= 0 {
|
||||
return t[i+1:]
|
||||
}
|
||||
return eventType
|
||||
return t
|
||||
}
|
||||
|
||||
// criticalEventVars 判定关键事件并生成模板变量;非关键事件 ok 为 false。
|
||||
// 成对事件只推 .begin(携带操作者/IP/成败),.end 无操作者且信息重复,不再告警;
|
||||
// actor/ip/outcome/resource 缺失时兜底 —,detail 包装为独立行(空则不占行)。
|
||||
func criticalEventVars(alias string, p parsedEvent) (map[string]string, bool) {
|
||||
if strings.HasSuffix(p.EventType, ".end") {
|
||||
return nil, false
|
||||
}
|
||||
name := relayEventShortName(p.EventType)
|
||||
if name == "" || !relayCriticalSet[name] {
|
||||
return nil, false
|
||||
}
|
||||
return map[string]string{
|
||||
"tenant": alias, "event": name,
|
||||
"resource": orDash(p.ResourceName), "actor": orDash(p.Actor),
|
||||
"resource": orDash(cmp.Or(p.ResourceName, p.Source)), "actor": orDash(p.Actor),
|
||||
"ip": orDash(p.SourceIP), "outcome": orDash(p.Outcome),
|
||||
"detail": detailLine(p.Detail),
|
||||
}, true
|
||||
|
||||
@@ -339,10 +339,22 @@ func TestCriticalEventText(t *testing.T) {
|
||||
wantOK: true,
|
||||
want: map[string]string{"event": "CreatePolicy", "detail": "\n允许发布到 ONS Topic"},
|
||||
},
|
||||
{
|
||||
name: "begin 阶段剥后缀命中并回退 source 作资源",
|
||||
event: parsedEvent{EventType: "com.oraclecloud.computeApi.TerminateInstance.begin",
|
||||
Source: "instance-20260717-1445", Actor: "IT Team", SourceIP: "137.131.7.136", Outcome: "成功"},
|
||||
wantOK: true,
|
||||
want: map[string]string{"event": "TerminateInstance", "resource": "instance-20260717-1445",
|
||||
"actor": "IT Team", "ip": "137.131.7.136", "outcome": "成功"},
|
||||
},
|
||||
{name: "end 阶段不重复告警",
|
||||
event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.TerminateInstance.end"}, wantOK: false},
|
||||
{name: "LaunchInstance 已移出清单不告警",
|
||||
event: parsedEvent{EventType: "com.oraclecloud.computeApi.LaunchInstance.begin"}, wantOK: false},
|
||||
{name: "List 噪声不推", event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.ListInstances"}, wantOK: false},
|
||||
{name: "空类型不推", event: parsedEvent{}, wantOK: false},
|
||||
{name: "短名直接命中", event: parsedEvent{EventType: "LaunchInstance"}, wantOK: true,
|
||||
want: map[string]string{"event": "LaunchInstance"}},
|
||||
{name: "短名直接命中", event: parsedEvent{EventType: "InstanceAction"}, wantOK: true,
|
||||
want: map[string]string{"event": "InstanceAction"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -370,7 +382,7 @@ func TestRelayEventClass(t *testing.T) {
|
||||
name string
|
||||
class string
|
||||
}{
|
||||
{"LaunchInstance", "instance"},
|
||||
{"LaunchInstance", ""}, // 已移出回传清单,不归类
|
||||
{"TerminateInstance", "instance"},
|
||||
{"InstanceAction", "instance"},
|
||||
{"CreateUser", "identity"},
|
||||
|
||||
Reference in New Issue
Block a user