Repository: metallb/metallb
Pull request #651 (2020-06-27 20:16:44)

Add command-line flags to run processes outside of k8s
When you're developing, running components at the command line and
having them attach to a cluster might be more convenient than
redeploying them to a cluster over and over.

For the controller, -kubeconfig and -config-ns are needed.  Speakers
need those and -node-name.

For example:

 metallb$ go run ./controller/main.go ./controller/service.go -config-ns metallb-system -kubeconfig $KUBECONFIG

 metallb$ go run ./speaker/main.go ./speaker/*controller.go -config-ns metallb-system -kubeconfig $KUBECONFIG -node-name node0

Thanks for sending a pull request! A few things before we get started:

1. If this is your first time, please read the [contributing guide](https://metallb.universe.tf/community/#contributing)
2. For non-trivial pull requests, please [file an
   issue](https://github.com/google/metallb/issues/new) first, and get
   agreement that the change is a good idea, and a general guideline
   for how it should be implemented, before sending code. Large PRs
   that weren't first discussed and agreed upon in an issue won't be
   accepted.
3. If the PR fixes a particular bug, please include the words "Fixed
   #<issue number>" in the PR text, so that the bug auto-closes when
   the PR is merged.


rata: Hi!

Sorry for the quick answer, I'm on my phone.

It's not very well documented here, https://metallb.universe.tf/community/, but basically you you install those things you can deploy locally. You can run all invoke commands, for local development inv dev-env (or alike, don't remember and I'm on my phone so not trivial to check) will do what you want automatically and quite easily (first time will create a cluster, next time you run it will recompile and update metalLB).

Just curious, have you tried that?
caboteria: Yup, dev-env is a useful tool and it works well.

The changes I'm proposing add another option that benefits developers in a few ways:
 * speed: running "dev-env" with an update takes about 15s because it builds docker images, pushes them into the cluster, undeploys everything in the cluster, and then redeploys everything in the cluster.  Running "go run" externally takes about 2s for the controller to run.
 * flexibility: these changes let you attach a locally-running controller or speaker to *any* cluster that you've got a kubeconfig file for.  This is great for testing setups with special networking config or complex apps.
 * debug: because the local process is running on the host and not inside a docker container inside kind it should be easier to attach a debugger to it (although I haven't tested that yet).


Patch:
diff --git a/controller/Dockerfile b/controller/Dockerfile
index 124daee5..e304583c 100644
--- a/controller/Dockerfile
+++ b/controller/Dockerfile
@@ -1,4 +1,10 @@
 FROM alpine:latest
 
 ADD controller /controller
+
+# When running as non root and building in an environment that `umask` masks out
+# '+x' for others, it won't be possible to execute. Make sure all can execute,
+# just in case
+RUN chmod a+x /controller
+
 ENTRYPOINT ["/controller"]
diff --git a/controller/main.go b/controller/main.go
index af1aa22b..b0d6c4f9 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -134,8 +134,10 @@ func main() {
 	}
 
 	var (
-		port   = flag.Int("port", 7472, "HTTP listening port for Prometheus metrics")
-		config = flag.String("config", "config", "Kubernetes ConfigMap containing MetalLB's configuration")
+		port       = flag.Int("port", 7472, "HTTP listening port for Prometheus metrics")
+		config     = flag.String("config", "config", "Kubernetes ConfigMap containing MetalLB's configuration")
+		configNS   = flag.String("config-ns", "", "config file namespace (only needed when running outside of k8s)")
+		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file (only needed when running outside of k8s)")
 	)
 	flag.Parse()
 
@@ -148,8 +150,10 @@ func main() {
 	client, err := k8s.New(&k8s.Config{
 		ProcessName:   "metallb-controller",
 		ConfigMapName: *config,
+		ConfigMapNS:   *configNS,
 		MetricsPort:   *port,
 		Logger:        logger,
+		Kubeconfig:    *kubeconfig,
 
 		ServiceChanged: c.SetBalancer,
 		ConfigChanged:  c.SetConfig,
diff --git a/internal/allocator/allocator.go b/internal/allocator/allocator.go
index fdc61985..474cfdc4 100644
--- a/internal/allocator/allocator.go
+++ b/internal/allocator/allocator.go
@@ -21,7 +21,6 @@ type Allocator struct {
 	portsInUse      map[string]map[Port]string // ip.String() -> Port -> svc
 	servicesOnIP    map[string]map[string]bool // ip.String() -> svc -> allocated?
 	poolIPsInUse    map[string]map[string]int  // poolName -> ip.String() -> number of users
-	poolServices    map[string]int             // poolName -> #services
 }
 
 // Port represents one port in use by a service.
@@ -57,7 +56,6 @@ func New() *Allocator {
 		portsInUse:      map[string]map[Port]string{},
 		servicesOnIP:    map[string]map[string]bool{},
 		poolIPsInUse:    map[string]map[string]int{},
-		poolServices:    map[string]int{},
 	}
 }
 
@@ -95,6 +93,12 @@ func (a *Allocator) SetPools(pools map[string]*config.Pool) error {
 		}
 	}
 
+	// Refresh or initiate stats
+	for n, p := range a.pools {
+		stats.poolCapacity.WithLabelValues(n).Set(float64(poolCount(p)))
+		stats.poolActive.WithLabelValues(n).Set(float64(len(a.poolIPsInUse[n])))
+	}
+
 	return nil
 }
 
@@ -118,10 +122,9 @@ func (a *Allocator) assign(svc string, alloc *alloc) {
 		a.poolIPsInUse[alloc.pool] = map[string]int{}
 	}
 	a.poolIPsInUse[alloc.pool][alloc.ip.String()]++
-	a.poolServices[alloc.pool]++
 
 	stats.poolCapacity.WithLabelValues(alloc.pool).Set(float64(poolCount(a.pools[alloc.pool])))
-	stats.poolActive.WithLabelValues(alloc.pool).Set(float64(a.poolServices[alloc.pool]))
+	stats.poolActive.WithLabelValues(alloc.pool).Set(float64(len(a.poolIPsInUse[alloc.pool])))
 }
 
 // Assign assigns the requested ip to svc, if the assignment is
@@ -206,7 +209,7 @@ func (a *Allocator) Unassign(svc string) bool {
 		// is an accurate count of IPs in use.
 		delete(a.poolIPsInUse[al.pool], al.ip.String())
 	}
-	a.poolServices[al.pool]--
+	stats.poolActive.WithLabelValues(al.pool).Set(float64(len(a.poolIPsInUse[al.pool])))
 	return true
 }
 
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 3edd74e2..baf38f16 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -18,6 +18,7 @@ import (
 	v1core "k8s.io/client-go/kubernetes/typed/core/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 )
@@ -67,11 +68,13 @@ const (
 type Config struct {
 	ProcessName   string
 	ConfigMapName string
+	ConfigMapNS   string
 	NodeName      string
 	MetricsHost   string
 	MetricsPort   int
 	ReadEndpoints bool
 	Logger        log.Logger
+	Kubeconfig    string
 
 	ServiceChanged func(log.Logger, string, *v1.Service, *v1.Endpoints) SyncState
 	ConfigChanged  func(log.Logger, *config.Config) SyncState
@@ -89,7 +92,20 @@ type synced string
 // The client uses processName to identify itself to the cluster
 // (e.g. when logging events).
 func New(cfg *Config) (*Client, error) {
-	k8sConfig, err := rest.InClusterConfig()
+	var (
+		k8sConfig *rest.Config
+		err       error
+	)
+
+	if cfg.Kubeconfig == "" {
+		// if the user didn't provide a config file, assume that we're
+		// running inside k8s.
+		k8sConfig, err = rest.InClusterConfig()
+	} else {
+		// the user provided a config file, so use that.  InClusterConfig
+		// would also work in this case but it emits an annoying warning.
+		k8sConfig, err = clientcmd.BuildConfigFromFlags("", cfg.Kubeconfig)
+	}
 	if err != nil {
 		return nil, fmt.Errorf("building client config: %s", err)
 	}
@@ -98,11 +114,14 @@ func New(cfg *Config) (*Client, error) {
 		return nil, fmt.Errorf("creating Kubernetes client: %s", err)
 	}
 
-	bs, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
-	if err != nil {
-		return nil, fmt.Errorf("getting namespace from pod service account data: %s", err)
+	namespace := cfg.ConfigMapNS
+	if namespace == "" {
+		bs, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
+		if err != nil {
+			return nil, fmt.Errorf("getting namespace from pod service account data: %s", err)
+		}
+		namespace = string(bs)
 	}
-	namespace := string(bs)
 
 	broadcaster := record.NewBroadcaster()
 	broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(clientset.CoreV1().RESTClient()).Events("")})
diff --git a/speaker/Dockerfile b/speaker/Dockerfile
index c8aacf5e..caa64aa0 100644
--- a/speaker/Dockerfile
+++ b/speaker/Dockerfile
@@ -1,4 +1,10 @@
 FROM alpine:latest
 
 ADD speaker /speaker
+
+# When running as non root and building in an environment that `umask` masks out
+# '+x' for others, it won't be possible to execute. Make sure all can execute,
+# just in case
+RUN chmod a+x /speaker
+
 ENTRYPOINT ["/speaker"]
diff --git a/speaker/main.go b/speaker/main.go
index 2e23c78f..4f1089aa 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -70,6 +70,8 @@ func main() {
 
 	var (
 		config      = flag.String("config", "config", "Kubernetes ConfigMap containing MetalLB's configuration")
+		configNS    = flag.String("config-ns", "", "config file namespace (only needed when running outside of k8s)")
+		kubeconfig  = flag.String("kubeconfig", "", "absolute path to the kubeconfig file (only needed when running outside of k8s)")
 		host        = flag.String("host", os.Getenv("METALLB_HOST"), "HTTP host address")
 		mlBindAddr  = flag.String("ml-bindaddr", os.Getenv("METALLB_ML_BIND_ADDR"), "Bind addr for MemberList (fast dead node detection)")
 		mlBindPort  = flag.String("ml-bindport", os.Getenv("METALLB_ML_BIND_PORT"), "Bind port for MemberList (fast dead node detection)")
@@ -146,8 +148,10 @@ func main() {
 	client, err := k8s.New(&k8s.Config{
 		ProcessName:   "metallb-speaker",
 		ConfigMapName: *config,
+		ConfigMapNS:   *configNS,
 		NodeName:      *myNode,
 		Logger:        logger,
+		Kubeconfig:    *kubeconfig,
 
 		MetricsHost:   *host,
 		MetricsPort:   *port,
diff --git a/tasks.py b/tasks.py
index 7264a7a3..002255af 100644
--- a/tasks.py
+++ b/tasks.py
@@ -310,6 +310,13 @@ def release(ctx, version, skip_release_notes=False):
     run("perl -pi -e 's,image: metallb/speaker:.*,image: metallb/speaker:v{},g' manifests/metallb.yaml".format(version), echo=True)
     run("perl -pi -e 's,image: metallb/controller:.*,image: metallb/controller:v{},g' manifests/metallb.yaml".format(version), echo=True)
 
+    # Update the version in kustomize instructions
+    #
+    # TODO: Check if kustomize instructions really need the version in the
+    # website or if there is a simpler way. For now, though, we just replace the
+    # only page that mentions the version on release.
+    run("perl -pi -e 's,github.com/metallb/metallb//manifests\?ref=.*,github.com/metallb/metallb//manifests\?ref=v{},g' website/content/installation/_index.md".format(version), echo=True)
+
     # Update the version embedded in the binary
     run("perl -pi -e 's/version\s+=.*/version = \"{}\"/g' internal/version/version.go".format(version), echo=True)
     run("gofmt -w internal/version/version.go", echo=True)
diff --git a/website/content/community/_index.md b/website/content/community/_index.md
index b543d420..97aedc39 100644
--- a/website/content/community/_index.md
+++ b/website/content/community/_index.md
@@ -116,7 +116,7 @@ To develop MetalLB, you'll need a couple of pieces of software:
 >versions may not work since there have been breaking changes between minor
 >versions.
 
-## Building the code
+## Building and running the code
 
 Start by fetching the MetalLB repository, with `git clone
 https://github.com/danderson/metallb`.
@@ -127,6 +127,19 @@ registries, and so forth. `inv -l` lists the available tasks.
 To build and deploy MetalLB to a local development environment using a kind
 cluster, run `inv dev-env`.
 
+When you're developing, running components at the command line and
+having them attach to a cluster might be more convenient than
+redeploying them to a cluster over and over.
+
+For the controller, the `-kubeconfig` and `-config-ns` command-line flags
+are needed.  Speakers need those and `-node-name`.
+
+For example:
+
+ metallb$ go run ./controller/main.go ./controller/service.go -config-ns metallb-system -kubeconfig $KUBECONFIG
+
+ metallb$ go run ./speaker/main.go ./speaker/*controller.go -config-ns metallb-system -kubeconfig $KUBECONFIG -node-name node0
+
 For development, fork
 the [github repository](https://github.com/google/metallb), and add
 your fork as a remote in `$GOPATH/src/go.universe.tf/metallb`, with
diff --git a/website/content/installation/_index.md b/website/content/installation/_index.md
index 59c78f6d..ced4e645 100644
--- a/website/content/installation/_index.md
+++ b/website/content/installation/_index.md
@@ -94,7 +94,7 @@ on the remote kustomization fle :
 namespace: metallb-system
 
 resources:
-  - github.com/danderson/metallb//manifests?ref=v0.8.2
+  - github.com/metallb/metallb//manifests?ref=v0.8.2
   - configmap.yml 
   - secret.yml
 ```
@@ -111,7 +111,7 @@ the configMap, as MetalLB is waiting for a configMap named `config`
 namespace: metallb-system
 
 resources:
-  - github.com/danderson/metallb//manifests?ref=v0.8.2
+  - github.com/metallb/metallb//manifests?ref=v0.8.2
 
 configMapGenerator:
 - name: config


Test patch:
diff --git a/internal/allocator/allocator_test.go b/internal/allocator/allocator_test.go
index cefebe84..b9a07cb5 100644
--- a/internal/allocator/allocator_test.go
+++ b/internal/allocator/allocator_test.go
@@ -8,6 +8,8 @@ import (
 	"testing"
 
 	"go.universe.tf/metallb/internal/config"
+
+	ptu "github.com/prometheus/client_golang/prometheus/testutil"
 )
 
 func TestAssignment(t *testing.T) {
@@ -1226,6 +1228,131 @@ func TestPoolCount(t *testing.T) {
 	}
 }
 
+func TestPoolMetrics(t *testing.T) {
+	alloc := New()
+	if err := alloc.SetPools(map[string]*config.Pool{
+		"test": {
+			AutoAssign: true,
+			CIDR: []*net.IPNet{
+				ipnet("1.2.3.4/30"),
+				ipnet("1000::4/126"),
+			},
+		},
+	}); err != nil {
+		t.Fatalf("SetPools: %s", err)
+	}
+
+	tests := []struct {
+		desc       string
+		svc        string
+		ip         string
+		ports      []Port
+		sharingKey string
+		backendKey string
+		ipsInUse   float64
+	}{
+		{
+			desc:     "assign s1",
+			svc:      "s1",
+			ip:       "1.2.3.4",
+			ipsInUse: 1,
+		},
+		{
+			desc:     "assign s2",
+			svc:      "s2",
+			ip:       "1.2.3.5",
+			ipsInUse: 2,
+		},
+		{
+			desc:     "unassign s1",
+			svc:      "s1",
+			ipsInUse: 1,
+		},
+		{
+			desc:     "unassign s2",
+			svc:      "s2",
+			ipsInUse: 0,
+		},
+		{
+			desc:       "assign s1 shared",
+			svc:        "s1",
+			ip:         "1.2.3.4",
+			sharingKey: "key",
+			ports:      ports("tcp/80"),
+			ipsInUse:   1,
+		},
+		{
+			desc:       "assign s2 shared",
+			svc:        "s2",
+			ip:         "1.2.3.4",
+			sharingKey: "key",
+			ports:      ports("tcp/443"),
+			ipsInUse:   1,
+		},
+		{
+			desc:       "assign s3 shared",
+			svc:        "s3",
+			ip:         "1.2.3.4",
+			sharingKey: "key",
+			ports:      ports("tcp/23"),
+			ipsInUse:   1,
+		},
+		{
+			desc:     "unassign s1 shared",
+			svc:      "s1",
+			ports:    ports("tcp/80"),
+			ipsInUse: 1,
+		},
+		{
+			desc:     "unassign s2 shared",
+			svc:      "s2",
+			ports:    ports("tcp/443"),
+			ipsInUse: 1,
+		},
+		{
+			desc:     "unassign s3 shared",
+			svc:      "s3",
+			ports:    ports("tcp/23"),
+			ipsInUse: 0,
+		},
+	}
+
+	// The "test" pool contains two ranges; 1.2.3.4/30, 1000::4/126
+	// All bits can be used for lb-addresses which gives a total capacity of; 4+4=8
+	value := ptu.ToFloat64(stats.poolCapacity.WithLabelValues("test"))
+	if int(value) != 8 {
+		t.Errorf("stats.poolCapacity invalid %f. Expected 8", value)
+	}
+
+	for _, test := range tests {
+		if test.ip == "" {
+			alloc.Unassign(test.svc)
+			value := ptu.ToFloat64(stats.poolActive.WithLabelValues("test"))
+			if value != test.ipsInUse {
+				t.Errorf("%v; in-use %v. Expected %v", test.desc, value, test.ipsInUse)
+			}
+			continue
+		}
+
+		ip := net.ParseIP(test.ip)
+		if ip == nil {
+			t.Fatalf("invalid IP %q in test %q", test.ip, test.desc)
+		}
+		err := alloc.Assign(test.svc, ip, test.ports, test.sharingKey, test.backendKey)
+
+		if err != nil {
+			t.Errorf("%q: Assign(%q, %q): %v", test.desc, test.svc, test.ip, err)
+		}
+		if a := assigned(alloc, test.svc); a != test.ip {
+			t.Errorf("%q: ran Assign(%q, %q), but allocator has recorded allocation of %q", test.desc, test.svc, test.ip, a)
+		}
+		value := ptu.ToFloat64(stats.poolActive.WithLabelValues("test"))
+		if value != test.ipsInUse {
+			t.Errorf("%v; in-use %v. Expected %v", test.desc, value, test.ipsInUse)
+		}
+	}
+}
+
 // Some helpers
 
 func assigned(a *Allocator, svc string) string {