github.com/grahambrereton-form3/tilt@v0.10.18/internal/k8s/client_test.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	v1 "k8s.io/api/core/v1"
     9  	"k8s.io/apimachinery/pkg/api/meta"
    10  	"k8s.io/apimachinery/pkg/runtime"
    11  	"k8s.io/apimachinery/pkg/watch"
    12  	"k8s.io/client-go/kubernetes/fake"
    13  	"k8s.io/client-go/kubernetes/scheme"
    14  	ktesting "k8s.io/client-go/testing"
    15  
    16  	"github.com/windmilleng/tilt/internal/testutils"
    17  
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"github.com/windmilleng/tilt/internal/k8s/testyaml"
    22  )
    23  
    24  func TestEmptyNamespace(t *testing.T) {
    25  	var emptyNamespace Namespace
    26  	assert.True(t, emptyNamespace.Empty())
    27  	assert.True(t, emptyNamespace == "")
    28  	assert.Equal(t, "default", emptyNamespace.String())
    29  }
    30  
    31  func TestNotEmptyNamespace(t *testing.T) {
    32  	var ns Namespace = "x"
    33  	assert.False(t, ns.Empty())
    34  	assert.False(t, ns == "")
    35  	assert.Equal(t, "x", ns.String())
    36  }
    37  
    38  func TestUpsert(t *testing.T) {
    39  	f := newClientTestFixture(t)
    40  	postgres, err := ParseYAMLFromString(testyaml.PostgresYAML)
    41  	assert.Nil(t, err)
    42  	_, err = f.client.Upsert(f.ctx, postgres)
    43  	assert.Nil(t, err)
    44  	assert.Equal(t, 1, len(f.runner.calls))
    45  	assert.Equal(t, []string{"apply", "-o", "yaml", "-f", "-"}, f.runner.calls[0].argv)
    46  }
    47  
    48  func TestUpsertMutableAndImmutable(t *testing.T) {
    49  	f := newClientTestFixture(t)
    50  	eDeploy := MustParseYAMLFromString(t, testyaml.SanchoYAML)[0]
    51  	eJob := MustParseYAMLFromString(t, testyaml.JobYAML)[0]
    52  	eNamespace := MustParseYAMLFromString(t, testyaml.MyNamespaceYAML)[0]
    53  
    54  	_, err := f.client.Upsert(f.ctx, []K8sEntity{eDeploy, eJob, eNamespace})
    55  	if !assert.Nil(t, err) {
    56  		t.FailNow()
    57  	}
    58  
    59  	// two different calls: one for mutable entities (namespace, deployment),
    60  	// one for immutable (job)
    61  	require.Len(t, f.runner.calls, 2)
    62  
    63  	call0 := f.runner.calls[0]
    64  	require.Equal(t, []string{"apply", "-o", "yaml", "-f", "-"}, call0.argv, "expected args for call 0")
    65  
    66  	// compare entities instead of strings because str > entity > string gets weird
    67  	call0Entities := mustParseYAML(t, call0.stdin)
    68  	require.Len(t, call0Entities, 2, "expect two mutable entities applied")
    69  
    70  	// `apply` should preserve input order of entities (we sort them further upstream)
    71  	require.Equal(t, eDeploy, call0Entities[0], "expect call 0 to have applied deployment first (preserve input order)")
    72  	require.Equal(t, eNamespace, call0Entities[1], "expect call 0 to have applied namespace second (preserve input order)")
    73  
    74  	call1 := f.runner.calls[1]
    75  	require.Equal(t, []string{"replace", "-o", "yaml", "--force", "-f", "-"}, call1.argv, "expected args for call 1")
    76  	call1Entities := mustParseYAML(t, call1.stdin)
    77  	require.Len(t, call1Entities, 1, "expect only one immutable entity applied")
    78  	require.Equal(t, eJob, call1Entities[0], "expect call 1 to have applied job")
    79  }
    80  
    81  func TestUpsertStatefulsetForbidden(t *testing.T) {
    82  	f := newClientTestFixture(t)
    83  	postgres, err := ParseYAMLFromString(testyaml.PostgresYAML)
    84  	assert.Nil(t, err)
    85  
    86  	f.setStderr(`The StatefulSet "postgres" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', and 'updateStrategy' are forbidden.`)
    87  	_, err = f.client.Upsert(f.ctx, postgres)
    88  	if assert.Nil(t, err) && assert.Equal(t, 3, len(f.runner.calls)) {
    89  		assert.Equal(t, []string{"apply", "-o", "yaml", "-f", "-"}, f.runner.calls[0].argv)
    90  		assert.Equal(t, []string{"delete", "-f", "-"}, f.runner.calls[1].argv)
    91  		assert.Equal(t, []string{"apply", "-o", "yaml", "-f", "-"}, f.runner.calls[2].argv)
    92  	}
    93  }
    94  
    95  func TestUpsertToTerminatingNamespaceForbidden(t *testing.T) {
    96  	f := newClientTestFixture(t)
    97  	postgres, err := ParseYAMLFromString(testyaml.SanchoYAML)
    98  	assert.Nil(t, err)
    99  
   100  	// Bad error parsing used to result in us treating this error as an immutable
   101  	// field error. Make sure we treat it as what it is and bail out of `kubectl apply`
   102  	// rather than trying to --force
   103  	errStr := `Error from server (Forbidden): error when creating "STDIN": deployments.apps "sancho" is forbidden: unable to create new content in namespace sancho-ns because it is being terminated`
   104  	f.setStderr(errStr)
   105  
   106  	_, err = f.client.Upsert(f.ctx, postgres)
   107  	if assert.NotNil(t, err) {
   108  		assert.Contains(t, err.Error(), errStr)
   109  	}
   110  	if assert.Equal(t, 1, len(f.runner.calls)) {
   111  		assert.Equal(t, []string{"apply", "-o", "yaml", "-f", "-"}, f.runner.calls[0].argv)
   112  	}
   113  
   114  }
   115  
   116  func TestGetGroup(t *testing.T) {
   117  	for _, test := range []struct {
   118  		name          string
   119  		apiVersion    string
   120  		expectedGroup string
   121  	}{
   122  		{"normal", "apps/v1", "apps"},
   123  		// core types have an empty group
   124  		{"core", "/v1", ""},
   125  		// on some versions of k8s, deployment is buggy and doesn't have a version in its apiVersion
   126  		{"no version", "extensions", "extensions"},
   127  		{"alpha version", "apps/v1alpha1", "apps"},
   128  		{"beta version", "apps/v1beta1", "apps"},
   129  		// I've never seen this in the wild, but the docs say it's legal
   130  		{"alpha version, no second number", "apps/v1alpha", "apps"},
   131  	} {
   132  		t.Run(test.name, func(t *testing.T) {
   133  			obj := v1.ObjectReference{APIVersion: test.apiVersion}
   134  			assert.Equal(t, test.expectedGroup, getGroup(obj))
   135  		})
   136  	}
   137  }
   138  
   139  type call struct {
   140  	argv  []string
   141  	stdin string
   142  }
   143  
   144  type fakeKubectlRunner struct {
   145  	stdout string
   146  	stderr string
   147  	err    error
   148  
   149  	calls []call
   150  }
   151  
   152  func (f *fakeKubectlRunner) execWithStdin(ctx context.Context, args []string, stdin string) (stdout string, stderr string, err error) {
   153  	f.calls = append(f.calls, call{argv: args, stdin: stdin})
   154  
   155  	defer func() {
   156  		f.stdout = ""
   157  		f.stderr = ""
   158  		f.err = nil
   159  	}()
   160  	return f.stdout, f.stderr, f.err
   161  }
   162  
   163  func (f *fakeKubectlRunner) exec(ctx context.Context, args []string) (stdout string, stderr string, err error) {
   164  	f.calls = append(f.calls, call{argv: args})
   165  	defer func() {
   166  		f.stdout = ""
   167  		f.stderr = ""
   168  		f.err = nil
   169  	}()
   170  	return f.stdout, f.stderr, f.err
   171  }
   172  
   173  var _ kubectlRunner = &fakeKubectlRunner{}
   174  
   175  type clientTestFixture struct {
   176  	t           *testing.T
   177  	ctx         context.Context
   178  	client      K8sClient
   179  	runner      *fakeKubectlRunner
   180  	tracker     ktesting.ObjectTracker
   181  	watchNotify chan watch.Interface
   182  }
   183  
   184  func newClientTestFixture(t *testing.T) *clientTestFixture {
   185  	ret := &clientTestFixture{}
   186  	ret.t = t
   187  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   188  	ret.ctx = ctx
   189  	ret.runner = &fakeKubectlRunner{}
   190  
   191  	tracker := ktesting.NewObjectTracker(scheme.Scheme, scheme.Codecs.UniversalDecoder())
   192  	watchNotify := make(chan watch.Interface, 100)
   193  	ret.watchNotify = watchNotify
   194  
   195  	cs := &fake.Clientset{}
   196  	cs.AddReactor("*", "*", ktesting.ObjectReaction(tracker))
   197  	cs.AddWatchReactor("*", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
   198  		gvr := action.GetResource()
   199  		ns := action.GetNamespace()
   200  		watch, err := tracker.Watch(gvr, ns)
   201  		if err != nil {
   202  			return false, nil, err
   203  		}
   204  
   205  		watchNotify <- watch
   206  		return true, watch, nil
   207  	})
   208  
   209  	ret.tracker = tracker
   210  
   211  	core := cs.CoreV1()
   212  	runtimeAsync := newRuntimeAsync(core)
   213  	registryAsync := newRegistryAsync(EnvUnknown, core, runtimeAsync)
   214  	ret.client = K8sClient{
   215  		env:               EnvUnknown,
   216  		kubectlRunner:     ret.runner,
   217  		core:              core,
   218  		portForwardClient: &FakePortForwardClient{},
   219  		runtimeAsync:      runtimeAsync,
   220  		registryAsync:     registryAsync,
   221  	}
   222  	return ret
   223  }
   224  
   225  func (c clientTestFixture) addObject(obj runtime.Object) {
   226  	err := c.tracker.Add(obj)
   227  	if err != nil {
   228  		c.t.Fatal(err)
   229  	}
   230  }
   231  
   232  func (c clientTestFixture) updatePod(pod *v1.Pod) {
   233  	gvks, _, err := scheme.Scheme.ObjectKinds(pod)
   234  	if err != nil {
   235  		c.t.Fatalf("updatePod: %v", err)
   236  	} else if len(gvks) == 0 {
   237  		c.t.Fatal("Could not parse pod into k8s schema")
   238  	}
   239  	for _, gvk := range gvks {
   240  		gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   241  		err = c.tracker.Update(gvr, pod, NamespaceFromPod(pod).String())
   242  		if err != nil {
   243  			c.t.Fatal(err)
   244  		}
   245  	}
   246  }
   247  
   248  func (c clientTestFixture) setOutput(s string) {
   249  	c.runner.stdout = s
   250  }
   251  
   252  func (c clientTestFixture) setStderr(stderr string) {
   253  	c.runner.stderr = stderr
   254  	c.runner.err = fmt.Errorf("exit status 1")
   255  }
   256  
   257  func (c clientTestFixture) setError(err error) {
   258  	c.runner.err = err
   259  }