diff --git a/.gitignore b/.gitignore index f44b0b37e..e34cbb460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .DS_Store vendor node_modules -/scripts/config.js +/scripts/* /rocketpool/rocketpool /rocketpool-cli/rocketpool-cli-darwin-amd64 /rocketpool-cli/rocketpool-cli-linux-amd64 diff --git a/addons/constructors.go b/addons/constructors.go index d7c8f639f..a3fd9aced 100644 --- a/addons/constructors.go +++ b/addons/constructors.go @@ -6,8 +6,8 @@ import ( "github.com/rocket-pool/smartnode/shared/types/addons" ) -func NewGraffitiWallWriter() addons.SmartnodeAddon { - return graffiti_wall_writer.NewGraffitiWallWriter() +func NewGraffitiWallWriter(containerTag string) addons.SmartnodeAddon { + return graffiti_wall_writer.NewGraffitiWallWriter(containerTag) } func NewRescueNode() addons.SmartnodeAddon { diff --git a/addons/graffiti_wall_writer/addon.go b/addons/graffiti_wall_writer/addon.go index dffe2476a..d9a19361b 100644 --- a/addons/graffiti_wall_writer/addon.go +++ b/addons/graffiti_wall_writer/addon.go @@ -16,9 +16,9 @@ type GraffitiWallWriter struct { cfg *GraffitiWallWriterConfig `yaml:"config,omitempty"` } -func NewGraffitiWallWriter() addons.SmartnodeAddon { +func NewGraffitiWallWriter(containerTag string) addons.SmartnodeAddon { return &GraffitiWallWriter{ - cfg: NewConfig(), + cfg: NewConfig(containerTag), } } @@ -43,5 +43,12 @@ func (gww *GraffitiWallWriter) GetEnabledParameter() *cfgtypes.Parameter { } func (gww *GraffitiWallWriter) GetContainerTag() string { - return containerTag + if gww.cfg.ContainerTag.Value != nil { + return gww.cfg.ContainerTag.Value.(string) + } + def, err := gww.cfg.ContainerTag.GetDefault(cfgtypes.Network_All) + if err != nil { + return "" + } + return def.(string) } diff --git a/addons/graffiti_wall_writer/config.go b/addons/graffiti_wall_writer/config.go index 87fce876a..3c44f2eef 100644 --- a/addons/graffiti_wall_writer/config.go +++ b/addons/graffiti_wall_writer/config.go @@ -4,11 +4,6 @@ import ( "github.com/rocket-pool/smartnode/shared/types/config" ) -// Constants -const ( - containerTag string = "rocketpool/graffiti-wall-addon:v1.0.1" -) - // Configuration for the Graffiti Wall Writer type GraffitiWallWriterConfig struct { Title string `yaml:"-"` @@ -31,7 +26,7 @@ type GraffitiWallWriterConfig struct { } // Creates a new configuration instance -func NewConfig() *GraffitiWallWriterConfig { +func NewConfig(containerTag string) *GraffitiWallWriterConfig { return &GraffitiWallWriterConfig{ Title: "Graffiti Wall Writer Settings", diff --git a/rocketpool-cli/service/config/standard-layout.go b/rocketpool-cli/service/config/standard-layout.go index e7fae17b2..7609efd7c 100644 --- a/rocketpool-cli/service/config/standard-layout.go +++ b/rocketpool-cli/service/config/standard-layout.go @@ -96,6 +96,13 @@ func (layout *standardLayout) createForm(networkParam *cfgtypes.Parameter, title param := layout.parameters[formItem].parameter defaultValue, _ := param.GetDefault(networkParam.Value.(cfgtypes.Network)) descriptionText := fmt.Sprintf("Default: %v\n\n%s", defaultValue, param.Description) + if param.ID == "containerTag" { + if fmt.Sprint(param.Value) == fmt.Sprint(defaultValue) { + descriptionText = "This is the default version of this service for the current Smart Node release.\n\n" + param.Description + } else { + descriptionText = fmt.Sprintf("The default version for this service is %v.\n\n%s", defaultValue, param.Description) + } + } layout.descriptionBox.SetText(descriptionText) layout.descriptionBox.ScrollToBeginning() } diff --git a/rocketpool-cli/service/service.go b/rocketpool-cli/service/service.go index 45f87f3ee..46f5bd670 100644 --- a/rocketpool-cli/service/service.go +++ b/rocketpool-cli/service/service.go @@ -81,6 +81,14 @@ func installService(yes, verbose, noDeps bool, path string) error { } } + if path == "" { + path = rp.ConfigPath() + } + path, err = homedir.Expand(path) + if err != nil { + return fmt.Errorf("error expanding install path [%s]: %w", path, err) + } + // Install service err = rp.InstallService(verbose, noDeps, path, dataPath) if err != nil { diff --git a/shared/services/config/alertmanager-config.go b/shared/services/config/alertmanager-config.go index 45efb4602..f30fc9891 100644 --- a/shared/services/config/alertmanager-config.go +++ b/shared/services/config/alertmanager-config.go @@ -14,7 +14,7 @@ import ( ) // Constants -const alertmanagerTag string = "prom/alertmanager:v0.26.0" + const AlertmanagerConfigTemplate string = "alerting/alertmanager.tmpl" const AlertmanagerConfigFile string = "alerting/alertmanager.yml" @@ -175,7 +175,7 @@ func NewAlertmanagerConfig(cfg *RocketPoolConfig) *AlertmanagerConfig { Name: "Alertmanager Container Tag", Description: "The tag name of the Alertmanager container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: map[config.Network]interface{}{config.Network_All: alertmanagerTag}, + Default: map[config.Network]interface{}{config.Network_All: cfg.imageMainnet(ImageAlertmanager)}, AffectsContainers: []config.ContainerID{config.ContainerID_Alertmanager}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/besu-params.go b/shared/services/config/besu-config.go similarity index 96% rename from shared/services/config/besu-params.go rename to shared/services/config/besu-config.go index 2328e7a2a..27f808866 100644 --- a/shared/services/config/besu-params.go +++ b/shared/services/config/besu-config.go @@ -6,8 +6,6 @@ import ( // Constants const ( - besuTagTest string = "hyperledger/besu:26.8.1" - besuTagProd string = "hyperledger/besu:26.8.1" besuEventLogInterval int = 1000 besuMaxPeers uint16 = 25 besuStopSignal string = "SIGTERM" @@ -97,7 +95,7 @@ func NewBesuConfig(cfg *RocketPoolConfig) *BesuConfig { Name: "Container Tag", Description: "The tag name of the Besu container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, besuTagProd, besuTagTest), + Default: cfg.imageTagDefaults(ImageBesu), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/commit-boost-config.go b/shared/services/config/commit-boost-config.go index 56b2b97b7..4b71e70e7 100644 --- a/shared/services/config/commit-boost-config.go +++ b/shared/services/config/commit-boost-config.go @@ -11,8 +11,6 @@ import ( const ( CommitBoostConfigFile string = "cb_config.toml" CommitBoostConfigTemplate string = "commit-boost-config" - commitBoostProdTag string = "ghcr.io/commit-boost/commit-boost:v0.10.1" - commitBoostTestTag string = "ghcr.io/commit-boost/commit-boost:v0.10.1" ) // Relay selection mode for Commit-Boost PBS @@ -180,7 +178,7 @@ func NewCommitBoostConfig(cfg *RocketPoolConfig) *CommitBoostConfig { CanBeBlank: false, OverwriteOnUpgrade: true, Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, commitBoostProdTag, commitBoostTestTag), + Default: cfg.imageTagDefaults(ImageCommitBoost), }, ExternalUrl: config.Parameter{ @@ -343,6 +341,9 @@ func (cfg *CommitBoostConfig) GetRelayString() string { // Get the container tag value as a string (for use in templates) func (cfg *CommitBoostConfig) GetContainerTag() string { + if cfg.parentConfig != nil { + return cfg.parentConfig.resolveParamImage(&cfg.ContainerTag, ImageCommitBoost) + } return fmt.Sprint(cfg.ContainerTag.Value) } diff --git a/shared/services/config/erigon-params.go b/shared/services/config/erigon-config.go similarity index 95% rename from shared/services/config/erigon-params.go rename to shared/services/config/erigon-config.go index c5111885e..f113ea5d4 100644 --- a/shared/services/config/erigon-params.go +++ b/shared/services/config/erigon-config.go @@ -8,8 +8,6 @@ import ( // Constants const ( - erigonTagProd string = "erigontech/erigon:v3.6.1" - erigonTagTest string = "erigontech/erigon:v3.6.1" erigonEventLogInterval int = 1000 erigonStopSignal string = "SIGINT" defaultErigonTorrentPort uint16 = 42069 @@ -85,7 +83,7 @@ func NewErigonConfig(cfg *RocketPoolConfig) *ErigonConfig { Name: "Container Tag", Description: "The tag name of the Erigon container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, erigonTagProd, erigonTagTest), + Default: cfg.imageTagDefaults(ImageErigon), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/exporter-config.go b/shared/services/config/exporter-config.go index 707f52b20..ef6be072e 100644 --- a/shared/services/config/exporter-config.go +++ b/shared/services/config/exporter-config.go @@ -4,9 +4,6 @@ import ( "github.com/rocket-pool/smartnode/shared/types/config" ) -// Constants -const exporterTag string = "prom/node-exporter:v1.12.1" - // Defaults const defaultExporterRootFs bool = false @@ -45,7 +42,7 @@ func NewExporterConfig(cfg *RocketPoolConfig) *ExporterConfig { Name: "Exporter Container Tag", Description: "The tag name of the Prometheus Node Exporter container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: map[config.Network]interface{}{config.Network_All: exporterTag}, + Default: map[config.Network]interface{}{config.Network_All: cfg.imageMainnet(ImageExporter)}, AffectsContainers: []config.ContainerID{config.ContainerID_Exporter}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/external-configs.go b/shared/services/config/external-configs.go index 2e7fb3fc2..27c9922fc 100644 --- a/shared/services/config/external-configs.go +++ b/shared/services/config/external-configs.go @@ -239,7 +239,7 @@ func NewExternalLighthouseConfig(cfg *RocketPoolConfig) *ExternalLighthouseConfi Name: "Container Tag", Description: "The tag name of the Lighthouse container you want to use from Docker Hub. This will be used for the Validator Client that Rocket Pool manages with your minipool keys.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, lighthouseTagPortableProd, lighthouseTagPortableTest), + Default: cfg.imageTagDefaults(ImageLighthouse), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -324,7 +324,7 @@ func NewExternalLodestarConfig(cfg *RocketPoolConfig) *ExternalLodestarConfig { Name: "Container Tag", Description: "The tag name of the Lodestar container you want to use from Docker Hub. This will be used for the Validator Client that Rocket Pool manages with your minipool keys.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, lodestarTagProd, lodestarTagTest), + Default: cfg.imageTagDefaults(ImageLodestar), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -410,7 +410,7 @@ func NewExternalNimbusConfig(cfg *RocketPoolConfig) *ExternalNimbusConfig { Name: "Container Tag", Description: "The tag name of the Nimbus validator container you want to use from Docker Hub. This will be used for the Validator Client that Rocket Pool manages with your minipool keys.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, nimbusVcTagProd, nimbusVcTagTest), + Default: cfg.imageTagDefaults(ImageNimbusVc), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -506,7 +506,7 @@ func NewExternalPrysmConfig(cfg *RocketPoolConfig) *ExternalPrysmConfig { Name: "Container Tag", Description: "The tag name of the Prysm validator container you want to use from Docker Hub. This will be used for the Validator Client that Rocket Pool manages with your minipool keys.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, prysmVcProd, prysmVcTest), + Default: cfg.imageTagDefaults(ImagePrysmVc), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -591,7 +591,7 @@ func NewExternalTekuConfig(cfg *RocketPoolConfig) *ExternalTekuConfig { Name: "Container Tag", Description: "The tag name of the Teku container you want to use from Docker Hub. This will be used for the Validator Client that Rocket Pool manages with your minipool keys.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, tekuTagProd, tekuTagTest), + Default: cfg.imageTagDefaults(ImageTeku), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/geth-params.go b/shared/services/config/geth-config.go similarity index 95% rename from shared/services/config/geth-params.go rename to shared/services/config/geth-config.go index d2d09e7d8..134d0af36 100644 --- a/shared/services/config/geth-params.go +++ b/shared/services/config/geth-config.go @@ -8,8 +8,6 @@ import ( // Constants const ( - gethTagProd string = "ethereum/client-go:v1.17.5" - gethTagTest string = "ethereum/client-go:v1.17.5" gethEventLogInterval int = 1000 gethStopSignal string = "SIGTERM" ) @@ -84,7 +82,7 @@ func NewGethConfig(cfg *RocketPoolConfig) *GethConfig { Name: "Container Tag", Description: "The tag name of the Geth container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, gethTagProd, gethTagTest), + Default: cfg.imageTagDefaults(ImageGeth), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/grafana-config.go b/shared/services/config/grafana-config.go index e7da720d5..41e46ffc6 100644 --- a/shared/services/config/grafana-config.go +++ b/shared/services/config/grafana-config.go @@ -4,9 +4,6 @@ import ( "github.com/rocket-pool/smartnode/shared/types/config" ) -// Constants -const grafanaTag string = "grafana/grafana:9.5.18" - // Defaults const defaultGrafanaPort uint16 = 3100 const defaultGrafanaOpenPort string = string(config.RPC_OpenExternal) @@ -59,7 +56,7 @@ func NewGrafanaConfig(cfg *RocketPoolConfig) *GrafanaConfig { Name: "Grafana Container Tag", Description: "The tag name of the Grafana container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: map[config.Network]interface{}{config.Network_All: grafanaTag}, + Default: map[config.Network]interface{}{config.Network_All: cfg.imageMainnet(ImageGrafana)}, AffectsContainers: []config.ContainerID{config.ContainerID_Grafana}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/images.go b/shared/services/config/images.go new file mode 100644 index 000000000..893914b9f --- /dev/null +++ b/shared/services/config/images.go @@ -0,0 +1,801 @@ +package config + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/rocket-pool/smartnode/shared/services/rocketpool/assets" + "github.com/rocket-pool/smartnode/shared/types/config" +) + +const ( + ImagesMainnetFile = "mainnet.env" + ImagesTestnetFile = "testnet.env" + ImagesDevnetFile = "devnet.env" + ComposeMainFile = "compose.yml" + + ImageSmartnode = "RP_IMAGE_SMARTNODE" + + ImageGeth = "RP_IMAGE_GETH" + ImageNethermind = "RP_IMAGE_NETHERMIND" + ImageBesu = "RP_IMAGE_BESU" + ImageReth = "RP_IMAGE_RETH" + ImageErigon = "RP_IMAGE_ERIGON" + + ImageLighthouse = "RP_IMAGE_LIGHTHOUSE" + ImageLodestar = "RP_IMAGE_LODESTAR" + ImageNimbusBn = "RP_IMAGE_NIMBUS_BN" + ImageNimbusVc = "RP_IMAGE_NIMBUS_VC" + ImageTeku = "RP_IMAGE_TEKU" + ImagePrysmBn = "RP_IMAGE_PRYSM_BN" + ImagePrysmVc = "RP_IMAGE_PRYSM_VC" + + ImageMevBoost = "RP_IMAGE_MEV_BOOST" + ImageCommitBoost = "RP_IMAGE_COMMIT_BOOST" + + ImagePrometheus = "RP_IMAGE_PROMETHEUS" + ImageExporter = "RP_IMAGE_EXPORTER" + ImageAlertmanager = "RP_IMAGE_ALERTMANAGER" + ImageGrafana = "RP_IMAGE_GRAFANA" + ImageGWW = "RP_IMAGE_GWW" + ImageCurl = "RP_IMAGE_CURL" + ImageAlpine = "RP_IMAGE_ALPINE" + + ECImageTagDefault = "EC_IMAGE_TAG_DEFAULT" + ECImageTagOverride = "EC_IMAGE_TAG_OVERRIDE" + BNImageTagDefault = "BN_IMAGE_TAG_DEFAULT" + BNImageTagOverride = "BN_IMAGE_TAG_OVERRIDE" + VCImageTagDefault = "VC_IMAGE_TAG_DEFAULT" + VCImageTagOverride = "VC_IMAGE_TAG_OVERRIDE" + SmartnodeImageTagDefault = "SMARTNODE_IMAGE_TAG_DEFAULT" + SmartnodeImageTagOverride = "SMARTNODE_IMAGE_TAG_OVERRIDE" + MevBoostImageTagDefault = "MEV_BOOST_IMAGE_TAG_DEFAULT" + MevBoostImageTagOverride = "MEV_BOOST_IMAGE_TAG_OVERRIDE" + CommitBoostImageTagDefault = "COMMIT_BOOST_IMAGE_TAG_DEFAULT" + CommitBoostImageTagOverride = "COMMIT_BOOST_IMAGE_TAG_OVERRIDE" + PrometheusImageTagDefault = "PROMETHEUS_IMAGE_TAG_DEFAULT" + PrometheusImageTagOverride = "PROMETHEUS_IMAGE_TAG_OVERRIDE" + GrafanaImageTagDefault = "GRAFANA_IMAGE_TAG_DEFAULT" + GrafanaImageTagOverride = "GRAFANA_IMAGE_TAG_OVERRIDE" + ExporterImageTagDefault = "EXPORTER_IMAGE_TAG_DEFAULT" + ExporterImageTagOverride = "EXPORTER_IMAGE_TAG_OVERRIDE" + AlertmanagerImageTagDefault = "ALERTMANAGER_IMAGE_TAG_DEFAULT" + AlertmanagerImageTagOverride = "ALERTMANAGER_IMAGE_TAG_OVERRIDE" + GWWImageTagDefault = "GWW_IMAGE_TAG_DEFAULT" + GWWImageTagOverride = "GWW_IMAGE_TAG_OVERRIDE" + CurlImageTagDefault = "CURL_IMAGE_TAG_DEFAULT" + CurlImageTagOverride = "CURL_IMAGE_TAG_OVERRIDE" +) + +func ImageTagRef(overrideKey, defaultKey string) string { + return "${" + overrideKey + ":-${" + defaultKey + "}}" +} + +// requiredImageKeys must be present in mainnet.env. Network overlays may omit keys. +var requiredImageKeys = []string{ + ImageSmartnode, + ImageGeth, ImageNethermind, ImageBesu, ImageReth, ImageErigon, + ImageLighthouse, ImageLodestar, ImageNimbusBn, ImageNimbusVc, ImageTeku, ImagePrysmBn, ImagePrysmVc, + ImageMevBoost, ImageCommitBoost, + ImagePrometheus, ImageExporter, ImageAlertmanager, ImageGrafana, + ImageGWW, ImageCurl, ImageAlpine, +} + +// ImageCatalog is the parsed official (or overlay) image env file. +type ImageCatalog struct { + values map[string]string +} + +func (c *ImageCatalog) Get(key string) (string, bool) { + if c == nil { + return "", false + } + v, ok := c.values[key] + return v, ok +} + +func (c *ImageCatalog) Must(key string) string { + v, ok := c.Get(key) + if !ok || v == "" { + panic("missing image catalog key " + key) + } + return v +} + +func (c *ImageCatalog) Set(key, value string) { + if c == nil { + return + } + if c.values == nil { + c.values = map[string]string{} + } + c.values[key] = value +} + +func (c *ImageCatalog) Map() map[string]string { + if c == nil { + return nil + } + out := make(map[string]string, len(c.values)) + for k, v := range c.values { + out[k] = v + } + return out +} + +func EnvRef(key string) string { + return "${" + key + "}" +} + +// ParseEnvFile parses KEY=VALUE lines. Comments and blank lines are ignored. +// Quoted values have a single pair of matching quotes stripped. +func ParseEnvFile(data []byte) (map[string]string, error) { + out := map[string]string{} + scanner := bufio.NewScanner(bytes.NewReader(data)) + lineNo := 0 + for scanner.Scan() { + lineNo++ + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, value, ok := strings.Cut(line, "=") + if !ok { + return nil, fmt.Errorf("line %d: expected KEY=VALUE", lineNo) + } + key = strings.TrimSpace(key) + if key == "" { + return nil, fmt.Errorf("line %d: empty key", lineNo) + } + value = strings.TrimSpace(value) + if len(value) >= 2 { + if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') { + value = value[1 : len(value)-1] + } + } + out[key] = value + } + if err := scanner.Err(); err != nil { + return nil, err + } + return out, nil +} + +func LoadEnvFile(path string) (map[string]string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return ParseEnvFile(data) +} + +func catalogFileName(network config.Network) string { + if network == "" || network == config.Network_Unknown { + return "" + } + return string(network) + ".env" +} + +func loadCatalogBytes(name string, data []byte, requireAll bool) (*ImageCatalog, error) { + values, err := ParseEnvFile(data) + if err != nil { + return nil, fmt.Errorf("could not parse %s: %w", name, err) + } + if requireAll { + if err := validateImageCatalog(name, values); err != nil { + return nil, err + } + } + return &ImageCatalog{values: values}, nil +} + +func loadCatalogFromDisk(rpDir, name string, requireAll bool) (*ImageCatalog, error) { + path := filepath.Join(rpDir, name) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return loadCatalogBytes(name, data, requireAll) +} + +// LoadImageCatalogs loads mainnet.env (full catalog) and optional per-network +// overlays ({network}.env). Overlay keys replace mainnet; missing keys inherit. +func LoadImageCatalogs(rpDir string, networks *NetworksConfig) (mainnet *ImageCatalog, overlays map[config.Network]*ImageCatalog, err error) { + mainnet, err = loadCatalogBytes(ImagesMainnetFile, assets.ImagesMainnetEnv(), true) + if err != nil { + return nil, nil, err + } + if rpDir != "" { + disk, err := loadCatalogFromDisk(rpDir, ImagesMainnetFile, true) + if err == nil { + mainnet = disk + } else if !os.IsNotExist(err) { + return nil, nil, fmt.Errorf("could not load %s: %w", filepath.Join(rpDir, ImagesMainnetFile), err) + } + } + + overlays = map[config.Network]*ImageCatalog{} + if networks == nil { + return mainnet, overlays, nil + } + for _, n := range networks.AllNetworks() { + id := n.ID() + if id == "mainnet" { + continue + } + name := catalogFileName(id) + var overlay *ImageCatalog + if rpDir != "" { + disk, err := loadCatalogFromDisk(rpDir, name, false) + if err == nil { + overlay = disk + } else if !os.IsNotExist(err) { + return nil, nil, fmt.Errorf("could not load %s: %w", filepath.Join(rpDir, name), err) + } + } + if overlay == nil { + if data, ok := assets.EmbeddedNetworkEnv(name); ok { + parsed, err := loadCatalogBytes(name, data, false) + if err != nil { + return nil, nil, err + } + overlay = parsed + } + } + if overlay != nil { + overlays[id] = overlay + } + } + return mainnet, overlays, nil +} + +func validateImageCatalog(name string, values map[string]string) error { + var missing []string + for _, key := range requiredImageKeys { + if v, ok := values[key]; !ok || v == "" { + missing = append(missing, key) + } + } + if len(missing) > 0 { + return fmt.Errorf("%s is missing required keys: %s", name, strings.Join(missing, ", ")) + } + return nil +} + +// UpdateEnvFile sets keys in an existing env file in place, preserving comments and other lines. +func UpdateEnvFile(path string, updates map[string]string) error { + if len(updates) == 0 { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + remaining := make(map[string]string, len(updates)) + for k, v := range updates { + remaining[k] = v + } + var b strings.Builder + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + if trimmed != "" && !strings.HasPrefix(trimmed, "#") { + k, _, ok := strings.Cut(trimmed, "=") + k = strings.TrimSpace(k) + if ok { + if value, found := remaining[k]; found { + b.WriteString(k) + b.WriteByte('=') + b.WriteString(value) + b.WriteByte('\n') + delete(remaining, k) + continue + } + } + } + b.WriteString(line) + b.WriteByte('\n') + } + if err := scanner.Err(); err != nil { + return err + } + keys := make([]string, 0, len(remaining)) + for k := range remaining { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + b.WriteString(k) + b.WriteByte('=') + b.WriteString(remaining[k]) + b.WriteByte('\n') + } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, []byte(b.String()), 0644); err != nil { + return err + } + return os.Rename(tmp, path) +} + +// WriteEnvFile writes KEY=VALUE lines (sorted). +func WriteEnvFile(path string, values map[string]string) error { + var b strings.Builder + // Stable-ish: iterate a sorted key list + keys := make([]string, 0, len(values)) + for k := range values { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + b.WriteString(k) + b.WriteByte('=') + b.WriteString(values[k]) + b.WriteByte('\n') + } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, []byte(b.String()), 0644); err != nil { + return err + } + return os.Rename(tmp, path) +} + +func (cfg *RocketPoolConfig) imageMainnet(key string) string { + return cfg.imagesMainnet.Must(key) +} + +func (cfg *RocketPoolConfig) imageForNetwork(key string, network config.Network) string { + if overlay := cfg.imagesByNetwork[network]; overlay != nil { + if v, ok := overlay.Get(key); ok && v != "" { + return v + } + } + return cfg.imagesMainnet.Must(key) +} + +func (cfg *RocketPoolConfig) imageTagDefaults(key string) map[config.Network]interface{} { + defaults := map[config.Network]interface{}{config.Network_All: cfg.imageMainnet(key)} + if cfg.networks == nil { + return defaults + } + for _, n := range cfg.networks.AllNetworks() { + defaults[n.ID()] = cfg.imageForNetwork(key, n.ID()) + } + return defaults +} + +func (cfg *RocketPoolConfig) overlayFileName() string { + info := cfg.GetNetworkInfo() + if info == nil || info.Name == "" || info.Name == "mainnet" { + return "" + } + return catalogFileName(info.ID()) +} + +func (cfg *RocketPoolConfig) IsTestnet() bool { + info := cfg.GetNetworkInfo() + return info != nil && string(info.ID()) == "testnet" +} + +func (cfg *RocketPoolConfig) IsDevnet() bool { + info := cfg.GetNetworkInfo() + return info != nil && string(info.ID()) == "devnet" +} + +// TestnetOnly returns "" on testnet so a compose.tmpl line is active, otherwise "#". +func (cfg *RocketPoolConfig) TestnetOnly() string { + if cfg.IsTestnet() { + return "" + } + return "#" +} + +// DevnetOnly returns "" on devnet so a compose.tmpl line is active, otherwise "#". +func (cfg *RocketPoolConfig) DevnetOnly() string { + if cfg.IsDevnet() { + return "" + } + return "#" +} + +// ImagesEnvFiles is the on-disk catalog list (mainnet.env, then {network}.env). +// Compose interpolates runtime/image-tags.env, which is generated from these. +func (cfg *RocketPoolConfig) OverlayNetworkNames() []string { + if cfg.networks == nil { + return nil + } + var names []string + for _, n := range cfg.networks.AllNetworks() { + id := string(n.ID()) + if id == "" || id == "mainnet" || id == "testnet" || id == "devnet" { + continue + } + names = append(names, id) + } + sort.Strings(names) + return names +} + +func (cfg *RocketPoolConfig) ImagesEnvFiles() []string { + files := []string{ImagesMainnetFile} + if name := cfg.overlayFileName(); name != "" { + path := filepath.Join(cfg.RocketPoolDirectory, name) + if cfg.RocketPoolDirectory == "" { + if _, ok := assets.EmbeddedNetworkEnv(name); ok { + files = append(files, name) + } + return files + } + if _, err := os.Stat(path); err == nil { + files = append(files, name) + } else if _, ok := assets.EmbeddedNetworkEnv(name); ok { + files = append(files, name) + } + } + return files +} + +func (cfg *RocketPoolConfig) selectedECCatalogKey() (string, error) { + if !cfg.ExecutionClientLocal() { + return "", fmt.Errorf("Execution client is external, there is no container tag") + } + switch cfg.ExecutionClient.Value.(config.ExecutionClient) { + case config.ExecutionClient_Geth: + return ImageGeth, nil + case config.ExecutionClient_Nethermind: + return ImageNethermind, nil + case config.ExecutionClient_Besu: + return ImageBesu, nil + case config.ExecutionClient_Reth: + return ImageReth, nil + case config.ExecutionClient_Erigon: + return ImageErigon, nil + } + return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) +} + +func (cfg *RocketPoolConfig) selectedECTagParam() (*config.Parameter, error) { + if !cfg.ExecutionClientLocal() { + return nil, fmt.Errorf("Execution client is external, there is no container tag") + } + switch cfg.ExecutionClient.Value.(config.ExecutionClient) { + case config.ExecutionClient_Geth: + return &cfg.Geth.ContainerTag, nil + case config.ExecutionClient_Nethermind: + return &cfg.Nethermind.ContainerTag, nil + case config.ExecutionClient_Besu: + return &cfg.Besu.ContainerTag, nil + case config.ExecutionClient_Reth: + return &cfg.Reth.ContainerTag, nil + case config.ExecutionClient_Erigon: + return &cfg.Erigon.ContainerTag, nil + } + return nil, fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) +} + +// GetECImageEnvRef is ${EC_IMAGE_TAG_OVERRIDE:-${EC_IMAGE_TAG_DEFAULT}}. +func (cfg *RocketPoolConfig) GetECImageEnvRef() (string, error) { + if _, err := cfg.selectedECCatalogKey(); err != nil { + return "", err + } + return ImageTagRef(ECImageTagOverride, ECImageTagDefault), nil +} + +func (cfg *RocketPoolConfig) selectedBNCatalogKey() (string, error) { + cCfg, err := cfg.GetSelectedConsensusClientConfig() + if err != nil { + return "", err + } + switch cCfg.(type) { + case *LighthouseConfig: + return ImageLighthouse, nil + case *LodestarConfig: + return ImageLodestar, nil + case *NimbusConfig: + return ImageNimbusBn, nil + case *PrysmConfig: + return ImagePrysmBn, nil + case *TekuConfig: + return ImageTeku, nil + default: + return "", fmt.Errorf("unknown consensus client config %T", cCfg) + } +} + +// GetBeaconImageEnvRef is ${BN_IMAGE_TAG_OVERRIDE:-${BN_IMAGE_TAG_DEFAULT}}. +func (cfg *RocketPoolConfig) GetBeaconImageEnvRef() (string, error) { + if _, err := cfg.selectedBNCatalogKey(); err != nil { + return "", err + } + return ImageTagRef(BNImageTagOverride, BNImageTagDefault), nil +} + +func (cfg *RocketPoolConfig) selectedVCCatalogKey() (string, error) { + mode := cfg.ConsensusClientMode.Value.(config.Mode) + if mode == config.Mode_Local { + cCfg, err := cfg.GetSelectedConsensusClientConfig() + if err != nil { + return "", err + } + switch cCfg.(type) { + case *LighthouseConfig: + return ImageLighthouse, nil + case *LodestarConfig: + return ImageLodestar, nil + case *NimbusConfig: + return ImageNimbusVc, nil + case *PrysmConfig: + return ImagePrysmVc, nil + case *TekuConfig: + return ImageTeku, nil + default: + return "", fmt.Errorf("unknown consensus client config %T", cCfg) + } + } + client := cfg.ExternalConsensusClient.Value.(config.ConsensusClient) + switch client { + case config.ConsensusClient_Lighthouse: + return ImageLighthouse, nil + case config.ConsensusClient_Lodestar: + return ImageLodestar, nil + case config.ConsensusClient_Nimbus: + return ImageNimbusVc, nil + case config.ConsensusClient_Prysm: + return ImagePrysmVc, nil + case config.ConsensusClient_Teku: + return ImageTeku, nil + default: + return "", fmt.Errorf("unknown external consensus client [%v]", client) + } +} + +func (cfg *RocketPoolConfig) selectedVCTagParam() *config.Parameter { + mode := cfg.ConsensusClientMode.Value.(config.Mode) + if mode == config.Mode_Local { + switch cfg.ConsensusClient.Value.(config.ConsensusClient) { + case config.ConsensusClient_Lighthouse: + return &cfg.Lighthouse.ContainerTag + case config.ConsensusClient_Lodestar: + return &cfg.Lodestar.ContainerTag + case config.ConsensusClient_Nimbus: + return &cfg.Nimbus.VcContainerTag + case config.ConsensusClient_Prysm: + return &cfg.Prysm.VcContainerTag + case config.ConsensusClient_Teku: + return &cfg.Teku.ContainerTag + } + return nil + } + switch cfg.ExternalConsensusClient.Value.(config.ConsensusClient) { + case config.ConsensusClient_Lighthouse: + return &cfg.ExternalLighthouse.ContainerTag + case config.ConsensusClient_Lodestar: + return &cfg.ExternalLodestar.ContainerTag + case config.ConsensusClient_Nimbus: + return &cfg.ExternalNimbus.ContainerTag + case config.ConsensusClient_Prysm: + return &cfg.ExternalPrysm.ContainerTag + case config.ConsensusClient_Teku: + return &cfg.ExternalTeku.ContainerTag + } + return nil +} + +func (cfg *RocketPoolConfig) selectedBNTagParam() *config.Parameter { + if cfg.ConsensusClientMode.Value.(config.Mode) != config.Mode_Local { + return nil + } + switch cfg.ConsensusClient.Value.(config.ConsensusClient) { + case config.ConsensusClient_Lighthouse: + return &cfg.Lighthouse.ContainerTag + case config.ConsensusClient_Lodestar: + return &cfg.Lodestar.ContainerTag + case config.ConsensusClient_Nimbus: + return &cfg.Nimbus.BnContainerTag + case config.ConsensusClient_Prysm: + return &cfg.Prysm.BnContainerTag + case config.ConsensusClient_Teku: + return &cfg.Teku.ContainerTag + } + return nil +} + +// GetVCImageEnvRef is ${VC_IMAGE_TAG_OVERRIDE:-${VC_IMAGE_TAG_DEFAULT}}. +func (cfg *RocketPoolConfig) GetVCImageEnvRef() (string, error) { + if _, err := cfg.selectedVCCatalogKey(); err != nil { + return "", err + } + return ImageTagRef(VCImageTagOverride, VCImageTagDefault), nil +} + +func (cfg *RocketPoolConfig) GetMevBoostImageEnvRef() string { + return ImageTagRef(MevBoostImageTagOverride, MevBoostImageTagDefault) +} + +func (cfg *RocketPoolConfig) GetCommitBoostImageEnvRef() string { + return ImageTagRef(CommitBoostImageTagOverride, CommitBoostImageTagDefault) +} + +func (cfg *RocketPoolConfig) tuiOverride(param *config.Parameter) (string, bool) { + if param == nil || param.Value == nil { + return "", false + } + value := fmt.Sprint(param.Value) + if value == "" { + return "", false + } + def, err := param.GetDefault(cfg.GetNetwork()) + if err != nil || value == fmt.Sprint(def) { + return "", false + } + return value, true +} + +// ComposeImageDefaults maps service-level *_IMAGE_TAG_DEFAULT vars to the +// catalog pin for the selected client/network. +func (cfg *RocketPoolConfig) ComposeImageDefaults() map[string]string { + out := map[string]string{ + SmartnodeImageTagDefault: cfg.ResolvedImage(ImageSmartnode), + PrometheusImageTagDefault: cfg.ResolvedImage(ImagePrometheus), + GrafanaImageTagDefault: cfg.ResolvedImage(ImageGrafana), + ExporterImageTagDefault: cfg.ResolvedImage(ImageExporter), + AlertmanagerImageTagDefault: cfg.ResolvedImage(ImageAlertmanager), + GWWImageTagDefault: cfg.ResolvedImage(ImageGWW), + CurlImageTagDefault: cfg.ResolvedImage(ImageCurl), + MevBoostImageTagDefault: cfg.ResolvedImage(ImageMevBoost), + CommitBoostImageTagDefault: cfg.ResolvedImage(ImageCommitBoost), + } + if key, err := cfg.selectedECCatalogKey(); err == nil { + out[ECImageTagDefault] = cfg.ResolvedImage(key) + } + if key, err := cfg.selectedBNCatalogKey(); err == nil { + out[BNImageTagDefault] = cfg.ResolvedImage(key) + } + if key, err := cfg.selectedVCCatalogKey(); err == nil { + out[VCImageTagDefault] = cfg.ResolvedImage(key) + } + return out +} + +// ComposeEnvOverrides returns *_IMAGE_TAG_OVERRIDE vars for TUI values that +// differ from the catalog pin. Unset override vars fall back to *_DEFAULT. +func (cfg *RocketPoolConfig) ComposeEnvOverrides() map[string]string { + out := map[string]string{} + if param, err := cfg.selectedECTagParam(); err == nil { + if v, ok := cfg.tuiOverride(param); ok { + out[ECImageTagOverride] = v + } + } + if v, ok := cfg.tuiOverride(cfg.selectedBNTagParam()); ok { + out[BNImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(cfg.selectedVCTagParam()); ok { + out[VCImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.MevBoost.ContainerTag); ok { + out[MevBoostImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.CommitBoost.ContainerTag); ok { + out[CommitBoostImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.Prometheus.ContainerTag); ok { + out[PrometheusImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.Grafana.ContainerTag); ok { + out[GrafanaImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.Exporter.ContainerTag); ok { + out[ExporterImageTagOverride] = v + } + if v, ok := cfg.tuiOverride(&cfg.Alertmanager.ContainerTag); ok { + out[AlertmanagerImageTagOverride] = v + } + gww := cfg.GraffitiWallWriter.GetConfig().GetParameters() + for _, p := range gww { + if p.ID == "containerTag" { + if v, ok := cfg.tuiOverride(p); ok { + out[GWWImageTagOverride] = v + } + break + } + } + return out +} + +func validateEnvAssignment(key, value string) error { + if strings.ContainsAny(key, " \t\n\r") { + return fmt.Errorf("image env name %q contains whitespace", key) + } + if strings.ContainsAny(value, " \t\n\r") { + return fmt.Errorf("image env %s value %q contains whitespace", key, value) + } + return nil +} + +// ComposeImageEnv is the single env map Compose interpolates: service +// *_DEFAULT pins (mainnet + network overlay) plus *_OVERRIDE when the TUI +// customized a tag. +func (cfg *RocketPoolConfig) ComposeImageEnv() (map[string]string, error) { + out := cfg.ComposeImageDefaults() + for key, value := range cfg.ComposeEnvOverrides() { + out[key] = value + } + for key, value := range out { + if err := validateEnvAssignment(key, value); err != nil { + return nil, err + } + } + return out, nil +} + +// ComposeEnvAssignments returns KEY=value strings for Compose interpolation: +// *_IMAGE_TAG_DEFAULT from the catalog and *_IMAGE_TAG_OVERRIDE when the TUI +// customized a tag. Keys are sorted so command logs and diffs are reproducible. +func (cfg *RocketPoolConfig) ComposeEnvAssignments() ([]string, error) { + env, err := cfg.ComposeImageEnv() + if err != nil { + return nil, err + } + keys := make([]string, 0, len(env)) + for key := range env { + keys = append(keys, key) + } + sort.Strings(keys) + out := make([]string, 0, len(keys)) + for _, key := range keys { + out = append(out, key+"="+env[key]) + } + return out, nil +} + +func (cfg *RocketPoolConfig) resolveParamImage(param *config.Parameter, key string) string { + if param == nil { + return "" + } + value := fmt.Sprint(param.Value) + if cfg.Smartnode != nil && cfg.Smartnode.Network.Value != nil { + network := cfg.Smartnode.Network.Value.(config.Network) + if def, err := param.GetDefault(network); err == nil && value != fmt.Sprint(def) { + return value + } + } + if v := cfg.ResolvedImage(key); v != "" { + return v + } + return value +} + +func (cfg *RocketPoolConfig) imageFromEnvRef(ref, tuiValue string) string { + key := strings.TrimSuffix(strings.TrimPrefix(ref, "${"), "}") + official := cfg.ResolvedImage(key) + if tuiValue != "" && official != "" && tuiValue != official { + return tuiValue + } + if official != "" { + return official + } + return tuiValue +} + +func (cfg *RocketPoolConfig) ResolvedImage(key string) string { + network := config.Network_Unknown + if cfg.Smartnode != nil && cfg.Smartnode.Network.Value != nil { + network = cfg.Smartnode.Network.Value.(config.Network) + } + if overlay := cfg.imagesByNetwork[network]; overlay != nil { + if v, ok := overlay.Get(key); ok && v != "" { + return v + } + } + if cfg.imagesMainnet != nil { + if v, ok := cfg.imagesMainnet.Get(key); ok && v != "" { + return v + } + } + return "" +} diff --git a/shared/services/config/images_test.go b/shared/services/config/images_test.go new file mode 100644 index 000000000..0df3fa43e --- /dev/null +++ b/shared/services/config/images_test.go @@ -0,0 +1,266 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/rocket-pool/smartnode/shared/types/config" +) + +func TestParseEnvFile(t *testing.T) { + data := []byte(` +# comment +RP_IMAGE_GETH=ethereum/client-go:v1.17.5@sha256:abc + +RP_IMAGE_CURL="curlimages/curl:8.13.0" +RP_IMAGE_ALPINE='alpine:3.21.3' +`) + got, err := ParseEnvFile(data) + if err != nil { + t.Fatal(err) + } + if got["RP_IMAGE_GETH"] != "ethereum/client-go:v1.17.5@sha256:abc" { + t.Fatalf("geth: %q", got["RP_IMAGE_GETH"]) + } + if got["RP_IMAGE_CURL"] != "curlimages/curl:8.13.0" { + t.Fatalf("curl: %q", got["RP_IMAGE_CURL"]) + } + if got["RP_IMAGE_ALPINE"] != "alpine:3.21.3" { + t.Fatalf("alpine: %q", got["RP_IMAGE_ALPINE"]) + } +} + +func TestParseEnvFileRejectsBadLine(t *testing.T) { + if _, err := ParseEnvFile([]byte("not-a-key-value")); err == nil { + t.Fatal("expected error") + } +} + +func TestLoadImageCatalogsEmbedded(t *testing.T) { + mainnet, overlays, err := LoadImageCatalogs("", nil) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(mainnet.Must(ImageGeth), "ethereum/client-go:") { + t.Fatalf("unexpected geth image %s", mainnet.Must(ImageGeth)) + } + if len(overlays) != 0 { + t.Fatalf("expected no overlays without networks, got %v", overlays) + } +} + +func TestLoadImageCatalogsPrefersDisk(t *testing.T) { + dir := t.TempDir() + mainnet, _, err := LoadImageCatalogs("", nil) + if err != nil { + t.Fatal(err) + } + values := mainnet.Map() + values[ImageGeth] = "example.com/geth:custom" + if err := WriteEnvFile(filepath.Join(dir, ImagesMainnetFile), values); err != nil { + t.Fatal(err) + } + loaded, _, err := LoadImageCatalogs(dir, nil) + if err != nil { + t.Fatal(err) + } + if loaded.Must(ImageGeth) != "example.com/geth:custom" { + t.Fatalf("got %s", loaded.Must(ImageGeth)) + } +} + +func TestNetworkOverlayOverridesMainnet(t *testing.T) { + dir := t.TempDir() + mainnet, _, err := LoadImageCatalogs("", nil) + if err != nil { + t.Fatal(err) + } + if err := WriteEnvFile(filepath.Join(dir, ImagesMainnetFile), mainnet.Map()); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, ImagesTestnetFile), []byte("RP_IMAGE_GETH=example.com/geth:testnet\n"), 0644); err != nil { + t.Fatal(err) + } + networks, err := LoadNetworks("") + if err != nil { + t.Fatal(err) + } + cfg := mustNewRocketPoolConfig(t, dir, false) + cfg.networks = networks + cfg.Smartnode.Network.Value = config.Network("testnet") + if got := cfg.imageForNetwork(ImageGeth, config.Network("testnet")); got != "example.com/geth:testnet" { + t.Fatalf("testnet geth %s", got) + } + if got := cfg.imageForNetwork(ImageLighthouse, config.Network("testnet")); got != mainnet.Must(ImageLighthouse) { + t.Fatalf("testnet lighthouse should inherit mainnet, got %s", got) + } + if got := cfg.imageForNetwork(ImageGeth, config.Network("mainnet")); got != mainnet.Must(ImageGeth) { + t.Fatalf("mainnet geth should be unchanged, got %s", got) + } +} + +func TestUpdateEnvFilePreservesComments(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, ImagesMainnetFile) + if err := os.WriteFile(path, []byte("# comment\nRP_IMAGE_GETH=old/geth:1\nRP_IMAGE_CURL=keep/curl:1\n"), 0644); err != nil { + t.Fatal(err) + } + if err := UpdateEnvFile(path, map[string]string{ImageGeth: "my/geth:custom"}); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + text := string(data) + if !strings.Contains(text, "# comment") { + t.Fatalf("lost comment: %s", text) + } + parsed, err := ParseEnvFile(data) + if err != nil { + t.Fatal(err) + } + if parsed[ImageGeth] != "my/geth:custom" { + t.Fatalf("geth %q", parsed[ImageGeth]) + } + if parsed[ImageCurl] != "keep/curl:1" { + t.Fatalf("curl %q", parsed[ImageCurl]) + } +} + +func TestClientImageEnvRefs(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.ExecutionClientMode.Value = config.Mode_Local + cfg.ExecutionClient.Value = config.ExecutionClient_Geth + cfg.ConsensusClientMode.Value = config.Mode_Local + cfg.ConsensusClient.Value = config.ConsensusClient_Lighthouse + cfg.Smartnode.Network.Value = config.Network("mainnet") + + ref, err := cfg.GetECImageEnvRef() + if err != nil { + t.Fatal(err) + } + if ref != ImageTagRef(ECImageTagOverride, ECImageTagDefault) { + t.Fatalf("ec ref %s", ref) + } + bn, err := cfg.GetBeaconImageEnvRef() + if err != nil { + t.Fatal(err) + } + if bn != ImageTagRef(BNImageTagOverride, BNImageTagDefault) { + t.Fatalf("bn ref %s", bn) + } + vc, err := cfg.GetVCImageEnvRef() + if err != nil { + t.Fatal(err) + } + if vc != ImageTagRef(VCImageTagOverride, VCImageTagDefault) { + t.Fatalf("vc ref %s", vc) + } + + cfg.ChangeNetwork(config.Network("testnet")) + ref, err = cfg.GetECImageEnvRef() + if err != nil { + t.Fatal(err) + } + if ref != ImageTagRef(ECImageTagOverride, ECImageTagDefault) { + t.Fatalf("ec test ref %s", ref) + } + if !cfg.IsTestnet() { + t.Fatal("expected IsTestnet") + } + if cfg.TestnetOnly() != "" { + t.Fatalf("TestnetOnly on testnet should be empty, got %q", cfg.TestnetOnly()) + } + files := cfg.ImagesEnvFiles() + if len(files) < 1 || files[0] != ImagesMainnetFile { + t.Fatalf("expected mainnet.env first, got %v", files) + } +} + +func TestComposeEnvOverrides(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.Smartnode.Network.Value = config.Network("mainnet") + if err := cfg.Geth.ContainerTag.SetToDefault(config.Network("mainnet")); err != nil { + t.Fatal(err) + } + if len(cfg.ComposeEnvOverrides()) != 0 { + t.Fatalf("expected no overrides, got %v", cfg.ComposeEnvOverrides()) + } + cfg.Geth.ContainerTag.Value = "my/geth:custom" + overrides := cfg.ComposeEnvOverrides() + if overrides[ECImageTagOverride] != "my/geth:custom" { + t.Fatalf("got %v", overrides) + } + defaults := cfg.ComposeImageDefaults() + if defaults[ECImageTagDefault] == "" || defaults[ECImageTagDefault] == "my/geth:custom" { + t.Fatalf("default should stay the catalog pin, got %q", defaults[ECImageTagDefault]) + } + env, err := cfg.ComposeImageEnv() + if err != nil { + t.Fatal(err) + } + if env[ECImageTagDefault] != defaults[ECImageTagDefault] { + t.Fatalf("merged default %q", env[ECImageTagDefault]) + } + if env[ECImageTagOverride] != "my/geth:custom" { + t.Fatalf("merged override %q", env[ECImageTagOverride]) + } +} + +func TestComposeEnvAssignmentsSorted(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.Smartnode.Network.Value = config.Network("mainnet") + cfg.ExecutionClientMode.Value = config.Mode_Local + cfg.ExecutionClient.Value = config.ExecutionClient_Geth + cfg.Geth.ContainerTag.Value = "my/geth:custom" + cfg.Prometheus.ContainerTag.Value = "my/prom:custom" + got, err := cfg.ComposeEnvAssignments() + if err != nil { + t.Fatal(err) + } + if len(got) < 2 { + t.Fatalf("expected multiple assignments, got %v", got) + } + for i := 1; i < len(got); i++ { + if got[i-1] >= got[i] { + t.Fatalf("assignments not sorted: %v", got) + } + } +} + +func TestComposeEnvAssignmentsRejectsWhitespace(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.Smartnode.Network.Value = config.Network("mainnet") + cfg.ExecutionClientMode.Value = config.Mode_Local + cfg.ExecutionClient.Value = config.ExecutionClient_Geth + cfg.Geth.ContainerTag.Value = "my/geth custom" + if _, err := cfg.ComposeEnvAssignments(); err == nil { + t.Fatal("expected error for whitespace in override value") + } +} + +func TestTestnetOnlyComment(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.Smartnode.Network.Value = config.Network("mainnet") + if cfg.IsTestnet() { + t.Fatal("mainnet should not be testnet") + } + if cfg.TestnetOnly() != "#" { + t.Fatalf("TestnetOnly on mainnet should be #, got %q", cfg.TestnetOnly()) + } + if cfg.DevnetOnly() != "#" { + t.Fatalf("DevnetOnly on mainnet should be #, got %q", cfg.DevnetOnly()) + } +} + +func TestContainerTagsAreSerialized(t *testing.T) { + cfg := mustNewRocketPoolConfig(t, "", false) + cfg.Geth.ContainerTag.Value = "my/geth:custom" + serialized := cfg.Serialize() + if tag, ok := serialized["geth"]["containerTag"]; !ok || tag != "my/geth:custom" { + t.Fatalf("container tags should be saved to user-settings.yml, got %v", serialized["geth"]["containerTag"]) + } +} diff --git a/shared/services/config/lighthouse-config.go b/shared/services/config/lighthouse-config.go index 73ec65a6f..50577f756 100644 --- a/shared/services/config/lighthouse-config.go +++ b/shared/services/config/lighthouse-config.go @@ -5,9 +5,7 @@ import ( ) const ( - lighthouseTagPortableTest string = "sigp/lighthouse:v8.2.2" - lighthouseTagPortableProd string = "sigp/lighthouse:v8.2.2" - defaultLhMaxPeers uint16 = 100 + defaultLhMaxPeers uint16 = 100 ) // Configuration for Lighthouse @@ -67,7 +65,7 @@ func NewLighthouseConfig(cfg *RocketPoolConfig) *LighthouseConfig { Name: "Container Tag", Description: "The tag name of the Lighthouse container you want to use from Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, lighthouseTagPortableProd, lighthouseTagPortableTest), + Default: cfg.imageTagDefaults(ImageLighthouse), AffectsContainers: []config.ContainerID{config.ContainerID_Eth2, config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/lodestar-config.go b/shared/services/config/lodestar-config.go index cd0856047..aa736f35e 100644 --- a/shared/services/config/lodestar-config.go +++ b/shared/services/config/lodestar-config.go @@ -5,8 +5,6 @@ import ( ) const ( - lodestarTagTest string = "chainsafe/lodestar:v1.48.0" - lodestarTagProd string = "chainsafe/lodestar:v1.48.0" defaultLodestarMaxPeers uint16 = 200 ) @@ -68,7 +66,7 @@ func NewLodestarConfig(cfg *RocketPoolConfig) *LodestarConfig { Name: "Container Tag", Description: "The tag name of the Lodestar container you want to use from Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, lodestarTagProd, lodestarTagTest), + Default: cfg.imageTagDefaults(ImageLodestar), AffectsContainers: []config.ContainerID{config.ContainerID_Eth2, config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/mev-boost-config.go b/shared/services/config/mev-boost-config.go index 0993f176c..f305e2696 100644 --- a/shared/services/config/mev-boost-config.go +++ b/shared/services/config/mev-boost-config.go @@ -9,8 +9,6 @@ import ( // Constants const ( - mevBoostTagProd string = "flashbots/mev-boost:1.12.0" - mevBoostTagTest string = "flashbots/mev-boost:1.12.0" mevDocsUrl string = "https://docs.rocketpool.net/node-staking/mev" RegulatedRelayDescription string = "Select this to enable the relays that comply with government regulations (e.g. OFAC sanctions), " UnregulatedRelayDescription string = "Select this to enable the relays that do not follow any sanctions lists (do not censor transactions), " @@ -179,7 +177,7 @@ func NewMevBoostConfig(cfg *RocketPoolConfig) *MevBoostConfig { Name: "Container Tag", Description: "The tag name of the MEV-Boost container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, mevBoostTagProd, mevBoostTagTest), + Default: cfg.imageTagDefaults(ImageMevBoost), AffectsContainers: []config.ContainerID{config.ContainerID_MevBoost}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/nethermind-params.go b/shared/services/config/nethermind-config.go similarity index 97% rename from shared/services/config/nethermind-params.go rename to shared/services/config/nethermind-config.go index 28c52abc3..1bf6bd8d2 100644 --- a/shared/services/config/nethermind-params.go +++ b/shared/services/config/nethermind-config.go @@ -10,8 +10,6 @@ import ( // Constants const ( - nethermindTagProd string = "nethermind/nethermind:1.39.3" - nethermindTagTest string = "nethermind/nethermind:1.39.3" nethermindEventLogInterval int = 1000 nethermindStopSignal string = "SIGTERM" ) @@ -170,7 +168,7 @@ func NewNethermindConfig(cfg *RocketPoolConfig) *NethermindConfig { Name: "Container Tag", Description: "The tag name of the Nethermind container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, nethermindTagProd, nethermindTagTest), + Default: cfg.imageTagDefaults(ImageNethermind), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/networks-config.go b/shared/services/config/networks-config.go index ccf420770..bc046cb4b 100644 --- a/shared/services/config/networks-config.go +++ b/shared/services/config/networks-config.go @@ -244,21 +244,6 @@ func hexAddresses(values []string) []common.Address { return out } -func clientTagDefaults(networks *NetworksConfig, prod, test string) map[config.Network]interface{} { - defaults := map[config.Network]interface{}{config.Network_All: test} - if networks == nil { - return defaults - } - for _, n := range networks.AllNetworks() { - if n.ClientTagSet == config.ClientTagSetProduction { - defaults[n.ID()] = prod - } else { - defaults[n.ID()] = test - } - } - return defaults -} - func nethermindPruneThresholdDefaults(networks *NetworksConfig) map[config.Network]interface{} { defaults := map[config.Network]interface{}{config.Network_All: uint64(51200)} if networks == nil { diff --git a/shared/services/config/networks-config_test.go b/shared/services/config/networks-config_test.go index c887c493a..4497d92b8 100644 --- a/shared/services/config/networks-config_test.go +++ b/shared/services/config/networks-config_test.go @@ -185,7 +185,7 @@ func TestOfficialGetterLock(t *testing.T) { if cfg.Smartnode.GetChainID() != 1 { t.Fatalf("mainnet chainID %d", cfg.Smartnode.GetChainID()) } - if tag, _ := cfg.Geth.ContainerTag.GetDefault(config.Network("mainnet")); tag != gethTagProd { + if tag, _ := cfg.Geth.ContainerTag.GetDefault(config.Network("mainnet")); tag != cfg.imageMainnet(ImageGeth) { t.Fatalf("mainnet geth tag %v", tag) } diff --git a/shared/services/config/nimbus-config.go b/shared/services/config/nimbus-config.go index a4539795a..81c5680f5 100644 --- a/shared/services/config/nimbus-config.go +++ b/shared/services/config/nimbus-config.go @@ -7,14 +7,6 @@ import ( ) const ( - // Testnet - nimbusBnTagTest string = "statusim/nimbus-eth2:multiarch-v26.8.0" - nimbusVcTagTest string = "statusim/nimbus-validator-client:multiarch-v26.8.0" - - // Mainnet - nimbusBnTagProd string = "statusim/nimbus-eth2:multiarch-v26.8.0" - nimbusVcTagProd string = "statusim/nimbus-validator-client:multiarch-v26.8.0" - defaultNimbusMaxPeersArm uint16 = 100 defaultNimbusMaxPeersAmd uint16 = 160 ) @@ -83,7 +75,7 @@ func NewNimbusConfig(cfg *RocketPoolConfig) *NimbusConfig { Name: "Beacon Node Container Tag", Description: "The tag name of the Nimbus Beacon Node container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, nimbusBnTagProd, nimbusBnTagTest), + Default: cfg.imageTagDefaults(ImageNimbusBn), AffectsContainers: []config.ContainerID{config.ContainerID_Eth2}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -94,7 +86,7 @@ func NewNimbusConfig(cfg *RocketPoolConfig) *NimbusConfig { Name: "Validator Client Container Tag", Description: "The tag name of the Nimbus Validator Client container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, nimbusVcTagProd, nimbusVcTagTest), + Default: cfg.imageTagDefaults(ImageNimbusVc), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/prometheus-config.go b/shared/services/config/prometheus-config.go index a33401822..18cfde535 100644 --- a/shared/services/config/prometheus-config.go +++ b/shared/services/config/prometheus-config.go @@ -4,9 +4,6 @@ import ( "github.com/rocket-pool/smartnode/shared/types/config" ) -// Constants -const prometheusTag string = "prom/prometheus:v3.14.0" - // Defaults const defaultPrometheusPort uint16 = 9091 const defaultPrometheusOpenPort string = string(config.RPC_Closed) @@ -62,7 +59,7 @@ func NewPrometheusConfig(cfg *RocketPoolConfig) *PrometheusConfig { Name: "Prometheus Container Tag", Description: "The tag name of the Prometheus container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: map[config.Network]interface{}{config.Network_All: prometheusTag}, + Default: map[config.Network]interface{}{config.Network_All: cfg.imageMainnet(ImagePrometheus)}, AffectsContainers: []config.ContainerID{config.ContainerID_Prometheus}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/prysm-config.go b/shared/services/config/prysm-config.go index ee47078ad..e9259f94d 100644 --- a/shared/services/config/prysm-config.go +++ b/shared/services/config/prysm-config.go @@ -5,10 +5,6 @@ import ( ) const ( - prysmBnTest string = "gcr.io/offchainlabs/prysm/beacon-chain:v7.1.8" - prysmBnProd string = "gcr.io/offchainlabs/prysm/beacon-chain:v7.1.8" - prysmVcTest string = "gcr.io/offchainlabs/prysm/validator:v7.1.8" - prysmVcProd string = "gcr.io/offchainlabs/prysm/validator:v7.1.8" defaultPrysmRpcPort uint16 = 5053 defaultPrysmOpenRpcPort string = string(config.RPC_Closed) defaultPrysmMaxPeers uint16 = 70 @@ -108,7 +104,7 @@ func NewPrysmConfig(cfg *RocketPoolConfig) *PrysmConfig { Name: "Beacon Node Container Tag", Description: "The tag name of the Prysm Beacon Node container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, prysmBnProd, prysmBnTest), + Default: cfg.imageTagDefaults(ImagePrysmBn), AffectsContainers: []config.ContainerID{config.ContainerID_Eth2}, CanBeBlank: false, OverwriteOnUpgrade: true, @@ -119,7 +115,7 @@ func NewPrysmConfig(cfg *RocketPoolConfig) *PrysmConfig { Name: "Validator Client Container Tag", Description: "The tag name of the Prysm Validator Client container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, prysmVcProd, prysmVcTest), + Default: cfg.imageTagDefaults(ImagePrysmVc), AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/reth-params.go b/shared/services/config/reth-config.go similarity index 96% rename from shared/services/config/reth-params.go rename to shared/services/config/reth-config.go index 2645a51d6..4974b050e 100644 --- a/shared/services/config/reth-params.go +++ b/shared/services/config/reth-config.go @@ -10,8 +10,6 @@ import ( // Constants const ( - rethTagProd string = "ghcr.io/paradigmxyz/reth:v2.5.2" - rethTagTest string = "ghcr.io/paradigmxyz/reth:v2.5.2" rethEventLogInterval int = 1000 rethStopSignal string = "SIGTERM" ) @@ -100,7 +98,7 @@ func NewRethConfig(cfg *RocketPoolConfig) *RethConfig { Name: "Container Tag", Description: "The tag name of the Reth container you want to use.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, rethTagProd, rethTagTest), + Default: cfg.imageTagDefaults(ImageReth), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index 90693c37f..9b7a783e6 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -67,7 +67,9 @@ type RocketPoolConfig struct { // IsCLI is true when this config was loaded by the rocketpool CLI (or TUI) not by the node daemon. IsCLI bool `yaml:"-"` - networks *NetworksConfig `yaml:"-"` + networks *NetworksConfig `yaml:"-"` + imagesMainnet *ImageCatalog `yaml:"-"` + imagesByNetwork map[config.Network]*ImageCatalog `yaml:"-"` // Execution client settings ExecutionClientMode config.Parameter `yaml:"executionClientMode,omitempty"` @@ -273,10 +275,14 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) (*RocketPoolConfig, er if err != nil { return nil, fmt.Errorf("could not load networks: %w", err) } - return newRocketPoolConfig(rpDir, isNativeMode, networks) + mainnet, overlays, err := LoadImageCatalogs(rpDir, networks) + if err != nil { + return nil, fmt.Errorf("could not load images: %w", err) + } + return newRocketPoolConfig(rpDir, isNativeMode, networks, mainnet, overlays) } -func newRocketPoolConfig(rpDir string, isNativeMode bool, networks *NetworksConfig) (*RocketPoolConfig, error) { +func newRocketPoolConfig(rpDir string, isNativeMode bool, networks *NetworksConfig, mainnet *ImageCatalog, overlays map[config.Network]*ImageCatalog) (*RocketPoolConfig, error) { clientModes := []config.ParameterOption{{ Name: "Locally Managed", @@ -293,6 +299,8 @@ func newRocketPoolConfig(rpDir string, isNativeMode bool, networks *NetworksConf RocketPoolDirectory: rpDir, IsNativeMode: isNativeMode, networks: networks, + imagesMainnet: mainnet, + imagesByNetwork: overlays, ExecutionClientMode: config.Parameter{ ID: "executionClientMode", @@ -614,7 +622,7 @@ func newRocketPoolConfig(rpDir string, isNativeMode bool, networks *NetworksConf cfg.MevBoost = NewMevBoostConfig(cfg) cfg.CommitBoost = NewCommitBoostConfig(cfg) // Addons - cfg.GraffitiWallWriter = addons.NewGraffitiWallWriter() + cfg.GraffitiWallWriter = addons.NewGraffitiWallWriter(mainnet.Must(ImageGWW)) cfg.RescueNode = addons.NewRescueNode() // Apply the default values for the default network from YAML @@ -649,7 +657,7 @@ func getAugmentedEcDescription(client config.ExecutionClient, originalDescriptio // Create a copy of this configuration. func (cfg *RocketPoolConfig) CreateCopy() *RocketPoolConfig { - newConfig, err := newRocketPoolConfig(cfg.RocketPoolDirectory, cfg.IsNativeMode, cfg.networks) + newConfig, err := newRocketPoolConfig(cfg.RocketPoolDirectory, cfg.IsNativeMode, cfg.networks, cfg.imagesMainnet, cfg.imagesByNetwork) if err != nil { panic(err) } @@ -1039,8 +1047,11 @@ func (cfg *RocketPoolConfig) GetVCContainerTag() (string, error) { if err != nil { return "", err } - - return cCfg.GetValidatorImage(), nil + ref, err := cfg.GetVCImageEnvRef() + if err != nil { + return cCfg.GetValidatorImage(), nil + } + return cfg.imageFromEnvRef(ref, cCfg.GetValidatorImage()), nil } // Used by text/template to format validator.yml @@ -1359,15 +1370,15 @@ func (cfg *RocketPoolConfig) GetECContainerTag() (string, error) { switch cfg.ExecutionClient.Value.(config.ExecutionClient) { case config.ExecutionClient_Geth: - return cfg.Geth.ContainerTag.Value.(string), nil + return cfg.resolveParamImage(&cfg.Geth.ContainerTag, ImageGeth), nil case config.ExecutionClient_Nethermind: - return cfg.Nethermind.ContainerTag.Value.(string), nil + return cfg.resolveParamImage(&cfg.Nethermind.ContainerTag, ImageNethermind), nil case config.ExecutionClient_Besu: - return cfg.Besu.ContainerTag.Value.(string), nil + return cfg.resolveParamImage(&cfg.Besu.ContainerTag, ImageBesu), nil case config.ExecutionClient_Reth: - return cfg.Reth.ContainerTag.Value.(string), nil + return cfg.resolveParamImage(&cfg.Reth.ContainerTag, ImageReth), nil case config.ExecutionClient_Erigon: - return cfg.Erigon.ContainerTag.Value.(string), nil + return cfg.resolveParamImage(&cfg.Erigon.ContainerTag, ImageErigon), nil } return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) @@ -1542,8 +1553,11 @@ func (cfg *RocketPoolConfig) GetBeaconContainerTag() (string, error) { if err != nil { return "", err } - - return cCfg.GetBeaconNodeImage(), nil + ref, err := cfg.GetBeaconImageEnvRef() + if err != nil { + return cCfg.GetBeaconNodeImage(), nil + } + return cfg.imageFromEnvRef(ref, cCfg.GetBeaconNodeImage()), nil } // Used by text/template to format eth2.yml diff --git a/shared/services/config/smartnode-config.go b/shared/services/config/smartnode-config.go index 57a81f7a4..8ea120d4a 100644 --- a/shared/services/config/smartnode-config.go +++ b/shared/services/config/smartnode-config.go @@ -475,6 +475,11 @@ func (cfg *SmartnodeConfig) GetRplTokenAddress() string { } func (cfg *SmartnodeConfig) GetSmartnodeContainerTag() string { + if cfg.parent != nil { + if tag := cfg.parent.ResolvedImage(ImageSmartnode); tag != "" { + return tag + } + } return smartnodeTagPrefix + shared.RocketPoolVersion() } diff --git a/shared/services/config/teku-config.go b/shared/services/config/teku-config.go index 3eae6eaae..9d696f845 100644 --- a/shared/services/config/teku-config.go +++ b/shared/services/config/teku-config.go @@ -7,8 +7,6 @@ import ( ) const ( - tekuTagTest string = "consensys/teku:26.8.0" - tekuTagProd string = "consensys/teku:26.8.0" defaultTekuMaxPeers uint16 = 100 defaultTekuP2pIpv6Port uint16 = 9090 defaultTekuP2pQuicIpv6Port uint16 = 8002 @@ -100,7 +98,7 @@ func NewTekuConfig(cfg *RocketPoolConfig) *TekuConfig { Name: "Container Tag", Description: "The tag name of the Teku container you want to use on Docker Hub.", Type: config.ParameterType_String, - Default: clientTagDefaults(cfg.networks, tekuTagProd, tekuTagTest), + Default: cfg.imageTagDefaults(ImageTeku), AffectsContainers: []config.ContainerID{config.ContainerID_Eth2, config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: true, diff --git a/shared/services/rocketpool/assets/assets.go b/shared/services/rocketpool/assets/assets.go index f5c4016e0..e12ad64d4 100644 --- a/shared/services/rocketpool/assets/assets.go +++ b/shared/services/rocketpool/assets/assets.go @@ -38,6 +38,30 @@ func NetworksDefaultYAML() []byte { return bytes } +func ImagesMainnetEnv() []byte { + bytes, err := installFS.ReadFile("install/mainnet.env") + if err != nil { + panic("embedded install/mainnet.env is missing: " + err.Error()) + } + return bytes +} + +func ImagesTestnetEnv() []byte { + bytes, ok := EmbeddedNetworkEnv("testnet.env") + if !ok { + panic("embedded install/testnet.env is missing") + } + return bytes +} + +func EmbeddedNetworkEnv(name string) ([]byte, bool) { + bytes, err := installFS.ReadFile("install/" + name) + if err != nil { + return nil, false + } + return bytes, true +} + func InstallUpdateTrackerScript() ScriptWithContext { return ScriptWithContext{Script: installUpdateTrackerScript, Context: rpUpdateTrackerFS} } diff --git a/shared/services/rocketpool/assets/install/devnet.env b/shared/services/rocketpool/assets/install/devnet.env new file mode 100644 index 000000000..407736be2 --- /dev/null +++ b/shared/services/rocketpool/assets/install/devnet.env @@ -0,0 +1,6 @@ +# Optional image overrides for the devnet network. +# Uncomment or add KEY=value lines to override mainnet.env for this network only. +# Keys not listed here inherit the mainnet pin. +# +# Example: +# RP_IMAGE_GETH=ethereum/client-go:v1.17.4@sha256:... diff --git a/shared/services/rocketpool/assets/install/mainnet.env b/shared/services/rocketpool/assets/install/mainnet.env new file mode 100644 index 000000000..5ca1f6d69 --- /dev/null +++ b/shared/services/rocketpool/assets/install/mainnet.env @@ -0,0 +1,32 @@ +# Default Smart Node image pins (used on every network). +# Do not edit this file; `rocketpool service install` replaces it. +# Per-network overrides go in {network}.env (testnet.env, devnet.env, +# plataberget.env, ...): only keys present there replace these defaults. +# TUI container-tag changes are set in the Compose process environment and win over both. + +RP_IMAGE_SMARTNODE=rocketpool/smartnode:v1.24.0@sha256:8a6b3e35da71bdbfae5fa396bb817e63e95be44e44d3e18fb961dd4848d5fb19 + +RP_IMAGE_GETH=ethereum/client-go:v1.17.5@sha256:523d3ba26623a619e912019068dc2784f02934070ac46bdae4d5b9df0d917814 +RP_IMAGE_NETHERMIND=nethermind/nethermind:1.39.3@sha256:1b6b01419de4ff75ed3d61995904bccc2fdcc2865fee6dae07d88c14a0758e40 +RP_IMAGE_BESU=hyperledger/besu:26.8.1@sha256:6f3f21ce533383fcc8db3bce02252b59d5a9e776b72b5a1c8ecd2db011600042 +RP_IMAGE_RETH=ghcr.io/paradigmxyz/reth:v2.5.2@sha256:68e76b32ad9ac1a3de54984d6f15bbce2047a92a771ff4efbcd08ee8f9dbd4d1 +RP_IMAGE_ERIGON=erigontech/erigon:v3.6.1@sha256:8cd3fbcbb35d8b16768225ff7494a0672b7b06c9bf13d466850aabb920ac0de7 + +RP_IMAGE_LIGHTHOUSE=sigp/lighthouse:v8.2.2@sha256:9a62bb8705455136e1cf96613460960f80dc2faa9d75b5c16a3bfcc9210dcdd1 +RP_IMAGE_LODESTAR=chainsafe/lodestar:v1.48.0@sha256:d717e4193699a67c1ee38568652827e4d33d243cabb51d5137938721aefd2886 +RP_IMAGE_NIMBUS_BN=statusim/nimbus-eth2:multiarch-v26.8.0@sha256:6ccd9d382885115d9d3b6d9c565e3da38caf1bdea3c9e9cadab0e879eca3b769 +RP_IMAGE_NIMBUS_VC=statusim/nimbus-validator-client:multiarch-v26.8.0@sha256:2a8af2c1c1be69cd47426130780ca2027cafea43363e6485372ab9ffad5514b1 +RP_IMAGE_TEKU=consensys/teku:26.8.0@sha256:3a4f5761ae1c441ea26ac1f32b774e15e06e49a9861185a07e29ca7a6c3e434e +RP_IMAGE_PRYSM_BN=gcr.io/offchainlabs/prysm/beacon-chain:v7.1.8@sha256:31239807cbbd446aabe8e7d18e0cd50c430a290ce7906b55eb5d54c7d4e3b676 +RP_IMAGE_PRYSM_VC=gcr.io/offchainlabs/prysm/validator:v7.1.8@sha256:7fe554adf0efd27c0e5c5a3f80a3bbbec3d3872626208cf0a003b0dee7761f89 + +RP_IMAGE_MEV_BOOST=flashbots/mev-boost:1.12.0@sha256:7d7f51ba6a3f02a592340086a425fa3aa80d39ace4575dd9556f01df7a4e8f26 +RP_IMAGE_COMMIT_BOOST=ghcr.io/commit-boost/commit-boost:v0.10.1@sha256:aa8fbe1af3cd8b9cb2f9ce01603c502d2dc6c2ef7ca9bae4952f281f7d1030dd + +RP_IMAGE_PROMETHEUS=prom/prometheus:v3.14.0@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0 +RP_IMAGE_EXPORTER=prom/node-exporter:v1.12.1@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0 +RP_IMAGE_ALERTMANAGER=prom/alertmanager:v0.26.0@sha256:361db356b33041437517f1cd298462055580585f26555c317df1a3caf2868552 +RP_IMAGE_GRAFANA=grafana/grafana:9.5.18@sha256:42bb294e8807526658eceb19461e7646171ca48d15f9ac2ac511f1fe63599c54 +RP_IMAGE_GWW=rocketpool/graffiti-wall-addon:v1.0.1@sha256:bf3c224047d1c098d1f714e0ea1aae55d729521b99729ab5a4ddba4d5aec22f1 +RP_IMAGE_CURL=curlimages/curl:8.13.0@sha256:d43bdb28bae0be0998f3be83199bfb2b81e0a30b034b6d7586ce7e05de34c3fd +RP_IMAGE_ALPINE=alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c diff --git a/shared/services/rocketpool/assets/install/override/compose.yml b/shared/services/rocketpool/assets/install/override/compose.yml new file mode 100644 index 000000000..48251825e --- /dev/null +++ b/shared/services/rocketpool/assets/install/override/compose.yml @@ -0,0 +1,8 @@ +# Global compose overrides. These persist across upgrades and are merged after +# runtime/compose.yml, so they can add includes or override any service. +# +# Example: +# include: +# - path: extra.yml +# env_file: +# - extra.env diff --git a/shared/services/rocketpool/assets/install/templates/addons/gww/addon_gww.tmpl b/shared/services/rocketpool/assets/install/templates/addons/gww/addon_gww.tmpl index e088ea881..802b502e0 100644 --- a/shared/services/rocketpool/assets/install/templates/addons/gww/addon_gww.tmpl +++ b/shared/services/rocketpool/assets/install/templates/addons/gww/addon_gww.tmpl @@ -7,7 +7,7 @@ services: addon_gww: - image: {{.GraffitiWallWriter.GetConfig.ContainerTag}} + image: ${GWW_IMAGE_TAG_OVERRIDE:-${GWW_IMAGE_TAG_DEFAULT}} user: root container_name: {{.Smartnode.ProjectName}}_addon_gww restart: unless-stopped diff --git a/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl b/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl index b82b55e54..517cefda6 100644 --- a/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl +++ b/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl @@ -7,7 +7,7 @@ services: alertmanager: - image: {{.Alertmanager.ContainerTag}} + image: ${ALERTMANAGER_IMAGE_TAG_OVERRIDE:-${ALERTMANAGER_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_alertmanager restart: unless-stopped command: diff --git a/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl b/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl index 12cb94fc2..6187f4cd6 100644 --- a/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl +++ b/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl @@ -7,7 +7,7 @@ services: commit-boost: - image: {{.CommitBoost.ContainerTag}} + image: ${COMMIT_BOOST_IMAGE_TAG_OVERRIDE:-${COMMIT_BOOST_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_commit-boost restart: unless-stopped ports: [{{.GetCommitBoostOpenPorts}}] diff --git a/shared/services/rocketpool/assets/install/templates/compose.tmpl b/shared/services/rocketpool/assets/install/templates/compose.tmpl new file mode 100644 index 000000000..d8f81aaa3 --- /dev/null +++ b/shared/services/rocketpool/assets/install/templates/compose.tmpl @@ -0,0 +1,16 @@ +# Autogenerated - DO NOT MODIFY THIS FILE DIRECTLY +# Global overrides belong in override/compose.yml (merged after this file). +# Network overlay env files are commented out unless that network is selected. +include: +{{- range .Includes }} + - path: {{.Path}} +{{- if .WithEnv }} + env_file: + - {{$.MainnetEnv}} + {{$.TestnetOnly}}- {{$.TestnetEnv}} + {{$.DevnetOnly}}- {{$.DevnetEnv}} +{{- range $.ExtraNetworks }} + {{$.CommentUnless .}}- {{$.EnvFile .}} +{{- end }} +{{- end }} +{{- end }} diff --git a/shared/services/rocketpool/assets/install/templates/eth1.tmpl b/shared/services/rocketpool/assets/install/templates/eth1.tmpl index 35b8114e4..eaa120e18 100644 --- a/shared/services/rocketpool/assets/install/templates/eth1.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth1.tmpl @@ -7,7 +7,7 @@ services: eth1: - image: {{.GetECContainerTag}} + image: ${EC_IMAGE_TAG_OVERRIDE:-${EC_IMAGE_TAG_DEFAULT}} user: root container_name: {{.Smartnode.ProjectName}}_eth1 restart: unless-stopped diff --git a/shared/services/rocketpool/assets/install/templates/eth2.tmpl b/shared/services/rocketpool/assets/install/templates/eth2.tmpl index 7e2c317ea..8d9c50c0b 100644 --- a/shared/services/rocketpool/assets/install/templates/eth2.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth2.tmpl @@ -7,7 +7,7 @@ {{define "GENESIS_DL"}} eth2_genesis_downloader: - image: curlimages/curl:8.13.0 + image: ${CURL_IMAGE_TAG_OVERRIDE:-${CURL_IMAGE_TAG_DEFAULT}} user: root container_name: {{.Smartnode.ProjectName}}_genesis_downloader volumes: @@ -26,7 +26,7 @@ services: eth2: - image: {{.GetBeaconContainerTag}} + image: ${BN_IMAGE_TAG_OVERRIDE:-${BN_IMAGE_TAG_DEFAULT}} user: root container_name: {{.Smartnode.ProjectName}}_eth2 restart: unless-stopped diff --git a/shared/services/rocketpool/assets/install/templates/exporter.tmpl b/shared/services/rocketpool/assets/install/templates/exporter.tmpl index 75df4c8d4..8c0284b24 100644 --- a/shared/services/rocketpool/assets/install/templates/exporter.tmpl +++ b/shared/services/rocketpool/assets/install/templates/exporter.tmpl @@ -7,7 +7,7 @@ services: node-exporter: - image: {{.Exporter.ContainerTag}} + image: ${EXPORTER_IMAGE_TAG_OVERRIDE:-${EXPORTER_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_exporter cap_drop: - ALL diff --git a/shared/services/rocketpool/assets/install/templates/grafana.tmpl b/shared/services/rocketpool/assets/install/templates/grafana.tmpl index de0467416..4705a6306 100644 --- a/shared/services/rocketpool/assets/install/templates/grafana.tmpl +++ b/shared/services/rocketpool/assets/install/templates/grafana.tmpl @@ -8,7 +8,7 @@ {{$port := (or .Grafana.Port.Value 3100) -}} services: grafana: - image: {{.Grafana.ContainerTag}} + image: ${GRAFANA_IMAGE_TAG_OVERRIDE:-${GRAFANA_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_grafana restart: unless-stopped environment: diff --git a/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl b/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl index 16217769f..2d27af0fb 100644 --- a/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl +++ b/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl @@ -7,7 +7,7 @@ services: mev-boost: - image: {{.MevBoost.ContainerTag}} + image: ${MEV_BOOST_IMAGE_TAG_OVERRIDE:-${MEV_BOOST_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_mev-boost restart: unless-stopped ports: [{{.GetMevBoostOpenPorts}}] diff --git a/shared/services/rocketpool/assets/install/templates/node.tmpl b/shared/services/rocketpool/assets/install/templates/node.tmpl index 482021489..8beb416a3 100644 --- a/shared/services/rocketpool/assets/install/templates/node.tmpl +++ b/shared/services/rocketpool/assets/install/templates/node.tmpl @@ -7,7 +7,7 @@ services: node: - image: {{.Smartnode.GetSmartnodeContainerTag}} + image: ${SMARTNODE_IMAGE_TAG_OVERRIDE:-${SMARTNODE_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_node restart: unless-stopped tty: true diff --git a/shared/services/rocketpool/assets/install/templates/prometheus.tmpl b/shared/services/rocketpool/assets/install/templates/prometheus.tmpl index 1a4591e3d..3cc91dbf0 100644 --- a/shared/services/rocketpool/assets/install/templates/prometheus.tmpl +++ b/shared/services/rocketpool/assets/install/templates/prometheus.tmpl @@ -7,7 +7,7 @@ services: prometheus: - image: {{.Prometheus.ContainerTag}} + image: ${PROMETHEUS_IMAGE_TAG_OVERRIDE:-${PROMETHEUS_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_prometheus restart: unless-stopped command: diff --git a/shared/services/rocketpool/assets/install/templates/validator.tmpl b/shared/services/rocketpool/assets/install/templates/validator.tmpl index 6d28e036b..24f8c6746 100644 --- a/shared/services/rocketpool/assets/install/templates/validator.tmpl +++ b/shared/services/rocketpool/assets/install/templates/validator.tmpl @@ -7,7 +7,7 @@ services: validator: - image: {{.GetVCContainerTag}} + image: ${VC_IMAGE_TAG_OVERRIDE:-${VC_IMAGE_TAG_DEFAULT}} user: root container_name: {{.Smartnode.ProjectName}}_validator restart: unless-stopped diff --git a/shared/services/rocketpool/assets/install/templates/watchtower.tmpl b/shared/services/rocketpool/assets/install/templates/watchtower.tmpl index 06fa59472..cfce37509 100644 --- a/shared/services/rocketpool/assets/install/templates/watchtower.tmpl +++ b/shared/services/rocketpool/assets/install/templates/watchtower.tmpl @@ -7,7 +7,7 @@ services: watchtower: - image: {{.Smartnode.GetSmartnodeContainerTag}} + image: ${SMARTNODE_IMAGE_TAG_OVERRIDE:-${SMARTNODE_IMAGE_TAG_DEFAULT}} container_name: {{.Smartnode.ProjectName}}_watchtower restart: unless-stopped tty: true diff --git a/shared/services/rocketpool/assets/install/testnet.env b/shared/services/rocketpool/assets/install/testnet.env new file mode 100644 index 000000000..c32085042 --- /dev/null +++ b/shared/services/rocketpool/assets/install/testnet.env @@ -0,0 +1,7 @@ +# Optional image overrides for the testnet network. +# Uncomment or add KEY=value lines to override mainnet.env for this network only. +# Keys not listed here inherit the mainnet pin. +# +# Example: +# RP_IMAGE_GETH=ethereum/client-go:v1.17.4@sha256:... +RP_IMAGE_GETH=ethereum/client-go:v1.17.4 \ No newline at end of file diff --git a/shared/services/rocketpool/assets/scripts/install.sh b/shared/services/rocketpool/assets/scripts/install.sh index 7ce01afac..6ad6d9dd5 100755 --- a/shared/services/rocketpool/assets/scripts/install.sh +++ b/shared/services/rocketpool/assets/scripts/install.sh @@ -411,6 +411,9 @@ progress 7 "Copying package files to Rocket Pool user data directory..." { cp -r "$PACKAGE_FILES_PATH/templates" "$RP_PATH" || fail "Could not copy templates folder to the Rocket Pool user data directory."; } >&2 { cp -r "$PACKAGE_FILES_PATH/alerting" "$RP_PATH" || fail "Could not copy alerting folder to the Rocket Pool user data directory."; } >&2 { cp -r "$PACKAGE_FILES_PATH/devnet" "$RP_PATH" || fail "Could not copy devnet folder to the Rocket Pool user data directory."; } >&2 +{ cp "$PACKAGE_FILES_PATH/mainnet.env" "$RP_PATH/mainnet.env" || fail "Could not copy mainnet.env to the Rocket Pool user data directory."; } >&2 +{ cp "$PACKAGE_FILES_PATH/testnet.env" "$RP_PATH/testnet.env" || fail "Could not copy testnet.env to the Rocket Pool user data directory."; } >&2 +{ cp "$PACKAGE_FILES_PATH/devnet.env" "$RP_PATH/devnet.env" || fail "Could not copy devnet.env to the Rocket Pool user data directory."; } >&2 { cp \ "$PACKAGE_FILES_PATH/grafana-prometheus-datasource.yml" \ "$PACKAGE_FILES_PATH/prometheus.tmpl" \ @@ -423,6 +426,7 @@ progress 7 "Copying package files to Rocket Pool user data directory..." # Clean up unnecessary files from old installations progress 8 "Cleaning up obsolete files from previous installs..." { rm -rf "$DATA_PATH/fr-default" || echo "NOTE: Could not remove '$DATA_PATH/fr-default' which is no longer needed."; } >&2 +{ rm -f "$RP_PATH/images.env" || true; } >&2 # Install shell completion for rocketpool CLI progress 9 "Installing shell completion for rocketpool CLI..." diff --git a/shared/services/rocketpool/client.go b/shared/services/rocketpool/client.go index e20d19545..6744bd57f 100644 --- a/shared/services/rocketpool/client.go +++ b/shared/services/rocketpool/client.go @@ -925,9 +925,20 @@ func (c *Client) RunPruneProvisioner(container, volume string) error { return c.TouchEthclientMarker(container, volume, "prune.lock") } +func (c *Client) resolvedImage(key, fallback string) string { + cfg, _, err := c.LoadConfig() + if err != nil || cfg == nil { + return fallback + } + if v := cfg.ResolvedImage(key); v != "" { + return v + } + return fallback +} + // Creates a marker file on the execution client volume (used for prune and DB migrations). func (c *Client) TouchEthclientMarker(container, volume, marker string) error { - cmd := fmt.Sprintf("docker run --rm --name %s -v %s:/ethclient alpine:latest sh -c 'touch /ethclient/%s'", container, volume, marker) + cmd := fmt.Sprintf("docker run --rm --name %s -v %s:/ethclient %s sh -c 'touch /ethclient/%s'", container, volume, shellescape.Quote(c.resolvedImage(config.ImageAlpine, "alpine:3.21.3")), marker) output, err := c.readOutput(cmd) if err != nil { return err @@ -948,7 +959,7 @@ func (c *Client) RunNethermindPruneStarter(executionContainerName string) error for i := 0; i < retryCount; i++ { command := fmt.Sprintf(`-m 30 -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"admin_prune","params":[],"id":%d}' %s`, i+1, nethermindAdminUrl) - cmdText := fmt.Sprintf(`docker run --quiet --rm --name curl%s --network container:%s curlimages/curl -Ss %s`, pruneStarterContainerSuffix, executionContainerName, command) + cmdText := fmt.Sprintf(`docker run --quiet --rm --name curl%s --network container:%s %s -Ss %s`, pruneStarterContainerSuffix, executionContainerName, shellescape.Quote(c.resolvedImage(config.ImageCurl, "curlimages/curl:8.13.0")), command) if i != 0 { fmt.Printf("Trying again in %v... (%d/%d)\n", retryTime, i+1, retryCount) @@ -1167,18 +1178,135 @@ func (c *Client) compose(composeFiles []string, args string) (string, error) { return "", fmt.Errorf("error deploying Docker templates: %w", err) } - // Include all of the relevant docker compose definition files + if err := ensureImageEnvFiles(expandedConfigPath); err != nil { + return "", err + } + + composePaths := template.ComposePaths{ + RuntimePath: filepath.Join(expandedConfigPath, runtimeDir), + TemplatePath: filepath.Join(expandedConfigPath, templatesDir), + OverridePath: filepath.Join(expandedConfigPath, overrideDir), + } + composePair, err := composePaths.File("compose").Write(composeTemplateData{ + Includes: composeIncludes(expandedConfigPath, deployedContainers), + ProjectDir: expandedConfigPath, + Network: string(cfg.GetNetwork()), + ExtraNetworks: cfg.OverlayNetworkNames(), + }) + if err != nil { + return "", fmt.Errorf("error writing compose.yml: %w", err) + } + composeFileFlags := []string{} - for _, container := range deployedContainers { - composeFileFlags = append(composeFileFlags, fmt.Sprintf("-f %s", shellescape.Quote(container))) + for _, path := range composePair { + composeFileFlags = append(composeFileFlags, fmt.Sprintf("-f %s", shellescape.Quote(path))) } for _, container := range composeFiles { composeFileFlags = append(composeFileFlags, fmt.Sprintf("-f %s", shellescape.Quote(container))) } - // Return command - return fmt.Sprintf("COMPOSE_PROJECT_NAME=%s docker compose --project-directory %s %s %s", cfg.Smartnode.ProjectName.Value.(string), shellescape.Quote(expandedConfigPath), strings.Join(composeFileFlags, " "), args), nil + envPrefix := []string{ + fmt.Sprintf("COMPOSE_PROJECT_NAME=%s", cfg.Smartnode.ProjectName.Value.(string)), + } + assignments, err := cfg.ComposeEnvAssignments() + if err != nil { + return "", err + } + envPrefix = append(envPrefix, assignments...) + + parts := append(envPrefix, "docker compose", + "--project-directory", shellescape.Quote(expandedConfigPath), + strings.Join(composeFileFlags, " "), + args, + ) + return strings.Join(parts, " "), nil + +} + +func ensureImageEnvFiles(dir string) error { + official := []string{config.ImagesMainnetFile, config.ImagesTestnetFile, config.ImagesDevnetFile} + for _, name := range official { + data, ok := assets.EmbeddedNetworkEnv(name) + if !ok && name == config.ImagesMainnetFile { + data = assets.ImagesMainnetEnv() + ok = true + } + if !ok { + continue + } + path := filepath.Join(dir, name) + if _, err := os.Stat(path); err == nil { + continue + } else if !os.IsNotExist(err) { + return fmt.Errorf("could not stat %s: %w", path, err) + } + if err := os.WriteFile(path, data, 0664); err != nil { + return fmt.Errorf("could not write %s: %w", path, err) + } + } + return nil +} + +type composeInclude struct { + Path string + WithEnv bool +} + +type composeTemplateData struct { + Includes []composeInclude + ProjectDir string + Network string + ExtraNetworks []string +} + +func (d composeTemplateData) TestnetOnly() string { + if d.Network == "testnet" { + return "" + } + return "#" +} + +func (d composeTemplateData) DevnetOnly() string { + if d.Network == "devnet" { + return "" + } + return "#" +} +func (d composeTemplateData) CommentUnless(network string) string { + if d.Network == network { + return "" + } + return "#" +} + +func (d composeTemplateData) MainnetEnv() string { + return filepath.Join(d.ProjectDir, config.ImagesMainnetFile) +} + +func (d composeTemplateData) TestnetEnv() string { + return filepath.Join(d.ProjectDir, config.ImagesTestnetFile) +} + +func (d composeTemplateData) DevnetEnv() string { + return filepath.Join(d.ProjectDir, config.ImagesDevnetFile) +} + +func (d composeTemplateData) EnvFile(network string) string { + return filepath.Join(d.ProjectDir, network+".env") +} + +func composeIncludes(projectDir string, deployed []string) []composeInclude { + out := make([]composeInclude, 0, len(deployed)) + for _, path := range deployed { + entry := composeInclude{Path: path, WithEnv: true} + rel, err := filepath.Rel(projectDir, path) + if err == nil && (rel == overrideDir || strings.HasPrefix(rel, overrideDir+string(filepath.Separator))) { + entry.WithEnv = false + } + out = append(out, entry) + } + return out } // Deploys all of the appropriate docker compose template files and provisions them based on the provided configuration diff --git a/shared/version.txt b/shared/version.txt index 49e0a31d4..53cc1a6f9 100644 --- a/shared/version.txt +++ b/shared/version.txt @@ -1 +1 @@ -1.23.1 +1.24.0