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
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
matrix:
go-version: [1.21.x, 1.x]
platform: [ubuntu-latest]
module: [artemis, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, elasticsearch, gcloud, inbucket, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, surrealdb, vault, weaviate]
module: [artemis, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, elasticsearch, gcloud, inbucket, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, vault, weaviate]
uses: ./.github/workflows/ci-test-go.yml
with:
go-version: ${{ matrix.go-version }}
Expand Down
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@
"name": "module / redpanda",
"path": "../modules/redpanda"
},
{
"name": "module / registry",
"path": "../modules/registry"
},
{
"name": "module / surrealdb",
"path": "../modules/surrealdb"
Expand Down
113 changes: 113 additions & 0 deletions docs/modules/registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Registry

Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

The Testcontainers module for Registry.

## Adding this module to your project dependencies

Please run the following command to add the Registry module to your Go dependencies:

```
go get github.com/testcontainers/testcontainers-go/modules/registry
```

## Usage example

<!--codeinclude-->
[Creating a Registry container](../../modules/registry/examples_test.go) inside_block:runRegistryContainer
<!--/codeinclude-->

## Module reference

The Registry module exposes one entrypoint function to create the Registry container, and this function receives two parameters:

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*RegistryContainer, error)
```

- `context.Context`, the Go context.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

When starting the Registry container, you can pass options in a variadic way to configure it.

#### Image

If you need to set a different Registry Docker image, you can use `testcontainers.WithImage` with a valid Docker image
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-->

#### ImageExists

The `ImageExists` method allows to check if an image exists in the Registry. It receives the Go context and the image reference as parameters.

!!! info
The image reference should be in the format `my-registry:port/image:tag` in order to be pushed to the Registry.

#### PushImage

The `PushImage` method allows to push an image to the Registry. It receives the Go context and the image reference as parameters.

!!! info
The image reference should be in the format `my-registry:port/image:tag` in order to be pushed to the Registry.

<!--codeinclude-->
[Pushing images to the registry](../../modules/registry/examples_test.go) inside_block:pushingImage
<!--/codeinclude-->

If the push operation is successful, the method will internally wait for the image to be available in the Registry, querying the Registry API, returning an error in case of any failure (e.g. pushing or waiting for the image).

#### DeleteImage

The `DeleteImage` method allows to delete an image from the Registry. It receives the Go context and the image reference as parameters.

!!! info
The image reference should be in the format `image:tag` in order to be deleted from the Registry.

<!--codeinclude-->
[Deleting images from the registry](../../modules/registry/examples_test.go) inside_block:deletingImage
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ nav:
- modules/rabbitmq.md
- modules/redis.md
- modules/redpanda.md
- modules/registry.md
- modules/surrealdb.md
- modules/vault.md
- modules/weaviate.md
Expand Down
5 changes: 5 additions & 0 deletions modules/registry/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-registry
246 changes: 246 additions & 0 deletions modules/registry/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package registry_test

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() {
// runRegistryContainer {
registryContainer, err := registry.RunContainer(context.Background(), testcontainers.WithImage("registry:2.8.3"))
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

// Clean up the container
defer func() {
if err := registryContainer.Terminate(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
// }

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

fmt.Println(state.Running)

// Output:
// true
}

func ExampleRunContainer_withAuthentication() {
// htpasswdFile {
registryContainer, err := registry.RunContainer(
context.Background(),
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(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

registryPort, err := registryContainer.MappedPort(context.Background(), "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,
// using RegistryName of the container as the registry.

redisC, err := testcontainers.GenericContainer(context.Background(), 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(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

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

fmt.Println(state.Running)

// Output:
// true
}

func ExampleRunContainer_pushImage() {
registryContainer, err := registry.RunContainer(
context.Background(),
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(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

registryPort, err := registryContainer.MappedPort(context.Background(), "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
// Besides, we are also setting the authentication
// for both the registry and localhost to make sure
// the image is pushed to the private registry.
os.Setenv("DOCKER_AUTH_CONFIG", `{
"auths": {
"localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" },
"`+registryContainer.RegistryName+`": { "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,
// using RegistryName of the container as the registry.
// We are agoing to build the image with a fixed tag
// that matches the private registry, and we are going to
// push it again to the registry after the build.

repo := registryContainer.RegistryName + "/customredis"
tag := "v1.2.3"

redisC, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join("testdata", "redis"),
BuildArgs: map[string]*string{
"REGISTRY_PORT": &strPort,
},
Repo: repo,
Tag: tag,
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(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

// pushingImage {
// repo is localhost:32878/customredis
// tag is v1.2.3
err = registryContainer.PushImage(context.Background(), fmt.Sprintf("%s:%s", repo, tag))
if err != nil {
log.Fatalf("failed to push image: %s", err) // nolint:gocritic
}
// }

newImage := fmt.Sprintf("%s:%s", repo, tag)

// now run a container from the new image
// But first remove the local image to avoid using the local one.

// deletingImage {
// newImage is customredis:v1.2.3
err = registryContainer.DeleteImage(context.Background(), newImage)
if err != nil {
log.Fatalf("failed to delete image: %s", err) // nolint:gocritic
}
// }

newRedisC, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: newImage,
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
},
Started: true,
})
if err != nil {
log.Fatalf("failed to start container from %s: %s", newImage, err) // nolint:gocritic
}
defer func() {
if err := newRedisC.Terminate(context.Background()); err != nil {
log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()

state, err := newRedisC.State(context.Background())
if err != nil {
log.Fatalf("failed to get redis container state from %s: %s", newImage, err) // nolint:gocritic
}

fmt.Println(state.Running)

// Output:
// true
}