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 }