vitess.io/vitess@v0.16.2/go/vt/topo/k8stopo/file.go (about) 1 /* 2 Copyright 2020 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreedto in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package k8stopo 18 19 import ( 20 "bytes" 21 "compress/gzip" 22 "context" 23 "encoding/base64" 24 "fmt" 25 "hash/fnv" 26 "io" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "time" 31 32 "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/client-go/util/retry" 35 36 "vitess.io/vitess/go/vt/log" 37 "vitess.io/vitess/go/vt/topo" 38 vtv1beta1 "vitess.io/vitess/go/vt/topo/k8stopo/apis/topo/v1beta1" 39 ) 40 41 // NodeReference contains the data relating to a node 42 type NodeReference struct { 43 id string 44 key string 45 value string 46 } 47 48 func packValue(value []byte) ([]byte, error) { 49 encoded := &bytes.Buffer{} 50 encoder := base64.NewEncoder(base64.StdEncoding, encoded) 51 52 zw := gzip.NewWriter(encoder) 53 _, err := zw.Write(value) 54 if err != nil { 55 return []byte{}, fmt.Errorf("gzip write error: %s", err) 56 } 57 58 err = zw.Close() 59 if err != nil { 60 return []byte{}, fmt.Errorf("gzip close error: %s", err) 61 } 62 63 err = encoder.Close() 64 if err != nil { 65 return []byte{}, fmt.Errorf("base64 encoder close error: %s", err) 66 } 67 68 return encoded.Bytes(), nil 69 } 70 71 func unpackValue(value []byte) ([]byte, error) { 72 decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(value)) 73 74 zr, err := gzip.NewReader(decoder) 75 if err != nil { 76 return []byte{}, fmt.Errorf("unable to create new gzip reader: %s", err) 77 } 78 79 decoded := &bytes.Buffer{} 80 if _, err := io.Copy(decoded, zr); err != nil { 81 return []byte{}, fmt.Errorf("error coppying uncompressed data: %s", err) 82 } 83 84 if err := zr.Close(); err != nil { 85 return []byte{}, fmt.Errorf("unable to close gzip reader: %s", err) 86 } 87 88 return decoded.Bytes(), nil 89 } 90 91 // ToData converts a nodeReference to the data type used in the VitessTopoNode 92 func (n *NodeReference) ToData() vtv1beta1.VitessTopoNodeData { 93 return vtv1beta1.VitessTopoNodeData{ 94 Key: n.key, 95 Value: string(n.value), 96 } 97 } 98 99 func getHash(parent string) string { 100 hasher := fnv.New64a() 101 hasher.Write([]byte(parent)) 102 return strconv.FormatUint(hasher.Sum64(), 10) 103 } 104 105 func (s *Server) newNodeReference(key string) *NodeReference { 106 key = filepath.Join(s.root, key) 107 108 node := &NodeReference{ 109 id: fmt.Sprintf("vt-%s", getHash(key)), 110 key: key, 111 } 112 113 return node 114 } 115 116 func (s *Server) buildFileResource(filePath string, contents []byte) (*vtv1beta1.VitessTopoNode, error) { 117 node := s.newNodeReference(filePath) 118 119 value, err := packValue(contents) 120 if err != nil { 121 return nil, err 122 } 123 124 // create data 125 node.value = string(value) 126 127 // Create "file" object 128 return &vtv1beta1.VitessTopoNode{ 129 ObjectMeta: metav1.ObjectMeta{ 130 Name: node.id, 131 Namespace: s.namespace, 132 }, 133 Data: node.ToData(), 134 }, nil 135 } 136 137 // Create is part of the topo.Conn interface. 138 func (s *Server) Create(ctx context.Context, filePath string, contents []byte) (topo.Version, error) { 139 log.V(7).Infof("Create at '%s' Contents: '%s'", filePath, string(contents)) 140 141 resource, err := s.buildFileResource(filePath, contents) 142 if err != nil { 143 return nil, convertError(err, filePath) 144 } 145 146 final, err := s.resourceClient.Create(ctx, resource, metav1.CreateOptions{}) 147 if err != nil { 148 return nil, convertError(err, filePath) 149 } 150 151 // Update the internal cache 152 err = s.memberIndexer.Update(final) 153 if err != nil { 154 return nil, convertError(err, filePath) 155 } 156 157 return KubernetesVersion(final.GetResourceVersion()), nil 158 } 159 160 // Update is part of the topo.Conn interface. 161 func (s *Server) Update(ctx context.Context, filePath string, contents []byte, version topo.Version) (topo.Version, error) { 162 log.V(7).Infof("Update at '%s' Contents: '%s'", filePath, string(contents)) 163 164 resource, err := s.buildFileResource(filePath, contents) 165 if err != nil { 166 return nil, convertError(err, filePath) 167 } 168 169 var finalVersion KubernetesVersion 170 171 err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 172 result, err := s.resourceClient.Get(ctx, resource.Name, metav1.GetOptions{}) 173 if err != nil && errors.IsNotFound(err) && version == nil { 174 // Update should create objects when the version is nil and the object is not found 175 createdVersion, err := s.Create(ctx, filePath, contents) 176 if err != nil { 177 return err 178 } 179 finalVersion = KubernetesVersion(createdVersion.String()) 180 return nil 181 } 182 183 // If a non-nil version is given to update, fail on mismatched version 184 if version != nil && KubernetesVersion(result.GetResourceVersion()) != version { 185 return topo.NewError(topo.BadVersion, filePath) 186 } 187 188 // set new contents 189 result.Data.Value = resource.Data.Value 190 191 // get result or err 192 final, err := s.resourceClient.Update(ctx, result, metav1.UpdateOptions{}) 193 if err != nil { 194 return convertError(err, filePath) 195 } 196 197 // Update the internal cache 198 err = s.memberIndexer.Update(final) 199 if err != nil { 200 return convertError(err, filePath) 201 } 202 203 finalVersion = KubernetesVersion(final.GetResourceVersion()) 204 205 return nil 206 }) 207 if err != nil { 208 return nil, err 209 } 210 211 return finalVersion, nil 212 } 213 214 // Get is part of the topo.Conn interface. 215 func (s *Server) Get(ctx context.Context, filePath string) ([]byte, topo.Version, error) { 216 log.V(7).Infof("Get at '%s'", filePath) 217 218 node := s.newNodeReference(filePath) 219 220 result, err := s.resourceClient.Get(ctx, node.id, metav1.GetOptions{}) 221 if err != nil { 222 return []byte{}, nil, convertError(err, filePath) 223 } 224 225 out, err := unpackValue([]byte(result.Data.Value)) 226 if err != nil { 227 return []byte{}, nil, convertError(err, filePath) 228 } 229 230 return out, KubernetesVersion(result.GetResourceVersion()), nil 231 } 232 233 // List is part of the topo.Conn interface. 234 func (s *Server) List(ctx context.Context, filePathPrefix string) ([]topo.KVInfo, error) { 235 nodeList, err := s.resourceClient.List(ctx, metav1.ListOptions{}) 236 237 results := []topo.KVInfo{} 238 if err != nil { 239 return results, convertError(err, filePathPrefix) 240 } 241 nodes := nodeList.Items 242 if len(nodes) == 0 { 243 return results, topo.NewError(topo.NoNode, filePathPrefix) 244 } 245 rootPrefix := filepath.Join(s.root, filePathPrefix) 246 for _, node := range nodes { 247 if strings.HasPrefix(node.Data.Key, rootPrefix) { 248 out, err := unpackValue([]byte(node.Data.Value)) 249 if err != nil { 250 return results, convertError(err, node.Data.Key) 251 } 252 results = append(results, topo.KVInfo{ 253 Key: []byte(node.Data.Key), 254 Value: out, 255 Version: KubernetesVersion(node.GetResourceVersion()), 256 }) 257 } 258 } 259 260 return results, nil 261 } 262 263 // Delete is part of the topo.Conn interface. 264 func (s *Server) Delete(ctx context.Context, filePath string, version topo.Version) error { 265 log.V(7).Infof("Delete at '%s'", filePath) 266 267 node := s.newNodeReference(filePath) 268 269 // Check version before delete 270 current, err := s.resourceClient.Get(ctx, node.id, metav1.GetOptions{}) 271 if err != nil { 272 return convertError(err, filePath) 273 } 274 if version != nil { 275 if KubernetesVersion(current.GetResourceVersion()) != version { 276 return topo.NewError(topo.BadVersion, filePath) 277 } 278 } 279 280 err = s.resourceClient.Delete(ctx, node.id, metav1.DeleteOptions{}) 281 if err != nil { 282 return convertError(err, filePath) 283 } 284 285 // Wait for one of the following conditions 286 // 1. Context is cancelled 287 // 2. The object is no longer in the cache 288 // 3. The object in the cache has a new uid (was deleted but recreated since we last checked) 289 for { 290 select { 291 case <-ctx.Done(): 292 return convertError(ctx.Err(), filePath) 293 case <-time.After(50 * time.Millisecond): 294 } 295 296 obj, ok, err := s.memberIndexer.Get(current) 297 if err != nil { // error getting from cache 298 return convertError(err, filePath) 299 } 300 if !ok { // deleted from cache 301 break 302 } 303 cached := obj.(*vtv1beta1.VitessTopoNode) 304 if cached.GetUID() != current.GetUID() { 305 break // deleted and recreated 306 } 307 } 308 309 return nil 310 }