diff --git a/components/buildless-serverless/api/v1alpha2/function_types.go b/components/buildless-serverless/api/v1alpha2/function_types.go index e618c16b0..75e2cc56b 100644 --- a/components/buildless-serverless/api/v1alpha2/function_types.go +++ b/components/buildless-serverless/api/v1alpha2/function_types.go @@ -43,7 +43,7 @@ type FunctionSpec struct { RuntimeImageOverride string `json:"runtimeImageOverride,omitempty"` // Contains the Function's source code configuration. - /* // +kubebuilder:validation:XValidation:message="Use GitRepository or Inline source",rule="has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) && has(self.inline)" */ + // +kubebuilder:validation:XValidation:message="Use GitRepository or Inline source",rule="has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) && has(self.inline)" // +kubebuilder:validation:Required Source Source `json:"source"` @@ -78,7 +78,7 @@ type FunctionSpec struct { type Source struct { // Defines the Function as git-sourced. Can't be used together with **Inline**. // +optional - // GitRepository *GitRepositorySource `json:"gitRepository,omitempty"` + GitRepository *GitRepositorySource `json:"gitRepository,omitempty"` // Defines the Function as the inline Function. Can't be used together with **GitRepository**. // +optional @@ -96,6 +96,33 @@ type InlineSource struct { Dependencies string `json:"dependencies,omitempty"` } +type GitRepositorySource struct { + // +kubebuilder:validation:Required + + // Specifies the URL of the Git repository with the Function's code and dependencies. + // Depending on whether the repository is public or private and what authentication method is used to access it, + // the URL must start with the `http(s)`, `git`, or `ssh` prefix. + URL string `json:"url"` + + // // Specifies the authentication method. Required for SSH. + // // +optional + // Auth *RepositoryAuth `json:"auth,omitempty"` + + // +kubebuilder:validation:XValidation:message="BaseDir is required and cannot be empty",rule="has(self.baseDir) && (self.baseDir.trim().size() != 0)" + // +kubebuilder:validation:XValidation:message="Reference is required and cannot be empty",rule="has(self.reference) && (self.reference.trim().size() != 0)" + Repository `json:",inline"` +} + +type Repository struct { + // Specifies the relative path to the Git directory that contains the source code + // from which the Function is built. + BaseDir string `json:"baseDir,omitempty"` + + // Specifies either the branch name, tag or commit revision from which the Function Controller + // automatically fetches the changes in the Function's code and dependencies. + Reference string `json:"reference,omitempty"` +} + type ResourceConfiguration struct { // Specifies resources requested by the Function's Pod. // +optional @@ -256,3 +283,21 @@ func (f *Function) PodLabels() map[string]string { } return labels.Merge(result, map[string]string{PodAppNameLabel: f.GetName()}) } + +func (f *Function) HasGitSources() bool { + return f.Spec.Source.GitRepository != nil +} + +func (f *Function) HasInlineSources() bool { + return f.Spec.Source.Inline != nil +} + +func (f *Function) HasPythonRuntime() bool { + runtime := f.Spec.Runtime + return runtime == Python312 +} + +func (f *Function) HasNodejsRuntime() bool { + runtime := f.Spec.Runtime + return runtime == NodeJs20 || runtime == NodeJs22 +} diff --git a/components/buildless-serverless/api/v1alpha2/zz_generated.deepcopy.go b/components/buildless-serverless/api/v1alpha2/zz_generated.deepcopy.go index 05f5052a8..0b5b40a7d 100644 --- a/components/buildless-serverless/api/v1alpha2/zz_generated.deepcopy.go +++ b/components/buildless-serverless/api/v1alpha2/zz_generated.deepcopy.go @@ -152,6 +152,22 @@ func (in *FunctionStatus) DeepCopy() *FunctionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitRepositorySource) DeepCopyInto(out *GitRepositorySource) { + *out = *in + out.Repository = in.Repository +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositorySource. +func (in *GitRepositorySource) DeepCopy() *GitRepositorySource { + if in == nil { + return nil + } + out := new(GitRepositorySource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InlineSource) DeepCopyInto(out *InlineSource) { *out = *in @@ -167,6 +183,21 @@ func (in *InlineSource) DeepCopy() *InlineSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Repository) DeepCopyInto(out *Repository) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Repository. +func (in *Repository) DeepCopy() *Repository { + if in == nil { + return nil + } + out := new(Repository) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceConfiguration) DeepCopyInto(out *ResourceConfiguration) { *out = *in @@ -225,6 +256,11 @@ func (in *SecretMount) DeepCopy() *SecretMount { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Source) DeepCopyInto(out *Source) { *out = *in + if in.GitRepository != nil { + in, out := &in.GitRepository, &out.GitRepository + *out = new(GitRepositorySource) + **out = **in + } if in.Inline != nil { in, out := &in.Inline, &out.Inline *out = new(InlineSource) diff --git a/components/buildless-serverless/config/crd/bases/serverless.kyma-project.io_functions.yaml b/components/buildless-serverless/config/crd/bases/serverless.kyma-project.io_functions.yaml index ecef32734..e19f32617 100644 --- a/components/buildless-serverless/config/crd/bases/serverless.kyma-project.io_functions.yaml +++ b/components/buildless-serverless/config/crd/bases/serverless.kyma-project.io_functions.yaml @@ -300,10 +300,37 @@ spec: type: object type: array source: - description: |- - Contains the Function's source code configuration. - // +kubebuilder:validation:XValidation:message="Use GitRepository or Inline source",rule="has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) && has(self.inline)" + description: Contains the Function's source code configuration. properties: + gitRepository: + description: Defines the Function as git-sourced. Can't be used + together with **Inline**. + properties: + baseDir: + description: |- + Specifies the relative path to the Git directory that contains the source code + from which the Function is built. + type: string + reference: + description: |- + Specifies either the branch name, tag or commit revision from which the Function Controller + automatically fetches the changes in the Function's code and dependencies. + type: string + url: + description: |- + Specifies the URL of the Git repository with the Function's code and dependencies. + Depending on whether the repository is public or private and what authentication method is used to access it, + the URL must start with the `http(s)`, `git`, or `ssh` prefix. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: BaseDir is required and cannot be empty + rule: has(self.baseDir) && (self.baseDir.trim().size() != 0) + - message: Reference is required and cannot be empty + rule: has(self.reference) && (self.reference.trim().size() != + 0) inline: description: Defines the Function as the inline Function. Can't be used together with **GitRepository**. @@ -319,6 +346,10 @@ spec: - source type: object type: object + x-kubernetes-validations: + - message: Use GitRepository or Inline source + rule: has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) + && has(self.inline) required: - runtime - source diff --git a/components/buildless-serverless/internal/controller/resources/deployment.go b/components/buildless-serverless/internal/controller/resources/deployment.go index a360623e0..e51705354 100644 --- a/components/buildless-serverless/internal/controller/resources/deployment.go +++ b/components/buildless-serverless/internal/controller/resources/deployment.go @@ -1,7 +1,9 @@ package resources import ( + "fmt" "path" + "strings" serverlessv1alpha2 "github.com/kyma-project/serverless/api/v1alpha2" "github.com/kyma-project/serverless/internal/config" @@ -65,10 +67,10 @@ func (d *Deployment) podRunAsUserUID() *int64 { func (d *Deployment) podSpec() corev1.PodSpec { secretVolumes, secretVolumeMounts := d.deploymentSecretVolumes() - defaultProcMount := corev1.DefaultProcMount return corev1.PodSpec{ - Volumes: append(d.volumes(), secretVolumes...), + Volumes: append(d.volumes(), secretVolumes...), + InitContainers: d.initContainerForGitRepository(), Containers: []corev1.Container{ { Name: d.name(), @@ -130,8 +132,8 @@ func (d *Deployment) podSpec() corev1.PodSpec { "ALL", }, }, - ProcMount: &defaultProcMount, - ReadOnlyRootFilesystem: ptr.To[bool](true), + ProcMount: ptr.To(corev1.DefaultProcMount), + ReadOnlyRootFilesystem: ptr.To[bool](false), }, }, }, @@ -145,6 +147,53 @@ func (d *Deployment) podSpec() corev1.PodSpec { } } +func (d *Deployment) initContainerForGitRepository() []corev1.Container { + if !d.function.HasGitSources() { + return []corev1.Container{} + } + return []corev1.Container{ + { + Name: fmt.Sprintf("%s-init", d.name()), + //TODO: should we use this image? + Image: "europe-docker.pkg.dev/kyma-project/prod/alpine-git:v20250212-39c86988", + WorkingDir: d.workingSourcesDir(), + Command: []string{ + "sh", + "-c", + d.initContainerCommand(), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "git-repository", + ReadOnly: false, + MountPath: "/git-repository", + }, + }, + SecurityContext: &corev1.SecurityContext{ + Privileged: ptr.To[bool](false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + ProcMount: ptr.To(corev1.DefaultProcMount), + ReadOnlyRootFilesystem: ptr.To[bool](false), + }, + }, + } +} + +func (d *Deployment) initContainerCommand() string { + gitRepo := d.function.Spec.Source.GitRepository + return fmt.Sprintf(`git clone --depth 1 --branch %s %s /git-repository/repo; +mkdir /git-repository/src; +cp /git-repository/repo/%s/* /git-repository/src`, + gitRepo.Reference, + gitRepo.URL, + strings.Trim(gitRepo.BaseDir, "/ "), + ) +} + func (d *Deployment) replicas() *int32 { replicas := d.function.Spec.Replicas if replicas != nil { @@ -155,7 +204,6 @@ func (d *Deployment) replicas() *int32 { } func (d *Deployment) volumes() []corev1.Volume { - runtime := d.function.Spec.Runtime volumes := []corev1.Volume{ { // used for writing sources (code&deps) to the sources dir @@ -180,7 +228,15 @@ func (d *Deployment) volumes() []corev1.Volume { }, }, } - if runtime == serverlessv1alpha2.Python312 { + if d.function.HasGitSources() { + volumes = append(volumes, corev1.Volume{ + Name: "git-repository", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + } + if d.function.HasPythonRuntime() { volumes = append(volumes, corev1.Volume{ // required by pip to save deps to .local dir Name: "local", @@ -193,7 +249,6 @@ func (d *Deployment) volumes() []corev1.Volume { } func (d *Deployment) volumeMounts() []corev1.VolumeMount { - runtime := d.function.Spec.Runtime volumeMounts := []corev1.VolumeMount{ { Name: "sources", @@ -205,15 +260,20 @@ func (d *Deployment) volumeMounts() []corev1.VolumeMount { MountPath: "/tmp", }, } - if runtime == serverlessv1alpha2.NodeJs20 || runtime == serverlessv1alpha2.NodeJs22 { + if d.function.HasGitSources() { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "git-repository", + MountPath: "/git-repository", + }) + } + if d.function.HasNodejsRuntime() { volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "package-registry-config", - ReadOnly: true, MountPath: path.Join(d.workingSourcesDir(), "package-registry-config/.npmrc"), SubPath: ".npmrc", }) } - if runtime == serverlessv1alpha2.Python312 { + if d.function.HasPythonRuntime() { volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "local", @@ -221,7 +281,6 @@ func (d *Deployment) volumeMounts() []corev1.VolumeMount { }, corev1.VolumeMount{ Name: "package-registry-config", - ReadOnly: true, MountPath: path.Join(d.workingSourcesDir(), "package-registry-config/pip.conf"), SubPath: "pip.conf", }) @@ -248,45 +307,72 @@ func (d *Deployment) runtimeImage() string { } func (d *Deployment) workingSourcesDir() string { - switch d.function.Spec.Runtime { - case serverlessv1alpha2.NodeJs20, serverlessv1alpha2.NodeJs22: + if d.function.HasNodejsRuntime() { return "/usr/src/app/function" - case serverlessv1alpha2.Python312: + } else if d.function.HasPythonRuntime() { return "/kubeless" - default: - return "" } + return "" } func (d *Deployment) runtimeCommand() string { + var result []string + result = append(result, d.runtimeCommandSources()) + result = append(result, d.runtimeCommandInstall()) + result = append(result, d.runtimeCommandStart()) + + return strings.Join(result, "\n") +} + +func (d *Deployment) runtimeCommandSources() string { + spec := &d.function.Spec + if spec.Source.GitRepository != nil { + return d.runtimeCommandGitSources() + } + return d.runtimeCommandInlineSources() +} + +func (d *Deployment) runtimeCommandGitSources() string { + return "cp /git-repository/src/* .;" +} + +func (d *Deployment) runtimeCommandInlineSources() string { + var result []string spec := &d.function.Spec dependencies := spec.Source.Inline.Dependencies - switch spec.Runtime { - case serverlessv1alpha2.NodeJs20, serverlessv1alpha2.NodeJs22: - if dependencies != "" { - return `echo "${FUNC_HANDLER_SOURCE}" > handler.js; -echo "${FUNC_HANDLER_DEPENDENCIES}" > package.json; -npm install --prefer-offline --no-audit --progress=false; -cd ..; -npm start;` - } - return `echo "${FUNC_HANDLER_SOURCE}" > handler.js; -cd ..; + + handlerName, dependenciesName := "", "" + if d.function.HasNodejsRuntime() { + handlerName, dependenciesName = "handler.js", "package.json" + } else if d.function.HasPythonRuntime() { + handlerName, dependenciesName = "handler.py", "requirements.txt" + } + + result = append(result, fmt.Sprintf(`echo "${FUNC_HANDLER_SOURCE}" > %s;`, handlerName)) + if dependencies != "" { + result = append(result, fmt.Sprintf(`echo "${FUNC_HANDLER_DEPENDENCIES}" > %s;`, dependenciesName)) + } + return strings.Join(result, "\n") +} + +func (d *Deployment) runtimeCommandInstall() string { + if d.function.HasNodejsRuntime() { + return `npm install --prefer-offline --no-audit --progress=false;` + } else if d.function.HasPythonRuntime() { + return `PIP_CONFIG_FILE=package-registry-config/pip.conf pip install --user --no-cache-dir -r /kubeless/requirements.txt;` + } + return "" +} + +func (d *Deployment) runtimeCommandStart() string { + if d.function.HasNodejsRuntime() { + return `cd ..; npm start;` - case serverlessv1alpha2.Python312: - if dependencies != "" { - return `echo "${FUNC_HANDLER_SOURCE}" > handler.py; -echo "${FUNC_HANDLER_DEPENDENCIES}" > requirements.txt; -PIP_CONFIG_FILE=package-registry-config/pip.conf pip install --user --no-cache-dir -r /kubeless/requirements.txt; -cd ..; -python /kubeless.py;` - } - return `echo "${FUNC_HANDLER_SOURCE}" > handler.py; -cd ..; + } else if d.function.HasPythonRuntime() { + return `cd ..; python /kubeless.py;` - default: - return "" } + return "" } func (d *Deployment) envs() []corev1.EnvVar { @@ -296,14 +382,6 @@ func (d *Deployment) envs() []corev1.EnvVar { Name: "SERVICE_NAMESPACE", Value: d.function.Namespace, }, - { - Name: "FUNC_HANDLER_SOURCE", - Value: spec.Source.Inline.Source, - }, - { - Name: "FUNC_HANDLER_DEPENDENCIES", - Value: spec.Source.Inline.Dependencies, - }, { Name: "TRACE_COLLECTOR_ENDPOINT", Value: d.functionConfig.FunctionTraceCollectorEndpoint, @@ -313,6 +391,18 @@ func (d *Deployment) envs() []corev1.EnvVar { Value: d.functionConfig.FunctionPublisherProxyAddress, }, } + if d.function.HasInlineSources() { + envs = append(envs, []corev1.EnvVar{ + { + Name: "FUNC_HANDLER_SOURCE", + Value: spec.Source.Inline.Source, + }, + { + Name: "FUNC_HANDLER_DEPENDENCIES", + Value: spec.Source.Inline.Dependencies, + }, + }...) + } if spec.Runtime == serverlessv1alpha2.Python312 { envs = append(envs, []corev1.EnvVar{ { diff --git a/components/buildless-serverless/internal/controller/resources/deployment_test.go b/components/buildless-serverless/internal/controller/resources/deployment_test.go index ab4e7a506..06d4c3b12 100644 --- a/components/buildless-serverless/internal/controller/resources/deployment_test.go +++ b/components/buildless-serverless/internal/controller/resources/deployment_test.go @@ -151,6 +151,7 @@ func TestDeployment_construct(t *testing.T) { "sh", "-c", `echo "${FUNC_HANDLER_SOURCE}" > handler.py; +PIP_CONFIG_FILE=package-registry-config/pip.conf pip install --user --no-cache-dir -r /kubeless/requirements.txt; cd ..; python /kubeless.py;`, }, @@ -214,7 +215,6 @@ python /kubeless.py;`, r.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ Name: "package-registry-config", - ReadOnly: true, MountPath: "/kubeless/package-registry-config/pip.conf", SubPath: "pip.conf", }) @@ -259,6 +259,34 @@ python /kubeless.py;`, }, }) }) + t.Run("doesn't create init container for inline function", func(t *testing.T) { + d := minimalDeployment() + + r := d.construct() + + require.NotNil(t, r) + require.Empty(t, r.Spec.Template.Spec.InitContainers) + }) + t.Run("create init container for git function with data based on function", func(t *testing.T) { + d := minimalDeployment() + d.function.Spec.Source = serverlessv1alpha2.Source{ + GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "wonderful-germain", + Repository: serverlessv1alpha2.Repository{ + BaseDir: "recursing-mcnulty", + Reference: "epic-mendel"}}} + + r := d.construct() + + require.NotNil(t, r) + require.Len(t, r.Spec.Template.Spec.InitContainers, 1) + c := r.Spec.Template.Spec.InitContainers[0] + expectedCommand := []string{"sh", "-c", + `git clone --depth 1 --branch epic-mendel wonderful-germain /git-repository/repo; +mkdir /git-repository/src; +cp /git-repository/repo/recursing-mcnulty/* /git-repository/src`} + require.Equal(t, expectedCommand, c.Command) + }) } func TestDeployment_name(t *testing.T) { @@ -472,11 +500,13 @@ func TestDeployment_volumeMounts(t *testing.T) { tests := []struct { name string runtime serverlessv1alpha2.Runtime + source serverlessv1alpha2.Source want []corev1.VolumeMount }{ { - name: "build volume mounts for nodejs20 based on function", + name: "build volume mounts for inline nodejs20 based on function", runtime: serverlessv1alpha2.NodeJs20, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, want: []corev1.VolumeMount{ { Name: "sources", @@ -489,15 +519,16 @@ func TestDeployment_volumeMounts(t *testing.T) { }, { Name: "package-registry-config", - ReadOnly: true, + ReadOnly: false, MountPath: "/usr/src/app/function/package-registry-config/.npmrc", SubPath: ".npmrc", }, }, }, { - name: "build volume mounts for nodejs22 based on function", + name: "build volume mounts for inline nodejs22 based on function", runtime: serverlessv1alpha2.NodeJs22, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, want: []corev1.VolumeMount{ { Name: "sources", @@ -510,15 +541,16 @@ func TestDeployment_volumeMounts(t *testing.T) { }, { Name: "package-registry-config", - ReadOnly: true, + ReadOnly: false, MountPath: "/usr/src/app/function/package-registry-config/.npmrc", SubPath: ".npmrc", }, }, }, { - name: "build volume mounts for python312 based on function", + name: "build volume mounts for inline python312 based on function", runtime: serverlessv1alpha2.Python312, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, want: []corev1.VolumeMount{ { Name: "sources", @@ -535,7 +567,67 @@ func TestDeployment_volumeMounts(t *testing.T) { }, { Name: "package-registry-config", - ReadOnly: true, + ReadOnly: false, + MountPath: "/kubeless/package-registry-config/pip.conf", + SubPath: "pip.conf", + }, + }, + }, + { + name: "build volume mounts for git nodejs22 based on function", + runtime: serverlessv1alpha2.NodeJs22, + source: serverlessv1alpha2.Source{GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "x", Repository: serverlessv1alpha2.Repository{BaseDir: "x", Reference: "x"}}}, + want: []corev1.VolumeMount{ + { + Name: "sources", + MountPath: "/usr/src/app/function", + }, + { + Name: "tmp", + ReadOnly: false, + MountPath: "/tmp", + }, + { + Name: "git-repository", + ReadOnly: false, + MountPath: "/git-repository", + }, + { + Name: "package-registry-config", + ReadOnly: false, + MountPath: "/usr/src/app/function/package-registry-config/.npmrc", + SubPath: ".npmrc", + }, + }, + }, + { + name: "build volume mounts for git python312 based on function", + runtime: serverlessv1alpha2.Python312, + source: serverlessv1alpha2.Source{GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "x", Repository: serverlessv1alpha2.Repository{BaseDir: "x", Reference: "x"}}}, + want: []corev1.VolumeMount{ + { + Name: "sources", + MountPath: "/kubeless", + }, + { + Name: "tmp", + ReadOnly: false, + MountPath: "/tmp", + }, + { + Name: "git-repository", + ReadOnly: false, + MountPath: "/git-repository", + }, + { + Name: "local", + MountPath: "/.local", + }, + { + Name: "package-registry-config", + ReadOnly: false, MountPath: "/kubeless/package-registry-config/pip.conf", SubPath: "pip.conf", }, @@ -548,6 +640,7 @@ func TestDeployment_volumeMounts(t *testing.T) { function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: tt.runtime, + Source: tt.source, }, }, } @@ -566,11 +659,69 @@ func TestDeployment_volumes(t *testing.T) { tests := []struct { name string runtime serverlessv1alpha2.Runtime + source serverlessv1alpha2.Source want []corev1.Volume }{ { - name: "build volumes for nodejs20 based on function", + name: "build volumes for inline nodejs20 based on function", runtime: serverlessv1alpha2.NodeJs20, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, + want: []corev1.Volume{ + { + Name: "sources", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "package-registry-config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "test-secret-name", + Optional: ptr.To[bool](true), + }, + }, + }, + { + Name: "tmp", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + { + name: "build volumes for inline nodejs22 based on function", + runtime: serverlessv1alpha2.NodeJs22, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, + want: []corev1.Volume{ + { + Name: "sources", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "package-registry-config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "test-secret-name", + Optional: ptr.To[bool](true), + }, + }, + }, + { + Name: "tmp", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + { + name: "build volumes for inline python312 based on function", + runtime: serverlessv1alpha2.Python312, + source: serverlessv1alpha2.Source{Inline: &serverlessv1alpha2.InlineSource{Source: "x", Dependencies: "x"}}, want: []corev1.Volume{ { Name: "sources", @@ -593,11 +744,19 @@ func TestDeployment_volumes(t *testing.T) { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "local", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, }, }, { - name: "build volumes for nodejs22 based on function", + name: "build volumes for git nodejs22 based on function", runtime: serverlessv1alpha2.NodeJs22, + source: serverlessv1alpha2.Source{GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "x", Repository: serverlessv1alpha2.Repository{BaseDir: "x", Reference: "x"}}}, want: []corev1.Volume{ { Name: "sources", @@ -620,11 +779,19 @@ func TestDeployment_volumes(t *testing.T) { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "git-repository", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, }, }, { - name: "build volumes for python312 based on function", + name: "build volumes for inline python312 based on function", runtime: serverlessv1alpha2.Python312, + source: serverlessv1alpha2.Source{GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "x", Repository: serverlessv1alpha2.Repository{BaseDir: "x", Reference: "x"}}}, want: []corev1.Volume{ { Name: "sources", @@ -647,6 +814,12 @@ func TestDeployment_volumes(t *testing.T) { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "git-repository", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, { Name: "local", VolumeSource: corev1.VolumeSource{ @@ -663,6 +836,7 @@ func TestDeployment_volumes(t *testing.T) { function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: tt.runtime, + Source: tt.source, }, }, } @@ -759,7 +933,7 @@ func TestDeployment_envs(t *testing.T) { want []corev1.EnvVar }{ { - name: "build envs based on nodejs20 function", + name: "build envs based on inline nodejs20 function", function: &serverlessv1alpha2.Function{ ObjectMeta: metav1.ObjectMeta{ Namespace: "function-namespace", @@ -798,7 +972,7 @@ func TestDeployment_envs(t *testing.T) { }, }, { - name: "build envs based on nodejs22 function", + name: "build envs based on inline nodejs22 function", function: &serverlessv1alpha2.Function{ ObjectMeta: metav1.ObjectMeta{ Namespace: "function-namespace", @@ -837,7 +1011,41 @@ func TestDeployment_envs(t *testing.T) { }, }, { - name: "build envs based on python312 function", + name: "build envs based on git nodejs22 function", + function: &serverlessv1alpha2.Function{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "function-namespace", + }, + Spec: serverlessv1alpha2.FunctionSpec{ + Runtime: serverlessv1alpha2.NodeJs22, + Source: serverlessv1alpha2.Source{ + GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "/some/url", + Repository: serverlessv1alpha2.Repository{ + BaseDir: "/some/dir", + Reference: "some-reference", + }, + }, + }, + }, + }, + want: []corev1.EnvVar{ + { + Name: "SERVICE_NAMESPACE", + Value: "function-namespace", + }, + { + Name: "TRACE_COLLECTOR_ENDPOINT", + Value: "test-trace-collector-endpoint", + }, + { + Name: "PUBLISHER_PROXY_ADDRESS", + Value: "test-proxy-address", + }, + }, + }, + { + name: "build envs based on inline python312 function", function: &serverlessv1alpha2.Function{ ObjectMeta: metav1.ObjectMeta{ Namespace: "function-namespace", @@ -896,7 +1104,7 @@ func TestDeployment_envs(t *testing.T) { r := d.envs() - assert.Equal(t, tt.want, r) + assert.ElementsMatch(t, tt.want, r) }) } } @@ -908,7 +1116,7 @@ func TestDeployment_runtimeCommand(t *testing.T) { want string }{ { - name: "build runtime command for python312 without dependencies", + name: "build runtime command for inline python312 without dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.Python312, @@ -920,11 +1128,12 @@ func TestDeployment_runtimeCommand(t *testing.T) { }, }, want: `echo "${FUNC_HANDLER_SOURCE}" > handler.py; +PIP_CONFIG_FILE=package-registry-config/pip.conf pip install --user --no-cache-dir -r /kubeless/requirements.txt; cd ..; python /kubeless.py;`, }, { - name: "build runtime command for python312 with dependencies", + name: "build runtime command for inline python312 with dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.Python312, @@ -943,7 +1152,28 @@ cd ..; python /kubeless.py;`, }, { - name: "build runtime command for nodejs20 without dependencies", + name: "build runtime command for git python312", + function: &serverlessv1alpha2.Function{ + Spec: serverlessv1alpha2.FunctionSpec{ + Runtime: serverlessv1alpha2.Python312, + Source: serverlessv1alpha2.Source{ + GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "/some/url", + Repository: serverlessv1alpha2.Repository{ + BaseDir: "/some/dir", + Reference: "some-reference", + }, + }, + }, + }, + }, + want: `cp /git-repository/src/* .; +PIP_CONFIG_FILE=package-registry-config/pip.conf pip install --user --no-cache-dir -r /kubeless/requirements.txt; +cd ..; +python /kubeless.py;`, + }, + { + name: "build runtime command for inline nodejs20 without dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.NodeJs20, @@ -955,11 +1185,12 @@ python /kubeless.py;`, }, }, want: `echo "${FUNC_HANDLER_SOURCE}" > handler.js; +npm install --prefer-offline --no-audit --progress=false; cd ..; npm start;`, }, { - name: "build runtime command for nodejs20 with dependencies", + name: "build runtime command for inline nodejs20 with dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.NodeJs20, @@ -978,7 +1209,28 @@ cd ..; npm start;`, }, { - name: "build runtime command for nodejs22 without dependencies", + name: "build runtime command for git nodejs20", + function: &serverlessv1alpha2.Function{ + Spec: serverlessv1alpha2.FunctionSpec{ + Runtime: serverlessv1alpha2.NodeJs20, + Source: serverlessv1alpha2.Source{ + GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "/some/url", + Repository: serverlessv1alpha2.Repository{ + BaseDir: "/some/dir", + Reference: "some-reference", + }, + }, + }, + }, + }, + want: `cp /git-repository/src/* .; +npm install --prefer-offline --no-audit --progress=false; +cd ..; +npm start;`, + }, + { + name: "build runtime command for inline nodejs22 without dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.NodeJs22, @@ -990,11 +1242,12 @@ npm start;`, }, }, want: `echo "${FUNC_HANDLER_SOURCE}" > handler.js; +npm install --prefer-offline --no-audit --progress=false; cd ..; npm start;`, }, { - name: "build runtime command for nodejs22 with dependencies", + name: "build runtime command for inline nodejs22 with dependencies", function: &serverlessv1alpha2.Function{ Spec: serverlessv1alpha2.FunctionSpec{ Runtime: serverlessv1alpha2.NodeJs22, @@ -1010,6 +1263,27 @@ npm start;`, echo "${FUNC_HANDLER_DEPENDENCIES}" > package.json; npm install --prefer-offline --no-audit --progress=false; cd ..; +npm start;`, + }, + { + name: "build runtime command for git nodejs22", + function: &serverlessv1alpha2.Function{ + Spec: serverlessv1alpha2.FunctionSpec{ + Runtime: serverlessv1alpha2.NodeJs22, + Source: serverlessv1alpha2.Source{ + GitRepository: &serverlessv1alpha2.GitRepositorySource{ + URL: "/some/url", + Repository: serverlessv1alpha2.Repository{ + BaseDir: "/some/dir", + Reference: "some-reference", + }, + }, + }, + }, + }, + want: `cp /git-repository/src/* .; +npm install --prefer-offline --no-audit --progress=false; +cd ..; npm start;`, }, } diff --git a/components/buildless-serverless/internal/controller/state/handle_deployment.go b/components/buildless-serverless/internal/controller/state/handle_deployment.go index 56c9ba16d..2f30e406d 100644 --- a/components/buildless-serverless/internal/controller/state/handle_deployment.go +++ b/components/buildless-serverless/internal/controller/state/handle_deployment.go @@ -23,6 +23,7 @@ import ( func sFnHandleDeployment(ctx context.Context, m *fsm.StateMachine) (fsm.StateFn, *ctrl.Result, error) { m.State.BuiltDeployment = resources.NewDeployment(&m.State.Function, &m.FunctionConfig) builtDeployment := m.State.BuiltDeployment.Deployment + //TODO: refactor this method - split get from create clusterDeployment, resultGet, errGet := getOrCreateDeployment(ctx, m, builtDeployment) if clusterDeployment == nil { @@ -111,6 +112,7 @@ func deploymentChanged(a *appsv1.Deployment, b *appsv1.Deployment) bool { len(b.Spec.Template.Spec.Containers) != 1 { return true } + aContainer := a.Spec.Template.Spec.Containers[0] bContainer := b.Spec.Template.Spec.Containers[0] @@ -134,7 +136,28 @@ func deploymentChanged(a *appsv1.Deployment, b *appsv1.Deployment) bool { resourcesChanged || envChanged || volumeMountsChanged || - portsChanged + portsChanged || + initContainerChanged(a, b) +} + +func initContainerChanged(a *appsv1.Deployment, b *appsv1.Deployment) bool { + // there are no init containers for inline function and one init container for git function + // when count of init containers is not equal function type has been changed + if len(a.Spec.Template.Spec.InitContainers) > 1 || + len(b.Spec.Template.Spec.InitContainers) > 1 || + len(a.Spec.Template.Spec.InitContainers) != len(b.Spec.Template.Spec.InitContainers) { + return true + } + if len(a.Spec.Template.Spec.InitContainers) == 0 { + return false + } + aInitContainer := a.Spec.Template.Spec.InitContainers[0] + bInitContainer := b.Spec.Template.Spec.InitContainers[0] + + initCommandChanged := !reflect.DeepEqual(aInitContainer.Command, bInitContainer.Command) + initVolumeMountsChanged := !reflect.DeepEqual(aInitContainer.VolumeMounts, bInitContainer.VolumeMounts) + return initCommandChanged || + initVolumeMountsChanged } func updateDeployment(ctx context.Context, m *fsm.StateMachine, clusterDeployment *appsv1.Deployment) (requeueNeeded bool, err error) { diff --git a/components/buildless-serverless/internal/controller/state/handle_deployment_test.go b/components/buildless-serverless/internal/controller/state/handle_deployment_test.go index 6ac692a4f..bd0e63f33 100644 --- a/components/buildless-serverless/internal/controller/state/handle_deployment_test.go +++ b/components/buildless-serverless/internal/controller/state/handle_deployment_test.go @@ -810,6 +810,96 @@ func Test_deploymentChanged(t *testing.T) { }, want: false, }, + { + name: "when there are no init containers should return false", + args: args{ + a: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{{}}}}}}, + b: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{{}}}}}}, + }, + want: false, + }, + { + name: "when init containers count is different should return true", + args: args{ + a: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Image: "vibrant-booth", + }}, + Containers: []corev1.Container{{}}}}}}, + b: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{{}}}}}}, + }, + want: true, + }, + { + name: "when init container commands are different should return true", + args: args{ + a: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Image: "gracious-mclaren", + Command: []string{"nice-mahavira"}, + }}, + Containers: []corev1.Container{{}}}}}}, + b: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Image: "gracious-mclaren", + Command: []string{"modest-kepler"}, + }}, + Containers: []corev1.Container{{}}}}}}, + }, + want: true, + }, + { + name: "when init container volume mounts are different should return true", + args: args{ + a: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Image: "thirsty-matsumoto", + VolumeMounts: []corev1.VolumeMount{{ + Name: "name", + MountPath: "/sweet/carver", + }}}}, + Containers: []corev1.Container{{}}}}}}, + b: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Image: "thirsty-matsumoto", + VolumeMounts: []corev1.VolumeMount{{ + Name: "name", + MountPath: "/focused/thompson", + }}}}, + Containers: []corev1.Container{{}}}}}}, + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/config/buildless-serverless/templates/crds.yaml b/config/buildless-serverless/templates/crds.yaml index 015d46d42..65ad77e1c 100644 --- a/config/buildless-serverless/templates/crds.yaml +++ b/config/buildless-serverless/templates/crds.yaml @@ -299,10 +299,37 @@ spec: type: object type: array source: - description: |- - Contains the Function's source code configuration. - // +kubebuilder:validation:XValidation:message="Use GitRepository or Inline source",rule="has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) && has(self.inline)" + description: Contains the Function's source code configuration. properties: + gitRepository: + description: Defines the Function as git-sourced. Can't be used + together with **Inline**. + properties: + baseDir: + description: |- + Specifies the relative path to the Git directory that contains the source code + from which the Function is built. + type: string + reference: + description: |- + Specifies either the branch name, tag or commit revision from which the Function Controller + automatically fetches the changes in the Function's code and dependencies. + type: string + url: + description: |- + Specifies the URL of the Git repository with the Function's code and dependencies. + Depending on whether the repository is public or private and what authentication method is used to access it, + the URL must start with the `http(s)`, `git`, or `ssh` prefix. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: BaseDir is required and cannot be empty + rule: has(self.baseDir) && (self.baseDir.trim().size() != 0) + - message: Reference is required and cannot be empty + rule: has(self.reference) && (self.reference.trim().size() != + 0) inline: description: Defines the Function as the inline Function. Can't be used together with **GitRepository**. @@ -318,6 +345,10 @@ spec: - source type: object type: object + x-kubernetes-validations: + - message: Use GitRepository or Inline source + rule: has(self.gitRepository) && !has(self.inline) || !has(self.gitRepository) + && has(self.inline) required: - runtime - source