// SetList sets the given list object's Items member have the elements given in // objects. // Returns an error if list is not a List type (does not have an Items member), // or if any of the objects are not of the right type. func SetList(list Object, objects []Object) error { itemsPtr, err := GetItemsPtr(list) if err != nil { return err } items, err := conversion.EnforcePtr(itemsPtr) if err != nil { return err } slice := reflect.MakeSlice(items.Type(), len(objects), len(objects)) for i := range objects { dest := slice.Index(i) src, err := conversion.EnforcePtr(objects[i]) if err != nil { return err } if src.Type().AssignableTo(dest.Type()) { dest.Set(src) } else if src.Type().ConvertibleTo(dest.Type()) { dest.Set(src.Convert(dest.Type())) } else { return fmt.Errorf("item[%v]: Type mismatch: Expected %v, got %v", dest.Type(), src.Type()) } } items.Set(slice) return nil }
func (h *etcdHelper) extractObj(response *etcd.Response, inErr error, objPtr runtime.Object, ignoreNotFound, prevNode bool) (body string, node *etcd.Node, err error) { if response != nil { if prevNode { node = response.PrevNode } else { node = response.Node } } if inErr != nil || node == nil || len(node.Value) == 0 { if ignoreNotFound { v, err := conversion.EnforcePtr(objPtr) if err != nil { return "", nil, err } v.Set(reflect.Zero(v.Type())) return "", nil, nil } else if inErr != nil { return "", nil, inErr } return "", nil, fmt.Errorf("unable to locate a value on the response: %#v", response) } body = node.Value err = h.codec.DecodeInto([]byte(body), objPtr) if h.versioner != nil { _ = h.versioner.UpdateObject(objPtr, node.Expiration, node.ModifiedIndex) // being unable to set the version does not prevent the object from being extracted } return body, node, err }
// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route. // The object must be a pointer to a struct; only fields at the top level of the struct that are not // themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard // Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is // the JSON field name. If a description struct tag is set on the field, that description is used on the // query parameter. In essence, it converts a standard JSON top level object into a query param schema. func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj interface{}) error { sv, err := conversion.EnforcePtr(obj) if err != nil { return err } st := sv.Type() switch st.Kind() { case reflect.Struct: for i := 0; i < st.NumField(); i++ { name := st.Field(i).Name sf, ok := st.FieldByName(name) if !ok { continue } switch sf.Type.Kind() { case reflect.Interface, reflect.Struct: default: jsonTag := sf.Tag.Get("json") if len(jsonTag) == 0 { continue } jsonName := strings.SplitN(jsonTag, ",", 2)[0] if len(jsonName) == 0 { continue } desc := sf.Tag.Get("description") route.Param(ws.QueryParameter(jsonName, desc).DataType(typeToJSON(sf.Type.Name()))) } } } return nil }
// Implements storage.Interface. func (h *etcdHelper) Create(key string, obj, out runtime.Object, ttl uint64) error { key = h.prefixEtcdKey(key) data, err := h.codec.Encode(obj) if err != nil { return err } if h.versioner != nil { if version, err := h.versioner.ObjectResourceVersion(obj); err == nil && version != 0 { return errors.New("resourceVersion may not be set on objects to be created") } } startTime := time.Now() response, err := h.client.Create(key, string(data), ttl) metrics.RecordEtcdRequestLatency("create", getTypeName(obj), startTime) if err != nil { return err } if out != nil { if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } _, _, err = h.extractObj(response, err, out, false, false) } return err }
// ExtractList returns obj's Items element as an array of runtime.Objects. // Returns an error if obj is not a List type (does not have an Items member). func ExtractList(obj Object) ([]Object, error) { itemsPtr, err := GetItemsPtr(obj) if err != nil { return nil, err } items, err := conversion.EnforcePtr(itemsPtr) if err != nil { return nil, err } list := make([]Object, items.Len()) for i := range list { raw := items.Index(i) var found bool switch raw.Kind() { case reflect.Interface, reflect.Ptr: list[i], found = raw.Interface().(Object) default: list[i], found = raw.Addr().Interface().(Object) } if !found { return nil, fmt.Errorf("item[%v]: Expected object, got %#v(%s)", i, raw.Interface(), raw.Kind()) } } return list, nil }
// decodeNodeList walks the tree of each node in the list and decodes into the specified object func (h *EtcdHelper) decodeNodeList(nodes []*etcd.Node, slicePtr interface{}) error { v, err := conversion.EnforcePtr(slicePtr) if err != nil || v.Kind() != reflect.Slice { // This should not happen at runtime. panic("need ptr to slice") } for _, node := range nodes { if node.Dir { if err := h.decodeNodeList(node.Nodes, slicePtr); err != nil { return err } continue } obj := reflect.New(v.Type().Elem()) if err := h.Codec.DecodeInto([]byte(node.Value), obj.Interface().(runtime.Object)); err != nil { return err } if h.ResourceVersioner != nil { _ = h.ResourceVersioner.SetResourceVersion(obj.Interface().(runtime.Object), node.ModifiedIndex) // being unable to set the version does not prevent the object from being extracted } v.Set(reflect.Append(v, obj.Elem())) } return nil }
func (h *EtcdHelper) bodyAndExtractObj(key string, objPtr runtime.Object, ignoreNotFound bool) (body string, modifiedIndex uint64, err error) { response, err := h.Client.Get(key, false, false) if err != nil && !IsEtcdNotFound(err) { return "", 0, err } if err != nil || response.Node == nil || len(response.Node.Value) == 0 { if ignoreNotFound { v, err := conversion.EnforcePtr(objPtr) if err != nil { return "", 0, err } v.Set(reflect.Zero(v.Type())) return "", 0, nil } else if err != nil { return "", 0, err } return "", 0, fmt.Errorf("key '%v' found no nodes field: %#v", key, response) } body = response.Node.Value err = h.Codec.DecodeInto([]byte(body), objPtr) if h.ResourceVersioner != nil { _ = h.ResourceVersioner.SetResourceVersion(objPtr, response.Node.ModifiedIndex) // being unable to set the version does not prevent the object from being extracted } return body, response.Node.ModifiedIndex, err }
// SetObj marshals obj via json, and stores under key. Will do an atomic update if obj's ResourceVersion // field is set. 'ttl' is time-to-live in seconds, and 0 means forever. If no error is returned and out is // not nil, out will be set to the read value from etcd. func (h *EtcdHelper) SetObj(key string, obj, out runtime.Object, ttl uint64) error { var response *etcd.Response data, err := h.Codec.Encode(obj) if err != nil { return err } create := true if h.Versioner != nil { if version, err := h.Versioner.ObjectResourceVersion(obj); err == nil && version != 0 { create = false response, err = h.Client.CompareAndSwap(key, string(data), ttl, "", version) if err != nil { return err } } } if create { // Create will fail if a key already exists. response, err = h.Client.Create(key, string(data), ttl) } if err != nil { return err } if out != nil { if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } _, _, err = h.extractObj(response, err, out, false, false) } return err }
// ListMetaFor returns a pointer to a provided object's ListMeta, // or an error if the object does not have that pointer. // TODO: allow runtime.Unknown to extract this object func ListMetaFor(obj runtime.Object) (*ListMeta, error) { v, err := conversion.EnforcePtr(obj) if err != nil { return nil, err } var meta *ListMeta err = runtime.FieldPtr(v, "ListMeta", &meta) return meta, err }
// GetObjectMetaPtr returns a pointer to a provided object's ObjectMeta. // TODO: allow runtime.Unknown to extract this object func ObjectMetaFor(obj runtime.Object) (*ObjectMeta, error) { v, err := conversion.EnforcePtr(obj) if err != nil { return nil, err } var objectMeta *ObjectMeta if err := runtime.FieldPtr(v, "ObjectMeta", &objectMeta); err != nil { return nil, err } return objectMeta, nil }
// DeleteObj removes the specified key and returns the value that existed at that spot. func (h *EtcdHelper) DeleteObj(key string, out runtime.Object) error { if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } response, err := h.Client.Delete(key, false) if !IsEtcdNotFound(err) { // if the object that existed prior to the delete is returned by etcd, update out. if err != nil || response.PrevNode != nil { _, _, err = h.extractObj(response, err, out, false, true) } } return err }
// GetItemsPtr returns a pointer to the list object's Items member. // If 'list' doesn't have an Items member, it's not really a list type // and an error will be returned. // This function will either return a pointer to a slice, or an error, but not both. func GetItemsPtr(list Object) (interface{}, error) { v, err := conversion.EnforcePtr(list) if err != nil { return nil, err } items := v.FieldByName("Items") if !items.IsValid() { return nil, fmt.Errorf("no Items field in %#v", list) } if items.Kind() != reflect.Slice { return nil, fmt.Errorf("Items field is not a slice") } return items.Addr().Interface(), nil }
// GuaranteedUpdate calls "tryUpdate()" to update key "key" that is of type "ptrToType". It keeps // calling tryUpdate() and retrying the update until success if there is etcd index conflict. Note that object // passed to tryUpdate() may change across invocations of tryUpdate() if other writers are simultaneously // updating it, so tryUpdate() needs to take into account the current contents of the object when // deciding how the updated object (that it returns) should look. // // Example: // // h := &util.EtcdHelper{client, encoding, versioning} // err := h.GuaranteedUpdate("myKey", &MyType{}, true, func(input runtime.Object) (runtime.Object, uint64, error) { // // Before each invocation of the user-defined function, "input" is reset to etcd's current contents for "myKey". // // cur := input.(*MyType) // Guaranteed to succeed. // // // Make a *modification*. // cur.Counter++ // // // Return the modified object. Return an error to stop iterating. Return a non-zero uint64 to set // // the TTL on the object. // return cur, 0, nil // }) // func (h *EtcdHelper) GuaranteedUpdate(key string, ptrToType runtime.Object, ignoreNotFound bool, tryUpdate EtcdUpdateFunc) error { v, err := conversion.EnforcePtr(ptrToType) if err != nil { // Panic is appropriate, because this is a programming error. panic("need ptr to type") } key = h.PrefixEtcdKey(key) for { obj := reflect.New(v.Type()).Interface().(runtime.Object) origBody, index, err := h.bodyAndExtractObj(key, obj, ignoreNotFound) if err != nil { return err } ret, ttl, err := tryUpdate(obj) if err != nil { return err } data, err := h.Codec.Encode(ret) if err != nil { return err } // First time this key has been used, try creating new value. if index == 0 { startTime := time.Now() response, err := h.Client.Create(key, string(data), ttl) recordEtcdRequestLatency("create", getTypeName(ptrToType), startTime) if IsEtcdNodeExist(err) { continue } _, _, err = h.extractObj(response, err, ptrToType, false, false) return err } if string(data) == origBody { return nil } startTime := time.Now() response, err := h.Client.CompareAndSwap(key, string(data), ttl, origBody, index) recordEtcdRequestLatency("compareAndSwap", getTypeName(ptrToType), startTime) if IsEtcdTestFailed(err) { continue } _, _, err = h.extractObj(response, err, ptrToType, false, false) return err } }
// Implements storage.Interface. func (h *etcdHelper) Delete(key string, out runtime.Object) error { key = h.prefixEtcdKey(key) if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } startTime := time.Now() response, err := h.client.Delete(key, false) metrics.RecordEtcdRequestLatency("delete", getTypeName(out), startTime) if !IsEtcdNotFound(err) { // if the object that existed prior to the delete is returned by etcd, update out. if err != nil || response.PrevNode != nil { _, _, err = h.extractObj(response, err, out, false, true) } } return err }
// WithType extracts a list of runtime objects with the specified type func (g *Generated) WithType(slicePtr interface{}) bool { found := false v, err := conversion.EnforcePtr(slicePtr) if err != nil || v.Kind() != reflect.Slice { // This should not happen at runtime. panic("need ptr to slice") } t := v.Type().Elem() for i := range g.Items { obj := reflect.ValueOf(g.Items[i]).Elem() if !obj.Type().ConvertibleTo(t) { continue } found = true v.Set(reflect.Append(v, obj.Convert(t))) } return found }
// AtomicUpdate generalizes the pattern that allows for making atomic updates to etcd objects. // Note, tryUpdate may be called more than once. // // Example: // // h := &util.EtcdHelper{client, encoding, versioning} // err := h.AtomicUpdate("myKey", &MyType{}, func(input runtime.Object) (runtime.Object, error) { // // Before this function is called, currentObj has been reset to etcd's current // // contents for "myKey". // // cur := input.(*MyType) // Guaranteed to work. // // // Make a *modification*. // cur.Counter++ // // // Return the modified object. Return an error to stop iterating. // return cur, nil // }) // func (h *EtcdHelper) AtomicUpdate(key string, ptrToType runtime.Object, tryUpdate EtcdUpdateFunc) error { v, err := conversion.EnforcePtr(ptrToType) if err != nil { // Panic is appropriate, because this is a programming error. panic("need ptr to type") } for { obj := reflect.New(v.Type()).Interface().(runtime.Object) origBody, index, err := h.bodyAndExtractObj(key, obj, true) if err != nil { return err } ret, err := tryUpdate(obj) if err != nil { return err } data, err := h.Codec.Encode(ret) if err != nil { return err } glog.V(3).Infof("Atomic update(%s): %+v", key, ret) // First time this key has been used, try creating new value. if index == 0 { _, err = h.Client.Create(key, string(data), 0) if IsEtcdNodeExist(err) { continue } return err } if string(data) == origBody { return nil } _, err = h.Client.CompareAndSwap(key, string(data), 0, origBody, index) if IsEtcdTestFailed(err) { continue } return err } }
// Accessor takes an arbitary object pointer and returns meta.Interface. // obj must be a pointer to an API type. An error is returned if the minimum // required fields are missing. Fields that are not required return the default // value and are a no-op if set. // TODO: add a fast path for *TypeMeta and *ObjectMeta for internal objects func Accessor(obj interface{}) (Interface, error) { v, err := conversion.EnforcePtr(obj) if err != nil { return nil, err } t := v.Type() if v.Kind() != reflect.Struct { return nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), t, v.Interface()) } typeMeta := v.FieldByName("TypeMeta") if !typeMeta.IsValid() { return nil, fmt.Errorf("struct %v lacks embedded TypeMeta type", t) } a := &genericAccessor{} if err := extractFromTypeMeta(typeMeta, a); err != nil { return nil, fmt.Errorf("unable to find type fields on %#v: %v", typeMeta, err) } objectMeta := v.FieldByName("ObjectMeta") if objectMeta.IsValid() { // look for the ObjectMeta fields if err := extractFromObjectMeta(objectMeta, a); err != nil { return nil, fmt.Errorf("unable to find object fields on %#v: %v", objectMeta, err) } } else { listMeta := v.FieldByName("ListMeta") if listMeta.IsValid() { // look for the ListMeta fields if err := extractFromListMeta(listMeta, a); err != nil { return nil, fmt.Errorf("unable to find list fields on %#v: %v", listMeta, err) } } else { // look for the older TypeMeta with all metadata if err := extractFromObjectMeta(typeMeta, a); err != nil { return nil, fmt.Errorf("unable to find object fields on %#v: %v", typeMeta, err) } } } return a, nil }
// ExtractList returns obj's Items element as an array of runtime.Objects. // Returns an error if obj is not a List type (does not have an Items member). func ExtractList(obj Object) ([]Object, error) { itemsPtr, err := GetItemsPtr(obj) if err != nil { return nil, err } items, err := conversion.EnforcePtr(itemsPtr) if err != nil { return nil, err } list := make([]Object, items.Len()) for i := range list { raw := items.Index(i) item, ok := raw.Addr().Interface().(Object) if !ok { return nil, fmt.Errorf("item[%v]: Expected object, got %#v", i, raw.Interface()) } list[i] = item } return list, nil }
// fieldPtr puts the address of fieldName, which must be a member of v, // into dest, which must be an address of a variable to which this field's // address can be assigned. func fieldPtr(v reflect.Value, fieldName string, dest interface{}) error { field := v.FieldByName(fieldName) if !field.IsValid() { return fmt.Errorf("Couldn't find %v field in %#v", fieldName, v.Interface()) } v, err := conversion.EnforcePtr(dest) if err != nil { return err } field = field.Addr() if field.Type().AssignableTo(v.Type()) { v.Set(field) return nil } if field.Type().ConvertibleTo(v.Type()) { v.Set(field.Convert(v.Type())) return nil } return fmt.Errorf("Couldn't assign/convert %v to %v", field.Type(), v.Type()) }
// TypeAccessor returns an interface that allows retrieving and modifying the APIVersion // and Kind of an in-memory internal object. // TODO: this interface is used to test code that does not have ObjectMeta or ListMeta // in round tripping (objects which can use apiVersion/kind, but do not fit the Kube // api conventions). func TypeAccessor(obj interface{}) (TypeInterface, error) { v, err := conversion.EnforcePtr(obj) if err != nil { return nil, err } t := v.Type() if v.Kind() != reflect.Struct { return nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), t, v.Interface()) } typeMeta := v.FieldByName("TypeMeta") if !typeMeta.IsValid() { return nil, fmt.Errorf("struct %v lacks embedded TypeMeta type", t) } a := &genericAccessor{} if err := extractFromTypeMeta(typeMeta, a); err != nil { return nil, fmt.Errorf("unable to find type fields on %#v: %v", typeMeta, err) } return a, nil }
// CreateObj adds a new object at a key unless it already exists. 'ttl' is time-to-live in seconds, // and 0 means forever. If no error is returned and out is not nil, out will be set to the read value // from etcd. func (h *EtcdHelper) CreateObj(key string, obj, out runtime.Object, ttl uint64) error { data, err := h.Codec.Encode(obj) if err != nil { return err } if h.Versioner != nil { if version, err := h.Versioner.ObjectResourceVersion(obj); err == nil && version != 0 { return errors.New("resourceVersion may not be set on objects to be created") } } response, err := h.Client.Create(key, string(data), ttl) if err != nil { return err } if out != nil { if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } _, _, err = h.extractObj(response, err, out, false, false) } return err }
// GetItemsPtr returns a pointer to the list object's Items member. // If 'list' doesn't have an Items member, it's not really a list type // and an error will be returned. // This function will either return a pointer to a slice, or an error, but not both. func GetItemsPtr(list Object) (interface{}, error) { v, err := conversion.EnforcePtr(list) if err != nil { return nil, err } items := v.FieldByName("Items") if !items.IsValid() { return nil, fmt.Errorf("no Items field in %#v", list) } switch items.Kind() { case reflect.Interface, reflect.Ptr: target := reflect.TypeOf(items.Interface()).Elem() if target.Kind() != reflect.Slice { return nil, fmt.Errorf("items: Expected slice, got %s", target.Kind()) } return items.Interface(), nil case reflect.Slice: return items.Addr().Interface(), nil default: return nil, fmt.Errorf("items: Expected slice, got %s", items.Kind()) } }
// Implements storage.Interface. func (h *etcdHelper) Set(key string, obj, out runtime.Object, ttl uint64) error { var response *etcd.Response data, err := h.codec.Encode(obj) if err != nil { return err } key = h.prefixEtcdKey(key) create := true if h.versioner != nil { if version, err := h.versioner.ObjectResourceVersion(obj); err == nil && version != 0 { create = false startTime := time.Now() response, err = h.client.CompareAndSwap(key, string(data), ttl, "", version) metrics.RecordEtcdRequestLatency("compareAndSwap", getTypeName(obj), startTime) if err != nil { return err } } } if create { // Create will fail if a key already exists. startTime := time.Now() response, err = h.client.Create(key, string(data), ttl) metrics.RecordEtcdRequestLatency("create", getTypeName(obj), startTime) } if err != nil { return err } if out != nil { if _, err := conversion.EnforcePtr(out); err != nil { panic("unable to convert output object to pointer") } _, _, err = h.extractObj(response, err, out, false, false) } return err }
// decodeNodeList walks the tree of each node in the list and decodes into the specified object func (h *etcdHelper) decodeNodeList(nodes []*etcd.Node, slicePtr interface{}) error { trace := util.NewTrace("decodeNodeList " + getTypeName(slicePtr)) defer trace.LogIfLong(500 * time.Millisecond) v, err := conversion.EnforcePtr(slicePtr) if err != nil || v.Kind() != reflect.Slice { // This should not happen at runtime. panic("need ptr to slice") } for _, node := range nodes { if node.Dir { trace.Step("Decoding dir " + node.Key + " START") if err := h.decodeNodeList(node.Nodes, slicePtr); err != nil { return err } trace.Step("Decoding dir " + node.Key + " END") continue } if obj, found := h.getFromCache(node.ModifiedIndex); found { v.Set(reflect.Append(v, reflect.ValueOf(obj).Elem())) } else { obj := reflect.New(v.Type().Elem()) if err := h.codec.DecodeInto([]byte(node.Value), obj.Interface().(runtime.Object)); err != nil { return err } if h.versioner != nil { // being unable to set the version does not prevent the object from being extracted _ = h.versioner.UpdateObject(obj.Interface().(runtime.Object), node.Expiration, node.ModifiedIndex) } v.Set(reflect.Append(v, obj.Elem())) if node.ModifiedIndex != 0 { h.addToCache(node.ModifiedIndex, obj.Interface().(runtime.Object)) } } } trace.Step(fmt.Sprintf("Decoded %v nodes", len(nodes))) return nil }
// Implements storage.Interface. func (h *etcdHelper) GuaranteedUpdate(key string, ptrToType runtime.Object, ignoreNotFound bool, tryUpdate storage.UpdateFunc) error { v, err := conversion.EnforcePtr(ptrToType) if err != nil { // Panic is appropriate, because this is a programming error. panic("need ptr to type") } key = h.prefixEtcdKey(key) for { obj := reflect.New(v.Type()).Interface().(runtime.Object) origBody, node, res, err := h.bodyAndExtractObj(key, obj, ignoreNotFound) if err != nil { return err } meta := storage.ResponseMeta{} if node != nil { meta.TTL = node.TTL if node.Expiration != nil { meta.Expiration = node.Expiration } meta.ResourceVersion = node.ModifiedIndex } // Get the object to be written by calling tryUpdate. ret, newTTL, err := tryUpdate(obj, meta) if err != nil { return err } index := uint64(0) ttl := uint64(0) if node != nil { index = node.ModifiedIndex if node.TTL > 0 { ttl = uint64(node.TTL) } } else if res != nil { index = res.EtcdIndex } if newTTL != nil { ttl = *newTTL } data, err := h.codec.Encode(ret) if err != nil { return err } // First time this key has been used, try creating new value. if index == 0 { startTime := time.Now() response, err := h.client.Create(key, string(data), ttl) metrics.RecordEtcdRequestLatency("create", getTypeName(ptrToType), startTime) if IsEtcdNodeExist(err) { continue } _, _, err = h.extractObj(response, err, ptrToType, false, false) return err } if string(data) == origBody { return nil } startTime := time.Now() // Swap origBody with data, if origBody is the latest etcd data. response, err := h.client.CompareAndSwap(key, string(data), ttl, origBody, index) metrics.RecordEtcdRequestLatency("compareAndSwap", getTypeName(ptrToType), startTime) if IsEtcdTestFailed(err) { // Try again. continue } _, _, err = h.extractObj(response, err, ptrToType, false, false) return err } }