github.com/cs3org/reva/v2@v2.27.7/tests/integration/grpc/grpc_suite_test.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     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  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package grpc_test
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"net"
    25  	"os"
    26  	"os/exec"
    27  	"path"
    28  	"path/filepath"
    29  	"strings"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/google/uuid"
    35  	"github.com/pkg/errors"
    36  
    37  	. "github.com/onsi/ginkgo/v2"
    38  	. "github.com/onsi/gomega"
    39  )
    40  
    41  const timeoutMs = 30000
    42  
    43  var mutex = sync.Mutex{}
    44  var port = 19000
    45  
    46  func TestGrpc(t *testing.T) {
    47  	RegisterFailHandler(Fail)
    48  	RunSpecs(t, "Grpc Suite")
    49  }
    50  
    51  type cleanupFunc func(bool) error
    52  
    53  // Revad represents a running revad process
    54  type Revad struct {
    55  	TmpRoot     string      // Temporary directory on disk. Will be cleaned up by the Cleanup func.
    56  	StorageRoot string      // Temporary directory used for the revad storage on disk. Will be cleaned up by the Cleanup func.
    57  	GrpcAddress string      // Address of the grpc service
    58  	ID          string      // ID of the grpc service
    59  	Cleanup     cleanupFunc // Function to kill the process and cleanup the temp. root. If the given parameter is true the files will be kept to make debugging failures easier.
    60  }
    61  
    62  type res struct{}
    63  
    64  func (res) isResource() {}
    65  
    66  type Resource interface {
    67  	isResource()
    68  }
    69  
    70  type Folder struct {
    71  	res
    72  }
    73  
    74  type File struct {
    75  	res
    76  	Content  any
    77  	Encoding string // json, plain
    78  }
    79  
    80  type RevadConfig struct {
    81  	Name      string
    82  	Config    string
    83  	Files     map[string]string
    84  	Resources map[string]Resource
    85  }
    86  
    87  // startRevads takes a list of revad configuration files plus a map of
    88  // variables that need to be substituted in them and starts them.
    89  //
    90  // A unique port is assigned to each spawned instance.
    91  // Placeholders in the config files can be replaced the variables from the
    92  // `variables` map, e.g. the config
    93  //
    94  //	redis = "{{redis_address}}"
    95  //
    96  // and the variables map
    97  //
    98  //	variables = map[string]string{"redis_address": "localhost:6379"}
    99  //
   100  // will result in the config
   101  //
   102  //	redis = "localhost:6379"
   103  //
   104  // Special variables are created for the revad addresses, e.g. having a
   105  // `storage` and a `users` revad will make `storage_address` and
   106  // `users_address` available wit the dynamically assigned ports so that
   107  // the services can be made available to each other.
   108  func startRevads(configs []RevadConfig, variables map[string]string) (map[string]*Revad, error) {
   109  	mutex.Lock()
   110  	defer mutex.Unlock()
   111  
   112  	revads := map[string]*Revad{}
   113  	addresses := map[string]string{}
   114  	ids := map[string]string{}
   115  	roots := map[string]string{}
   116  
   117  	tmpBase, err := os.MkdirTemp("", "reva-grpc-integration-tests")
   118  	if err != nil {
   119  		return nil, errors.Wrapf(err, "Could not create tmpdir")
   120  	}
   121  
   122  	for _, c := range configs {
   123  		ids[c.Name] = uuid.New().String()
   124  		// Create a temporary root for this revad
   125  		tmpRoot := path.Join(tmpBase, c.Name)
   126  		roots[c.Name] = tmpRoot
   127  		addresses[c.Name] = fmt.Sprintf("localhost:%d", port)
   128  		port++
   129  		addresses[c.Name+"+1"] = fmt.Sprintf("localhost:%d", port)
   130  		port++
   131  		addresses[c.Name+"+2"] = fmt.Sprintf("localhost:%d", port)
   132  		port++
   133  	}
   134  
   135  	for _, c := range configs {
   136  		ownAddress := addresses[c.Name]
   137  		ownID := ids[c.Name]
   138  		filesPath := map[string]string{}
   139  
   140  		tmpRoot := roots[c.Name]
   141  		err := os.Mkdir(tmpRoot, 0755)
   142  		if err != nil {
   143  			return nil, errors.Wrapf(err, "Could not create tmpdir")
   144  		}
   145  
   146  		newCfgPath := path.Join(tmpRoot, "config.toml")
   147  		rawCfg, err := os.ReadFile(path.Join("fixtures", c.Config))
   148  		if err != nil {
   149  			return nil, errors.Wrapf(err, "Could not read config file")
   150  		}
   151  
   152  		for name, p := range c.Files {
   153  			rawFile, err := os.ReadFile(path.Join("fixtures", p))
   154  			if err != nil {
   155  				return nil, errors.Wrapf(err, "error reading file")
   156  			}
   157  			cfg := string(rawFile)
   158  			for v, value := range variables {
   159  				cfg = strings.ReplaceAll(cfg, "{{"+v+"}}", value)
   160  			}
   161  			for name, address := range addresses {
   162  				cfg = strings.ReplaceAll(cfg, "{{"+name+"_address}}", address)
   163  			}
   164  			newFilePath := path.Join(tmpRoot, p)
   165  			err = os.WriteFile(newFilePath, []byte(cfg), 0600)
   166  			if err != nil {
   167  				return nil, errors.Wrapf(err, "error writing file")
   168  			}
   169  			filesPath[name] = newFilePath
   170  		}
   171  		for name, resource := range c.Resources {
   172  			tmpResourcePath := filepath.Join(tmpRoot, name)
   173  
   174  			switch r := resource.(type) {
   175  			case File:
   176  				// fill the file with the initial content
   177  				switch r.Encoding {
   178  				case "", "plain":
   179  					if err := os.WriteFile(tmpResourcePath, []byte(r.Content.(string)), 0644); err != nil {
   180  						return nil, err
   181  					}
   182  				case "json":
   183  					d, err := json.Marshal(r.Content)
   184  					if err != nil {
   185  						return nil, err
   186  					}
   187  					if err := os.WriteFile(tmpResourcePath, d, 0644); err != nil {
   188  						return nil, err
   189  					}
   190  				default:
   191  					return nil, errors.New("encoding not known " + r.Encoding)
   192  				}
   193  			case Folder:
   194  				if err := os.MkdirAll(tmpResourcePath, 0755); err != nil {
   195  					return nil, err
   196  				}
   197  			}
   198  
   199  			filesPath[name] = tmpResourcePath
   200  		}
   201  
   202  		cfg := string(rawCfg)
   203  		cfg = strings.ReplaceAll(cfg, "{{root}}", tmpRoot)
   204  		cfg = strings.ReplaceAll(cfg, "{{id}}", ownID)
   205  		cfg = strings.ReplaceAll(cfg, "{{grpc_address}}", ownAddress)
   206  		cfg = strings.ReplaceAll(cfg, "{{grpc_address+1}}", addresses[c.Name+"+1"])
   207  		cfg = strings.ReplaceAll(cfg, "{{grpc_address+2}}", addresses[c.Name+"+2"])
   208  		for name, path := range filesPath {
   209  			cfg = strings.ReplaceAll(cfg, "{{file_"+name+"}}", path)
   210  			cfg = strings.ReplaceAll(cfg, "{{"+name+"}}", path)
   211  		}
   212  		for v, value := range variables {
   213  			cfg = strings.ReplaceAll(cfg, "{{"+v+"}}", value)
   214  		}
   215  		for name, address := range addresses {
   216  			cfg = strings.ReplaceAll(cfg, "{{"+name+"_address}}", address)
   217  		}
   218  		for name, id := range ids {
   219  			cfg = strings.ReplaceAll(cfg, "{{"+name+"_id}}", id)
   220  		}
   221  		for name, root := range roots {
   222  			cfg = strings.ReplaceAll(cfg, "{{"+name+"_root}}", root)
   223  		}
   224  		err = os.WriteFile(newCfgPath, []byte(cfg), 0600)
   225  		if err != nil {
   226  			return nil, errors.Wrapf(err, "Could not write config file")
   227  		}
   228  
   229  		// Run revad
   230  		cmd := exec.Command("../../../cmd/revad/revad", "-log", "debug", "-c", newCfgPath)
   231  
   232  		outfile, err := os.Create(path.Join(tmpRoot, c.Name+"-out.log"))
   233  		if err != nil {
   234  			panic(err)
   235  		}
   236  		defer outfile.Close()
   237  		cmd.Stdout = outfile
   238  		cmd.Stderr = outfile
   239  
   240  		err = cmd.Start()
   241  		if err != nil {
   242  			return nil, errors.Wrapf(err, "Could not start revad")
   243  		}
   244  
   245  		err = waitForPort(ownAddress, "open")
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  
   250  		// even the port is open the service might not be available yet
   251  		time.Sleep(2 * time.Second)
   252  
   253  		revad := &Revad{
   254  			TmpRoot:     tmpRoot,
   255  			StorageRoot: path.Join(tmpRoot, "storage"),
   256  			GrpcAddress: ownAddress,
   257  			ID:          ownID,
   258  			Cleanup: func(keepLogs bool) error {
   259  				err := cmd.Process.Signal(os.Kill)
   260  				if err != nil {
   261  					return errors.Wrap(err, "Could not kill revad")
   262  				}
   263  				_ = waitForPort(ownAddress, "close")
   264  				if keepLogs {
   265  					fmt.Println("Test failed, keeping root", tmpRoot, "around for debugging")
   266  				} else {
   267  					os.RemoveAll(tmpRoot)
   268  					os.Remove(tmpBase) // Remove base temp dir if it's empty
   269  				}
   270  				return nil
   271  			},
   272  		}
   273  		revads[c.Name] = revad
   274  	}
   275  	return revads, nil
   276  }
   277  
   278  func waitForPort(grpcAddress, expectedStatus string) error {
   279  	if expectedStatus != "open" && expectedStatus != "close" {
   280  		return errors.New("status can only be 'open' or 'close'")
   281  	}
   282  	timoutCounter := 0
   283  	for timoutCounter <= timeoutMs {
   284  		conn, err := net.Dial("tcp", grpcAddress)
   285  		if err == nil {
   286  			_ = conn.Close()
   287  			if expectedStatus == "open" {
   288  				break
   289  			}
   290  		} else if expectedStatus == "close" {
   291  			break
   292  		}
   293  
   294  		time.Sleep(1 * time.Millisecond)
   295  		timoutCounter++
   296  	}
   297  	return nil
   298  }