Skip to content

Commit

Permalink
feat: opsRule supports workspace config (#711)
Browse files Browse the repository at this point in the history
* feat: opsRule supports workspace config

* feat: opsRule supports workspace config
  • Loading branch information
SparkYuan committed Dec 22, 2023
1 parent d09fa12 commit fe2ce94
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 43 deletions.
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")
}
}
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

0 comments on commit fe2ce94

Please sign in to comment.