etcd_storage_path_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. "github.com/coreos/etcd/clientv3"
  22. "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. // force enable all resources so we can check storage.
  45. // TODO: drop these once we stop allowing them to be served.
  46. opts.APIEnablement.RuntimeConfig["extensions/v1beta1/deployments"] = "true"
  47. opts.APIEnablement.RuntimeConfig["extensions/v1beta1/daemonsets"] = "true"
  48. opts.APIEnablement.RuntimeConfig["extensions/v1beta1/replicasets"] = "true"
  49. opts.APIEnablement.RuntimeConfig["extensions/v1beta1/podsecuritypolicies"] = "true"
  50. opts.APIEnablement.RuntimeConfig["extensions/v1beta1/networkpolicies"] = "true"
  51. })
  52. defer master.Cleanup()
  53. defer dumpEtcdKVOnFailure(t, master.KV)
  54. client := &allClient{dynamicClient: master.Dynamic}
  55. if _, err := master.Client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil {
  56. t.Fatal(err)
  57. }
  58. etcdStorageData := GetEtcdStorageData()
  59. kindSeen := sets.NewString()
  60. pathSeen := map[string][]schema.GroupVersionResource{}
  61. etcdSeen := map[schema.GroupVersionResource]empty{}
  62. cohabitatingResources := map[string]map[schema.GroupVersionKind]empty{}
  63. for _, resourceToPersist := range master.Resources {
  64. t.Run(resourceToPersist.Mapping.Resource.String(), func(t *testing.T) {
  65. mapping := resourceToPersist.Mapping
  66. gvk := resourceToPersist.Mapping.GroupVersionKind
  67. gvResource := resourceToPersist.Mapping.Resource
  68. kind := gvk.Kind
  69. if kindWhiteList.Has(kind) {
  70. kindSeen.Insert(kind)
  71. t.Skip("whitelisted")
  72. }
  73. etcdSeen[gvResource] = empty{}
  74. testData, hasTest := etcdStorageData[gvResource]
  75. if !hasTest {
  76. t.Fatalf("no test data for %s. Please add a test for your new type to GetEtcdStorageData().", gvResource)
  77. }
  78. if len(testData.ExpectedEtcdPath) == 0 {
  79. t.Fatalf("empty test data for %s", gvResource)
  80. }
  81. shouldCreate := len(testData.Stub) != 0 // try to create only if we have a stub
  82. var (
  83. input *metaObject
  84. err error
  85. )
  86. if shouldCreate {
  87. if input, err = jsonToMetaObject([]byte(testData.Stub)); err != nil || input.isEmpty() {
  88. t.Fatalf("invalid test data for %s: %v", gvResource, err)
  89. }
  90. // unset type meta fields - we only set these in the CRD test data and it makes
  91. // any CRD test with an expectedGVK override fail the DeepDerivative test
  92. input.Kind = ""
  93. input.APIVersion = ""
  94. }
  95. all := &[]cleanupData{}
  96. defer func() {
  97. if !t.Failed() { // do not cleanup if test has already failed since we may need things in the etcd dump
  98. if err := client.cleanup(all); err != nil {
  99. t.Fatalf("failed to clean up etcd: %#v", err)
  100. }
  101. }
  102. }()
  103. if err := client.createPrerequisites(master.Mapper, testNamespace, testData.Prerequisites, all); err != nil {
  104. t.Fatalf("failed to create prerequisites for %s: %#v", gvResource, err)
  105. }
  106. if shouldCreate { // do not try to create items with no stub
  107. if err := client.create(testData.Stub, testNamespace, mapping, all); err != nil {
  108. t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
  109. }
  110. }
  111. output, err := getFromEtcd(master.KV, testData.ExpectedEtcdPath)
  112. if err != nil {
  113. t.Fatalf("failed to get from etcd for %s: %#v", gvResource, err)
  114. }
  115. expectedGVK := gvk
  116. if testData.ExpectedGVK != nil {
  117. if gvk == *testData.ExpectedGVK {
  118. t.Errorf("GVK override %s for %s is unnecessary or something was changed incorrectly", testData.ExpectedGVK, gvk)
  119. }
  120. expectedGVK = *testData.ExpectedGVK
  121. }
  122. actualGVK := output.getGVK()
  123. if actualGVK != expectedGVK {
  124. t.Errorf("GVK for %s does not match, expected %s got %s", kind, expectedGVK, actualGVK)
  125. }
  126. if !apiequality.Semantic.DeepDerivative(input, output) {
  127. t.Errorf("Test stub for %s does not match: %s", kind, diff.ObjectGoPrintDiff(input, output))
  128. }
  129. addGVKToEtcdBucket(cohabitatingResources, actualGVK, getEtcdBucket(testData.ExpectedEtcdPath))
  130. pathSeen[testData.ExpectedEtcdPath] = append(pathSeen[testData.ExpectedEtcdPath], mapping.Resource)
  131. })
  132. }
  133. if inEtcdData, inEtcdSeen := diffMaps(etcdStorageData, etcdSeen); len(inEtcdData) != 0 || len(inEtcdSeen) != 0 {
  134. 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)
  135. }
  136. if inKindData, inKindSeen := diffMaps(kindWhiteList, kindSeen); len(inKindData) != 0 || len(inKindSeen) != 0 {
  137. 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)
  138. }
  139. for bucket, gvks := range cohabitatingResources {
  140. if len(gvks) != 1 {
  141. gvkStrings := []string{}
  142. for key := range gvks {
  143. gvkStrings = append(gvkStrings, keyStringer(key))
  144. }
  145. 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)
  146. }
  147. }
  148. for path, gvrs := range pathSeen {
  149. if len(gvrs) != 1 {
  150. gvrStrings := []string{}
  151. for _, key := range gvrs {
  152. gvrStrings = append(gvrStrings, keyStringer(key))
  153. }
  154. t.Errorf("invalid test data, please ensure all expectedEtcdPath are unique, path %s has duplicate GVRs:\n%s", path, gvrStrings)
  155. }
  156. }
  157. }
  158. func dumpEtcdKVOnFailure(t *testing.T, kvClient clientv3.KV) {
  159. if t.Failed() {
  160. response, err := kvClient.Get(context.Background(), "/", clientv3.WithPrefix())
  161. if err != nil {
  162. t.Fatal(err)
  163. }
  164. for _, kv := range response.Kvs {
  165. t.Error(string(kv.Key), "->", string(kv.Value))
  166. }
  167. }
  168. }
  169. func addGVKToEtcdBucket(cohabitatingResources map[string]map[schema.GroupVersionKind]empty, gvk schema.GroupVersionKind, bucket string) {
  170. if cohabitatingResources[bucket] == nil {
  171. cohabitatingResources[bucket] = map[schema.GroupVersionKind]empty{}
  172. }
  173. cohabitatingResources[bucket][gvk] = empty{}
  174. }
  175. // getEtcdBucket assumes the last segment of the given etcd path is the name of the object.
  176. // Thus it strips that segment to extract the object's storage "bucket" in etcd. We expect
  177. // all objects that share the a bucket (cohabitating resources) to be stored as the same GVK.
  178. func getEtcdBucket(path string) string {
  179. idx := strings.LastIndex(path, "/")
  180. if idx == -1 {
  181. panic("path with no slashes " + path)
  182. }
  183. bucket := path[:idx]
  184. if len(bucket) == 0 {
  185. panic("invalid bucket for path " + path)
  186. }
  187. return bucket
  188. }
  189. // stable fields to compare as a sanity check
  190. type metaObject struct {
  191. // all of type meta
  192. Kind string `json:"kind,omitempty"`
  193. APIVersion string `json:"apiVersion,omitempty"`
  194. // parts of object meta
  195. Metadata struct {
  196. Name string `json:"name,omitempty"`
  197. Namespace string `json:"namespace,omitempty"`
  198. } `json:"metadata,omitempty"`
  199. }
  200. func (obj *metaObject) getGVK() schema.GroupVersionKind {
  201. return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
  202. }
  203. func (obj *metaObject) isEmpty() bool {
  204. return obj == nil || *obj == metaObject{} // compare to zero value since all fields are strings
  205. }
  206. type empty struct{}
  207. type cleanupData struct {
  208. obj *unstructured.Unstructured
  209. resource schema.GroupVersionResource
  210. }
  211. func jsonToMetaObject(stub []byte) (*metaObject, error) {
  212. obj := &metaObject{}
  213. if err := json.Unmarshal(stub, obj); err != nil {
  214. return nil, err
  215. }
  216. return obj, nil
  217. }
  218. func keyStringer(i interface{}) string {
  219. base := "\n\t"
  220. switch key := i.(type) {
  221. case string:
  222. return base + key
  223. case schema.GroupVersionResource:
  224. return base + key.String()
  225. case schema.GroupVersionKind:
  226. return base + key.String()
  227. default:
  228. panic("unexpected type")
  229. }
  230. }
  231. type allClient struct {
  232. dynamicClient dynamic.Interface
  233. }
  234. func (c *allClient) create(stub, ns string, mapping *meta.RESTMapping, all *[]cleanupData) error {
  235. resourceClient, obj, err := JSONToUnstructured(stub, ns, mapping, c.dynamicClient)
  236. if err != nil {
  237. return err
  238. }
  239. actual, err := resourceClient.Create(obj, metav1.CreateOptions{})
  240. if err != nil {
  241. return err
  242. }
  243. *all = append(*all, cleanupData{obj: actual, resource: mapping.Resource})
  244. return nil
  245. }
  246. func (c *allClient) cleanup(all *[]cleanupData) error {
  247. for i := len(*all) - 1; i >= 0; i-- { // delete in reverse order in case creation order mattered
  248. obj := (*all)[i].obj
  249. gvr := (*all)[i].resource
  250. if err := c.dynamicClient.Resource(gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), nil); err != nil {
  251. return err
  252. }
  253. }
  254. return nil
  255. }
  256. func (c *allClient) createPrerequisites(mapper meta.RESTMapper, ns string, prerequisites []Prerequisite, all *[]cleanupData) error {
  257. for _, prerequisite := range prerequisites {
  258. gvk, err := mapper.KindFor(prerequisite.GvrData)
  259. if err != nil {
  260. return err
  261. }
  262. mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
  263. if err != nil {
  264. return err
  265. }
  266. if err := c.create(prerequisite.Stub, ns, mapping, all); err != nil {
  267. return err
  268. }
  269. }
  270. return nil
  271. }
  272. func getFromEtcd(keys clientv3.KV, path string) (*metaObject, error) {
  273. response, err := keys.Get(context.Background(), path)
  274. if err != nil {
  275. return nil, err
  276. }
  277. if response.More || response.Count != 1 || len(response.Kvs) != 1 {
  278. return nil, fmt.Errorf("Invalid etcd response (not found == %v): %#v", response.Count == 0, response)
  279. }
  280. return jsonToMetaObject(response.Kvs[0].Value)
  281. }
  282. func diffMaps(a, b interface{}) ([]string, []string) {
  283. inA := diffMapKeys(a, b, keyStringer)
  284. inB := diffMapKeys(b, a, keyStringer)
  285. return inA, inB
  286. }
  287. func diffMapKeys(a, b interface{}, stringer func(interface{}) string) []string {
  288. av := reflect.ValueOf(a)
  289. bv := reflect.ValueOf(b)
  290. ret := []string{}
  291. for _, ka := range av.MapKeys() {
  292. kat := ka.Interface()
  293. found := false
  294. for _, kb := range bv.MapKeys() {
  295. kbt := kb.Interface()
  296. if kat == kbt {
  297. found = true
  298. break
  299. }
  300. }
  301. if !found {
  302. ret = append(ret, stringer(kat))
  303. }
  304. }
  305. return ret
  306. }