Upstream fission/fission state of the touched files before fddd1d2205
This commit is contained in:
1 parent
fd83c655ca
commit
180cbd38f6
2 files changed
+1136
No files matched your search
@@ -0,0 +1,495 @@
|
||||
// SPDX-FileCopyrightText: The Fission Authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package newdeploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
k8s_err "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/core/v1"
|
||||
"github.com/fission/fission/pkg/executor/util"
|
||||
"github.com/fission/fission/pkg/svcinfo"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
otelUtils "github.com/fission/fission/pkg/utils/otel"
|
||||
)
|
||||
|
||||
func (deploy *NewDeploy) createOrGetDeployment(ctx context.Context, fn *fv1.Function, env *fv1.Environment,
|
||||
deployName string, deployLabels map[string]string, deployAnnotations map[string]string, deployNamespace string) (*appsv1.Deployment, error) {
|
||||
|
||||
specializationTimeout := fn.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout
|
||||
minScale := int32(fn.Spec.InvokeStrategy.ExecutionStrategy.MinScale)
|
||||
|
||||
// Always scale to at least one pod when createOrGetDeployment
|
||||
// is called. The idleObjectReaper will scale-in the deployment
|
||||
// later if no requests to the function.
|
||||
if minScale <= 0 {
|
||||
minScale = 1
|
||||
}
|
||||
|
||||
deployment, err := deploy.getDeploymentSpec(ctx, fn, env, &minScale, deployName, deployNamespace, deployLabels, deployAnnotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingDepl, err := deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Get(ctx, deployName, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
// Try to adopt orphan deployment created by the old executor.
|
||||
if existingDepl.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] != deploy.instanceID {
|
||||
existingDepl.Annotations = deployment.Annotations
|
||||
existingDepl.Labels = deployment.Labels
|
||||
existingDepl.OwnerReferences = deployment.OwnerReferences
|
||||
existingDepl.Spec.Template.Spec.Containers = deployment.Spec.Template.Spec.Containers
|
||||
existingDepl.Spec.Template.Spec.ServiceAccountName = deployment.Spec.Template.Spec.ServiceAccountName
|
||||
existingDepl.Spec.Template.Spec.TerminationGracePeriodSeconds = deployment.Spec.Template.Spec.TerminationGracePeriodSeconds
|
||||
|
||||
// Update with the latest deployment spec. Kubernetes will trigger
|
||||
// rolling update if spec is different from the one in the cluster.
|
||||
existingDepl, err = deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Update(ctx, existingDepl, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
deploy.logger.Error(err, "error adopting deploy", "deploy", deployName, "ns", deployNamespace)
|
||||
return nil, err
|
||||
}
|
||||
// In this case, we just return without waiting for it for fast bootstraping.
|
||||
return existingDepl, nil
|
||||
}
|
||||
|
||||
if *existingDepl.Spec.Replicas < minScale {
|
||||
err = util.ScaleDeployment(ctx, deploy.kubernetesClient, deploy.logger, existingDepl.Namespace, existingDepl.Name, minScale)
|
||||
if err != nil {
|
||||
deploy.logger.Error(err, "error scaling up function deployment", "function", fn.Name)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if existingDepl.Status.AvailableReplicas < minScale {
|
||||
existingDepl, err = util.WaitForDeployment(ctx, deploy.kubernetesClient, deploy.logger, existingDepl, minScale, specializationTimeout)
|
||||
}
|
||||
|
||||
return existingDepl, err
|
||||
} else if k8s_err.IsNotFound(err) {
|
||||
|
||||
depl, err := deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Create(ctx, deployment, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
if k8s_err.IsAlreadyExists(err) {
|
||||
depl, err = deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Get(ctx, deployName, metav1.GetOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
deploy.logger.Error(err, "error while creating function deployment", "function", fn.Name,
|
||||
"deployment_name", deployName,
|
||||
"deployment_namespace", deployNamespace)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if minScale > 0 {
|
||||
depl, err = util.WaitForDeployment(ctx, deploy.kubernetesClient, deploy.logger, depl, minScale, specializationTimeout)
|
||||
}
|
||||
return depl, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (deploy *NewDeploy) updateDeployment(ctx context.Context, deployment *appsv1.Deployment, ns string) error {
|
||||
_, err := deploy.kubernetesClient.AppsV1().Deployments(ns).Update(ctx, deployment, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (deploy *NewDeploy) deleteDeployment(ctx context.Context, ns string, name string) error {
|
||||
// DeletePropagationBackground deletes the object immediately and dependent are deleted later
|
||||
// DeletePropagationForeground not advisable; it marks for deletion and API can still serve those objects
|
||||
deletePropagation := metav1.DeletePropagationBackground
|
||||
return deploy.kubernetesClient.AppsV1().Deployments(ns).Delete(ctx, name, metav1.DeleteOptions{
|
||||
PropagationPolicy: &deletePropagation,
|
||||
})
|
||||
}
|
||||
|
||||
func (deploy *NewDeploy) getDeploymentSpec(ctx context.Context, fn *fv1.Function, env *fv1.Environment, targetReplicas *int32,
|
||||
deployName string, deployNamespace string, deployLabels map[string]string, deployAnnotations map[string]string) (*appsv1.Deployment, error) {
|
||||
|
||||
replicas := int32(fn.Spec.InvokeStrategy.ExecutionStrategy.MinScale)
|
||||
if targetReplicas != nil {
|
||||
replicas = *targetReplicas
|
||||
}
|
||||
|
||||
// Effective value: the apiserver fills an absent field with the CRD
|
||||
// default (90); the helper mirrors that for objects that never crossed
|
||||
// it. An explicit 0 — expressible from typed clients since the field
|
||||
// became a pointer — means no drain window.
|
||||
gracePeriodSeconds := env.Spec.EffectiveTerminationGracePeriod()
|
||||
|
||||
podAnnotations := env.Annotations
|
||||
if podAnnotations == nil {
|
||||
podAnnotations = make(map[string]string)
|
||||
}
|
||||
|
||||
// Here, we don't append deployAnnotations to podAnnotations
|
||||
// since newdeploy doesn't manager pod lifecycle directly.
|
||||
|
||||
if deploy.useIstio && env.Spec.AllowAccessToExternalNetwork {
|
||||
podAnnotations["sidecar.istio.io/inject"] = "false"
|
||||
}
|
||||
|
||||
podLabels := env.Labels
|
||||
if podLabels == nil {
|
||||
podLabels = make(map[string]string)
|
||||
}
|
||||
|
||||
maps.Copy(podLabels, deployLabels)
|
||||
|
||||
resources := deploy.getResources(env, fn)
|
||||
|
||||
// Set maxUnavailable and maxSurge to 20% is because we want
|
||||
// fission to rollout newer function version gradually without
|
||||
// affecting any online service. For example, if you set maxSurge
|
||||
// to 100%, the new ReplicaSet scales up immediately and may
|
||||
// consume all remaining compute resources which might be an
|
||||
// issue if a cluster's resource is on a budget.
|
||||
// TODO: add to ExecutionStrategy so that the user
|
||||
// can do more fine control over different functions.
|
||||
maxUnavailable := intstr.FromString("20%")
|
||||
maxSurge := intstr.FromString("20%")
|
||||
|
||||
// Newdeploy updates the environment variable "LastUpdateTimestamp" of deployment
|
||||
// whenever a configmap/secret gets an update, but it also leaves multiple ReplicaSets for
|
||||
// rollback purpose. Since fission always update a deployment instead of performing a
|
||||
// rollback, set RevisionHistoryLimit to 0 to disable this feature.
|
||||
revisionHistoryLimit := int32(0)
|
||||
|
||||
rvCount, err := util.ReferencedResourcesRVSum(ctx, deploy.kubernetesClient, fn.Namespace, fn.Spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Platform-owned env for the user container. Kept as one slice because
|
||||
// ApplyFunctionEnv re-appends it LAST when the function declares its own
|
||||
// env — kubelet last-wins is what keeps these names authoritative
|
||||
// (RFC-0030 §1); admission's reserved-name denylist is only fast feedback.
|
||||
platformEnv := append([]apiv1.EnvVar{
|
||||
{
|
||||
Name: fv1.ResourceVersionCount,
|
||||
Value: fmt.Sprintf("%d", rvCount),
|
||||
},
|
||||
// RFC-0023 state API env (nil when functionState is off). The token
|
||||
// itself arrives via the specialize-on-startup fetcher, which writes
|
||||
// it to the shared mount (see fetcher.StateTokenFileName).
|
||||
}, util.StateAPIEnvVars(deploy.fetcherConfig.SharedMountPath())...)
|
||||
|
||||
container, err := util.MergeContainer(&apiv1.Container{
|
||||
Name: env.Name,
|
||||
Image: env.Spec.Runtime.Image,
|
||||
ImagePullPolicy: deploy.runtimeImagePullPolicy,
|
||||
TerminationMessagePath: "/dev/termination-log",
|
||||
// Connection-draining preStop hook; see utils.DrainLifecycle.
|
||||
Lifecycle: utils.DrainLifecycle(gracePeriodSeconds),
|
||||
Env: platformEnv,
|
||||
// https://istio.io/docs/setup/kubernetes/additional-setup/requirements/
|
||||
Ports: []apiv1.ContainerPort{
|
||||
{
|
||||
Name: "http-env",
|
||||
// Now that we have added Port field in spec, should we make this configurable too?
|
||||
ContainerPort: int32(svcinfo.PortEnvRuntime),
|
||||
},
|
||||
},
|
||||
Resources: resources,
|
||||
}, env.Spec.Runtime.Container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// AutomountServiceAccountToken=false stops Kubernetes from injecting
|
||||
// the fission-fetcher ServiceAccount token into every container in
|
||||
// the pod. The fetcher container re-mounts the token via the
|
||||
// projected volume defined below — the user-code container does not.
|
||||
// See GHSA-85g2-pmrx-r49q.
|
||||
automountSAToken := false
|
||||
pod := apiv1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: podLabels,
|
||||
Annotations: podAnnotations,
|
||||
},
|
||||
Spec: apiv1.PodSpec{
|
||||
Containers: []apiv1.Container{*container},
|
||||
ServiceAccountName: fv1.FissionFetcherSA,
|
||||
AutomountServiceAccountToken: &automountSAToken,
|
||||
TerminationGracePeriodSeconds: &gracePeriodSeconds,
|
||||
Volumes: []apiv1.Volume{
|
||||
util.FetcherSATokenProjectedVolume(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if deploy.podSpecPatch != nil {
|
||||
|
||||
updatedPodSpec, err := util.MergePodSpec(&pod.Spec, deploy.podSpecPatch)
|
||||
if err == nil {
|
||||
pod.Spec = *updatedPodSpec
|
||||
} else {
|
||||
deploy.logger.Error(err, "Failed to merge the specs")
|
||||
}
|
||||
// Re-clamp after the merge: MergePodSpec propagates a non-nil
|
||||
// AutomountServiceAccountToken from the patch, which would otherwise
|
||||
// re-enable the kubelet auto-mount on the user container. See
|
||||
// GHSA-85g2-pmrx-r49q.
|
||||
pod.Spec.AutomountServiceAccountToken = new(false)
|
||||
}
|
||||
|
||||
pod.Spec = *(util.ApplyImagePullSecret(env.Spec.ImagePullSecret, pod.Spec))
|
||||
|
||||
var ownerReferences []metav1.OwnerReference
|
||||
if deploy.enableOwnerReferences {
|
||||
ownerReferences = []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(fn, fv1.SchemeGroupVersion.WithKind("Function")),
|
||||
}
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: deployName,
|
||||
Labels: deployLabels,
|
||||
Annotations: deployAnnotations,
|
||||
OwnerReferences: ownerReferences,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: &replicas,
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: deployLabels,
|
||||
},
|
||||
Template: pod,
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
RollingUpdate: &appsv1.RollingUpdateDeployment{
|
||||
MaxUnavailable: &maxUnavailable,
|
||||
MaxSurge: &maxSurge,
|
||||
},
|
||||
},
|
||||
RevisionHistoryLimit: &revisionHistoryLimit,
|
||||
},
|
||||
}
|
||||
|
||||
// If custom runtime container name - default env name
|
||||
mainContainerName, err := deploy.mainContainerName(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Order of merging is important here - first fetcher, then containers and lastly pod spec
|
||||
err = deploy.fetcherConfig.AddSpecializingFetcherToPodSpec(
|
||||
&deployment.Spec.Template.Spec,
|
||||
mainContainerName,
|
||||
deployNamespace,
|
||||
fn,
|
||||
env,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if env.Spec.Runtime.PodSpec != nil {
|
||||
newPodSpec, err := util.MergePodSpec(&deployment.Spec.Template.Spec, env.Spec.Runtime.PodSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deployment.Spec.Template.Spec = *newPodSpec
|
||||
// Re-clamp after the merge: MergePodSpec propagates a non-nil
|
||||
// AutomountServiceAccountToken from env.Spec.Runtime.PodSpec, which
|
||||
// would otherwise re-enable the kubelet auto-mount on the user
|
||||
// container. See GHSA-85g2-pmrx-r49q.
|
||||
deployment.Spec.Template.Spec.AutomountServiceAccountToken = new(false)
|
||||
}
|
||||
|
||||
// Fill-if-nil: applies env.Spec.RuntimeClassName only when the merge
|
||||
// above (or the base spec) left RuntimeClassName unset, so a
|
||||
// Runtime.PodSpec override always wins. Unconditional — an env can set
|
||||
// RuntimeClassName with no Runtime.PodSpec at all.
|
||||
util.ApplyEnvRuntimeClass(&deployment.Spec.Template.Spec, env)
|
||||
|
||||
// RFC-0030 per-function env. This must run AFTER the
|
||||
// env.Spec.Runtime.PodSpec merge (and after the container merge above):
|
||||
// MergePodSpec appends the environment podspec container's Env/EnvFrom,
|
||||
// so applying earlier would let the environment's entries land last and
|
||||
// win — inverting the specified precedence (env podspec < function
|
||||
// EnvFrom < function Env) — and would leave a duplicate env name for
|
||||
// MergeContainer to reject outright. It also targets mainContainerName,
|
||||
// not env.Name: a custom runtime container renames the user container.
|
||||
if err := util.ApplyFunctionEnv(&deployment.Spec.Template.Spec, mainContainerName, deployNamespace, fn, platformEnv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Re-mount the fission-fetcher SA token at the canonical Kubernetes
|
||||
// path on the fetcher container only. The pod-level
|
||||
// AutomountServiceAccountToken=false flag set above suppresses the
|
||||
// implicit mount on every container, including fetcher, so we have
|
||||
// to add it back explicitly here. This must run AFTER the
|
||||
// env.Spec.Runtime.PodSpec merge — MergePodSpec can append additional
|
||||
// volumeMounts to the fetcher container, including one at this same
|
||||
// path, and kubelet would reject the pod with a duplicate-mount-path
|
||||
// error. The helper strips any pre-existing mount at the path before
|
||||
// adding its own, so running it last guarantees a single mount on the
|
||||
// fetcher container backed by the projected SA token volume. See
|
||||
// GHSA-85g2-pmrx-r49q.
|
||||
if err := util.MountFetcherSATokenOnFetcher(&deployment.Spec.Template.Spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// RFC-0001 Path B: mount the package image read-only at the fetcher's
|
||||
// store path on both containers. The fetcher's exists-early-exit then
|
||||
// skips the pull and proceeds straight to secrets + load — Path B for
|
||||
// newdeploy is delivery-only; the stock fetcher flow runs unchanged
|
||||
// (the early-exit makes the fetch a no-op). Applied AFTER every
|
||||
// MergePodSpec (same convention as the SA-token re-clamps) so a runtime
|
||||
// pod spec cannot strip or shadow the code mount.
|
||||
oci, err := deploy.getFunctionOCIArchive(ctx, fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oci != nil {
|
||||
if err := util.AddImageVolume(&deployment.Spec.Template.Spec, oci,
|
||||
filepath.Join(deploy.fetcherConfig.SharedMountPath(), deploy.fetcherConfig.TargetFilename(fn, env)),
|
||||
mainContainerName, util.FetcherContainerName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return deployment, nil
|
||||
}
|
||||
|
||||
// mainContainerName resolves the name of the function's main (user-code)
|
||||
// container in the deployment's pod spec. It defaults to the environment name
|
||||
// and switches to the custom runtime container name when the environment
|
||||
// declares one that is actually present in the runtime PodSpec. It is shared by
|
||||
// the deployment-spec builder and the HPA call site so the ContainerResource
|
||||
// metric can never name a container that differs from the one in the pod.
|
||||
func (deploy *NewDeploy) mainContainerName(env *fv1.Environment) (string, error) {
|
||||
mainContainerName := env.Name
|
||||
if env.Spec.Runtime.Container != nil && env.Spec.Runtime.Container.Name != "" && env.Spec.Runtime.PodSpec != nil {
|
||||
if util.DoesContainerExistInPodSpec(env.Spec.Runtime.Container.Name, env.Spec.Runtime.PodSpec) {
|
||||
mainContainerName = env.Spec.Runtime.Container.Name
|
||||
} else {
|
||||
return "", fmt.Errorf("runtime container %s not found in pod spec", env.Spec.Runtime.Container.Name)
|
||||
}
|
||||
}
|
||||
return mainContainerName, nil
|
||||
}
|
||||
|
||||
// getResources overrides only the resources which are overridden at function level otherwise
|
||||
// default to resources specified at environment level
|
||||
func (deploy *NewDeploy) getResources(env *fv1.Environment, fn *fv1.Function) apiv1.ResourceRequirements {
|
||||
resources := env.Spec.Resources
|
||||
if resources.Requests == nil {
|
||||
resources.Requests = make(map[apiv1.ResourceName]resource.Quantity)
|
||||
}
|
||||
if resources.Limits == nil {
|
||||
resources.Limits = make(map[apiv1.ResourceName]resource.Quantity)
|
||||
}
|
||||
// Only override the once specified at function, rest default to values from env.
|
||||
val, ok := fn.Spec.Resources.Requests[apiv1.ResourceCPU]
|
||||
if ok && !val.IsZero() {
|
||||
resources.Requests[apiv1.ResourceCPU] = fn.Spec.Resources.Requests[apiv1.ResourceCPU]
|
||||
}
|
||||
|
||||
val, ok = fn.Spec.Resources.Requests[apiv1.ResourceMemory]
|
||||
if ok && !val.IsZero() {
|
||||
resources.Requests[apiv1.ResourceMemory] = fn.Spec.Resources.Requests[apiv1.ResourceMemory]
|
||||
}
|
||||
|
||||
val, ok = fn.Spec.Resources.Limits[apiv1.ResourceCPU]
|
||||
if ok && !val.IsZero() {
|
||||
resources.Limits[apiv1.ResourceCPU] = fn.Spec.Resources.Limits[apiv1.ResourceCPU]
|
||||
}
|
||||
|
||||
val, ok = fn.Spec.Resources.Limits[apiv1.ResourceMemory]
|
||||
if ok && !val.IsZero() {
|
||||
resources.Limits[apiv1.ResourceMemory] = fn.Spec.Resources.Limits[apiv1.ResourceMemory]
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func (deploy *NewDeploy) createOrGetSvc(ctx context.Context, fn *fv1.Function, deployLabels map[string]string, deployAnnotations map[string]string, svcName string, svcNamespace string) (*apiv1.Service, error) {
|
||||
logger := otelUtils.LoggerWithTraceID(ctx, deploy.logger)
|
||||
var ownerReferences []metav1.OwnerReference
|
||||
if deploy.enableOwnerReferences {
|
||||
ownerReferences = []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(fn, fv1.SchemeGroupVersion.WithKind("Function")),
|
||||
}
|
||||
}
|
||||
|
||||
// The Service carries the managed-by label (RFC-0002) so the EndpointSlice
|
||||
// controller mirrors it onto the slices and the router's label-filtered
|
||||
// informer sees them. Labels only — the selector stays deployLabels.
|
||||
svcLabels := make(map[string]string, len(deployLabels)+1)
|
||||
maps.Copy(svcLabels, deployLabels)
|
||||
svcLabels[fv1.MANAGED_BY_LABEL] = fv1.MANAGED_BY_VALUE
|
||||
|
||||
service := &apiv1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
Labels: svcLabels,
|
||||
Annotations: deployAnnotations,
|
||||
OwnerReferences: ownerReferences,
|
||||
},
|
||||
Spec: apiv1.ServiceSpec{
|
||||
Ports: []apiv1.ServicePort{
|
||||
{
|
||||
Name: "http-env",
|
||||
Port: int32(80),
|
||||
// Since Function spec now supports Port , should we make this configurable too?
|
||||
TargetPort: intstr.FromInt(svcinfo.PortEnvRuntime),
|
||||
},
|
||||
},
|
||||
Selector: deployLabels,
|
||||
Type: apiv1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
|
||||
svc, created, err := util.CreateOrAdoptService(ctx, deploy.kubernetesClient, logger, deploy.instanceID, svcNamespace, service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if created {
|
||||
otelUtils.SpanTrackEvent(ctx, "createdService", otelUtils.GetAttributesForSvc(svc)...)
|
||||
}
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (deploy *NewDeploy) deleteSvc(ctx context.Context, ns string, name string) error {
|
||||
return deploy.kubernetesClient.CoreV1().Services(ns).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// cleanupNewdeploy cleans all kubernetes objects related to function
|
||||
func (deploy *NewDeploy) cleanupNewdeploy(ctx context.Context, ns string, name string) error {
|
||||
var result error
|
||||
|
||||
err := deploy.deleteSvc(ctx, ns, name)
|
||||
if err != nil && !k8s_err.IsNotFound(err) {
|
||||
deploy.logger.Error(err, "error deleting service for newdeploy function", "function_name", name,
|
||||
"function_namespace", ns)
|
||||
result = errors.Join(result, err)
|
||||
}
|
||||
|
||||
err = deploy.hpaops.DeleteHpa(ctx, ns, name)
|
||||
if err != nil && !k8s_err.IsNotFound(err) {
|
||||
deploy.logger.Error(err, "error deleting HPA for newdeploy function", "function_name", name,
|
||||
"function_namespace", ns)
|
||||
result = errors.Join(result, err)
|
||||
}
|
||||
|
||||
err = deploy.deleteDeployment(ctx, ns, name)
|
||||
if err != nil && !k8s_err.IsNotFound(err) {
|
||||
deploy.logger.Error(err, "error deleting deployment for newdeploy function", "function_name", name,
|
||||
"function_namespace", ns)
|
||||
result = errors.Join(result, err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
// SPDX-FileCopyrightText: The Fission Authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package newdeploy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/core/v1"
|
||||
ferror "github.com/fission/fission/pkg/error"
|
||||
"github.com/fission/fission/pkg/executor/fscache"
|
||||
"github.com/fission/fission/pkg/executor/util"
|
||||
hpautils "github.com/fission/fission/pkg/executor/util/hpa"
|
||||
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
|
||||
fissionfake "github.com/fission/fission/pkg/generated/clientset/versioned/fake"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
"github.com/fission/fission/pkg/utils/loggerfactory"
|
||||
)
|
||||
|
||||
const envCName = "newdeploy-test-env"
|
||||
|
||||
// newTestNewDeploy returns a minimal NewDeploy wired up for unit tests of
|
||||
// getDeploymentSpec.
|
||||
func newTestNewDeploy(t *testing.T) *NewDeploy {
|
||||
t.Helper()
|
||||
cfg, err := fetcherConfig.MakeFetcherConfig("/userfunc")
|
||||
require.NoError(t, err)
|
||||
return &NewDeploy{
|
||||
logger: loggerfactory.GetLogger(),
|
||||
kubernetesClient: fake.NewSimpleClientset(),
|
||||
fetcherConfig: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func newTestNewDeployEnv() *fv1.Environment {
|
||||
return &fv1.Environment{
|
||||
Name: envCName,
|
||||
Namespace: "default",
|
||||
Spec: fv1.EnvironmentSpec{
|
||||
Version: 1,
|
||||
Runtime: fv1.Runtime{
|
||||
Image: "fission/test-env:latest",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTestNewDeployFunction() *fv1.Function {
|
||||
return &fv1.Function{
|
||||
Name: "newdeploy-test-fn",
|
||||
Namespace: "default",
|
||||
Spec: fv1.FunctionSpec{
|
||||
Environment: fv1.EnvironmentReference{
|
||||
Name: envCName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Package: fv1.FunctionPackageRef{
|
||||
PackageRef: fv1.PackageRef{
|
||||
Namespace: "default",
|
||||
Name: "pkg-1",
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
},
|
||||
InvokeStrategy: fv1.InvokeStrategy{
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestMainContainerName covers mainContainerName, the helper shared by the
|
||||
// deployment-spec builder and the HPA call site. The updateFunction path relies
|
||||
// on its error branch to refuse rewriting the ContainerResource metric to a
|
||||
// container that does not exist in the pod spec.
|
||||
func TestMainContainerName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(env *fv1.Environment)
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "defaults to env name when no custom runtime container",
|
||||
mutate: func(*fv1.Environment) {},
|
||||
want: envCName,
|
||||
},
|
||||
{
|
||||
name: "uses custom runtime container name present in pod spec",
|
||||
mutate: func(env *fv1.Environment) {
|
||||
env.Spec.Runtime.Container = &apiv1.Container{Name: "custom-runtime"}
|
||||
env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
|
||||
Containers: []apiv1.Container{{Name: "custom-runtime"}},
|
||||
}
|
||||
},
|
||||
want: "custom-runtime",
|
||||
},
|
||||
{
|
||||
name: "errors when custom runtime container absent from pod spec",
|
||||
mutate: func(env *fv1.Environment) {
|
||||
env.Spec.Runtime.Container = &apiv1.Container{Name: "custom-runtime"}
|
||||
env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
|
||||
Containers: []apiv1.Container{{Name: "some-other-container"}},
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
tc.mutate(env)
|
||||
|
||||
got, err := deploy.mainContainerName(env)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, got)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewDeployFnDeleteCacheMiss verifies that deleting a function whose fsvc
|
||||
// is absent from the in-memory cache (never specialized, evicted, or executor
|
||||
// restarted) does not error out — it must fall back to the deterministic
|
||||
// computed object name and clean up the backing objects instead of leaking
|
||||
// them.
|
||||
func TestNewDeployFnDeleteCacheMiss(t *testing.T) {
|
||||
logger := loggerfactory.GetLogger()
|
||||
deploy := &NewDeploy{
|
||||
logger: logger,
|
||||
kubernetesClient: fake.NewSimpleClientset(),
|
||||
fsCache: fscache.MakeFunctionServiceCache(logger),
|
||||
nsResolver: utils.DefaultNSResolver(),
|
||||
hpaops: hpautils.NewHpaOperations(logger, fake.NewSimpleClientset(), "test-instance"),
|
||||
}
|
||||
|
||||
fn := newTestNewDeployFunction()
|
||||
fn.UID = "abcdef01-2345-6789-abcd-ef0123456789"
|
||||
|
||||
// fsCache is intentionally left empty so ListByFunctionUID finds nothing.
|
||||
require.NoError(t, deploy.fnDelete(t.Context(), fn),
|
||||
"fnDelete must tolerate a cache miss and clean up by computed name")
|
||||
}
|
||||
|
||||
// TestNewDeployFnCreateDeletionGuard verifies the authoritative re-read at the
|
||||
// top of fnCreate refuses to create backing objects for a Function that the
|
||||
// router presented but which is gone or being deleted in the cluster. Without
|
||||
// this guard an in-flight create can race the delete teardown and re-create the
|
||||
// Deployment/Service/HPA after teardown removed them, leaking objects whose
|
||||
// owning Function CR is already gone.
|
||||
func TestNewDeployFnCreateDeletionGuard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const liveUID = "abcdef01-2345-6789-abcd-ef0123456789"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// stored is the Function in the authoritative store (fake
|
||||
// fissionClient); nil means the function is absent.
|
||||
stored *fv1.Function
|
||||
}{
|
||||
{
|
||||
name: "function absent from fissionClient",
|
||||
stored: nil,
|
||||
},
|
||||
{
|
||||
name: "function present but being deleted",
|
||||
stored: func() *fv1.Function {
|
||||
fn := newTestNewDeployFunction()
|
||||
fn.UID = liveUID
|
||||
fn.DeletionTimestamp = &metav1.Time{Time: time.Now()}
|
||||
fn.Finalizers = []string{"fission.io/test"}
|
||||
return fn
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "function present but with a different UID",
|
||||
stored: func() *fv1.Function {
|
||||
fn := newTestNewDeployFunction()
|
||||
fn.UID = "00000000-0000-0000-0000-000000000000"
|
||||
return fn
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := loggerfactory.GetLogger()
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
fissionClient := fissionfake.NewSimpleClientset()
|
||||
if tc.stored != nil {
|
||||
fissionClient = fissionfake.NewSimpleClientset(tc.stored)
|
||||
}
|
||||
deploy := &NewDeploy{
|
||||
logger: logger,
|
||||
kubernetesClient: kubeClient,
|
||||
fissionClient: fissionClient,
|
||||
fsCache: fscache.MakeFunctionServiceCache(logger),
|
||||
nsResolver: utils.DefaultNSResolver(),
|
||||
hpaops: hpautils.NewHpaOperations(logger, kubeClient, "test-instance"),
|
||||
}
|
||||
|
||||
// The Function the router presents looks alive (no DeletionTimestamp).
|
||||
fn := newTestNewDeployFunction()
|
||||
fn.UID = liveUID
|
||||
|
||||
_, err := deploy.fnCreate(t.Context(), fn)
|
||||
require.Error(t, err, "fnCreate must refuse a gone/deleting function")
|
||||
assert.True(t, ferror.IsNotFound(err),
|
||||
"guard must surface a ferror NotFound, got: %v", err)
|
||||
|
||||
deploys, listErr := kubeClient.AppsV1().Deployments(metav1.NamespaceAll).List(t.Context(), metav1.ListOptions{})
|
||||
require.NoError(t, listErr)
|
||||
assert.Empty(t, deploys.Items,
|
||||
"no Deployment must be created when the function is gone/deleting")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewDeployFnCreateGuardPass verifies the guard lets a live, matching
|
||||
// Function proceed past the re-read. We do not stand up a full happy-path
|
||||
// (Environment, package, pods); proving the guard passed is enough: fnCreate
|
||||
// fails later on the missing Environment with a non-NotFound error (a k8s
|
||||
// NotFound from the apiserver lookup, which is not a ferror NotFound).
|
||||
func TestNewDeployFnCreateGuardPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
live := newTestNewDeployFunction()
|
||||
live.UID = "abcdef01-2345-6789-abcd-ef0123456789"
|
||||
|
||||
logger := loggerfactory.GetLogger()
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
deploy := &NewDeploy{
|
||||
logger: logger,
|
||||
kubernetesClient: kubeClient,
|
||||
fissionClient: fissionfake.NewSimpleClientset(live),
|
||||
fsCache: fscache.MakeFunctionServiceCache(logger),
|
||||
nsResolver: utils.DefaultNSResolver(),
|
||||
hpaops: hpautils.NewHpaOperations(logger, kubeClient, "test-instance"),
|
||||
}
|
||||
|
||||
fn := newTestNewDeployFunction()
|
||||
fn.UID = live.UID
|
||||
|
||||
_, err := deploy.fnCreate(t.Context(), fn)
|
||||
require.Error(t, err, "expected fnCreate to fail past the guard on the missing Environment")
|
||||
assert.False(t, ferror.IsNotFound(err),
|
||||
"guard must have passed; the error should come from the missing Environment lookup, not the guard. got: %v", err)
|
||||
}
|
||||
|
||||
// TestNewDeployPodSpecDoesNotAutomountTokenInUserContainer asserts the
|
||||
// security-advisory-5 invariant for the new-deploy backend: the
|
||||
// fission-fetcher SA token is only mounted inside the fetcher container,
|
||||
// never in the user-code container.
|
||||
func TestNewDeployPodSpecDoesNotAutomountTokenInUserContainer(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
pod := deployment.Spec.Template
|
||||
|
||||
require.NotNil(t, pod.Spec.AutomountServiceAccountToken,
|
||||
"pod-level AutomountServiceAccountToken must be explicitly set, not nil")
|
||||
assert.False(t, *pod.Spec.AutomountServiceAccountToken,
|
||||
"pod-level AutomountServiceAccountToken must be false to keep the user container clean")
|
||||
assert.Equal(t, fv1.FissionFetcherSA, pod.Spec.ServiceAccountName)
|
||||
|
||||
var projected *apiv1.Volume
|
||||
for i := range pod.Spec.Volumes {
|
||||
if pod.Spec.Volumes[i].Name == util.FetcherSATokenVolumeName {
|
||||
projected = &pod.Spec.Volumes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, projected, "projected SA token volume %q must exist", util.FetcherSATokenVolumeName)
|
||||
require.NotNil(t, projected.Projected, "%q must be a projected volume", util.FetcherSATokenVolumeName)
|
||||
var hasToken, hasCA, hasNS bool
|
||||
for _, src := range projected.Projected.Sources {
|
||||
switch {
|
||||
case src.ServiceAccountToken != nil:
|
||||
hasToken = true
|
||||
assert.Equal(t, "token", src.ServiceAccountToken.Path)
|
||||
case src.ConfigMap != nil:
|
||||
hasCA = src.ConfigMap.Name == "kube-root-ca.crt"
|
||||
case src.DownwardAPI != nil:
|
||||
hasNS = true
|
||||
}
|
||||
}
|
||||
assert.True(t, hasToken, "projected volume must include a ServiceAccountToken source")
|
||||
assert.True(t, hasCA, "projected volume must include the kube-root-ca.crt ConfigMap")
|
||||
assert.True(t, hasNS, "projected volume must include a namespace DownwardAPI source")
|
||||
|
||||
var fetcher, user *apiv1.Container
|
||||
for i := range pod.Spec.Containers {
|
||||
switch pod.Spec.Containers[i].Name {
|
||||
case util.FetcherContainerName:
|
||||
fetcher = &pod.Spec.Containers[i]
|
||||
case envCName:
|
||||
user = &pod.Spec.Containers[i]
|
||||
}
|
||||
}
|
||||
require.NotNil(t, fetcher, "fetcher container must be present")
|
||||
require.NotNil(t, user, "user (env) container must be present")
|
||||
|
||||
hasProjectedTokenMount := false
|
||||
for _, vm := range fetcher.VolumeMounts {
|
||||
if vm.MountPath == util.FetcherSATokenMountPath {
|
||||
hasProjectedTokenMount = true
|
||||
assert.Equal(t, util.FetcherSATokenVolumeName, vm.Name,
|
||||
"fetcher SA token mount must be backed by the projected volume")
|
||||
assert.True(t, vm.ReadOnly, "fetcher SA token mount must be read-only")
|
||||
}
|
||||
}
|
||||
assert.True(t, hasProjectedTokenMount,
|
||||
"fetcher must mount its own SA token via projected volume")
|
||||
|
||||
for _, vm := range user.VolumeMounts {
|
||||
assert.NotEqual(t, util.FetcherSATokenMountPath, vm.MountPath,
|
||||
"user container must not have any volume mount at the SA token path")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewDeployPodSpecRuntimePodSpecCannotReEnableAutomount asserts that an
|
||||
// environment whose Spec.Runtime.PodSpec sets AutomountServiceAccountToken=true
|
||||
// cannot override the security-advisory-5 invariant on the new-deploy
|
||||
// backend. See GHSA-85g2-pmrx-r49q.
|
||||
func TestNewDeployPodSpecRuntimePodSpecCannotReEnableAutomount(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
|
||||
AutomountServiceAccountToken: new(true),
|
||||
}
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
pod := deployment.Spec.Template
|
||||
|
||||
require.NotNil(t, pod.Spec.AutomountServiceAccountToken,
|
||||
"pod-level AutomountServiceAccountToken must be explicitly set, not nil")
|
||||
assert.False(t, *pod.Spec.AutomountServiceAccountToken,
|
||||
"env.Spec.Runtime.PodSpec must not be able to re-enable auto-mount of the SA token")
|
||||
}
|
||||
|
||||
// TestNewDeployPodSpecRuntimePodSpecCannotIntroduceDuplicateSATokenMount
|
||||
// pins the Copilot-flagged invariant from PR #3366: an env author who supplies
|
||||
// env.Spec.Runtime.PodSpec.Containers = [{name: "fetcher", volumeMounts:
|
||||
// [{mountPath: <SA token path>}]}] must not cause the final fetcher container
|
||||
// to end up with two mounts at the SA token path, which kubelet would reject
|
||||
// as a duplicate. The mount-enforcement helper must run AFTER the merge so
|
||||
// any user-supplied mount at that path is stripped before the projected
|
||||
// volume is added back. See GHSA-85g2-pmrx-r49q.
|
||||
func TestNewDeployPodSpecRuntimePodSpecCannotIntroduceDuplicateSATokenMount(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
|
||||
Containers: []apiv1.Container{
|
||||
{
|
||||
Name: util.FetcherContainerName,
|
||||
VolumeMounts: []apiv1.VolumeMount{
|
||||
{
|
||||
Name: "evil-sa-mount",
|
||||
MountPath: util.FetcherSATokenMountPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
pod := deployment.Spec.Template
|
||||
|
||||
var fetcher *apiv1.Container
|
||||
for i := range pod.Spec.Containers {
|
||||
if pod.Spec.Containers[i].Name == util.FetcherContainerName {
|
||||
fetcher = &pod.Spec.Containers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, fetcher, "fetcher container must be present")
|
||||
|
||||
mountsAtSAPath := 0
|
||||
var mountVolumeName string
|
||||
for _, vm := range fetcher.VolumeMounts {
|
||||
if vm.MountPath == util.FetcherSATokenMountPath {
|
||||
mountsAtSAPath++
|
||||
mountVolumeName = vm.Name
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, mountsAtSAPath,
|
||||
"fetcher container must have exactly one mount at the SA token path; user-supplied mount at the same path must be stripped by MountFetcherSATokenOnFetcher")
|
||||
assert.Equal(t, util.FetcherSATokenVolumeName, mountVolumeName,
|
||||
"the surviving mount must be backed by the projected SA token volume, not the user-supplied one")
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecRuntimeClassFieldOnly asserts that
|
||||
// env.Spec.RuntimeClassName reaches the pod template's RuntimeClassName when
|
||||
// NO env.Spec.Runtime.PodSpec is set at all — the common case, and the one
|
||||
// that would silently pass if ApplyEnvRuntimeClass were wired inside the
|
||||
// "if env.Spec.Runtime.PodSpec != nil" block instead of unconditionally.
|
||||
func TestGetDeploymentSpecRuntimeClassFieldOnly(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.RuntimeClassName = new("gvisor")
|
||||
require.Nil(t, env.Spec.Runtime.PodSpec, "this test must exercise the no-PodSpec path")
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, deployment.Spec.Template.Spec.RuntimeClassName)
|
||||
assert.Equal(t, "gvisor", *deployment.Spec.Template.Spec.RuntimeClassName)
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecRuntimeClassNilWhenUnset asserts that an env with no
|
||||
// RuntimeClassName leaves the pod template's RuntimeClassName nil.
|
||||
func TestGetDeploymentSpecRuntimeClassNilWhenUnset(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Nil(t, deployment.Spec.Template.Spec.RuntimeClassName)
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecRuntimeClassPodSpecOverridesField pins the precedence
|
||||
// rule: when both env.Spec.RuntimeClassName and env.Spec.Runtime.PodSpec.
|
||||
// RuntimeClassName are set to different values, the PodSpec's value wins.
|
||||
func TestGetDeploymentSpecRuntimeClassPodSpecOverridesField(t *testing.T) {
|
||||
deploy := newTestNewDeploy(t)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.RuntimeClassName = new("gvisor")
|
||||
env.Spec.Runtime.PodSpec = &apiv1.PodSpec{
|
||||
RuntimeClassName: new("kata"),
|
||||
}
|
||||
fn := newTestNewDeployFunction()
|
||||
ctx := t.Context()
|
||||
|
||||
replicas := int32(1)
|
||||
deployment, err := deploy.getDeploymentSpec(
|
||||
ctx, fn, env, &replicas,
|
||||
"newdeploy-test-fn",
|
||||
"default",
|
||||
map[string]string{"app": "newdeploy-test"},
|
||||
map[string]string{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, deployment.Spec.Template.Spec.RuntimeClassName)
|
||||
assert.Equal(t, "kata", *deployment.Spec.Template.Spec.RuntimeClassName,
|
||||
"env.Spec.Runtime.PodSpec.RuntimeClassName must win over env.Spec.RuntimeClassName")
|
||||
}
|
||||
|
||||
func ociTestPackage(deployment fv1.Archive) *fv1.Package {
|
||||
return &fv1.Package{
|
||||
Name: "pkg-1", Namespace: "default",
|
||||
Spec: fv1.PackageSpec{Deployment: deployment},
|
||||
}
|
||||
}
|
||||
|
||||
// newTestOCINewDeploy wires a NewDeploy whose fissionClient holds pkg and
|
||||
// whose image-volume gate is set as given.
|
||||
func newTestOCINewDeploy(t *testing.T, pkg *fv1.Package, imageVolumeOK bool) *NewDeploy {
|
||||
t.Helper()
|
||||
deploy := newTestNewDeploy(t)
|
||||
deploy.fissionClient = fissionfake.NewSimpleClientset(pkg)
|
||||
deploy.imageVolumeOK = imageVolumeOK
|
||||
return deploy
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecOCIImageVolume asserts the newdeploy Path B pod shape:
|
||||
// the fetcher container STAYS (it materializes secrets and drives the load),
|
||||
// and the package image mounts read-only at the fetcher's store path on both
|
||||
// containers, so the fetcher's exists-early-exit skips the pull.
|
||||
func TestGetDeploymentSpecOCIImageVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
oci := &fv1.OCIArchive{
|
||||
Image: "registry.example.com/code/hello:v1",
|
||||
ImagePullSecrets: []apiv1.LocalObjectReference{{Name: "regcred"}},
|
||||
}
|
||||
deploy := newTestOCINewDeploy(t, ociTestPackage(fv1.Archive{Type: fv1.ArchiveTypeOCI, OCI: oci}), true)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.Version = 2
|
||||
fn := newTestNewDeployFunction()
|
||||
|
||||
dep, err := deploy.getDeploymentSpec(t.Context(), fn, env, nil,
|
||||
"nd-oci", "default", map[string]string{"a": "b"}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
pod := dep.Spec.Template
|
||||
|
||||
// Both containers present: env runtime + fetcher.
|
||||
var fetcher, user *apiv1.Container
|
||||
for i := range pod.Spec.Containers {
|
||||
switch pod.Spec.Containers[i].Name {
|
||||
case util.FetcherContainerName:
|
||||
fetcher = &pod.Spec.Containers[i]
|
||||
case envCName:
|
||||
user = &pod.Spec.Containers[i]
|
||||
}
|
||||
}
|
||||
require.NotNil(t, fetcher, "newdeploy Path B keeps the fetcher container")
|
||||
require.NotNil(t, user)
|
||||
|
||||
// Image volume present.
|
||||
var imgVol *apiv1.Volume
|
||||
for i := range pod.Spec.Volumes {
|
||||
if pod.Spec.Volumes[i].Image != nil {
|
||||
imgVol = &pod.Spec.Volumes[i]
|
||||
}
|
||||
}
|
||||
require.NotNil(t, imgVol, "an image volume must be present")
|
||||
assert.Equal(t, oci.Image, imgVol.Image.Reference)
|
||||
assert.Equal(t, apiv1.PullIfNotPresent, imgVol.Image.PullPolicy)
|
||||
|
||||
// Mounted read-only at the fetcher's store path on BOTH containers.
|
||||
wantPath := deploy.fetcherConfig.SharedMountPath() + "/" + fetcherConfig.TargetFilenameDeployArchive
|
||||
for _, c := range []*apiv1.Container{fetcher, user} {
|
||||
var mount *apiv1.VolumeMount
|
||||
for i := range c.VolumeMounts {
|
||||
if c.VolumeMounts[i].Name == imgVol.Name {
|
||||
mount = &c.VolumeMounts[i]
|
||||
}
|
||||
}
|
||||
require.NotNilf(t, mount, "container %s must mount the image volume", c.Name)
|
||||
assert.Equal(t, wantPath, mount.MountPath)
|
||||
assert.True(t, mount.ReadOnly)
|
||||
}
|
||||
|
||||
// Pull secrets reach the pod; fetcher SA token mount intact; automount off.
|
||||
assert.Contains(t, pod.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: "regcred"})
|
||||
hasSAToken := false
|
||||
for _, vm := range fetcher.VolumeMounts {
|
||||
if vm.MountPath == util.FetcherSATokenMountPath {
|
||||
hasSAToken = true
|
||||
}
|
||||
}
|
||||
assert.True(t, hasSAToken, "fetcher keeps its projected SA token mount")
|
||||
require.NotNil(t, pod.Spec.AutomountServiceAccountToken)
|
||||
assert.False(t, *pod.Spec.AutomountServiceAccountToken)
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecOCIGateOff asserts Path A behavior when the gate is
|
||||
// off: no image volume; the fetcher pulls instead.
|
||||
func TestGetDeploymentSpecOCIGateOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
oci := &fv1.OCIArchive{Image: "registry.example.com/code/hello:v1"}
|
||||
deploy := newTestOCINewDeploy(t, ociTestPackage(fv1.Archive{Type: fv1.ArchiveTypeOCI, OCI: oci}), false)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.Version = 2
|
||||
fn := newTestNewDeployFunction()
|
||||
|
||||
dep, err := deploy.getDeploymentSpec(t.Context(), fn, env, nil,
|
||||
"nd-oci-off", "default", map[string]string{"a": "b"}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
for _, v := range dep.Spec.Template.Spec.Volumes {
|
||||
assert.Nil(t, v.Image, "gate off must not produce an image volume")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetDeploymentSpecNonOCIUnchanged is the parity guard for tarball
|
||||
// packages with the gate ON: nothing about their pods changes.
|
||||
func TestGetDeploymentSpecNonOCIUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
deploy := newTestOCINewDeploy(t, ociTestPackage(fv1.Archive{Type: fv1.ArchiveTypeUrl, URL: "http://example.com/a.zip"}), true)
|
||||
env := newTestNewDeployEnv()
|
||||
env.Spec.Version = 2
|
||||
fn := newTestNewDeployFunction()
|
||||
|
||||
dep, err := deploy.getDeploymentSpec(t.Context(), fn, env, nil,
|
||||
"nd-tarball", "default", map[string]string{"a": "b"}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
for _, v := range dep.Spec.Template.Spec.Volumes {
|
||||
assert.Nil(t, v.Image, "tarball packages must not grow an image volume")
|
||||
}
|
||||
assert.Empty(t, dep.Spec.Template.Spec.ImagePullSecrets)
|
||||
}
|
||||
Reference in new issue
Block a user