Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add distribution-registry module #2341

Merged
merged 13 commits into from
Mar 22, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: add options and tests for the module
mdelapenya committed Mar 11, 2024
commit a2b4b34ac34c564a554839aa9a504401ad2d4aef
35 changes: 35 additions & 0 deletions docs/modules/registry.md
Original file line number Diff line number Diff line change
@@ -42,6 +42,41 @@ for Registry. E.g. `testcontainers.WithImage("registry:2.8.3")`.

{% include "../features/common_functional_options.md" %}

#### With Authentication

It's possible to enable authentication for the Registry container. By default, it is disabled, but you can enable it in two ways:
- You can use `WithHtpasswd` to enable authentication with a string representing the contents of a `htpasswd` file.
A temporary file will be created with the contents of the string and copied to the container.
- You can use `WithHtpasswdFile` to copy a `htpasswd` file from your local filesystem to the container.
In both cases, the `htpasswd` file will be copied into the `/auth` directory inside the container.
<!--codeinclude-->
[Htpasswd string](../../modules/registry/registry_test.go) inside_block:htpasswdString
[Htpasswd file](../../modules/registry/examples_test.go) inside_block:htpasswdFile
<!--/codeinclude-->
#### WithData
In the case you want to initialise the Registry with your own images, you can use `WithData` to copy a directory from your local filesystem to the container.
The directory will be copied into the `/data` directory inside the container.
The format of the directory should be the same as the one used by the Registry to store images.
Otherwise, the Registry will start but you won't be able to read any images from it.

<!--codeinclude-->
[Including data](../../modules/registry/examples_test.go) inside_block:htpasswdFile
<!--/codeinclude-->

### Container Methods

The Registry container exposes the following methods:

#### Address

This method returns the HTTP address string to connect to the Distribution Registry, so that you can use to connect to the Registry.
E.g. `http://localhost:32878/v2/_catalog`.

<!--codeinclude-->
[HTTP Address](../../modules/registry/registry_test.go) inside_block:httpAddress
<!--/codeinclude-->
83 changes: 83 additions & 0 deletions modules/registry/examples_test.go
Original file line number Diff line number Diff line change
@@ -4,9 +4,12 @@ import (
"context"
"fmt"
"log"
"os"
"path/filepath"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/registry"
"github.com/testcontainers/testcontainers-go/wait"
)

func ExampleRunContainer() {
@@ -36,3 +39,83 @@ func ExampleRunContainer() {
// Output:
// true
}

func ExampleRunContainer_withAuthentication() {
ctx := context.Background()

// htpasswdFile {
registryContainer, err := registry.RunContainer(
ctx, testcontainers.WithImage("registry:2.8.3"),
registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")),
registry.WithData(filepath.Join("testdata", "data")),
)
// }
if err != nil {
log.Fatalf("failed to start container: %s", err)
}
defer func() {
if err := registryContainer.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

registryPort, err := registryContainer.MappedPort(ctx, "5000/tcp")
if err != nil {
log.Fatalf("failed to get mapped port: %s", err) // nolint:gocritic
}
strPort := registryPort.Port()

previousAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG")

// make sure the Docker Auth credentials are set
// using the same as in the Docker Registry
// testuser:testpassword
os.Setenv("DOCKER_AUTH_CONFIG", `{
"auths": {
"localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" }
},
"credsStore": "desktop"
}`)
defer func() {
// reset the original state after the example.
os.Unsetenv("DOCKER_AUTH_CONFIG")
os.Setenv("DOCKER_AUTH_CONFIG", previousAuthConfig)
}()

// build a custom redis image from the private registry
// it will use localhost:$exposedPort as the registry

redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join("testdata", "redis"),
BuildArgs: map[string]*string{
"REGISTRY_PORT": &strPort,
},
PrintBuildLog: true,
},
AlwaysPullImage: true, // make sure the authentication takes place
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
},
Started: true,
})
if err != nil {
log.Fatalf("failed to start container: %s", err) // nolint:gocritic
}
defer func() {
if err := redisC.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

state, err := redisC.State(ctx)
if err != nil {
log.Fatalf("failed to get redis container state: %s", err) // nolint:gocritic
}

fmt.Println(state.Running)

// Output:
// true
}
74 changes: 74 additions & 0 deletions modules/registry/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package registry

import (
"os"
"path/filepath"

"github.com/testcontainers/testcontainers-go"
)

const (
containerDataPath string = "/data"
containerHtpasswdPath string = "/auth/htpasswd"
)

// WithData is a custom option to set the data directory for the registry,
// which is used to store the images. It will copy the data from the host to
// the container in the /data path. The container will be configured to use
// this path as the root directory for the registry, thanks to the
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY environment variable.
// The dataPath must have the same structure as the registry data directory.
func WithData(dataPath string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
req.Files = append(req.Files, testcontainers.ContainerFile{
HostFilePath: dataPath,
ContainerFilePath: containerDataPath,
})

req.Env["REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY"] = containerDataPath
}
}

// WithHtpasswd is a custom option to set the htpasswd credentials for the registry
// It will create a temporary file with the credentials and copy it to the container
// in the /auth/htpasswd path. The container will be configured to use this file as
// the htpasswd file, thanks to the REGISTRY_AUTH_HTPASSWD_PATH environment variable.
func WithHtpasswd(credentials string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
tmpFile, err := os.Create(filepath.Join(os.TempDir(), "htpasswd"))
if err != nil {
tmpFile, err = os.Create(".")
if err != nil {
// cannot create the file in the temp dir or in the current dir
panic(err)
}
}
defer tmpFile.Close()

_, err = tmpFile.WriteString(credentials)
if err != nil {
panic(err)
}

WithHtpasswdFile(tmpFile.Name())(req)
}
}

// WithHtpasswdFile is a custom option to set the htpasswd file for the registry
// It will copy a file with the credentials in the /auth/htpasswd path.
// The container will be configured to use this file as the htpasswd file,
// thanks to the REGISTRY_AUTH_HTPASSWD_PATH environment variable.
func WithHtpasswdFile(htpasswdPath string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
req.Files = append(req.Files, testcontainers.ContainerFile{
HostFilePath: htpasswdPath,
ContainerFilePath: containerHtpasswdPath,
FileMode: 0o644,
})

req.Env["REGISTRY_AUTH"] = "htpasswd"
req.Env["REGISTRY_AUTH_HTPASSWD_REALM"] = "Registry"
req.Env["REGISTRY_AUTH_HTPASSWD_PATH"] = containerHtpasswdPath
req.Env["REGISTRY_AUTH_HTPASSWD_PATH"] = containerHtpasswdPath
}
}
22 changes: 21 additions & 1 deletion modules/registry/registry.go
Original file line number Diff line number Diff line change
@@ -2,19 +2,39 @@ package registry

import (
"context"
"fmt"

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

// RegistryContainer represents the Registry container type used in the module
type RegistryContainer struct {
testcontainers.Container
}

// Address returns the address of the Registry container, using the HTTP protocol
func (c *RegistryContainer) Address(ctx context.Context) (string, error) {
port, err := c.MappedPort(ctx, "5000")
if err != nil {
return "", err
}

ipAddress, err := c.Host(ctx)
if err != nil {
return "", err
}

return fmt.Sprintf("http://%s:%s", ipAddress, port.Port()), nil
}

// RunContainer creates an instance of the Registry container type
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*RegistryContainer, error) {
req := testcontainers.ContainerRequest{
Image: "registry:2.8.3",
Image: "registry:2.8.3",
ExposedPorts: []string{"5000/tcp"},
Env: map[string]string{},
WaitingFor: wait.ForExposedPort(),
}

genericContainerReq := testcontainers.GenericContainerRequest{
Loading