etcd_storage_path_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package etcd
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "reflect"
  19. "strings"
  20. "testing"
  21. "go.etcd.io/etcd/clientv3"
  22. v1 "k8s.io/api/core/v1"
  23. apiequality "k8s.io/apimachinery/pkg/api/equality"
  24. "k8s.io/apimachinery/pkg/api/meta"
  25. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  26. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  27. "k8s.io/apimachinery/pkg/runtime/schema"
  28. "k8s.io/apimachinery/pkg/util/diff"
  29. "k8s.io/apimachinery/pkg/util/sets"
  30. "k8s.io/client-go/dynamic"
  31. "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
  32. )
  33. // Only add kinds to this list when this a virtual resource with get and create verbs that doesn't actually
  34. // store into it's kind. We've used this downstream for mappings before.
  35. var kindWhiteList = sets.NewString()
  36. // namespace used for all tests, do not change this
  37. const testNamespace = "etcdstoragepathtestnamespace"
  38. // TestEtcdStoragePath tests to make sure that all objects are stored in an expected location in etcd.
  39. // It will start failing when a new type is added to ensure that all future types are added to this test.
  40. // It will also fail when a type gets moved to a different location. Be very careful in this situation because
  41. // it essentially means that you will be break old clusters unless you create some migration path for the old data.
  42. func TestEtcdStoragePath(t *testing.T) {
  43. master := StartRealMasterOrDie(t, func(opts *options.ServerRunOptions) {
  44. })
  45. defer master.Cleanup()
  46. defer dumpEtcdKVOnFailure(t, master.KV)
  47. client := &allClient{dynamicClient: master.Dynamic}
  48. if _, err := master.Client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
  49. t.Fatal(err)
  50. }
  51. etcdStorageData := GetEtcdStorageData()
  52. kindSeen := sets.NewString()
  53. pathSeen := map[string][]schema.GroupVersionResource{}
  54. etcdSeen := map[schema.GroupVersionResource]empty{}
  55. cohabitatingResources := map[string]map[schema.GroupVersionKind]empty{}
  56. for _, resourceToPersist := range master.Resources {
  57. t.Run(resourceToPersist.Mapping.Resource.String(), func(t *testing.T) {
  58. mapping := resourceToPersist.Mapping
  59. gvk := resourceToPersist.Mapping.GroupVersionKind
  60. gvResource := resourceToPersist.Mapping.Resource
  61. kind := gvk.Kind
  62. if kindWhiteList.Has(kind) {
  63. kindSeen.Insert(kind)
  64. t.Skip("whitelisted")
  65. }
  66. etcdSeen[gvResource] = empty{}
  67. testData, hasTest := etcdStorageData[gvResource]
  68. if !hasTest {
  69. t.Fatalf("no test data for %s. Please add a test for your new type to GetEtcdStorageData().", gvResource)
  70. }
  71. if len(testData.ExpectedEtcdPath) == 0 {
  72. t.Fatalf("empty test data for %s", gvResource)
  73. }
  74. shouldCreate := len(testData.Stub) != 0 // try to create only if we have a stub
  75. var (
  76. input *metaObject
  77. err error
  78. )
  79. if shouldCreate {
  80. if input, err = jsonToMetaObject([]byte(testData.Stub)); err != nil || input.isEmpty() {
  81. t.Fatalf("invalid test data for %s: %v", gvResource, err)
  82. }
  83. // unset type meta fields - we only set these in the CRD test data and it makes
  84. // any CRD test with an expectedGVK override fail the DeepDerivative test
  85. input.Kind = ""
  86. input.APIVersion = ""
  87. }
  88. all := &[]cleanupData{}
  89. defer func() {
  90. if !t.Failed() { // do not cleanup if test has already failed since we may need things in the etcd dump
  91. if err := client.cleanup(all); err != nil {
  92. t.Fatalf("failed to clean up etcd: %#v", err)
  93. }
  94. }
  95. }()
  96. if err := client.createPrerequisites(master.Mapper, testNamespace, testData.Prerequisites, all); err != nil {
  97. t.Fatalf("failed to create prerequisites for %s: %#v", gvResource, err)
  98. }
  99. if shouldCreate { // do not try to create items with no stub
  100. if err := client.create(testData.Stub, testNamespace, mapping, all); err != nil {
  101. t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
  102. }
  103. }
  104. output, err := getFromEtcd(master.KV, testData.ExpectedEtcdPath)
  105. if err != nil {
  106. t.Fatalf("failed to get from etcd for %s: %#v", gvResource, err)
  107. }
  108. expectedGVK := gvk
  109. if testData.ExpectedGVK != nil {
  110. if gvk == *testData.ExpectedGVK {
  111. t.Errorf("GVK override %s for %s is unnecessary or something was changed incorrectly", testData.ExpectedGVK, gvk)
  112. }
  113. expectedGVK = *testData.ExpectedGVK
  114. }
  115. actualGVK := output.getGVK()
  116. if actualGVK != expectedGVK {
  117. t.Errorf("GVK for %s does not match, expected %s got %s", kind, expectedGVK, actualGVK)
  118. }
  119. if !apiequality.Semantic.DeepDerivative(input, output) {
  120. t.Errorf("Test stub for %s does not match: %s", kind, diff.ObjectGoPrintDiff(input, output))
  121. }
  122. addGVKToEtcdBucket(cohabitatingResources, actualGVK, getEtcdBucket(testData.ExpectedEtcdPath))
  123. pathSeen[testData.ExpectedEtcdPath] = append(pathSeen[testData.ExpectedEtcdPath], mapping.Resource)
  124. })
  125. }
  126. if inEtcdData, inEtcdSeen := diffMaps(etcdStorageData, etcdSeen); len(inEtcdData) != 0 || len(inEtcdSeen) != 0 {
  127. t.Errorf("etcd data does not match the types we saw:\nin etcd data but not seen:\n%s\nseen but not in etcd data:\n%s", inEtcdData, inEtcdSeen)
  128. }
  129. if inKindData, inKindSeen := diffMaps(kindWhiteList, kindSeen); len(inKindData) != 0 || len(inKindSeen) != 0 {
  130. t.Errorf("kind whitelist data does not match the types we saw:\nin kind whitelist but not seen:\n%s\nseen but not in kind whitelist:\n%s", inKindData, inKindSeen)
  131. }
  132. for bucket, gvks := range cohabitatingResources {
  133. if len(gvks) != 1 {
  134. gvkStrings := []string{}
  135. for key := range gvks {
  136. gvkStrings = append(gvkStrings, keyStringer(key))
  137. }
  138. t.Errorf("cohabitating resources in etcd bucket %s have inconsistent GVKs\nyou may need to use DefaultStorageFactory.AddCohabitatingResources to sync the GVK of these resources:\n%s", bucket, gvkStrings)
  139. }
  140. }
  141. for path, gvrs := range pathSeen {
  142. if len(gvrs) != 1 {
  143. gvrStrings := []string{}
  144. for _, key := range gvrs {
  145. gvrStrings = append(gvrStrings, keyStringer(key))
  146. }
  147. t.Errorf("invalid test data, please ensure all expectedEtcdPath are unique, path %s has duplicate GVRs:\n%s", path, gvrStrings)
  148. }
  149. }
  150. }
  151. func dumpEtcdKVOnFailure(t *testing.T, kvClient clientv3.KV) {
  152. if t.Failed() {
  153. response, err := kvClient.Get(context.Background(), "/", clientv3.WithPrefix())
  154. if err != nil {
  155. t.Fatal(err)
  156. }
  157. for _, kv := range response.Kvs {
  158. t.Error(string(kv.Key), "->", string(kv.Value))
  159. }
  160. }
  161. }
  162. func addGVKToEtcdBucket(cohabitatingResources map[string]map[schema.GroupVersionKind]empty, gvk schema.GroupVersionKind, bucket string) {
  163. if cohabitatingResources[bucket] == nil {
  164. cohabitatingResources[bucket] = map[schema.GroupVersionKind]empty{}
  165. }
  166. cohabitatingResources[bucket][gvk] = empty{}
  167. }
  168. // getEtcdBucket assumes the last segment of the given etcd path is the name of the object.
  169. // Thus it strips that segment to extract the object's storage "bucket" in etcd. We expect
  170. // all objects that share the a bucket (cohabitating resources) to be stored as the same GVK.
  171. func getEtcdBucket(path string) string {
  172. idx := strings.LastIndex(path, "/")
  173. if idx == -1 {
  174. panic("path with no slashes " + path)
  175. }
  176. bucket := path[:idx]
  177. if len(bucket) == 0 {
  178. panic("invalid bucket for path " + path)
  179. }
  180. return bucket
  181. }
  182. // stable fields to compare as a sanity check
  183. type metaObject struct {
  184. // all of type meta
  185. Kind string `json:"kind,omitempty"`
  186. APIVersion string `json:"apiVersion,omitempty"`
  187. // parts of object meta
  188. Metadata struct {
  189. Name string `json:"name,omitempty"`
  190. Namespace string `json:"namespace,omitempty"`
  191. } `json:"metadata,omitempty"`
  192. }
  193. func (obj *metaObject) getGVK() schema.GroupVersionKind {
  194. return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
  195. }
  196. func (obj *metaObject) isEmpty() bool {
  197. return obj == nil || *obj == metaObject{} // compare to zero value since all fields are strings
  198. }
  199. type empty struct{}
  200. type cleanupData struct {
  201. obj *unstructured.Unstructured
  202. resource schema.GroupVersionResource
  203. }
  204. func jsonToMetaObject(stub []byte) (*metaObject, error) {
  205. obj := &metaObject{}
  206. if err := json.Unmarshal(stub, obj); err != nil {
  207. return nil, err
  208. }
  209. return obj, nil
  210. }
  211. func keyStringer(i interface{}) string {
  212. base := "\n\t"
  213. switch key := i.(type) {
  214. case string:
  215. return base + key
  216. case schema.GroupVersionResource:
  217. return base + key.String()
  218. case schema.GroupVersionKind:
  219. return base + key.String()
  220. default:
  221. panic("unexpected type")
  222. }
  223. }
  224. type allClient struct {
  225. dynamicClient dynamic.Interface
  226. }
  227. func (c *allClient) create(stub, ns string, mapping *meta.RESTMapping, all *[]cleanupData) error {
  228. resourceClient, obj, err := JSONToUnstructured(stub, ns, mapping, c.dynamicClient)
  229. if err != nil {
  230. return err
  231. }
  232. actual, err := resourceClient.Create(obj, metav1.CreateOptions{})
  233. if err != nil {
  234. return err
  235. }
  236. *all = append(*all, cleanupData{obj: actual, resource: mapping.Resource})
  237. return nil
  238. }
  239. func (c *allClient) cleanup(all *[]cleanupData) error {
  240. for i := len(*all) - 1; i >= 0; i-- { // delete in reverse order in case creation order mattered
  241. obj := (*all)[i].obj
  242. gvr := (*all)[i].resource
  243. if err := c.dynamicClient.Resource(gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), nil); err != nil {
  244. return err
  245. }
  246. }
  247. return nil
  248. }
  249. func (c *allClient) createPrerequisites(mapper meta.RESTMapper, ns string, prerequisites []Prerequisite, all *[]cleanupData) error {
  250. for _, prerequisite := range prerequisites {
  251. gvk, err := mapper.KindFor(prerequisite.GvrData)
  252. if err != nil {
  253. return err
  254. }
  255. mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
  256. if err != nil {
  257. return err
  258. }
  259. if err := c.create(prerequisite.Stub, ns, mapping, all); err != nil {
  260. return err
  261. }
  262. }
  263. return nil
  264. }
  265. func getFromEtcd(keys clientv3.KV, path string) (*metaObject, error) {
  266. response, err := keys.Get(context.Background(), path)
  267. if err != nil {
  268. return nil, err
  269. }
  270. if response.More || response.Count != 1 || len(response.Kvs) != 1 {
  271. return nil, fmt.Errorf("Invalid etcd response (not found == %v): %#v", response.Count == 0, response)
  272. }
  273. return jsonToMetaObject(response.Kvs[0].Value)
  274. }
  275. func diffMaps(a, b interface{}) ([]string, []string) {
  276. inA := diffMapKeys(a, b, keyStringer)
  277. inB := diffMapKeys(b, a, keyStringer)
  278. return inA, inB
  279. }
  280. func diffMapKeys(a, b interface{}, stringer func(interface{}) string) []string {
  281. av := reflect.ValueOf(a)
  282. bv := reflect.ValueOf(b)
  283. ret := []string{}
  284. for _, ka := range av.MapKeys() {
  285. kat := ka.Interface()
  286. found := false
  287. for _, kb := range bv.MapKeys() {
  288. kbt := kb.Interface()
  289. if kat == kbt {
  290. found = true
  291. break
  292. }
  293. }
  294. if !found {
  295. ret = append(ret, stringer(kat))
  296. }
  297. }
  298. return ret
  299. }