Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions internal/providers/compute/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const (

// K8sProvider implements compute.Provider using the local k8s cluster.
type K8sProvider struct {
clientset *kubernetes.Clientset
namespace string // shared namespace (legacy fallback); per-deploy namespaces are preferred
clientset kubernetes.Interface // accepts both *Clientset and *fake.Clientset (tests)
namespace string // shared namespace (legacy fallback); per-deploy namespaces are preferred
}

// New creates a K8sProvider targeting the given namespace.
Expand Down Expand Up @@ -816,6 +816,22 @@ func (p *K8sProvider) createKanikoJob(ctx context.Context, ns, jobName, ctxSecre
"--single-snapshot",
"--cleanup",
},
// Explicit resources override the per-namespace LimitRange
// default (hobby tier defaults to 50m/256Mi which throttles
// kaniko + npm install to 5+ minutes). 250m/512Mi keeps a
// medium npm install under a minute without inflating the
// app's own quota: builds run as a Job, not part of the
// app's permanent footprint.
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("250m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("512Mi"),
},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "build-context", MountPath: "/workspace"},
{Name: "registry-auth", MountPath: "/kaniko/.docker"},
Expand Down Expand Up @@ -885,7 +901,10 @@ func (p *K8sProvider) applyDeploymentInNS(
memReq, memLimit, cpuReq string,
) error {
replicas := int32(1)
pullPolicy := corev1.PullIfNotPresent
// PullAlways because images are pushed under a single :latest tag — without
// Always, k8s caches the old image on nodes and redeploys silently serve
// stale content. Future: sha-pin the tag and switch back to IfNotPresent.
pullPolicy := corev1.PullAlways
saFalse := false
appID := appIDFromDeployName(name)

Expand Down
73 changes: 73 additions & 0 deletions internal/providers/compute/k8s/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package k8s

import (
"context"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

// TestKanikoJobHasExplicitResources guards against regressing the build pod's
// resource overrides. Without explicit Requests/Limits, the per-namespace
// LimitRange (hobby default: 50m/256Mi) throttles kaniko + npm install to
// 5+ minutes. See fix/deploy-compute-correctness.
func TestKanikoJobHasExplicitResources(t *testing.T) {
cs := fake.NewSimpleClientset()
p := &K8sProvider{clientset: cs}

const ns, jobName = "instant-deploy-test", "build-test"
if err := p.createKanikoJob(context.Background(), ns, jobName, "ctx-sec", "auth-sec", "ghcr.io/x/y:latest"); err != nil {
t.Fatalf("createKanikoJob: %v", err)
}

job, err := cs.BatchV1().Jobs(ns).Get(context.Background(), jobName, metav1.GetOptions{})
if err != nil {
t.Fatalf("get job: %v", err)
}
containers := job.Spec.Template.Spec.Containers
if len(containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(containers))
}
c := containers[0]

for _, k := range []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} {
if _, ok := c.Resources.Requests[k]; !ok {
t.Errorf("kaniko container is missing Requests[%s] — LimitRange default will throttle the build", k)
}
if _, ok := c.Resources.Limits[k]; !ok {
t.Errorf("kaniko container is missing Limits[%s] — LimitRange default will throttle the build", k)
}
}

// Concrete sanity check on the floor value — if someone bumps it down again,
// the test fires.
if got := c.Resources.Requests[corev1.ResourceCPU]; got.MilliValue() < 250 {
t.Errorf("kaniko CPU request %s is below the 250m floor for non-trivial npm installs", got.String())
}
}

// TestAppDeploymentUsesPullAlways guards against regressing to IfNotPresent on
// the :latest tag, which caused redeploys to silently serve cached old images.
func TestAppDeploymentUsesPullAlways(t *testing.T) {
cs := fake.NewSimpleClientset()
p := &K8sProvider{clientset: cs}

const ns, name = "instant-deploy-test", "app-test"
if err := p.applyDeploymentInNS(context.Background(),
ns, name, "ghcr.io/x/y:latest",
map[string]string{"FOO": "bar"},
8080, "64Mi", "256Mi", "50m",
); err != nil {
t.Fatalf("applyDeploymentInNS: %v", err)
}

d, err := cs.AppsV1().Deployments(ns).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("get deployment: %v", err)
}
if got := d.Spec.Template.Spec.Containers[0].ImagePullPolicy; got != corev1.PullAlways {
t.Errorf("imagePullPolicy = %s; want PullAlways (otherwise :latest gets cached and redeploys serve stale images)", got)
}
}
4 changes: 3 additions & 1 deletion internal/providers/compute/k8s/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ func (p *K8sStackProvider) createStackDeployment(
memReq, memLimit, cpuReq string,
) error {
replicas := int32(1)
pullPolicy := corev1.PullIfNotPresent
// PullAlways for the same reason as client.go: images are pushed under
// :latest, so without Always, redeploys silently serve cached old images.
pullPolicy := corev1.PullAlways
saFalse := false

desired := &appsv1.Deployment{
Expand Down