github.com/MetalBlockchain/metalgo@v1.11.9/tests/antithesis/compose.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package antithesis
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/compose-spec/compose-go/types"
    14  	"gopkg.in/yaml.v3"
    15  
    16  	"github.com/MetalBlockchain/metalgo/config"
    17  	"github.com/MetalBlockchain/metalgo/tests/fixture/tmpnet"
    18  	"github.com/MetalBlockchain/metalgo/utils/constants"
    19  	"github.com/MetalBlockchain/metalgo/utils/logging"
    20  	"github.com/MetalBlockchain/metalgo/utils/perms"
    21  )
    22  
    23  const bootstrapIndex = 0
    24  
    25  // Initialize the given path with the docker-compose configuration (compose file and
    26  // volumes) needed for an Antithesis test setup.
    27  func GenerateComposeConfig(
    28  	network *tmpnet.Network,
    29  	nodeImageName string,
    30  	workloadImageName string,
    31  	targetPath string,
    32  ) error {
    33  	// Generate a compose project for the specified network
    34  	project, err := newComposeProject(network, nodeImageName, workloadImageName)
    35  	if err != nil {
    36  		return fmt.Errorf("failed to create compose project: %w", err)
    37  	}
    38  
    39  	absPath, err := filepath.Abs(targetPath)
    40  	if err != nil {
    41  		return fmt.Errorf("failed to convert target path to absolute path: %w", err)
    42  	}
    43  
    44  	if err := os.MkdirAll(absPath, perms.ReadWriteExecute); err != nil {
    45  		return fmt.Errorf("failed to create target path %q: %w", absPath, err)
    46  	}
    47  
    48  	// Write the compose file
    49  	bytes, err := yaml.Marshal(&project)
    50  	if err != nil {
    51  		return fmt.Errorf("failed to marshal compose project: %w", err)
    52  	}
    53  	composePath := filepath.Join(targetPath, "docker-compose.yml")
    54  	if err := os.WriteFile(composePath, bytes, perms.ReadWrite); err != nil {
    55  		return fmt.Errorf("failed to write genesis: %w", err)
    56  	}
    57  
    58  	// Create the volume paths
    59  	for _, service := range project.Services {
    60  		for _, volume := range service.Volumes {
    61  			volumePath := filepath.Join(absPath, volume.Source)
    62  			if err := os.MkdirAll(volumePath, perms.ReadWriteExecute); err != nil {
    63  				return fmt.Errorf("failed to create volume path %q: %w", volumePath, err)
    64  			}
    65  		}
    66  	}
    67  	return nil
    68  }
    69  
    70  // Create a new docker compose project for an antithesis test setup
    71  // for the provided network configuration.
    72  func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadImageName string) (*types.Project, error) {
    73  	networkName := "avalanche-testnet"
    74  	baseNetworkAddress := "10.0.20"
    75  
    76  	services := make(types.Services, len(network.Nodes)+1)
    77  	uris := make([]string, len(network.Nodes))
    78  	var (
    79  		bootstrapIP  string
    80  		bootstrapIDs string
    81  	)
    82  	for i, node := range network.Nodes {
    83  		address := fmt.Sprintf("%s.%d", baseNetworkAddress, 3+i)
    84  
    85  		tlsKey, err := node.Flags.GetStringVal(config.StakingTLSKeyContentKey)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		tlsCert, err := node.Flags.GetStringVal(config.StakingCertContentKey)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  		signerKey, err := node.Flags.GetStringVal(config.StakingSignerKeyContentKey)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  
    98  		env := types.Mapping{
    99  			config.NetworkNameKey:             constants.LocalName,
   100  			config.LogLevelKey:                logging.Debug.String(),
   101  			config.LogDisplayLevelKey:         logging.Trace.String(),
   102  			config.HTTPHostKey:                "0.0.0.0",
   103  			config.PublicIPKey:                address,
   104  			config.StakingTLSKeyContentKey:    tlsKey,
   105  			config.StakingCertContentKey:      tlsCert,
   106  			config.StakingSignerKeyContentKey: signerKey,
   107  		}
   108  
   109  		// Apply configuration appropriate to a test network
   110  		for k, v := range tmpnet.DefaultTestFlags() {
   111  			switch value := v.(type) {
   112  			case string:
   113  				env[k] = value
   114  			case bool:
   115  				env[k] = strconv.FormatBool(value)
   116  			default:
   117  				return nil, fmt.Errorf("unable to convert unsupported type %T to string", v)
   118  			}
   119  		}
   120  
   121  		serviceName := getServiceName(i)
   122  
   123  		volumes := []types.ServiceVolumeConfig{
   124  			{
   125  				Type:   types.VolumeTypeBind,
   126  				Source: fmt.Sprintf("./volumes/%s/logs", serviceName),
   127  				Target: "/root/.avalanchego/logs",
   128  			},
   129  		}
   130  
   131  		trackSubnets, err := node.Flags.GetStringVal(config.TrackSubnetsKey)
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  		if len(trackSubnets) > 0 {
   136  			env[config.TrackSubnetsKey] = trackSubnets
   137  			if i == bootstrapIndex {
   138  				// DB volume for bootstrap node will need to initialized with the subnet
   139  				volumes = append(volumes, types.ServiceVolumeConfig{
   140  					Type:   types.VolumeTypeBind,
   141  					Source: fmt.Sprintf("./volumes/%s/db", serviceName),
   142  					Target: "/root/.avalanchego/db",
   143  				})
   144  			}
   145  		}
   146  
   147  		if i == 0 {
   148  			bootstrapIP = address + ":9651"
   149  			bootstrapIDs = node.NodeID.String()
   150  		} else {
   151  			env[config.BootstrapIPsKey] = bootstrapIP
   152  			env[config.BootstrapIDsKey] = bootstrapIDs
   153  		}
   154  
   155  		// The env is defined with the keys and then converted to env
   156  		// vars because only the keys are available as constants.
   157  		env = keyMapToEnvVarMap(env)
   158  
   159  		services[i+1] = types.ServiceConfig{
   160  			Name:          serviceName,
   161  			ContainerName: serviceName,
   162  			Hostname:      serviceName,
   163  			Image:         nodeImageName,
   164  			Volumes:       volumes,
   165  			Environment:   env.ToMappingWithEquals(),
   166  			Networks: map[string]*types.ServiceNetworkConfig{
   167  				networkName: {
   168  					Ipv4Address: address,
   169  				},
   170  			},
   171  		}
   172  
   173  		// Collect URIs for the workload container
   174  		uris[i] = fmt.Sprintf("http://%s:9650", address)
   175  	}
   176  
   177  	workloadEnv := types.Mapping{
   178  		"AVAWL_URIS": strings.Join(uris, " "),
   179  	}
   180  	chainIDs := []string{}
   181  	for _, subnet := range network.Subnets {
   182  		for _, chain := range subnet.Chains {
   183  			chainIDs = append(chainIDs, chain.ChainID.String())
   184  		}
   185  	}
   186  	if len(chainIDs) > 0 {
   187  		workloadEnv["AVAWL_CHAIN_IDS"] = strings.Join(chainIDs, " ")
   188  	}
   189  
   190  	workloadName := "workload"
   191  	services[0] = types.ServiceConfig{
   192  		Name:          workloadName,
   193  		ContainerName: workloadName,
   194  		Hostname:      workloadName,
   195  		Image:         workloadImageName,
   196  		Environment:   workloadEnv.ToMappingWithEquals(),
   197  		Networks: map[string]*types.ServiceNetworkConfig{
   198  			networkName: {
   199  				Ipv4Address: baseNetworkAddress + ".129",
   200  			},
   201  		},
   202  	}
   203  
   204  	return &types.Project{
   205  		Networks: types.Networks{
   206  			networkName: types.NetworkConfig{
   207  				Driver: "bridge",
   208  				Ipam: types.IPAMConfig{
   209  					Config: []*types.IPAMPool{
   210  						{
   211  							Subnet: baseNetworkAddress + ".0/24",
   212  						},
   213  					},
   214  				},
   215  			},
   216  		},
   217  		Services: services,
   218  	}, nil
   219  }
   220  
   221  // Convert a mapping of avalanche config keys to a mapping of env vars
   222  func keyMapToEnvVarMap(keyMap types.Mapping) types.Mapping {
   223  	envVarMap := make(types.Mapping, len(keyMap))
   224  	for key, val := range keyMap {
   225  		// e.g. network-id -> AVAGO_NETWORK_ID
   226  		envVar := strings.ToUpper(config.EnvPrefix + "_" + config.DashesToUnderscores.Replace(key))
   227  		envVarMap[envVar] = val
   228  	}
   229  	return envVarMap
   230  }
   231  
   232  // Retrieve the service name for a node at the given index. Common to
   233  // GenerateComposeConfig and InitDBVolumes to ensure consistency
   234  // between db volumes configuration and volume paths.
   235  func getServiceName(index int) string {
   236  	baseName := "avalanche"
   237  	if index == 0 {
   238  		return baseName + "-bootstrap-node"
   239  	}
   240  	return fmt.Sprintf("%s-node-%d", baseName, index)
   241  }