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  }