diff --git a/pkg/modules/generators/app_configurations_generator.go b/pkg/modules/generators/app_configurations_generator.go index 7186d52d..382fc43e 100644 --- a/pkg/modules/generators/app_configurations_generator.go +++ b/pkg/modules/generators/app_configurations_generator.go @@ -77,7 +77,7 @@ func (g *appConfigurationGenerator) Generate(i *apiv1.Intent) error { } // retrieve the module configs of the specified project - moduleConfigs, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project.Name) + modulesConfig, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project.Name) if err != nil { return err } @@ -86,8 +86,8 @@ func (g *appConfigurationGenerator) Generate(i *apiv1.Intent) error { gfs := []modules.NewGeneratorFunc{ NewNamespaceGeneratorFunc(g.project.Name, g.ws), accessories.NewDatabaseGeneratorFunc(g.project, g.stack, g.appName, g.app.Workload, g.app.Database), - workload.NewWorkloadGeneratorFunc(g.project, g.stack, g.appName, g.app.Workload, moduleConfigs), - trait.NewOpsRuleGeneratorFunc(g.project, g.stack, g.appName, g.app), + workload.NewWorkloadGeneratorFunc(g.project, g.stack, g.appName, g.app.Workload, modulesConfig), + trait.NewOpsRuleGeneratorFunc(g.project, g.stack, g.appName, g.app, modulesConfig), monitoring.NewMonitoringGeneratorFunc(g.project, g.app.Monitoring, g.appName), // The OrderedResourcesGenerator should be executed after all resources are generated. NewOrderedResourcesGeneratorFunc(), @@ -98,7 +98,7 @@ func (g *appConfigurationGenerator) Generate(i *apiv1.Intent) error { // Patcher logic patches generated resources pfs := []modules.NewPatcherFunc{ - pattrait.NewOpsRulePatcherFunc(g.app), + pattrait.NewOpsRulePatcherFunc(g.app, modulesConfig), patmonitoring.NewMonitoringPatcherFunc(g.appName, g.app, g.project), } if err := modules.CallPatchers(i.Resources.GVKIndex(), pfs...); err != nil { diff --git a/pkg/modules/generators/trait/ops_rule_generator.go b/pkg/modules/generators/trait/ops_rule_generator.go index a1f777b8..7dd04922 100644 --- a/pkg/modules/generators/trait/ops_rule_generator.go +++ b/pkg/modules/generators/trait/ops_rule_generator.go @@ -2,20 +2,21 @@ package trait import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" "kusionstack.io/kube-api/apps/v1alpha1" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" "kusionstack.io/kusion/pkg/modules" appmodule "kusionstack.io/kusion/pkg/modules/inputs" + "kusionstack.io/kusion/pkg/modules/inputs/trait" "kusionstack.io/kusion/pkg/modules/inputs/workload" ) type opsRuleGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - app *appmodule.AppConfiguration + project *apiv1.Project + stack *apiv1.Stack + appName string + app *appmodule.AppConfiguration + modulesConfig map[string]apiv1.GenericConfig } func NewOpsRuleGenerator( @@ -23,12 +24,14 @@ func NewOpsRuleGenerator( stack *apiv1.Stack, appName string, app *appmodule.AppConfiguration, + modulesConfig map[string]apiv1.GenericConfig, ) (modules.Generator, error) { return &opsRuleGenerator{ - project: project, - stack: stack, - appName: appName, - app: app, + project: project, + stack: stack, + appName: appName, + app: app, + modulesConfig: modulesConfig, }, nil } @@ -37,14 +40,16 @@ func NewOpsRuleGeneratorFunc( stack *apiv1.Stack, appName string, app *appmodule.AppConfiguration, + modulesConfig map[string]apiv1.GenericConfig, ) modules.NewGeneratorFunc { return func() (modules.Generator, error) { - return NewOpsRuleGenerator(project, stack, appName, app) + return NewOpsRuleGenerator(project, stack, appName, app, modulesConfig) } } func (g *opsRuleGenerator) Generate(spec *apiv1.Intent) error { - if g.app.OpsRule == nil { + // opsRule does not exist in AppConfig and workspace config + if g.app.OpsRule == nil && g.modulesConfig[trait.OpsRuleConst] == nil { return nil } @@ -54,7 +59,10 @@ func (g *opsRuleGenerator) Generate(spec *apiv1.Intent) error { } if g.app.Workload.Service.Type == workload.TypeCollaset { - maxUnavailable := intstr.Parse(g.app.OpsRule.MaxUnavailable) + maxUnavailable, err := trait.GetMaxUnavailable(g.app.OpsRule, g.modulesConfig) + if err != nil { + return err + } resource := &v1alpha1.PodTransitionRule{ TypeMeta: metav1.TypeMeta{ APIVersion: v1alpha1.GroupVersion.String(), diff --git a/pkg/modules/generators/trait/ops_rule_generator_test.go b/pkg/modules/generators/trait/ops_rule_generator_test.go index 0f4b317e..06fb4dd6 100644 --- a/pkg/modules/generators/trait/ops_rule_generator_test.go +++ b/pkg/modules/generators/trait/ops_rule_generator_test.go @@ -1,6 +1,7 @@ package trait import ( + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -13,10 +14,11 @@ import ( func Test_opsRuleGenerator_Generate(t *testing.T) { type fields struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - app *appmodule.AppConfiguration + project *apiv1.Project + stack *apiv1.Stack + appName string + app *appmodule.AppConfiguration + workspaceConfig map[string]apiv1.GenericConfig } type args struct { spec *apiv1.Intent @@ -59,7 +61,7 @@ func Test_opsRuleGenerator_Generate(t *testing.T) { exp: &apiv1.Intent{}, }, { - name: "test CollaSet", + name: "test CollaSet with opsRule in AppConfig", fields: fields{ project: project, stack: stack, @@ -117,32 +119,104 @@ func Test_opsRuleGenerator_Generate(t *testing.T) { }, }, }, + { + name: "test CollaSet with opsRule in workspace", + fields: fields{ + project: project, + stack: stack, + appName: appName, + app: &appmodule.AppConfiguration{ + Workload: &workload.Workload{ + Header: workload.Header{ + Type: workload.TypeService, + }, + Service: &workload.Service{ + Type: workload.TypeCollaset, + }, + }, + }, + workspaceConfig: map[string]apiv1.GenericConfig{ + "opsRule": { + "maxUnavailable": 7, + }, + }, + }, + args: args{ + spec: &apiv1.Intent{}, + }, + wantErr: false, + exp: &apiv1.Intent{ + Resources: apiv1.Resources{ + apiv1.Resource{ + ID: "apps.kusionstack.io/v1alpha1:PodTransitionRule:default:default-dev-foo", + Type: "Kubernetes", + Attributes: map[string]interface{}{ + "apiVersion": "apps.kusionstack.io/v1alpha1", + "kind": "PodTransitionRule", + "metadata": map[string]interface{}{ + "creationTimestamp": interface{}(nil), + "name": "default-dev-foo", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "rules": []interface{}{map[string]interface{}{ + "availablePolicy": map[string]interface{}{ + "maxUnavailableValue": 7, + }, + "name": "maxUnavailable", + }}, + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "app.kubernetes.io/name": "foo", "app.kubernetes.io/part-of": "default", + }, + }, + }, "status": map[string]interface{}{}, + }, + DependsOn: []string(nil), + Extensions: map[string]interface{}{ + "GVK": "apps.kusionstack.io/v1alpha1, Kind=PodTransitionRule", + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &opsRuleGenerator{ - project: tt.fields.project, - stack: tt.fields.stack, - appName: tt.fields.appName, - app: tt.fields.app, + project: tt.fields.project, + stack: tt.fields.stack, + appName: tt.fields.appName, + app: tt.fields.app, + modulesConfig: tt.fields.workspaceConfig, } err := g.Generate(tt.args.spec) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) - require.Equal(t, tt.exp, tt.args.spec) + exp, _ := json.Marshal(tt.exp) + act, _ := json.Marshal(tt.args.spec) + require.Equal(t, exp, act) } }) } } func TestNewOpsRuleGeneratorFunc(t *testing.T) { + p := &apiv1.Project{ + Name: "default", + } + s := &apiv1.Stack{ + Name: "dev", + } + type args struct { project *apiv1.Project stack *apiv1.Stack appName string app *appmodule.AppConfiguration + ws map[string]apiv1.GenericConfig } tests := []struct { name string @@ -153,18 +227,33 @@ func TestNewOpsRuleGeneratorFunc(t *testing.T) { { name: "test1", args: args{ - project: nil, - stack: nil, + project: p, + stack: s, appName: "", app: nil, + ws: map[string]apiv1.GenericConfig{ + "opsRule": { + "maxUnavailable": "30%", + }, + }, }, wantErr: false, - want: &opsRuleGenerator{}, + want: &opsRuleGenerator{ + project: p, + stack: s, + appName: "", + modulesConfig: map[string]apiv1.GenericConfig{ + "opsRule": { + "maxUnavailable": "30%", + }, + }, + }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := NewOpsRuleGeneratorFunc(tt.args.project, tt.args.stack, tt.args.appName, tt.args.app) + f := NewOpsRuleGeneratorFunc(tt.args.project, tt.args.stack, tt.args.appName, tt.args.app, tt.args.ws) g, err := f() if tt.wantErr { require.Error(t, err) diff --git a/pkg/modules/inputs/trait/ops_rule.go b/pkg/modules/inputs/trait/ops_rule.go index be201141..2b4412dd 100644 --- a/pkg/modules/inputs/trait/ops_rule.go +++ b/pkg/modules/inputs/trait/ops_rule.go @@ -1,5 +1,45 @@ package trait +import ( + "errors" + "strconv" + + "k8s.io/apimachinery/pkg/util/intstr" + + apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" +) + type OpsRule struct { MaxUnavailable string `json:"maxUnavailable,omitempty" yaml:"maxUnavailable,omitempty"` } + +const ( + OpsRuleConst = "opsRule" + MaxUnavailableConst = "maxUnavailable" +) + +func GetMaxUnavailable(opsRule *OpsRule, modulesConfig map[string]apiv1.GenericConfig) (intstr.IntOrString, error) { + var maxUnavailable intstr.IntOrString + if opsRule != nil { + maxUnavailable = intstr.Parse(opsRule.MaxUnavailable) + } else { + // An example of opsRule config in modulesConfig + // opsRule: + // maxUnavailable: 1 # or 10% + if modulesConfig[OpsRuleConst] == nil || modulesConfig[OpsRuleConst][MaxUnavailableConst] == nil { + return intstr.IntOrString{}, nil + } + var wsValue string + wsValue, isString := modulesConfig[OpsRuleConst][MaxUnavailableConst].(string) + if !isString { + temp, isInt := modulesConfig[OpsRuleConst][MaxUnavailableConst].(int) + if isInt { + wsValue = strconv.Itoa(temp) + } else { + return intstr.IntOrString{}, errors.New("illegal workspace config. opsRule.maxUnavailable in the workspace config is not string or int") + } + } + maxUnavailable = intstr.Parse(wsValue) + } + return maxUnavailable, nil +} diff --git a/pkg/modules/patchers/trait/ops_rule_patcher.go b/pkg/modules/patchers/trait/ops_rule_patcher.go index 9f2af928..80d9664c 100644 --- a/pkg/modules/patchers/trait/ops_rule_patcher.go +++ b/pkg/modules/patchers/trait/ops_rule_patcher.go @@ -2,39 +2,44 @@ package trait import ( appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/util/intstr" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" "kusionstack.io/kusion/pkg/modules" modelsapp "kusionstack.io/kusion/pkg/modules/inputs" + "kusionstack.io/kusion/pkg/modules/inputs/trait" ) type opsRulePatcher struct { - app *modelsapp.AppConfiguration + app *modelsapp.AppConfiguration + modulesConfig map[string]apiv1.GenericConfig } // NewOpsRulePatcherFunc returns a NewPatcherFunc. -func NewOpsRulePatcherFunc(app *modelsapp.AppConfiguration) modules.NewPatcherFunc { +func NewOpsRulePatcherFunc(app *modelsapp.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) modules.NewPatcherFunc { return func() (modules.Patcher, error) { - return NewOpsRulePatcher(app) + return NewOpsRulePatcher(app, modulesConfig) } } // NewOpsRulePatcher returns a Patcher. -func NewOpsRulePatcher(app *modelsapp.AppConfiguration) (modules.Patcher, error) { +func NewOpsRulePatcher(app *modelsapp.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) (modules.Patcher, error) { return &opsRulePatcher{ - app: app, + app: app, + modulesConfig: modulesConfig, }, nil } // Patch implements Patcher interface. func (p *opsRulePatcher) Patch(resources map[string][]*apiv1.Resource) error { - if p.app.OpsRule == nil { + if p.app.OpsRule == nil && p.modulesConfig["opsRule"] == nil { return nil } return modules.PatchResource[appsv1.Deployment](resources, modules.GVKDeployment, func(deploy *appsv1.Deployment) error { - maxUnavailable := intstr.Parse(p.app.OpsRule.MaxUnavailable) + maxUnavailable, err := trait.GetMaxUnavailable(p.app.OpsRule, p.modulesConfig) + if err != nil { + return err + } deploy.Spec.Strategy.Type = appsv1.RollingUpdateDeploymentStrategyType deploy.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ MaxUnavailable: &maxUnavailable, diff --git a/pkg/modules/patchers/trait/ops_rule_patcher_test.go b/pkg/modules/patchers/trait/ops_rule_patcher_test.go index 83b957d8..9770933f 100644 --- a/pkg/modules/patchers/trait/ops_rule_patcher_test.go +++ b/pkg/modules/patchers/trait/ops_rule_patcher_test.go @@ -23,7 +23,8 @@ func Test_opsRulePatcher_Patch(t *testing.T) { } type fields struct { - app *modelsapp.AppConfiguration + app *modelsapp.AppConfiguration + workspaceConfig map[string]apiv1.GenericConfig } type args struct { resources map[string][]*apiv1.Resource @@ -47,11 +48,26 @@ func Test_opsRulePatcher_Patch(t *testing.T) { resources: i.Resources.GVKIndex(), }, }, + { + name: "Patch Deployment with workspace config", + fields: fields{ + app: &modelsapp.AppConfiguration{}, + workspaceConfig: map[string]apiv1.GenericConfig{ + "opsRule": { + "maxUnavailable": "30%", + }, + }, + }, + args: args{ + resources: i.Resources.GVKIndex(), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &opsRulePatcher{ - app: tt.fields.app, + app: tt.fields.app, + modulesConfig: tt.fields.workspaceConfig, } if err := p.Patch(tt.args.resources); (err != nil) != tt.wantErr { t.Errorf("Patch() error = %v, wantErr %v", err, tt.wantErr) @@ -63,7 +79,12 @@ func Test_opsRulePatcher_Patch(t *testing.T) { } assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, deployment.Spec.Strategy.Type) assert.NotNil(t, deployment.Spec.Strategy.RollingUpdate) - assert.Equal(t, intstr.Parse(tt.fields.app.OpsRule.MaxUnavailable), *deployment.Spec.Strategy.RollingUpdate.MaxUnavailable) + if tt.fields.app.OpsRule != nil { + assert.Equal(t, intstr.Parse(tt.fields.app.OpsRule.MaxUnavailable), *deployment.Spec.Strategy.RollingUpdate.MaxUnavailable) + } else { + assert.Equal(t, tt.fields.workspaceConfig["opsRule"]["maxUnavailable"], + (*deployment.Spec.Strategy.RollingUpdate.MaxUnavailable).String()) + } }) } } @@ -83,8 +104,13 @@ func buildMockDeployment() *appsv1.Deployment { } func TestNewOpsRulePatcherFunc(t *testing.T) { + p := &apiv1.Project{ + Name: "default", + } type args struct { - app *modelsapp.AppConfiguration + app *modelsapp.AppConfiguration + project *apiv1.Project + workspace map[string]apiv1.GenericConfig } tests := []struct { name string @@ -94,12 +120,19 @@ func TestNewOpsRulePatcherFunc(t *testing.T) { name: "NewOpsRulePatcherFunc", args: args{ app: &modelsapp.AppConfiguration{}, + workspace: map[string]apiv1.GenericConfig{ + "opsRule": { + "maxUnavailable": "30%", + }, + }, + project: p, }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - patcherFunc := NewOpsRulePatcherFunc(tt.args.app) + patcherFunc := NewOpsRulePatcherFunc(tt.args.app, tt.args.workspace) assert.NotNil(t, patcherFunc) patcher, err := patcherFunc() assert.NoError(t, err) diff --git a/pkg/modules/util.go b/pkg/modules/util.go index 11be8cea..2700dbe4 100644 --- a/pkg/modules/util.go +++ b/pkg/modules/util.go @@ -162,6 +162,7 @@ func AppendToIntent(resourceType apiv1.Type, resourceID string, i *apiv1.Intent, } gvk := resource.(runtime.Object).GetObjectKind().GroupVersionKind().String() + // fixme: this function converts int to int64 by default unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) if err != nil { return err