diff --git a/pkg/engine/api/builders/appconfig_builder.go b/pkg/engine/api/builders/appconfig_builder.go index 77d21bb5..9843913c 100644 --- a/pkg/engine/api/builders/appconfig_builder.go +++ b/pkg/engine/api/builders/appconfig_builder.go @@ -17,7 +17,6 @@ package builders import ( "fmt" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "kcl-lang.io/kpm/pkg/api" v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" @@ -41,7 +40,7 @@ func (acg *AppsConfigBuilder) Build(kclPackage *api.KclPackage, project *v1.Proj return fmt.Errorf("kcl package is nil when generating app configuration for %s", appName) } dependencies := kclPackage.GetDependenciesInModFile() - gfs = append(gfs, generators.NewAppConfigurationGeneratorFunc(project.Name, stack.Name, appName, &app, acg.Workspace, dependencies)) + gfs = append(gfs, generators.NewAppConfigurationGeneratorFunc(project, stack, appName, &app, acg.Workspace, dependencies)) return nil }) if err != nil { @@ -51,53 +50,5 @@ func (acg *AppsConfigBuilder) Build(kclPackage *api.KclPackage, project *v1.Proj return nil, err } - // updates generated spec resources based on project and stack extensions. - patchResourcesWithExtensions(project, stack, i) - return i, nil } - -// patchResourcesWithExtensions updates generated spec resources based on project and stack extensions. -func patchResourcesWithExtensions(project *v1.Project, stack *v1.Stack, spec *v1.Spec) { - extensions := mergeExtensions(project, stack) - if len(extensions) == 0 { - return - } - - for _, extension := range extensions { - switch extension.Kind { - case v1.KubernetesNamespace: - patchResourcesKubeNamespace(spec, extension.KubeNamespace.Namespace) - default: - // do nothing - } - } -} - -func patchResourcesKubeNamespace(spec *v1.Spec, namespace string) { - for _, resource := range spec.Resources { - if resource.Type == v1.Kubernetes { - u := &unstructured.Unstructured{Object: resource.Attributes} - u.SetNamespace(namespace) - } - } -} - -func mergeExtensions(project *v1.Project, stack *v1.Stack) []*v1.Extension { - var extensions []*v1.Extension - extensionKindMap := make(map[string]struct{}) - if stack.Extensions != nil && len(stack.Extensions) != 0 { - for _, extension := range stack.Extensions { - extensions = append(extensions, extension) - extensionKindMap[string(extension.Kind)] = struct{}{} - } - } - if project.Extensions != nil && len(project.Extensions) != 0 { - for _, extension := range project.Extensions { - if _, exist := extensionKindMap[string(extension.Kind)]; !exist { - extensions = append(extensions, extension) - } - } - } - return extensions -} diff --git a/pkg/engine/api/generate/run/testdata/base/base.k b/pkg/engine/api/generate/run/testdata/base/base.k index 2e4aab8a..41d9bc5c 100644 --- a/pkg/engine/api/generate/run/testdata/base/base.k +++ b/pkg/engine/api/generate/run/testdata/base/base.k @@ -1,12 +1,10 @@ import kam.v1.app_configuration as ac -import kam.v1.workload as wl -import kam.v1.workload.container as c -import kam.v1.workload.container.probe as p -import opsrule as t +import service as svc +import service.container as c # base.k declares reusable configurations for all stacks. helloworld: ac.AppConfiguration { - workload: wl.Service { + workload: svc.Service { containers: { "nginx": c.Container { image: "nginx:v1" @@ -23,22 +21,9 @@ helloworld: ac.AppConfiguration { } # Run the command "/bin/sh -c echo hi", as defined above, in the directory "/tmp" workingDir: "/tmp" - # Configure a HTTP readiness probe - readinessProbe: p.Probe { - probeHandler: p.Http { - url: "http://localhost:80" - } - initialDelaySeconds: 10 - } } } # Set the replicas replicas: 2 } - - accessories: { - "kusionstack/opsrule@v0.0.9": t.OpsRule { - maxUnavailable: "30%" - } - } } \ No newline at end of file diff --git a/pkg/engine/api/generate/run/testdata/prod/kcl.mod b/pkg/engine/api/generate/run/testdata/prod/kcl.mod index 33f08aa9..fc321f81 100644 --- a/pkg/engine/api/generate/run/testdata/prod/kcl.mod +++ b/pkg/engine/api/generate/run/testdata/prod/kcl.mod @@ -3,8 +3,8 @@ name = "testdata" version = "0.1.0" [dependencies] -opsrule = { oci = "oci://ghcr.io/kusionstack/opsrule", tag = "0.1.0" } -kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.1.0" } +kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0-beta" } +service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0-beta" } [profile] entries = ["../base/base.k", "main.k"] diff --git a/pkg/engine/api/generate/run/testdata/prod/main.k b/pkg/engine/api/generate/run/testdata/prod/main.k index b52afa1d..7f46173d 100644 --- a/pkg/engine/api/generate/run/testdata/prod/main.k +++ b/pkg/engine/api/generate/run/testdata/prod/main.k @@ -2,8 +2,4 @@ import kam.v1.app_configuration as ac # main.k declares customized configurations for prod stack. helloworld: ac.AppConfiguration { - workload.containers.nginx: { - # prod stack has different image - image = "nginx:v2" - } } \ No newline at end of file diff --git a/pkg/engine/api/generate/run/testdata/project.yaml b/pkg/engine/api/generate/run/testdata/project.yaml index 6c69f98f..0f7d2890 100644 --- a/pkg/engine/api/generate/run/testdata/project.yaml +++ b/pkg/engine/api/generate/run/testdata/project.yaml @@ -1,2 +1,6 @@ # The project basic info name: testdata +extensions: + - kind: "kubernetesNamespace" + kubernetesNamespace: + namespace: "dev" \ No newline at end of file diff --git a/pkg/modules/generators/app_configurations_generator.go b/pkg/modules/generators/app_configurations_generator.go index 3dff46a5..5e33af83 100644 --- a/pkg/modules/generators/app_configurations_generator.go +++ b/pkg/modules/generators/app_configurations_generator.go @@ -1,3 +1,17 @@ +// Copyright 2024 KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package generators import ( @@ -24,8 +38,8 @@ import ( ) type appConfigurationGenerator struct { - project string - stack string + project *v1.Project + stack *v1.Stack appName string app *v1.AppConfiguration ws *v1.Workspace @@ -33,19 +47,19 @@ type appConfigurationGenerator struct { } func NewAppConfigurationGenerator( - project string, - stack string, + project *v1.Project, + stack *v1.Stack, appName string, app *v1.AppConfiguration, ws *v1.Workspace, dependencies *pkg.Dependencies, ) (modules.Generator, error) { - if len(project) == 0 { - return nil, fmt.Errorf("project name must not be empty") + if project == nil { + return nil, fmt.Errorf("project must not be nil") } - if len(stack) == 0 { - return nil, fmt.Errorf("stack name must not be empty") + if stack == nil { + return nil, fmt.Errorf("stack must not be nil") } if len(appName) == 0 { @@ -61,7 +75,7 @@ func NewAppConfigurationGenerator( } if err := workspace.ValidateWorkspace(ws); err != nil { - return nil, fmt.Errorf("invalid config of workspace %s, %w", stack, err) + return nil, fmt.Errorf("invalid config of workspace %s, %w", ws.Name, err) } return &appConfigurationGenerator{ @@ -75,8 +89,8 @@ func NewAppConfigurationGenerator( } func NewAppConfigurationGeneratorFunc( - project string, - stack string, + project *v1.Project, + stack *v1.Stack, appName string, app *v1.AppConfiguration, ws *v1.Workspace, @@ -94,20 +108,18 @@ func (g *appConfigurationGenerator) Generate(spec *v1.Spec) error { g.app.Name = g.appName // retrieve the module configs of the specified project - projectModuleConfigs, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project) + projectModuleConfigs, err := workspace.GetProjectModuleConfigs(g.ws.Modules, g.project.Name) if err != nil { return err } - // todo: is namespace a module? how to retrieve it? Currently, it is configured in the workspace file. - namespace := g.getNamespaceName(projectModuleConfigs) - // generate built-in resources + namespace := g.getNamespaceName() gfs := []modules.NewGeneratorFunc{ NewNamespaceGeneratorFunc(namespace), workload.NewWorkloadGeneratorFunc(&workload.Generator{ - Project: g.project, - Stack: g.stack, + Project: g.project.Name, + Stack: g.stack.Name, App: g.appName, Namespace: namespace, Workload: g.app.Workload, @@ -489,8 +501,8 @@ func (g *appConfigurationGenerator) initModuleRequest(config moduleConfig) (*pro } protoRequest := &proto.GeneratorRequest{ - Project: g.project, - Stack: g.stack, + Project: g.project.Name, + Stack: g.stack.Name, App: g.appName, Workload: workloadConfig, DevConfig: devConfig, @@ -503,21 +515,38 @@ func (g *appConfigurationGenerator) initModuleRequest(config moduleConfig) (*pro // getNamespaceName obtains the final namespace name using the following precedence // (from lower to higher): // - Project name -// - Namespace module config (specified in corresponding workspace file) -func (g *appConfigurationGenerator) getNamespaceName(moduleConfigs map[string]v1.GenericConfig) string { - if moduleConfigs == nil { - return g.project - } - - namespaceName := g.project - namespaceModuleConfigs, exist := moduleConfigs["namespace"] - if exist { - if name, ok := namespaceModuleConfigs["name"]; ok { - customNamespaceName, isString := name.(string) - if isString && len(customNamespaceName) > 0 { - namespaceName = customNamespaceName +// - KubernetesNamespace extensions (specified in corresponding workspace file) +func (g *appConfigurationGenerator) getNamespaceName() string { + extensions := mergeExtensions(g.project, g.stack) + if len(extensions) != 0 { + for _, extension := range extensions { + switch extension.Kind { + case v1.KubernetesNamespace: + return extension.KubeNamespace.Namespace + default: + // do nothing + } + } + } + + return g.project.Name +} + +func mergeExtensions(project *v1.Project, stack *v1.Stack) []*v1.Extension { + var extensions []*v1.Extension + extensionKindMap := make(map[string]struct{}) + if stack.Extensions != nil && len(stack.Extensions) != 0 { + for _, extension := range stack.Extensions { + extensions = append(extensions, extension) + extensionKindMap[string(extension.Kind)] = struct{}{} + } + } + if project.Extensions != nil && len(project.Extensions) != 0 { + for _, extension := range project.Extensions { + if _, exist := extensionKindMap[string(extension.Kind)]; !exist { + extensions = append(extensions, extension) } } } - return namespaceName + return extensions } diff --git a/pkg/modules/generators/app_configurations_generator_test.go b/pkg/modules/generators/app_configurations_generator_test.go index bbb3a3c6..9fac6a9c 100644 --- a/pkg/modules/generators/app_configurations_generator_test.go +++ b/pkg/modules/generators/app_configurations_generator_test.go @@ -71,7 +71,7 @@ func mockPlugin() (*mockey.Mocker, *mockey.Mocker) { func TestAppConfigurationGenerator_Generate_CustomNamespace(t *testing.T) { appName, app := buildMockApp() - ws := buildMockWorkspace("fakeNs") + ws := buildMockWorkspace() dep := &pkg.Dependencies{ Deps: map[string]pkg.Dependency{ "port": { @@ -81,9 +81,17 @@ func TestAppConfigurationGenerator_Generate_CustomNamespace(t *testing.T) { }, } + project, stack := buildMockProjectAndStack() + kubeNamespaceExt := &v1.Extension{ + Kind: v1.KubernetesNamespace, + KubeNamespace: v1.KubeNamespaceExtension{ + Namespace: "fakeNs", + }, + } + project.Extensions = []*v1.Extension{kubeNamespaceExt} g := &appConfigurationGenerator{ - project: "testproject", - stack: "test", + project: project, + stack: stack, appName: appName, app: app, ws: ws, @@ -127,34 +135,35 @@ func TestAppConfigurationGenerator_Generate_CustomNamespace(t *testing.T) { func TestNewAppConfigurationGeneratorFunc(t *testing.T) { appName, app := buildMockApp() - ws := buildMockWorkspace("") + ws := buildMockWorkspace() + project, stack := buildMockProjectAndStack() t.Run("Valid app configuration generator func", func(t *testing.T) { - g, err := NewAppConfigurationGeneratorFunc("tesstproject", "test", appName, app, ws, nil)() + g, err := NewAppConfigurationGeneratorFunc(project, stack, appName, app, ws, nil)() assert.NoError(t, err) assert.NotNil(t, g) }) t.Run("Empty app name", func(t *testing.T) { - g, err := NewAppConfigurationGeneratorFunc("tesstproject", "test", "", app, ws, nil)() + g, err := NewAppConfigurationGeneratorFunc(project, stack, "", app, ws, nil)() assert.EqualError(t, err, "app name must not be empty") assert.Nil(t, g) }) t.Run("Nil app", func(t *testing.T) { - g, err := NewAppConfigurationGeneratorFunc("tesstproject", "test", appName, nil, ws, nil)() + g, err := NewAppConfigurationGeneratorFunc(project, stack, appName, nil, ws, nil)() assert.EqualError(t, err, "can not find app configuration when generating the Spec") assert.Nil(t, g) }) - t.Run("Empty project name", func(t *testing.T) { - g, err := NewAppConfigurationGeneratorFunc("", "test", appName, app, ws, nil)() - assert.EqualError(t, err, "project name must not be empty") + t.Run("Nil project", func(t *testing.T) { + g, err := NewAppConfigurationGeneratorFunc(nil, stack, appName, app, ws, nil)() + assert.EqualError(t, err, "project must not be nil") assert.Nil(t, g) }) t.Run("Empty workspace", func(t *testing.T) { - g, err := NewAppConfigurationGeneratorFunc("tesstproject", "test", appName, app, nil, nil)() + g, err := NewAppConfigurationGeneratorFunc(project, stack, appName, app, nil, nil)() assert.EqualError(t, err, "workspace must not be empty") assert.Nil(t, g) }) @@ -186,7 +195,7 @@ func buildMockApp() (string, *v1.AppConfiguration) { } } -func buildMockWorkspace(namespace string) *v1.Workspace { +func buildMockWorkspace() *v1.Workspace { return &v1.Workspace{ Name: "test", Modules: v1.ModuleConfigs{ @@ -218,15 +227,6 @@ func buildMockWorkspace(namespace string) *v1.Workspace { }, }, }, - "namespace": &v1.ModuleConfig{ - Path: "kusionstack.io/namespace", - Version: "v1.0.0", - Configs: v1.Configs{ - Default: v1.GenericConfig{ - "name": namespace, - }, - }, - }, }, Context: map[string]any{ "Kubernetes": map[string]string{ @@ -236,6 +236,18 @@ func buildMockWorkspace(namespace string) *v1.Workspace { } } +func buildMockProjectAndStack() (*v1.Project, *v1.Stack) { + p := &v1.Project{ + Name: "testproject", + } + + s := &v1.Stack{ + Name: "test", + } + + return p, s +} + func mapToUnstructured(data map[string]interface{}) *unstructured.Unstructured { unstructuredObj := &unstructured.Unstructured{} unstructuredObj.SetUnstructuredContent(data) @@ -375,12 +387,13 @@ func TestAppConfigurationGenerator_CallModules(t *testing.T) { // Mock app appConfig generator _, appConfig := buildMockApp() + project, stack := buildMockProjectAndStack() g := &appConfigurationGenerator{ - project: "testproject", - stack: "teststack", + project: project, + stack: stack, appName: "testapp", app: appConfig, - ws: buildMockWorkspace(""), + ws: buildMockWorkspace(), dependencies: dependencies, }