Skip to content

Commit

Permalink
fix: customize namespace issue to get correct k8s namespace name
Browse files Browse the repository at this point in the history
  • Loading branch information
adohe committed May 22, 2024
1 parent 519b8c3 commit 2de4826
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 116 deletions.
51 changes: 1 addition & 50 deletions pkg/engine/api/builders/appconfig_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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
}
21 changes: 3 additions & 18 deletions pkg/engine/api/generate/run/testdata/base/base.k
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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%"
}
}
}
4 changes: 2 additions & 2 deletions pkg/engine/api/generate/run/testdata/prod/kcl.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
4 changes: 0 additions & 4 deletions pkg/engine/api/generate/run/testdata/prod/main.k
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
4 changes: 4 additions & 0 deletions pkg/engine/api/generate/run/testdata/project.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# The project basic info
name: testdata
extensions:
- kind: "kubernetesNamespace"
kubernetesNamespace:
namespace: "dev"
93 changes: 61 additions & 32 deletions pkg/modules/generators/app_configurations_generator.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -24,28 +38,28 @@ import (
)

type appConfigurationGenerator struct {
project string
stack string
project *v1.Project
stack *v1.Stack
appName string
app *v1.AppConfiguration
ws *v1.Workspace
dependencies *pkg.Dependencies
}

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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
35 changes: 25 additions & 10 deletions pkg/modules/generators/app_configurations_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ func TestAppConfigurationGenerator_Generate_CustomNamespace(t *testing.T) {
},
}

project, stack := buildMockProjectAndStack()
g := &appConfigurationGenerator{
project: "testproject",
stack: "test",
project: project,
stack: stack,
appName: appName,
app: app,
ws: ws,
Expand Down Expand Up @@ -129,32 +130,33 @@ func TestNewAppConfigurationGeneratorFunc(t *testing.T) {
appName, app := buildMockApp()
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)()
t.Run("Nil project", func(t *testing.T) {
g, err := NewAppConfigurationGeneratorFunc(nil, stack, appName, app, ws, nil)()
assert.EqualError(t, err, "project name must not be empty")
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)
})
Expand Down Expand Up @@ -236,6 +238,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)
Expand Down Expand Up @@ -375,9 +389,10 @@ 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(""),
Expand Down

0 comments on commit 2de4826

Please sign in to comment.