Skip to content

Commit

Permalink
feat: add KCL based customized health check
Browse files Browse the repository at this point in the history
  • Loading branch information
Yangyang96 committed Aug 19, 2024
1 parent f2ee452 commit 7d3e180
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 5 deletions.
2 changes: 2 additions & 0 deletions pkg/apis/api.kusion.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ const (
EnvViettelCloudProjectID = "VIETTEL_CLOUD_PROJECT_ID"

FieldImportedResources = "importedResources"
FieldHealthPolicy = "healthPolicy"
FieldKCLHealthCheckKCL = "health.kcl"
)

// BackendConfigs contains the configuration of multiple backends and the current backend.
Expand Down
24 changes: 22 additions & 2 deletions pkg/cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/kubectl/pkg/util/templates"

"gopkg.in/yaml.v3"

apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
v1 "kusionstack.io/kusion/pkg/apis/status/v1"
"kusionstack.io/kusion/pkg/cmd/generate"
Expand All @@ -48,6 +50,7 @@ import (
runtimeinit "kusionstack.io/kusion/pkg/engine/runtime/init"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/util/i18n"
"kusionstack.io/kusion/pkg/util/kcl"
"kusionstack.io/kusion/pkg/util/pretty"
"kusionstack.io/kusion/pkg/util/signal"
"kusionstack.io/kusion/pkg/util/terminal"
Expand Down Expand Up @@ -758,7 +761,7 @@ func Watch(

// Setup a go-routine to concurrently watch K8s and TF resources.
if res.Type == apiv1.Kubernetes {
go watchK8sResources(id, w.Watchers, table, tables, dryRun)
go watchK8sResources(id, w.Watchers, table, tables, dryRun, res.Extensions[apiv1.FieldHealthPolicy])
} else if res.Type == apiv1.Terraform {
go watchTFResources(id, w.TFWatcher, table, dryRun)
} else {
Expand Down Expand Up @@ -889,6 +892,7 @@ func watchK8sResources(
table *printers.Table,
tables map[string]*printers.Table,
dryRun bool,
healthPolicy interface{},
) {
defer func() {
var err error
Expand Down Expand Up @@ -930,7 +934,23 @@ func watchK8sResources(
} else {
// Restore to actual type
target := printers.Convert(o)
detail, ready = printers.Generate(target)

// Check reconcile status with customized health policy
if healthPolicy != nil {
if code, ok := kcl.ConvertKCLCode(healthPolicy); ok {
resByte, err := yaml.Marshal(o.Object)
if err != nil {
log.Error(err)
return
}
detail, ready = printers.PrintCustomizedHealthCheck(code, resByte)
} else {
detail, ready = printers.Generate(target)
}
// Check reconcile status with default setup
} else {
detail, ready = printers.Generate(target)
}
}

// Mark ready for breaking loop
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/apply/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ func TestWatchK8sResources(t *testing.T) {
id: table,
}

watchK8sResources(id, chs, table, tables, true)
watchK8sResources(id, chs, table, tables, true, nil)

assert.Equal(t, true, table.AllCompleted())
})
Expand Down
9 changes: 9 additions & 0 deletions pkg/engine/printers/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"

"kusionstack.io/kusion/pkg/engine/printers/printer"
"kusionstack.io/kusion/pkg/util/kcl"
)

var tg = printer.NewTableGenerator()
Expand All @@ -15,3 +16,11 @@ func init() {
func Generate(obj runtime.Object) (string, bool) {
return tg.GenerateTable(obj)
}

func PrintCustomizedHealthCheck(healthPolicyCode string, resource []byte) (string, bool) {
err := kcl.RunKCLHealthCheck(healthPolicyCode, resource)
if err != nil {
return "Reconciling...", false
}
return "Reconciled", true
}
22 changes: 20 additions & 2 deletions pkg/modules/generators/app_configurations_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func (g *appConfigurationGenerator) callModules(projectModuleConfigs map[string]
if err != nil {
return nil, nil, nil, err
}

healthPolicy := config.platformConfig[v1.FieldHealthPolicy]
// parse module result
// if only one resource exists in the workload module, it is the workload
if workloadKey == t && len(response.Resources) == 1 {
Expand All @@ -436,14 +436,21 @@ func (g *appConfigurationGenerator) callModules(projectModuleConfigs map[string]
if err != nil {
return nil, nil, nil, err
}
// Add healthPolicy to resource extensions
if healthPolicy != nil {
patchHealthPolicy(workload, healthPolicy)
}
} else {
for _, res := range response.Resources {
temp := &v1.Resource{}
err = yaml.Unmarshal(res, temp)
if err != nil {
return nil, nil, nil, err
}

// Add healthPolicy to resource extensions
if healthPolicy != nil {
patchHealthPolicy(workload, healthPolicy)
}
// filter out workload
if workloadKey == t && temp.Extensions[isWorkload] == "true" {
workload = temp
Expand Down Expand Up @@ -670,3 +677,14 @@ func patchImportedResources(resources v1.Resources, projectImportedResources map

return nil
}

// patchHealthPolicy patch the health policy to the `extensions` field of the resource in the Spec.
func patchHealthPolicy(resource *v1.Resource, healthPolicy any) {
healthPolicyMap := make(map[string]any)
if hp, ok := healthPolicy.(v1.GenericConfig); ok {
for k, v := range hp {
healthPolicyMap[k] = v
}
resource.Extensions[v1.FieldHealthPolicy] = healthPolicyMap
}
}
45 changes: 45 additions & 0 deletions pkg/util/kcl/kcl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kcl

import (
"fmt"

"kcl-lang.io/kcl-go/pkg/kcl"
"kcl-lang.io/kcl-go/pkg/tools/format"
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
)

const ref = "res"

// Assemble and format the whole KCL code with yaml integration.
func assembleKCLHealthCheck(healthPolicyCode string, resource []byte) (string, error) {
yamlStr := fmt.Sprintf(`
import yaml
%s = yaml.decode(%q)
`, ref, resource)
kclCode := yamlStr + healthPolicyCode
kclFormatted, err := format.FormatCode(kclCode)
if err != nil {
return "", err
}
return string(kclFormatted), nil
}

// Run health check with KCL health policy during apply.
func RunKCLHealthCheck(healthPolicyCode string, resource []byte) error {
kclCode, err := assembleKCLHealthCheck(healthPolicyCode, resource)
if err != nil {
return err
}
_, err = kcl.Run("", kcl.WithCode(kclCode))
return err
}

// Get KCL code from extensions of the resource in the Spec.
func ConvertKCLCode(healthPolicy any) (string, bool) {
if hp, ok := healthPolicy.(map[string]any); ok {
if code, ok := hp[v1.FieldKCLHealthCheckKCL].(string); ok {
return code, true
}
}
return "", false
}
102 changes: 102 additions & 0 deletions pkg/util/kcl/kcl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package kcl

import (
"testing"

v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
)

func TestAssembleKCLHealthCheck(t *testing.T) {
tests := []struct {
name string
hpCode string
resource []byte
want string
expectError bool
}{
{
name: "Valid input",
hpCode: "assert res.a == res.b",
resource: []byte("this is resource"),
want: "import yaml\n\nres = yaml.decode(\"this is resource\")\nassert res.a == res.b\n",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := assembleKCLHealthCheck(tt.hpCode, tt.resource)
if (err != nil) != tt.expectError {
t.Errorf("assembleKCLHealthCheck() error = %v, expectError %v", err, tt.expectError)
return
}
if got != tt.want {
t.Errorf("assembleKCLHealthCheck() got = %v, want %v", got, tt.want)
}
})
}
}

func TestRunKCLHealthCheck(t *testing.T) {
tests := []struct {
name string
hpCode string
resource []byte
expectError bool
}{
{
name: "Valid input",
hpCode: "a = \"this is health policy\"",
resource: []byte("this is resource"),
expectError: false,
},
// Add more test cases if needed
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := RunKCLHealthCheck(tt.hpCode, tt.resource)
if (err != nil) != tt.expectError {
t.Errorf("RunKCLHealthCheck() error = %v, expectError %v", err, tt.expectError)
}
})
}
}

func TestConvertKCLCode(t *testing.T) {
tests := []struct {
name string
healthPolicy any
want string
expectOk bool
}{
{
name: "Valid KCL code in health policy",
healthPolicy: map[string]any{
v1.FieldKCLHealthCheckKCL: "assert res.a == res.b",
},
want: "assert res.a == res.b",
expectOk: true,
},
{
name: "No KCL code in health policy",
healthPolicy: map[string]any{
"other_field": "other_value",
},
want: "",
expectOk: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ConvertKCLCode(tt.healthPolicy)
if got != tt.want {
t.Errorf("ConvertKCLCode() got = %v, want %v", got, tt.want)
}
if ok != tt.expectOk {
t.Errorf("ConvertKCLCode() ok = %v, expectOk %v", ok, tt.expectOk)
}
})
}
}

0 comments on commit 7d3e180

Please sign in to comment.