Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

k8s: send Pods and Jobs down the same update codepath as other resources #5552

Merged
merged 1 commit into from
Mar 3, 2022
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
43 changes: 20 additions & 23 deletions internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ type KubeContextOverride string
// NOTE(nick): This isn't right. DefaultNamespace is a function of your kubectl context.
const DefaultNamespace = Namespace("default")

var ForbiddenFieldsRe = regexp.MustCompile(`updates to .* are forbidden`)
// Kubernetes uses "Forbidden" errors for a variety of field immutability errors.
//
// https://github.com/kubernetes/kubernetes/blob/5d6a793221370d890af6ea766d056af4e33f1118/pkg/apis/core/validation/validation.go#L4383
// https://github.com/kubernetes/kubernetes/blob/5d6a793221370d890af6ea766d056af4e33f1118/pkg/apis/core/validation/validation.go#L4196
var ForbiddenFieldsPrefix = "Forbidden:"

func (pID PodID) Empty() bool { return pID.String() == "" }
func (pID PodID) String() string { return string(pID) }
Expand Down Expand Up @@ -320,10 +324,7 @@ func (k *K8sClient) ToRawKubeConfigLoader() clientcmd.ClientConfig {

func (k *K8sClient) Upsert(ctx context.Context, entities []K8sEntity, timeout time.Duration) ([]K8sEntity, error) {
result := make([]K8sEntity, 0, len(entities))

mutable, immutable := MutableAndImmutableEntities(entities)

for _, e := range mutable {
for _, e := range entities {
innerCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

Expand All @@ -337,20 +338,6 @@ func (k *K8sClient) Upsert(ctx context.Context, entities []K8sEntity, timeout ti
result = append(result, newEntity...)
}

for _, e := range immutable {
innerCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

newEntities, err := k.deleteAndCreateEntity(innerCtx, e)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, timeoutError(timeout)
}
return nil, err
}
result = append(result, newEntities...)
}

return result, nil
}

Expand Down Expand Up @@ -516,10 +503,11 @@ func (k *K8sClient) escalatingUpdate(ctx context.Context, entity K8sEntity) ([]K
}

if err != nil {
isImmutable := maybeImmutableFieldStderr(err.Error())
if isImmutable {
maybeImmutable := maybeImmutableFieldStderr(err.Error())
if maybeImmutable {
fallback = true
logger.Get(ctx).Infof("Updating %q failed: immutable field error", entity.Name())
logger.Get(ctx).Infof("Updating %q failed: %s", entity.Name(),
truncateErrorToOneLine(err.Error()))
logger.Get(ctx).Infof("Attempting to delete and re-create")
result, err = k.deleteAndCreateEntity(ctx, entity)
}
Expand All @@ -534,14 +522,23 @@ func (k *K8sClient) escalatingUpdate(ctx context.Context, entity K8sEntity) ([]K
return result, nil
}

func truncateErrorToOneLine(stderr string) string {
index := strings.Index(stderr, "\n")
if index != -1 {
return stderr[:index]
}
return stderr
}

// We're using kubectl, so we only get stderr, not structured errors.
//
// Take a wild guess if the update is failing due to immutable field errors.
//
// This should bias towards false positives (i.e., we think something is an
// immutable field error when it's not).
func maybeImmutableFieldStderr(stderr string) bool {
return strings.Contains(stderr, validation.FieldImmutableErrorMsg) || ForbiddenFieldsRe.Match([]byte(stderr))
return strings.Contains(stderr, validation.FieldImmutableErrorMsg) ||
strings.Contains(stderr, ForbiddenFieldsPrefix)
}

var MetadataAnnotationsTooLongRe = regexp.MustCompile(`metadata.annotations: Too long: must have at most \d+ bytes.*`)
Expand Down
27 changes: 0 additions & 27 deletions internal/k8s/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/pkg/kube"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -85,32 +84,6 @@ func TestDeleteMissingKind(t *testing.T) {
kinds)
}

func TestUpsertMutableAndImmutable(t *testing.T) {
f := newClientTestFixture(t)
eDeploy := MustParseYAMLFromString(t, testyaml.SanchoYAML)[0]
eJob := MustParseYAMLFromString(t, testyaml.JobYAML)[0]
eNamespace := MustParseYAMLFromString(t, testyaml.MyNamespaceYAML)[0]

_, err := f.k8sUpsert(f.ctx, []K8sEntity{eDeploy, eJob, eNamespace})
if !assert.Nil(t, err) {
t.FailNow()
}

require.Len(t, f.resourceClient.updates, 2)
require.Len(t, f.resourceClient.creates, 1)

// compare entities instead of strings because str > entity > string gets weird
call0Entity := NewK8sEntity(f.resourceClient.updates[0].Object)
call1Entity := NewK8sEntity(f.resourceClient.updates[1].Object)

// `apply` should preserve input order of entities (we sort them further upstream)
require.Equal(t, eDeploy, call0Entity, "expect call 0 to have applied deployment first (preserve input order)")
require.Equal(t, eNamespace, call1Entity, "expect call 0 to have applied namespace second (preserve input order)")

call2Entity := NewK8sEntity(f.resourceClient.creates[0].Object)
require.Equal(t, eJob, call2Entity, "expect create job")
}

func TestUpsertAnnotationTooLong(t *testing.T) {
f := newClientTestFixture(t)
postgres := MustParseYAMLFromString(t, testyaml.PostgresYAML)
Expand Down
35 changes: 0 additions & 35 deletions internal/k8s/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,41 +159,6 @@ func CopyEntities(entities []K8sEntity) []K8sEntity {
return res
}

// MutableAndImmutableEntities returns two lists of k8s entities: mutable ones (that can simply be
// `kubectl apply`'d), and immutable ones (such as jobs and pods, which will need to be `--force`'d).
// (We assume input entities are already sorted in a safe order to apply -- see kustomize/ordering.go.)
func MutableAndImmutableEntities(entities entityList) (mutable, immutable []K8sEntity) {
for _, e := range entities {
if e.ImmutableOnceCreated() {
immutable = append(immutable, e)
continue
}
mutable = append(mutable, e)
}

return mutable, immutable
}

func ImmutableEntities(entities []K8sEntity) []K8sEntity {
result := make([]K8sEntity, 0)
for _, e := range entities {
if e.ImmutableOnceCreated() {
result = append(result, e)
}
}
return result
}

func MutableEntities(entities []K8sEntity) []K8sEntity {
result := make([]K8sEntity, 0)
for _, e := range entities {
if !e.ImmutableOnceCreated() {
result = append(result, e)
}
}
return result
}

type LoadBalancerSpec struct {
Name string
Namespace Namespace
Expand Down
76 changes: 0 additions & 76 deletions internal/k8s/entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/tilt-dev/tilt/internal/k8s/testyaml"
"github.com/tilt-dev/tilt/internal/kustomize"
)

func TestTypedPodGVK(t *testing.T) {
Expand Down Expand Up @@ -50,43 +49,6 @@ func TestNamespace(t *testing.T) {
assert.Equal(t, "kube-system", string(entities[0].Namespace()))
}

func TestImmutableFilter(t *testing.T) {
yaml := fmt.Sprintf("%s\n---\n%s\n---\n%s", testyaml.JobYAML, testyaml.SanchoYAML, testyaml.PodYAML)
entities, err := ParseYAMLFromString(yaml)
if err != nil {
t.Fatal(err)
}

immEntities := ImmutableEntities(entities)
if len(immEntities) != 2 {
t.Fatalf("Expected 2 entities, actual: %d", len(immEntities))
}

if immEntities[0].GVK().Kind != "Job" {
t.Errorf("Expected Job entity, actual: %+v", immEntities)
}
if immEntities[1].GVK().Kind != "Pod" {
t.Errorf("Expected Pod entity, actual: %+v", immEntities)
}
}

func TestMutableFilter(t *testing.T) {
yaml := fmt.Sprintf("%s\n---\n%s", testyaml.JobYAML, testyaml.SanchoYAML)
entities, err := ParseYAMLFromString(yaml)
if err != nil {
t.Fatal(err)
}

results := MutableEntities(entities)
if len(results) != 1 {
t.Fatalf("Expected 1 entity, actual: %d", len(results))
}

if results[0].GVK().Kind != "Deployment" {
t.Errorf("Expected Deployment entity, actual: %+v", results)
}
}

func TestLoadBalancerSpecs(t *testing.T) {
entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML)
if err != nil {
Expand Down Expand Up @@ -230,44 +192,6 @@ func TestSortEntities(t *testing.T) {
}
}

func TestMutableAndImmutableEntities(t *testing.T) {
for _, test := range []struct {
name string
inputKindOrder []string
expectedMutableKindOrder []string
expectedImmutableKindOrder []string
}{
{"only mutable",
[]string{"Deployment", "Namespace", "Service"},
[]string{"Deployment", "Namespace", "Service"},
[]string{},
},
{"only immutable",
[]string{"Job", "Pod"},
[]string{},
[]string{"Job", "Pod"},
},
{"mutable and immutable interspersed",
[]string{"Deployment", "Job", "Namespace", "Pod", "Service"},
[]string{"Deployment", "Namespace", "Service"},
[]string{"Job", "Pod"},
},
{"no explicitly sorted kinds are immutable",
// If any kinds in the explicit sort list are also immutable, things will get weird
kustomize.OrderFirst,
kustomize.OrderFirst,
[]string{},
},
} {
t.Run(test.name, func(t *testing.T) {
input := entitiesWithKinds(test.inputKindOrder)
mutable, immutable := MutableAndImmutableEntities(input)
assertKindOrder(t, test.expectedMutableKindOrder, mutable, "mutable entities")
assertKindOrder(t, test.expectedImmutableKindOrder, immutable, "immutable entities")
})
}
}

func TestClean(t *testing.T) {
yaml := `apiVersion: v1
kind: Namespace
Expand Down