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 }