Feat: Consolidate Health & Status and Pass Status Context Data (#6860)

Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
Brian Kane
2025-08-23 19:30:06 +01:00
committed by GitHub
parent af1fb9a0fd
commit 56bc3b02e9
12 changed files with 528 additions and 82 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -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,

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
}
})
}
}

View File

@@ -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)
}

View File

@@ -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, "\\\\", "\\")

View File

@@ -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 {

View File

@@ -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"))
})
})

View File

@@ -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

View File

@@ -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: ""