From 41cda7e191f95857900b06e9ca92269ddb7752d3 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Thu, 19 Sep 2024 12:31:54 -0700 Subject: [PATCH] feat: ability to specify network interface (#1204) * feat: ability to specify network interface --- cmd/embedded-cluster/install.go | 14 ++- cmd/embedded-cluster/join.go | 6 +- cmd/embedded-cluster/preflights.go | 2 +- cmd/embedded-cluster/proxy.go | 30 +++---- cmd/embedded-cluster/restore.go | 18 +++- dagger.json | 2 +- dagger/go.mod | 20 ++--- dagger/go.sum | 40 ++++----- pkg/addons/adminconsole/adminconsole.go | 5 +- pkg/addons/applier.go | 10 +-- pkg/defaults/defaults.go | 5 -- pkg/defaults/provider.go | 14 --- pkg/defaults/provider_test.go | 10 --- pkg/netutils/ips.go | 111 ++++++++++++++++-------- pkg/netutils/ips_test.go | 28 +++++- 15 files changed, 186 insertions(+), 129 deletions(-) diff --git a/cmd/embedded-cluster/install.go b/cmd/embedded-cluster/install.go index 402e5d62a..9249080ee 100644 --- a/cmd/embedded-cluster/install.go +++ b/cmd/embedded-cluster/install.go @@ -20,6 +20,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/goods" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" @@ -331,12 +332,16 @@ func ensureK0sConfig(c *cli.Context, applier *addons.Applier) (*k0sconfig.Cluste return nil, fmt.Errorf("unable to create directory: %w", err) } cfg := config.RenderK0sConfig() + address, err := netutils.FirstValidAddress(c.String("network-interface")) + if err != nil { + return nil, fmt.Errorf("unable to find first valid address: %w", err) + } + cfg.Spec.API.Address = address cfg.Spec.Network.PodCIDR = c.String("pod-cidr") cfg.Spec.Network.ServiceCIDR = c.String("service-cidr") if err := config.UpdateHelmConfigs(applier, cfg); err != nil { return nil, fmt.Errorf("unable to update helm configs: %w", err) } - var err error cfg, err = applyUnsupportedOverrides(c, cfg) if err != nil { return nil, fmt.Errorf("unable to apply unsupported overrides: %w", err) @@ -490,7 +495,7 @@ func runOutro(c *cli.Context, applier *addons.Applier, cfg *k0sconfig.ClusterCon return fmt.Errorf("unable to process overrides file: %w", err) } - return applier.Outro(c.Context, cfg, eucfg, metadata) + return applier.Outro(c.Context, cfg, eucfg, metadata, c.String("network-interface")) } func maybeAskAdminConsolePassword(c *cli.Context) (string, error) { @@ -557,6 +562,11 @@ var installCommand = &cli.Command{ Usage: "Path to the license file", Hidden: false, }, + &cli.StringFlag{ + Name: "network-interface", + Usage: "The network interface to use for the cluster", + Value: "", + }, &cli.BoolFlag{ Name: "no-prompt", Usage: "Disable interactive prompts. The Admin Console password will be set to password.", diff --git a/cmd/embedded-cluster/join.go b/cmd/embedded-cluster/join.go index 974ec033d..b3b823306 100644 --- a/cmd/embedded-cluster/join.go +++ b/cmd/embedded-cluster/join.go @@ -190,7 +190,7 @@ var joinCommand = &cli.Command{ } setProxyEnv(jcmd.Proxy) - proxyOK, localIP, err := checkProxyConfigForLocalIP(jcmd.Proxy) + proxyOK, localIP, err := checkProxyConfigForLocalIP(jcmd.Proxy, "") // TODO (@salah): detect network interface from join command if err != nil { return fmt.Errorf("failed to check proxy config for local IP: %w", err) } @@ -265,7 +265,7 @@ var joinCommand = &cli.Command{ } logrus.Debugf("overriding network configuration") - if err := applyNetworkConfiguration(jcmd); err != nil { + if err := applyNetworkConfiguration(c, jcmd); err != nil { err := fmt.Errorf("unable to apply network configuration: %w", err) metrics.ReportJoinFailed(c.Context, jcmd.MetricsBaseURL, jcmd.ClusterID, err) } @@ -326,7 +326,7 @@ var joinCommand = &cli.Command{ }, } -func applyNetworkConfiguration(jcmd *JoinCommandResponse) error { +func applyNetworkConfiguration(c *cli.Context, jcmd *JoinCommandResponse) error { if jcmd.Network != nil { clusterSpec := config.RenderK0sConfig() // NOTE: we should be copying everything from the in cluster config spec and overriding diff --git a/cmd/embedded-cluster/preflights.go b/cmd/embedded-cluster/preflights.go index b213e80f2..67fe1d97d 100644 --- a/cmd/embedded-cluster/preflights.go +++ b/cmd/embedded-cluster/preflights.go @@ -118,7 +118,7 @@ var joinRunPreflightsCommand = &cli.Command{ } setProxyEnv(jcmd.Proxy) - proxyOK, localIP, err := checkProxyConfigForLocalIP(jcmd.Proxy) + proxyOK, localIP, err := checkProxyConfigForLocalIP(jcmd.Proxy, "") // TODO (@salah): detect network interface from join command if err != nil { return fmt.Errorf("failed to check proxy config for local IP: %w", err) } diff --git a/cmd/embedded-cluster/proxy.go b/cmd/embedded-cluster/proxy.go index bb0bb34dc..002e76652 100644 --- a/cmd/embedded-cluster/proxy.go +++ b/cmd/embedded-cluster/proxy.go @@ -99,26 +99,26 @@ func includeLocalIPInNoProxy(c *cli.Context, proxy *ecv1beta1.ProxySpec) (*ecv1b // if there is a proxy set, then there needs to be a no proxy set // if it is not set, prompt with a default (the local IP or subnet) // if it is set, we need to check that it covers the local IP - defaultIPNet, err := netutils.GetDefaultIPNet() + ipnet, err := netutils.FirstValidIPNet(c.String("network-interface")) if err != nil { - return nil, fmt.Errorf("failed to get default IPNet: %w", err) + return nil, fmt.Errorf("failed to get first valid ip net: %w", err) } - cleanDefaultIPNet, err := cleanCIDR(defaultIPNet) + cleanIPNet, err := cleanCIDR(ipnet) if err != nil { return nil, fmt.Errorf("failed to clean subnet: %w", err) } if proxy.ProvidedNoProxy == "" { - logrus.Infof("--no-proxy was not set. Adding the default interface's subnet (%q) to the no-proxy list.", cleanDefaultIPNet) - proxy.ProvidedNoProxy = cleanDefaultIPNet + logrus.Infof("--no-proxy was not set. Adding the default interface's subnet (%q) to the no-proxy list.", cleanIPNet) + proxy.ProvidedNoProxy = cleanIPNet combineNoProxySuppliedValuesAndDefaults(c, proxy) return proxy, nil } else { - isValid, err := validateNoProxy(proxy.NoProxy, defaultIPNet.IP.String()) + isValid, err := validateNoProxy(proxy.NoProxy, ipnet.IP.String()) if err != nil { return nil, fmt.Errorf("failed to validate no-proxy: %w", err) } else if !isValid { - logrus.Infof("The node IP (%q) is not included in the provided no-proxy list (%q). Adding the default interface's subnet (%q) to the no-proxy list.", defaultIPNet.IP.String(), proxy.ProvidedNoProxy, cleanDefaultIPNet) - proxy.ProvidedNoProxy = cleanDefaultIPNet + logrus.Infof("The node IP (%q) is not included in the provided no-proxy list (%q). Adding the default interface's subnet (%q) to the no-proxy list.", ipnet.IP.String(), proxy.ProvidedNoProxy, cleanIPNet) + proxy.ProvidedNoProxy = cleanIPNet combineNoProxySuppliedValuesAndDefaults(c, proxy) return proxy, nil } @@ -128,10 +128,10 @@ func includeLocalIPInNoProxy(c *cli.Context, proxy *ecv1beta1.ProxySpec) (*ecv1b } // cleanCIDR returns a `.0/x` subnet instead of a `.2/x` etc subnet -func cleanCIDR(defaultIPNet *net.IPNet) (string, error) { - _, newNet, err := net.ParseCIDR(defaultIPNet.String()) +func cleanCIDR(ipnet *net.IPNet) (string, error) { + _, newNet, err := net.ParseCIDR(ipnet.String()) if err != nil { - return "", fmt.Errorf("failed to parse local inet CIDR %q: %w", defaultIPNet.String(), err) + return "", fmt.Errorf("failed to parse local inet CIDR %q: %w", ipnet.String(), err) } return newNet.String(), nil } @@ -155,7 +155,7 @@ func validateNoProxy(newNoProxy string, localIP string) (bool, error) { return foundLocal, nil } -func checkProxyConfigForLocalIP(proxy *ecv1beta1.ProxySpec) (bool, string, error) { +func checkProxyConfigForLocalIP(proxy *ecv1beta1.ProxySpec, networkInterface string) (bool, string, error) { if proxy == nil { return true, "", nil // no proxy is fine } @@ -163,11 +163,11 @@ func checkProxyConfigForLocalIP(proxy *ecv1beta1.ProxySpec) (bool, string, error return true, "", nil // no proxy is fine } - defaultIPNet, err := netutils.GetDefaultIPNet() + ipnet, err := netutils.FirstValidIPNet(networkInterface) if err != nil { return false, "", fmt.Errorf("failed to get default IPNet: %w", err) } - ok, err := validateNoProxy(proxy.NoProxy, defaultIPNet.IP.String()) - return ok, defaultIPNet.IP.String(), err + ok, err := validateNoProxy(proxy.NoProxy, ipnet.IP.String()) + return ok, ipnet.IP.String(), err } diff --git a/cmd/embedded-cluster/restore.go b/cmd/embedded-cluster/restore.go index 2df9cf54d..b309c98a3 100644 --- a/cmd/embedded-cluster/restore.go +++ b/cmd/embedded-cluster/restore.go @@ -26,6 +26,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/defaults" "github.com/replicatedhq/embedded-cluster/pkg/kotscli" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/spinner" @@ -323,12 +324,16 @@ func ensureK0sConfigForRestore(c *cli.Context, applier *addons.Applier) (*k0sv1b return nil, fmt.Errorf("unable to create directory: %w", err) } cfg := config.RenderK0sConfig() + address, err := netutils.FirstValidAddress(c.String("network-interface")) + if err != nil { + return nil, fmt.Errorf("unable to find first valid address: %w", err) + } + cfg.Spec.API.Address = address cfg.Spec.Network.PodCIDR = c.String("pod-cidr") cfg.Spec.Network.ServiceCIDR = c.String("service-cidr") if err := config.UpdateHelmConfigsForRestore(applier, cfg); err != nil { return nil, fmt.Errorf("unable to update helm configs: %w", err) } - var err error cfg, err = applyUnsupportedOverrides(c, cfg) if err != nil { return nil, fmt.Errorf("unable to apply unsupported overrides: %w", err) @@ -799,7 +804,7 @@ func restoreFromBackup(ctx context.Context, backup *velerov1.Backup, drComponent } // waitForAdditionalNodes waits for for user to add additional nodes to the cluster. -func waitForAdditionalNodes(ctx context.Context, highAvailability bool) error { +func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkInterface string) error { kcli, err := kubeutils.KubeClient() if err != nil { return fmt.Errorf("unable to create kube client: %w", err) @@ -808,7 +813,7 @@ func waitForAdditionalNodes(ctx context.Context, highAvailability bool) error { successColor := "\033[32m" colorReset := "\033[0m" joinNodesMsg := fmt.Sprintf("\nVisit the Admin Console if you need to add nodes to the cluster: %s%s%s\n", - successColor, adminconsole.GetURL(), colorReset, + successColor, adminconsole.GetURL(networkInterface), colorReset, ) logrus.Info(joinNodesMsg) @@ -880,6 +885,11 @@ var restoreCommand = &cli.Command{ Usage: "Path to the air gap bundle. If set, the restore will complete without internet access.", Hidden: true, }, + &cli.StringFlag{ + Name: "network-interface", + Usage: "The network interface to use for the cluster", + Value: "", + }, &cli.BoolFlag{ Name: "no-prompt", Usage: "Disable interactive prompts.", @@ -1078,7 +1088,7 @@ var restoreCommand = &cli.Command{ return err } logrus.Debugf("waiting for additional nodes to be added") - if err := waitForAdditionalNodes(c.Context, highAvailability); err != nil { + if err := waitForAdditionalNodes(c.Context, highAvailability, c.String("network-interface")); err != nil { return err } fallthrough diff --git a/dagger.json b/dagger.json index 99f27fd93..47be40cb4 100644 --- a/dagger.json +++ b/dagger.json @@ -2,5 +2,5 @@ "name": "embedded-cluster", "sdk": "go", "source": "dagger", - "engineVersion": "v0.13.0" + "engineVersion": "v0.13.1" } diff --git a/dagger/go.mod b/dagger/go.mod index 019ff3754..f69bc757f 100644 --- a/dagger/go.mod +++ b/dagger/go.mod @@ -17,26 +17,26 @@ require ( go.opentelemetry.io/otel/trace v1.27.0 go.opentelemetry.io/proto/otlp v1.3.1 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/sync v0.7.0 - google.golang.org/grpc v1.64.0 + golang.org/x/sync v0.8.0 + google.golang.org/grpc v1.66.1 ) require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sosodev/duration v1.3.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 diff --git a/dagger/go.sum b/dagger/go.sum index 6fea81b9c..40f88b0b1 100644 --- a/dagger/go.sum +++ b/dagger/go.sum @@ -10,16 +10,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -63,22 +63,22 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index d0796c534..df8025c68 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -27,6 +27,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/kotscli" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "github.com/replicatedhq/embedded-cluster/pkg/versions" @@ -258,11 +259,11 @@ func WaitForReady(ctx context.Context, cli client.Client, ns string, writer *spi } // GetURL returns the URL to the admin console. -func GetURL() string { +func GetURL(networkInterface string) string { ipaddr := defaults.TryDiscoverPublicIP() if ipaddr == "" { var err error - ipaddr, err = defaults.PreferredNodeIPAddress() + ipaddr, err = netutils.FirstValidAddress(networkInterface) if err != nil { logrus.Errorf("unable to determine node IP address: %v", err) ipaddr = "NODE-IP-ADDRESS" diff --git a/pkg/addons/applier.go b/pkg/addons/applier.go index f49f8a580..04e5671e1 100644 --- a/pkg/addons/applier.go +++ b/pkg/addons/applier.go @@ -53,7 +53,7 @@ type Applier struct { } // Outro runs the outro in all enabled add-ons. -func (a *Applier) Outro(ctx context.Context, k0sCfg *k0sv1beta1.ClusterConfig, endUserCfg *ecv1beta1.Config, releaseMetadata *types.ReleaseMetadata) error { +func (a *Applier) Outro(ctx context.Context, k0sCfg *k0sv1beta1.ClusterConfig, endUserCfg *ecv1beta1.Config, releaseMetadata *types.ReleaseMetadata, networkInterface string) error { kcli, err := kubeutils.KubeClient() if err != nil { return fmt.Errorf("unable to create kube client: %w", err) @@ -79,7 +79,7 @@ func (a *Applier) Outro(ctx context.Context, k0sCfg *k0sv1beta1.ClusterConfig, e if err := spinForInstallation(ctx, kcli); err != nil { return err } - if err := printKotsadmLinkMessage(a.licenseFile); err != nil { + if err := printKotsadmLinkMessage(a.licenseFile, networkInterface); err != nil { return fmt.Errorf("unable to print success message: %w", err) } return nil @@ -391,7 +391,7 @@ func spinForInstallation(ctx context.Context, cli client.Client) error { } // printKotsadmLinkMessage prints the success message when the admin console is online. -func printKotsadmLinkMessage(licenseFile string) error { +func printKotsadmLinkMessage(licenseFile string, networkInterface string) error { var err error license := &kotsv1beta1.License{} if licenseFile != "" { @@ -406,11 +406,11 @@ func printKotsadmLinkMessage(licenseFile string) error { var successMessage string if license != nil { successMessage = fmt.Sprintf("Visit the Admin Console to configure and install %s: %s%s%s", - license.Spec.AppSlug, successColor, adminconsole.GetURL(), colorReset, + license.Spec.AppSlug, successColor, adminconsole.GetURL(networkInterface), colorReset, ) } else { successMessage = fmt.Sprintf("Visit the Admin Console to configure and install your application: %s%s%s", - successColor, adminconsole.GetURL(), colorReset, + successColor, adminconsole.GetURL(networkInterface), colorReset, ) } logrus.Info(successMessage) diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index b2dd91b89..1e7088d48 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -68,11 +68,6 @@ func PathToKubeConfig() string { return DefaultProvider.PathToKubeConfig() } -// PreferredNodeIPAddress calls PreferredNodeIPAddress on the default provider. -func PreferredNodeIPAddress() (string, error) { - return DefaultProvider.PreferredNodeIPAddress() -} - // TryDiscoverPublicIP calls TryDiscoverPublicIP on the default provider. func TryDiscoverPublicIP() string { return DefaultProvider.TryDiscoverPublicIP() diff --git a/pkg/defaults/provider.go b/pkg/defaults/provider.go index d84a878ba..2e79ef1e9 100644 --- a/pkg/defaults/provider.go +++ b/pkg/defaults/provider.go @@ -1,7 +1,6 @@ package defaults import ( - "fmt" "io" "net" "net/http" @@ -116,19 +115,6 @@ func (d *Provider) PathToKubeConfig() string { return "/var/lib/k0s/pki/admin.conf" } -// PreferredNodeIPAddress returns the ip address the node uses when reaching -// the internet. This is useful when the node has multiple interfaces and we -// want to bind to one of the interfaces. -func (d *Provider) PreferredNodeIPAddress() (string, error) { - conn, err := net.Dial("udp", "8.8.8.8:53") - if err != nil { - return "", fmt.Errorf("unable to get local IP: %w", err) - } - defer conn.Close() - addr := conn.LocalAddr().(*net.UDPAddr) - return addr.IP.String(), nil -} - // TryDiscoverPublicIP tries to discover the public IP of the node by querying // a list of known providers. If the public IP cannot be discovered, an empty // string is returned. diff --git a/pkg/defaults/provider_test.go b/pkg/defaults/provider_test.go index d8b9a9386..5727ac0ee 100644 --- a/pkg/defaults/provider_test.go +++ b/pkg/defaults/provider_test.go @@ -17,16 +17,6 @@ func TestInit(t *testing.T) { assert.DirExists(t, def.EmbeddedClusterBinsSubDir(), "embedded-cluster binary dir should exist") } -func TestPreferredNodeIPAddress(t *testing.T) { - tmpdir, err := os.MkdirTemp("", "embedded-cluster") - assert.NoError(t, err) - defer os.RemoveAll(tmpdir) - def := NewProvider(tmpdir) - ip, err := def.PreferredNodeIPAddress() - assert.NoError(t, err) - assert.NotEmpty(t, ip, "ip address should not be empty") -} - func TestEnsureAllDirectoriesAreInsideBase(t *testing.T) { tmpdir, err := os.MkdirTemp("", "embedded-cluster") assert.NoError(t, err) diff --git a/pkg/netutils/ips.go b/pkg/netutils/ips.go index 8916cbfaa..6f20b0de9 100644 --- a/pkg/netutils/ips.go +++ b/pkg/netutils/ips.go @@ -4,51 +4,92 @@ import ( "fmt" "net" "strings" - - "github.com/sirupsen/logrus" ) -// GetDefaultIPNet returns the default interface for the node, and the subnet mask for that node, using the same logic -// as k0s in https://github.com/k0sproject/k0s/blob/v1.30.4%2Bk0s.0/internal/pkg/iface/iface.go#L61 -func GetDefaultIPNet() (*net.IPNet, error) { - ifs, err := net.Interfaces() +// adapted from https://github.com/k0sproject/k0s/blob/v1.30.4%2Bk0s.0/internal/pkg/iface/iface.go#L61 +func FirstValidAddress(networkInterface string) (string, error) { + ipnet, err := FirstValidIPNet(networkInterface) + if err != nil { + return "", fmt.Errorf("get ipnet for interface %s: %w", networkInterface, err) + } + if ipnet.IP.To4() == nil { + return "", fmt.Errorf("interface %s has no ipv4 addresses", networkInterface) + } + return ipnet.IP.String(), nil +} + +func FirstValidIPNet(networkInterface string) (*net.IPNet, error) { + ifs, err := listValidInterfaces() if err != nil { - return nil, fmt.Errorf("failed to list network interfaces: %w", err) + return nil, fmt.Errorf("list valid network interfaces: %w", err) + } + if len(ifs) == 0 { + return nil, fmt.Errorf("no valid network interfaces found on this machine") + } + if networkInterface == "" { + return firstValidIPNet(ifs[0]) } for _, i := range ifs { - if isPodnetworkInterface(i.Name) { - continue + if i.Name == networkInterface { + return firstValidIPNet(i) } - addresses, err := i.Addrs() - if err != nil { - logrus.Debugf("failed to get addresses for interface %s: %s", i.Name, err.Error()) + } + var ifNames []string + for _, i := range ifs { + ifNames = append(ifNames, i.Name) + } + return nil, fmt.Errorf("interface %s not found or is not valid. The following interfaces were detected: %s", networkInterface, strings.Join(ifNames, ", ")) +} + +// listValidInterfaces returns a list of valid network interfaces for the node. +func listValidInterfaces() ([]net.Interface, error) { + ifs, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("list network interfaces: %w", err) + } + validIfs := []net.Interface{} + for _, i := range ifs { + if !isValidInterface(i) { continue } - for _, a := range addresses { - // check the address type and skip if loopback - if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return ipnet, nil - } - } - } + validIfs = append(validIfs, i) } - - return nil, fmt.Errorf("failed to find any non-local, non podnetwork ipv4 addresses on host") + return validIfs, nil } -func isPodnetworkInterface(name string) bool { +func isValidInterface(i net.Interface) bool { switch { - case name == "vxlan.calico": - return true - case name == "kube-bridge": - return true - case name == "dummyvip0": - return true - case strings.HasPrefix(name, "veth"): - return true - case strings.HasPrefix(name, "cali"): - return true - } - return false + case i.Name == "vxlan.calico": + return false + case i.Name == "kube-bridge": + return false + case i.Name == "dummyvip0": + return false + case strings.HasPrefix(i.Name, "veth"): + return false + case strings.HasPrefix(i.Name, "cali"): + return false + } + return hasValidIPNet(i) +} + +func hasValidIPNet(i net.Interface) bool { + ipnet, err := firstValidIPNet(i) + return err == nil && ipnet != nil +} + +func firstValidIPNet(i net.Interface) (*net.IPNet, error) { + addresses, err := i.Addrs() + if err != nil { + return nil, fmt.Errorf("get addresses: %w", err) + } + for _, a := range addresses { + // check the address type and skip if loopback + if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet, nil + } + } + } + return nil, fmt.Errorf("could not find any non-local, non podnetwork ipv4 addresses") } diff --git a/pkg/netutils/ips_test.go b/pkg/netutils/ips_test.go index c415f0533..0f0a015c8 100644 --- a/pkg/netutils/ips_test.go +++ b/pkg/netutils/ips_test.go @@ -7,10 +7,34 @@ import ( "github.com/stretchr/testify/require" ) -func TestGetDefaultIPAndMask(t *testing.T) { +func TestFirstValidAddress(t *testing.T) { req := require.New(t) - got, err := GetDefaultIPNet() + + // no specified interface + got, err := FirstValidAddress("") + req.NoError(err) + fmt.Printf("got ip address: %s\n", got) + req.NotEmpty(got) + + // invalid interface + got, err = FirstValidAddress("foo") + req.Error(err) + req.Contains(err.Error(), "interface foo not found or is not valid") + req.Empty(got) +} + +func TestFirstValidIPNet(t *testing.T) { + req := require.New(t) + + // no specified interface + got, err := FirstValidIPNet("") req.NoError(err) fmt.Printf("got network: %s, got ip: %s\n", got.String(), got.IP.String()) req.NotNil(got) + + // invalid interface + got, err = FirstValidIPNet("foo") + req.Error(err) + req.Contains(err.Error(), "interface foo not found or is not valid") + req.Nil(got) }