Repository: docker/compose (go)

Issue:
[BUG] goroutine leak in `init()` of `v2/cmd/formatter` ?
### Description

Trying to build something that depends on `github.com/docker/compose/v2`, then ran into this issue, because all of our tests checks for goroutine leaks at the end of running.

### Steps To Reproduce

Run following test


```go
package main

import (
	"testing"

	_ "github.com/docker/compose/v2/cmd/formatter"
	"go.uber.org/goleak"
)

func TestLeak(t *testing.T) {
	defer goleak.VerifyNone(t)
}
```

Output:


```shell
$ go test
--- FAIL: TestLeak (0.45s)
    main_test.go:12: found unexpected goroutines:
        [Goroutine 19 in state chan send, with github.com/docker/compose/v2/cmd/formatter.init.0.func1 on top of the stack:
        goroutine 19 [chan send]:
        github.com/docker/compose/v2/cmd/formatter.init.0.func1()
                /home/wxh/go/pkg/mod/github.com/docker/compose/v2@v2.14.2/cmd/formatter/colors.go:120 +0x1de
        created by github.com/docker/compose/v2/cmd/formatter.init.0
                /home/wxh/go/pkg/mod/github.com/docker/compose/v2@v2.14.2/cmd/formatter/colors.go:104 +0x8b
        ]
FAIL
exit status 1
FAIL    tt      0.451s
```


This is because there is a non-terminating goroutine in the `init()`:

```go
	go func() {
		i := 0
		rainbow := []colorFunc{
			colors["cyan"],
			colors["yellow"],
			colors["green"],
			colors["magenta"],
			colors["blue"],
			colors["intense_cyan"],
			colors["intense_yellow"],
			colors["intense_green"],
			colors["intense_magenta"],
			colors["intense_blue"],
		}

		for {
			loop <- rainbow[i]
			i = (i + 1) % len(rainbow)
		}
	}()
```

### Workaround

Not a best practice to bypass questionable func, but...

```go
	defer goleak.VerifyNone(t, goleak.IgnoreTopFunction("github.com/docker/compose/v2/cmd/formatter.init.0.func1"))
```

### Compose Version

```Text
https://github.com/docker/compose/blob/v2.14.2/cmd/formatter/colors.go
```


### Docker Environment

_No response_

### Anything else?

_No response_

Pull request description:
10157 fix goroutine leak v2
**What I did**
Fixed the goroutine leak, by removing the infinite goroutine that will send colors to the channel, and then refactored the `rainbowColor()` function inside the `colors.go`, to replace the logic of the infinite goroutine. 

**Related issue**
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
Fixes #10157.


**Screenshots of testing**
<img width="999" alt="Screen Shot 2023-01-25 at 10 07 22 AM" src="https://user-images.githubusercontent.com/48932084/214524393-ac10259a-027b-499d-9fb0-7c2dc2835e82.png">

<img width="480" alt="Screen Shot 2023-01-25 at 10 08 51 AM" src="https://user-images.githubusercontent.com/48932084/214524464-e8bf8520-6efa-4e6a-bafc-2dc5e2d7dde3.png">

Patch:
diff --git a/cmd/formatter/colors.go b/cmd/formatter/colors.go
index 79afb5354..e050a6aef 100644
--- a/cmd/formatter/colors.go
+++ b/cmd/formatter/colors.go
@@ -19,6 +19,7 @@ package formatter
 import (
 	"fmt"
 	"strconv"
+	"sync"
 
 	"github.com/docker/compose/v2/pkg/api"
 )
@@ -86,38 +87,34 @@ func makeColorFunc(code string) colorFunc {
 }
 
 var nextColor = rainbowColor
+var rainbow []colorFunc
+var currentIndex = 0
+var mutex sync.Mutex
 
 func rainbowColor() colorFunc {
-	return <-loop
+	mutex.Lock()
+	defer mutex.Unlock()
+	result := rainbow[currentIndex]
+	currentIndex = (currentIndex + 1) % len(rainbow)
+	return result
 }
 
-var loop = make(chan colorFunc)
-
 func init() {
 	colors := map[string]colorFunc{}
 	for i, name := range names {
 		colors[name] = makeColorFunc(strconv.Itoa(30 + i))
 		colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1")
 	}
-
-	go func() {
-		i := 0
-		rainbow := []colorFunc{
-			colors["cyan"],
-			colors["yellow"],
-			colors["green"],
-			colors["magenta"],
-			colors["blue"],
-			colors["intense_cyan"],
-			colors["intense_yellow"],
-			colors["intense_green"],
-			colors["intense_magenta"],
-			colors["intense_blue"],
-		}
-
-		for {
-			loop <- rainbow[i]
-			i = (i + 1) % len(rainbow)
-		}
-	}()
+	rainbow = []colorFunc{
+		colors["cyan"],
+		colors["yellow"],
+		colors["green"],
+		colors["magenta"],
+		colors["blue"],
+		colors["intense_cyan"],
+		colors["intense_yellow"],
+		colors["intense_green"],
+		colors["intense_magenta"],
+		colors["intense_blue"],
+	}
 }
diff --git a/go.mod b/go.mod
index 47e1a9e0b..de537124c 100644
--- a/go.mod
+++ b/go.mod
@@ -150,6 +150,8 @@ require (
 	sigs.k8s.io/yaml v1.2.0 // indirect
 )
 
+require go.uber.org/goleak v1.1.12
+
 replace (
 	// Override for e2e tests
 	github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
diff --git a/go.sum b/go.sum
index da0b87b2e..b10e13382 100644
--- a/go.sum
+++ b/go.sum
@@ -770,6 +770,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@@ -978,6 +979,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=


Test patch:
diff --git a/cmd/formatter/formatter_test.go b/cmd/formatter/formatter_test.go
index 052226e65..9da0dc85c 100644
--- a/cmd/formatter/formatter_test.go
+++ b/cmd/formatter/formatter_test.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"testing"
 
+	"go.uber.org/goleak"
 	"gotest.tools/v3/assert"
 )
 
@@ -71,3 +72,7 @@ func TestPrint(t *testing.T) {
 {"Name":"myName2","Status":"myStatus2"}
 `)
 }
+
+func TestColorsGoroutinesLeak(t *testing.T) {
+	goleak.VerifyNone(t)
+}