From 4ec5eefe6c33300dc062b963634187643ab97630 Mon Sep 17 00:00:00 2001 From: Dayuan Date: Mon, 26 Feb 2024 18:00:33 +0800 Subject: [PATCH] refactor: workload, port and secret according to the new appConfig model the way of using namespace isn't changed and marked it as a todo --- .../v1/appconfiguration/appconfiguration.go | 79 +- .../v1/appconfiguration/workload/common.go | 11 +- .../workload/container/container.go | 2 +- .../v1/appconfiguration/workload/service.go | 8 +- .../v1/appconfiguration/workload/workload.go | 5 +- .../workload/workload_test.go | 16 +- pkg/apis/core/v1/workspace.go | 1 + pkg/cmd/build/build.go | 6 +- pkg/cmd/build/builders/appconfig_builder.go | 6 +- .../build/builders/appconfig_builder_test.go | 14 +- pkg/cmd/build/util.go | 10 +- pkg/cmd/init/options.go | 4 +- pkg/engine/operation/models/change.go | 2 +- pkg/engine/operation/models/change_test.go | 4 +- .../printers/convertor/oam_convertor.go | 2 +- pkg/engine/states/remote/mysql/mysql_state.go | 2 +- .../accessories/database_generator.go | 78 -- .../accessories/database_generator_test.go | 249 ----- .../accessories/mysql/alicloud_rds.go | 176 ---- .../accessories/mysql/alicloud_rds_test.go | 429 -------- .../generators/accessories/mysql/aws_rds.go | 166 --- .../accessories/mysql/aws_rds_test.go | 332 ------ .../accessories/mysql/local_database.go | 284 ------ .../accessories/mysql/local_database_test.go | 274 ----- .../accessories/mysql/mysql_generator.go | 330 ------ .../accessories/mysql/mysql_generator_test.go | 948 ------------------ .../accessories/postgres/alicloud_rds.go | 174 ---- .../accessories/postgres/alicloud_rds_test.go | 429 -------- .../accessories/postgres/aws_rds.go | 164 --- .../accessories/postgres/aws_rds_test.go | 332 ------ .../accessories/postgres/local_database.go | 288 ------ .../postgres/local_database_test.go | 274 ----- .../postgres/postgres_generator.go | 330 ------ .../postgres/postgres_generator_test.go | 948 ------------------ .../app_configurations_generator.go | 73 +- .../app_configurations_generator_test.go | 11 +- .../monitoring/monitoring_generator.go | 233 ----- .../monitoring/monitoring_generator_test.go | 156 --- pkg/modules/generators/namespace_generator.go | 12 +- .../generators/namespace_generator_test.go | 2 +- .../generators/trait/ops_rule_generator.go | 83 -- .../trait/ops_rule_generator_test.go | 279 ------ .../generators/workload/job_generator.go | 30 +- .../generators/workload/job_generator_test.go | 104 +- .../workload/network/ports_generator.go | 303 ------ .../workload/network/ports_generator_test.go | 363 ------- .../workload/secret/secret_generator.go | 35 +- .../workload/secret/secret_generator_test.go | 32 +- .../generators/workload/service_generator.go | 180 ++-- .../workload/service_generator_test.go | 100 +- .../generators/workload/workload_generator.go | 96 +- .../workload/workload_generator_test.go | 139 +-- pkg/modules/inputs/accessories/database.go | 120 --- .../inputs/accessories/database_test.go | 292 ------ pkg/modules/inputs/accessories/mysql/mysql.go | 47 - .../inputs/accessories/postgres/postgres.go | 47 - pkg/modules/inputs/appconfiguration.go | 48 - pkg/modules/inputs/appconfiguration_test.go | 68 -- pkg/modules/inputs/monitoring/monitoring.go | 44 - pkg/modules/inputs/provider.go | 84 -- pkg/modules/inputs/provider_test.go | 141 --- pkg/modules/inputs/trait/ops_rule.go | 45 - pkg/modules/inputs/workload/common.go | 35 - .../inputs/workload/container/container.go | 354 ------- .../workload/container/container_test.go | 791 --------------- pkg/modules/inputs/workload/job.go | 14 - pkg/modules/inputs/workload/network/port.go | 40 - pkg/modules/inputs/workload/secret.go | 8 - pkg/modules/inputs/workload/service.go | 25 - pkg/modules/inputs/workload/workload.go | 117 --- pkg/modules/inputs/workload/workload_test.go | 309 ------ pkg/modules/interfaces.go | 42 +- .../patchers/monitoring/monitoring_patcher.go | 163 --- .../monitoring/monitoring_patcher_test.go | 175 ---- .../patchers/trait/ops_rule_patcher.go | 49 - .../patchers/trait/ops_rule_patcher_test.go | 142 --- pkg/modules/util.go | 48 - pkg/modules/util_test.go | 31 - pkg/project/paths.go | 6 +- pkg/scaffold/templates.go | 4 +- pkg/scaffold/templates_test.go | 4 +- pkg/workspace/util.go | 34 +- pkg/workspace/util_test.go | 2 +- third_party/kubevela/doc.go | 2 +- .../kubevela/kubevela/apis/common/types.go | 8 +- 85 files changed, 522 insertions(+), 11425 deletions(-) delete mode 100644 pkg/modules/generators/accessories/database_generator.go delete mode 100644 pkg/modules/generators/accessories/database_generator_test.go delete mode 100644 pkg/modules/generators/accessories/mysql/alicloud_rds.go delete mode 100644 pkg/modules/generators/accessories/mysql/alicloud_rds_test.go delete mode 100644 pkg/modules/generators/accessories/mysql/aws_rds.go delete mode 100644 pkg/modules/generators/accessories/mysql/aws_rds_test.go delete mode 100644 pkg/modules/generators/accessories/mysql/local_database.go delete mode 100644 pkg/modules/generators/accessories/mysql/local_database_test.go delete mode 100644 pkg/modules/generators/accessories/mysql/mysql_generator.go delete mode 100644 pkg/modules/generators/accessories/mysql/mysql_generator_test.go delete mode 100644 pkg/modules/generators/accessories/postgres/alicloud_rds.go delete mode 100644 pkg/modules/generators/accessories/postgres/alicloud_rds_test.go delete mode 100644 pkg/modules/generators/accessories/postgres/aws_rds.go delete mode 100644 pkg/modules/generators/accessories/postgres/aws_rds_test.go delete mode 100644 pkg/modules/generators/accessories/postgres/local_database.go delete mode 100644 pkg/modules/generators/accessories/postgres/local_database_test.go delete mode 100644 pkg/modules/generators/accessories/postgres/postgres_generator.go delete mode 100644 pkg/modules/generators/accessories/postgres/postgres_generator_test.go delete mode 100644 pkg/modules/generators/monitoring/monitoring_generator.go delete mode 100644 pkg/modules/generators/monitoring/monitoring_generator_test.go delete mode 100644 pkg/modules/generators/trait/ops_rule_generator.go delete mode 100644 pkg/modules/generators/trait/ops_rule_generator_test.go delete mode 100644 pkg/modules/generators/workload/network/ports_generator.go delete mode 100644 pkg/modules/generators/workload/network/ports_generator_test.go delete mode 100644 pkg/modules/inputs/accessories/database.go delete mode 100644 pkg/modules/inputs/accessories/database_test.go delete mode 100644 pkg/modules/inputs/accessories/mysql/mysql.go delete mode 100644 pkg/modules/inputs/accessories/postgres/postgres.go delete mode 100644 pkg/modules/inputs/appconfiguration.go delete mode 100644 pkg/modules/inputs/appconfiguration_test.go delete mode 100644 pkg/modules/inputs/monitoring/monitoring.go delete mode 100644 pkg/modules/inputs/provider.go delete mode 100644 pkg/modules/inputs/provider_test.go delete mode 100644 pkg/modules/inputs/trait/ops_rule.go delete mode 100644 pkg/modules/inputs/workload/common.go delete mode 100644 pkg/modules/inputs/workload/container/container.go delete mode 100644 pkg/modules/inputs/workload/container/container_test.go delete mode 100644 pkg/modules/inputs/workload/job.go delete mode 100644 pkg/modules/inputs/workload/network/port.go delete mode 100644 pkg/modules/inputs/workload/secret.go delete mode 100644 pkg/modules/inputs/workload/service.go delete mode 100644 pkg/modules/inputs/workload/workload.go delete mode 100644 pkg/modules/inputs/workload/workload_test.go delete mode 100644 pkg/modules/patchers/monitoring/monitoring_patcher.go delete mode 100644 pkg/modules/patchers/monitoring/monitoring_patcher_test.go delete mode 100644 pkg/modules/patchers/trait/ops_rule_patcher.go delete mode 100644 pkg/modules/patchers/trait/ops_rule_patcher_test.go diff --git a/pkg/apis/core/v1/appconfiguration/appconfiguration.go b/pkg/apis/core/v1/appconfiguration/appconfiguration.go index 96b551a06..0b294c7be 100644 --- a/pkg/apis/core/v1/appconfiguration/appconfiguration.go +++ b/pkg/apis/core/v1/appconfiguration/appconfiguration.go @@ -1,12 +1,12 @@ package appconfiguration import ( - "kusionstack.io/kusion/pkg/modules/inputs/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" ) type Accessory map[string]interface{} -// AppConfiguration is a developer-centric definition that describes how to run an Application. The application model is built on a decade +// AppConfiguration is a developer-centric definition that describes how to run an App. The application model is built on a decade // of experience from AntGroup in operating a large-scale internal developer platform and combines the best ideas and practices from the // community. // @@ -20,59 +20,60 @@ type Accessory map[string]interface{} // import models.schema.v1.monitoring as m // import models.schema.v1.database as d // -// helloWorld: ac.AppConfiguration { -// # Built-in module -// workload: wl.Service { -// containers: { -// "main": c.Container { -// image: "ghcr.io/kusion-stack/samples/helloworld:latest" -// # Configure a HTTP readiness probe -// readinessProbe: p.Probe { -// probeHandler: p.Http { -// url: "http://localhost:80" -// } -// } -// } -// } -// } +// helloWorld: ac.AppConfiguration { +// # Built-in module +// workload: wl.Service { +// containers: { +// "main": c.Container { +// image: "ghcr.io/kusion-stack/samples/helloworld:latest" +// # Configure a HTTP readiness probe +// readinessProbe: p.Probe { +// probeHandler: p.Http { +// url: "http://localhost:80" +// } +// } +// } +// } +// } // -// # extend accessories module base -// accessories: { -// # Built-in module -// "mysql" : d.MySQL { +// # extend accessories module base +// accessories: { +// # Built-in module, key represents the module source +// "kusionstack/mysql@v0.1" : d.MySQL { // type: "cloud" // version: "8.0" // } -// # Built-in module -// "pro" : m.Prometheus { +// # Built-in module, key represents the module source +// "kusionstack/prometheus@v0.1" : m.Prometheus { // path: "/metrics" // } -// # Customized module -// "customize": customizedModule { +// # Customized module, key represents the module source +// "foo/customize": customizedModule { // ... // } // } // -// # extend pipeline module base -// pipeline: { -// # Step is a module -// "step" : Step { -// use: "exec" -// args: ["--test-all"] -// } -// } +// # extend pipeline module base +// pipeline: { +// # Step is a module +// "step" : Step { +// use: "exec" +// args: ["--test-all"] +// } +// } // -// # Dependent app list -// dependency: { -// dependentApps: ["init-kusion"] -// } -// } +// # Dependent app list +// dependency: { +// dependentApps: ["init-kusion"] +// } +// } type AppConfiguration struct { - // Name of the target Application. + // Name of the target App. Name string `json:"name,omitempty" yaml:"name,omitempty"` // Workload defines how to run your application code. Workload *workload.Workload `json:"workload" yaml:"workload"` // Accessories defines a collection of accessories that will be attached to the workload. + // The key in this map represents the module source. e.g. kusionstack/mysql@v0.1 Accessories map[string]*Accessory `json:"accessories,omitempty" yaml:"accessories,omitempty"` // Labels and Annotations can be used to attach arbitrary metadata as key-value pairs to resources. Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` diff --git a/pkg/apis/core/v1/appconfiguration/workload/common.go b/pkg/apis/core/v1/appconfiguration/workload/common.go index 2adef9fdf..fffd84bfa 100644 --- a/pkg/apis/core/v1/appconfiguration/workload/common.go +++ b/pkg/apis/core/v1/appconfiguration/workload/common.go @@ -1,14 +1,19 @@ package workload -import "kusionstack.io/kusion/pkg/modules/inputs/workload/container" +import "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/container" + +const ( + FieldLabels = "labels" + FieldAnnotations = "annotations" +) // Base defines set of attributes shared by different workload profile, e.g. Service and Job. You can inherit this Schema to reuse these // common attributes. type Base struct { // The templates of containers to be run. Containers map[string]container.Container `yaml:"containers,omitempty" json:"containers,omitempty"` - // The number of containers that should be run. Default is 2 to meet high availability requirements. - Replicas int `yaml:"replicas,omitempty" json:"replicas,omitempty"` + // The number of containers that should be run. + Replicas *int32 `yaml:"replicas,omitempty" json:"replicas,omitempty"` // Secret Secrets map[string]Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"` // Dirs configures one or more volumes to be mounted to the specified folder. diff --git a/pkg/apis/core/v1/appconfiguration/workload/container/container.go b/pkg/apis/core/v1/appconfiguration/workload/container/container.go index 198a3ef4a..7dc0035d5 100644 --- a/pkg/apis/core/v1/appconfiguration/workload/container/container.go +++ b/pkg/apis/core/v1/appconfiguration/workload/container/container.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v2" ) -// Container describes how the Application's tasks are expected to be run. +// Container describes how the App's tasks are expected to be run. type Container struct { // Image to run for this container Image string `yaml:"image" json:"image"` diff --git a/pkg/apis/core/v1/appconfiguration/workload/service.go b/pkg/apis/core/v1/appconfiguration/workload/service.go index a2100f5ba..90b609284 100644 --- a/pkg/apis/core/v1/appconfiguration/workload/service.go +++ b/pkg/apis/core/v1/appconfiguration/workload/service.go @@ -1,14 +1,16 @@ package workload import ( - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" ) type ServiceType string const ( - Deployment ServiceType = "Deployment" - Collaset ServiceType = "CollaSet" + ModuleService = "service" + ModuleServiceType = "type" + Deployment ServiceType = "Deployment" + Collaset ServiceType = "CollaSet" ) // Service is a kind of workload profile that describes how to run your application code. diff --git a/pkg/apis/core/v1/appconfiguration/workload/workload.go b/pkg/apis/core/v1/appconfiguration/workload/workload.go index 63d22a213..d05552d49 100644 --- a/pkg/apis/core/v1/appconfiguration/workload/workload.go +++ b/pkg/apis/core/v1/appconfiguration/workload/workload.go @@ -8,8 +8,9 @@ import ( type Type string const ( - TypeJob = "Job" - TypeService = "Service" + TypeJob = "Job" + TypeService = "Service" + FieldReplicas = "replicas" ) type Header struct { diff --git a/pkg/apis/core/v1/appconfiguration/workload/workload_test.go b/pkg/apis/core/v1/appconfiguration/workload/workload_test.go index 819d36999..738bd1919 100644 --- a/pkg/apis/core/v1/appconfiguration/workload/workload_test.go +++ b/pkg/apis/core/v1/appconfiguration/workload/workload_test.go @@ -10,6 +10,7 @@ import ( ) func TestWorkload_MarshalJSON(t *testing.T) { + r2 := int32(2) tests := []struct { name string data *Workload @@ -25,7 +26,7 @@ func TestWorkload_MarshalJSON(t *testing.T) { Service: &Service{ Type: "Deployment", Base: Base{ - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "app": "my-service", }, @@ -80,6 +81,7 @@ func TestWorkload_MarshalJSON(t *testing.T) { } func TestWorkload_UnmarshalJSON(t *testing.T) { + r1 := int32(1) tests := []struct { name string data string @@ -95,7 +97,7 @@ func TestWorkload_UnmarshalJSON(t *testing.T) { }, Service: &Service{ Base: Base{ - Replicas: 1, + Replicas: &r1, Labels: map[string]string{}, Annotations: map[string]string{}, Dirs: map[string]string{}, @@ -144,6 +146,7 @@ func TestWorkload_UnmarshalJSON(t *testing.T) { } func TestWorkload_MarshalYAML(t *testing.T) { + r2 := int32(2) tests := []struct { name string workload *Workload @@ -159,7 +162,7 @@ func TestWorkload_MarshalYAML(t *testing.T) { Service: &Service{ Type: "Deployment", Base: Base{ - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "app": "my-service", }, @@ -185,7 +188,7 @@ type: Deployment`, Service: &Service{ Type: "Deployment", Base: Base{ - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "app": "my-service", }, @@ -228,6 +231,7 @@ schedule: '* * * * *'`, } func TestWorkload_UnmarshalYAML(t *testing.T) { + r1 := int32(1) tests := []struct { name string data string @@ -248,7 +252,7 @@ schedule: '* * * * *'`, }, Service: &Service{ Base: Base{ - Replicas: 1, + Replicas: &r1, Labels: map[string]string{}, Annotations: map[string]string{}, Dirs: map[string]string{}, @@ -271,7 +275,7 @@ schedule: '* * * * *'`, }, Job: &Job{ Base: Base{ - Replicas: 1, + Replicas: &r1, Labels: map[string]string{}, Annotations: map[string]string{}, Dirs: map[string]string{}, diff --git a/pkg/apis/core/v1/workspace.go b/pkg/apis/core/v1/workspace.go index 4c344bfdb..048d397e5 100644 --- a/pkg/apis/core/v1/workspace.go +++ b/pkg/apis/core/v1/workspace.go @@ -41,6 +41,7 @@ type Workspace struct { } // ModuleConfigs is a set of multiple ModuleConfig, whose key is the module name. +// The module name format is "source@version". type ModuleConfigs map[string]*ModuleConfig // ModuleConfig is the config of a module, which contains a default and several patcher blocks. diff --git a/pkg/cmd/build/build.go b/pkg/cmd/build/build.go index 1917f83d9..2fe7c3363 100644 --- a/pkg/cmd/build/build.go +++ b/pkg/cmd/build/build.go @@ -10,12 +10,12 @@ import ( func NewCmdBuild() *cobra.Command { var ( - short = i18n.T(`Build Kusion modules in a Stack to the Intent`) + short = i18n.T(`Build Kusion modules in a stack to the Intent`) long = i18n.T(` - Build Kusion modules in a Stack to the Intent + Build Kusion modules in a stack to the Intent - The command must be executed in a Stack or by specifying a Stack directory with the -w flag. + The command must be executed in a stack or by specifying a stack directory with the -w flag. You can provide a list of arguments to replace the placeholders defined in KCL, and use the --output flag to output the built results to a file`) diff --git a/pkg/cmd/build/builders/appconfig_builder.go b/pkg/cmd/build/builders/appconfig_builder.go index 8a5caf2ce..e8bee4329 100644 --- a/pkg/cmd/build/builders/appconfig_builder.go +++ b/pkg/cmd/build/builders/appconfig_builder.go @@ -2,13 +2,13 @@ package builders import ( "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration" "kusionstack.io/kusion/pkg/modules" "kusionstack.io/kusion/pkg/modules/generators" - "kusionstack.io/kusion/pkg/modules/inputs" ) type AppsConfigBuilder struct { - Apps map[string]inputs.AppConfiguration + Apps map[string]appconfiguration.AppConfiguration Workspace *v1.Workspace } @@ -22,7 +22,7 @@ func (acg *AppsConfigBuilder) Build( } var gfs []modules.NewGeneratorFunc - err := modules.ForeachOrdered(acg.Apps, func(appName string, app inputs.AppConfiguration) error { + err := modules.ForeachOrdered(acg.Apps, func(appName string, app appconfiguration.AppConfiguration) error { gfs = append(gfs, generators.NewAppConfigurationGeneratorFunc(project, stack, appName, &app, acg.Workspace)) return nil }) diff --git a/pkg/cmd/build/builders/appconfig_builder_test.go b/pkg/cmd/build/builders/appconfig_builder_test.go index f8d54351f..92392596b 100644 --- a/pkg/cmd/build/builders/appconfig_builder_test.go +++ b/pkg/cmd/build/builders/appconfig_builder_test.go @@ -6,16 +6,16 @@ import ( "github.com/stretchr/testify/assert" v1 "kusionstack.io/kusion/pkg/apis/core/v1" - appmodel "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" ) func TestAppsConfigBuilder_Build(t *testing.T) { p, s := buildMockProjectAndStack() appName, app := buildMockApp() acg := &AppsConfigBuilder{ - Apps: map[string]appmodel.AppConfiguration{ + Apps: map[string]appconfiguration.AppConfiguration{ appName: *app, }, Workspace: buildMockWorkspace(), @@ -26,8 +26,8 @@ func TestAppsConfigBuilder_Build(t *testing.T) { assert.NotNil(t, intent) } -func buildMockApp() (string, *appmodel.AppConfiguration) { - return "app1", &appmodel.AppConfiguration{ +func buildMockApp() (string, *appconfiguration.AppConfiguration) { + return "app1", &appconfiguration.AppConfiguration{ Workload: &workload.Workload{ Header: workload.Header{ Type: "Service", @@ -37,10 +37,8 @@ func buildMockApp() (string, *appmodel.AppConfiguration) { Type: "Deployment", Ports: []network.Port{ { - Type: network.CSPAliCloud, Port: 80, Protocol: "TCP", - Public: true, }, }, }, diff --git a/pkg/cmd/build/util.go b/pkg/cmd/build/util.go index dc60bc764..123139b36 100644 --- a/pkg/cmd/build/util.go +++ b/pkg/cmd/build/util.go @@ -13,10 +13,10 @@ import ( yamlv3 "gopkg.in/yaml.v3" "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration" "kusionstack.io/kusion/pkg/cmd/build/builders" "kusionstack.io/kusion/pkg/cmd/build/builders/kcl" "kusionstack.io/kusion/pkg/log" - "kusionstack.io/kusion/pkg/modules/inputs" "kusionstack.io/kusion/pkg/util/pretty" "kusionstack.io/kusion/pkg/workspace" ) @@ -24,10 +24,10 @@ import ( func IntentWithSpinner(o *builders.Options, project *v1.Project, stack *v1.Stack) (*v1.Intent, error) { var sp *pterm.SpinnerPrinter if o.NoStyle { - fmt.Printf("Generating Intent in the Stack %s...\n", stack.Name) + fmt.Printf("Generating Intent in the stack %s...\n", stack.Name) } else { sp = &pretty.SpinnerT - sp, _ = sp.Start(fmt.Sprintf("Generating Intent in the Stack %s...", stack.Name)) + sp, _ = sp.Start(fmt.Sprintf("Generating Intent in the stack %s...", stack.Name)) } // style means color and prompt here. Currently, sp will be nil only when o.NoStyle is true @@ -95,7 +95,7 @@ func Intent(o *builders.Options, p *v1.Project, s *v1.Stack) (*v1.Intent, error) return i, nil } -func buildAppConfigs(o *builders.Options, stack *v1.Stack) (map[string]inputs.AppConfiguration, error) { +func buildAppConfigs(o *builders.Options, stack *v1.Stack) (map[string]appconfiguration.AppConfiguration, error) { o.Arguments[kcl.IncludeSchemaTypePath] = "true" compileResult, err := kcl.Run(o, stack) if err != nil { @@ -110,7 +110,7 @@ func buildAppConfigs(o *builders.Options, stack *v1.Stack) (map[string]inputs.Ap out := documents[0].YAMLString() log.Debugf("unmarshal %s to app configs", out) - appConfigs := map[string]inputs.AppConfiguration{} + appConfigs := map[string]appconfiguration.AppConfiguration{} // Note: we use the type of MapSlice in yaml.v2 to maintain the order of container // environment variables, thus we unmarshal appConfigs with yaml.v2 rather than yaml.v3. diff --git a/pkg/cmd/init/options.go b/pkg/cmd/init/options.go index 71e9fa20e..612daeb6f 100644 --- a/pkg/cmd/init/options.go +++ b/pkg/cmd/init/options.go @@ -113,7 +113,7 @@ func (o *Options) Run() error { pterm.Cyan("")) pterm.Printfln("Press %s at any time to quit.", pterm.Cyan("^C")) pterm.Println() - pterm.Bold.Println("Project Config:") + pterm.Bold.Println("project Config:") } // o.ProjectName is used to make root directory @@ -157,7 +157,7 @@ func (o *Options) Run() error { tc.StacksConfig = make(map[string]map[string]interface{}) for _, stack := range template.StackTemplates { if !o.Yes { - pterm.Bold.Printfln("Stack Config: %s", pterm.Cyan(stack.Name)) + pterm.Bold.Printfln("stack Config: %s", pterm.Cyan(stack.Name)) } configs := make(map[string]interface{}) for _, f := range stack.Fields { diff --git a/pkg/engine/operation/models/change.go b/pkg/engine/operation/models/change.go index 45b359acb..b80a75860 100644 --- a/pkg/engine/operation/models/change.go +++ b/pkg/engine/operation/models/change.go @@ -165,7 +165,7 @@ func (p *Changes) AllUnChange() bool { func (p *Changes) Summary(writer io.Writer) { // Create a fork of the default table, fill it with data and print it. // Data can also be generated and inserted later. - tableHeader := []string{fmt.Sprintf("Stack: %s", p.stack.Name), "ID", "Action"} + tableHeader := []string{fmt.Sprintf("stack: %s", p.stack.Name), "ID", "Action"} tableData := pterm.TableData{tableHeader} for i, step := range p.Values() { diff --git a/pkg/engine/operation/models/change_test.go b/pkg/engine/operation/models/change_test.go index 1ced16459..c7ed35a2f 100644 --- a/pkg/engine/operation/models/change_test.go +++ b/pkg/engine/operation/models/change_test.go @@ -306,7 +306,7 @@ func TestChanges_Stack(t *testing.T) { stack: tt.fields.stack, } if got := p.Stack(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Changes.Stack() = %v, want %v", got, tt.want) + t.Errorf("Changes.stack() = %v, want %v", got, tt.want) } }) } @@ -341,7 +341,7 @@ func TestChanges_Project(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := NewChanges(tt.fields.project, tt.fields.stack, tt.fields.order) if got := p.Project(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Changes.Project() = %v, want %v", got, tt.want) + t.Errorf("Changes.project() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/engine/printers/convertor/oam_convertor.go b/pkg/engine/printers/convertor/oam_convertor.go index c37e9538e..33247cb9e 100644 --- a/pkg/engine/printers/convertor/oam_convertor.go +++ b/pkg/engine/printers/convertor/oam_convertor.go @@ -9,7 +9,7 @@ import ( // APIs in core.oam.dev/v1beta1 const ( - Application = "Application" + Application = "App" ) func ToOAM(o *unstructured.Unstructured) runtime.Object { diff --git a/pkg/engine/states/remote/mysql/mysql_state.go b/pkg/engine/states/remote/mysql/mysql_state.go index 6c4b1cdde..1d000b05a 100644 --- a/pkg/engine/states/remote/mysql/mysql_state.go +++ b/pkg/engine/states/remote/mysql/mysql_state.go @@ -62,7 +62,7 @@ func (s *MysqlState) GetLatestState(q *states.StateQuery) (*states.State, error) where := make(map[string]interface{}) if len(q.Project) == 0 { - msg := "no Project in query" + msg := "no project in query" log.Errorf(msg) return nil, fmt.Errorf(msg) } diff --git a/pkg/modules/generators/accessories/database_generator.go b/pkg/modules/generators/accessories/database_generator.go deleted file mode 100644 index 96293f08a..000000000 --- a/pkg/modules/generators/accessories/database_generator.go +++ /dev/null @@ -1,78 +0,0 @@ -package database - -import ( - "fmt" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/generators/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/generators/accessories/postgres" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -const ( - errUnsupportedDatabaseType = "unsupported database type: %s" -) - -var _ modules.Generator = &databaseGenerator{} - -type databaseGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - workload *workload.Workload - database map[string]*database.Database - moduleConfigs map[string]apiv1.GenericConfig - tfConfigs apiv1.TerraformConfig - namespace string - - // for internal generator - context modules.GeneratorContext -} - -func NewDatabaseGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - return &databaseGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - workload: ctx.Application.Workload, - database: ctx.Application.Database, - moduleConfigs: ctx.ModuleInputs, - tfConfigs: ctx.TerraformConfig, - namespace: ctx.Namespace, - context: ctx, - }, nil -} - -func NewDatabaseGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewDatabaseGenerator(ctx) - } -} - -func (g *databaseGenerator) Generate(spec *apiv1.Intent) error { - if spec.Resources == nil { - spec.Resources = make(apiv1.Resources, 0) - } - - if len(g.database) > 0 { - var gfs []modules.NewGeneratorFunc - - for dbKey, db := range g.database { - switch db.Header.Type { - case database.TypeMySQL: - gfs = append(gfs, mysql.NewMySQLGeneratorFunc(g.context, dbKey, db.MySQL)) - case database.TypePostgreSQL: - gfs = append(gfs, postgres.NewPostgresGeneratorFunc(g.context, dbKey, db.PostgreSQL)) - default: - return fmt.Errorf(errUnsupportedDatabaseType, db.Header.Type) - } - } - - if err := modules.CallGenerators(spec, gfs...); err != nil { - return err - } - } - return nil -} diff --git a/pkg/modules/generators/accessories/database_generator_test.go b/pkg/modules/generators/accessories/database_generator_test.go deleted file mode 100644 index b0d29b9d2..000000000 --- a/pkg/modules/generators/accessories/database_generator_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package database - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func newGeneratorContext( - project *apiv1.Project, - stack *apiv1.Stack, - appName string, - workload *workload.Workload, - database map[string]*database.Database, - moduleInputs map[string]apiv1.GenericConfig, - tfConfigs apiv1.TerraformConfig, -) modules.GeneratorContext { - application := &inputs.AppConfiguration{ - Name: appName, - Workload: workload, - Database: database, - } - - return modules.GeneratorContext{ - Project: project, - Stack: stack, - Application: application, - Namespace: project.Name, - ModuleInputs: moduleInputs, - TerraformConfig: tfConfigs, - } -} - -func TestNewDatabaseGenerator(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{} - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - - tests := []struct { - name string - data modules.GeneratorContext - expected *databaseGenerator - expectedErr error - }{ - { - name: "Valid Database Generator", - data: context, - expected: &databaseGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - database: database, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - context: context, - }, - }, - } - - for _, test := range tests { - actual, actualErr := NewDatabaseGenerator(test.data) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestDatabaseGenerator_NewDatabaseGeneratorFunc(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{} - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - - tests := []struct { - name string - data modules.GeneratorContext - expected modules.Generator - expectedErr error - }{ - { - name: "Valid Database Generator Func", - data: context, - expected: &databaseGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - database: database, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - context: context, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - testGeneratorFunc := NewDatabaseGeneratorFunc(test.data) - actual, actualErr := testGeneratorFunc() - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestGenerate(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - - tests := []struct { - name string - database map[string]*database.Database - moduleInputs map[string]apiv1.GenericConfig - expectedErr error - }{ - { - name: "Generate Local MySQL Database", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - expectedErr: nil, - }, - { - name: "Generate Local PostgreSQL Database", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "14.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - expectedErr: nil, - }, - { - name: "Generate Local MySQL And PostgreSQL Database", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "14.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - expectedErr: nil, - }, - { - name: "Generate Unsupported Database Type", - database: map[string]*database.Database{ - "test": { - Header: database.Header{ - Type: "Unsupported", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - expectedErr: fmt.Errorf(errUnsupportedDatabaseType, "Unsupported"), - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, - appName, workload, test.database, test.moduleInputs, tfConfigs) - g, _ := NewDatabaseGenerator(context) - actualErr := g.(*databaseGenerator).Generate(&apiv1.Intent{}) - - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} diff --git a/pkg/modules/generators/accessories/mysql/alicloud_rds.go b/pkg/modules/generators/accessories/mysql/alicloud_rds.go deleted file mode 100644 index 6ef17c5f3..000000000 --- a/pkg/modules/generators/accessories/mysql/alicloud_rds.go +++ /dev/null @@ -1,176 +0,0 @@ -package mysql - -import ( - "fmt" - "os" - "strings" - - v1 "k8s.io/api/core/v1" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" -) - -const ( - defaultAlicloudProviderURL = "registry.terraform.io/aliyun/alicloud/1.209.1" - alicloudRegionEnv = "ALICLOUD_REGION" - alicloudDBInstance = "alicloud_db_instance" - alicloudDBConnection = "alicloud_db_connection" - alicloudRDSAccount = "alicloud_rds_account" -) - -type alicloudServerlessConfig struct { - AutoPause bool `yaml:"auto_pause" json:"auto_pause"` - SwitchForce bool `yaml:"switch_force" json:"switch_force"` - MaxCapacity int `yaml:"max_capacity,omitempty" json:"max_capacity,omitempty"` - MinCapacity int `yaml:"min_capacity,omitempty" json:"min_capacity,omitempty"` -} - -// generateAlicloudResources generates alicloud provided mysql database instance. -func (g *mysqlGenerator) generateAlicloudResources(db *mysql.MySQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Set the terraform random and alicloud provider. - randomProvider, alicloudProvider := &inputs.Provider{}, &inputs.Provider{} - - randomProviderCfg, ok := g.tfConfigs[inputs.RandomProvider] - if !ok { - randomProvider.SetString(defaultRandomProviderURL) - } else { - randomProviderURL, err := inputs.GetProviderURL(randomProviderCfg) - if err != nil { - return nil, err - } - if err := randomProvider.SetString(randomProviderURL); err != nil { - return nil, err - } - } - - alicloudProviderCfg, ok := g.tfConfigs[inputs.AlicloudProvider] - if !ok { - alicloudProvider.SetString(defaultAlicloudProviderURL) - } else { - alicloudProviderURL, err := inputs.GetProviderURL(alicloudProviderCfg) - if err != nil { - return nil, err - } - if err := alicloudProvider.SetString(alicloudProviderURL); err != nil { - return nil, err - } - } - - // Get the alicloud provider region, and the region of the alicloud provider must be set. - var alicloudProviderRegion string - if alicloudProviderRegion = inputs.GetProviderRegion(g.tfConfigs[inputs.AlicloudProvider]); alicloudProviderRegion == "" { - alicloudProviderRegion = os.Getenv(alicloudRegionEnv) - } - if alicloudProviderRegion == "" { - return nil, fmt.Errorf("alicloud provider region should not be empty") - } - - // Build alicloud_db_instance. - alicloudDBInstanceID, r := g.generateAlicloudDBInstance(alicloudProviderRegion, alicloudProvider, db) - spec.Resources = append(spec.Resources, r) - - // Build alicloud_db_connection for alicloud_db_instance. - var alicloudDBConnectionID string - if isPublicAccessible(db.SecurityIPs) { - alicloudDBConnectionID, r = g.generateAlicloudDBConnection(alicloudDBInstanceID, alicloudProviderRegion, alicloudProvider) - spec.Resources = append(spec.Resources, r) - } - - // Build random_password for alicloud_rds_account. - randomPasswordID, r := g.generateTFRandomPassword(randomProvider) - spec.Resources = append(spec.Resources, r) - - // Build alicloud_rds_account. - r = g.generateAlicloudRDSAccount(db.Username, randomPasswordID, alicloudDBInstanceID, alicloudProviderRegion, alicloudProvider, db) - spec.Resources = append(spec.Resources, r) - - // Inject the host address, username and password into k8s secret. - password := modules.KusionPathDependency(randomPasswordID, "result") - hostAddress := modules.KusionPathDependency(alicloudDBInstanceID, "connection_string") - if !db.PrivateRouting { - // Set the public network connection string as the host address. - hostAddress = modules.KusionPathDependency(alicloudDBConnectionID, "connection_string") - } - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *mysqlGenerator) generateAlicloudDBInstance( - region string, - provider *inputs.Provider, - db *mysql.MySQL, -) (string, apiv1.Resource) { - dbAttrs := map[string]interface{}{ - "category": db.Category, - "engine": "MySQL", - "engine_version": db.Version, - "instance_storage": db.Size, - "instance_type": db.InstanceType, - "security_ips": db.SecurityIPs, - "vswitch_id": db.SubnetID, - "instance_name": db.DatabaseName, - } - - // Set serverless specific attributes. - if strings.Contains(db.Category, "serverless") { - dbAttrs["db_instance_storage_type"] = "cloud_essd" - dbAttrs["instance_charge_type"] = "Serverless" - - serverlessConfig := alicloudServerlessConfig{ - MaxCapacity: 8, - MinCapacity: 1, - } - serverlessConfig.AutoPause = false - serverlessConfig.SwitchForce = false - - dbAttrs["serverless_config"] = []alicloudServerlessConfig{ - serverlessConfig, - } - } - - id := modules.TerraformResourceID(provider, alicloudDBInstance, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudDBInstance) - - return id, modules.TerraformResource(id, nil, dbAttrs, pvdExts) -} - -func (g *mysqlGenerator) generateAlicloudDBConnection( - dbInstanceID, region string, - provider *inputs.Provider, -) (string, apiv1.Resource) { - dbConnectionAttrs := map[string]interface{}{ - "instance_id": modules.KusionPathDependency(dbInstanceID, "id"), - } - - id := modules.TerraformResourceID(provider, alicloudDBConnection, g.mysql.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudDBConnection) - - return id, modules.TerraformResource(id, nil, dbConnectionAttrs, pvdExts) -} - -func (g *mysqlGenerator) generateAlicloudRDSAccount( - accountName, randomPasswordID, dbInstanceID, region string, - provider *inputs.Provider, db *mysql.MySQL, -) apiv1.Resource { - rdsAccountAttrs := map[string]interface{}{ - "account_name": accountName, - "account_password": modules.KusionPathDependency(randomPasswordID, "result"), - "account_type": "Super", - "db_instance_id": modules.KusionPathDependency(dbInstanceID, "id"), - } - - id := modules.TerraformResourceID(provider, alicloudRDSAccount, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudRDSAccount) - - return modules.TerraformResource(id, nil, rdsAccountAttrs, pvdExts) -} diff --git a/pkg/modules/generators/accessories/mysql/alicloud_rds_test.go b/pkg/modules/generators/accessories/mysql/alicloud_rds_test.go deleted file mode 100644 index 8b698721b..000000000 --- a/pkg/modules/generators/accessories/mysql/alicloud_rds_test.go +++ /dev/null @@ -1,429 +0,0 @@ -package mysql - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestMySQLGenerator_GenerateAlicloudResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - "size": 20, - "instanceType": "mysql.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "mysql.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate Alicloud Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testmysql-mysql", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.aliyun:alicloud:alicloud_db_connection:testmysql.connection_string", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testmysql-mysql.result", - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateAlicloudResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateAlicloudDBInstance(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - "size": 20, - "instanceType": "mysql.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "mysql.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - region string - providerURL string - db *mysql.MySQL - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud DB Instance", - region: "cn-beijing", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - db: db, - expectedID: "aliyun:alicloud:alicloud_db_instance:testmysql", - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_db_instance:testmysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "category": "serverless_basic", - "db_instance_storage_type": "cloud_essd", - "engine": "MySQL", - "engine_version": "8.0", - "instance_name": "testmysql", - "instance_charge_type": "Serverless", - "instance_storage": 20, - "instance_type": "mysql.n2.serverless.1c", - "security_ips": []string{ - "0.0.0.0/0", - }, - "serverless_config": []alicloudServerlessConfig{ - { - AutoPause: false, - SwitchForce: false, - MaxCapacity: 8, - MinCapacity: 1, - }, - }, - "vswitch_id": "xxxxxxx", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudDBInstance, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualID, actualRes := g.(*mysqlGenerator).generateAlicloudDBInstance( - test.region, alicloudProvider, test.db, - ) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestMySQLGenerator_GenerateAlicloudDBConnection(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - "size": 20, - "instanceType": "mysql.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "mysql.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - dbInstanceID string - region string - providerURL string - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud DB Connection", - dbInstanceID: "aliyun:alicloud:alicloud_db_instance:testmysql", - region: "cn-beijing", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - expectedID: "aliyun:alicloud:alicloud_db_connection:testmysql", - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_db_connection:testmysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "instance_id": "$kusion_path.aliyun:alicloud:alicloud_db_instance:testmysql.id", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudDBConnection, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualID, actualRes := g.(*mysqlGenerator).generateAlicloudDBConnection( - test.dbInstanceID, test.region, alicloudProvider, - ) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestMySQLGenerator_GenerateAlicloudDBAccount(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - "size": 20, - "instanceType": "mysql.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "mysql.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - providerURL string - accountName string - randomPasswordID string - dbInstanceID string - region string - db *mysql.MySQL - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud RDS Account", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - accountName: "root", - randomPasswordID: "hashicorp:random:random_password:testmysql-mysql", - dbInstanceID: "aliyun:alicloud:alicloud_db_instance:testmysql", - region: "cn-beijing", - db: db, - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_rds_account:testmysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "account_name": "root", - "account_password": "$kusion_path.hashicorp:random:random_password:testmysql-mysql.result", - "account_type": "Super", - "db_instance_id": "$kusion_path.aliyun:alicloud:alicloud_db_instance:testmysql.id", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudRDSAccount, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualRes := g.(*mysqlGenerator).generateAlicloudRDSAccount( - test.accountName, test.randomPasswordID, test.dbInstanceID, test.region, alicloudProvider, test.db) - - assert.Equal(t, test.expectedRes, actualRes) - } -} diff --git a/pkg/modules/generators/accessories/mysql/aws_rds.go b/pkg/modules/generators/accessories/mysql/aws_rds.go deleted file mode 100644 index e563fed9f..000000000 --- a/pkg/modules/generators/accessories/mysql/aws_rds.go +++ /dev/null @@ -1,166 +0,0 @@ -package mysql - -import ( - "fmt" - "os" - - v1 "k8s.io/api/core/v1" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" -) - -const ( - defaultAWSProviderURL = "registry.terraform.io/hashicorp/aws/5.0.1" - awsRegionEnv = "AWS_REGION" - awsSecurityGroup = "aws_security_group" - awsDBInstance = "aws_db_instance" -) - -type awsSecurityGroupTraffic struct { - CidrBlocks []string `yaml:"cidr_blocks" json:"cidr_blocks"` - Description string `yaml:"description" json:"description"` - FromPort int `yaml:"from_port" json:"from_port"` - IPv6CIDRBlocks []string `yaml:"ipv6_cidr_blocks" json:"ipv6_cidr_blocks"` - PrefixListIDs []string `yaml:"prefix_list_ids" json:"prefix_list_ids"` - Protocol string `yaml:"protocol" json:"protocol"` - SecurityGroups []string `yaml:"security_groups" json:"security_groups"` - Self bool `yaml:"self" json:"self"` - ToPort int `yaml:"to_port" json:"to_port"` -} - -// generateAWSResources generates aws provided mysql database instance. -func (g *mysqlGenerator) generateAWSResources(db *mysql.MySQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Set the terraform random and aws provider. - randomProvider, awsProvider := &inputs.Provider{}, &inputs.Provider{} - - randomProviderCfg, ok := g.tfConfigs[inputs.RandomProvider] - if !ok { - randomProvider.SetString(defaultRandomProviderURL) - } else { - randomProviderURL, err := inputs.GetProviderURL(randomProviderCfg) - if err != nil { - return nil, err - } - if err := randomProvider.SetString(randomProviderURL); err != nil { - return nil, err - } - } - - awsProviderCfg, ok := g.tfConfigs[inputs.AWSProvider] - if !ok { - awsProvider.SetString(defaultAWSProviderURL) - } else { - awsProviderURL, err := inputs.GetProviderURL(awsProviderCfg) - if err != nil { - return nil, err - } - if err := awsProvider.SetString(awsProviderURL); err != nil { - return nil, err - } - } - - // Get the aws provider region, and the region of the aws provider must be set. - var awsProviderRegion string - if awsProviderRegion = inputs.GetProviderRegion(g.tfConfigs[inputs.AWSProvider]); awsProviderRegion == "" { - awsProviderRegion = os.Getenv(awsRegionEnv) - } - if awsProviderRegion == "" { - return nil, fmt.Errorf("aws provider region should not be empty") - } - - // Build random_password for aws_db_instance. - randomPasswordID, r := g.generateTFRandomPassword(randomProvider) - spec.Resources = append(spec.Resources, r) - - // Build aws_security group for aws_db_instance. - awsSecurityGroupID, r, err := g.generateAWSSecurityGroup(awsProvider, awsProviderRegion, db) - if err != nil { - return nil, err - } - spec.Resources = append(spec.Resources, r) - - // Build aws_db_instance. - awsDBInstanceID, r := g.generateAWSDBInstance(awsProviderRegion, awsSecurityGroupID, randomPasswordID, awsProvider, db) - spec.Resources = append(spec.Resources, r) - - // Inject the database host address, username and password into k8s secret. - hostAddress := modules.KusionPathDependency(awsDBInstanceID, "address") - password := modules.KusionPathDependency(randomPasswordID, "result") - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *mysqlGenerator) generateAWSSecurityGroup( - provider *inputs.Provider, - region string, - db *mysql.MySQL, -) (string, apiv1.Resource, error) { - // SecurityIPs should be in the format of IP address or Classes Inter-Domain - // Routing (CIDR) mode. - for _, ip := range db.SecurityIPs { - if !isIPAddress(ip) && !isCIDR(ip) { - return "", apiv1.Resource{}, fmt.Errorf("illegal security ip format: %v", ip) - } - } - - sgAttrs := map[string]interface{}{ - "egress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "-1", - FromPort: 0, - ToPort: 0, - }, - }, - "ingress": []awsSecurityGroupTraffic{ - { - CidrBlocks: db.SecurityIPs, - Protocol: "tcp", - FromPort: 3306, - ToPort: 3306, - }, - }, - } - - id := modules.TerraformResourceID(provider, awsSecurityGroup, g.mysql.DatabaseName+dbResSuffix) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, awsSecurityGroup) - - return id, modules.TerraformResource(id, nil, sgAttrs, pvdExts), nil -} - -func (g *mysqlGenerator) generateAWSDBInstance( - region, awsSecurityGroupID, randomPasswordID string, - provider *inputs.Provider, db *mysql.MySQL, -) (string, apiv1.Resource) { - dbAttrs := map[string]interface{}{ - "allocated_storage": db.Size, - "engine": dbEngine, - "engine_version": db.Version, - "identifier": db.DatabaseName, - "instance_class": db.InstanceType, - "password": modules.KusionPathDependency(randomPasswordID, "result"), - "publicly_accessible": isPublicAccessible(db.SecurityIPs), - "skip_final_snapshot": true, - "username": db.Username, - "vpc_security_group_ids": []string{ - modules.KusionPathDependency(awsSecurityGroupID, "id"), - }, - } - - if db.SubnetID != "" { - dbAttrs["db_subnet_group_name"] = db.SubnetID - } - - id := modules.TerraformResourceID(provider, awsDBInstance, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, awsDBInstance) - - return id, modules.TerraformResource(id, nil, dbAttrs, pvdExts) -} diff --git a/pkg/modules/generators/accessories/mysql/aws_rds_test.go b/pkg/modules/generators/accessories/mysql/aws_rds_test.go deleted file mode 100644 index 05926b524..000000000 --- a/pkg/modules/generators/accessories/mysql/aws_rds_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package mysql - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestMySQLGenerator_GenerateAWSResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate AWS Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testmysql-mysql", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.hashicorp:aws:aws_db_instance:testmysql.address", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testmysql-mysql.result", - }, - }, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateAWSResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateAWSSecurityGroup(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - providerURL string - region string - db *mysql.MySQL - expectedID string - expectedRes apiv1.Resource - expectedErr error - }{ - { - name: "Generate AWS Security Group", - providerURL: "registry.terraform.io/hashicorp/aws/5.0.1", - region: "us-east-1", - db: db, - expectedID: "hashicorp:aws:aws_security_group:testmysql-mysql", - expectedRes: apiv1.Resource{ - ID: "hashicorp:aws:aws_security_group:testmysql-mysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "egress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "-1", - FromPort: 0, - ToPort: 0, - }, - }, - "ingress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "tcp", - FromPort: 3306, - ToPort: 3306, - }, - }, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/aws/5.0.1", - "providerMeta": map[string]interface{}{ - "region": "us-east-1", - }, - "resourceType": awsSecurityGroup, - }, - }, - }, - } - - for _, test := range tests { - awsProvider := &inputs.Provider{} - _ = awsProvider.SetString(test.providerURL) - actualID, actualRes, actualErr := g.(*mysqlGenerator).generateAWSSecurityGroup( - awsProvider, test.region, test.db, - ) - - if test.expectedErr == nil { - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateAWSDBInstance(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - region string - awsSecurityGroupID string - randomPasswordID string - providerURL string - db *mysql.MySQL - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate AWS DB Instance", - region: "us-east-1", - awsSecurityGroupID: "hashicorp:aws:aws_security_group:testmysql-mysql", - randomPasswordID: "hashicorp:random:random_password:testmysql-mysql", - providerURL: "registry.terraform.io/hashicorp/aws/5.0.1", - db: db, - expectedID: "hashicorp:aws:aws_db_instance:testmysql", - expectedRes: apiv1.Resource{ - ID: "hashicorp:aws:aws_db_instance:testmysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "allocated_storage": 20, - "engine": "mysql", - "engine_version": "8.0", - "identifier": "testmysql", - "instance_class": "db.t3.micro", - "password": "$kusion_path.hashicorp:random:random_password:testmysql-mysql.result", - "publicly_accessible": true, - "skip_final_snapshot": true, - "username": "root", - "vpc_security_group_ids": []string{ - "$kusion_path.hashicorp:aws:aws_security_group:testmysql-mysql.id", - }, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/aws/5.0.1", - "providerMeta": map[string]interface{}{ - "region": "us-east-1", - }, - "resourceType": awsDBInstance, - }, - }, - }, - } - - for _, test := range tests { - awsProvider := &inputs.Provider{} - _ = awsProvider.SetString(test.providerURL) - actualID, actualRes := g.(*mysqlGenerator).generateAWSDBInstance( - test.region, test.awsSecurityGroupID, test.randomPasswordID, - awsProvider, test.db, - ) - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} diff --git a/pkg/modules/generators/accessories/mysql/local_database.go b/pkg/modules/generators/accessories/mysql/local_database.go deleted file mode 100644 index ab383fdaf..000000000 --- a/pkg/modules/generators/accessories/mysql/local_database.go +++ /dev/null @@ -1,284 +0,0 @@ -package mysql - -import ( - "crypto/md5" - "encoding/hex" - "strconv" - - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" -) - -var ( - localSecretSuffix = "-mysql-local-secret" - localPVCSuffix = "-mysql-local-pvc" - localServiceSuffix = "-mysql-local-service" -) - -// generateLocalResources generates locally deployed mysql database instance. -func (g *mysqlGenerator) generateLocalResources(db *mysql.MySQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Build k8s secret for local database's random password. - password, err := g.generateLocalSecret(spec) - if err != nil { - return nil, err - } - - // Build k8s persistentvolumeclaim for local database. - if err = g.generateLocalPVC(db, spec); err != nil { - return nil, err - } - - // Build k8s deployment for local database. - if err = g.generateLocalDeployment(db, spec); err != nil { - return nil, err - } - - // Build k8s service for local database. - hostAddress, err := g.generateLocalService(db, spec) - if err != nil { - return nil, err - } - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *mysqlGenerator) generateLocalSecret(spec *apiv1.Intent) (string, error) { - password := g.generateLocalPassword(16) - - data := make(map[string]string) - data["password"] = password - - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.mysql.DatabaseName + localSecretSuffix, - Namespace: g.namespace, - }, - StringData: data, - } - secID := modules.KubernetesResourceID(secret.TypeMeta, secret.ObjectMeta) - - // Fixme: return $kusion_path with `stringData.password` of local database secret id. - return password, modules.AppendToIntent( - apiv1.Kubernetes, - secID, - spec, - secret, - ) -} - -func (g *mysqlGenerator) generateLocalPVC(db *mysql.MySQL, spec *apiv1.Intent) error { - // Create the k8s pvc with the storage size of `db.Size`. - pvc := &v1.PersistentVolumeClaim{ - TypeMeta: metav1.TypeMeta{ - Kind: "PersistentVolumeClaim", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.mysql.DatabaseName + localPVCSuffix, - Namespace: g.namespace, - Labels: g.generateLocalMatchLabels(), - }, - Spec: v1.PersistentVolumeClaimSpec{ - AccessModes: []v1.PersistentVolumeAccessMode{ - v1.ReadWriteOnce, - }, - Resources: v1.ResourceRequirements{ - Requests: map[v1.ResourceName]resource.Quantity{ - v1.ResourceStorage: resource.MustParse(strconv.Itoa(db.Size) + "Gi"), - }, - }, - }, - } - - return modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(pvc.TypeMeta, pvc.ObjectMeta), - spec, - pvc, - ) -} - -func (g *mysqlGenerator) generateLocalDeployment(db *mysql.MySQL, spec *apiv1.Intent) error { - // Prepare the pod spec for specific local database. - podSpec, err := g.generateLocalPodSpec(db) - if err != nil { - return err - } - - // Create the k8s deployment for local database. - deployment := &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: appsv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.mysql.DatabaseName, - Namespace: g.namespace, - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: g.generateLocalMatchLabels(), - }, - Template: v1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: g.generateLocalMatchLabels(), - }, - Spec: podSpec, - }, - }, - } - - return modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(deployment.TypeMeta, deployment.ObjectMeta), - spec, - deployment, - ) -} - -func (g *mysqlGenerator) generateLocalPodSpec(db *mysql.MySQL) (v1.PodSpec, error) { - var env []v1.EnvVar - var ports []v1.ContainerPort - var volumes []v1.Volume - var volumeMounts []v1.VolumeMount - var podSpec v1.PodSpec - - image := dbEngine + ":" + db.Version - secretName := g.mysql.DatabaseName + localSecretSuffix - ports = []v1.ContainerPort{ - { - Name: g.mysql.DatabaseName, - ContainerPort: int32(3306), - }, - } - volumes = []v1.Volume{ - { - Name: g.mysql.DatabaseName, - VolumeSource: v1.VolumeSource{ - PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ - ClaimName: g.mysql.DatabaseName + localPVCSuffix, - }, - }, - }, - } - volumeMounts = []v1.VolumeMount{ - { - Name: g.mysql.DatabaseName, - MountPath: "/var/lib/mysql", - }, - } - - if db.Username != "root" { - env = []v1.EnvVar{ - { - Name: "MYSQL_USER", - Value: db.Username, - }, - { - Name: "MYSQL_PASSWORD", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: secretName, - }, - Key: "password", - }, - }, - }, - } - } else { - env = []v1.EnvVar{ - { - Name: "MYSQL_ROOT_PASSWORD", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: secretName, - }, - Key: "password", - }, - }, - }, - } - } - - podSpec = v1.PodSpec{ - Containers: []v1.Container{ - { - Name: g.mysql.DatabaseName, - Image: image, - Env: env, - Ports: ports, - VolumeMounts: volumeMounts, - }, - }, - Volumes: volumes, - } - - return podSpec, nil -} - -func (g *mysqlGenerator) generateLocalService(db *mysql.MySQL, spec *apiv1.Intent) (string, error) { - // Prepare the service port for specific local database. - svcPort := g.generateLocalSvcPort(db) - - svcName := g.mysql.DatabaseName + localServiceSuffix - // Create the k8s service for local database. - service := &v1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: svcName, - Namespace: g.namespace, - Labels: g.generateLocalMatchLabels(), - }, - Spec: v1.ServiceSpec{ - ClusterIP: "None", - Ports: svcPort, - Selector: g.generateLocalMatchLabels(), - }, - } - - return svcName, modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(service.TypeMeta, service.ObjectMeta), - spec, - service, - ) -} - -func (g *mysqlGenerator) generateLocalSvcPort(db *mysql.MySQL) []v1.ServicePort { - svcPort := []v1.ServicePort{ - { - Port: int32(3306), - }, - } - - return svcPort -} - -func (g *mysqlGenerator) generateLocalPassword(n int) string { - hashInput := g.appName + g.project.Name + g.stack.Name - hash := md5.Sum([]byte(hashInput)) - - hashString := hex.EncodeToString(hash[:]) - - return hashString[:n] -} - -func (g *mysqlGenerator) generateLocalMatchLabels() map[string]string { - return map[string]string{ - "accessory": g.mysql.DatabaseName, - } -} diff --git a/pkg/modules/generators/accessories/mysql/local_database_test.go b/pkg/modules/generators/accessories/mysql/local_database_test.go deleted file mode 100644 index 16636902d..000000000 --- a/pkg/modules/generators/accessories/mysql/local_database_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package mysql - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestMySQLGenerator_GenerateLocalResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate Local Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testmysql-mysql", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "testmysql-mysql-local-service", - "username": "root", - "password": g.(*mysqlGenerator).generateLocalPassword(16), - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateLocalResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateLocalSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - spec *apiv1.Intent - expected string - expectedErr error - }{ - { - name: "Generate Local Secret", - spec: &apiv1.Intent{}, - expected: g.(*mysqlGenerator).generateLocalPassword(16), - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateLocalSecret(test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateLocalPVC(t *testing.T) { - g, db := newDefaultMySQLGenerator() - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expectedErr error - }{ - { - name: "Generate Local PVC", - db: db, - spec: &apiv1.Intent{}, - expectedErr: nil, - }, - } - - for _, test := range tests { - actualErr := g.generateLocalPVC(test.db, test.spec) - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateLocalDeployment(t *testing.T) { - g, db := newDefaultMySQLGenerator() - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expectedErr error - }{ - { - name: "Generate Local Deployment", - db: db, - spec: &apiv1.Intent{}, - expectedErr: nil, - }, - } - - for _, test := range tests { - actualErr := g.generateLocalDeployment(test.db, test.spec) - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateLocalService(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - db *mysql.MySQL - spec *apiv1.Intent - expected string - expectedErr error - }{ - { - name: "Generate Local Service", - db: db, - spec: &apiv1.Intent{}, - expected: "testmysql-mysql-local-service", - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateLocalService(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func newDefaultMySQLGenerator() (*mysqlGenerator, *mysql.MySQL) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "local", - Version: "8.0", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - return g.(*mysqlGenerator), db -} diff --git a/pkg/modules/generators/accessories/mysql/mysql_generator.go b/pkg/modules/generators/accessories/mysql/mysql_generator.go deleted file mode 100644 index 9300913c0..000000000 --- a/pkg/modules/generators/accessories/mysql/mysql_generator.go +++ /dev/null @@ -1,330 +0,0 @@ -package mysql - -import ( - "errors" - "fmt" - "net" - "strings" - - "gopkg.in/yaml.v2" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/workspace" -) - -const ( - errUnsupportedTFProvider = "unsupported terraform provider for mysql generator: %s" - errUnsupportedMySQLType = "unsupported mysql type: %s" - errEmptyCloudInfo = "empty cloud info in module config" -) - -const ( - dbEngine = "mysql" - dbResSuffix = "-mysql" - dbHostAddressEnv = "KUSION_DB_HOST" - dbUsernameEnv = "KUSION_DB_USERNAME" - dbPasswordEnv = "KUSION_DB_PASSWORD" -) - -const ( - defaultRandomProviderURL = "registry.terraform.io/hashicorp/random/3.5.1" - randomPassword = "random_password" -) - -var ( - defaultUsername string = "root" - defaultCategory string = "Basic" - defaultSecurityIPs []string = []string{"0.0.0.0/0"} - defaultPrivateRouting bool = true - defaultSize int = 10 -) - -var _ modules.Generator = &mysqlGenerator{} - -// mysqlGenerator implements the modules.Generator interface. -type mysqlGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - workload *workload.Workload - mysql *mysql.MySQL - moduleConfigs map[string]apiv1.GenericConfig - tfConfigs apiv1.TerraformConfig - namespace string - dbKey string -} - -// NewMySQLGenerator returns a new generator for mysql database. -func NewMySQLGenerator(ctx modules.GeneratorContext, dbKey string, db *mysql.MySQL) (modules.Generator, error) { - return &mysqlGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - workload: ctx.Application.Workload, - mysql: db, - moduleConfigs: ctx.ModuleInputs, - tfConfigs: ctx.TerraformConfig, - namespace: ctx.Namespace, - dbKey: dbKey, - }, nil -} - -// NewMySQLGeneratorFunc returns a new generator function for -// generating a new mysql database. -func NewMySQLGeneratorFunc(ctx modules.GeneratorContext, dbKey string, db *mysql.MySQL) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewMySQLGenerator(ctx, dbKey, db) - } -} - -// Generate generates a new mysql database instance for the workload. -func (g *mysqlGenerator) Generate(spec *apiv1.Intent) error { - if spec.Resources == nil { - spec.Resources = make(apiv1.Resources, 0) - } - - // Skip empty mysql database instance. - db := g.mysql - if db == nil { - return nil - } - - // Patch workspace configurations for mysql generator. - if err := g.patchWorkspaceConfig(); err != nil { - if !errors.Is(err, workspace.ErrEmptyModuleConfigBlock) { - return err - } - } - - // Validate the complete mysql database module input. - if err := db.Validate(); err != nil { - return err - } - - var secret *v1.Secret - var err error - // Generate the mysql resources based on the type and provider config. - switch strings.ToLower(db.Type) { - case mysql.LocalDBType: - secret, err = g.generateLocalResources(db, spec) - case mysql.CloudDBType: - var providerType string - providerType, err = g.getTFProviderType() - if err != nil { - return err - } - - switch strings.ToLower(providerType) { - case "aws": - secret, err = g.generateAWSResources(db, spec) - case "alicloud": - secret, err = g.generateAlicloudResources(db, spec) - default: - return fmt.Errorf(errUnsupportedTFProvider, providerType) - } - default: - return fmt.Errorf(errUnsupportedMySQLType, db.Type) - } - - if err != nil { - return err - } - - return g.injectSecret(secret) -} - -// patchWorkspaceConfig patches the config items for mysql generator in workspace configurations. -func (g *mysqlGenerator) patchWorkspaceConfig() error { - // Get the workspace configurations for mysql database instance of the workload. - mysqlCfg, ok := g.moduleConfigs[dbEngine] - if !ok { - g.mysql.Username = defaultUsername - g.mysql.Category = defaultCategory - g.mysql.SecurityIPs = defaultSecurityIPs - g.mysql.PrivateRouting = defaultPrivateRouting - g.mysql.Size = defaultSize - g.mysql.DatabaseName = g.dbKey - - return workspace.ErrEmptyModuleConfigBlock - } - - // Patch workspace configurations for mysql generator. - if username, ok := mysqlCfg["username"]; ok { - g.mysql.Username = username.(string) - } else { - g.mysql.Username = defaultUsername - } - - if category, ok := mysqlCfg["category"]; ok { - g.mysql.Category = category.(string) - } else { - g.mysql.Category = defaultCategory - } - - if securityIPs, ok := mysqlCfg["securityIPs"]; ok { - g.mysql.SecurityIPs = securityIPs.([]string) - } else { - g.mysql.SecurityIPs = defaultSecurityIPs - } - - if privateRouting, ok := mysqlCfg["privateRouting"]; ok { - g.mysql.PrivateRouting = privateRouting.(bool) - } else { - g.mysql.PrivateRouting = defaultPrivateRouting - } - - if size, ok := mysqlCfg["size"]; ok { - g.mysql.Size = size.(int) - } else { - g.mysql.Size = defaultSize - } - - if instanceType, ok := mysqlCfg["instanceType"]; ok { - g.mysql.InstanceType = instanceType.(string) - } - - if subnetID, ok := mysqlCfg["subnetID"]; ok { - g.mysql.SubnetID = subnetID.(string) - } - - if suffix, ok := mysqlCfg["suffix"]; ok { - g.mysql.DatabaseName = g.dbKey + suffix.(string) - } else { - g.mysql.DatabaseName = g.dbKey - } - - return nil -} - -// getTFProviderType returns the type of terraform provider, e.g. "aws" or "alicloud", etc. -func (g *mysqlGenerator) getTFProviderType() (string, error) { - // Get the workspace configurations for mysql database instance of the workload. - mysqlCfg, ok := g.moduleConfigs[dbEngine] - if !ok { - return "", workspace.ErrEmptyModuleConfigBlock - } - - if cloud, ok := mysqlCfg["cloud"]; ok { - return cloud.(string), nil - } - - return "", fmt.Errorf(errEmptyCloudInfo) -} - -// injectSecret injects the mysql instance host address, username and password into -// the containers of the workload as environment variables with kubernetes secret. -func (g *mysqlGenerator) injectSecret(secret *v1.Secret) error { - secEnvs := yaml.MapSlice{ - { - Key: dbHostAddressEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.mysql.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: dbUsernameEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.mysql.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/username", - }, - { - Key: dbPasswordEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.mysql.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/password", - }, - } - - // Inject the database information into the containers of service/job workload. - if g.workload.Service != nil { - for k, v := range g.workload.Service.Containers { - v.Env = append(secEnvs, v.Env...) - g.workload.Service.Containers[k] = v - } - } else if g.workload.Job != nil { - for k, v := range g.workload.Job.Containers { - v.Env = append(secEnvs, v.Env...) - g.workload.Job.Containers[k] = v - } - } - - return nil -} - -// generateDBSecret generates kubernetes secret resource to store the host address, -// username and password of the mysql database instance. -func (g *mysqlGenerator) generateDBSecret(hostAddress, username, password string, spec *apiv1.Intent) (*v1.Secret, error) { - // Create the data map of k8s secret storing the database host address, username - // and password. - data := make(map[string]string) - data["hostAddress"] = hostAddress - data["username"] = username - data["password"] = password - - // Create the k8s secret and append to the spec. - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.mysql.DatabaseName + dbResSuffix, - Namespace: g.namespace, - }, - StringData: data, - } - - return secret, modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(secret.TypeMeta, secret.ObjectMeta), - spec, - secret, - ) -} - -// generateTFRandomPassword generates terraform random_password resource as the password -// the mysql database instance. -func (g *mysqlGenerator) generateTFRandomPassword(provider *inputs.Provider) (string, apiv1.Resource) { - pswAttrs := map[string]interface{}{ - "length": 16, - "special": true, - "override_special": "_", - } - - id := modules.TerraformResourceID(provider, randomPassword, g.mysql.DatabaseName+dbResSuffix) - pvdExts := modules.ProviderExtensions(provider, nil, randomPassword) - - return id, modules.TerraformResource(id, nil, pswAttrs, pvdExts) -} - -// isPublicAccessible returns whether the mysql database instance is publicly -// accessible according to the securityIPs. -func isPublicAccessible(securityIPs []string) bool { - var parsedIP net.IP - for _, ip := range securityIPs { - if isIPAddress(ip) { - parsedIP = net.ParseIP(ip) - } else if isCIDR(ip) { - parsedIP, _, _ = net.ParseCIDR(ip) - } - - if parsedIP != nil && !parsedIP.IsPrivate() { - return true - } - } - - return false -} - -// isIPAddress returns whether the input string is a valid ip address. -func isIPAddress(ipStr string) bool { - ip := net.ParseIP(ipStr) - - return ip != nil -} - -// isCIDR returns whether the input string is a valid CIDR record. -func isCIDR(cidrStr string) bool { - _, _, err := net.ParseCIDR(cidrStr) - - return err == nil -} diff --git a/pkg/modules/generators/accessories/mysql/mysql_generator_test.go b/pkg/modules/generators/accessories/mysql/mysql_generator_test.go deleted file mode 100644 index 3d2b0c12f..000000000 --- a/pkg/modules/generators/accessories/mysql/mysql_generator_test.go +++ /dev/null @@ -1,948 +0,0 @@ -package mysql - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" - "kusionstack.io/kusion/pkg/workspace" -) - -func newGeneratorContext( - project *apiv1.Project, - stack *apiv1.Stack, - appName string, - workload *workload.Workload, - database map[string]*database.Database, - moduleInputs map[string]apiv1.GenericConfig, - tfConfigs apiv1.TerraformConfig, -) modules.GeneratorContext { - application := &inputs.AppConfiguration{ - Name: appName, - Workload: workload, - Database: database, - } - - return modules.GeneratorContext{ - Project: project, - Stack: stack, - Application: application, - Namespace: project.Name, - ModuleInputs: moduleInputs, - TerraformConfig: tfConfigs, - } -} - -func TestNewMySQLGenerator(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - ctx modules.GeneratorContext - dbKey string - db *mysql.MySQL - expected modules.Generator - expectedErr error - }{ - { - name: "New Valid MySQL Generator", - ctx: context, - dbKey: "testmysql", - db: db, - expected: &mysqlGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - mysql: db, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - dbKey: "testmysql", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := NewMySQLGenerator(test.ctx, test.dbKey, test.db) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestNewMySQLGeneratorFunc(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - ctx modules.GeneratorContext - dbKey string - db *mysql.MySQL - expected modules.Generator - expectedErr error - }{ - { - name: "New Valid MySQL Generator Func", - ctx: context, - dbKey: "testmysql", - db: db, - expected: &mysqlGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - mysql: db, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - dbKey: "testmysql", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - testGeneratorFunc := NewMySQLGeneratorFunc(test.ctx, test.dbKey, test.db) - actual, actualErr := testGeneratorFunc() - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_Generate(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - - tests := []struct { - name string - database map[string]*database.Database - moduleInputs map[string]apiv1.GenericConfig - db *mysql.MySQL - expectedErr error - }{ - { - name: "Generate Local Database", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - db: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Generate AWS RDS", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - }, - db: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Generate Alicloud RDS", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - "size": 20, - "instanceType": "mysql.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - }, - db: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Empty Cloud MySQL Instance Type", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - }, - }, - db: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: fmt.Errorf(mysql.ErrEmptyInstanceTypeForCloudDB), - }, - { - name: "Empty Cloud MySQL Instance Type", - database: map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "unsupported-type", - "instanceType": "db.t3.micro", - }, - }, - db: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: fmt.Errorf(errUnsupportedTFProvider, "unsupported-type"), - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, test.database, - test.moduleInputs, tfConfigs) - g, _ := NewMySQLGenerator(context, "testmysql", test.db) - actualErr := g.(*mysqlGenerator).Generate(&apiv1.Intent{}) - - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_PatchWorkspaceConfig(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - moduleInputs map[string]apiv1.GenericConfig - expected *mysql.MySQL - expectedErr error - }{ - { - name: "MySQL with Default Values", - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "instanceType": "db.t3.micro", - }, - }, - expected: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: defaultSize, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: defaultPrivateRouting, - DatabaseName: "testmysql", - }, - expectedErr: nil, - }, - { - name: "MySQL with Customized Values", - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "username": "username", - "securityIPs": []string{ - "172.16.0.0/24", - }, - "privateRouting": false, - }, - }, - expected: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: "username", - SecurityIPs: []string{ - "172.16.0.0/24", - }, - PrivateRouting: false, - DatabaseName: "testmysql", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, database, - test.moduleInputs, tfConfigs) - g, _ := NewMySQLGenerator(context, "testmysql", db) - actualErr := g.(*mysqlGenerator).patchWorkspaceConfig() - - if test.expectedErr == nil { - assert.Equal(t, test.expected, g.(*mysqlGenerator).mysql) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GetTFProviderType(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - moduleInputs map[string]apiv1.GenericConfig - expected string - expectedErr error - }{ - { - name: "AWS Provider", - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - }, - }, - expected: "aws", - expectedErr: nil, - }, - { - name: "Alicloud Provider", - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "alicloud", - }, - }, - expected: "alicloud", - expectedErr: nil, - }, - { - name: "Empty Moudle Config Block", - moduleInputs: map[string]apiv1.GenericConfig{}, - expected: "", - expectedErr: workspace.ErrEmptyModuleConfigBlock, - }, - { - name: "Empty Cloud Info", - moduleInputs: map[string]apiv1.GenericConfig{ - "mysql": {}, - }, - expected: "", - expectedErr: fmt.Errorf(errEmptyCloudInfo), - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, database, - test.moduleInputs, tfConfigs) - g, _ := NewMySQLGenerator(context, "testmysql", db) - actual, actualErr := g.(*mysqlGenerator).getTFProviderType() - - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_InjectSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testmysql", - } - - data := make(map[string]string) - data["hostAddress"] = "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address" - data["username"] = "root" - data["password"] = "$kusion_path.hashicorp:random:random_password:testapp-db.result" - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: appName + dbResSuffix, - Namespace: project.Name, - }, - StringData: data, - } - - tests := []struct { - name string - workload *workload.Workload - expected container.Container - expectedErr error - }{ - { - name: "Inject Secret into Service", - workload: &workload.Workload{ - Header: workload.Header{ - Type: "Service", - }, - Service: &workload.Service{ - Base: workload.Base{ - Containers: map[string]container.Container{ - "testcontainer": { - Image: "testimage:latest", - }, - }, - }, - }, - }, - expected: container.Container{ - Image: "testimage:latest", - Env: yaml.MapSlice{ - { - Key: "KUSION_DB_HOST_TESTMYSQL", - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: "KUSION_DB_USERNAME_TESTMYSQL", - Value: "secret://" + secret.Name + "/username", - }, - { - Key: "KUSION_DB_PASSWORD_TESTMYSQL", - Value: "secret://" + secret.Name + "/password", - }, - }, - }, - expectedErr: nil, - }, - { - name: "Inject Secret into Job", - workload: &workload.Workload{ - Header: workload.Header{ - Type: "Job", - }, - Job: &workload.Job{ - Base: workload.Base{ - Containers: map[string]container.Container{ - "testcontainer": { - Image: "testimage:latest", - }, - }, - }, - }, - }, - expected: container.Container{ - Image: "testimage:latest", - Env: yaml.MapSlice{ - { - Key: "KUSION_DB_HOST_TESTMYSQL", - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: "KUSION_DB_USERNAME_TESTMYSQL", - Value: "secret://" + secret.Name + "/username", - }, - { - Key: "KUSION_DB_PASSWORD_TESTMYSQL", - Value: "secret://" + secret.Name + "/password", - }, - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, test.workload, database, - moduleInputs, tfConfigs) - g, _ := NewMySQLGenerator(context, "testmysql", db) - actualErr := g.(*mysqlGenerator).injectSecret(secret) - - if test.expectedErr == nil { - switch test.workload.Header.Type { - case "Service": - assert.Equal(t, test.expected, g.(*mysqlGenerator).workload.Service.Containers["testcontainer"]) - assert.NoError(t, actualErr) - case "Job": - assert.Equal(t, test.expected, g.(*mysqlGenerator).workload.Job.Containers["testcontainer"]) - assert.NoError(t, actualErr) - } - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateDBSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - hostAddress string - username string - password string - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate DB Secret", - hostAddress: "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address", - username: "root", - password: "$kusion_path.hashicorp:random:random_password:testapp-db.result", - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testmysql-mysql", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testapp-db.result", - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*mysqlGenerator).generateDBSecret(test.hostAddress, test.username, test.password, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestMySQLGenerator_GenerateTFRandomPassword(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testmysql": { - Header: database.Header{ - Type: "MySQL", - }, - MySQL: &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "mysql": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &mysql.MySQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testmysql", - } - g, _ := NewMySQLGenerator(context, "testmysql", db) - - tests := []struct { - name string - providerURL string - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate TF random_password", - providerURL: "registry.terraform.io/hashicorp/random/3.5.1", - expectedID: "hashicorp:random:random_password:testmysql-mysql", - expectedRes: apiv1.Resource{ - ID: "hashicorp:random:random_password:testmysql-mysql", - Type: "Terraform", - Attributes: map[string]interface{}{ - "length": 16, - "override_special": "_", - "special": true, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/random/3.5.1", - "providerMeta": map[string]interface{}(nil), - "resourceType": "random_password", - }, - }, - }, - } - - for _, test := range tests { - randomProvider := &inputs.Provider{} - _ = randomProvider.SetString(test.providerURL) - actualID, actualRes := g.(*mysqlGenerator).generateTFRandomPassword(randomProvider) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestIsPublicAccessible(t *testing.T) { - tests := []struct { - name string - securityIPs []string - expected bool - }{ - { - name: "Public CIDR", - securityIPs: []string{ - "0.0.0.0/0", - }, - expected: true, - }, - { - name: "Private CIDR", - securityIPs: []string{ - "172.16.0.0/24", - }, - expected: false, - }, - { - name: "Private IP Address", - securityIPs: []string{ - "172.16.0.1", - }, - }, - } - - for _, test := range tests { - actual := isPublicAccessible(test.securityIPs) - assert.Equal(t, test.expected, actual) - } -} diff --git a/pkg/modules/generators/accessories/postgres/alicloud_rds.go b/pkg/modules/generators/accessories/postgres/alicloud_rds.go deleted file mode 100644 index 07763aa73..000000000 --- a/pkg/modules/generators/accessories/postgres/alicloud_rds.go +++ /dev/null @@ -1,174 +0,0 @@ -package postgres - -import ( - "fmt" - "os" - "strings" - - v1 "k8s.io/api/core/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" -) - -const ( - defaultAlicloudProviderURL = "registry.terraform.io/aliyun/alicloud/1.209.1" - alicloudRegionEnv = "ALICLOUD_REGION" - alicloudDBInstance = "alicloud_db_instance" - alicloudDBConnection = "alicloud_db_connection" - alicloudRDSAccount = "alicloud_rds_account" -) - -type alicloudServerlessConfig struct { - AutoPause bool `yaml:"auto_pause" json:"auto_pause"` - SwitchForce bool `yaml:"switch_force" json:"switch_force"` - MaxCapacity int `yaml:"max_capacity,omitempty" json:"max_capacity,omitempty"` - MinCapacity int `yaml:"min_capacity,omitempty" json:"min_capacity,omitempty"` -} - -// generateAlicloudResources generates alicloud provided postgresql database instance. -func (g *postgresGenerator) generateAlicloudResources(db *postgres.PostgreSQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Set the terraform random and alicloud provider. - randomProvider, alicloudProvider := &inputs.Provider{}, &inputs.Provider{} - - randomProviderCfg, ok := g.tfConfigs[inputs.RandomProvider] - if !ok { - randomProvider.SetString(defaultRandomProviderURL) - } else { - randomProviderURL, err := inputs.GetProviderURL(randomProviderCfg) - if err != nil { - return nil, err - } - if err := randomProvider.SetString(randomProviderURL); err != nil { - return nil, err - } - } - - alicloudProviderCfg, ok := g.tfConfigs[inputs.AlicloudProvider] - if !ok { - alicloudProvider.SetString(defaultAlicloudProviderURL) - } else { - alicloudProviderURL, err := inputs.GetProviderURL(alicloudProviderCfg) - if err != nil { - return nil, err - } - if err := alicloudProvider.SetString(alicloudProviderURL); err != nil { - return nil, err - } - } - - // Get the alicloud provider region, and the region of the alicloud provider must be set. - var alicloudProviderRegion string - if alicloudProviderRegion = inputs.GetProviderRegion(g.tfConfigs[inputs.AlicloudProvider]); alicloudProviderRegion == "" { - alicloudProviderRegion = os.Getenv(alicloudRegionEnv) - } - if alicloudProviderRegion == "" { - return nil, fmt.Errorf("alicloud provider region should not be empty") - } - - // Build alicloud_db_instance. - alicloudDBInstanceID, r := g.generateAlicloudDBInstance(alicloudProviderRegion, alicloudProvider, db) - spec.Resources = append(spec.Resources, r) - - // Build alicloud_db_connection for alicloud_db_instance. - var alicloudDBConnectionID string - if isPublicAccessible(db.SecurityIPs) { - alicloudDBConnectionID, r = g.generateAlicloudDBConnection(alicloudDBInstanceID, alicloudProviderRegion, alicloudProvider) - spec.Resources = append(spec.Resources, r) - } - - // Build random_password for alicloud_rds_account. - randomPasswordID, r := g.generateTFRandomPassword(randomProvider) - spec.Resources = append(spec.Resources, r) - - // Build alicloud_rds_account. - r = g.generateAlicloudRDSAccount(db.Username, randomPasswordID, alicloudDBInstanceID, alicloudProviderRegion, alicloudProvider, db) - spec.Resources = append(spec.Resources, r) - - // Inject the host address, username and password into k8s secret. - password := modules.KusionPathDependency(randomPasswordID, "result") - hostAddress := modules.KusionPathDependency(alicloudDBInstanceID, "connection_string") - if !db.PrivateRouting { - // Set the public network connection string as the host address. - hostAddress = modules.KusionPathDependency(alicloudDBConnectionID, "connection_string") - } - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *postgresGenerator) generateAlicloudDBInstance( - region string, - provider *inputs.Provider, - db *postgres.PostgreSQL, -) (string, apiv1.Resource) { - dbAttrs := map[string]interface{}{ - "category": db.Category, - "engine": "PostgreSQL", - "engine_version": db.Version, - "instance_storage": db.Size, - "instance_type": db.InstanceType, - "security_ips": db.SecurityIPs, - "vswitch_id": db.SubnetID, - "instance_name": db.DatabaseName, - } - - // Set serverless specific attributes. - if strings.Contains(db.Category, "serverless") { - dbAttrs["db_instance_storage_type"] = "cloud_essd" - dbAttrs["instance_charge_type"] = "Serverless" - - serverlessConfig := alicloudServerlessConfig{ - MaxCapacity: 12, - MinCapacity: 1, - } - serverlessConfig.AutoPause = false - serverlessConfig.SwitchForce = false - - dbAttrs["serverless_config"] = []alicloudServerlessConfig{ - serverlessConfig, - } - } - - id := modules.TerraformResourceID(provider, alicloudDBInstance, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudDBInstance) - - return id, modules.TerraformResource(id, nil, dbAttrs, pvdExts) -} - -func (g *postgresGenerator) generateAlicloudDBConnection( - dbInstanceID, region string, - provider *inputs.Provider, -) (string, apiv1.Resource) { - dbConnectionAttrs := map[string]interface{}{ - "instance_id": modules.KusionPathDependency(dbInstanceID, "id"), - } - - id := modules.TerraformResourceID(provider, alicloudDBConnection, g.postgres.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudDBConnection) - - return id, modules.TerraformResource(id, nil, dbConnectionAttrs, pvdExts) -} - -func (g *postgresGenerator) generateAlicloudRDSAccount( - accountName, randomPasswordID, dbInstanceID, region string, - provider *inputs.Provider, db *postgres.PostgreSQL, -) apiv1.Resource { - rdsAccountAttrs := map[string]interface{}{ - "account_name": accountName, - "account_password": modules.KusionPathDependency(randomPasswordID, "result"), - "account_type": "Super", - "db_instance_id": modules.KusionPathDependency(dbInstanceID, "id"), - } - - id := modules.TerraformResourceID(provider, alicloudRDSAccount, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, alicloudRDSAccount) - - return modules.TerraformResource(id, nil, rdsAccountAttrs, pvdExts) -} diff --git a/pkg/modules/generators/accessories/postgres/alicloud_rds_test.go b/pkg/modules/generators/accessories/postgres/alicloud_rds_test.go deleted file mode 100644 index bf2737379..000000000 --- a/pkg/modules/generators/accessories/postgres/alicloud_rds_test.go +++ /dev/null @@ -1,429 +0,0 @@ -package postgres - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestPostgresGenerator_GenerateAlicloudResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - "size": 20, - "instanceType": "pg.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "pg.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate Alicloud Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testpostgres-postgres", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.aliyun:alicloud:alicloud_db_connection:testpostgres.connection_string", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testpostgres-postgres.result", - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateAlicloudResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateAlicloudDBInstance(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - "size": 20, - "instanceType": "pg.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "pg.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - region string - providerURL string - db *postgres.PostgreSQL - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud DB Instance", - region: "cn-beijing", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - db: db, - expectedID: "aliyun:alicloud:alicloud_db_instance:testpostgres", - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_db_instance:testpostgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "category": "serverless_basic", - "db_instance_storage_type": "cloud_essd", - "engine": "PostgreSQL", - "engine_version": "8.0", - "instance_name": "testpostgres", - "instance_charge_type": "Serverless", - "instance_storage": 20, - "instance_type": "pg.n2.serverless.1c", - "security_ips": []string{ - "0.0.0.0/0", - }, - "serverless_config": []alicloudServerlessConfig{ - { - AutoPause: false, - SwitchForce: false, - MaxCapacity: 12, - MinCapacity: 1, - }, - }, - "vswitch_id": "xxxxxxx", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudDBInstance, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualID, actualRes := g.(*postgresGenerator).generateAlicloudDBInstance( - test.region, alicloudProvider, test.db, - ) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestPostgresGenerator_GenerateAlicloudDBConnection(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - "size": 20, - "instanceType": "pg.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "pg.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - dbInstanceID string - region string - providerURL string - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud DB Connection", - dbInstanceID: "aliyun:alicloud:alicloud_db_instance:testpostgres", - region: "cn-beijing", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - expectedID: "aliyun:alicloud:alicloud_db_connection:testpostgres", - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_db_connection:testpostgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "instance_id": "$kusion_path.aliyun:alicloud:alicloud_db_instance:testpostgres.id", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudDBConnection, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualID, actualRes := g.(*postgresGenerator).generateAlicloudDBConnection( - test.dbInstanceID, test.region, alicloudProvider, - ) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestPostgresGenerator_GenerateAlicloudDBAccount(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - "size": 20, - "instanceType": "pg.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "pg.n2.serverless.1c", - Category: "serverless_basic", - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - SubnetID: "xxxxxxx", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - providerURL string - accountName string - randomPasswordID string - dbInstanceID string - region string - db *postgres.PostgreSQL - expectedRes apiv1.Resource - }{ - { - name: "Generate Alicloud RDS Account", - providerURL: "registry.terraform.io/aliyun/alicloud/1.209.1", - accountName: "root", - randomPasswordID: "hashicorp:random:random_password:testpostgres-postgres", - dbInstanceID: "aliyun:alicloud:alicloud_db_instance:testpostgres", - region: "cn-beijing", - db: db, - expectedRes: apiv1.Resource{ - ID: "aliyun:alicloud:alicloud_rds_account:testpostgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "account_name": "root", - "account_password": "$kusion_path.hashicorp:random:random_password:testpostgres-postgres.result", - "account_type": "Super", - "db_instance_id": "$kusion_path.aliyun:alicloud:alicloud_db_instance:testpostgres.id", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/aliyun/alicloud/1.209.1", - "providerMeta": map[string]interface{}{ - "region": "cn-beijing", - }, - "resourceType": alicloudRDSAccount, - }, - }, - }, - } - - for _, test := range tests { - alicloudProvider := &inputs.Provider{} - _ = alicloudProvider.SetString(test.providerURL) - actualRes := g.(*postgresGenerator).generateAlicloudRDSAccount( - test.accountName, test.randomPasswordID, test.dbInstanceID, test.region, alicloudProvider, test.db) - - assert.Equal(t, test.expectedRes, actualRes) - } -} diff --git a/pkg/modules/generators/accessories/postgres/aws_rds.go b/pkg/modules/generators/accessories/postgres/aws_rds.go deleted file mode 100644 index 150c5a9e9..000000000 --- a/pkg/modules/generators/accessories/postgres/aws_rds.go +++ /dev/null @@ -1,164 +0,0 @@ -package postgres - -import ( - "fmt" - "os" - - v1 "k8s.io/api/core/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" -) - -const ( - defaultAWSProviderURL = "registry.terraform.io/hashicorp/aws/5.0.1" - awsRegionEnv = "AWS_REGION" - awsSecurityGroup = "aws_security_group" - awsDBInstance = "aws_db_instance" -) - -type awsSecurityGroupTraffic struct { - CidrBlocks []string `yaml:"cidr_blocks" json:"cidr_blocks"` - Description string `yaml:"description" json:"description"` - FromPort int `yaml:"from_port" json:"from_port"` - IPv6CIDRBlocks []string `yaml:"ipv6_cidr_blocks" json:"ipv6_cidr_blocks"` - PrefixListIDs []string `yaml:"prefix_list_ids" json:"prefix_list_ids"` - Protocol string `yaml:"protocol" json:"protocol"` - SecurityGroups []string `yaml:"security_groups" json:"security_groups"` - Self bool `yaml:"self" json:"self"` - ToPort int `yaml:"to_port" json:"to_port"` -} - -// generateAWSResources generates aws provided postgresql database instance. -func (g *postgresGenerator) generateAWSResources(db *postgres.PostgreSQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Set the terraform random and aws provider. - randomProvider, awsProvider := &inputs.Provider{}, &inputs.Provider{} - - randomProviderCfg, ok := g.tfConfigs[inputs.RandomProvider] - if !ok { - randomProvider.SetString(defaultRandomProviderURL) - } else { - randomProviderURL, err := inputs.GetProviderURL(randomProviderCfg) - if err != nil { - return nil, err - } - if err := randomProvider.SetString(randomProviderURL); err != nil { - return nil, err - } - } - - awsProviderCfg, ok := g.tfConfigs[inputs.AWSProvider] - if !ok { - awsProvider.SetString(defaultAWSProviderURL) - } else { - awsProviderURL, err := inputs.GetProviderURL(awsProviderCfg) - if err != nil { - return nil, err - } - if err := awsProvider.SetString(awsProviderURL); err != nil { - return nil, err - } - } - - // Get the aws provider region, and the region of the aws provider must be set. - var awsProviderRegion string - if awsProviderRegion = inputs.GetProviderRegion(g.tfConfigs[inputs.AWSProvider]); awsProviderRegion == "" { - awsProviderRegion = os.Getenv(awsRegionEnv) - } - if awsProviderRegion == "" { - return nil, fmt.Errorf("aws provider region should not be empty") - } - - // Build random_password for aws_db_instance. - randomPasswordID, r := g.generateTFRandomPassword(randomProvider) - spec.Resources = append(spec.Resources, r) - - // Build aws_security group for aws_db_instance. - awsSecurityGroupID, r, err := g.generateAWSSecurityGroup(awsProvider, awsProviderRegion, db) - if err != nil { - return nil, err - } - spec.Resources = append(spec.Resources, r) - - // Build aws_db_instance. - awsDBInstanceID, r := g.generateAWSDBInstance(awsProviderRegion, awsSecurityGroupID, randomPasswordID, awsProvider, db) - spec.Resources = append(spec.Resources, r) - - // Inject the database host address, username and password into k8s secret. - hostAddress := modules.KusionPathDependency(awsDBInstanceID, "address") - password := modules.KusionPathDependency(randomPasswordID, "result") - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *postgresGenerator) generateAWSSecurityGroup( - provider *inputs.Provider, - region string, - db *postgres.PostgreSQL, -) (string, apiv1.Resource, error) { - // SecurityIPs should be in the format of IP address or Classes Inter-Domain - // Routing (CIDR) mode. - for _, ip := range db.SecurityIPs { - if !isIPAddress(ip) && !isCIDR(ip) { - return "", apiv1.Resource{}, fmt.Errorf("illegal security ip format: %v", ip) - } - } - - sgAttrs := map[string]interface{}{ - "egress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "-1", - FromPort: 0, - ToPort: 0, - }, - }, - "ingress": []awsSecurityGroupTraffic{ - { - CidrBlocks: db.SecurityIPs, - Protocol: "tcp", - FromPort: 5432, - ToPort: 5432, - }, - }, - } - - id := modules.TerraformResourceID(provider, awsSecurityGroup, g.postgres.DatabaseName+dbResSuffix) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, awsSecurityGroup) - - return id, modules.TerraformResource(id, nil, sgAttrs, pvdExts), nil -} - -func (g *postgresGenerator) generateAWSDBInstance( - region, awsSecurityGroupID, randomPasswordID string, - provider *inputs.Provider, db *postgres.PostgreSQL, -) (string, apiv1.Resource) { - dbAttrs := map[string]interface{}{ - "allocated_storage": db.Size, - "engine": dbEngine, - "engine_version": db.Version, - "identifier": db.DatabaseName, - "instance_class": db.InstanceType, - "password": modules.KusionPathDependency(randomPasswordID, "result"), - "publicly_accessible": isPublicAccessible(db.SecurityIPs), - "skip_final_snapshot": true, - "username": db.Username, - "vpc_security_group_ids": []string{ - modules.KusionPathDependency(awsSecurityGroupID, "id"), - }, - } - - if db.SubnetID != "" { - dbAttrs["db_subnet_group_name"] = db.SubnetID - } - - id := modules.TerraformResourceID(provider, awsDBInstance, db.DatabaseName) - pvdExts := modules.ProviderExtensions(provider, map[string]any{ - "region": region, - }, awsDBInstance) - - return id, modules.TerraformResource(id, nil, dbAttrs, pvdExts) -} diff --git a/pkg/modules/generators/accessories/postgres/aws_rds_test.go b/pkg/modules/generators/accessories/postgres/aws_rds_test.go deleted file mode 100644 index 51febe97d..000000000 --- a/pkg/modules/generators/accessories/postgres/aws_rds_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package postgres - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestPostgresGenerator_GenerateAWSResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate AWS Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testpostgres-postgres", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.hashicorp:aws:aws_db_instance:testpostgres.address", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testpostgres-postgres.result", - }, - }, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateAWSResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateAWSSecurityGroup(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - providerURL string - region string - db *postgres.PostgreSQL - expectedID string - expectedRes apiv1.Resource - expectedErr error - }{ - { - name: "Generate AWS Security Group", - providerURL: "registry.terraform.io/hashicorp/aws/5.0.1", - region: "us-east-1", - db: db, - expectedID: "hashicorp:aws:aws_security_group:testpostgres-postgres", - expectedRes: apiv1.Resource{ - ID: "hashicorp:aws:aws_security_group:testpostgres-postgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "egress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "-1", - FromPort: 0, - ToPort: 0, - }, - }, - "ingress": []awsSecurityGroupTraffic{ - { - CidrBlocks: []string{"0.0.0.0/0"}, - Protocol: "tcp", - FromPort: 5432, - ToPort: 5432, - }, - }, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/aws/5.0.1", - "providerMeta": map[string]interface{}{ - "region": "us-east-1", - }, - "resourceType": awsSecurityGroup, - }, - }, - }, - } - - for _, test := range tests { - awsProvider := &inputs.Provider{} - _ = awsProvider.SetString(test.providerURL) - actualID, actualRes, actualErr := g.(*postgresGenerator).generateAWSSecurityGroup( - awsProvider, test.region, test.db, - ) - - if test.expectedErr == nil { - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateAWSDBInstance(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: false, - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - region string - awsSecurityGroupID string - randomPasswordID string - providerURL string - db *postgres.PostgreSQL - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate AWS DB Instance", - region: "us-east-1", - awsSecurityGroupID: "hashicorp:aws:aws_security_group:testpostgres-postgres", - randomPasswordID: "hashicorp:random:random_password:testpostgres-postgres", - providerURL: "registry.terraform.io/hashicorp/aws/5.0.1", - db: db, - expectedID: "hashicorp:aws:aws_db_instance:testpostgres", - expectedRes: apiv1.Resource{ - ID: "hashicorp:aws:aws_db_instance:testpostgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "allocated_storage": 20, - "engine": "postgres", - "engine_version": "8.0", - "identifier": "testpostgres", - "instance_class": "db.t3.micro", - "password": "$kusion_path.hashicorp:random:random_password:testpostgres-postgres.result", - "publicly_accessible": true, - "skip_final_snapshot": true, - "username": "root", - "vpc_security_group_ids": []string{ - "$kusion_path.hashicorp:aws:aws_security_group:testpostgres-postgres.id", - }, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/aws/5.0.1", - "providerMeta": map[string]interface{}{ - "region": "us-east-1", - }, - "resourceType": awsDBInstance, - }, - }, - }, - } - - for _, test := range tests { - awsProvider := &inputs.Provider{} - _ = awsProvider.SetString(test.providerURL) - actualID, actualRes := g.(*postgresGenerator).generateAWSDBInstance( - test.region, test.awsSecurityGroupID, test.randomPasswordID, - awsProvider, test.db, - ) - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} diff --git a/pkg/modules/generators/accessories/postgres/local_database.go b/pkg/modules/generators/accessories/postgres/local_database.go deleted file mode 100644 index e4f1d8155..000000000 --- a/pkg/modules/generators/accessories/postgres/local_database.go +++ /dev/null @@ -1,288 +0,0 @@ -package postgres - -import ( - "crypto/md5" - "encoding/hex" - "strconv" - - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" -) - -var ( - localSecretSuffix = "-postgres-local-secret" - localPVCSuffix = "-postgres-local-pvc" - localServiceSuffix = "-postgres-local-service" -) - -// generateLocalResources generates locally deployed postgres database instance. -func (g *postgresGenerator) generateLocalResources(db *postgres.PostgreSQL, spec *apiv1.Intent) (*v1.Secret, error) { - // Build k8s secret for local database's random password. - password, err := g.generateLocalSecret(spec) - if err != nil { - return nil, err - } - - // Build k8s persistentvolumeclaim for local database. - if err = g.generateLocalPVC(db, spec); err != nil { - return nil, err - } - - // Build k8s deployment for local database. - if err = g.generateLocalDeployment(db, spec); err != nil { - return nil, err - } - - // Build k8s service for local database. - hostAddress, err := g.generateLocalService(db, spec) - if err != nil { - return nil, err - } - - return g.generateDBSecret(hostAddress, db.Username, password, spec) -} - -func (g *postgresGenerator) generateLocalSecret(spec *apiv1.Intent) (string, error) { - password := g.generateLocalPassword(16) - - data := make(map[string]string) - data["password"] = password - data["username"] = g.postgres.Username - data["database"] = g.postgres.DatabaseName - - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.postgres.DatabaseName + localSecretSuffix, - Namespace: g.project.Name, - }, - StringData: data, - } - secID := modules.KubernetesResourceID(secret.TypeMeta, secret.ObjectMeta) - - // Fixme: return $kusion_path with `stringData.password` of local database secret id. - return password, modules.AppendToIntent( - apiv1.Kubernetes, - secID, - spec, - secret, - ) -} - -func (g *postgresGenerator) generateLocalPVC(db *postgres.PostgreSQL, spec *apiv1.Intent) error { - // Create the k8s pvc with the storage size of `db.Size`. - pvc := &v1.PersistentVolumeClaim{ - TypeMeta: metav1.TypeMeta{ - Kind: "PersistentVolumeClaim", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.postgres.DatabaseName + localPVCSuffix, - Namespace: g.project.Name, - Labels: g.generateLocalMatchLabels(), - }, - Spec: v1.PersistentVolumeClaimSpec{ - AccessModes: []v1.PersistentVolumeAccessMode{ - v1.ReadWriteOnce, - }, - Resources: v1.ResourceRequirements{ - Requests: map[v1.ResourceName]resource.Quantity{ - v1.ResourceStorage: resource.MustParse(strconv.Itoa(db.Size) + "Gi"), - }, - }, - }, - } - - return modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(pvc.TypeMeta, pvc.ObjectMeta), - spec, - pvc, - ) -} - -func (g *postgresGenerator) generateLocalDeployment(db *postgres.PostgreSQL, spec *apiv1.Intent) error { - // Prepare the pod spec for specific local database. - podSpec, err := g.generateLocalPodSpec(db) - if err != nil { - return err - } - - // Create the k8s deployment for local database. - deployment := &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: appsv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.postgres.DatabaseName, - Namespace: g.project.Name, - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: g.generateLocalMatchLabels(), - }, - Template: v1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: g.generateLocalMatchLabels(), - }, - Spec: podSpec, - }, - }, - } - - return modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(deployment.TypeMeta, deployment.ObjectMeta), - spec, - deployment, - ) -} - -func (g *postgresGenerator) generateLocalPodSpec(db *postgres.PostgreSQL) (v1.PodSpec, error) { - var env []v1.EnvVar - var ports []v1.ContainerPort - var volumes []v1.Volume - var volumeMounts []v1.VolumeMount - var podSpec v1.PodSpec - - image := dbEngine + ":" + db.Version - secretName := g.postgres.DatabaseName + localSecretSuffix - ports = []v1.ContainerPort{ - { - Name: g.postgres.DatabaseName, - ContainerPort: int32(5432), - }, - } - volumes = []v1.Volume{ - { - Name: g.postgres.DatabaseName, - VolumeSource: v1.VolumeSource{ - PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ - ClaimName: g.postgres.DatabaseName + localPVCSuffix, - }, - }, - }, - } - volumeMounts = []v1.VolumeMount{ - { - Name: g.postgres.DatabaseName, - MountPath: "/var/lib/postgresql/data", - }, - } - - env = []v1.EnvVar{ - { - Name: "POSTGRES_USER", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: secretName, - }, - Key: "username", - }, - }, - }, - { - Name: "POSTGRES_PASSWORD", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: secretName, - }, - Key: "password", - }, - }, - }, - { - Name: "POSTGRES_DB", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: secretName, - }, - Key: "database", - }, - }, - }, - } - - podSpec = v1.PodSpec{ - Containers: []v1.Container{ - { - Name: g.postgres.DatabaseName, - Image: image, - Env: env, - Ports: ports, - VolumeMounts: volumeMounts, - }, - }, - Volumes: volumes, - } - - return podSpec, nil -} - -func (g *postgresGenerator) generateLocalService(db *postgres.PostgreSQL, spec *apiv1.Intent) (string, error) { - // Prepare the service port for specific local database. - svcPort := g.generateLocalSvcPort(db) - - svcName := g.postgres.DatabaseName + localServiceSuffix - // Create the k8s service for local database. - service := &v1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: svcName, - Namespace: g.project.Name, - Labels: g.generateLocalMatchLabels(), - }, - Spec: v1.ServiceSpec{ - ClusterIP: "None", - Ports: svcPort, - Selector: g.generateLocalMatchLabels(), - }, - } - - return svcName, modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(service.TypeMeta, service.ObjectMeta), - spec, - service, - ) -} - -func (g *postgresGenerator) generateLocalSvcPort(db *postgres.PostgreSQL) []v1.ServicePort { - svcPort := []v1.ServicePort{ - { - Port: int32(5432), - }, - } - - return svcPort -} - -func (g *postgresGenerator) generateLocalPassword(n int) string { - hashInput := g.appName + g.project.Name + g.stack.Name - hash := md5.Sum([]byte(hashInput)) - - hashString := hex.EncodeToString(hash[:]) - - return hashString[:n] -} - -func (g *postgresGenerator) generateLocalMatchLabels() map[string]string { - return map[string]string{ - "accessory": g.postgres.DatabaseName, - } -} diff --git a/pkg/modules/generators/accessories/postgres/local_database_test.go b/pkg/modules/generators/accessories/postgres/local_database_test.go deleted file mode 100644 index f734b536b..000000000 --- a/pkg/modules/generators/accessories/postgres/local_database_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package postgres - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -func TestPostgresGenerator_GenerateLocalResources(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate Local Resources", - db: db, - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testpostgres-postgres", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "testpostgres-postgres-local-service", - "username": "root", - "password": g.(*postgresGenerator).generateLocalPassword(16), - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateLocalResources(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateLocalSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - spec *apiv1.Intent - expected string - expectedErr error - }{ - { - name: "Generate Local Secret", - spec: &apiv1.Intent{}, - expected: g.(*postgresGenerator).generateLocalPassword(16), - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateLocalSecret(test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateLocalPVC(t *testing.T) { - g, db := newDefaultPostgresGenerator() - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expectedErr error - }{ - { - name: "Generate Local PVC", - db: db, - spec: &apiv1.Intent{}, - expectedErr: nil, - }, - } - - for _, test := range tests { - actualErr := g.generateLocalPVC(test.db, test.spec) - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateLocalDeployment(t *testing.T) { - g, db := newDefaultPostgresGenerator() - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expectedErr error - }{ - { - name: "Generate Local Deployment", - db: db, - spec: &apiv1.Intent{}, - expectedErr: nil, - }, - } - - for _, test := range tests { - actualErr := g.generateLocalDeployment(test.db, test.spec) - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgresGenerator_GenerateLocalService(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - Username: "root", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - db *postgres.PostgreSQL - spec *apiv1.Intent - expected string - expectedErr error - }{ - { - name: "Generate Local Service", - db: db, - spec: &apiv1.Intent{}, - expected: "testpostgres-postgres-local-service", - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateLocalService(test.db, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func newDefaultPostgresGenerator() (*postgresGenerator, *postgres.PostgreSQL) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{} - tfConfigs := apiv1.TerraformConfig{} - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - return g.(*postgresGenerator), db -} diff --git a/pkg/modules/generators/accessories/postgres/postgres_generator.go b/pkg/modules/generators/accessories/postgres/postgres_generator.go deleted file mode 100644 index af3878351..000000000 --- a/pkg/modules/generators/accessories/postgres/postgres_generator.go +++ /dev/null @@ -1,330 +0,0 @@ -package postgres - -import ( - "errors" - "fmt" - "net" - "strings" - - "gopkg.in/yaml.v2" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/workspace" -) - -const ( - errUnsupportedTFProvider = "unsupported terraform provider for postgres generator: %s" - errUnsupportedPostgreSQLType = "unsupported postgres type: %s" - errEmptyCloudInfo = "empty cloud info in module config" -) - -const ( - dbEngine = "postgres" - dbResSuffix = "-postgres" - dbHostAddressEnv = "KUSION_DB_HOST" - dbUsernameEnv = "KUSION_DB_USERNAME" - dbPasswordEnv = "KUSION_DB_PASSWORD" -) - -const ( - defaultRandomProviderURL = "registry.terraform.io/hashicorp/random/3.5.1" - randomPassword = "random_password" -) - -var ( - defaultUsername string = "root" - defaultCategory string = "Basic" - defaultSecurityIPs []string = []string{"0.0.0.0/0"} - defaultPrivateRouting bool = true - defaultSize int = 10 -) - -var _ modules.Generator = &postgresGenerator{} - -// postgresGenerator implements the modules.Generator interface. -type postgresGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - workload *workload.Workload - postgres *postgres.PostgreSQL - moduleConfigs map[string]apiv1.GenericConfig - tfConfigs apiv1.TerraformConfig - namespace string - dbKey string -} - -// NewPostgreGenerator returns a new generator for postgres database. -func NewPostgresGenerator(ctx modules.GeneratorContext, dbKey string, db *postgres.PostgreSQL) (modules.Generator, error) { - return &postgresGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - workload: ctx.Application.Workload, - postgres: db, - moduleConfigs: ctx.ModuleInputs, - tfConfigs: ctx.TerraformConfig, - namespace: ctx.Namespace, - dbKey: dbKey, - }, nil -} - -// NewPostgresGeneratorFunc returns a new generator function for -// generating a new postgres database. -func NewPostgresGeneratorFunc(ctx modules.GeneratorContext, dbKey string, db *postgres.PostgreSQL) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewPostgresGenerator(ctx, dbKey, db) - } -} - -// Generate generates a new postgres database instance for the workload. -func (g *postgresGenerator) Generate(spec *apiv1.Intent) error { - if spec.Resources == nil { - spec.Resources = make(apiv1.Resources, 0) - } - - // Skip empty postgres database instance. - db := g.postgres - if db == nil { - return nil - } - - // Patch workspace configurations for postgres generator. - if err := g.patchWorkspaceConfig(); err != nil { - if !errors.Is(err, workspace.ErrEmptyModuleConfigBlock) { - return err - } - } - - // Validate the complete mysql database module input. - if err := db.Validate(); err != nil { - return err - } - - var secret *v1.Secret - var err error - // Generate the postgres resources based on the type and provider config. - switch strings.ToLower(db.Type) { - case postgres.LocalDBType: - secret, err = g.generateLocalResources(db, spec) - case postgres.CloudDBType: - var providerType string - providerType, err = g.getTFProviderType() - if err != nil { - return err - } - - switch strings.ToLower(providerType) { - case "aws": - secret, err = g.generateAWSResources(db, spec) - case "alicloud": - secret, err = g.generateAlicloudResources(db, spec) - default: - return fmt.Errorf(errUnsupportedTFProvider, providerType) - } - default: - return fmt.Errorf(errUnsupportedPostgreSQLType, db.Type) - } - - if err != nil { - return err - } - - return g.injectSecret(secret) -} - -// patchWorkspaceConfig patches the config items for postgres generator in workspace configurations. -func (g *postgresGenerator) patchWorkspaceConfig() error { - // Get the workspace configurations for postgres database instance of the workload. - postgresCfg, ok := g.moduleConfigs[dbEngine] - if !ok { - g.postgres.Username = defaultUsername - g.postgres.Category = defaultCategory - g.postgres.SecurityIPs = defaultSecurityIPs - g.postgres.PrivateRouting = defaultPrivateRouting - g.postgres.Size = defaultSize - g.postgres.DatabaseName = g.dbKey - - return workspace.ErrEmptyModuleConfigBlock - } - - // Patch workspace configurations for postgres generator. - if username, ok := postgresCfg["username"]; ok { - g.postgres.Username = username.(string) - } else { - g.postgres.Username = defaultUsername - } - - if category, ok := postgresCfg["category"]; ok { - g.postgres.Category = category.(string) - } else { - g.postgres.Category = defaultCategory - } - - if securityIPs, ok := postgresCfg["securityIPs"]; ok { - g.postgres.SecurityIPs = securityIPs.([]string) - } else { - g.postgres.SecurityIPs = defaultSecurityIPs - } - - if privateRouting, ok := postgresCfg["privateRouting"]; ok { - g.postgres.PrivateRouting = privateRouting.(bool) - } else { - g.postgres.PrivateRouting = defaultPrivateRouting - } - - if size, ok := postgresCfg["size"]; ok { - g.postgres.Size = size.(int) - } else { - g.postgres.Size = defaultSize - } - - if instanceType, ok := postgresCfg["instanceType"]; ok { - g.postgres.InstanceType = instanceType.(string) - } - - if subnetID, ok := postgresCfg["subnetID"]; ok { - g.postgres.SubnetID = subnetID.(string) - } - - if suffix, ok := postgresCfg["suffix"]; ok { - g.postgres.DatabaseName = g.dbKey + suffix.(string) - } else { - g.postgres.DatabaseName = g.dbKey - } - - return nil -} - -// getTFProviderType returns the type of terraform provider, e.g. "aws" or "alicloud", etc. -func (g *postgresGenerator) getTFProviderType() (string, error) { - // Get the workspace configurations for postgres database instance of the workload. - postgresCfg, ok := g.moduleConfigs[dbEngine] - if !ok { - return "", workspace.ErrEmptyModuleConfigBlock - } - - if cloud, ok := postgresCfg["cloud"]; ok { - return cloud.(string), nil - } - - return "", fmt.Errorf(errEmptyCloudInfo) -} - -// injectSecret injects the postgres instance host address, username and password into -// the containers of the workload as environment variables with kubernetes secret. -func (g *postgresGenerator) injectSecret(secret *v1.Secret) error { - secEnvs := yaml.MapSlice{ - { - Key: dbHostAddressEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.postgres.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: dbUsernameEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.postgres.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/username", - }, - { - Key: dbPasswordEnv + "_" + strings.ToUpper(strings.ReplaceAll(g.postgres.DatabaseName, "-", "_")), - Value: "secret://" + secret.Name + "/password", - }, - } - - // Inject the database information into the containers of service/job workload. - if g.workload.Service != nil { - for k, v := range g.workload.Service.Containers { - v.Env = append(secEnvs, v.Env...) - g.workload.Service.Containers[k] = v - } - } else if g.workload.Job != nil { - for k, v := range g.workload.Job.Containers { - v.Env = append(secEnvs, v.Env...) - g.workload.Job.Containers[k] = v - } - } - - return nil -} - -// generateDBSecret generates kubernetes secret resource to store the host address, -// username and password of the postgres database instance. -func (g *postgresGenerator) generateDBSecret(hostAddress, username, password string, spec *apiv1.Intent) (*v1.Secret, error) { - // Create the data map of k8s secret storing the database host address, username - // and password. - data := make(map[string]string) - data["hostAddress"] = hostAddress - data["username"] = username - data["password"] = password - - // Create the k8s secret and append to the spec. - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: g.postgres.DatabaseName + dbResSuffix, - Namespace: g.project.Name, - }, - StringData: data, - } - - return secret, modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID(secret.TypeMeta, secret.ObjectMeta), - spec, - secret, - ) -} - -// generateTFRandomPassword generates terraform random_password resource as the password -// the postgres database instance. -func (g *postgresGenerator) generateTFRandomPassword(provider *inputs.Provider) (string, apiv1.Resource) { - pswAttrs := map[string]interface{}{ - "length": 16, - "special": true, - "override_special": "_", - } - - id := modules.TerraformResourceID(provider, randomPassword, g.postgres.DatabaseName+dbResSuffix) - pvdExts := modules.ProviderExtensions(provider, nil, randomPassword) - - return id, modules.TerraformResource(id, nil, pswAttrs, pvdExts) -} - -// isPublicAccessible returns whether the postgres database instance is publicly -// accessible according to the securityIPs. -func isPublicAccessible(securityIPs []string) bool { - var parsedIP net.IP - for _, ip := range securityIPs { - if isIPAddress(ip) { - parsedIP = net.ParseIP(ip) - } else if isCIDR(ip) { - parsedIP, _, _ = net.ParseCIDR(ip) - } - - if parsedIP != nil && !parsedIP.IsPrivate() { - return true - } - } - - return false -} - -// isIPAddress returns whether the input string is a valid ip address. -func isIPAddress(ipStr string) bool { - ip := net.ParseIP(ipStr) - - return ip != nil -} - -// isCIDR returns whether the input string is a valid CIDR record. -func isCIDR(cidrStr string) bool { - _, _, err := net.ParseCIDR(cidrStr) - - return err == nil -} diff --git a/pkg/modules/generators/accessories/postgres/postgres_generator_test.go b/pkg/modules/generators/accessories/postgres/postgres_generator_test.go deleted file mode 100644 index d90913f38..000000000 --- a/pkg/modules/generators/accessories/postgres/postgres_generator_test.go +++ /dev/null @@ -1,948 +0,0 @@ -package postgres - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" - "kusionstack.io/kusion/pkg/workspace" -) - -func newGeneratorContext( - project *apiv1.Project, - stack *apiv1.Stack, - appName string, - workload *workload.Workload, - database map[string]*database.Database, - moduleInputs map[string]apiv1.GenericConfig, - tfConfigs apiv1.TerraformConfig, -) modules.GeneratorContext { - application := &inputs.AppConfiguration{ - Name: appName, - Workload: workload, - Database: database, - } - - return modules.GeneratorContext{ - Project: project, - Stack: stack, - Application: application, - Namespace: project.Name, - ModuleInputs: moduleInputs, - TerraformConfig: tfConfigs, - } -} - -func TestNewPostgresGenerator(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - ctx modules.GeneratorContext - dbKey string - db *postgres.PostgreSQL - expected modules.Generator - expectedErr error - }{ - { - name: "New Valid PostgreSQL Generator", - ctx: context, - dbKey: "testpostgres", - db: db, - expected: &postgresGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - postgres: db, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - dbKey: "testpostgres", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := NewPostgresGenerator(test.ctx, test.dbKey, test.db) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestNewPostgresGeneratorFunc(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - ctx modules.GeneratorContext - dbKey string - db *postgres.PostgreSQL - expected modules.Generator - expectedErr error - }{ - { - name: "New Valid PostgreSQL Generator Func", - ctx: context, - dbKey: "testpostgres", - db: db, - expected: &postgresGenerator{ - project: project, - stack: stack, - appName: appName, - workload: workload, - postgres: db, - moduleConfigs: moduleInputs, - tfConfigs: tfConfigs, - namespace: project.Name, - dbKey: "testpostgres", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - testGeneratorFunc := NewPostgresGeneratorFunc(test.ctx, test.dbKey, test.db) - actual, actualErr := testGeneratorFunc() - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_Generate(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - "alicloud": &apiv1.ProviderConfig{ - Version: "1.209.1", - Source: "aliyun/alicloud", - GenericConfig: apiv1.GenericConfig{ - "region": "cn-beijing", - }, - }, - } - - tests := []struct { - name string - database map[string]*database.Database - moduleInputs map[string]apiv1.GenericConfig - db *postgres.PostgreSQL - expectedErr error - }{ - { - name: "Generate Local Database", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{}, - db: &postgres.PostgreSQL{ - Type: "local", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Generate AWS RDS", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - }, - db: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Generate Alicloud RDS", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - "size": 20, - "instanceType": "postgres.n2.serverless.1c", - "category": "serverless_basic", - "privateRouting": false, - "subnetID": "xxxxxxx", - }, - }, - db: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: nil, - }, - { - name: "Empty Cloud PostgreSQL Instance Type", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - }, - }, - db: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: fmt.Errorf(postgres.ErrEmptyInstanceTypeForCloudDB), - }, - { - name: "Empty Cloud PostgreSQL Instance Type", - database: map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - }, - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "unsupported-type", - "instanceType": "db.t3.micro", - }, - }, - db: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - expectedErr: fmt.Errorf(errUnsupportedTFProvider, "unsupported-type"), - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, test.database, - test.moduleInputs, tfConfigs) - g, _ := NewPostgresGenerator(context, "testpostgres", test.db) - actualErr := g.(*postgresGenerator).Generate(&apiv1.Intent{}) - - if test.expectedErr == nil { - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_PatchWorkspaceConfig(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - moduleInputs map[string]apiv1.GenericConfig - expected *postgres.PostgreSQL - expectedErr error - }{ - { - name: "PostgreSQL with Default Values", - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "instanceType": "db.t3.micro", - }, - }, - expected: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: defaultSize, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: defaultUsername, - SecurityIPs: defaultSecurityIPs, - PrivateRouting: defaultPrivateRouting, - DatabaseName: "testpostgres", - }, - expectedErr: nil, - }, - { - name: "PostgreSQL with Customized Values", - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "username": "username", - "securityIPs": []string{ - "172.16.0.0/24", - }, - "privateRouting": false, - }, - }, - expected: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - Size: 20, - InstanceType: "db.t3.micro", - Category: defaultCategory, - Username: "username", - SecurityIPs: []string{ - "172.16.0.0/24", - }, - PrivateRouting: false, - DatabaseName: "testpostgres", - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, database, - test.moduleInputs, tfConfigs) - g, _ := NewPostgresGenerator(context, "testpostgres", db) - actualErr := g.(*postgresGenerator).patchWorkspaceConfig() - - if test.expectedErr == nil { - assert.Equal(t, test.expected, g.(*postgresGenerator).postgres) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_GetTFProviderType(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - } - - tests := []struct { - name string - moduleInputs map[string]apiv1.GenericConfig - expected string - expectedErr error - }{ - { - name: "AWS Provider", - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - }, - }, - expected: "aws", - expectedErr: nil, - }, - { - name: "Alicloud Provider", - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "alicloud", - }, - }, - expected: "alicloud", - expectedErr: nil, - }, - { - name: "Empty Moudle Config Block", - moduleInputs: map[string]apiv1.GenericConfig{}, - expected: "", - expectedErr: workspace.ErrEmptyModuleConfigBlock, - }, - { - name: "Empty Cloud Info", - moduleInputs: map[string]apiv1.GenericConfig{ - "postgres": {}, - }, - expected: "", - expectedErr: fmt.Errorf(errEmptyCloudInfo), - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, workload, database, - test.moduleInputs, tfConfigs) - g, _ := NewPostgresGenerator(context, "testpostgres", db) - actual, actualErr := g.(*postgresGenerator).getTFProviderType() - - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_InjectSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testpostgres", - } - - data := make(map[string]string) - data["hostAddress"] = "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address" - data["username"] = "root" - data["password"] = "$kusion_path.hashicorp:random:random_password:testapp-db.result" - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: appName + dbResSuffix, - Namespace: project.Name, - }, - StringData: data, - } - - tests := []struct { - name string - workload *workload.Workload - expected container.Container - expectedErr error - }{ - { - name: "Inject Secret into Service", - workload: &workload.Workload{ - Header: workload.Header{ - Type: "Service", - }, - Service: &workload.Service{ - Base: workload.Base{ - Containers: map[string]container.Container{ - "testcontainer": { - Image: "testimage:latest", - }, - }, - }, - }, - }, - expected: container.Container{ - Image: "testimage:latest", - Env: yaml.MapSlice{ - { - Key: "KUSION_DB_HOST_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: "KUSION_DB_USERNAME_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/username", - }, - { - Key: "KUSION_DB_PASSWORD_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/password", - }, - }, - }, - expectedErr: nil, - }, - { - name: "Inject Secret into Job", - workload: &workload.Workload{ - Header: workload.Header{ - Type: "Job", - }, - Job: &workload.Job{ - Base: workload.Base{ - Containers: map[string]container.Container{ - "testcontainer": { - Image: "testimage:latest", - }, - }, - }, - }, - }, - expected: container.Container{ - Image: "testimage:latest", - Env: yaml.MapSlice{ - { - Key: "KUSION_DB_HOST_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/hostAddress", - }, - { - Key: "KUSION_DB_USERNAME_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/username", - }, - { - Key: "KUSION_DB_PASSWORD_TESTPOSTGRES", - Value: "secret://" + secret.Name + "/password", - }, - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - context := newGeneratorContext(project, stack, appName, test.workload, database, - moduleInputs, tfConfigs) - g, _ := NewPostgresGenerator(context, "testpostgres", db) - actualErr := g.(*postgresGenerator).injectSecret(secret) - - if test.expectedErr == nil { - switch test.workload.Header.Type { - case "Service": - assert.Equal(t, test.expected, g.(*postgresGenerator).workload.Service.Containers["testcontainer"]) - assert.NoError(t, actualErr) - case "Job": - assert.Equal(t, test.expected, g.(*postgresGenerator).workload.Job.Containers["testcontainer"]) - assert.NoError(t, actualErr) - } - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_GenerateDBSecret(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - hostAddress string - username string - password string - spec *apiv1.Intent - expected *v1.Secret - expectedErr error - }{ - { - name: "Generate DB Secret", - hostAddress: "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address", - username: "root", - password: "$kusion_path.hashicorp:random:random_password:testapp-db.result", - spec: &apiv1.Intent{}, - expected: &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: v1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testpostgres-postgres", - Namespace: "testproject", - }, - StringData: map[string]string{ - "hostAddress": "$kusion_path.hashicorp:aws:aws_db_instance:testapp.address", - "username": "root", - "password": "$kusion_path.hashicorp:random:random_password:testapp-db.result", - }, - }, - expectedErr: nil, - }, - } - - for _, test := range tests { - actual, actualErr := g.(*postgresGenerator).generateDBSecret(test.hostAddress, test.username, test.password, test.spec) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - } -} - -func TestPostgreSQLGenerator_GenerateTFRandomPassword(t *testing.T) { - project := &apiv1.Project{Name: "testproject"} - stack := &apiv1.Stack{Name: "teststack"} - appName := "testapp" - workload := &workload.Workload{} - database := map[string]*database.Database{ - "testpostgres": { - Header: database.Header{ - Type: "PostgreSQL", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - }, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - "postgres": { - "cloud": "aws", - "size": 20, - "instanceType": "db.t3.micro", - "privateRouting": false, - }, - } - tfConfigs := apiv1.TerraformConfig{ - "random": &apiv1.ProviderConfig{ - Version: "3.5.1", - Source: "hashicorp/random", - }, - "aws": &apiv1.ProviderConfig{ - Version: "5.0.1", - Source: "hashicorp/aws", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - } - context := newGeneratorContext(project, stack, appName, workload, database, - moduleInputs, tfConfigs) - db := &postgres.PostgreSQL{ - Type: "cloud", - Version: "8.0", - DatabaseName: "testpostgres", - } - g, _ := NewPostgresGenerator(context, "testpostgres", db) - - tests := []struct { - name string - providerURL string - expectedID string - expectedRes apiv1.Resource - }{ - { - name: "Generate TF random_password", - providerURL: "registry.terraform.io/hashicorp/random/3.5.1", - expectedID: "hashicorp:random:random_password:testpostgres-postgres", - expectedRes: apiv1.Resource{ - ID: "hashicorp:random:random_password:testpostgres-postgres", - Type: "Terraform", - Attributes: map[string]interface{}{ - "length": 16, - "override_special": "_", - "special": true, - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/random/3.5.1", - "providerMeta": map[string]interface{}(nil), - "resourceType": "random_password", - }, - }, - }, - } - - for _, test := range tests { - randomProvider := &inputs.Provider{} - _ = randomProvider.SetString(test.providerURL) - actualID, actualRes := g.(*postgresGenerator).generateTFRandomPassword(randomProvider) - - assert.Equal(t, test.expectedID, actualID) - assert.Equal(t, test.expectedRes, actualRes) - } -} - -func TestIsPublicAccessible(t *testing.T) { - tests := []struct { - name string - securityIPs []string - expected bool - }{ - { - name: "Public CIDR", - securityIPs: []string{ - "0.0.0.0/0", - }, - expected: true, - }, - { - name: "Private CIDR", - securityIPs: []string{ - "172.16.0.0/24", - }, - expected: false, - }, - { - name: "Private IP Address", - securityIPs: []string{ - "172.16.0.1", - }, - }, - } - - for _, test := range tests { - actual := isPublicAccessible(test.securityIPs) - assert.Equal(t, test.expected, actual) - } -} diff --git a/pkg/modules/generators/app_configurations_generator.go b/pkg/modules/generators/app_configurations_generator.go index 5afb87f47..a5a5a7b80 100644 --- a/pkg/modules/generators/app_configurations_generator.go +++ b/pkg/modules/generators/app_configurations_generator.go @@ -5,14 +5,9 @@ import ( "fmt" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration" "kusionstack.io/kusion/pkg/modules" - database "kusionstack.io/kusion/pkg/modules/generators/accessories" - "kusionstack.io/kusion/pkg/modules/generators/monitoring" - "kusionstack.io/kusion/pkg/modules/generators/trait" "kusionstack.io/kusion/pkg/modules/generators/workload" - "kusionstack.io/kusion/pkg/modules/inputs" - patmonitoring "kusionstack.io/kusion/pkg/modules/patchers/monitoring" - pattrait "kusionstack.io/kusion/pkg/modules/patchers/trait" "kusionstack.io/kusion/pkg/workspace" ) @@ -20,7 +15,7 @@ type appConfigurationGenerator struct { project *apiv1.Project stack *apiv1.Stack appName string - app *inputs.AppConfiguration + app *appconfiguration.AppConfiguration ws *apiv1.Workspace } @@ -28,7 +23,7 @@ func NewAppConfigurationGenerator( project *apiv1.Project, stack *apiv1.Stack, appName string, - app *inputs.AppConfiguration, + app *appconfiguration.AppConfiguration, ws *apiv1.Workspace, ) (modules.Generator, error) { if len(project.Name) == 0 { @@ -46,6 +41,7 @@ func NewAppConfigurationGenerator( if ws == nil { return nil, errors.New("workspace must not be empty") // AppConfiguration asks for non-empty workspace } + if err := workspace.ValidateWorkspace(ws); err != nil { return nil, fmt.Errorf("invalid config of workspace %s, %w", stack.Name, err) } @@ -63,7 +59,7 @@ func NewAppConfigurationGeneratorFunc( project *apiv1.Project, stack *apiv1.Stack, appName string, - app *inputs.AppConfiguration, + app *appconfiguration.AppConfiguration, ws *apiv1.Workspace, ) modules.NewGeneratorFunc { return func() (modules.Generator, error) { @@ -75,49 +71,48 @@ func (g *appConfigurationGenerator) Generate(i *apiv1.Intent) error { if i.Resources == nil { i.Resources = make(apiv1.Resources, 0) } + g.app.Name = g.appName // retrieve the module configs of the specified project - modulesConfig, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project.Name) + platformConfigs, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project.Name) if err != nil { return err } - // retrieve the provider configs for the terraform runtime - terraformConfig := workspace.GetTerraformConfig(g.ws.Runtimes) - - // construct proper generator context - namespaceName := g.getNamespaceName(modulesConfig) - g.app.Name = g.appName - context := modules.GeneratorContext{ - Project: g.project, - Stack: g.stack, - Application: g.app, - Namespace: namespaceName, - ModuleInputs: modulesConfig, - TerraformConfig: terraformConfig, - SecretStoreSpec: g.ws.SecretStore, - } + // todo: is namespace a module? how to retrieve it? Currently, it is configured in the workspace file. + namespace := g.getNamespaceName(platformConfigs) - // Generate resources + // Generate built-in resources gfs := []modules.NewGeneratorFunc{ - NewNamespaceGeneratorFunc(context), - database.NewDatabaseGeneratorFunc(context), - workload.NewWorkloadGeneratorFunc(context), - trait.NewOpsRuleGeneratorFunc(context), - monitoring.NewMonitoringGeneratorFunc(context), - // The OrderedResourcesGenerator should be executed after all resources are generated. - NewOrderedResourcesGeneratorFunc(), + NewNamespaceGeneratorFunc(namespace), + workload.NewWorkloadGeneratorFunc(&workload.Generator{ + Project: g.project.Name, + Stack: g.stack.Name, + App: g.appName, + Namespace: namespace, + Workload: g.app.Workload, + PlatformConfigs: platformConfigs, + }), } - if err := modules.CallGenerators(i, gfs...); err != nil { + if err = modules.CallGenerators(i, gfs...); err != nil { return err } - // Patcher logic patches generated resources - pfs := []modules.NewPatcherFunc{ - pattrait.NewOpsRulePatcherFunc(g.app, modulesConfig), - patmonitoring.NewMonitoringPatcherFunc(g.app, modulesConfig), + // Generate customized module resources + for t, config := range platformConfigs { + _ = modules.GeneratorRequest{ + Project: g.project.Name, + Stack: g.stack.Name, + App: g.appName, + Type: t, + Config: config, + } + + // todo: invoke kusion module generators for each module } - if err := modules.CallPatchers(i.Resources.GVKIndex(), pfs...); err != nil { + + // The OrderedResourcesGenerator should be executed after all resources are generated. + if err := modules.CallGenerators(i, NewOrderedResourcesGeneratorFunc()); err != nil { return err } diff --git a/pkg/modules/generators/app_configurations_generator_test.go b/pkg/modules/generators/app_configurations_generator_test.go index cdb09810b..219f90fee 100644 --- a/pkg/modules/generators/app_configurations_generator_test.go +++ b/pkg/modules/generators/app_configurations_generator_test.go @@ -7,9 +7,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" v1 "kusionstack.io/kusion/pkg/apis/core/v1" - appmodel "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" ) func TestAppConfigurationGenerator_Generate(t *testing.T) { @@ -119,8 +119,8 @@ func TestNewAppConfigurationGeneratorFunc(t *testing.T) { }) } -func buildMockApp() (string, *appmodel.AppConfiguration) { - return "app1", &appmodel.AppConfiguration{ +func buildMockApp() (string, *appconfiguration.AppConfiguration) { + return "app1", &appconfiguration.AppConfiguration{ Workload: &workload.Workload{ Header: workload.Header{ Type: "Service", @@ -132,7 +132,6 @@ func buildMockApp() (string, *appmodel.AppConfiguration) { { Port: 80, Protocol: "TCP", - Public: true, }, }, }, diff --git a/pkg/modules/generators/monitoring/monitoring_generator.go b/pkg/modules/generators/monitoring/monitoring_generator.go deleted file mode 100644 index 7c47ad492..000000000 --- a/pkg/modules/generators/monitoring/monitoring_generator.go +++ /dev/null @@ -1,233 +0,0 @@ -package monitoring - -import ( - "fmt" - "time" - - prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "kusionstack.io/kusion/pkg/modules/inputs" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/monitoring" - "kusionstack.io/kusion/pkg/workspace" -) - -type monitoringGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - app *inputs.AppConfiguration - modulesConfig map[string]apiv1.GenericConfig - namespace string -} - -func NewMonitoringGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - if len(ctx.Project.Name) == 0 { - return nil, fmt.Errorf("project name must not be empty") - } - - if len(ctx.Application.Name) == 0 { - return nil, fmt.Errorf("app name must not be empty") - } - return &monitoringGenerator{ - project: ctx.Project, - stack: ctx.Stack, - app: ctx.Application, - appName: ctx.Application.Name, - modulesConfig: ctx.ModuleInputs, - namespace: ctx.Namespace, - }, nil -} - -func NewMonitoringGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewMonitoringGenerator(ctx) - } -} - -func (g *monitoringGenerator) Generate(spec *apiv1.Intent) error { - if spec.Resources == nil { - spec.Resources = make(apiv1.Resources, 0) - } - // If AppConfiguration does not contain monitoring config, return - if g.app.Monitoring == nil { - return nil - } - - // Patch workspace configurations for monitoring generator. - if err := g.parseWorkspaceConfig(); err != nil { - return err - } - - if g.app.Monitoring != nil && g.app.Monitoring.OperatorMode { - if g.app.Monitoring.MonitorType == monitoring.ServiceMonitorType { - serviceMonitor, err := g.buildMonitorObject(g.app.Monitoring.MonitorType) - if err != nil { - return err - } - err = modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID( - serviceMonitor.(*prometheusv1.ServiceMonitor).TypeMeta, - serviceMonitor.(*prometheusv1.ServiceMonitor).ObjectMeta, - ), - spec, - serviceMonitor, - ) - if err != nil { - return err - } - } else if g.app.Monitoring.MonitorType == monitoring.PodMonitorType { - podMonitor, err := g.buildMonitorObject(g.app.Monitoring.MonitorType) - if err != nil { - return err - } - err = modules.AppendToIntent( - apiv1.Kubernetes, - modules.KubernetesResourceID( - podMonitor.(*prometheusv1.PodMonitor).TypeMeta, - podMonitor.(*prometheusv1.PodMonitor).ObjectMeta, - ), - spec, - podMonitor, - ) - if err != nil { - return err - } - } else { - return fmt.Errorf("MonitorType should either be service or pod %s", g.app.Monitoring.MonitorType) - } - } - - return nil -} - -// parseWorkspaceConfig parses the config items for monitoring generator in workspace configurations. -func (g *monitoringGenerator) parseWorkspaceConfig() error { - wsConfig, ok := g.modulesConfig[monitoring.ModuleName] - // If AppConfiguration contains monitoring config but workspace does not, - // respond with the error ErrEmptyModuleConfigBlock - if g.app.Monitoring != nil && !ok { - return workspace.ErrEmptyModuleConfigBlock - } - - if operatorMode, ok := wsConfig[monitoring.OperatorModeKey]; ok { - g.app.Monitoring.OperatorMode = operatorMode.(bool) - } - - if monitorType, ok := wsConfig[monitoring.MonitorTypeKey]; ok { - g.app.Monitoring.MonitorType = monitoring.MonitorType(monitorType.(string)) - } else { - g.app.Monitoring.MonitorType = monitoring.DefaultMonitorType - } - - if interval, ok := wsConfig[monitoring.IntervalKey]; ok { - g.app.Monitoring.Interval = prometheusv1.Duration(interval.(string)) - } else { - g.app.Monitoring.Interval = monitoring.DefaultInterval - } - - if timeout, ok := wsConfig[monitoring.TimeoutKey]; ok { - g.app.Monitoring.Timeout = prometheusv1.Duration(timeout.(string)) - } else { - g.app.Monitoring.Timeout = monitoring.DefaultTimeout - } - - if scheme, ok := wsConfig[monitoring.SchemeKey]; ok { - g.app.Monitoring.Scheme = scheme.(string) - } else { - g.app.Monitoring.Scheme = monitoring.DefaultScheme - } - - parsedTimeout, err := time.ParseDuration(string(g.app.Monitoring.Timeout)) - if err != nil { - return err - } - parsedInterval, err := time.ParseDuration(string(g.app.Monitoring.Interval)) - if err != nil { - return err - } - - if parsedTimeout > parsedInterval { - return monitoring.ErrTimeoutGreaterThanInterval - } - - return nil -} - -func (g *monitoringGenerator) buildMonitorObject(monitorType monitoring.MonitorType) (runtime.Object, error) { - // If Prometheus runs as an operator, it relies on Custom Resources to - // manage the scrape configs. CRs (ServiceMonitors and PodMonitors) rely on - // corresponding resources (Services and Pods) to have labels that can be - // used as part of the label selector for the CR to determine which - // service/pods to scrape from. - // Here we choose the label name kusion_monitoring_appname for two reasons: - // 1. Unlike the label validation in Kubernetes, the label name accepted by - // Prometheus cannot contain non-alphanumeric characters except underscore: - // https://github.com/prometheus/common/blob/main/model/labels.go#L94 - // 2. The name should be unique enough that is only created by Kusion and - // used to identify a certain application - monitoringLabels := map[string]string{ - "kusion_monitoring_appname": g.appName, - } - - if monitorType == monitoring.ServiceMonitorType { - serviceEndpoint := prometheusv1.Endpoint{ - Interval: g.app.Monitoring.Interval, - ScrapeTimeout: g.app.Monitoring.Timeout, - Port: g.app.Monitoring.Port, - Path: g.app.Monitoring.Path, - Scheme: g.app.Monitoring.Scheme, - } - serviceEndpointList := []prometheusv1.Endpoint{serviceEndpoint} - serviceMonitor := &prometheusv1.ServiceMonitor{ - TypeMeta: metav1.TypeMeta{ - Kind: "ServiceMonitor", - APIVersion: prometheusv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-service-monitor", modules.UniqueAppName(g.project.Name, g.stack.Name, g.appName)), - Namespace: g.namespace, - }, - Spec: prometheusv1.ServiceMonitorSpec{ - Selector: metav1.LabelSelector{ - MatchLabels: monitoringLabels, - }, - Endpoints: serviceEndpointList, - }, - } - return serviceMonitor, nil - } else if monitorType == monitoring.PodMonitorType { - podMetricsEndpoint := prometheusv1.PodMetricsEndpoint{ - Interval: g.app.Monitoring.Interval, - ScrapeTimeout: g.app.Monitoring.Timeout, - Port: g.app.Monitoring.Port, - Path: g.app.Monitoring.Path, - Scheme: g.app.Monitoring.Scheme, - } - podMetricsEndpointList := []prometheusv1.PodMetricsEndpoint{podMetricsEndpoint} - - podMonitor := &prometheusv1.PodMonitor{ - TypeMeta: metav1.TypeMeta{ - Kind: "PodMonitor", - APIVersion: prometheusv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-pod-monitor", modules.UniqueAppName(g.project.Name, g.stack.Name, g.appName)), - Namespace: g.namespace, - }, - Spec: prometheusv1.PodMonitorSpec{ - Selector: metav1.LabelSelector{ - MatchLabels: monitoringLabels, - }, - PodMetricsEndpoints: podMetricsEndpointList, - }, - } - return podMonitor, nil - } - - return nil, fmt.Errorf("MonitorType should either be service or pod %s", monitorType) -} diff --git a/pkg/modules/generators/monitoring/monitoring_generator_test.go b/pkg/modules/generators/monitoring/monitoring_generator_test.go deleted file mode 100644 index 2bbdf9b04..000000000 --- a/pkg/modules/generators/monitoring/monitoring_generator_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package monitoring - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/monitoring" -) - -type Fields struct { - project *apiv1.Project - stack *apiv1.Stack - app *inputs.AppConfiguration - ws map[string]apiv1.GenericConfig -} - -type Args struct { - spec *apiv1.Intent -} - -type TestCase struct { - name string - fields Fields - args Args - want *apiv1.Intent - wantErr bool -} - -func BuildMonitoringTestCase( - testName, projectName, stackName, appName string, - interval, timeout, path, port, scheme, monitorType string, - operatorMode, wantErr bool, -) *TestCase { - var endpointType string - var monitorKind monitoring.MonitorType - if monitorType == string(monitoring.ServiceMonitorType) { - monitorKind = "ServiceMonitor" - endpointType = "endpoints" - } else if monitorType == string(monitoring.PodMonitorType) { - monitorKind = "PodMonitor" - endpointType = "podMetricsEndpoints" - } - expectedResources := make([]apiv1.Resource, 0) - uniqueName := modules.UniqueAppName(projectName, stackName, appName) - if operatorMode { - expectedResources = []apiv1.Resource{ - { - ID: fmt.Sprintf("monitoring.coreos.com/v1:%s:%s:%s-%s-monitor", monitorKind, projectName, uniqueName, strings.ToLower(monitorType)), - Type: "Kubernetes", - Attributes: map[string]interface{}{ - "apiVersion": "monitoring.coreos.com/v1", - "kind": string(monitorKind), - "metadata": map[string]interface{}{ - "creationTimestamp": nil, - "name": fmt.Sprintf("%s-%s-monitor", uniqueName, strings.ToLower(monitorType)), - "namespace": projectName, - }, - "spec": map[string]interface{}{ - endpointType: []interface{}{ - map[string]interface{}{ - "bearerTokenSecret": map[string]interface{}{ - "key": "", - }, - "interval": interval, - "scrapeTimeout": timeout, - "path": path, - "port": port, - "scheme": scheme, - }, - }, - "namespaceSelector": make(map[string]interface{}), - "selector": map[string]interface{}{ - "matchLabels": map[string]interface{}{ - "kusion_monitoring_appname": appName, - }, - }, - }, - }, - DependsOn: nil, - Extensions: map[string]interface{}{ - "GVK": fmt.Sprintf("monitoring.coreos.com/v1, Kind=%s", string(monitorKind)), - }, - }, - } - } - testCase := &TestCase{ - name: testName, - fields: Fields{ - project: &apiv1.Project{ - Name: projectName, - }, - stack: &apiv1.Stack{ - Name: stackName, - }, - app: &inputs.AppConfiguration{ - Name: appName, - Monitoring: &monitoring.Monitor{ - Path: path, - Port: port, - }, - }, - ws: map[string]apiv1.GenericConfig{ - "monitoring": { - "operatorMode": operatorMode, - "monitorType": monitorType, - "scheme": scheme, - "interval": interval, - "timeout": timeout, - }, - }, - }, - args: Args{ - spec: &apiv1.Intent{}, - }, - want: &apiv1.Intent{ - Resources: expectedResources, - }, - wantErr: wantErr, - } - return testCase -} - -func TestMonitoringGenerator_Generate(t *testing.T) { - tests := []TestCase{ - *BuildMonitoringTestCase("ServiceMonitorTest", "test-project", "test-stack", "test-app", "15s", "5s", "/metrics", "web", "http", "Service", true, false), - *BuildMonitoringTestCase("PodMonitorTest", "test-project", "test-stack", "test-app", "15s", "5s", "/metrics", "web", "http", "Pod", true, false), - *BuildMonitoringTestCase("ServiceAnnotationTest", "test-project", "test-stack", "test-app", "30s", "15s", "/metrics", "8080", "http", "Service", false, false), - *BuildMonitoringTestCase("PodAnnotationTest", "test-project", "test-stack", "test-app", "30s", "15s", "/metrics", "8080", "http", "Pod", false, false), - *BuildMonitoringTestCase("InvalidDurationTest", "test-project", "test-stack", "test-app", "15s", "5ssss", "/metrics", "8080", "http", "Pod", false, true), - *BuildMonitoringTestCase("InvalidTimeoutTest", "test-project", "test-stack", "test-app", "15s", "30s", "/metrics", "8080", "http", "Pod", false, true), - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := &monitoringGenerator{ - project: tt.fields.project, - stack: tt.fields.stack, - appName: tt.fields.app.Name, - app: tt.fields.app, - modulesConfig: tt.fields.ws, - namespace: tt.fields.project.Name, - } - if err := g.Generate(tt.args.spec); (err != nil) != tt.wantErr { - t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) - } - if !tt.wantErr { - require.Equal(t, tt.want, tt.args.spec) - } - }) - } -} diff --git a/pkg/modules/generators/namespace_generator.go b/pkg/modules/generators/namespace_generator.go index 96d6e4e61..efde976d1 100644 --- a/pkg/modules/generators/namespace_generator.go +++ b/pkg/modules/generators/namespace_generator.go @@ -9,18 +9,18 @@ import ( ) type namespaceGenerator struct { - context modules.GeneratorContext + namespace string } -func NewNamespaceGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { +func NewNamespaceGenerator(namespace string) (modules.Generator, error) { return &namespaceGenerator{ - context: ctx, + namespace: namespace, }, nil } -func NewNamespaceGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { +func NewNamespaceGeneratorFunc(namespace string) modules.NewGeneratorFunc { return func() (modules.Generator, error) { - return NewNamespaceGenerator(ctx) + return NewNamespaceGenerator(namespace) } } @@ -34,7 +34,7 @@ func (g *namespaceGenerator) Generate(i *apiv1.Intent) error { Kind: "Namespace", APIVersion: corev1.SchemeGroupVersion.String(), }, - ObjectMeta: metav1.ObjectMeta{Name: g.context.Namespace}, + ObjectMeta: metav1.ObjectMeta{Name: g.namespace}, } // Avoid generating duplicate namespaces with the same ID. diff --git a/pkg/modules/generators/namespace_generator_test.go b/pkg/modules/generators/namespace_generator_test.go index abe398409..9d098162a 100644 --- a/pkg/modules/generators/namespace_generator_test.go +++ b/pkg/modules/generators/namespace_generator_test.go @@ -58,7 +58,7 @@ func Test_namespaceGenerator_Generate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := modules.GeneratorContext{ + ctx := modules.GeneratorRequest{ Namespace: tt.fields.namespace, } g := &namespaceGenerator{ diff --git a/pkg/modules/generators/trait/ops_rule_generator.go b/pkg/modules/generators/trait/ops_rule_generator.go deleted file mode 100644 index cf5e83ded..000000000 --- a/pkg/modules/generators/trait/ops_rule_generator.go +++ /dev/null @@ -1,83 +0,0 @@ -package trait - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "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 - modulesConfig map[string]apiv1.GenericConfig - namespace string -} - -func NewOpsRuleGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - return &opsRuleGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - app: ctx.Application, - modulesConfig: ctx.ModuleInputs, - }, nil -} - -func NewOpsRuleGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewOpsRuleGenerator(ctx) - } -} - -func (g *opsRuleGenerator) Generate(spec *apiv1.Intent) error { - // opsRule does not exist in AppConfig and workspace config - if g.app.OpsRule == nil && g.modulesConfig[trait.OpsRuleConst] == nil { - return nil - } - - // Job does not support maxUnavailable - if g.app.Workload.Header.Type == workload.TypeJob { - return nil - } - - if g.app.Workload.Service.Type == workload.TypeCollaset { - maxUnavailable, err := trait.GetMaxUnavailable(g.app.OpsRule, g.modulesConfig) - if err != nil { - return err - } - resource := &v1alpha1.PodTransitionRule{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1alpha1.GroupVersion.String(), - Kind: "PodTransitionRule", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: modules.UniqueAppName(g.project.Name, g.stack.Name, g.appName), - Namespace: g.namespace, - }, - Spec: v1alpha1.PodTransitionRuleSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: modules.UniqueAppLabels(g.project.Name, g.appName), - }, - Rules: []v1alpha1.TransitionRule{ - { - Name: "maxUnavailable", - TransitionRuleDefinition: v1alpha1.TransitionRuleDefinition{ - AvailablePolicy: &v1alpha1.AvailableRule{ - MaxUnavailableValue: &maxUnavailable, - }, - }, - }, - }, - }, - } - return modules.AppendToIntent(apiv1.Kubernetes, modules.KubernetesResourceID(resource.TypeMeta, resource.ObjectMeta), spec, resource) - } - return nil -} diff --git a/pkg/modules/generators/trait/ops_rule_generator_test.go b/pkg/modules/generators/trait/ops_rule_generator_test.go deleted file mode 100644 index 62b704db5..000000000 --- a/pkg/modules/generators/trait/ops_rule_generator_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package trait - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - 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" -) - -func Test_opsRuleGenerator_Generate(t *testing.T) { - type fields struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - app *appmodule.AppConfiguration - workspaceConfig map[string]apiv1.GenericConfig - } - type args struct { - spec *apiv1.Intent - } - project := &apiv1.Project{ - Name: "default", - } - stack := &apiv1.Stack{ - Name: "dev", - } - appName := "foo" - tests := []struct { - name string - fields fields - args args - wantErr bool - exp *apiv1.Intent - }{ - { - name: "test Job", - fields: fields{ - project: project, - stack: stack, - appName: appName, - app: &appmodule.AppConfiguration{ - Workload: &workload.Workload{ - Header: workload.Header{ - Type: workload.TypeJob, - }, - }, - OpsRule: &trait.OpsRule{ - MaxUnavailable: "30%", - }, - }, - }, - args: args{ - spec: &apiv1.Intent{}, - }, - wantErr: false, - exp: &apiv1.Intent{}, - }, - { - name: "test CollaSet with opsRule in AppConfig", - 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, - }, - }, - OpsRule: &trait.OpsRule{ - MaxUnavailable: "30%", - }, - }, - }, - 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": "30%", - }, - "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", - }, - }, - }, - }, - }, - { - 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, - modulesConfig: tt.fields.workspaceConfig, - namespace: tt.fields.project.Name, - } - err := g.Generate(tt.args.spec) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - 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", - } - app := &appmodule.AppConfiguration{ - Name: "beep", - } - - type args struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - app *appmodule.AppConfiguration - ws map[string]apiv1.GenericConfig - } - tests := []struct { - name string - args args - wantErr bool - want *opsRuleGenerator - }{ - { - name: "test1", - args: args{ - project: p, - stack: s, - appName: "", - app: app, - ws: map[string]apiv1.GenericConfig{ - "opsRule": { - "maxUnavailable": "30%", - }, - }, - }, - wantErr: false, - want: &opsRuleGenerator{ - project: p, - stack: s, - appName: "beep", - app: app, - modulesConfig: map[string]apiv1.GenericConfig{ - "opsRule": { - "maxUnavailable": "30%", - }, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - context := modules.GeneratorContext{ - Project: tt.args.project, - Stack: tt.args.stack, - Application: tt.args.app, - Namespace: tt.args.project.Name, - ModuleInputs: tt.args.ws, - } - f := NewOpsRuleGeneratorFunc(context) - g, err := f() - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tt.want, g) - } - }) - } -} diff --git a/pkg/modules/generators/workload/job_generator.go b/pkg/modules/generators/workload/job_generator.go index 04578a03c..c78943217 100644 --- a/pkg/modules/generators/workload/job_generator.go +++ b/pkg/modules/generators/workload/job_generator.go @@ -8,33 +8,33 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/workload" ) type jobGenerator struct { - project *apiv1.Project - stack *apiv1.Stack + project string + stack string appName string job *workload.Job jobConfig apiv1.GenericConfig namespace string } -func NewJobGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { +func NewJobGenerator(generator *Generator) (modules.Generator, error) { return &jobGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - job: ctx.Application.Workload.Job, - jobConfig: ctx.ModuleInputs[workload.ModuleJob], - namespace: ctx.Namespace, + project: generator.Project, + stack: generator.Stack, + appName: generator.App, + job: generator.Workload.Job, + jobConfig: generator.PlatformConfigs[workload.ModuleJob], + namespace: generator.Namespace, }, nil } -func NewJobGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { +func NewJobGeneratorFunc(generator *Generator) modules.NewGeneratorFunc { return func() (modules.Generator, error) { - return NewJobGenerator(ctx) + return NewJobGenerator(generator) } } @@ -52,13 +52,13 @@ func (g *jobGenerator) Generate(spec *apiv1.Intent) error { return fmt.Errorf("complete job input by workspace config failed, %w", err) } - uniqueAppName := modules.UniqueAppName(g.project.Name, g.stack.Name, g.appName) + uniqueAppName := modules.UniqueAppName(g.project, g.stack, g.appName) meta := metav1.ObjectMeta{ Namespace: g.namespace, Name: uniqueAppName, Labels: modules.MergeMaps( - modules.UniqueAppLabels(g.project.Name, g.appName), + modules.UniqueAppLabels(g.project, g.appName), g.job.Labels, ), Annotations: modules.MergeMaps( @@ -88,7 +88,7 @@ func (g *jobGenerator) Generate(spec *apiv1.Intent) error { Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: modules.MergeMaps( - modules.UniqueAppLabels(g.project.Name, g.appName), + modules.UniqueAppLabels(g.project, g.appName), g.job.Labels, ), Annotations: modules.MergeMaps( diff --git a/pkg/modules/generators/workload/job_generator_test.go b/pkg/modules/generators/workload/job_generator_test.go index 0c3803709..2d983678c 100644 --- a/pkg/modules/generators/workload/job_generator_test.go +++ b/pkg/modules/generators/workload/job_generator_test.go @@ -7,53 +7,35 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" ) -func newGeneratorContextWithJob( - project *apiv1.Project, - stack *apiv1.Stack, - appName string, - job *workload.Job, - jobConfig apiv1.GenericConfig, -) modules.GeneratorContext { - application := &inputs.AppConfiguration{ - Name: appName, - Workload: &workload.Workload{ - Job: job, - }, - } - moduleInputs := map[string]apiv1.GenericConfig{ - workload.ModuleJob: jobConfig, - } - return modules.GeneratorContext{ - Project: project, - Stack: stack, - Application: application, - Namespace: project.Name, - ModuleInputs: moduleInputs, - } -} - func TestNewJobGenerator(t *testing.T) { - expectedProject := &apiv1.Project{ - Name: "test", - } - expectedStack := &apiv1.Stack{} + expectedProject := "test" + expectedStack := "dev" expectedAppName := "test" expectedJob := &workload.Job{} expectedJobConfig := apiv1.GenericConfig{ "labels": apiv1.GenericConfig{ - "workload-type": "Job", + "Workload-type": "Job", }, "annotations": apiv1.GenericConfig{ - "workload-type": "Job", + "Workload-type": "Job", }, } - ctx := newGeneratorContextWithJob(expectedProject, expectedStack, expectedAppName, expectedJob, expectedJobConfig) - actual, err := NewJobGenerator(ctx) + actual, err := NewJobGenerator(&Generator{ + Project: expectedProject, + Stack: expectedStack, + App: expectedAppName, + Namespace: expectedAppName, + Workload: &workload.Workload{ + Job: expectedJob, + }, + PlatformConfigs: map[string]apiv1.GenericConfig{ + workload.ModuleJob: expectedJobConfig, + }, + }) assert.NoError(t, err, "Error should be nil") assert.NotNil(t, actual, "Generator should not be nil") @@ -65,10 +47,8 @@ func TestNewJobGenerator(t *testing.T) { } func TestNewJobGeneratorFunc(t *testing.T) { - expectedProject := &apiv1.Project{ - Name: "test", - } - expectedStack := &apiv1.Stack{} + expectedProject := "test" + expectedStack := "dev" expectedAppName := "test" expectedJob := &workload.Job{} expectedJobConfig := apiv1.GenericConfig{ @@ -79,8 +59,18 @@ func TestNewJobGeneratorFunc(t *testing.T) { "workload-type": "Job", }, } - ctx := newGeneratorContextWithJob(expectedProject, expectedStack, expectedAppName, expectedJob, expectedJobConfig) - generatorFunc := NewJobGeneratorFunc(ctx) + generatorFunc := NewJobGeneratorFunc(&Generator{ + Project: expectedProject, + Stack: expectedStack, + App: expectedAppName, + Namespace: expectedAppName, + Workload: &workload.Workload{ + Job: expectedJob, + }, + PlatformConfigs: map[string]apiv1.GenericConfig{ + workload.ModuleJob: expectedJobConfig, + }, + }) actualGenerator, err := generatorFunc() assert.NoError(t, err, "Error should be nil") @@ -95,18 +85,16 @@ func TestNewJobGeneratorFunc(t *testing.T) { func TestJobGenerator_Generate(t *testing.T) { testCases := []struct { name string - expectedProject *apiv1.Project - expectedStack *apiv1.Stack + expectedProject string + expectedStack string expectedAppName string expectedJob *workload.Job expectedJobConfig apiv1.GenericConfig }{ { - name: "test generate", - expectedProject: &apiv1.Project{ - Name: "test", - }, - expectedStack: &apiv1.Stack{}, + name: "test generate", + expectedProject: "test", + expectedStack: "dev", expectedAppName: "test", expectedJob: &workload.Job{}, expectedJobConfig: apiv1.GenericConfig{ @@ -122,8 +110,18 @@ func TestJobGenerator_Generate(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - ctx := newGeneratorContextWithJob(tc.expectedProject, tc.expectedStack, tc.expectedAppName, tc.expectedJob, tc.expectedJobConfig) - generator, _ := NewJobGenerator(ctx) + generator, _ := NewJobGenerator(&Generator{ + Project: tc.expectedProject, + Stack: tc.expectedStack, + App: tc.expectedAppName, + Namespace: tc.expectedAppName, + Workload: &workload.Workload{ + Job: tc.expectedJob, + }, + PlatformConfigs: map[string]apiv1.GenericConfig{ + workload.ModuleJob: tc.expectedJobConfig, + }, + }) spec := &apiv1.Intent{} err := generator.Generate(spec) @@ -136,9 +134,9 @@ func TestJobGenerator_Generate(t *testing.T) { actual := mapToUnstructured(resource.Attributes) assert.Equal(t, "Job", actual.GetKind(), "Kind mismatch") - assert.Equal(t, tc.expectedProject.Name, actual.GetNamespace(), "Namespace mismatch") - assert.Equal(t, modules.UniqueAppName(tc.expectedProject.Name, tc.expectedStack.Name, tc.expectedAppName), actual.GetName(), "Name mismatch") - assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(tc.expectedProject.Name, tc.expectedAppName), tc.expectedJob.Labels), actual.GetLabels(), "Labels mismatch") + assert.Equal(t, tc.expectedProject, actual.GetNamespace(), "Namespace mismatch") + assert.Equal(t, modules.UniqueAppName(tc.expectedProject, tc.expectedStack, tc.expectedAppName), actual.GetName(), "Name mismatch") + assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(tc.expectedProject, tc.expectedAppName), tc.expectedJob.Labels), actual.GetLabels(), "Labels mismatch") assert.Equal(t, modules.MergeMaps(tc.expectedJob.Annotations), actual.GetAnnotations(), "Annotations mismatch") }) } diff --git a/pkg/modules/generators/workload/network/ports_generator.go b/pkg/modules/generators/workload/network/ports_generator.go deleted file mode 100644 index 4edf7bcbe..000000000 --- a/pkg/modules/generators/workload/network/ports_generator.go +++ /dev/null @@ -1,303 +0,0 @@ -package network - -import ( - "errors" - "fmt" - "strings" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" - "kusionstack.io/kusion/pkg/workspace" -) - -const ( - k8sKindService = "Service" - suffixPublic = "public" - suffixPrivate = "private" -) - -var ( - ErrEmptyAppName = errors.New("app name must not be empty") - ErrEmptyProjectName = errors.New("project name must not be empty") - ErrEmptyStackName = errors.New("stack name must not be empty") - ErrEmptySelectors = errors.New("selectors must not be empty") - ErrEmptyPorts = errors.New("ports must not be empty") - ErrEmptyType = errors.New("type must not be empty when public") - ErrUnsupportedType = errors.New("type only support alicloud and aws for now") - ErrInvalidPort = errors.New("port must be between 1 and 65535") - ErrInvalidTargetPort = errors.New("targetPort must be between 1 and 65535 if exist") - ErrInvalidProtocol = errors.New("protocol must be TCP or UDP") - ErrDuplicatePortProtocol = errors.New("port-protocol pair must not be duplicate") - ErrUnsupportedPortConfigItem = errors.New("unsupported item for port workspace config") - ErrEmptyPortConfig = errors.New("empty port config") -) - -// portsGenerator is used to generate k8s service. -type portsGenerator struct { - appName string - projectName string - stackName string - selector map[string]string - labels map[string]string - annotations map[string]string - ports []network.Port - portConfig apiv1.GenericConfig - namespace string -} - -// NewPortsGenerator returns a new portsGenerator instance, and do the validation and completion job. -func NewPortsGenerator( - ctx modules.GeneratorContext, - selectors, labels, annotations map[string]string, -) (modules.Generator, error) { - generator := &portsGenerator{ - appName: ctx.Application.Name, - projectName: ctx.Project.Name, - stackName: ctx.Stack.Name, - selector: selectors, - labels: labels, - annotations: annotations, - ports: ctx.Application.Workload.Service.Ports, - portConfig: ctx.ModuleInputs[network.ModulePort], - namespace: ctx.Namespace, - } - - if err := generator.validate(); err != nil { - return nil, err - } - if err := generator.complete(); err != nil { - return nil, err - } - - return generator, nil -} - -// NewPortsGeneratorFunc returns a new NewGeneratorFunc that returns a portsGenerator instance. -func NewPortsGeneratorFunc( - ctx modules.GeneratorContext, - selectors, labels, annotations map[string]string, -) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewPortsGenerator(ctx, selectors, labels, annotations) - } -} - -// Generate renders k8s ClusterIP or LoadBalancer service from the portsGenerator. -func (g *portsGenerator) Generate(spec *apiv1.Intent) error { - privatePorts, publicPorts := splitPorts(g.ports) - if len(privatePorts) != 0 { - svc := g.generateK8sSvc(false, privatePorts) - if err := appendToSpec(spec, svc); err != nil { - return err - } - } - if len(publicPorts) != 0 { - svc := g.generateK8sSvc(true, publicPorts) - if err := appendToSpec(spec, svc); err != nil { - return err - } - } - return nil -} - -func (g *portsGenerator) validate() error { - if g.appName == "" { - return ErrEmptyAppName - } - if g.projectName == "" { - return ErrEmptyProjectName - } - if g.stackName == "" { - return ErrEmptyStackName - } - if len(g.selector) == 0 { - return ErrEmptySelectors - } - if len(g.ports) == 0 { - return ErrEmptyPorts - } - if err := validatePorts(g.ports); err != nil { - return err - } - if err := validatePortConfig(g.portConfig); err != nil { - return err - } - return nil -} - -func (g *portsGenerator) complete() error { - for i := range g.ports { - if err := completePort(&g.ports[i], g.portConfig); err != nil { - return err - } - } - return nil -} - -func (g *portsGenerator) generateK8sSvc(public bool, ports []network.Port) *v1.Service { - appUname := modules.UniqueAppName(g.projectName, g.stackName, g.appName) - var name string - if public { - name = fmt.Sprintf("%s-%s", appUname, suffixPublic) - } else { - name = fmt.Sprintf("%s-%s", appUname, suffixPrivate) - } - svcType := v1.ServiceTypeClusterIP - if public { - svcType = v1.ServiceTypeLoadBalancer - } - - svc := &v1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: k8sKindService, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: g.namespace, - Labels: g.labels, - Annotations: g.annotations, - }, - Spec: v1.ServiceSpec{ - Ports: toSvcPorts(name, ports), - Selector: g.selector, - Type: svcType, - }, - } - - if public { - if len(svc.Labels) == 0 { - svc.Labels = make(map[string]string) - } - if len(svc.Annotations) == 0 { - svc.Annotations = make(map[string]string) - } - - labels := ports[0].Labels - for k, v := range labels { - svc.Labels[k] = v - } - annotations := ports[0].Annotations - for k, v := range annotations { - svc.Annotations[k] = v - } - } - - return svc -} - -func validatePorts(ports []network.Port) error { - portProtocolRecord := make(map[string]struct{}) - // portType is the correct type for public port, it gets assigned a value when calling validatePort. - for _, port := range ports { - if err := validatePort(&port); err != nil { - return fmt.Errorf("invalid port config %+v, %w", port, err) - } - - // duplicate "port-protocol" pairs are not allowed. - portProtocol := fmt.Sprintf("%d-%s", port.Port, port.Protocol) - if _, ok := portProtocolRecord[portProtocol]; ok { - return fmt.Errorf("invalid port config %+v, %v", port, ErrDuplicatePortProtocol) - } - portProtocolRecord[portProtocol] = struct{}{} - } - return nil -} - -func validatePort(port *network.Port) error { - if port.Port < 1 || port.Port > 65535 { - return ErrInvalidPort - } - if port.TargetPort < 0 || port.Port > 65535 { - return ErrInvalidTargetPort - } - if port.Protocol != network.ProtocolTCP && port.Protocol != network.ProtocolUDP { - return ErrInvalidProtocol - } - return nil -} - -func validatePortConfig(portConfig apiv1.GenericConfig) error { - if portConfig == nil { - return nil - } - for k := range portConfig { - if k != network.FieldType && k != network.FieldLabels && k != network.FieldAnnotations { - return fmt.Errorf("%w, %s", ErrUnsupportedPortConfigItem, k) - } - } - return nil -} - -func completePort(port *network.Port, portConfig apiv1.GenericConfig) error { - if port.TargetPort == 0 { - port.TargetPort = port.Port - } - if port.Public { - // get port type from workspace - if portConfig == nil { - return ErrEmptyPortConfig - } - portType, err := workspace.GetStringFromGenericConfig(portConfig, network.FieldType) - if err != nil { - return err - } - if portType == "" { - return ErrEmptyType - } - if portType != network.CSPAliCloud && portType != network.CSPAWS { - return ErrUnsupportedType - } - port.Type = portType - - // get labels from workspace - labels, err := workspace.GetStringMapFromGenericConfig(portConfig, network.FieldLabels) - if err != nil { - return err - } - port.Labels = labels - - // get annotations from workspace - annotations, err := workspace.GetStringMapFromGenericConfig(portConfig, network.FieldAnnotations) - if err != nil { - return err - } - port.Annotations = annotations - } - return nil -} - -func splitPorts(ports []network.Port) ([]network.Port, []network.Port) { - var privatePorts, publicPorts []network.Port - for _, port := range ports { - if port.Public { - publicPorts = append(publicPorts, port) - } else { - privatePorts = append(privatePorts, port) - } - } - return privatePorts, publicPorts -} - -func toSvcPorts(name string, ports []network.Port) []v1.ServicePort { - svcPorts := make([]v1.ServicePort, len(ports)) - for i, port := range ports { - svcPorts[i] = v1.ServicePort{ - Name: fmt.Sprintf("%s-%d-%s", name, port.Port, strings.ToLower(port.Protocol)), - Port: int32(port.Port), - TargetPort: intstr.FromInt(port.TargetPort), - Protocol: v1.Protocol(port.Protocol), - } - } - return svcPorts -} - -func appendToSpec(spec *apiv1.Intent, svc *v1.Service) error { - id := modules.KubernetesResourceID(svc.TypeMeta, svc.ObjectMeta) - return modules.AppendToIntent(apiv1.Kubernetes, id, spec, svc) -} diff --git a/pkg/modules/generators/workload/network/ports_generator_test.go b/pkg/modules/generators/workload/network/ports_generator_test.go deleted file mode 100644 index 07a4ff879..000000000 --- a/pkg/modules/generators/workload/network/ports_generator_test.go +++ /dev/null @@ -1,363 +0,0 @@ -package network - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" -) - -func TestValidatePorts(t *testing.T) { - type args struct { - ports []network.Port - } - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "invalid_ports", - args: struct { - ports []network.Port - }{ - ports: []network.Port{ - { - Port: 80, - Protocol: "TCP", - }, - { - Port: 80, - Protocol: "UDP", - }, - { - Port: 80, - TargetPort: 8080, - Protocol: "TCP", - }, - }, - }, - wantErr: true, - }, - { - name: "valid_ports", - args: struct { - ports []network.Port - }{ - ports: []network.Port{ - { - Port: 80, - Protocol: "TCP", - }, - { - Port: 9090, - TargetPort: 8080, - Protocol: "UDP", - }, - }, - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validatePorts(tt.args.ports); (err != nil) != tt.wantErr { - t.Errorf("validatePorts() error = %x, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestValidatePortConfig(t *testing.T) { - testcases := []struct { - name string - portConfig apiv1.GenericConfig - success bool - }{ - { - name: "valid port config", - portConfig: apiv1.GenericConfig{ - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, - success: true, - }, - { - name: "invalid port config unsupported item", - portConfig: apiv1.GenericConfig{ - "unsupported": "unsupported", - }, - success: false, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - err := validatePortConfig(tc.portConfig) - assert.Equal(t, tc.success, err == nil) - }) - } -} - -func TestCompletePort(t *testing.T) { - testcases := []struct { - name string - port *network.Port - portConfig apiv1.GenericConfig - success bool - completedPort *network.Port - }{ - { - name: "complete target port", - port: &network.Port{ - Port: 80, - Protocol: "TCP", - }, - portConfig: nil, - success: true, - completedPort: &network.Port{ - Port: 80, - TargetPort: 80, - Protocol: "TCP", - }, - }, - { - name: "complete type", - port: &network.Port{ - Port: 80, - Protocol: "TCP", - Public: true, - }, - portConfig: apiv1.GenericConfig{ - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, - success: true, - completedPort: &network.Port{ - Type: "alicloud", - Port: 80, - TargetPort: 80, - Protocol: "TCP", - Public: true, - Labels: map[string]string{ - "kusionstack.io/control": "true", - }, - Annotations: map[string]string{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, - }, - { - name: "complete failed invalid port type", - port: &network.Port{ - Port: 80, - Protocol: "TCP", - Public: true, - }, - portConfig: apiv1.GenericConfig{ - "type": "unsupported", - }, - success: false, - completedPort: nil, - }, - { - name: "complete failed empty port config", - port: &network.Port{ - Port: 80, - Protocol: "TCP", - Public: true, - }, - portConfig: nil, - success: false, - completedPort: nil, - }, - { - name: "complete failed type not exist", - port: &network.Port{ - Port: 80, - Protocol: "TCP", - Public: true, - }, - portConfig: apiv1.GenericConfig{}, - success: false, - completedPort: nil, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - err := completePort(tc.port, tc.portConfig) - assert.Equal(t, tc.success, err == nil) - if tc.success { - assert.True(t, reflect.DeepEqual(tc.completedPort, tc.port)) - } - }) - } -} - -func TestPortsGenerator_Generate(t *testing.T) { - type fields struct { - portsGenerator - } - type args struct { - spec *apiv1.Intent - } - - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "ports_generate", - fields: struct { - portsGenerator - }{ - portsGenerator{ - appName: "testApp", - projectName: "testProject", - stackName: "testStack", - selector: map[string]string{ - "test-s-key": "test-s-value", - }, - labels: map[string]string{ - "test-l-key": "test-l-value", - }, - annotations: map[string]string{ - "test-a-key": "test-a-value", - }, - ports: []network.Port{ - { - Port: 80, - TargetPort: 80, - Protocol: "TCP", - Public: true, - }, - { - Port: 9090, - TargetPort: 8080, - Protocol: "UDP", - Public: false, - }, - }, - portConfig: apiv1.GenericConfig{ - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, - namespace: "testProject", - }, - }, - args: struct { - spec *apiv1.Intent - }{ - spec: &apiv1.Intent{}, - }, - wantErr: false, - }, - } - - privateSvc := &v1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: k8sKindService, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testProject-testStack-testApp-private", - Namespace: "testProject", - Labels: map[string]string{ - "test-l-key": "test-l-value", - }, - Annotations: map[string]string{ - "test-a-key": "test-a-value", - }, - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "testProject-testStack-testApp-private-9090-udp", - Port: 9090, - TargetPort: intstr.FromInt(8080), - Protocol: v1.ProtocolUDP, - }, - }, - Selector: map[string]string{ - "test-s-key": "test-s-value", - }, - Type: v1.ServiceTypeClusterIP, - }, - } - unstructuredPrivateSvc, err := runtime.DefaultUnstructuredConverter.ToUnstructured(privateSvc) - assert.NoError(t, err) - - publicSvc := &v1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: k8sKindService, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testProject-testStack-testApp-public", - Namespace: "testProject", - Labels: map[string]string{ - "test-l-key": "test-l-value", - "kusionstack.io/control": "true", - }, - Annotations: map[string]string{ - "test-a-key": "test-a-value", - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "testProject-testStack-testApp-public-80-tcp", - Port: 80, - TargetPort: intstr.FromInt(80), - Protocol: v1.ProtocolTCP, - }, - }, - Selector: map[string]string{ - "test-s-key": "test-s-value", - }, - Type: v1.ServiceTypeLoadBalancer, - }, - } - unstructuredPublicSvc, err := runtime.DefaultUnstructuredConverter.ToUnstructured(publicSvc) - assert.NoError(t, err) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := &tt.fields.portsGenerator - _ = g.complete() - if err = g.Generate(tt.args.spec); (err != nil) != tt.wantErr { - t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) - } - assert.Equal(t, unstructuredPrivateSvc, tt.args.spec.Resources[0].Attributes) - assert.Equal(t, unstructuredPublicSvc, tt.args.spec.Resources[1].Attributes) - }) - } -} diff --git a/pkg/modules/generators/workload/secret/secret_generator.go b/pkg/modules/generators/workload/secret/secret_generator.go index 84c55b1a5..d637ab736 100644 --- a/pkg/modules/generators/workload/secret/secret_generator.go +++ b/pkg/modules/generators/workload/secret/secret_generator.go @@ -13,41 +13,52 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs/workload" "kusionstack.io/kusion/pkg/secrets" ) type secretGenerator struct { - project *apiv1.Project + project string namespace string secrets map[string]workload.Secret secretStoreSpec *apiv1.SecretStoreSpec } -func NewSecretGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - if len(ctx.Project.Name) == 0 { +type GeneratorRequest struct { + // Project represents the Project name + Project string + // Namespace represents the K8s Namespace + Namespace string + // Workload represents the Workload configuration + Workload *workload.Workload + // SecretStoreSpec contains configuration to describe target secret store. + SecretStoreSpec *apiv1.SecretStoreSpec +} + +func NewSecretGenerator(request *GeneratorRequest) (modules.Generator, error) { + if len(request.Project) == 0 { return nil, fmt.Errorf("project name must not be empty") } var secretMap map[string]workload.Secret - if ctx.Application.Workload.Service != nil { - secretMap = ctx.Application.Workload.Service.Secrets + if request.Workload.Service != nil { + secretMap = request.Workload.Service.Secrets } else { - secretMap = ctx.Application.Workload.Job.Secrets + secretMap = request.Workload.Job.Secrets } return &secretGenerator{ - project: ctx.Project, + project: request.Project, secrets: secretMap, - namespace: ctx.Namespace, - secretStoreSpec: ctx.SecretStoreSpec, + namespace: request.Namespace, + secretStoreSpec: request.SecretStoreSpec, }, nil } -func NewSecretGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { +func NewSecretGeneratorFunc(request *GeneratorRequest) modules.NewGeneratorFunc { return func() (modules.Generator, error) { - return NewSecretGenerator(ctx) + return NewSecretGenerator(request) } } diff --git a/pkg/modules/generators/workload/secret/secret_generator_test.go b/pkg/modules/generators/workload/secret/secret_generator_test.go index 2e4ca5138..25062d60a 100644 --- a/pkg/modules/generators/workload/secret/secret_generator_test.go +++ b/pkg/modules/generators/workload/secret/secret_generator_test.go @@ -7,34 +7,28 @@ import ( "github.com/stretchr/testify/require" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" // ensure we can get correct secret store provider _ "kusionstack.io/kusion/pkg/secrets/providers/register" ) -var testProject = &apiv1.Project{ - Name: "helloworld", -} +var testProject = "helloworld" -func initGeneratorContext( - project *apiv1.Project, +func initGeneratorRequest( + project string, secrets map[string]workload.Secret, secretStoreSpec *apiv1.SecretStoreSpec, -) modules.GeneratorContext { - return modules.GeneratorContext{ +) *GeneratorRequest { + return &GeneratorRequest{ Project: project, - Application: &inputs.AppConfiguration{ - Workload: &workload.Workload{ - Service: &workload.Service{ - Base: workload.Base{ - Secrets: secrets, - }, + Workload: &workload.Workload{ + Service: &workload.Service{ + Base: workload.Base{ + Secrets: secrets, }, }, }, - Namespace: project.Name, + Namespace: project, SecretStoreSpec: secretStoreSpec, } } @@ -118,7 +112,7 @@ func TestGenerateSecret(t *testing.T) { Data: test.secretData, }, } - context := initGeneratorContext(testProject, secrets, nil) + context := initGeneratorRequest(testProject, secrets, nil) generator, _ := NewSecretGenerator(context) err := generator.Generate(&apiv1.Intent{}) if test.expectErr == "" { @@ -183,7 +177,7 @@ func TestGenerateSecretWithExternalRef(t *testing.T) { }, } secretStoreSpec := initSecretStoreSpec(test.providerData) - context := initGeneratorContext(testProject, secrets, secretStoreSpec) + context := initGeneratorRequest(testProject, secrets, secretStoreSpec) generator, _ := NewSecretGenerator(context) err := generator.Generate(&apiv1.Intent{}) if test.expectErr == "" { diff --git a/pkg/modules/generators/workload/service_generator.go b/pkg/modules/generators/workload/service_generator.go index 688c48f72..684ca3052 100644 --- a/pkg/modules/generators/workload/service_generator.go +++ b/pkg/modules/generators/workload/service_generator.go @@ -1,6 +1,7 @@ package workload import ( + "errors" "fmt" appsv1 "k8s.io/api/apps/v1" @@ -10,60 +11,64 @@ import ( "kusionstack.io/kube-api/apps/v1alpha1" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/generators/workload/network" - "kusionstack.io/kusion/pkg/modules/inputs/workload" "kusionstack.io/kusion/pkg/workspace" ) -// workloadServiceGenerator is a struct for generating service workload resources. -type workloadServiceGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - service *workload.Service - serviceConfig apiv1.GenericConfig - namespace string - - // for internal generator - context modules.GeneratorContext +var ( + ErrEmptySelectors = errors.New("selectors must not be empty") + ErrInvalidPort = errors.New("port must be between 1 and 65535") + ErrInvalidTargetPort = errors.New("targetPort must be between 1 and 65535 if exist") + ErrInvalidProtocol = errors.New("protocol must be TCP or UDP") + ErrDuplicatePortProtocol = errors.New("port-protocol pair must not be duplicate") +) + +// ServiceGenerator is a struct for generating Service Workload resources. +type ServiceGenerator struct { + Project string + Stack string + App string + Namespace string + Service *workload.Service + Config apiv1.GenericConfig } -// NewWorkloadServiceGenerator returns a new workloadServiceGenerator instance. -func NewWorkloadServiceGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - if len(ctx.Project.Name) == 0 { +// NewWorkloadServiceGenerator returns a new ServiceGenerator instance. +func NewWorkloadServiceGenerator(request *Generator) (modules.Generator, error) { + if len(request.Project) == 0 { return nil, fmt.Errorf("project name must not be empty") } - if len(ctx.Application.Name) == 0 { + if len(request.App) == 0 { return nil, fmt.Errorf("app name must not be empty") } - if ctx.Application.Workload.Service == nil { - return nil, fmt.Errorf("service workload must not be nil") + if request.Workload.Service == nil { + return nil, fmt.Errorf("service Workload must not be nil") } - return &workloadServiceGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - service: ctx.Application.Workload.Service, - serviceConfig: ctx.ModuleInputs[workload.ModuleService], - namespace: ctx.Namespace, - context: ctx, + return &ServiceGenerator{ + Project: request.Project, + Stack: request.Stack, + App: request.App, + Service: request.Workload.Service, + Config: request.PlatformConfigs[workload.ModuleService], + Namespace: request.Namespace, }, nil } -// NewWorkloadServiceGeneratorFunc returns a new NewGeneratorFunc that returns a workloadServiceGenerator instance. -func NewWorkloadServiceGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { +// NewWorkloadServiceGeneratorFunc returns a new NewGeneratorFunc that returns a ServiceGenerator instance. +func NewWorkloadServiceGeneratorFunc(workloadGenerator *Generator) modules.NewGeneratorFunc { return func() (modules.Generator, error) { - return NewWorkloadServiceGenerator(ctx) + return NewWorkloadServiceGenerator(workloadGenerator) } } -// Generate generates a service workload resource to the given spec. -func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { - service := g.service +// Generate generates a Service Workload resource to the given spec. +func (g *ServiceGenerator) Generate(spec *apiv1.Intent) error { + service := g.Service if service == nil { return nil } @@ -73,23 +78,23 @@ func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { spec.Resources = make(apiv1.Resources, 0) } - if err := completeServiceInput(g.service, g.serviceConfig); err != nil { - return fmt.Errorf("complete service input by workspace config failed, %w", err) + if err := completeServiceInput(g.Service, g.Config); err != nil { + return fmt.Errorf("complete Service input by workspace config failed, %w", err) } - uniqueAppName := modules.UniqueAppName(g.project.Name, g.stack.Name, g.appName) + uniqueAppName := modules.UniqueAppName(g.Project, g.Stack, g.App) - // Create a slice of containers based on the app's + // Create a slice of containers based on the App's // containers along with related volumes and configMaps. containers, volumes, configMaps, err := toOrderedContainers(service.Containers, uniqueAppName) if err != nil { return err } - // Create ConfigMap objects based on the app's configuration. + // Create ConfigMap objects based on the App's configuration. for _, cm := range configMaps { cmObj := cm - cmObj.Namespace = g.namespace + cmObj.Namespace = g.Namespace if err = modules.AppendToIntent( apiv1.Kubernetes, modules.KubernetesResourceID(cmObj.TypeMeta, cmObj.ObjectMeta), @@ -100,17 +105,17 @@ func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { } } - labels := modules.MergeMaps(modules.UniqueAppLabels(g.project.Name, g.appName), g.service.Labels) - annotations := modules.MergeMaps(g.service.Annotations) - selector := modules.UniqueAppLabels(g.project.Name, g.appName) + labels := modules.MergeMaps(modules.UniqueAppLabels(g.Project, g.App), g.Service.Labels) + annotations := modules.MergeMaps(g.Service.Annotations) + selectors := modules.UniqueAppLabels(g.Project, g.App) - // Create a K8s workload object based on the app's configuration. + // Create a K8s Workload object based on the App's configuration. // common parts objectMeta := metav1.ObjectMeta{ Labels: labels, Annotations: annotations, Name: uniqueAppName, - Namespace: g.namespace, + Namespace: g.Namespace, } podTemplateSpec := v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -127,14 +132,14 @@ func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { typeMeta := metav1.TypeMeta{} switch service.Type { - case workload.TypeDeployment: + case workload.Deployment: typeMeta = metav1.TypeMeta{ APIVersion: appsv1.SchemeGroupVersion.String(), - Kind: workload.TypeDeployment, + Kind: string(workload.Deployment), } spec := appsv1.DeploymentSpec{ - Replicas: modules.GenericPtr(int32(service.Replicas)), - Selector: &metav1.LabelSelector{MatchLabels: selector}, + Replicas: service.Replicas, + Selector: &metav1.LabelSelector{MatchLabels: selectors}, Template: podTemplateSpec, } resource = &appsv1.Deployment{ @@ -142,17 +147,17 @@ func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { ObjectMeta: objectMeta, Spec: spec, } - case workload.TypeCollaset: + case workload.Collaset: typeMeta = metav1.TypeMeta{ APIVersion: v1alpha1.GroupVersion.String(), - Kind: workload.TypeCollaset, + Kind: string(workload.Collaset), } resource = &v1alpha1.CollaSet{ TypeMeta: typeMeta, ObjectMeta: objectMeta, Spec: v1alpha1.CollaSetSpec{ - Replicas: modules.GenericPtr(int32(service.Replicas)), - Selector: &metav1.LabelSelector{MatchLabels: selector}, + Replicas: service.Replicas, + Selector: &metav1.LabelSelector{MatchLabels: selectors}, Template: podTemplateSpec, }, } @@ -163,14 +168,64 @@ func (g *workloadServiceGenerator) Generate(spec *apiv1.Intent) error { return err } - // generate K8s Service from ports config. - if len(g.service.Ports) != 0 { - portsGeneratorFunc := network.NewPortsGeneratorFunc(g.context, selector, labels, annotations) - if err = modules.CallGenerators(spec, portsGeneratorFunc); err != nil { + // validate and complete service ports + if len(g.Service.Ports) != 0 { + if err = validate(selectors, service.Ports); err != nil { + return err + } + if err = complete(service.Ports); err != nil { return err } } + return nil +} + +func validatePorts(ports []network.Port) error { + portProtocolRecord := make(map[string]struct{}) + for _, port := range ports { + if err := validatePort(&port); err != nil { + return fmt.Errorf("invalid port config %+v, %w", port, err) + } + + // duplicate "port-protocol" pairs are not allowed. + portProtocol := fmt.Sprintf("%d-%s", port.Port, port.Protocol) + if _, ok := portProtocolRecord[portProtocol]; ok { + return fmt.Errorf("invalid port config %+v, %v", port, ErrDuplicatePortProtocol) + } + portProtocolRecord[portProtocol] = struct{}{} + } + return nil +} +func validatePort(port *network.Port) error { + if port.Port < 1 || port.Port > 65535 { + return ErrInvalidPort + } + if port.TargetPort < 0 || port.Port > 65535 { + return ErrInvalidTargetPort + } + if port.Protocol != network.TCP && port.Protocol != network.UDP { + return ErrInvalidProtocol + } + return nil +} + +func validate(selectors map[string]string, ports []network.Port) error { + if len(selectors) == 0 { + return ErrEmptySelectors + } + if err := validatePorts(ports); err != nil { + return err + } + return nil +} + +func complete(ports []network.Port) error { + for i := range ports { + if ports[i].TargetPort == 0 { + ports[i].TargetPort = ports[i].Port + } + } return nil } @@ -178,19 +233,20 @@ func completeServiceInput(service *workload.Service, config apiv1.GenericConfig) if err := completeBaseWorkload(&service.Base, config); err != nil { return err } - serviceType, err := workspace.GetStringFromGenericConfig(config, workload.FieldType) + serviceTypeStr, err := workspace.GetStringFromGenericConfig(config, workload.ModuleServiceType) + platformServiceType := workload.ServiceType(serviceTypeStr) if err != nil { return err } // if not set in workspace, use Deployment as default type - if serviceType == "" { - serviceType = workload.TypeDeployment + if platformServiceType == "" { + platformServiceType = workload.Deployment } - if serviceType != workload.TypeDeployment && serviceType != workload.TypeCollaset { - return fmt.Errorf("unsupported service type %s", serviceType) + if platformServiceType != workload.Deployment && platformServiceType != workload.Collaset { + return fmt.Errorf("unsupported Service type %s", platformServiceType) } if service.Type == "" { - service.Type = serviceType + service.Type = platformServiceType } return nil } diff --git a/pkg/modules/generators/workload/service_generator_test.go b/pkg/modules/generators/workload/service_generator_test.go index eea1f3508..ce06a6426 100644 --- a/pkg/modules/generators/workload/service_generator_test.go +++ b/pkg/modules/generators/workload/service_generator_test.go @@ -9,11 +9,9 @@ import ( "gopkg.in/yaml.v3" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/container" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" ) func Test_workloadServiceGenerator_Generate(t *testing.T) { @@ -163,13 +161,15 @@ spec: name: default-dev-foo-nginx-0 status: {} ` + r2 := new(int32) + *r2 = 2 + type fields struct { - project *apiv1.Project - stack *apiv1.Stack + project string + stack string appName string service *workload.Service serviceConfig apiv1.GenericConfig - portConfig apiv1.GenericConfig } type args struct { spec *apiv1.Intent @@ -185,13 +185,8 @@ status: {} { name: "CollaSet", fields: fields{ - project: &apiv1.Project{ - Name: "default", - Path: "/test", - }, - stack: &apiv1.Stack{ - Name: "dev", - }, + project: "default", + stack: "dev", appName: "foo", service: &workload.Service{ Base: workload.Base{ @@ -206,13 +201,12 @@ status: {} }, }, }, - Replicas: 2, + Replicas: r2, }, Ports: []network.Port{ { Port: 80, Protocol: "TCP", - Public: true, }, }, }, @@ -225,15 +219,6 @@ status: {} "service-workload-type": "CollaSet", }, }, - portConfig: apiv1.GenericConfig{ - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, }, args: args{ spec: &apiv1.Intent{}, @@ -244,13 +229,8 @@ status: {} { name: "Deployment", fields: fields{ - project: &apiv1.Project{ - Name: "default", - Path: "/test", - }, - stack: &apiv1.Stack{ - Name: "dev", - }, + project: "default", + stack: "dev", appName: "foo", service: &workload.Service{ Base: workload.Base{ @@ -270,7 +250,6 @@ status: {} { Port: 80, Protocol: "TCP", - Public: true, }, }, }, @@ -280,15 +259,6 @@ status: {} "service-workload-type": "Deployment", }, }, - portConfig: apiv1.GenericConfig{ - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, }, args: args{ spec: &apiv1.Intent{}, @@ -299,29 +269,13 @@ status: {} } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := modules.GeneratorContext{ - Project: tt.fields.project, - Stack: tt.fields.stack, - Application: &inputs.AppConfiguration{ - Name: tt.fields.appName, - Workload: &workload.Workload{ - Service: tt.fields.service, - }, - }, - Namespace: tt.fields.project.Name, - ModuleInputs: map[string]apiv1.GenericConfig{ - "service": tt.fields.serviceConfig, - "port": tt.fields.portConfig, - }, - } - g := &workloadServiceGenerator{ - project: tt.fields.project, - stack: tt.fields.stack, - appName: tt.fields.appName, - service: tt.fields.service, - serviceConfig: tt.fields.serviceConfig, - namespace: tt.fields.project.Name, - context: ctx, + g := &ServiceGenerator{ + Project: tt.fields.project, + Stack: tt.fields.stack, + App: tt.fields.appName, + Service: tt.fields.service, + Config: tt.fields.serviceConfig, + Namespace: tt.fields.project, } if err := g.Generate(tt.args.spec); (err != nil) != tt.wantErr { t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) @@ -336,6 +290,8 @@ status: {} } func TestCompleteServiceInput(t *testing.T) { + r2 := int32(2) + testcases := []struct { name string service *workload.Service @@ -352,7 +308,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, @@ -372,7 +328,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, @@ -392,7 +348,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, @@ -410,7 +366,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, @@ -430,7 +386,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, @@ -454,7 +410,7 @@ func TestCompleteServiceInput(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r2, Labels: map[string]string{ "k1": "v1", }, diff --git a/pkg/modules/generators/workload/workload_generator.go b/pkg/modules/generators/workload/workload_generator.go index e8c181113..412ae3f9c 100644 --- a/pkg/modules/generators/workload/workload_generator.go +++ b/pkg/modules/generators/workload/workload_generator.go @@ -16,65 +16,71 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/container" "kusionstack.io/kusion/pkg/modules" "kusionstack.io/kusion/pkg/modules/generators/workload/secret" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" "kusionstack.io/kusion/pkg/util/net" "kusionstack.io/kusion/pkg/workspace" ) -type workloadGenerator struct { - project *apiv1.Project - stack *apiv1.Stack - appName string - workload *workload.Workload - moduleConfigs map[string]apiv1.GenericConfig - namespace string - - // for internal generator - context modules.GeneratorContext +type Generator struct { + // Project represents the Project name + Project string + // Stack represents the Stack name + Stack string + // App represents the application name + App string + // Namespace represents the K8s Namespace + Namespace string + // Workload represents the Workload configuration + Workload *workload.Workload + // PlatformConfigs represents the module platform configurations + PlatformConfigs map[string]apiv1.GenericConfig + // SecretStoreSpec contains configuration to describe target secret store. + SecretStoreSpec *apiv1.SecretStoreSpec } -func NewWorkloadGenerator(ctx modules.GeneratorContext) (modules.Generator, error) { - if len(ctx.Project.Name) == 0 { - return nil, fmt.Errorf("project name must not be empty") - } +func NewWorkloadGeneratorFunc(g *Generator) modules.NewGeneratorFunc { + return func() (modules.Generator, error) { + if len(g.Project) == 0 { + return nil, fmt.Errorf("project name must not be empty") + } + if len(g.Stack) == 0 { + return nil, fmt.Errorf("stack name must not be empty") + } - return &workloadGenerator{ - project: ctx.Project, - stack: ctx.Stack, - appName: ctx.Application.Name, - workload: ctx.Application.Workload, - moduleConfigs: ctx.ModuleInputs, - namespace: ctx.Namespace, - context: ctx, - }, nil -} + if len(g.App) == 0 { + return nil, fmt.Errorf("app name must not be empty") + } -func NewWorkloadGeneratorFunc(ctx modules.GeneratorContext) modules.NewGeneratorFunc { - return func() (modules.Generator, error) { - return NewWorkloadGenerator(ctx) + return g, nil } } -func (g *workloadGenerator) Generate(spec *apiv1.Intent) error { +func (g *Generator) Generate(spec *apiv1.Intent) error { if spec.Resources == nil { spec.Resources = make(apiv1.Resources, 0) } - if g.workload != nil { + if g.Workload != nil { var gfs []modules.NewGeneratorFunc - switch g.workload.Header.Type { + switch g.Workload.Header.Type { case workload.TypeService: - gfs = append(gfs, - NewWorkloadServiceGeneratorFunc(g.context), - secret.NewSecretGeneratorFunc(g.context)) + gfs = append(gfs, NewWorkloadServiceGeneratorFunc(g), secret.NewSecretGeneratorFunc(&secret.GeneratorRequest{ + Project: g.Project, + Namespace: g.Namespace, + Workload: g.Workload, + SecretStoreSpec: g.SecretStoreSpec, + })) case workload.TypeJob: - gfs = append(gfs, - NewJobGeneratorFunc(g.context), - secret.NewSecretGeneratorFunc(g.context)) + gfs = append(gfs, NewJobGeneratorFunc(g), secret.NewSecretGeneratorFunc(&secret.GeneratorRequest{ + Project: g.Project, + Namespace: g.Namespace, + Workload: g.Workload, + SecretStoreSpec: g.SecretStoreSpec, + })) } if err := modules.CallGenerators(spec, gfs...); err != nil { @@ -89,8 +95,7 @@ func toOrderedContainers( appContainers map[string]container.Container, uniqueAppName string, ) ([]corev1.Container, []corev1.Volume, []corev1.ConfigMap, error) { - // Create a slice of containers based on the app's - // containers. + // Create a slice of containers based on the App's containers. var containers []corev1.Container // Create a slice of volumes and configMaps based on the containers' files to be created. @@ -404,16 +409,15 @@ func handleFileCreation(c container.Container, uniqueAppName, containerName stri return } -// completeBaseWorkload uses config from workspace to complete the workload base config. +// completeBaseWorkload uses config from workspace to complete the Workload base config. func completeBaseWorkload(base *workload.Base, config apiv1.GenericConfig) error { - replicas, err := workspace.GetIntFromGenericConfig(config, workload.FieldReplicas) + replicas, err := workspace.GetInt32PointerFromGenericConfig(config, workload.FieldReplicas) if err != nil { return err } - if replicas == 0 { - replicas = workload.DefaultReplicas - } - if base.Replicas == 0 { + + // override the base replicas with the value from workspace if it is null + if base.Replicas == nil { base.Replicas = replicas } labels, err := workspace.GetStringMapFromGenericConfig(config, workload.FieldLabels) diff --git a/pkg/modules/generators/workload/workload_generator_test.go b/pkg/modules/generators/workload/workload_generator_test.go index 581a28f5e..1f873403e 100644 --- a/pkg/modules/generators/workload/workload_generator_test.go +++ b/pkg/modules/generators/workload/workload_generator_test.go @@ -8,71 +8,18 @@ import ( "gopkg.in/yaml.v2" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/container" + "kusionstack.io/kusion/pkg/apis/core/v1/appconfiguration/workload/network" "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" ) -func newGeneratorContext( - project *apiv1.Project, - stack *apiv1.Stack, - appName string, - workload *workload.Workload, - moduleInputs map[string]apiv1.GenericConfig, -) modules.GeneratorContext { - application := &inputs.AppConfiguration{ - Name: appName, - Workload: workload, - } - return modules.GeneratorContext{ - Project: project, - Stack: stack, - Application: application, - Namespace: project.Name, - ModuleInputs: moduleInputs, - } -} - -func TestNewWorkloadGenerator(t *testing.T) { - t.Run("NewWorkloadGenerator should return a valid generator", func(t *testing.T) { - expectedProject := &apiv1.Project{ - Name: "test", - } - expectedStack := &apiv1.Stack{} - expectedWorkload := &workload.Workload{} - expectedAppName := "test" - expectedModuleConfigs := map[string]apiv1.GenericConfig{ - "service": { - "type": "Deployment", - }, - "job": { - "replicas": 2, - }, - } - - ctx := newGeneratorContext(expectedProject, expectedStack, expectedAppName, expectedWorkload, expectedModuleConfigs) - actualGenerator, err := NewWorkloadGenerator(ctx) - - assert.NoError(t, err, "Error should be nil") - assert.NotNil(t, actualGenerator, "Generator should not be nil") - assert.Equal(t, expectedProject, actualGenerator.(*workloadGenerator).project, "Project mismatch") - assert.Equal(t, expectedStack, actualGenerator.(*workloadGenerator).stack, "Stack mismatch") - assert.Equal(t, expectedAppName, actualGenerator.(*workloadGenerator).appName, "AppName mismatch") - assert.Equal(t, expectedWorkload, actualGenerator.(*workloadGenerator).workload, "Workload mismatch") - assert.Equal(t, expectedModuleConfigs, actualGenerator.(*workloadGenerator).moduleConfigs, "ModuleConfigs mismatch") - }) -} - func TestNewWorkloadGeneratorFunc(t *testing.T) { t.Run("NewWorkloadGeneratorFunc should return a valid generator function", func(t *testing.T) { - expectedProject := &apiv1.Project{ - Name: "test", - } - expectedStack := &apiv1.Stack{} expectedWorkload := &workload.Workload{} expectedAppName := "test" + expectedProject := "test" + expectedStack := "test" expectedModuleConfigs := map[string]apiv1.GenericConfig{ "service": { "type": "Deployment", @@ -82,17 +29,23 @@ func TestNewWorkloadGeneratorFunc(t *testing.T) { }, } - ctx := newGeneratorContext(expectedProject, expectedStack, expectedAppName, expectedWorkload, expectedModuleConfigs) - generatorFunc := NewWorkloadGeneratorFunc(ctx) + generatorFunc := NewWorkloadGeneratorFunc(&Generator{ + Project: expectedProject, + Stack: expectedStack, + App: expectedAppName, + Namespace: expectedAppName, + Workload: expectedWorkload, + PlatformConfigs: expectedModuleConfigs, + }) actualGenerator, err := generatorFunc() assert.NoError(t, err, "Error should be nil") assert.NotNil(t, actualGenerator, "Generator should not be nil") - assert.Equal(t, expectedProject, actualGenerator.(*workloadGenerator).project, "Project mismatch") - assert.Equal(t, expectedStack, actualGenerator.(*workloadGenerator).stack, "Stack mismatch") - assert.Equal(t, expectedAppName, actualGenerator.(*workloadGenerator).appName, "AppName mismatch") - assert.Equal(t, expectedWorkload, actualGenerator.(*workloadGenerator).workload, "Workload mismatch") - assert.Equal(t, expectedModuleConfigs, actualGenerator.(*workloadGenerator).moduleConfigs, "ModuleConfigs mismatch") + assert.Equal(t, expectedProject, actualGenerator.(*Generator).Project, "Project mismatch") + assert.Equal(t, expectedStack, actualGenerator.(*Generator).Stack, "Stack mismatch") + assert.Equal(t, expectedAppName, actualGenerator.(*Generator).App, "AppName mismatch") + assert.Equal(t, expectedWorkload, actualGenerator.(*Generator).Workload, "Workload mismatch") + assert.Equal(t, expectedModuleConfigs, actualGenerator.(*Generator).PlatformConfigs, "ModuleConfigs mismatch") }) } @@ -113,7 +66,6 @@ func TestWorkloadGenerator_Generate(t *testing.T) { { Port: 80, Protocol: "TCP", - Public: true, }, }, }, @@ -135,12 +87,8 @@ func TestWorkloadGenerator_Generate(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - expectedProject := &apiv1.Project{ - Name: "test", - } - expectedStack := &apiv1.Stack{ - Name: "teststack", - } + expectedProject := "test" + expectedStack := "test" expectedAppName := "test" expectedModuleConfigs := map[string]apiv1.GenericConfig{ "service": { @@ -149,21 +97,19 @@ func TestWorkloadGenerator_Generate(t *testing.T) { "job": { "replicas": 2, }, - "port": { - "type": "alicloud", - "labels": apiv1.GenericConfig{ - "kusionstack.io/control": "true", - }, - "annotations": apiv1.GenericConfig{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec": "slb.s1.small", - }, - }, } - ctx := newGeneratorContext(expectedProject, expectedStack, expectedAppName, tc.expectedWorkload, expectedModuleConfigs) - actualGenerator, _ := NewWorkloadGenerator(ctx) + generatorFunc := NewWorkloadGeneratorFunc(&Generator{ + Project: expectedProject, + Stack: expectedStack, + App: expectedAppName, + Namespace: expectedAppName, + PlatformConfigs: expectedModuleConfigs, + Workload: tc.expectedWorkload, + }) + actualGenerator, err := generatorFunc() spec := &apiv1.Intent{} - err := actualGenerator.Generate(spec) + err = actualGenerator.Generate(spec) assert.NoError(t, err, "Error should be nil") assert.NotNil(t, spec.Resources, "Resources should not be nil") @@ -171,15 +117,15 @@ func TestWorkloadGenerator_Generate(t *testing.T) { resource := spec.Resources[0] actual := mapToUnstructured(resource.Attributes) - assert.Equal(t, expectedProject.Name, actual.GetNamespace(), "Namespace mismatch") - assert.Equal(t, modules.UniqueAppName(expectedProject.Name, expectedStack.Name, expectedAppName), actual.GetName(), "Name mismatch") + assert.Equal(t, expectedProject, actual.GetNamespace(), "Namespace mismatch") + assert.Equal(t, modules.UniqueAppName(expectedProject, expectedStack, expectedAppName), actual.GetName(), "Name mismatch") if tc.expectedWorkload.Header.Type == "Service" { assert.Equal(t, "Deployment", actual.GetKind(), "Resource kind mismatch") - assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(expectedProject.Name, expectedAppName), tc.expectedWorkload.Service.Labels), actual.GetLabels(), "Labels mismatch") + assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(expectedProject, expectedAppName), tc.expectedWorkload.Service.Labels), actual.GetLabels(), "Labels mismatch") } else if tc.expectedWorkload.Header.Type == "Job" { assert.Equal(t, "CronJob", actual.GetKind(), "Resource kind mismatch") - assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(expectedProject.Name, expectedAppName), tc.expectedWorkload.Job.Labels), actual.GetLabels(), "Labels mismatch") + assert.Equal(t, modules.MergeMaps(modules.UniqueAppLabels(expectedProject, expectedAppName), tc.expectedWorkload.Job.Labels), actual.GetLabels(), "Labels mismatch") assert.Equal(t, modules.MergeMaps(tc.expectedWorkload.Job.Annotations), actual.GetAnnotations(), "Annotations mismatch") } }) @@ -371,6 +317,9 @@ func TestToOrderedContainers(t *testing.T) { } func TestCompleteBaseWorkload(t *testing.T) { + r4 := int32(4) + r3 := int32(3) + testcases := []struct { name string base *workload.Base @@ -408,7 +357,7 @@ func TestCompleteBaseWorkload(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 4, + Replicas: &r4, Labels: map[string]string{ "k1": "v1", "k2": "v2", @@ -427,7 +376,7 @@ func TestCompleteBaseWorkload(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 3, + Replicas: &r3, Labels: map[string]string{ "k1": "v1", }, @@ -445,7 +394,7 @@ func TestCompleteBaseWorkload(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 3, + Replicas: &r3, Labels: map[string]string{ "k1": "v1", }, @@ -455,7 +404,7 @@ func TestCompleteBaseWorkload(t *testing.T) { }, }, { - name: "use default replicas", + name: "use platform replicas", base: &workload.Base{ Containers: map[string]container.Container{ "nginx": { @@ -469,7 +418,9 @@ func TestCompleteBaseWorkload(t *testing.T) { "k1": "v1", }, }, - config: nil, + config: apiv1.GenericConfig{ + "replicas": 4, + }, success: true, completedBase: &workload.Base{ Containers: map[string]container.Container{ @@ -477,7 +428,7 @@ func TestCompleteBaseWorkload(t *testing.T) { Image: "nginx:v1", }, }, - Replicas: 2, + Replicas: &r4, Labels: map[string]string{ "k1": "v1", }, diff --git a/pkg/modules/inputs/accessories/database.go b/pkg/modules/inputs/accessories/database.go deleted file mode 100644 index 301230c99..000000000 --- a/pkg/modules/inputs/accessories/database.go +++ /dev/null @@ -1,120 +0,0 @@ -package database - -import ( - "encoding/json" - "errors" - - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" -) - -type Type string - -const ( - TypeMySQL = "MySQL" - TypePostgreSQL = "PostgreSQL" -) - -type Header struct { - Type Type `yaml:"_type" json:"_type"` -} - -type Database struct { - Header `yaml:",inline" json:",inline"` - *mysql.MySQL `yaml:",inline" json:",inline"` - *postgres.PostgreSQL `yaml:",inline" json:",inline"` -} - -func (db Database) MarshalJSON() ([]byte, error) { - switch db.Header.Type { - case TypeMySQL: - return json.Marshal(struct { - Header `yaml:",inline" json:",inline"` - *mysql.MySQL `yaml:",inline" json:",inline"` - }{ - Header: Header{db.Header.Type}, - MySQL: db.MySQL, - }) - case TypePostgreSQL: - return json.Marshal(struct { - Header `yaml:",inline" json:",inline"` - *postgres.PostgreSQL `yaml:",inline" json:",inline"` - }{ - Header: Header{db.Header.Type}, - PostgreSQL: db.PostgreSQL, - }) - default: - return nil, errors.New("unknown database type") - } -} - -func (db *Database) UnmarshalJSON(data []byte) error { - var databaseData Header - err := json.Unmarshal(data, &databaseData) - if err != nil { - return err - } - - db.Header.Type = databaseData.Type - switch db.Header.Type { - case TypeMySQL: - var v mysql.MySQL - err = json.Unmarshal(data, &v) - db.MySQL = &v - case TypePostgreSQL: - var v postgres.PostgreSQL - err = json.Unmarshal(data, &v) - db.PostgreSQL = &v - default: - err = errors.New("unknown database type") - } - - return err -} - -func (db Database) MarshalYAML() (interface{}, error) { - switch db.Header.Type { - case TypeMySQL: - return struct { - Header `yaml:",inline" json:",inline"` - *mysql.MySQL `yaml:",inline" json:",inline"` - }{ - Header: Header{db.Header.Type}, - MySQL: db.MySQL, - }, nil - case TypePostgreSQL: - return struct { - Header `yaml:",inline" json:",inline"` - *postgres.PostgreSQL `yaml:",inline" json:",inline"` - }{ - Header: Header{db.Header.Type}, - PostgreSQL: db.PostgreSQL, - }, nil - default: - return nil, errors.New("unknown database type") - } -} - -func (db *Database) UnmarshalYAML(unmarshal func(interface{}) error) error { - var databaseData Header - err := unmarshal(&databaseData) - if err != nil { - return err - } - - db.Header.Type = databaseData.Type - switch db.Header.Type { - case TypeMySQL: - var v mysql.MySQL - err = unmarshal(&v) - db.MySQL = &v - case TypePostgreSQL: - var v postgres.PostgreSQL - err = unmarshal(&v) - db.PostgreSQL = &v - default: - err = errors.New("unknown database type") - } - - return err -} diff --git a/pkg/modules/inputs/accessories/database_test.go b/pkg/modules/inputs/accessories/database_test.go deleted file mode 100644 index 0c90d7b5b..000000000 --- a/pkg/modules/inputs/accessories/database_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package database - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/mysql" - "kusionstack.io/kusion/pkg/modules/inputs/accessories/postgres" -) - -func TestDatabase_MarshalJSON(t *testing.T) { - tests := []struct { - name string - data *Database - expected string - expectedError error - }{ - { - name: "Valid MarshalJSON for MySQL", - data: &Database{ - Header: Header{ - Type: TypeMySQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: `{"_type": "MySQL", "type": "local", "version": "8.0"}`, - expectedError: nil, - }, - { - name: "Valid MarshalJSON for PostgreSQL", - data: &Database{ - Header: Header{ - Type: TypePostgreSQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: `{"_type": "PostgreSQL", "type": "cloud", "version": "15.5"}`, - expectedError: nil, - }, - { - name: "Unknown Type", - data: &Database{ - Header: Header{ - Type: Type("Unknown"), - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: "", - expectedError: errors.New("unknown database type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, actualErr := json.Marshal(test.data) - if test.expectedError == nil { - assert.JSONEq(t, test.expected, string(actual)) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestDatabase_UnmarshalJSON(t *testing.T) { - tests := []struct { - name string - data string - expected Database - expectedError error - }{ - { - name: "Valid UnmarshalJSON for MySQL", - data: `{"_type": "MySQL", "type": "local", "version": "8.0"}`, - expected: Database{ - Header: Header{ - Type: TypeMySQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - expectedError: nil, - }, - { - name: "Valid UnmarshalJSON for PostgreSQL", - data: `{"_type": "PostgreSQL", "type": "cloud", "version": "15.5"}`, - expected: Database{ - Header: Header{ - Type: TypePostgreSQL, - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expectedError: nil, - }, - { - name: "Unknown Type", - data: `{"_type": "Unknown", "type": "local", "version": "15.5"}`, - expected: Database{ - Header: Header{ - Type: "Unknown", - }, - }, - expectedError: errors.New("unknown database type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var actual Database - actualErr := json.Unmarshal([]byte(test.data), &actual) - if test.expectedError == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestDatabase_MarshalYAML(t *testing.T) { - tests := []struct { - name string - data Database - expected string - expectedError error - }{ - { - name: "Valid MarshalYAML for MySQL", - data: Database{ - Header: Header{ - Type: TypeMySQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: `_type: MySQL -type: local -version: "8.0"`, - expectedError: nil, - }, - { - name: "Valid MarshalYAML for PostgreSQL", - data: Database{ - Header: Header{ - Type: TypePostgreSQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: `_type: PostgreSQL -type: cloud -version: "15.5"`, - expectedError: nil, - }, - { - name: "Unknown Type", - data: Database{ - Header: Header{ - Type: Type("Unknown"), - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expected: "", - expectedError: errors.New("unknown database type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, actualErr := yaml.Marshal(test.data) - if test.expectedError == nil { - assert.YAMLEq(t, test.expected, string(actual)) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestDatabase_UnmarshalYAML(t *testing.T) { - tests := []struct { - name string - data string - expected Database - expectedError error - }{ - { - name: "Valid UnmarshalYAML for MySQL", - data: `_type: MySQL -type: "local" -version: "8.0"`, - expected: Database{ - Header: Header{ - Type: TypeMySQL, - }, - MySQL: &mysql.MySQL{ - Type: "local", - Version: "8.0", - }, - }, - expectedError: nil, - }, - { - name: "Valid UnmarshalYAML for PostgreSQL", - data: `_type: PostgreSQL -type: "cloud" -version: "15.5"`, - expected: Database{ - Header: Header{ - Type: TypePostgreSQL, - }, - PostgreSQL: &postgres.PostgreSQL{ - Type: "cloud", - Version: "15.5", - }, - }, - expectedError: nil, - }, - { - name: "Unknown Type", - data: `_type: Unknown -type: "local" -version: "15.5"`, - expected: Database{}, - expectedError: errors.New("unknown database type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var actual Database - actualErr := yaml.Unmarshal([]byte(test.data), &actual) - if test.expectedError == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} diff --git a/pkg/modules/inputs/accessories/mysql/mysql.go b/pkg/modules/inputs/accessories/mysql/mysql.go deleted file mode 100644 index d30d53e5b..000000000 --- a/pkg/modules/inputs/accessories/mysql/mysql.go +++ /dev/null @@ -1,47 +0,0 @@ -package mysql - -import "fmt" - -const ( - CloudDBType = "cloud" - LocalDBType = "local" -) - -const ( - ErrEmptyInstanceTypeForCloudDB = "empty instance type for cloud managed mysql instance" -) - -// MySQL describes the attributes to locally deploy or create a cloud provider -// managed mysql database instance for the workload. -type MySQL struct { - // The deployment mode of the mysql database. - Type string `json:"type,omitempty" yaml:"type,omitempty"` - // The mysql database version to use. - Version string `json:"version,omitempty" yaml:"version,omitempty"` - // The type of the mysql instance. - InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"` - // The allocated storage size of the mysql instance. - Size int `json:"size,omitempty" yaml:"size,omitempty"` - // The edition of the mysql instance provided by the cloud vendor. - Category string `json:"category,omitempty" yaml:"category,omitempty"` - // The operation account for the mysql database. - Username string `json:"username,omitempty" yaml:"username,omitempty"` - // The list of IP addresses allowed to access the mysql instance provided by the cloud vendor. - SecurityIPs []string `json:"securityIPs,omitempty" yaml:"securityIPs,omitempty"` - // The virtual subnet ID associated with the VPC that the cloud mysql instance will be created in. - SubnetID string `json:"subnetID,omitempty" yaml:"subnetID,omitempty"` - // Whether the host address of the cloud mysql instance for the workload to connect with is via - // public network or private network of the cloud vendor. - PrivateRouting bool `json:"privateRouting,omitempty" yaml:"privateRouting,omitempty"` - // The specified name of the mysql database instance, composed with `dbKey` and `suffix`. - DatabaseName string `json:"databaseName,omitempty" yaml:"databaseName,omitempty"` -} - -// Validate validates whether the input of a mysql database instance is valid. -func (db *MySQL) Validate() error { - if db.Type == CloudDBType && db.InstanceType == "" { - return fmt.Errorf(ErrEmptyInstanceTypeForCloudDB) - } - - return nil -} diff --git a/pkg/modules/inputs/accessories/postgres/postgres.go b/pkg/modules/inputs/accessories/postgres/postgres.go deleted file mode 100644 index cb0e1e0d6..000000000 --- a/pkg/modules/inputs/accessories/postgres/postgres.go +++ /dev/null @@ -1,47 +0,0 @@ -package postgres - -import "fmt" - -const ( - CloudDBType = "cloud" - LocalDBType = "local" -) - -const ( - ErrEmptyInstanceTypeForCloudDB = "empty instance type for cloud managed postgresql instance" -) - -// PostgreSQL describes the attributes to locally deploy or create a cloud provider -// managed postgresql database instance for the workload. -type PostgreSQL struct { - // The deployment mode of the postgresql database. - Type string `json:"type,omitempty" yaml:"type,omitempty"` - // The postgresql database version to use. - Version string `json:"version,omitempty" yaml:"version,omitempty"` - // The type of the postgresql instance. - InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"` - // The allocated storage size of the postgresql instance. - Size int `json:"size,omitempty" yaml:"size,omitempty"` - // The edition of the postgresql instance provided by the cloud vendor. - Category string `json:"category,omitempty" yaml:"category,omitempty"` - // The operation account for the postgresql database. - Username string `json:"username,omitempty" yaml:"username,omitempty"` - // The list of IP addresses allowed to access the postgresql instance provided by the cloud vendor. - SecurityIPs []string `json:"securityIPs,omitempty" yaml:"securityIPs,omitempty"` - // The virtual subnet ID associated with the VPC that the cloud postgresql instance will be created in. - SubnetID string `json:"subnetID,omitempty" yaml:"subnetID,omitempty"` - // Whether the host address of the cloud postgresql instance for the workload to connect with is via - // public network or private network of the cloud vendor. - PrivateRouting bool `json:"privateRouting,omitempty" yaml:"privateRouting,omitempty"` - // The specified name of the postgresql database instance, composed with `dbKey` and `suffix`. - DatabaseName string `json:"databaseName,omitempty" yaml:"databaseName,omitempty"` -} - -// Validate validates whether the input of a postgresql database instance is valid. -func (db *PostgreSQL) Validate() error { - if db.Type == CloudDBType && db.InstanceType == "" { - return fmt.Errorf(ErrEmptyInstanceTypeForCloudDB) - } - - return nil -} diff --git a/pkg/modules/inputs/appconfiguration.go b/pkg/modules/inputs/appconfiguration.go deleted file mode 100644 index c4dfd8e52..000000000 --- a/pkg/modules/inputs/appconfiguration.go +++ /dev/null @@ -1,48 +0,0 @@ -package inputs - -import ( - database "kusionstack.io/kusion/pkg/modules/inputs/accessories" - "kusionstack.io/kusion/pkg/modules/inputs/monitoring" - "kusionstack.io/kusion/pkg/modules/inputs/trait" - "kusionstack.io/kusion/pkg/modules/inputs/workload" -) - -// AppConfiguration is a developer-centric definition that describes -// how to run an Application. -// This application model builds upon a decade of experience at -// AntGroup running super large scale internal developer platform, -// combined with best-of-breed ideas and practices from the community. -// -// Example: -// -// import models.schema.v1 as ac -// import models.schema.v1.workload as wl -// import models.schema.v1.workload.container as c -// -// appConfiguration = ac.AppConfiguration { -// workload: wl.Service { -// containers: { -// "nginx": c.Container { -// image: "nginx:v1" -// } -// } -// } -// } -type AppConfiguration struct { - // Name of the target Application. - Name string `json:"name,omitempty" yaml:"name,omitempty"` - // Workload defines how to run your application code. - Workload *workload.Workload `json:"workload" yaml:"workload"` - // OpsRule specifies collection of rules that will be checked for Day-2 operation. - OpsRule *trait.OpsRule `json:"opsRule,omitempty" yaml:"opsRule,omitempty"` - Monitoring *monitoring.Monitor `json:"monitoring,omitempty" yaml:"monitoring,omitempty"` - - // Database defines a locally deployed or a cloud provider managed - // database instance for the workload. - Database map[string]*database.Database `json:"database,omitempty" yaml:"database,omitempty"` - - // Labels and annotations can be used to attach arbitrary metadata - // as key-value pairs to resources. - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` -} diff --git a/pkg/modules/inputs/appconfiguration_test.go b/pkg/modules/inputs/appconfiguration_test.go deleted file mode 100644 index 06631d6bd..000000000 --- a/pkg/modules/inputs/appconfiguration_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package inputs - -import ( - "testing" - - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "kusionstack.io/kusion/pkg/modules/inputs/trait" - "kusionstack.io/kusion/pkg/modules/inputs/workload" - "kusionstack.io/kusion/pkg/modules/inputs/workload/container" -) - -var ( - appString = `workload: - _type: Job - containers: - busybox: - image: busybox:1.28 - command: - - /bin/sh - - -c - - echo hello - replicas: 2 - schedule: 0 * * * * -opsRule: - maxUnavailable: 30% -` - appStruct = AppConfiguration{ - Workload: &workload.Workload{ - Header: workload.Header{ - Type: workload.TypeJob, - }, - Job: &workload.Job{ - Base: workload.Base{ - Containers: map[string]container.Container{ - "busybox": { - Image: "busybox:1.28", - Command: []string{"/bin/sh", "-c", "echo hello"}, - }, - }, - Replicas: 2, - }, - Schedule: "0 * * * *", - }, - }, - OpsRule: &trait.OpsRule{ - MaxUnavailable: "30%", - }, - } -) - -func TestAppConfigurationMarshal(t *testing.T) { - in := appStruct - exp := appString - out, err := yaml.Marshal(in) - require.NoError(t, err) - require.Equal(t, exp, string(out)) -} - -func TestAppConfigurationUnmarshal(t *testing.T) { - in := appString - exp := appStruct - out := AppConfiguration{} - err := yaml.Unmarshal([]byte(in), &out) - require.NoError(t, err) - require.Equal(t, exp, out) -} diff --git a/pkg/modules/inputs/monitoring/monitoring.go b/pkg/modules/inputs/monitoring/monitoring.go deleted file mode 100644 index 8891a60ac..000000000 --- a/pkg/modules/inputs/monitoring/monitoring.go +++ /dev/null @@ -1,44 +0,0 @@ -package monitoring - -import ( - "errors" - - prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" -) - -const ( - ModuleName = "monitoring" - OperatorModeKey = "operatorMode" - MonitorTypeKey = "monitorType" - IntervalKey = "interval" - TimeoutKey = "timeout" - SchemeKey = "scheme" - DefaultMonitorType = "Service" - DefaultInterval = "30s" - DefaultTimeout = "15s" - DefaultScheme = "http" - PodMonitorType MonitorType = "Pod" - ServiceMonitorType MonitorType = "Service" -) - -var ( - ErrTimeoutGreaterThanInterval = errors.New("timeout cannot be greater than interval") - ErrPathAndPortEmpty = errors.New("path and port must be present in monitoring configuration") -) - -type ( - MonitorType string -) - -type Monitor struct { - OperatorMode bool `yaml:"operatorMode,omitempty" json:"operatorMode,omitempty"` - Interval prometheusv1.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` - Timeout prometheusv1.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` - MonitorType MonitorType `yaml:"monitorType,omitempty" json:"monitorType,omitempty"` - Path string `yaml:"path,omitempty" json:"path,omitempty"` - // Despite what the name suggests, PodMonitor and ServiceMonitor actually - // only accept port names as the input. So in operator mode, this port field - // need to be the user-provided port name. - Port string `yaml:"port,omitempty" json:"port,omitempty"` - Scheme string `yaml:"scheme,omitempty" json:"scheme,omitempty"` -} diff --git a/pkg/modules/inputs/provider.go b/pkg/modules/inputs/provider.go deleted file mode 100644 index f57c7772f..000000000 --- a/pkg/modules/inputs/provider.go +++ /dev/null @@ -1,84 +0,0 @@ -package inputs - -import ( - "fmt" - "path/filepath" - "strings" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" -) - -const ( - errInvalidProviderSource = "invalid provider source: %s" - errEmptyProviderVersion = "empty provider version" -) - -const ( - RandomProvider = "random" - AWSProvider = "aws" - AlicloudProvider = "alicloud" - defaultTFHost = "registry.terraform.io" -) - -// Provider records the information of the Terraform provider -// used to provision cloud resources. -type Provider struct { - // The complete provider address. - URL string - // The host of the provider registry. - Host string - // The namespace of the provider. - Namespace string - // The name of the provider. - Name string - // The version of the provider. - Version string -} - -// SetString sets the attributes into the provider object. -func (provider *Provider) SetString(providerURL string) error { - // An example of the provider URL is shown below - // registry.terraform.io/hashicorp/aws/5.0.1 - attrs := strings.Split(providerURL, "/") - if len(attrs) != 4 { - return fmt.Errorf("wrong provider url format: %s", providerURL) - } - - provider.URL = providerURL - provider.Host = attrs[0] - provider.Namespace = attrs[1] - provider.Name = attrs[2] - provider.Version = attrs[3] - - return nil -} - -// GetProviderURL returns the complete provider address from provider config in workspace. -func GetProviderURL(providerConfig *apiv1.ProviderConfig) (string, error) { - if providerConfig.Version == "" { - return "", fmt.Errorf(errEmptyProviderVersion) - } - - // Conduct whether to use the default terraform provider registry host - // according to the source of the provider config. - // For example, "hashicorp/aws" means using the default tf provider registry, - // while "registry.customized.io/hashicorp/aws" implies to use a customized registry host. - attrs := strings.Split(providerConfig.Source, "/") - if len(attrs) == 3 { - return filepath.Join(providerConfig.Source, providerConfig.Version), nil - } else if len(attrs) == 2 { - return filepath.Join(defaultTFHost, providerConfig.Source, providerConfig.Version), nil - } - - return "", fmt.Errorf(errInvalidProviderSource, providerConfig.Source) -} - -// GetProviderRegion returns the region of the terraform provider. -func GetProviderRegion(providerConfig *apiv1.ProviderConfig) string { - region, ok := providerConfig.GenericConfig["region"] - if !ok { - return "" - } - - return region.(string) -} diff --git a/pkg/modules/inputs/provider_test.go b/pkg/modules/inputs/provider_test.go deleted file mode 100644 index a7066bc72..000000000 --- a/pkg/modules/inputs/provider_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package inputs - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" -) - -func TestProvider_SetString(t *testing.T) { - tests := []struct { - name string - data string - expected *Provider - expectedError error - }{ - { - name: "Valid Provider URL", - data: "registry.terraform.io/hashicorp/aws/5.0.1", - expected: &Provider{ - URL: "registry.terraform.io/hashicorp/aws/5.0.1", - Host: "registry.terraform.io", - Namespace: "hashicorp", - Name: "aws", - Version: "5.0.1", - }, - expectedError: nil, - }, - { - name: "Invalid Provider URL", - data: "registry.terraform.io/hashicorp/aws/invalid-field/5.0.1", - expected: nil, - expectedError: fmt.Errorf("wrong provider url format: %s", - "registry.terraform.io/hashicorp/aws/invalid-field/5.0.1"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - testProvider := &Provider{} - actualErr := testProvider.SetString(test.data) - if test.expectedError == nil { - assert.Equal(t, test.expected, testProvider) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestGetProviderURL(t *testing.T) { - tests := []struct { - name string - data *apiv1.ProviderConfig - expected string - expectedErr error - }{ - { - name: "Default Hashicorp Registry Provider Config", - data: &apiv1.ProviderConfig{ - Source: "hashicorp/aws", - Version: "5.0.1", - }, - expected: "registry.terraform.io/hashicorp/aws/5.0.1", - expectedErr: nil, - }, - { - name: "Customized Registry Provider Config", - data: &apiv1.ProviderConfig{ - Source: "registry.customized.io/hashicorp/aws", - Version: "5.0.1", - }, - expected: "registry.customized.io/hashicorp/aws/5.0.1", - expectedErr: nil, - }, - { - name: "Empty Version Provider Config", - data: &apiv1.ProviderConfig{ - Source: "hashicorp/aws", - }, - expected: "", - expectedErr: fmt.Errorf(errEmptyProviderVersion), - }, - { - name: "Invalid Provider Source", - data: &apiv1.ProviderConfig{ - Source: "aws", - Version: "5.0.1", - }, - expected: "", - expectedErr: fmt.Errorf(errInvalidProviderSource, "aws"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, actualErr := GetProviderURL(test.data) - if test.expectedErr == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedErr.Error()) - } - }) - } -} - -func TestGetProviderRegion(t *testing.T) { - tests := []struct { - name string - data *apiv1.ProviderConfig - expected string - }{ - { - name: "Valid Provider Config", - data: &apiv1.ProviderConfig{ - Source: "hashicorp/aws", - Version: "5.0.1", - GenericConfig: apiv1.GenericConfig{ - "region": "us-east-1", - }, - }, - expected: "us-east-1", - }, - { - name: "Empty Provider Region", - data: &apiv1.ProviderConfig{ - Source: "hashicorp/aws", - Version: "5.0.1", - }, - expected: "", - }, - } - - for _, test := range tests { - actual := GetProviderRegion(test.data) - assert.Equal(t, test.expected, actual) - } -} diff --git a/pkg/modules/inputs/trait/ops_rule.go b/pkg/modules/inputs/trait/ops_rule.go deleted file mode 100644 index 7911cffd3..000000000 --- a/pkg/modules/inputs/trait/ops_rule.go +++ /dev/null @@ -1,45 +0,0 @@ -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 && opsRule.MaxUnavailable != "" { - 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/inputs/workload/common.go b/pkg/modules/inputs/workload/common.go deleted file mode 100644 index 869fcd6ad..000000000 --- a/pkg/modules/inputs/workload/common.go +++ /dev/null @@ -1,35 +0,0 @@ -package workload - -import "kusionstack.io/kusion/pkg/modules/inputs/workload/container" - -const ( - FieldReplicas = "replicas" - FieldLabels = "labels" - FieldAnnotations = "annotations" - - DefaultReplicas = 2 -) - -// Base defines set of attributes shared by different workload -// profile, e.g. Service and Job. You can inherit this Schema to reuse -// these common attributes. -type Base struct { - // The templates of containers to be run. - Containers map[string]container.Container `yaml:"containers,omitempty" json:"containers,omitempty"` - - // The number of containers that should be run. - // Default is 2 to meet high availability requirements. - Replicas int `yaml:"replicas,omitempty" json:"replicas,omitempty"` - - // Labels and annotations can be used to attach arbitrary metadata - // as key-value pairs to resources. - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` - - // Secret - Secrets map[string]Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"` - - // Dirs configures one or more volumes to be mounted to the - // specified folder. - Dirs map[string]string `json:"dirs,omitempty" yaml:"dirs,omitempty"` -} diff --git a/pkg/modules/inputs/workload/container/container.go b/pkg/modules/inputs/workload/container/container.go deleted file mode 100644 index 198a3ef4a..000000000 --- a/pkg/modules/inputs/workload/container/container.go +++ /dev/null @@ -1,354 +0,0 @@ -package container - -import ( - "encoding/json" - "errors" - - "gopkg.in/yaml.v2" -) - -// Container describes how the Application's tasks are expected to be run. -type Container struct { - // Image to run for this container - Image string `yaml:"image" json:"image"` - // Entrypoint array. - // The image's ENTRYPOINT is used if this is not provided. - Command []string `yaml:"command,omitempty" json:"command,omitempty"` - // Arguments to the entrypoint. - // The image's CMD is used if this is not provided. - Args []string `yaml:"args,omitempty" json:"args,omitempty"` - // Collection of environment variables to set in the container. - // The value of environment variable may be static text or a value from a secret. - Env yaml.MapSlice `yaml:"env,omitempty" json:"env,omitempty"` - // The current working directory of the running process defined in entrypoint. - WorkingDir string `yaml:"workingDir,omitempty" json:"workingDir,omitempty"` - // Resource requirements for this container. - Resources map[string]string `yaml:"resources,omitempty" json:"resources,omitempty"` - // Files configures one or more files to be created in the container. - Files map[string]FileSpec `yaml:"files,omitempty" json:"files,omitempty"` - // Dirs configures one or more volumes to be mounted to the specified folder. - Dirs map[string]string `yaml:"dirs,omitempty" json:"dirs,omitempty"` - // Periodic probe of container liveness. - LivenessProbe *Probe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"` - // Periodic probe of container service readiness. - ReadinessProbe *Probe `yaml:"readinessProbe,omitempty" json:"readinessProbe,omitempty"` - // StartupProbe indicates that the Pod has successfully initialized. - StartupProbe *Probe `yaml:"startupProbe,omitempty" json:"startupProbe,omitempty"` - // Actions that the management system should take in response to container lifecycle events. - Lifecycle *Lifecycle `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"` -} - -// FileSpec defines the target file in a Container -type FileSpec struct { - // The content of target file in plain text. - Content string `yaml:"content,omitempty" json:"content,omitempty"` - // Source for the file content, might be a reference to a secret value. - ContentFrom string `yaml:"contentFrom,omitempty" json:"contentFrom,omitempty"` - // Mode bits used to set permissions on this file. - Mode string `yaml:"mode" json:"mode"` -} - -// TypeWrapper is a thin wrapper to make YAML decoder happy. -type TypeWrapper struct { - // Type of action to be taken. - Type string `yaml:"_type" json:"_type"` -} - -// Probe describes a health check to be performed against a container to determine whether it is -// alive or ready to receive traffic. -type Probe struct { - // The action taken to determine the health of a container. - ProbeHandler *ProbeHandler `yaml:"probeHandler" json:"probeHandler"` - // Number of seconds after the container has started before liveness probes are initiated. - InitialDelaySeconds int32 `yaml:"initialDelaySeconds,omitempty" json:"initialDelaySeconds,omitempty"` - // Number of seconds after which the probe times out. - TimeoutSeconds int32 `yaml:"timeoutSeconds,omitempty" json:"timeoutSeconds,omitempty"` - // How often (in seconds) to perform the probe. - PeriodSeconds int32 `yaml:"periodSeconds,omitempty" json:"periodSeconds,omitempty"` - // Minimum consecutive successes for the probe to be considered successful after having failed. - SuccessThreshold int32 `yaml:"successThreshold,omitempty" json:"successThreshold,omitempty"` - // Minimum consecutive failures for the probe to be considered failed after having succeeded. - FailureThreshold int32 `yaml:"failureThreshold,omitempty" json:"failureThreshold,omitempty"` -} - -// ProbeHandler defines a specific action that should be taken in a probe. -// One and only one of the fields must be specified. -type ProbeHandler struct { - // Type of action to be taken. - TypeWrapper `yaml:"_type" json:"_type"` - // Exec specifies the action to take. - // +optional - *ExecAction `yaml:",inline" json:",inline"` - // HTTPGet specifies the http request to perform. - // +optional - *HTTPGetAction `yaml:",inline" json:",inline"` - // TCPSocket specifies an action involving a TCP port. - // +optional - *TCPSocketAction `yaml:",inline" json:",inline"` -} - -// ExecAction describes a "run in container" action. -type ExecAction struct { - // Command is the command line to execute inside the container, the working directory for the - // command is root ('/') in the container's filesystem. - // Exit status of 0 is treated as live/healthy and non-zero is unhealthy. - Command []string `yaml:"command,omitempty" json:"command,omitempty"` -} - -// HTTPGetAction describes an action based on HTTP Get requests. -type HTTPGetAction struct { - // URL is the full qualified url location to send HTTP requests. - URL string `yaml:"url,omitempty" json:"url,omitempty"` - // Custom headers to set in the request. HTTP allows repeated headers. - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` -} - -// TCPSocketAction describes an action based on opening a socket. -type TCPSocketAction struct { - // URL is the full qualified url location to open a socket. - URL string `yaml:"url,omitempty" json:"url,omitempty"` -} - -// Lifecycle describes actions that the management system should take in response -// to container lifecycle events. -type Lifecycle struct { - // PreStop is called immediately before a container is terminated due to an - // API request or management event such as liveness/startup probe failure, - // preemption, resource contention, etc. - PreStop *LifecycleHandler `yaml:"preStop,omitempty" json:"preStop,omitempty"` - // PostStart is called immediately after a container is created. - PostStart *LifecycleHandler `yaml:"postStart,omitempty" json:"postStart,omitempty"` -} - -// LifecycleHandler defines a specific action that should be taken in a lifecycle -// hook. One and only one of the fields, except TCPSocket must be specified. -type LifecycleHandler struct { - // Type of action to be taken. - TypeWrapper `yaml:"_type" json:"_type"` - // Exec specifies the action to take. - // +optional - *ExecAction `yaml:",inline" json:",inline"` - // HTTPGet specifies the http request to perform. - // +optional - *HTTPGetAction `yaml:",inline" json:",inline"` -} - -// MarshalJSON implements the json.Marshaler interface for ProbeHandler. -func (p *ProbeHandler) MarshalJSON() ([]byte, error) { - switch p.Type { - case "Http": - return json.Marshal(struct { - TypeWrapper `json:",inline"` - *HTTPGetAction `json:",inline"` - }{ - TypeWrapper: TypeWrapper{p.Type}, - HTTPGetAction: p.HTTPGetAction, - }) - case "Exec": - return json.Marshal(struct { - TypeWrapper `json:",inline"` - *ExecAction `json:",inline"` - }{ - TypeWrapper: TypeWrapper{p.Type}, - ExecAction: p.ExecAction, - }) - case "Tcp": - return json.Marshal(struct { - TypeWrapper `json:",inline"` - *TCPSocketAction `json:",inline"` - }{ - TypeWrapper: TypeWrapper{p.Type}, - TCPSocketAction: p.TCPSocketAction, - }) - default: - return nil, errors.New("unrecognized probe handler type") - } -} - -// UnmarshalJSON implements the json.Unmarshaller interface for ProbeHandler. -func (p *ProbeHandler) UnmarshalJSON(data []byte) error { - var probeType TypeWrapper - err := json.Unmarshal(data, &probeType) - if err != nil { - return err - } - - p.Type = probeType.Type - switch p.Type { - case "Http": - handler := &HTTPGetAction{} - err = json.Unmarshal(data, handler) - p.HTTPGetAction = handler - case "Exec": - handler := &ExecAction{} - err = json.Unmarshal(data, handler) - p.ExecAction = handler - case "Tcp": - handler := &TCPSocketAction{} - err = json.Unmarshal(data, handler) - p.TCPSocketAction = handler - default: - return errors.New("unrecognized probe handler type") - } - - return err -} - -// MarshalYAML implements the yaml.Marshaler interface for ProbeHandler. -func (p *ProbeHandler) MarshalYAML() (interface{}, error) { - switch p.Type { - case "Http": - return struct { - TypeWrapper `yaml:",inline" json:",inline"` - HTTPGetAction `yaml:",inline" json:",inline"` - }{ - TypeWrapper: TypeWrapper{Type: p.Type}, - HTTPGetAction: *p.HTTPGetAction, - }, nil - case "Exec": - return struct { - TypeWrapper `yaml:",inline" json:",inline"` - ExecAction `yaml:",inline" json:",inline"` - }{ - TypeWrapper: TypeWrapper{Type: p.Type}, - ExecAction: *p.ExecAction, - }, nil - case "Tcp": - return struct { - TypeWrapper `yaml:",inline" json:",inline"` - TCPSocketAction `yaml:",inline" json:",inline"` - }{ - TypeWrapper: TypeWrapper{Type: p.Type}, - TCPSocketAction: *p.TCPSocketAction, - }, nil - } - - return nil, nil -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface for ProbeHandler. -func (p *ProbeHandler) UnmarshalYAML(unmarshal func(interface{}) error) error { - var probeType TypeWrapper - err := unmarshal(&probeType) - if err != nil { - return err - } - - p.Type = probeType.Type - switch p.Type { - case "Http": - handler := &HTTPGetAction{} - err = unmarshal(handler) - p.HTTPGetAction = handler - case "Exec": - handler := &ExecAction{} - err = unmarshal(handler) - p.ExecAction = handler - case "Tcp": - handler := &TCPSocketAction{} - err = unmarshal(handler) - p.TCPSocketAction = handler - default: - return errors.New("unrecognized probe handler type") - } - - return err -} - -// MarshalJSON implements the json.Marshaler interface for LifecycleHandler. -func (l *LifecycleHandler) MarshalJSON() ([]byte, error) { - switch l.Type { - case "Http": - return json.Marshal(struct { - TypeWrapper `json:",inline"` - *HTTPGetAction `json:",inline"` - }{ - TypeWrapper: TypeWrapper{l.Type}, - HTTPGetAction: l.HTTPGetAction, - }) - case "Exec": - return json.Marshal(struct { - TypeWrapper `json:",inline"` - *ExecAction `json:",inline"` - }{ - TypeWrapper: TypeWrapper{l.Type}, - ExecAction: l.ExecAction, - }) - default: - return nil, errors.New("unrecognized lifecycle handler type") - } -} - -// UnmarshalJSON implements the json.Unmarshaller interface for LifecycleHandler. -func (l *LifecycleHandler) UnmarshalJSON(data []byte) error { - var handlerType TypeWrapper - err := json.Unmarshal(data, &handlerType) - if err != nil { - return err - } - - l.Type = handlerType.Type - switch l.Type { - case "Http": - handler := &HTTPGetAction{} - err = json.Unmarshal(data, handler) - l.HTTPGetAction = handler - case "Exec": - handler := &ExecAction{} - err = json.Unmarshal(data, handler) - l.ExecAction = handler - default: - return errors.New("unrecognized lifecycle handler type") - } - - return err -} - -// MarshalYAML implements the yaml.Marshaler interface for LifecycleHandler. -func (l *LifecycleHandler) MarshalYAML() (interface{}, error) { - switch l.Type { - case "Http": - return struct { - TypeWrapper `yaml:",inline" json:",inline"` - HTTPGetAction `yaml:",inline" json:",inline"` - }{ - TypeWrapper: TypeWrapper{Type: l.Type}, - HTTPGetAction: *l.HTTPGetAction, - }, nil - case "Exec": - return struct { - TypeWrapper `yaml:",inline" json:",inline"` - ExecAction `yaml:",inline" json:",inline"` - }{ - TypeWrapper: TypeWrapper{Type: l.Type}, - ExecAction: *l.ExecAction, - }, nil - default: - return nil, errors.New("unrecognized lifecycle handler type") - } -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface for LifecycleHandler. -func (l *LifecycleHandler) UnmarshalYAML(unmarshal func(interface{}) error) error { - var handlerType TypeWrapper - err := unmarshal(&handlerType) - if err != nil { - return err - } - - l.Type = handlerType.Type - switch l.Type { - case "Http": - handler := &HTTPGetAction{} - err = unmarshal(handler) - l.HTTPGetAction = handler - case "Exec": - handler := &ExecAction{} - err = unmarshal(handler) - l.ExecAction = handler - default: - return errors.New("unrecognized lifecycle handler type") - } - - return err -} diff --git a/pkg/modules/inputs/workload/container/container_test.go b/pkg/modules/inputs/workload/container/container_test.go deleted file mode 100644 index 4c51a4216..000000000 --- a/pkg/modules/inputs/workload/container/container_test.go +++ /dev/null @@ -1,791 +0,0 @@ -package container - -import ( - "encoding/json" - "reflect" - "testing" - - "gopkg.in/yaml.v2" -) - -func TestContainerMarshalJSON(t *testing.T) { - cases := []struct { - input Container - result string - }{ - { - input: Container{ - Image: "nginx:v1", - Resources: map[string]string{ - "cpu": "4", - "memory": "8Gi", - }, - Files: map[string]FileSpec{ - "/tmp/test.txt": { - Content: "hello world", - Mode: "0644", - }, - }, - }, - result: `{"image":"nginx:v1","resources":{"cpu":"4","memory":"8Gi"},"files":{"/tmp/test.txt":{"content":"hello world","mode":"0644"}}}`, - }, - { - input: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Http","url":"http://localhost:80"},"initialDelaySeconds":10}}`, - }, - { - input: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"cat", "/tmp/healthy"}, - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Exec","command":["cat","/tmp/healthy"]},"initialDelaySeconds":10}}`, - }, - { - input: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Tcp"}, - TCPSocketAction: &TCPSocketAction{ - URL: "127.0.0.1:8080", - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Tcp","url":"127.0.0.1:8080"},"initialDelaySeconds":10}}`, - }, - { - input: Container{ - Image: "nginx:v1", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"}, - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"}, - }, - }, - }, - }, - result: `{"image":"nginx:v1","lifecycle":{"preStop":{"_type":"Exec","command":["/bin/sh","-c","echo Hello from the postStart handler \u003e /usr/share/message"]},"postStart":{"_type":"Exec","command":["/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done"]}}}`, - }, - { - input: Container{ - Image: "nginx:v1", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - }, - }, - result: `{"image":"nginx:v1","lifecycle":{"preStop":{"_type":"Http","url":"http://localhost:80"},"postStart":{"_type":"Http","url":"http://localhost:80"}}}`, - }, - } - - for _, c := range cases { - result, err := json.Marshal(&c.input) - if err != nil { - t.Errorf("Failed to marshal input: '%v': %v", c.input, err) - } - if string(result) != c.result { - t.Errorf("Failed to marshal input: '%v': expected %+v, got %q", c.input, c.result, string(result)) - } - } -} - -func TestContainerUnmarshalJSON(t *testing.T) { - cases := []struct { - input string - result Container - }{ - { - input: `{"image":"nginx:v1","resources":{"cpu":"4","memory":"8Gi"},"files":{"/tmp/test.txt":{"content":"hello world","mode":"0644"}}}`, - result: Container{ - Image: "nginx:v1", - Resources: map[string]string{ - "cpu": "4", - "memory": "8Gi", - }, - Files: map[string]FileSpec{ - "/tmp/test.txt": { - Content: "hello world", - Mode: "0644", - }, - }, - }, - }, - { - input: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Http","url":"http://localhost:80"},"initialDelaySeconds":10}}`, - result: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Exec","command":["cat","/tmp/healthy"]},"initialDelaySeconds":10}}`, - result: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Exec"}, - ExecAction: &ExecAction{ - Command: []string{"cat", "/tmp/healthy"}, - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `{"image":"nginx:v1","readinessProbe":{"probeHandler":{"_type":"Tcp","url":"127.0.0.1:8080"},"initialDelaySeconds":10}}`, - result: Container{ - Image: "nginx:v1", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Tcp"}, - TCPSocketAction: &TCPSocketAction{ - URL: "127.0.0.1:8080", - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `{"image":"nginx:v1","lifecycle":{"preStop":{"_type":"Exec","command":["/bin/sh","-c","echo Hello from the postStart handler \u003e /usr/share/message"]},"postStart":{"_type":"Exec","command":["/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done"]}}}`, - result: Container{ - Image: "nginx:v1", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"}, - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"}, - }, - }, - }, - }, - }, - { - input: `{"image":"nginx:v1","lifecycle":{"preStop":{"_type":"Http","url":"http://localhost:80"},"postStart":{"_type":"Http","url":"http://localhost:80"}}}`, - result: Container{ - Image: "nginx:v1", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - }, - }, - }, - } - - for _, c := range cases { - var result Container - if err := json.Unmarshal([]byte(c.input), &result); err != nil { - t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) - } - if !reflect.DeepEqual(result, c.result) { - t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result) - } - } -} - -func TestContainerMarshalYAML(t *testing.T) { - cases := []struct { - input Container - result string - }{ - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -`, - }, - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Http - url: http://localhost:80 - initialDelaySeconds: 10 -`, - }, - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Exec"}, - ExecAction: &ExecAction{ - Command: []string{"cat", "/tmp/healthy"}, - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Exec - command: - - cat - - /tmp/healthy - initialDelaySeconds: 10 -`, - }, - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Tcp"}, - TCPSocketAction: &TCPSocketAction{ - URL: "127.0.0.1:8080", - }, - }, - InitialDelaySeconds: 10, - }, - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Tcp - url: 127.0.0.1:8080 - initialDelaySeconds: 10 -`, - }, - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"}, - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"}, - }, - }, - }, - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -lifecycle: - preStop: - _type: Exec - command: - - /bin/sh - - -c - - echo Hello from the postStart handler > /usr/share/message - postStart: - _type: Exec - command: - - /bin/sh - - -c - - nginx -s quit; while killall -0 nginx; do sleep 1; done -`, - }, - { - input: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - }, - }, - result: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -lifecycle: - preStop: - _type: Http - url: http://localhost:80 - postStart: - _type: Http - url: http://localhost:80 -`, - }, - } - - for _, c := range cases { - result, err := yaml.Marshal(&c.input) - if err != nil { - t.Errorf("Failed to marshal input: '%v': %v", c.input, err) - } - if string(result) != c.result { - t.Errorf("Failed to marshal input: '%v': expected %+v, got %q", c.input, c.result, string(result)) - } - } -} - -func TestContainerUnmarshalYAML(t *testing.T) { - cases := []struct { - input string - result Container - }{ - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - }, - }, - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Http - url: http://localhost:80 - initialDelaySeconds: 10 -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Exec - command: - - cat - - /tmp/healthy - initialDelaySeconds: 10 -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Exec"}, - ExecAction: &ExecAction{ - Command: []string{"cat", "/tmp/healthy"}, - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -readinessProbe: - probeHandler: - _type: Tcp - url: 127.0.0.1:8080 - initialDelaySeconds: 10 -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - ReadinessProbe: &Probe{ - ProbeHandler: &ProbeHandler{ - TypeWrapper: TypeWrapper{Type: "Tcp"}, - TCPSocketAction: &TCPSocketAction{ - URL: "127.0.0.1:8080", - }, - }, - InitialDelaySeconds: 10, - }, - }, - }, - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -lifecycle: - preStop: - _type: Exec - command: - - /bin/sh - - -c - - echo Hello from the postStart handler > /usr/share/message - postStart: - _type: Exec - command: - - /bin/sh - - -c - - nginx -s quit; while killall -0 nginx; do sleep 1; done -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"}, - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Exec"}, - ExecAction: &ExecAction{ - Command: []string{"/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"}, - }, - }, - }, - }, - }, - { - input: `image: nginx:v1 -command: -- /bin/sh -- -c -- echo hi -args: -- /bin/sh -- -c -- echo hi -env: - env1: VALUE -workingDir: /tmp -lifecycle: - preStop: - _type: Http - url: http://localhost:80 - postStart: - _type: Http - url: http://localhost:80 -`, - result: Container{ - Image: "nginx:v1", - Command: []string{"/bin/sh", "-c", "echo hi"}, - Args: []string{"/bin/sh", "-c", "echo hi"}, - Env: yaml.MapSlice{ - { - Key: "env1", - Value: "VALUE", - }, - }, - WorkingDir: "/tmp", - Lifecycle: &Lifecycle{ - PostStart: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - PreStop: &LifecycleHandler{ - TypeWrapper: TypeWrapper{"Http"}, - HTTPGetAction: &HTTPGetAction{ - URL: "http://localhost:80", - }, - }, - }, - }, - }, - } - - for _, c := range cases { - var result Container - if err := yaml.Unmarshal([]byte(c.input), &result); err != nil { - t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) - } - if !reflect.DeepEqual(result, c.result) { - t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result) - } - } -} diff --git a/pkg/modules/inputs/workload/job.go b/pkg/modules/inputs/workload/job.go deleted file mode 100644 index b33fc8091..000000000 --- a/pkg/modules/inputs/workload/job.go +++ /dev/null @@ -1,14 +0,0 @@ -package workload - -const ModuleJob = "job" - -// Job is a kind of workload profile that describes how to run your -// application code. This is typically used for tasks that take from a -// few seconds to a few days to complete. -type Job struct { - Base `yaml:",inline" json:",inline"` - - // The scheduling strategy in Cron format. - // More info: https://en.wikipedia.org/wiki/Cron. - Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty"` -} diff --git a/pkg/modules/inputs/workload/network/port.go b/pkg/modules/inputs/workload/network/port.go deleted file mode 100644 index 6acb4b35b..000000000 --- a/pkg/modules/inputs/workload/network/port.go +++ /dev/null @@ -1,40 +0,0 @@ -package network - -const ( - ModulePort = "port" - - FieldType = "type" - FieldLabels = "labels" - FieldAnnotations = "annotations" - - CSPAliCloud = "alicloud" - CSPAWS = "aws" - ProtocolTCP = "TCP" - ProtocolUDP = "UDP" -) - -// Port defines the exposed port of workload.Service, which can be used to describe how -// the workload.Service get accessed. -type Port struct { - // Type is the specific cloud vendor that provides load balancer, works when Public - // is true, supports CSPAliCloud and CSPAWS for now. - Type string `yaml:"type,omitempty" json:"type,omitempty"` - - // Port is the exposed port of the workload.Service. - Port int `yaml:"port,omitempty" json:"port,omitempty"` - - // TargetPort is the backend container.Container port. - TargetPort int `yaml:"targetPort,omitempty" json:"targetPort,omitempty"` - - // Protocol is protocol used to expose the port, support ProtocolTCP and ProtocolUDP. - Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` - - // Public defines whether to expose the port through Internet. - Public bool `yaml:"public,omitempty" json:"public,omitempty"` - - // Labels are the attached labels of the port, works only when the Public is true. - Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` - - // Annotations are the attached annotations of the port, works only when the Public is true. - Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` -} diff --git a/pkg/modules/inputs/workload/secret.go b/pkg/modules/inputs/workload/secret.go deleted file mode 100644 index ab0eb5069..000000000 --- a/pkg/modules/inputs/workload/secret.go +++ /dev/null @@ -1,8 +0,0 @@ -package workload - -type Secret struct { - Type string `yaml:"type" json:"type"` - Params map[string]string `yaml:"params,omitempty" json:"params,omitempty"` - Data map[string]string `yaml:"data,omitempty" json:"data,omitempty"` - Immutable bool `yaml:"immutable,omitempty" json:"immutable,omitempty"` -} diff --git a/pkg/modules/inputs/workload/service.go b/pkg/modules/inputs/workload/service.go deleted file mode 100644 index 197f63ed6..000000000 --- a/pkg/modules/inputs/workload/service.go +++ /dev/null @@ -1,25 +0,0 @@ -package workload - -import ( - "kusionstack.io/kusion/pkg/modules/inputs/workload/network" -) - -const ( - ModuleService = "service" - - FieldType = "type" - TypeDeployment = "Deployment" - TypeCollaset = "CollaSet" -) - -// Service is a kind of workload profile that describes how to run -// your application code. This is typically used for long-running web -// applications that should "never" go down, and handle short-lived -// latency-sensitive web requests, or events. -type Service struct { - Base `yaml:",inline" json:",inline"` - Type string `yaml:"type" json:"type"` - - // Ports describe the list of ports need getting exposed. - Ports []network.Port `yaml:"ports,omitempty" json:"ports,omitempty"` -} diff --git a/pkg/modules/inputs/workload/workload.go b/pkg/modules/inputs/workload/workload.go deleted file mode 100644 index bfefc6f56..000000000 --- a/pkg/modules/inputs/workload/workload.go +++ /dev/null @@ -1,117 +0,0 @@ -package workload - -import ( - "encoding/json" - "errors" -) - -type Type string - -const ( - TypeJob = "Job" - TypeService = "Service" -) - -type Header struct { - Type Type `yaml:"_type" json:"_type"` -} - -type Workload struct { - Header `yaml:",inline" json:",inline"` - *Service `yaml:",inline" json:",inline"` - *Job `yaml:",inline" json:",inline"` -} - -func (w Workload) MarshalJSON() ([]byte, error) { - switch w.Header.Type { - case TypeService: - return json.Marshal(struct { - Header `yaml:",inline" json:",inline"` - *Service `json:",inline"` - }{ - Header: Header{w.Header.Type}, - Service: w.Service, - }) - case TypeJob: - return json.Marshal(struct { - Header `yaml:",inline" json:",inline"` - *Job `json:",inline"` - }{ - Header: Header{w.Header.Type}, - Job: w.Job, - }) - default: - return nil, errors.New("unknown workload type") - } -} - -func (w *Workload) UnmarshalJSON(data []byte) error { - var workloadData Header - err := json.Unmarshal(data, &workloadData) - if err != nil { - return err - } - - w.Header.Type = workloadData.Type - switch w.Header.Type { - case TypeJob: - var v Job - err = json.Unmarshal(data, &v) - w.Job = &v - case TypeService: - var v Service - err = json.Unmarshal(data, &v) - w.Service = &v - default: - err = errors.New("unknown workload type") - } - - return err -} - -func (w Workload) MarshalYAML() (interface{}, error) { - switch w.Header.Type { - case TypeService: - return struct { - Header `yaml:",inline" json:",inline"` - Service `yaml:",inline" json:",inline"` - }{ - Header: Header{w.Header.Type}, - Service: *w.Service, - }, nil - case TypeJob: - return struct { - Header `yaml:",inline" json:",inline"` - *Job `yaml:",inline" json:",inline"` - }{ - Header: Header{w.Header.Type}, - Job: w.Job, - }, nil - default: - return nil, errors.New("unknown workload type") - } -} - -func (w *Workload) UnmarshalYAML(unmarshal func(interface{}) error) error { - var workloadData Header - err := unmarshal(&workloadData) - if err != nil { - return err - } - - w.Header.Type = workloadData.Type - switch w.Header.Type { - case TypeJob: - var v Job - err = unmarshal(&v) - w.Job = &v - case TypeService: - var v Service - err = unmarshal(&v) - w.Service = &v - default: - err = errors.New("unknown workload type") - } - - return err -} diff --git a/pkg/modules/inputs/workload/workload_test.go b/pkg/modules/inputs/workload/workload_test.go deleted file mode 100644 index 9cfe4f283..000000000 --- a/pkg/modules/inputs/workload/workload_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package workload - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -func TestWorkload_MarshalJSON(t *testing.T) { - tests := []struct { - name string - data *Workload - expected string - expectedError error - }{ - { - name: "Valid MarshalJSON for Service", - data: &Workload{ - Header: Header{ - Type: TypeService, - }, - Service: &Service{ - Type: "Deployment", - Base: Base{ - Replicas: 2, - Labels: map[string]string{ - "app": "my-service", - }, - }, - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: `{"_type": "Service", "replicas": 2, "labels": {"app": "my-service"}, "type": "Deployment"}`, - expectedError: nil, - }, - { - name: "Valid MarshalJSON for Job", - data: &Workload{ - Header: Header{ - Type: TypeJob, - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: `{"_type": "Job", "schedule": "* * * * *"}`, - expectedError: nil, - }, - { - name: "Unknown Type", - data: &Workload{ - Header: Header{ - Type: Type("Unknown"), - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: "", - expectedError: errors.New("unknown workload type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, actualErr := json.Marshal(test.data) - if test.expectedError == nil { - assert.JSONEq(t, test.expected, string(actual)) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestWorkload_UnmarshalJSON(t *testing.T) { - tests := []struct { - name string - data string - expected Workload - expectedError error - }{ - { - name: "Valid UnmarshalJSON for Service", - data: `{"_type": "Service", "replicas": 1, "labels": {}, "annotations": {}, "dirs": {}, "schedule": "* * * * *"}`, - expected: Workload{ - Header: Header{ - Type: TypeService, - }, - Service: &Service{ - Base: Base{ - Replicas: 1, - Labels: map[string]string{}, - Annotations: map[string]string{}, - Dirs: map[string]string{}, - }, - }, - }, - expectedError: nil, - }, - { - name: "Valid UnmarshalJSON for Job", - data: `{"_type": "Job", "schedule": "* * * * *"}`, - expected: Workload{ - Header: Header{ - Type: TypeJob, - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expectedError: nil, - }, - { - name: "Unknown Type", - data: `{"_type": "Unknown", "replicas": 1, "labels": {}, "annotations": {}, "dirs": {}, "schedule": "* * * * *"}`, - expected: Workload{ - Header: Header{ - Type: "Unknown", - }, - }, - expectedError: errors.New("unknown workload type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var actual Workload - actualErr := json.Unmarshal([]byte(test.data), &actual) - if test.expectedError == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestWorkload_MarshalYAML(t *testing.T) { - tests := []struct { - name string - workload Workload - expected string - expectedError error - }{ - { - name: "Valid MarshalYAML for Service", - workload: Workload{ - Header: Header{ - Type: TypeService, - }, - Service: &Service{ - Type: "Deployment", - Base: Base{ - Replicas: 2, - Labels: map[string]string{ - "app": "my-service", - }, - }, - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: `_type: Service -replicas: 2 -labels: - app: my-service -type: Deployment`, - expectedError: nil, - }, - { - name: "Valid MarshalYAML for Job", - workload: Workload{ - Header: Header{ - Type: TypeJob, - }, - Service: &Service{ - Type: "Deployment", - Base: Base{ - Replicas: 2, - Labels: map[string]string{ - "app": "my-service", - }, - }, - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: `_type: Job -schedule: '* * * * *'`, - expectedError: nil, - }, - { - name: "Unknown Type", - workload: Workload{ - Header: Header{ - Type: Type("Unknown"), - }, - Job: &Job{ - Schedule: "* * * * *", - }, - }, - expected: "", - expectedError: errors.New("unknown workload type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual, actualErr := yaml.Marshal(test.workload) - if test.expectedError == nil { - assert.YAMLEq(t, test.expected, string(actual)) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} - -func TestWorkload_UnmarshalYAML(t *testing.T) { - tests := []struct { - name string - data string - expected Workload - expectedError error - }{ - { - name: "Valid UnmarshalYAML for Service", - data: `_type: Service -replicas: 1 -labels: {} -annotations: {} -dirs: {} -schedule: '* * * * *'`, - expected: Workload{ - Header: Header{ - Type: TypeService, - }, - Service: &Service{ - Base: Base{ - Replicas: 1, - Labels: map[string]string{}, - Annotations: map[string]string{}, - Dirs: map[string]string{}, - }, - }, - }, - expectedError: nil, - }, - { - name: "Valid UnmarshalYAML for Job", - data: `_type: Job -replicas: 1 -labels: {} -annotations: {} -dirs: {} -schedule: '* * * * *'`, - expected: Workload{ - Header: Header{ - Type: TypeJob, - }, - Job: &Job{ - Base: Base{ - Replicas: 1, - Labels: map[string]string{}, - Annotations: map[string]string{}, - Dirs: map[string]string{}, - }, - Schedule: "* * * * *", - }, - }, - expectedError: nil, - }, - { - name: "Unknown Type", - data: `_type: Unknown -replicas: 1 -labels: {} -annotations: {} -dirs: {} -schedule: '* * * * *'`, - expected: Workload{}, - expectedError: errors.New("unknown workload type"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var actual Workload - actualErr := yaml.Unmarshal([]byte(test.data), &actual) - if test.expectedError == nil { - assert.Equal(t, test.expected, actual) - assert.NoError(t, actualErr) - } else { - assert.ErrorContains(t, actualErr, test.expectedError.Error()) - } - }) - } -} diff --git a/pkg/modules/interfaces.go b/pkg/modules/interfaces.go index 20bb37676..d0a63d480 100644 --- a/pkg/modules/interfaces.go +++ b/pkg/modules/interfaces.go @@ -1,17 +1,7 @@ package modules import ( - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - v1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" -) - -// GVKDeployment is the GroupVersionKind of Deployment -var ( - GVKDeployment = appsv1.SchemeGroupVersion.WithKind("Deployment").String() - GVKService = corev1.SchemeGroupVersion.WithKind("Service").String() ) // Generator is an interface for things that can generate Intent from input @@ -32,26 +22,18 @@ type NewGeneratorFunc func() (Generator, error) // NewPatcherFunc is a function that returns a Patcher. type NewPatcherFunc func() (Patcher, error) -// GeneratorContext defines the context object used for generator. -type GeneratorContext struct { - // Project provides basic project information for a given generator. - Project *v1.Project - - // Stack provides basic stack information for a given generator. - Stack *v1.Stack - - // Application provides basic application information for a given generator. - Application *inputs.AppConfiguration - - // Namespace specifies the target Kubernetes namespace. - Namespace string - - // ModuleInputs is the collection of module inputs for the target project. - ModuleInputs map[string]v1.GenericConfig - +// GeneratorRequest defines the request of generators. +type GeneratorRequest struct { + // Project represents the project name + Project string + // Stack represents the stack name + Stack string + // App represents the application name + App string + // Type represents the module type + Type string + // Config is the module inputs of the specific module type + Config v1.GenericConfig // TerraformConfig is the collection of provider configs for the terraform runtime. TerraformConfig v1.TerraformConfig - - // SecretStoreSpec is the external secret store spec - SecretStoreSpec *v1.SecretStoreSpec } diff --git a/pkg/modules/patchers/monitoring/monitoring_patcher.go b/pkg/modules/patchers/monitoring/monitoring_patcher.go deleted file mode 100644 index d9b958c97..000000000 --- a/pkg/modules/patchers/monitoring/monitoring_patcher.go +++ /dev/null @@ -1,163 +0,0 @@ -package monitoring - -import ( - "time" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - - prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "kusionstack.io/kube-api/apps/v1alpha1" - - apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules" - "kusionstack.io/kusion/pkg/modules/inputs" - "kusionstack.io/kusion/pkg/modules/inputs/monitoring" - "kusionstack.io/kusion/pkg/workspace" -) - -type monitoringPatcher struct { - app *inputs.AppConfiguration - modulesConfig map[string]apiv1.GenericConfig -} - -// NewMonitoringPatcher returns a Patcher. -func NewMonitoringPatcher(app *inputs.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) (modules.Patcher, error) { - return &monitoringPatcher{ - app: app, - modulesConfig: modulesConfig, - }, nil -} - -// NewMonitoringPatcherFunc returns a NewPatcherFunc. -func NewMonitoringPatcherFunc(app *inputs.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) modules.NewPatcherFunc { - return func() (modules.Patcher, error) { - return NewMonitoringPatcher(app, modulesConfig) - } -} - -// Patch implements Patcher interface. -func (p *monitoringPatcher) Patch(resources map[string][]*apiv1.Resource) error { - // If AppConfiguration does not contain monitoring config, return - if p.app.Monitoring == nil { - return nil - } - - // Patch workspace configurations for monitoring generator. - if err := p.parseWorkspaceConfig(); err != nil { - return err - } - - // If Prometheus runs as an operator, it relies on Custom Resources to - // manage the scrape configs. CRs (ServiceMonitors and PodMonitors) rely on - // corresponding resources (Services and Pods) to have labels that can be - // used as part of the label selector for the CR to determine which - // service/pods to scrape from. - // Here we choose the label name kusion_monitoring_appname for two reasons: - // 1. Unlike the label validation in Kubernetes, the label name accepted by - // Prometheus cannot contain non-alphanumeric characters except underscore: - // https://github.com/prometheus/common/blob/main/model/labels.go#L94 - // 2. The name should be unique enough that is only created by Kusion and - // used to identify a certain application - monitoringLabels := make(map[string]string) - monitoringAnnotations := make(map[string]string) - - if p.app.Monitoring.OperatorMode { - monitoringLabels["kusion_monitoring_appname"] = p.app.Name - } else { - // If Prometheus doesn't run as an operator, kusion will generate the - // most widely-known annotation for workloads that can be consumed by - // the out-of-the-box community version of Prometheus server - // installation shown as below. In this case, path and port cannot be - // omitted - if p.app.Monitoring.Path == "" || p.app.Monitoring.Port == "" { - return monitoring.ErrPathAndPortEmpty - } - monitoringAnnotations["prometheus.io/scrape"] = "true" - monitoringAnnotations["prometheus.io/scheme"] = p.app.Monitoring.Scheme - monitoringAnnotations["prometheus.io/path"] = p.app.Monitoring.Path - monitoringAnnotations["prometheus.io/port"] = p.app.Monitoring.Port - } - - if err := modules.PatchResource(resources, modules.GVKDeployment, func(obj *appsv1.Deployment) error { - obj.Labels = modules.MergeMaps(obj.Labels, monitoringLabels) - obj.Annotations = modules.MergeMaps(obj.Annotations, monitoringAnnotations) - obj.Spec.Template.Labels = modules.MergeMaps(obj.Spec.Template.Labels, monitoringLabels) - obj.Spec.Template.Annotations = modules.MergeMaps(obj.Spec.Template.Annotations, monitoringAnnotations) - return nil - }); err != nil { - return err - } - - if err := modules.PatchResource(resources, modules.GVKDeployment, func(obj *v1alpha1.CollaSet) error { - obj.Labels = modules.MergeMaps(obj.Labels, monitoringLabels) - obj.Annotations = modules.MergeMaps(obj.Annotations, monitoringAnnotations) - obj.Spec.Template.Labels = modules.MergeMaps(obj.Spec.Template.Labels, monitoringLabels) - obj.Spec.Template.Annotations = modules.MergeMaps(obj.Spec.Template.Annotations, monitoringAnnotations) - return nil - }); err != nil { - return err - } - - if err := modules.PatchResource(resources, modules.GVKService, func(obj *corev1.Service) error { - obj.Labels = modules.MergeMaps(obj.Labels, monitoringLabels) - obj.Annotations = modules.MergeMaps(obj.Annotations, monitoringAnnotations) - return nil - }); err != nil { - return err - } - return nil -} - -// parseWorkspaceConfig parses the config items for monitoring generator in workspace configurations. -func (p *monitoringPatcher) parseWorkspaceConfig() error { - wsConfig, ok := p.modulesConfig[monitoring.ModuleName] - // If AppConfiguration contains monitoring config but workspace does not, - // respond with the error ErrEmptyModuleConfigBlock - if p.app.Monitoring != nil && !ok { - return workspace.ErrEmptyModuleConfigBlock - } - - if operatorMode, ok := wsConfig[monitoring.OperatorModeKey]; ok { - p.app.Monitoring.OperatorMode = operatorMode.(bool) - } - - if monitorType, ok := wsConfig[monitoring.MonitorTypeKey]; ok { - p.app.Monitoring.MonitorType = monitoring.MonitorType(monitorType.(string)) - } else { - p.app.Monitoring.MonitorType = monitoring.DefaultMonitorType - } - - if interval, ok := wsConfig[monitoring.IntervalKey]; ok { - p.app.Monitoring.Interval = prometheusv1.Duration(interval.(string)) - } else { - p.app.Monitoring.Interval = monitoring.DefaultInterval - } - - if timeout, ok := wsConfig[monitoring.TimeoutKey]; ok { - p.app.Monitoring.Timeout = prometheusv1.Duration(timeout.(string)) - } else { - p.app.Monitoring.Timeout = monitoring.DefaultTimeout - } - - if scheme, ok := wsConfig[monitoring.SchemeKey]; ok { - p.app.Monitoring.Scheme = scheme.(string) - } else { - p.app.Monitoring.Scheme = monitoring.DefaultScheme - } - - parsedTimeout, err := time.ParseDuration(string(p.app.Monitoring.Timeout)) - if err != nil { - return err - } - parsedInterval, err := time.ParseDuration(string(p.app.Monitoring.Interval)) - if err != nil { - return err - } - - if parsedTimeout > parsedInterval { - return monitoring.ErrTimeoutGreaterThanInterval - } - - return nil -} diff --git a/pkg/modules/patchers/monitoring/monitoring_patcher_test.go b/pkg/modules/patchers/monitoring/monitoring_patcher_test.go deleted file mode 100644 index 7b61869b7..000000000 --- a/pkg/modules/patchers/monitoring/monitoring_patcher_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package monitoring - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - - 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/monitoring" -) - -func Test_monitoringPatcher_Patch(t *testing.T) { - i := &apiv1.Intent{} - err := modules.AppendToIntent(apiv1.Kubernetes, "id", i, buildMockDeployment()) - if err != nil { - t.Fatal(err) - } - - type fields struct { - app *modelsapp.AppConfiguration - workspace map[string]apiv1.GenericConfig - } - type args struct { - resources map[string][]*apiv1.Resource - } - tests := []struct { - name string - fields fields - args args - wantErr assert.ErrorAssertionFunc - }{ - { - name: "operatorModeTrue", - fields: fields{ - app: &modelsapp.AppConfiguration{ - Name: "test-app", - Monitoring: &monitoring.Monitor{ - Path: "/metrics", - Port: "web", - }, - }, - workspace: map[string]apiv1.GenericConfig{ - "monitoring": { - "operatorMode": true, - "monitorType": "Pod", - "scheme": "http", - "interval": "30s", - "timeout": "15s", - }, - }, - }, - args: args{ - resources: i.Resources.GVKIndex(), - }, - wantErr: assert.NoError, - }, - { - name: "operatorModeFalse", - fields: fields{ - app: &modelsapp.AppConfiguration{ - Name: "test-app", - Monitoring: &monitoring.Monitor{ - Path: "/metrics", - Port: "8080", - }, - }, - workspace: map[string]apiv1.GenericConfig{ - "monitoring": { - "operatorMode": false, - "scheme": "http", - "interval": "30s", - "timeout": "15s", - }, - }, - }, - args: args{ - resources: i.Resources.GVKIndex(), - }, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := &monitoringPatcher{ - app: tt.fields.app, - modulesConfig: tt.fields.workspace, - } - tt.wantErr(t, p.Patch(tt.args.resources), fmt.Sprintf("Patch(%v)", tt.args.resources)) - // check if the deployment is patched - var deployment appsv1.Deployment - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(i.Resources[0].Attributes, &deployment); err != nil { - t.Fatal(err) - } - if tt.fields.app.Monitoring.OperatorMode { - assert.NotNil(t, deployment.Labels) - assert.NotNil(t, deployment.Spec.Template.Labels) - assert.Equal(t, deployment.Labels["kusion_monitoring_appname"], tt.fields.app.Name) - assert.Equal(t, deployment.Spec.Template.Labels["kusion_monitoring_appname"], tt.fields.app.Name) - } else { - assert.NotNil(t, deployment.Annotations) - assert.NotNil(t, deployment.Spec.Template.Annotations) - assert.Equal(t, deployment.Annotations["prometheus.io/scrape"], "true") - assert.Equal(t, deployment.Annotations["prometheus.io/scheme"], tt.fields.app.Monitoring.Scheme) - assert.Equal(t, deployment.Annotations["prometheus.io/path"], tt.fields.app.Monitoring.Path) - assert.Equal(t, deployment.Annotations["prometheus.io/port"], tt.fields.app.Monitoring.Port) - assert.Equal(t, deployment.Spec.Template.Annotations["prometheus.io/scrape"], "true") - assert.Equal(t, deployment.Spec.Template.Annotations["prometheus.io/scheme"], tt.fields.app.Monitoring.Scheme) - assert.Equal(t, deployment.Spec.Template.Annotations["prometheus.io/path"], tt.fields.app.Monitoring.Path) - assert.Equal(t, deployment.Spec.Template.Annotations["prometheus.io/port"], tt.fields.app.Monitoring.Port) - } - }) - } -} - -// generate a mock Deployment -func buildMockDeployment() *appsv1.Deployment { - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mock-deployment", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1", - }, - Spec: appsv1.DeploymentSpec{}, - } -} - -func TestNewMonitoringPatcherFunc(t *testing.T) { - type args struct { - app *modelsapp.AppConfiguration - workspace map[string]apiv1.GenericConfig - } - tests := []struct { - name string - args args - }{ - { - name: "NewMonitoringPatcherFunc", - args: args{ - app: &modelsapp.AppConfiguration{ - Name: "test-app", - Monitoring: &monitoring.Monitor{ - Path: "/metrics", - Port: "web", - }, - }, - workspace: map[string]apiv1.GenericConfig{ - "monitoring": { - "operatorMode": true, - "monitorType": "Pod", - "scheme": "http", - "interval": "15s", - "timeout": "30s", - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - patcherFunc := NewMonitoringPatcherFunc(tt.args.app, tt.args.workspace) - assert.NotNil(t, patcherFunc) - patcher, err := patcherFunc() - assert.NoError(t, err) - assert.Equal(t, tt.args.app.Name, patcher.(*monitoringPatcher).app.Name) - }) - } -} diff --git a/pkg/modules/patchers/trait/ops_rule_patcher.go b/pkg/modules/patchers/trait/ops_rule_patcher.go deleted file mode 100644 index 80d9664cf..000000000 --- a/pkg/modules/patchers/trait/ops_rule_patcher.go +++ /dev/null @@ -1,49 +0,0 @@ -package trait - -import ( - appsv1 "k8s.io/api/apps/v1" - - 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 - modulesConfig map[string]apiv1.GenericConfig -} - -// NewOpsRulePatcherFunc returns a NewPatcherFunc. -func NewOpsRulePatcherFunc(app *modelsapp.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) modules.NewPatcherFunc { - return func() (modules.Patcher, error) { - return NewOpsRulePatcher(app, modulesConfig) - } -} - -// NewOpsRulePatcher returns a Patcher. -func NewOpsRulePatcher(app *modelsapp.AppConfiguration, modulesConfig map[string]apiv1.GenericConfig) (modules.Patcher, error) { - return &opsRulePatcher{ - app: app, - modulesConfig: modulesConfig, - }, nil -} - -// Patch implements Patcher interface. -func (p *opsRulePatcher) Patch(resources map[string][]*apiv1.Resource) error { - 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, 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, - } - return nil - }) -} diff --git a/pkg/modules/patchers/trait/ops_rule_patcher_test.go b/pkg/modules/patchers/trait/ops_rule_patcher_test.go deleted file mode 100644 index 9770933f2..000000000 --- a/pkg/modules/patchers/trait/ops_rule_patcher_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package trait - -import ( - "testing" - - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "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" -) - -func Test_opsRulePatcher_Patch(t *testing.T) { - i := &apiv1.Intent{} - err := modules.AppendToIntent(apiv1.Kubernetes, "id", i, buildMockDeployment()) - if err != nil { - t.Fatal(err) - } - - type fields struct { - app *modelsapp.AppConfiguration - workspaceConfig map[string]apiv1.GenericConfig - } - type args struct { - resources map[string][]*apiv1.Resource - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "Patch Deployment", - fields: fields{ - app: &modelsapp.AppConfiguration{ - OpsRule: &trait.OpsRule{ - MaxUnavailable: "30%", - }, - }, - }, - args: args{ - 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, - 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) - } - // check if the deployment is patched - var deployment appsv1.Deployment - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(i.Resources[0].Attributes, &deployment); err != nil { - t.Fatal(err) - } - assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, deployment.Spec.Strategy.Type) - assert.NotNil(t, deployment.Spec.Strategy.RollingUpdate) - 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()) - } - }) - } -} - -// generate a mock Deployment -func buildMockDeployment() *appsv1.Deployment { - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mock-deployment", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1", - }, - Spec: appsv1.DeploymentSpec{}, - } -} - -func TestNewOpsRulePatcherFunc(t *testing.T) { - p := &apiv1.Project{ - Name: "default", - } - type args struct { - app *modelsapp.AppConfiguration - project *apiv1.Project - workspace map[string]apiv1.GenericConfig - } - tests := []struct { - name string - args args - }{ - { - 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, tt.args.workspace) - assert.NotNil(t, patcherFunc) - patcher, err := patcherFunc() - assert.NoError(t, err) - assert.Equal(t, tt.args.app, patcher.(*opsRulePatcher).app) - }) - } -} diff --git a/pkg/modules/util.go b/pkg/modules/util.go index 2700dbe4d..602fa5f48 100644 --- a/pkg/modules/util.go +++ b/pkg/modules/util.go @@ -8,7 +8,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" apiv1 "kusionstack.io/kusion/pkg/apis/core/v1" - "kusionstack.io/kusion/pkg/modules/inputs" "kusionstack.io/kusion/pkg/workspace" ) @@ -41,25 +40,6 @@ func CallGenerators(i *apiv1.Intent, newGenerators ...NewGeneratorFunc) error { return nil } -// CallPatchers calls the Patch method of each Generator instance -// returned by the given NewPatcherFuncs. -func CallPatchers(resources map[string][]*apiv1.Resource, newPatchers ...NewPatcherFunc) error { - ps := make([]Patcher, 0, len(newPatchers)) - for _, newPatcher := range newPatchers { - if p, err := newPatcher(); err != nil { - return err - } else { - ps = append(ps, p) - } - } - for _, p := range ps { - if err := p.Patch(resources); err != nil { - return err - } - } - return nil -} - // ForeachOrdered executes the given function on each // item in the map in order of their keys. func ForeachOrdered[T any](m map[string]T, f func(key string, value T) error) error { @@ -120,34 +100,6 @@ func KubernetesResourceID(typeMeta metav1.TypeMeta, objectMeta metav1.ObjectMeta return id } -// TerraformResource returns the Terraform resource in the form of Intent.Resource -func TerraformResource(id string, dependsOn []string, attrs, exts map[string]interface{}) apiv1.Resource { - return apiv1.Resource{ - ID: id, - Type: apiv1.Terraform, - Attributes: attrs, - DependsOn: dependsOn, - Extensions: exts, - } -} - -// TerraformResourceID returns the unique ID of a Terraform resource -// based on its provider, type and name. -func TerraformResourceID(provider *inputs.Provider, resourceType string, resourceName string) string { - // resource id example: hashicorp:aws:aws_db_instance:wordpressdev - return provider.Namespace + ":" + provider.Name + ":" + resourceType + ":" + resourceName -} - -// ProviderExtensions returns the extended information of provider based on -// the provider and type of the resource. -func ProviderExtensions(provider *inputs.Provider, providerMeta map[string]any, resourceType string) map[string]interface{} { - return map[string]interface{}{ - "provider": provider.URL, - "providerMeta": providerMeta, - "resourceType": resourceType, - } -} - // KusionPathDependency returns the implicit resource dependency path based on // the resource id and name with the "$kusion_path" prefix. func KusionPathDependency(id, name string) string { diff --git a/pkg/modules/util_test.go b/pkg/modules/util_test.go index 9dd400802..ebeb12267 100644 --- a/pkg/modules/util_test.go +++ b/pkg/modules/util_test.go @@ -18,14 +18,6 @@ func (m *mockGenerator) Generate(i *apiv1.Intent) error { return m.GenerateFunc(i) } -type mockPatcher struct { - PatchFunc func(resources map[string][]*apiv1.Resource) error -} - -func (m *mockPatcher) Patch(resources map[string][]*apiv1.Resource) error { - return m.PatchFunc(resources) -} - func TestCallGenerators(t *testing.T) { i := &apiv1.Intent{} @@ -49,29 +41,6 @@ func TestCallGenerators(t *testing.T) { assert.EqualError(t, err, assert.AnError.Error()) } -func TestCallPatchers(t *testing.T) { - var ( - patcher1 Patcher = &mockPatcher{ - PatchFunc: func(resources map[string][]*apiv1.Resource) error { - return nil - }, - } - patcher2 Patcher = &mockPatcher{ - PatchFunc: func(resources map[string][]*apiv1.Resource) error { - return assert.AnError - }, - } - pf1 = func() (Patcher, error) { return patcher1, nil } - pf2 = func() (Patcher, error) { return patcher2, nil } - ) - err := CallPatchers(nil, pf1) - assert.NoError(t, err) - - err = CallPatchers(nil, pf1, pf2) - assert.Error(t, err) - assert.EqualError(t, err, assert.AnError.Error()) -} - func TestCallGeneratorFuncs(t *testing.T) { generatorFunc1 := func() (Generator, error) { return &mockGenerator{}, nil diff --git a/pkg/project/paths.go b/pkg/project/paths.go index 6fc3e6b9b..ed020e348 100644 --- a/pkg/project/paths.go +++ b/pkg/project/paths.go @@ -52,13 +52,13 @@ func DetectProjectAndStack(stackDir string) (p *v1.Project, s *v1.Stack, err err return p, s, nil } -// isProjectFile determine whether the given path is Project file +// isProjectFile determine whether the given path is project file func isProjectFile(path string) bool { f, err := os.Stat(path) return err == nil && !f.IsDir() && f.Mode().IsRegular() && filepath.Base(path) == ProjectFile } -// isProject determine whether the given path is Project directory +// isProject determine whether the given path is project directory func isProject(path string) bool { f, err := os.Stat(path) f2, err2 := os.Stat(filepath.Join(path, ProjectFile)) @@ -138,7 +138,7 @@ func FindAllProjectsFrom(path string) ([]*v1.Project, error) { return projects, err } -// IsStack determine whether the given path is Stack directory +// IsStack determine whether the given path is stack directory func IsStack(path string) bool { f, err := os.Stat(path) f2, err2 := os.Stat(filepath.Join(path, StackFile)) diff --git a/pkg/scaffold/templates.go b/pkg/scaffold/templates.go index f36cc731b..2f2eb6aa2 100644 --- a/pkg/scaffold/templates.go +++ b/pkg/scaffold/templates.go @@ -415,7 +415,7 @@ func RenderFSTemplate(srcFS afero.Fs, srcDir string, destFS afero.Fs, destDir st // Base dir or stack dir fileInfo, err := srcFS.Stat(filepath.Join(src, "stack.yaml")) if err == nil && fileInfo.Mode().IsRegular() { - // Project config can be overridden + // project config can be overridden configs := make(map[string]interface{}, len(tc.ProjectConfig)) for k, v := range tc.ProjectConfig { configs[k] = v @@ -435,7 +435,7 @@ func RenderFSTemplate(srcFS afero.Fs, srcDir string, destFS afero.Fs, destDir st return err } } else { - // Stack dir nested in 3rd level or even deeper, eg: meta_app/deployed_unit/stack_dir + // stack dir nested in 3rd level or even deeper, eg: meta_app/deployed_unit/stack_dir err = RenderFSTemplate(srcFS, src, destFS, dest, tc) if err != nil { return err diff --git a/pkg/scaffold/templates_test.go b/pkg/scaffold/templates_test.go index 4daee8d75..3cb2837d2 100644 --- a/pkg/scaffold/templates_test.go +++ b/pkg/scaffold/templates_test.go @@ -34,7 +34,7 @@ var ( ProjectFields: []*FieldTemplate{ { Name: "AppName", - Description: "The Application Name.", + Description: "The App Name.", Type: StringField, Default: "nginx", }, @@ -242,7 +242,7 @@ func Test_RenderMemTemplateFiles(t *testing.T) { }, StacksConfig: map[string]map[string]interface{}{ "dev": { - "Stack": "dev", + "stack": "dev", "Image": "foo/bar:v1", "ClusterName": "minikube", }, diff --git a/pkg/workspace/util.go b/pkg/workspace/util.go index ba9b5bbd5..8ac0d1951 100644 --- a/pkg/workspace/util.go +++ b/pkg/workspace/util.go @@ -21,8 +21,7 @@ func CompleteWorkspace(ws *v1.Workspace, name string) { } } -// GetProjectModuleConfigs returns the module configs of a specified project, whose key is the module name, -// should be called after ValidateModuleConfigs. +// GetProjectModuleConfigs returns the module configs of a specified project, whose key is the module name, should be called after ValidateModuleConfigs. // If got empty module configs, return nil config and nil error. func GetProjectModuleConfigs(configs v1.ModuleConfigs, projectName string) (map[string]v1.GenericConfig, error) { if len(configs) == 0 { @@ -32,25 +31,24 @@ func GetProjectModuleConfigs(configs v1.ModuleConfigs, projectName string) (map[ return nil, ErrEmptyProjectName } - projectCfgs := make(map[string]v1.GenericConfig) + projectConfigs := make(map[string]v1.GenericConfig) for name, cfg := range configs { - projectCfg, err := getProjectModuleConfig(cfg, projectName) - if projectCfg == nil { + moduleConfig, err := getProjectModuleConfig(cfg, projectName) + if moduleConfig == nil { continue } if err != nil { return nil, fmt.Errorf("%w, module name: %s", err, name) } - if len(projectCfg) != 0 { - projectCfgs[name] = projectCfg + if len(moduleConfig) != 0 { + projectConfigs[name] = moduleConfig } } - return projectCfgs, nil + return projectConfigs, nil } -// GetProjectModuleConfig returns the module config of a specified project, should be called after -// ValidateModuleConfig. +// GetProjectModuleConfig returns the module config of a specified project, should be called after ValidateModuleConfig. // If got empty module config, return nil config and nil error. func GetProjectModuleConfig(config *v1.ModuleConfig, projectName string) (v1.GenericConfig, error) { if config == nil { @@ -63,8 +61,7 @@ func GetProjectModuleConfig(config *v1.ModuleConfig, projectName string) (v1.Gen return getProjectModuleConfig(config, projectName) } -// getProjectModuleConfig gets the module config of a specified project without checking the correctness -// of project name. +// getProjectModuleConfig gets the module config of a specified project without checking the correctness of project name. func getProjectModuleConfig(config *v1.ModuleConfig, projectName string) (v1.GenericConfig, error) { projectCfg := config.Default if len(projectCfg) == 0 { @@ -212,18 +209,19 @@ func CompleteWholeS3Config(config *v1.S3Config) { } } -// GetIntFromGenericConfig returns the value of the key in config which should be of type int. -// If exist but not int, return error. If not exist, return 0, nil. -func GetIntFromGenericConfig(config v1.GenericConfig, key string) (int, error) { +// GetInt32PointerFromGenericConfig returns the value of the key in config which should be of type int. +// If exist but not int, return error. If not exist, return nil. +func GetInt32PointerFromGenericConfig(config v1.GenericConfig, key string) (*int32, error) { value, ok := config[key] if !ok { - return 0, nil + return nil, nil } i, ok := value.(int) if !ok { - return 0, fmt.Errorf("the value of %s is not int", key) + return nil, fmt.Errorf("the value of %s is not int", key) } - return i, nil + res := int32(i) + return &res, nil } // GetStringFromGenericConfig returns the value of the key in config which should be of type string. diff --git a/pkg/workspace/util_test.go b/pkg/workspace/util_test.go index 756f366c7..24a4a9207 100644 --- a/pkg/workspace/util_test.go +++ b/pkg/workspace/util_test.go @@ -219,7 +219,7 @@ func Test_GetIntFieldFromGenericConfig(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - value, err := GetIntFromGenericConfig(mockGenericConfig(), tc.key) + value, err := GetInt32PointerFromGenericConfig(mockGenericConfig(), tc.key) assert.Equal(t, tc.success, err == nil) assert.Equal(t, tc.expectedValue, value) }) diff --git a/third_party/kubevela/doc.go b/third_party/kubevela/doc.go index 0b128c135..5cd789bd7 100644 --- a/third_party/kubevela/doc.go +++ b/third_party/kubevela/doc.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package kubevela contains `Application` API +// Package kubevela contains `App` API // which is copied from [kubevela](https://github.com/oam-dev/kubevela) // and [workflow](https://github.com/kubevela/workflow). package kubevela diff --git a/third_party/kubevela/kubevela/apis/common/types.go b/third_party/kubevela/kubevela/apis/common/types.go index 9e4c57b36..69ac15b4d 100644 --- a/third_party/kubevela/kubevela/apis/common/types.go +++ b/third_party/kubevela/kubevela/apis/common/types.go @@ -9,7 +9,7 @@ import ( workflowv1alpha1 "kusionstack.io/kusion/third_party/kubevela/workflow/api/v1alpha1" ) -// AppStatus defines the observed state of Application +// AppStatus defines the observed state of App type AppStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file @@ -21,7 +21,7 @@ type AppStatus struct { Phase ApplicationPhase `json:"status,omitempty"` - // Components record the related Components created by Application Controller + // Components record the related Components created by App Controller Components []corev1.ObjectReference `json:"components,omitempty"` // Services record the status of the application services @@ -71,7 +71,7 @@ type ApplicationComponent struct { ReplicaKey string `json:"-"` } -// ApplicationComponentStatus record the health status of App component +// ApplicationComponentStatus record the health status of app component type ApplicationComponentStatus struct { Name string `json:"name"` Namespace string `json:"namespace,omitempty"` @@ -85,7 +85,7 @@ type ApplicationComponentStatus struct { Scopes []corev1.ObjectReference `json:"scopes,omitempty"` } -// WorkloadGVK refer to a Workload Type +// WorkloadGVK refer to a workload Type type WorkloadGVK struct { APIVersion string `json:"apiVersion"` Kind string `json:"kind"`