mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 10:00:06 +00:00
Feat: Consolidate Health & Status and Pass Status Context Data (#6860)
Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
@@ -120,15 +120,6 @@ func (comp *Component) EvalStatus(templateContext map[string]interface{}) (*heal
|
||||
return comp.engine.Status(templateContext, comp.FullTemplate.AsStatusRequest(comp.Params))
|
||||
}
|
||||
|
||||
// EvalHealth eval workload health check
|
||||
func (comp *Component) EvalHealth(templateContext map[string]interface{}) (bool, error) {
|
||||
// if the health of template is not set or standard workload is managed by trait always return true
|
||||
if comp.SkipApplyWorkload {
|
||||
return true, nil
|
||||
}
|
||||
return comp.engine.HealthCheck(templateContext, comp.FullTemplate.Health, comp.Params)
|
||||
}
|
||||
|
||||
// Trait is ComponentTrait
|
||||
type Trait struct {
|
||||
// The Name is name of TraitDefinition, actually it's a type of the trait instance
|
||||
@@ -137,7 +128,6 @@ type Trait struct {
|
||||
Params map[string]interface{}
|
||||
|
||||
Template string
|
||||
HealthCheckPolicy string
|
||||
CustomStatusFormat string
|
||||
|
||||
// RequiredSecrets stores secret names which the trait needs from cloud resource component and its context
|
||||
@@ -161,16 +151,11 @@ func (trait *Trait) GetTemplateContext(ctx process.Context, client client.Client
|
||||
return templateContext, err
|
||||
}
|
||||
|
||||
// EvalStatus eval trait status
|
||||
// EvalStatus eval trait status (including health)
|
||||
func (trait *Trait) EvalStatus(templateContext map[string]interface{}) (*health.StatusResult, error) {
|
||||
return trait.engine.Status(templateContext, trait.FullTemplate.AsStatusRequest(trait.Params))
|
||||
}
|
||||
|
||||
// EvalHealth eval trait health check
|
||||
func (trait *Trait) EvalHealth(templateContext map[string]interface{}) (bool, error) {
|
||||
return trait.engine.HealthCheck(templateContext, trait.HealthCheckPolicy, trait.Params)
|
||||
}
|
||||
|
||||
// Appfile describes application
|
||||
type Appfile struct {
|
||||
Name string
|
||||
|
||||
@@ -704,7 +704,6 @@ func (p *Parser) convertTemplate2Trait(name string, properties map[string]interf
|
||||
CapabilityCategory: templ.CapabilityCategory,
|
||||
Params: properties,
|
||||
Template: templ.TemplateStr,
|
||||
HealthCheckPolicy: templ.Health,
|
||||
CustomStatusFormat: templ.CustomStatus,
|
||||
FullTemplate: templ,
|
||||
engine: definition.NewTraitAbstractEngine(traitName),
|
||||
|
||||
@@ -405,6 +405,7 @@ func ConvertTemplateJSON2Object(capabilityName string, in *runtime.RawExtension,
|
||||
|
||||
func (t *Template) AsStatusRequest(parameter map[string]interface{}) *health.StatusRequest {
|
||||
return &health.StatusRequest{
|
||||
Health: t.Health,
|
||||
Custom: t.CustomStatus,
|
||||
Details: t.Details,
|
||||
Parameter: parameter,
|
||||
|
||||
@@ -245,14 +245,12 @@ func (h *AppHandler) collectTraitHealthStatus(comp *appfile.Component, tr *appfi
|
||||
if err != nil {
|
||||
return common.ApplicationTraitStatus{}, nil, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, get template context error", appName, comp.Name, tr.Name)
|
||||
}
|
||||
if ok, err := tr.EvalHealth(templateContext); !ok || err != nil {
|
||||
traitStatus.Healthy = false
|
||||
}
|
||||
if err != nil {
|
||||
return common.ApplicationTraitStatus{}, nil, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appName, comp.Name, tr.Name)
|
||||
}
|
||||
statusResult, err := tr.EvalStatus(templateContext)
|
||||
if err == nil && statusResult != nil {
|
||||
traitStatus.Healthy = statusResult.Healthy
|
||||
traitStatus.Message = statusResult.Message
|
||||
traitStatus.Details = statusResult.Details
|
||||
}
|
||||
@@ -264,9 +262,8 @@ func (h *AppHandler) collectWorkloadHealthStatus(ctx context.Context, comp *appf
|
||||
var output *unstructured.Unstructured
|
||||
var outputs []*unstructured.Unstructured
|
||||
var (
|
||||
appRev = h.currentAppRev
|
||||
appName = appRev.Spec.Application.Name
|
||||
isHealth = true
|
||||
appRev = h.currentAppRev
|
||||
appName = appRev.Spec.Application.Name
|
||||
)
|
||||
if comp.CapabilityCategory == types.TerraformCategory {
|
||||
var configuration terraforv1beta2.Configuration
|
||||
@@ -276,13 +273,13 @@ func (h *AppHandler) collectWorkloadHealthStatus(ctx context.Context, comp *appf
|
||||
if err := h.Client.Get(ctx, client.ObjectKey{Name: comp.Name, Namespace: accessor.Namespace()}, &legacyConfiguration); err != nil {
|
||||
return false, nil, nil, errors.WithMessagef(err, "app=%s, comp=%s, check health error", appName, comp.Name)
|
||||
}
|
||||
isHealth = setStatus(status, legacyConfiguration.Status.ObservedGeneration, legacyConfiguration.Generation,
|
||||
setStatus(status, legacyConfiguration.Status.ObservedGeneration, legacyConfiguration.Generation,
|
||||
legacyConfiguration.GetLabels(), appRev.Name, legacyConfiguration.Status.Apply.State, legacyConfiguration.Status.Apply.Message)
|
||||
} else {
|
||||
return false, nil, nil, errors.WithMessagef(err, "app=%s, comp=%s, check health error", appName, comp.Name)
|
||||
}
|
||||
} else {
|
||||
isHealth = setStatus(status, configuration.Status.ObservedGeneration, configuration.Generation, configuration.GetLabels(),
|
||||
setStatus(status, configuration.Status.ObservedGeneration, configuration.Generation, configuration.GetLabels(),
|
||||
appRev.Name, configuration.Status.Apply.State, configuration.Status.Apply.Message)
|
||||
}
|
||||
} else {
|
||||
@@ -290,23 +287,24 @@ func (h *AppHandler) collectWorkloadHealthStatus(ctx context.Context, comp *appf
|
||||
if err != nil {
|
||||
return false, nil, nil, errors.WithMessagef(err, "app=%s, comp=%s, get template context error", appName, comp.Name)
|
||||
}
|
||||
if ok, err := comp.EvalHealth(templateContext); !ok || err != nil {
|
||||
isHealth = false
|
||||
}
|
||||
status.Healthy = isHealth
|
||||
statusResult, err := comp.EvalStatus(templateContext)
|
||||
if statusResult.Message != "" {
|
||||
status.Message = statusResult.Message
|
||||
}
|
||||
if statusResult.Details != nil {
|
||||
status.Details = statusResult.Details
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, nil, errors.WithMessagef(err, "app=%s, comp=%s, evaluate workload status message error", appName, comp.Name)
|
||||
}
|
||||
if statusResult != nil {
|
||||
status.Healthy = statusResult.Healthy
|
||||
if statusResult.Message != "" {
|
||||
status.Message = statusResult.Message
|
||||
}
|
||||
if statusResult.Details != nil {
|
||||
status.Details = statusResult.Details
|
||||
}
|
||||
} else {
|
||||
status.Healthy = false
|
||||
}
|
||||
output, outputs = extractOutputAndOutputs(templateContext)
|
||||
}
|
||||
return isHealth, output, outputs, nil
|
||||
return status.Healthy, output, outputs, nil
|
||||
}
|
||||
|
||||
// nolint
|
||||
@@ -372,7 +370,7 @@ collectNext:
|
||||
}
|
||||
|
||||
func setStatus(status *common.ApplicationComponentStatus, observedGeneration, generation int64, labels map[string]string,
|
||||
appRevName string, state terraformtypes.ConfigurationState, message string) bool {
|
||||
appRevName string, state terraformtypes.ConfigurationState, message string) {
|
||||
isLatest := func() bool {
|
||||
if observedGeneration != 0 && observedGeneration != generation {
|
||||
return false
|
||||
@@ -388,10 +386,9 @@ func setStatus(status *common.ApplicationComponentStatus, observedGeneration, ge
|
||||
status.Message = message
|
||||
if !isLatest() || state != terraformtypes.Available {
|
||||
status.Healthy = false
|
||||
return false
|
||||
return
|
||||
}
|
||||
status.Healthy = true
|
||||
return true
|
||||
}
|
||||
|
||||
// ApplyPolicies will render policies into manifests from appfile and dispatch them
|
||||
|
||||
@@ -35,12 +35,14 @@ const (
|
||||
)
|
||||
|
||||
type StatusRequest struct {
|
||||
Health string
|
||||
Custom string
|
||||
Details string
|
||||
Parameter map[string]interface{}
|
||||
}
|
||||
|
||||
type StatusResult struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
@@ -64,16 +66,33 @@ func CheckHealth(templateContext map[string]interface{}, healthPolicyTemplate st
|
||||
}
|
||||
|
||||
func GetStatus(templateContext map[string]interface{}, request *StatusRequest) (*StatusResult, error) {
|
||||
message, msgErr := getStatusMessage(templateContext, request.Custom, request.Parameter)
|
||||
if msgErr != nil {
|
||||
klog.Warningf("failed to get status message: %v", msgErr)
|
||||
if templateContext["status"] == nil {
|
||||
templateContext["status"] = make(map[string]interface{})
|
||||
}
|
||||
statusMap, mapErr := getStatusMap(templateContext, request.Details, request.Parameter)
|
||||
|
||||
templateContext, statusMap, mapErr := getStatusMap(templateContext, request.Details, request.Parameter)
|
||||
if mapErr != nil {
|
||||
klog.Warningf("failed to get status map: %v", mapErr)
|
||||
}
|
||||
|
||||
healthy, healthErr := CheckHealth(templateContext, request.Health, request.Parameter)
|
||||
if healthErr != nil {
|
||||
klog.Warningf("failed to check health: %v", healthErr)
|
||||
}
|
||||
|
||||
if statusMap, ok := templateContext["status"].(map[string]interface{}); ok {
|
||||
statusMap["healthy"] = healthy
|
||||
} else {
|
||||
klog.Warningf("templateContext['status'] is not a map[string]interface{}, cannot set healthy field")
|
||||
}
|
||||
|
||||
message, msgErr := getStatusMessage(templateContext, request.Custom, request.Parameter)
|
||||
if msgErr != nil {
|
||||
klog.Warningf("failed to get status message: %v", msgErr)
|
||||
}
|
||||
|
||||
return &StatusResult{
|
||||
Healthy: healthy,
|
||||
Message: message,
|
||||
Details: statusMap,
|
||||
}, nil
|
||||
@@ -100,16 +119,20 @@ func getStatusMessage(templateContext map[string]interface{}, customStatusTempla
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func getStatusMap(templateContext map[string]interface{}, statusFields string, parameter interface{}) (map[string]string, error) {
|
||||
func getStatusMap(templateContext map[string]interface{}, statusFields string, parameter interface{}) (map[string]interface{}, map[string]string, error) {
|
||||
status := make(map[string]string)
|
||||
|
||||
if templateContext["status"] == nil {
|
||||
templateContext["status"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if statusFields == "" {
|
||||
return status, nil
|
||||
return templateContext, status, nil
|
||||
}
|
||||
|
||||
runtimeContextBuff, err := formatRuntimeContext(templateContext, parameter)
|
||||
if err != nil {
|
||||
return status, errors.WithMessage(err, "format runtime context")
|
||||
return templateContext, status, errors.WithMessage(err, "format runtime context")
|
||||
}
|
||||
cueCtx := cuecontext.New()
|
||||
|
||||
@@ -117,7 +140,7 @@ func getStatusMap(templateContext map[string]interface{}, statusFields string, p
|
||||
contextVal := cueCtx.CompileString(runtimeContextBuff)
|
||||
iter, err := contextVal.Fields(cue.All())
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "get context fields")
|
||||
return templateContext, nil, errors.WithMessage(err, "get context fields")
|
||||
}
|
||||
for iter.Next() {
|
||||
contextLabels = append(contextLabels, iter.Label())
|
||||
@@ -126,13 +149,15 @@ func getStatusMap(templateContext map[string]interface{}, statusFields string, p
|
||||
cueBuffer := runtimeContextBuff + "\n" + statusFields
|
||||
val := cueCtx.CompileString(cueBuffer)
|
||||
if val.Err() != nil {
|
||||
return nil, errors.WithMessage(val.Err(), "compile status fields template")
|
||||
return templateContext, nil, errors.WithMessage(val.Err(), "compile status fields template")
|
||||
}
|
||||
iter, err = val.Fields()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "get status fields")
|
||||
return templateContext, nil, errors.WithMessage(err, "get status fields")
|
||||
}
|
||||
|
||||
detailsMap := make(map[string]interface{})
|
||||
|
||||
outer:
|
||||
for iter.Next() {
|
||||
label := iter.Label()
|
||||
@@ -142,22 +167,43 @@ outer:
|
||||
continue // Skip labels that are too long
|
||||
}
|
||||
|
||||
if strings.HasPrefix(label, "$") {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(contextLabels, label) {
|
||||
continue // Skip fields that are already in the context
|
||||
}
|
||||
|
||||
v := iter.Value()
|
||||
|
||||
// Check if field should be excluded via attributes
|
||||
shouldExclude := false
|
||||
for _, a := range v.Attributes(cue.FieldAttr) {
|
||||
if a.Name() == "local" || a.Name() == "exclude" {
|
||||
continue outer
|
||||
if a.Name() == "local" || a.Name() == "private" {
|
||||
shouldExclude = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// For $ fields, include in context but not in status map
|
||||
if strings.HasPrefix(label, "$") {
|
||||
if err = v.Value().Validate(cue.Concrete(true)); err == nil {
|
||||
var nonStringValue interface{}
|
||||
if err := v.Value().Decode(&nonStringValue); err == nil {
|
||||
detailsMap[label] = nonStringValue
|
||||
}
|
||||
}
|
||||
continue // Skip adding to status map
|
||||
}
|
||||
|
||||
// Skip excluded fields entirely
|
||||
if shouldExclude {
|
||||
continue outer
|
||||
}
|
||||
|
||||
if err = v.Value().Validate(cue.Concrete(true)); err == nil {
|
||||
var nonStringValue interface{}
|
||||
if err := v.Value().Decode(&nonStringValue); err == nil {
|
||||
detailsMap[label] = nonStringValue
|
||||
}
|
||||
|
||||
if v.Value().IncompleteKind() == cue.StringKind {
|
||||
status[label], _ = v.Value().String()
|
||||
continue
|
||||
@@ -165,14 +211,21 @@ outer:
|
||||
node := v.Value().Syntax(cue.Final())
|
||||
b, err := format.Node(node)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "format status field %s", label)
|
||||
return templateContext, nil, errors.WithMessagef(err, "format status field %s", label)
|
||||
}
|
||||
status[label] = string(b)
|
||||
} else {
|
||||
status[label] = cue.BottomKind.String() // Use a default value for invalid fields
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
|
||||
if statusContext, ok := templateContext["status"].(map[string]interface{}); ok {
|
||||
statusContext["details"] = detailsMap
|
||||
} else {
|
||||
klog.Warningf("templateContext['status'] is not a map[string]interface{}, cannot store details")
|
||||
}
|
||||
|
||||
return templateContext, status, nil
|
||||
}
|
||||
|
||||
func formatRuntimeContext(templateContext map[string]interface{}, parameter interface{}) (string, error) {
|
||||
|
||||
@@ -374,7 +374,7 @@ func TestGetStatus(t *testing.T) {
|
||||
parameter: make(map[string]interface{}),
|
||||
statusCue: strings.TrimSpace(`
|
||||
a: 1 @local()
|
||||
b: 2 @exclude()
|
||||
b: 2 @private()
|
||||
c: a + b
|
||||
`),
|
||||
expStatus: map[string]string{
|
||||
@@ -402,7 +402,7 @@ func TestGetStatus(t *testing.T) {
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
status, err := getStatusMap(tc.tpContext, tc.statusCue, tc.parameter)
|
||||
_, status, err := getStatusMap(tc.tpContext, tc.statusCue, tc.parameter)
|
||||
if !tc.expErr {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -410,3 +410,374 @@ func TestGetStatus(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextPassing(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
initialCtx map[string]interface{}
|
||||
request StatusRequest
|
||||
expMessage string
|
||||
expDetails map[string]string
|
||||
validateCtx func(t *testing.T, ctx map[string]interface{})
|
||||
}{
|
||||
"basic-context-passing": {
|
||||
initialCtx: map[string]interface{}{},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
stringValue: "example"
|
||||
intValue: 1 + 2
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "\(context.status.details.stringValue) \(context.status.details.intValue)"
|
||||
`),
|
||||
},
|
||||
expMessage: "example 3",
|
||||
expDetails: map[string]string{
|
||||
"stringValue": "example",
|
||||
"intValue": "3",
|
||||
},
|
||||
},
|
||||
"complex-types-in-context": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"outputs": map[string]interface{}{
|
||||
"service": map[string]interface{}{
|
||||
"port": 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{
|
||||
"replicas": 3,
|
||||
},
|
||||
Details: strings.TrimSpace(`
|
||||
replicas: parameter.replicas
|
||||
port: context.outputs.service.port
|
||||
isReady: parameter.replicas > 0 && context.outputs.service.port > 0
|
||||
config: {
|
||||
enabled: true
|
||||
timeout: 30
|
||||
} @private()
|
||||
configEnabled: config.enabled
|
||||
configTimeout: config.timeout
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Service on port \(context.status.details.port) with \(context.status.details.replicas) replicas is ready: \(context.status.details.isReady)"
|
||||
`),
|
||||
},
|
||||
expMessage: "Service on port 8080 with 3 replicas is ready: true",
|
||||
expDetails: map[string]string{
|
||||
"replicas": "3",
|
||||
"port": "8080",
|
||||
"isReady": "true",
|
||||
"configEnabled": "true",
|
||||
"configTimeout": "30",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
|
||||
assert.Equal(t, 3, details["replicas"])
|
||||
assert.Equal(t, 8080, details["port"])
|
||||
assert.Equal(t, true, details["isReady"])
|
||||
assert.Equal(t, true, details["configEnabled"])
|
||||
assert.Equal(t, 30, details["configTimeout"])
|
||||
|
||||
assert.Nil(t, details["config"])
|
||||
},
|
||||
},
|
||||
"array-handling-in-context": {
|
||||
initialCtx: map[string]interface{}{},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
$ports: [80, 443, 8080]
|
||||
$protocols: ["http", "https", "http"]
|
||||
$mappings: [
|
||||
{port: 80, protocol: "http"},
|
||||
{port: 443, protocol: "https"}
|
||||
]
|
||||
portCount: len($ports)
|
||||
firstPort: $ports[0]
|
||||
mainProtocol: $protocols[0]
|
||||
portsString: "80,443,8080"
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Serving on \(len(context.status.details.$ports)) ports"
|
||||
`),
|
||||
},
|
||||
expMessage: "Serving on 3 ports",
|
||||
expDetails: map[string]string{
|
||||
"portCount": "3",
|
||||
"firstPort": "80",
|
||||
"mainProtocol": "http",
|
||||
"portsString": "80,443,8080",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
|
||||
ports := details["$ports"].([]interface{})
|
||||
assert.Len(t, ports, 3)
|
||||
assert.Equal(t, 80, ports[0])
|
||||
assert.Equal(t, 443, ports[1])
|
||||
assert.Equal(t, 8080, ports[2])
|
||||
|
||||
protocols := details["$protocols"].([]interface{})
|
||||
assert.Len(t, protocols, 3)
|
||||
assert.Equal(t, "http", protocols[0])
|
||||
assert.Equal(t, "https", protocols[1])
|
||||
|
||||
mappings := details["$mappings"].([]interface{})
|
||||
assert.Len(t, mappings, 2)
|
||||
|
||||
assert.Equal(t, 3, details["portCount"])
|
||||
assert.Equal(t, 80, details["firstPort"])
|
||||
assert.Equal(t, "http", details["mainProtocol"])
|
||||
assert.Equal(t, "80,443,8080", details["portsString"])
|
||||
},
|
||||
},
|
||||
"nested-references": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"appName": "my-app",
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{
|
||||
"env": "production",
|
||||
},
|
||||
Details: strings.TrimSpace(`
|
||||
environment: parameter.env
|
||||
$appInfo: {
|
||||
name: context.appName
|
||||
env: parameter.env
|
||||
fullName: "\(context.appName)-\(parameter.env)"
|
||||
}
|
||||
appName: $appInfo.name
|
||||
appEnv: $appInfo.env
|
||||
appFullName: $appInfo.fullName
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Deployed \(context.status.details.$appInfo.fullName) to \(context.status.details.environment)"
|
||||
`),
|
||||
},
|
||||
expMessage: "Deployed my-app-production to production",
|
||||
expDetails: map[string]string{
|
||||
"environment": "production",
|
||||
"appName": "my-app",
|
||||
"appEnv": "production",
|
||||
"appFullName": "my-app-production",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
|
||||
appInfo := details["$appInfo"].(map[string]interface{})
|
||||
assert.Equal(t, "my-app", appInfo["name"])
|
||||
assert.Equal(t, "production", appInfo["env"])
|
||||
assert.Equal(t, "my-app-production", appInfo["fullName"])
|
||||
|
||||
assert.Equal(t, "production", details["environment"])
|
||||
assert.Equal(t, "my-app", details["appName"])
|
||||
assert.Equal(t, "production", details["appEnv"])
|
||||
assert.Equal(t, "my-app-production", details["appFullName"])
|
||||
},
|
||||
},
|
||||
"existing-status-preserved": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"existingField": "should-be-preserved",
|
||||
},
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
newField: "added-value"
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Status has existing: \(context.status.existingField)"
|
||||
`),
|
||||
},
|
||||
expMessage: "Status has existing: should-be-preserved",
|
||||
expDetails: map[string]string{
|
||||
"newField": "added-value",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
assert.Equal(t, "should-be-preserved", statusCtx["existingField"])
|
||||
assert.NotNil(t, statusCtx["details"])
|
||||
},
|
||||
},
|
||||
"dollar-fields-in-context-only": {
|
||||
initialCtx: map[string]interface{}{},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{
|
||||
"baseValue": 10,
|
||||
},
|
||||
Details: strings.TrimSpace(`
|
||||
$multiplier: 2
|
||||
$offset: 5
|
||||
result: parameter.baseValue * $multiplier + $offset
|
||||
displayText: "Result is \(result)"
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Computed using multiplier \(context.status.details.$multiplier) and offset \(context.status.details.$offset)"
|
||||
`),
|
||||
},
|
||||
expMessage: "Computed using multiplier 2 and offset 5",
|
||||
expDetails: map[string]string{
|
||||
"result": "25",
|
||||
"displayText": "Result is 25",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
|
||||
assert.Equal(t, 2, details["$multiplier"])
|
||||
assert.Equal(t, 5, details["$offset"])
|
||||
assert.Equal(t, 25, details["result"])
|
||||
assert.Equal(t, "Result is 25", details["displayText"])
|
||||
},
|
||||
},
|
||||
"health-check-references-status-details": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"output": map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"replicas": 5,
|
||||
"readyReplicas": 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
replicas: context.output.status.replicas
|
||||
readyReplicas: context.output.status.readyReplicas
|
||||
percentReady: "\(readyReplicas * 100 / replicas)%"
|
||||
`),
|
||||
Health: strings.TrimSpace(`
|
||||
isHealth: context.status.details.replicas == context.status.details.readyReplicas
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
message: "Deployment status: \(context.status.details.percentReady) ready"
|
||||
`),
|
||||
},
|
||||
expMessage: "Deployment status: 60% ready",
|
||||
expDetails: map[string]string{
|
||||
"replicas": "5",
|
||||
"readyReplicas": "3",
|
||||
"percentReady": "60%",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
assert.Equal(t, false, statusCtx["healthy"])
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
assert.Equal(t, 5, details["replicas"])
|
||||
assert.Equal(t, 3, details["readyReplicas"])
|
||||
},
|
||||
},
|
||||
"message-references-health-and-details": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"output": map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"phase": "Running",
|
||||
"replicas": 3,
|
||||
"readyReplicas": 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
phase: context.output.status.phase
|
||||
replicas: context.output.status.replicas
|
||||
readyReplicas: context.output.status.readyReplicas
|
||||
`),
|
||||
Health: strings.TrimSpace(`
|
||||
isHealth: context.status.details.phase == "Running" && context.status.details.readyReplicas == context.status.details.replicas
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
if context.status.healthy {
|
||||
message: "Deployment is healthy: \(context.status.details.readyReplicas)/\(context.status.details.replicas) replicas ready"
|
||||
}
|
||||
if !context.status.healthy {
|
||||
message: "Deployment unhealthy: \(context.status.details.readyReplicas)/\(context.status.details.replicas) replicas ready"
|
||||
}
|
||||
`),
|
||||
},
|
||||
expMessage: "Deployment is healthy: 3/3 replicas ready",
|
||||
expDetails: map[string]string{
|
||||
"phase": "Running",
|
||||
"replicas": "3",
|
||||
"readyReplicas": "3",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
assert.Equal(t, true, statusCtx["healthy"])
|
||||
},
|
||||
},
|
||||
"complex-health-with-computed-details": {
|
||||
initialCtx: map[string]interface{}{
|
||||
"output": map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"capacity": 100,
|
||||
"used": 85,
|
||||
"threshold": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
request: StatusRequest{
|
||||
Parameter: map[string]interface{}{},
|
||||
Details: strings.TrimSpace(`
|
||||
capacity: context.output.status.capacity
|
||||
used: context.output.status.used
|
||||
threshold: context.output.status.threshold
|
||||
$utilization: used * 100.0 / capacity
|
||||
utilizationPercent: "\($utilization)%"
|
||||
$overThreshold: $utilization > threshold
|
||||
`),
|
||||
Health: strings.TrimSpace(`
|
||||
isHealth: !context.status.details.$overThreshold
|
||||
`),
|
||||
Custom: strings.TrimSpace(`
|
||||
if context.status.healthy {
|
||||
message: "Resource usage OK at \(context.status.details.utilizationPercent)"
|
||||
}
|
||||
if !context.status.healthy {
|
||||
message: "Resource usage HIGH at \(context.status.details.utilizationPercent) (threshold: \(context.status.details.threshold)%)"
|
||||
}
|
||||
`),
|
||||
},
|
||||
expMessage: "Resource usage HIGH at 85.0% (threshold: 80%)",
|
||||
expDetails: map[string]string{
|
||||
"capacity": "100",
|
||||
"used": "85",
|
||||
"threshold": "80",
|
||||
"utilizationPercent": "85.0%",
|
||||
},
|
||||
validateCtx: func(t *testing.T, ctx map[string]interface{}) {
|
||||
statusCtx := ctx["status"].(map[string]interface{})
|
||||
assert.Equal(t, false, statusCtx["healthy"])
|
||||
details := statusCtx["details"].(map[string]interface{})
|
||||
assert.Equal(t, float64(85), details["$utilization"])
|
||||
assert.Equal(t, true, details["$overThreshold"])
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := make(map[string]interface{})
|
||||
for k, v := range tc.initialCtx {
|
||||
ctx[k] = v
|
||||
}
|
||||
|
||||
result, err := GetStatus(ctx, &tc.request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expMessage, result.Message)
|
||||
assert.Equal(t, tc.expDetails, result.Details)
|
||||
|
||||
if tc.validateCtx != nil {
|
||||
tc.validateCtx(t, ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ const (
|
||||
// AbstractEngine defines Definition's Render interface
|
||||
type AbstractEngine interface {
|
||||
Complete(ctx process.Context, abstractTemplate string, params interface{}) error
|
||||
HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error)
|
||||
Status(templateContext map[string]interface{}, request *health.StatusRequest) (*health.StatusResult, error)
|
||||
GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error)
|
||||
}
|
||||
@@ -205,11 +204,6 @@ func (wd *workloadDef) getTemplateContext(ctx process.Context, cli client.Reader
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// HealthCheck address health check for workload
|
||||
func (wd *workloadDef) HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error) {
|
||||
return health.CheckHealth(templateContext, healthPolicyTemplate, parameter)
|
||||
}
|
||||
|
||||
// Status get workload status by customStatusTemplate
|
||||
func (wd *workloadDef) Status(templateContext map[string]interface{}, request *health.StatusRequest) (*health.StatusResult, error) {
|
||||
return health.GetStatus(templateContext, request)
|
||||
@@ -410,11 +404,6 @@ func (td *traitDef) Status(templateContext map[string]interface{}, request *heal
|
||||
return health.GetStatus(templateContext, request)
|
||||
}
|
||||
|
||||
// HealthCheck address health check for trait
|
||||
func (td *traitDef) HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error) {
|
||||
return health.CheckHealth(templateContext, healthPolicyTemplate, parameter)
|
||||
}
|
||||
|
||||
func (td *traitDef) GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
|
||||
return td.getTemplateContext(ctx, cli, accessor)
|
||||
}
|
||||
|
||||
@@ -183,6 +183,9 @@ func TrimCueRawString(s string) string {
|
||||
}
|
||||
|
||||
// Handle escape sequences for backward compatibility with existing definitions
|
||||
// For quoted strings (after strconv.Unquote): replace actual tab characters
|
||||
s = strings.ReplaceAll(s, "\t", " ")
|
||||
// For raw strings (not unquoted): replace literal \t
|
||||
s = strings.ReplaceAll(s, "\\t", " ")
|
||||
s = strings.ReplaceAll(s, "\\\\", "\\")
|
||||
|
||||
|
||||
@@ -411,6 +411,28 @@ func TestTrimCueRawString(t *testing.T) {
|
||||
input: `"unterminated`,
|
||||
expected: `"unterminated`,
|
||||
},
|
||||
{
|
||||
name: "raw string with tab character",
|
||||
input: `#"""hello\tworld"""#`,
|
||||
expected: `hello world`,
|
||||
},
|
||||
{
|
||||
name: "quoted string with escaped tab",
|
||||
input: `"hello\tworld"`,
|
||||
expected: `hello world`,
|
||||
},
|
||||
{
|
||||
name: "raw string with multiple tabs",
|
||||
input: `#"""line1\t\tvalue
|
||||
line2\t\tvalue"""#`,
|
||||
expected: `line1 value
|
||||
line2 value`,
|
||||
},
|
||||
{
|
||||
name: "triple quoted string with tabs",
|
||||
input: `"""hello\tworld"""`,
|
||||
expected: `hello world`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -506,13 +506,25 @@ var _ = Describe("Application Normal tests", func() {
|
||||
By("Checking the initial application status")
|
||||
Expect(app.Status.Services).ShouldNot(BeEmpty())
|
||||
Expect(app.Status.Services[0].Healthy).Should(BeFalse())
|
||||
Expect(app.Status.Services[0].Message).Should(BeEmpty())
|
||||
Expect(app.Status.Services[0].Message).Should(Equal(fmt.Sprintf("Unhealthy - 0 / %d replicas are ready", compReplicas)))
|
||||
Expect(app.Status.Services[0].Details["readyReplicas"]).Should(Equal("0"))
|
||||
Expect(app.Status.Services[0].Details["deploymentReady"]).Should(Equal("false"))
|
||||
|
||||
verifyWorkloadRunningExpected(ctx, namespaceName, compDef.Name, int32(compReplicas), compImage)
|
||||
verifyWorkloadRunningExpected(ctx, namespaceName, traitDef.Name, int32(traitReplicas), traitImage)
|
||||
|
||||
By("Triggering application reconciliation to ensure status is updated (to avoid flakiness)")
|
||||
Eventually(func() error {
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Annotations == nil {
|
||||
app.Annotations = make(map[string]string)
|
||||
}
|
||||
app.Annotations["force.reconcile"] = fmt.Sprintf("%d", time.Now().Unix())
|
||||
return k8sClient.Update(ctx, app)
|
||||
}, 10*time.Second, 500*time.Millisecond).Should(Succeed())
|
||||
|
||||
By("Waiting for the app to turn healthy")
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, client.ObjectKey{
|
||||
@@ -530,13 +542,13 @@ var _ = Describe("Application Normal tests", func() {
|
||||
|
||||
By("Checking the component status matches expectations")
|
||||
Expect(app.Status.Services[0].Healthy).Should(BeTrue())
|
||||
Expect(app.Status.Services[0].Message).Should(Equal(fmt.Sprintf("%v / %v replicas are ready", compReplicas, compReplicas)))
|
||||
Expect(app.Status.Services[0].Message).Should(Equal(fmt.Sprintf("Healthy - %v / %v replicas are ready", compReplicas, compReplicas)))
|
||||
Expect(app.Status.Services[0].Details["readyReplicas"]).Should(Equal(fmt.Sprintf("%v", compReplicas)))
|
||||
Expect(app.Status.Services[0].Details["deploymentReady"]).Should(Equal("true"))
|
||||
|
||||
By("Checking the trait status matches expectations")
|
||||
Expect(app.Status.Services[0].Traits[0].Healthy).Should(BeTrue())
|
||||
Expect(app.Status.Services[0].Traits[0].Message).Should(Equal(fmt.Sprintf("%v / %v replicas are ready", traitReplicas, traitReplicas)))
|
||||
Expect(app.Status.Services[0].Traits[0].Message).Should(Equal(fmt.Sprintf("Healthy - %v / %v replicas are ready", traitReplicas, traitReplicas)))
|
||||
Expect(app.Status.Services[0].Traits[0].Details["allReplicasReady"]).Should(Equal("true"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,13 +40,19 @@ spec:
|
||||
}
|
||||
}
|
||||
status:
|
||||
customStatus: |
|
||||
message: "\(context.output.status.readyReplicas) / \(context.output.status.replicas) replicas are ready"
|
||||
healthPolicy: |
|
||||
isHealth: context.output.status.replicas == context.output.status.readyReplicas
|
||||
details: |
|
||||
deploymentReady: *(context.output.status.replicas == context.output.status.readyReplicas) | false
|
||||
$expectedReplicas: context.output.spec.replicas
|
||||
readyReplicas: *context.output.status.readyReplicas | 0
|
||||
healthPolicy: |
|
||||
isHealth: context.status.details.readyReplicas == context.status.details.$expectedReplicas
|
||||
customStatus: |
|
||||
if context.status.healthy {
|
||||
message: "Healthy - \(context.status.details.readyReplicas) / \(context.status.details.$expectedReplicas) replicas are ready"
|
||||
}
|
||||
if !context.status.healthy {
|
||||
message: "Unhealthy - \(context.status.details.readyReplicas) / \(context.status.details.$expectedReplicas) replicas are ready"
|
||||
}
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
|
||||
@@ -45,10 +45,18 @@ spec:
|
||||
}
|
||||
}
|
||||
status:
|
||||
customStatus: |
|
||||
message: "\(context.outputs.deployment.status.readyReplicas) / \(context.outputs.deployment.status.replicas) replicas are ready"
|
||||
healthPolicy: |
|
||||
isHealth: context.outputs.deployment.status.replicas == context.outputs.deployment.status.readyReplicas
|
||||
details: |
|
||||
allReplicasReady: *(context.outputs.deployment.status.replicas == context.outputs.deployment.status.readyReplicas) | false
|
||||
deploymentReady: *(context.outputs.deployment.status.replicas == context.outputs.deployment.status.readyReplicas) | false
|
||||
$expectedReplicas: context.outputs.deployment.spec.replicas
|
||||
readyReplicas: *context.outputs.deployment.status.readyReplicas | 0
|
||||
allReplicasReady: readyReplicas == $expectedReplicas
|
||||
healthPolicy: |
|
||||
isHealth: context.status.details.readyReplicas == context.status.details.$expectedReplicas
|
||||
customStatus: |
|
||||
if context.status.healthy {
|
||||
message: "Healthy - \(context.status.details.readyReplicas) / \(context.status.details.$expectedReplicas) replicas are ready"
|
||||
}
|
||||
if !context.status.healthy {
|
||||
message: "Unhealthy - \(context.status.details.readyReplicas) / \(context.status.details.$expectedReplicas) replicas are ready"
|
||||
}
|
||||
workloadRefPath: ""
|
||||
Reference in New Issue
Block a user