Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: opsRule supports workspace config #711

Merged
merged 2 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pkg/modules/generators/app_configurations_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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(),
Expand All @@ -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 {
Expand Down
32 changes: 20 additions & 12 deletions pkg/modules/generators/trait/ops_rule_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,36 @@ 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(
project *apiv1.Project,
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
}

Expand All @@ -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
}

Expand All @@ -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(),
Expand Down
117 changes: 103 additions & 14 deletions pkg/modules/generators/trait/ops_rule_generator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trait

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions pkg/modules/inputs/trait/ops_rule.go
Original file line number Diff line number Diff line change
@@ -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")
SparkYuan marked this conversation as resolved.
Show resolved Hide resolved
}
}
maxUnavailable = intstr.Parse(wsValue)
}
return maxUnavailable, nil
}
21 changes: 13 additions & 8 deletions pkg/modules/patchers/trait/ops_rule_patcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading