Skip to content

Commit

Permalink
add tests_resource
Browse files Browse the repository at this point in the history
  • Loading branch information
joshrwolf committed Jan 29, 2025
1 parent 1446ea6 commit 405886d
Show file tree
Hide file tree
Showing 22 changed files with 2,097 additions and 15 deletions.
104 changes: 104 additions & 0 deletions docs/resources/tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "imagetest_tests Resource - terraform-provider-imagetest"
subcategory: ""
description: |-
---

# imagetest_tests (Resource)





<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `driver` (String) The driver to use for the test suite. Only one driver can be used at a time.
- `images` (Map of String) Images to use for the test suite.

### Optional

- `drivers` (Attributes) The resource specific driver configuration. This is merged with the provider scoped drivers configuration. (see [below for nested schema](#nestedatt--drivers))
- `name` (String) The name of the test. If one is not provided, a random name will be generated.
- `tests` (Attributes List) An ordered list of test suites to run (see [below for nested schema](#nestedatt--tests))
- `timeout` (String) The maximum amount of time to wait for all tests to complete. This includes the time it takes to start and destroy the driver.

### Read-Only

- `id` (String) The unique identifier for the test. If a name is provided, this will be the name appended with a random suffix.

<a id="nestedatt--drivers"></a>
### Nested Schema for `drivers`

Optional:

- `docker_in_docker` (Attributes) The docker_in_docker driver (see [below for nested schema](#nestedatt--drivers--docker_in_docker))
- `k3s_in_docker` (Attributes) The k3s_in_docker driver (see [below for nested schema](#nestedatt--drivers--k3s_in_docker))

<a id="nestedatt--drivers--docker_in_docker"></a>
### Nested Schema for `drivers.docker_in_docker`

Optional:

- `image` (String) The image reference to use for the docker-in-docker driver


<a id="nestedatt--drivers--k3s_in_docker"></a>
### Nested Schema for `drivers.k3s_in_docker`

Optional:

- `cni` (Boolean) Enable the CNI plugin
- `image` (String) The image reference to use for the k3s_in_docker driver
- `metrics_server` (Boolean) Enable the metrics server
- `network_policy` (Boolean) Enable the network policy
- `registries` (Attributes Map) A map of registries containing configuration for optional auth, tls, and mirror configuration. (see [below for nested schema](#nestedatt--drivers--k3s_in_docker--registries))
- `traefik` (Boolean) Enable the traefik ingress controller

<a id="nestedatt--drivers--k3s_in_docker--registries"></a>
### Nested Schema for `drivers.k3s_in_docker.registries`

Optional:

- `mirrors` (Attributes) A map of registries containing configuration for optional auth, tls, and mirror configuration. (see [below for nested schema](#nestedatt--drivers--k3s_in_docker--registries--mirrors))

<a id="nestedatt--drivers--k3s_in_docker--registries--mirrors"></a>
### Nested Schema for `drivers.k3s_in_docker.registries.mirrors`

Optional:

- `endpoints` (List of String)





<a id="nestedatt--tests"></a>
### Nested Schema for `tests`

Required:

- `image` (String) The image reference to use as the base image for the test.
- `name` (String) The name of the test

Optional:

- `cmd` (String) When specified, will override the sandbox image's CMD (oci config).
- `content` (Attributes List) The content to use for the test (see [below for nested schema](#nestedatt--tests--content))
- `envs` (Map of String) Environment variables to set on the test container. These will overwrite the environment variables set in the image's config on conflicts.
- `timeout` (String) The maximum amount of time to wait for the individual test to complete. This is encompassed by the overall timeout of the parent tests resource.

<a id="nestedatt--tests--content"></a>
### Nested Schema for `tests.content`

Required:

- `source` (String) The source path to use for the test

Optional:

- `target` (String) The target path to use for the test
32 changes: 32 additions & 0 deletions examples/tests_resource/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
terraform {
required_providers {
imagetest = {
source = "registry.terraform.io/chainguard-dev/imagetest"
}
}
backend "inmem" {}
}

locals { repo = "localhost:55232/foo" }

provider "imagetest" {
repo = local.repo
}

resource "imagetest_tests" "foo" {
name = "foo"
driver = "k3s_in_docker"

images = {
foo = "cgr.dev/chainguard/busybox:latest@sha256:b7fc3eef4303188eb295aaf8e02d888ced307d2a45090d6f673b95a41bfc033d"
}

tests = [
{
name = "sample"
image = "cgr.dev/chainguard/kubectl:latest-dev@sha256:5751a1672a7debcc5e847bc1cc6ebfc8899aad188ff90f0445bfef194a9fa512"
content = [{ source = "${path.module}/tests" }]
cmd = "/imagetest/foo.sh"
}
]
}
11 changes: 11 additions & 0 deletions examples/tests_resource/tests/foo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh

# Test sandbox _always_ has these set via the entrypoint wrapper
# set -eux -o pipefail

# Test sandbox environment is based on wolfi
apk add jq

# Test sandbox always has $IMAGES, which are the terraform images parsed into
# their constituent parts
echo "$IMAGES" | jq '.'
99 changes: 99 additions & 0 deletions internal/bundler/append.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,105 @@ type AppendOpts struct {
Entrypoint []string
}

type MutateOpts struct {
RemoteOptions []remote.Option
ImageMutators []func(base v1.Image) (v1.Image, error)
}

func Mutate(ctx context.Context, base name.Reference, target name.Repository, opts MutateOpts) (name.Reference, error) {
desc, err := remote.Get(base, opts.RemoteOptions...)
if err != nil {
return nil, fmt.Errorf("failed to get image: %w", err)
}

if desc.MediaType.IsIndex() {
baseidx, err := desc.ImageIndex()
if err != nil {
return nil, fmt.Errorf("failed to get image index: %w", err)
}

var midx v1.ImageIndex = empty.Index

mfst, err := baseidx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get index manifest: %w", err)
}

for _, m := range mfst.Manifests {
img, err := baseidx.Image(m.Digest)
if err != nil {
return nil, fmt.Errorf("failed to load image: %w", err)
}

for _, mutator := range opts.ImageMutators {
img, err = mutator(img)
if err != nil {
return nil, fmt.Errorf("failed to mutate image: %w", err)
}
}

dig, err := img.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}

if err := remote.Write(target.Digest(dig.String()), img, opts.RemoteOptions...); err != nil {
return nil, fmt.Errorf("failed to push image: %w", err)
}

midx = mutate.AppendManifests(midx, mutate.IndexAddendum{
Add: img,
Descriptor: v1.Descriptor{
MediaType: m.MediaType,
URLs: m.URLs,
Annotations: m.Annotations,
Platform: m.Platform,
ArtifactType: m.ArtifactType,
},
})
}

dig, err := midx.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get index digest: %w", err)
}

ref := target.Digest(dig.String())
if err := remote.WriteIndex(ref, midx, opts.RemoteOptions...); err != nil {
return nil, fmt.Errorf("failed to push index: %w", err)
}

return ref, nil

} else if desc.MediaType.IsImage() {
img, err := remote.Image(base, opts.RemoteOptions...)
if err != nil {
return nil, fmt.Errorf("failed to get image: %w", err)
}

for _, mutator := range opts.ImageMutators {
img, err = mutator(img)
if err != nil {
return nil, fmt.Errorf("failed to mutate image: %w", err)
}
}

mdig, err := img.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}

ref := target.Digest(mdig.String())
if err := remote.Write(ref, img, opts.RemoteOptions...); err != nil {
return nil, fmt.Errorf("failed to push image: %w", err)
}

return ref, nil
}

return nil, fmt.Errorf("reference [%s] uses an unsupported media type: [%s]", base.String(), desc.MediaType)
}

// Append mutates the source Image or ImageIndex with the provided append
// options, and pushes it to the target repository via its digest.
func Append(ctx context.Context, base name.Reference, target name.Repository, opts AppendOpts) (name.Reference, error) {
Expand Down
54 changes: 42 additions & 12 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ func New(opts ...Option) (*Client, error) {
}

func (d *Client) Run(ctx context.Context, req *Request) (string, error) {
req.AutoRemove = true
cid, err := d.start(ctx, req)
if err != nil {
return "", fmt.Errorf("starting container: %w", err)
Expand All @@ -109,25 +108,53 @@ func (d *Client) Run(ctx context.Context, req *Request) (string, error) {
// adding this to Start(), but its unclear how useful those logs would be,
// and how to even surface them without being overly verbose.
if req.Logger != nil {
defer func() {
logs, err := d.cli.ContainerLogs(ctx, cid, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
})
if err != nil {
fmt.Fprintf(req.Logger, "failed to get logs: %v\n", err)
return
}
defer logs.Close()
logs, err := d.cli.ContainerLogs(ctx, cid, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
})
if err != nil {
return "", fmt.Errorf("failed to get logs: %w", err)
}
defer logs.Close()

go func() {
defer logs.Close()
_, err = stdcopy.StdCopy(req.Logger, req.Logger, logs)
if err != nil {
fmt.Fprintf(req.Logger, "error copying logs: %v", err)
}
}()
}

// If a health check is present, set up a poller to poll on health status
unhealthyCh := make(chan error)
if req.HealthCheck != nil {
go func() {
for {
select {
case <-ctx.Done():
return
default:
inspect, err := d.cli.ContainerInspect(ctx, cid)
if err != nil {
unhealthyCh <- fmt.Errorf("inspecting container: %w", err)
return
}

if inspect.State != nil && inspect.State.Health != nil {
if inspect.State.Health.Status == "unhealthy" {
status := inspect.State.Health.Log[len(inspect.State.Health.Log)-1].Output
unhealthyCh <- fmt.Errorf("container became unhealthy, last status: %s", status)
return
}
}
time.Sleep(time.Second)
}
}
}()
}

select {
case <-ctx.Done():
return "", fmt.Errorf("context cancelled while waiting for container to exit: %w", ctx.Err())
Expand All @@ -143,6 +170,9 @@ func (d *Client) Run(ctx context.Context, req *Request) (string, error) {
if status.StatusCode != 0 {
return "", fmt.Errorf("container exited with non-zero status code: %d", status.StatusCode)
}

case err := <-unhealthyCh:
return "", err
}

return cid, nil
Expand Down
12 changes: 12 additions & 0 deletions internal/drivers/docker_in_docker/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// dockerindocker is a driver that runs each test container in its _own_ dind
// sandbox. Each test container is created as a new image, with the base layer
// containing the dind image, and subsequent layers containing the test
// container. Mapped out, the layers look like:
//
// 0: cgr.dev/chainguard-private/docker-dind:latest
// 1: imagetest created layer, with the appropriate test content and apk dependencies
//
// Things are done this way to ensure the tests that run _feel_ like they are
// simply in an environment with docker installed, while also ensuring they are
// portable to other drivers, such as docker-in-a-vm.
package dockerindocker
Loading

0 comments on commit 405886d

Please sign in to comment.