Skip to content

Commit

Permalink
Merge branch 'main' into refactor/ollama-local
Browse files Browse the repository at this point in the history
  • Loading branch information
mdelapenya authored Dec 20, 2024
2 parents 2f2865b + 63fad4d commit 9927ad0
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 12 deletions.
21 changes: 17 additions & 4 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type ImageBuildInfo interface {
GetDockerfile() string // the relative path to the Dockerfile, including the file itself
GetRepo() string // get repo label for image
GetTag() string // get tag label for image
ShouldPrintBuildLog() bool // allow build log to be printed to stdout
BuildLogWriter() io.Writer // for output of build log, use io.Discard to disable the output
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the from Dockerfile
GetAuthConfigs() map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Return the auth configs to be able to pull from an authenticated docker registry
Expand All @@ -92,7 +92,8 @@ type FromDockerfile struct {
Repo string // the repo label for image, defaults to UUID
Tag string // the tag label for image, defaults to UUID
BuildArgs map[string]*string // enable user to pass build args to docker daemon
PrintBuildLog bool // enable user to print build log
PrintBuildLog bool // Deprecated: Use BuildLogWriter instead
BuildLogWriter io.Writer // for output of build log, defaults to io.Discard
AuthConfigs map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Enable auth configs to be able to pull from an authenticated docker registry
// KeepImage describes whether DockerContainer.Terminate should not delete the
// container image. Useful for images that are built from a Dockerfile and take a
Expand Down Expand Up @@ -410,8 +411,20 @@ func (c *ContainerRequest) ShouldKeepBuiltImage() bool {
return c.FromDockerfile.KeepImage
}

func (c *ContainerRequest) ShouldPrintBuildLog() bool {
return c.FromDockerfile.PrintBuildLog
// BuildLogWriter returns the io.Writer for output of log when building a Docker image from
// a Dockerfile. It returns the BuildLogWriter from the ContainerRequest, defaults to io.Discard.
// For backward compatibility, if BuildLogWriter is default and PrintBuildLog is true,
// the function returns os.Stderr.
func (c *ContainerRequest) BuildLogWriter() io.Writer {
if c.FromDockerfile.BuildLogWriter != nil {
return c.FromDockerfile.BuildLogWriter
}
if c.FromDockerfile.PrintBuildLog {
c.FromDockerfile.BuildLogWriter = os.Stderr
} else {
c.FromDockerfile.BuildLogWriter = io.Discard
}
return c.FromDockerfile.BuildLogWriter
}

// BuildOptions returns the image build options when building a Docker image from a Dockerfile.
Expand Down
17 changes: 12 additions & 5 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1004,10 +1004,7 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st
}
defer resp.Body.Close()

output := io.Discard
if img.ShouldPrintBuildLog() {
output = os.Stderr
}
output := img.BuildLogWriter()

// Always process the output, even if it is not printed
// to ensure that errors during the build process are
Expand Down Expand Up @@ -1498,7 +1495,11 @@ func (p *DockerProvider) daemonHostLocked(ctx context.Context) (string, error) {
p.hostCache = daemonURL.Hostname()
case "unix", "npipe":
if core.InAContainer() {
ip, err := p.GetGatewayIP(ctx)
defaultNetwork, err := p.ensureDefaultNetworkLocked(ctx)
if err != nil {
return "", fmt.Errorf("ensure default network: %w", err)
}
ip, err := p.getGatewayIP(ctx, defaultNetwork)
if err != nil {
ip, err = core.DefaultGatewayIP()
if err != nil {
Expand Down Expand Up @@ -1598,7 +1599,10 @@ func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) {
if err != nil {
return "", fmt.Errorf("ensure default network: %w", err)
}
return p.getGatewayIP(ctx, defaultNetwork)
}

func (p *DockerProvider) getGatewayIP(ctx context.Context, defaultNetwork string) (string, error) {
nw, err := p.GetNetwork(ctx, NetworkRequest{Name: defaultNetwork})
if err != nil {
return "", err
Expand All @@ -1624,7 +1628,10 @@ func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) {
func (p *DockerProvider) ensureDefaultNetwork(ctx context.Context) (string, error) {
p.mtx.Lock()
defer p.mtx.Unlock()
return p.ensureDefaultNetworkLocked(ctx)
}

func (p *DockerProvider) ensureDefaultNetworkLocked(ctx context.Context) (string, error) {
if p.defaultNetwork != "" {
// Already set.
return p.defaultNetwork, nil
Expand Down
69 changes: 69 additions & 0 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/testcontainers/testcontainers-go/internal/core"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand All @@ -35,6 +36,7 @@ const (
nginxAlpineImage = "nginx:alpine"
nginxDefaultPort = "80/tcp"
nginxHighPort = "8080/tcp"
golangImage = "golang"
daemonMaxVersion = "1.41"
)

Expand Down Expand Up @@ -705,6 +707,37 @@ func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) {
assert.Regexpf(t, `^Step\s*1/\d+\s*:\s*FROM alpine$`, temp[0], "Expected stdout first line to be %s. Got '%s'.", "Step 1/* : FROM alpine", temp[0])
}

func Test_BuildContainerFromDockerfileWithBuildLogWriter(t *testing.T) {
var buffer bytes.Buffer

ctx := context.Background()

// fromDockerfile {
req := ContainerRequest{
FromDockerfile: FromDockerfile{
Context: filepath.Join(".", "testdata"),
Dockerfile: "buildlog.Dockerfile",
BuildLogWriter: &buffer,
},
}
// }

genContainerReq := GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Started: true,
}

c, err := GenericContainer(ctx, genContainerReq)
CleanupContainer(t, c)
require.NoError(t, err)

out := buffer.String()
temp := strings.Split(out, "\n")
require.NotEmpty(t, temp)
require.Regexpf(t, `^Step\s*1/\d+\s*:\s*FROM alpine$`, temp[0], "Expected stdout first line to be %s. Got '%s'.", "Step 1/* : FROM alpine", temp[0])
}

func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) {
ctx := context.Background()
req := ContainerRequest{
Expand Down Expand Up @@ -2125,3 +2158,39 @@ func TestCustomPrefixTrailingSlashIsProperlyRemovedIfPresent(t *testing.T) {
dockerContainer := c.(*DockerContainer)
require.Equal(t, fmt.Sprintf("%s%s", hubPrefixWithTrailingSlash, dockerImage), dockerContainer.Image)
}

// TODO: remove this skip check when context rework is merged alongside [core.DockerEnvFile] removal.
func Test_Provider_DaemonHost_Issue2897(t *testing.T) {
ctx := context.Background()
provider, err := NewDockerProvider()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, provider.Close())
})

orig := core.DockerEnvFile
core.DockerEnvFile = filepath.Join(t.TempDir(), ".dockerenv")
t.Cleanup(func() {
core.DockerEnvFile = orig
})

f, err := os.Create(core.DockerEnvFile)
require.NoError(t, err)
require.NoError(t, f.Close())
t.Cleanup(func() {
require.NoError(t, os.Remove(f.Name()))
})

errCh := make(chan error, 1)
go func() {
_, err := provider.DaemonHost(ctx)
errCh <- err
}()

select {
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for DaemonHost")
case err := <-errCh:
require.NoError(t, err)
}
}
40 changes: 39 additions & 1 deletion docs/features/wait/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
The Log wait strategy will check if a string occurs in the container logs for a desired number of times, and allows to set the following conditions:

- the string to be waited for in the container log.
- the number of occurrences of the string to wait for, default is `1`.
- the number of occurrences of the string to wait for, default is `1` (ignored for Submatch).
- look for the string using a regular expression, default is `false`.
- the startup timeout to be used in seconds, default is 60 seconds.
- the poll interval to be used in milliseconds, default is 100 milliseconds.
- the regular expression submatch callback, default nil (occurrences is ignored).

```golang
req := ContainerRequest{
Expand All @@ -33,3 +34,40 @@ req := ContainerRequest{
WaitingFor: wait.ForLog(`.*MySQL Community Server`).AsRegexp(),
}
```

Using regular expression with submatch:

```golang
var host, port string
req := ContainerRequest{
Image: "ollama/ollama:0.1.25",
ExposedPorts: []string{"11434/tcp"},
WaitingFor: wait.ForLog(`Listening on (.*:\d+) \(version\s(.*)\)`).Submatch(func(pattern string, submatches [][][]byte) error {
var err error
for _, matches := range submatches {
if len(matches) != 3 {
err = fmt.Errorf("`%s` matched %d times, expected %d", pattern, len(matches), 3)
continue
}
host, port, err = net.SplitHostPort(string(matches[1]))
if err != nil {
return wait.NewPermanentError(fmt.Errorf("split host port: %w", err))
}

// Host and port successfully extracted from log.
return nil
}

if err != nil {
// Return the last error encountered.
return err
}

return fmt.Errorf("address and version not found: `%s` no matches", pattern)
}),
}
```

If the return from a Submatch callback function is a `wait.PermanentError` the
wait will stop and the error will be returned. Use `wait.NewPermanentError(err error)`
to achieve this.
7 changes: 6 additions & 1 deletion internal/core/docker_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,15 @@ func testcontainersHostFromProperties(ctx context.Context) (string, error) {
return "", ErrTestcontainersHostNotSetInProperties
}

// DockerEnvFile is the file that is created when running inside a container.
// It's a variable to allow testing.
// TODO: Remove this once context rework is done, which eliminates need for the default network creation.
var DockerEnvFile = "/.dockerenv"

// InAContainer returns true if the code is running inside a container
// See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25
func InAContainer() bool {
return inAContainer("/.dockerenv")
return inAContainer(DockerEnvFile)
}

func inAContainer(path string) bool {
Expand Down
5 changes: 5 additions & 0 deletions network/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,8 @@ func TestWithNewNetworkContextTimeout(t *testing.T) {
require.Empty(t, req.Networks)
require.Empty(t, req.NetworkAliases)
}

func TestCleanupWithNil(t *testing.T) {
var network *testcontainers.DockerNetwork
testcontainers.CleanupNetwork(t, network)
}
4 changes: 3 additions & 1 deletion testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ func CleanupNetwork(tb testing.TB, network Network) {
tb.Helper()

tb.Cleanup(func() {
noErrorOrIgnored(tb, network.Remove(context.Background()))
if !isNil(network) {
noErrorOrIgnored(tb, network.Remove(context.Background()))
}
})
}

Expand Down

0 comments on commit 9927ad0

Please sign in to comment.