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 17, 2025
1 parent 36ac243 commit 5e7817d
Show file tree
Hide file tree
Showing 18 changed files with 1,646 additions and 15 deletions.
83 changes: 83 additions & 0 deletions docs/resources/tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
# 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))

### 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_ref` (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
- `metrics_server` (Boolean) Enable the metrics server
- `network_policy` (Boolean) Enable the network policy
- `traefik` (Boolean) Enable the traefik ingress controller



<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:

- `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.

<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
33 changes: 33 additions & 0 deletions examples/tests_resource/helm/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
terraform {
required_providers {
apko = { source = "chainguard-dev/apko" }
}
}

variable "target_repository" {}

variable "content" {}

data "apko_config" "sandbox" {
config_contents = jsonencode({
contents = {
packages = ["busybox", "bash", "helm", "kubectl", "jq"]
}
cmd = "bash -eux -o pipefail -c 'source /imagetest/foo.sh'"
})
}

resource "apko_build" "sandbox" {
repo = var.target_repository
config = data.apko_config.sandbox.config
}

output "test" {
value = [{
name = "helm test"
image = apko_build.sandbox.image_ref
content = [{
source = var.content
}]
}]
}
52 changes: 52 additions & 0 deletions examples/tests_resource/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
terraform {
required_providers {
imagetest = {
source = "registry.terraform.io/chainguard-dev/imagetest"
}
apko = { source = "chainguard-dev/apko" }
}
backend "inmem" {}
}

locals { repo = "gcr.io/wolf-chainguard/images" }

provider "imagetest" {
repo = local.repo
}

provider "apko" {
extra_repositories = concat([
"https://packages.wolfi.dev/os",
"https://packages.cgr.dev/extras",
])
build_repositories = ["https://apk.cgr.dev/chainguard-private"]
extra_keyring = concat([
"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub",
"https://packages.cgr.dev/extras/chainguard-extras.rsa.pub",
])

extra_packages = ["chainguard-baselayout"]

default_archs = ["aarch64"]
}

module "helm_test" {
source = "./helm"
target_repository = local.repo
content = "${path.module}/tests"
}

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

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

tests = concat([], module.helm_test.test)
}

output "tests" {
value = imagetest_tests.foo
}
7 changes: 7 additions & 0 deletions examples/tests_resource/tests/foo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

echo "Hello World"

kubectl get po -A

echo "$IMAGES" | jq '.'
76 changes: 76 additions & 0 deletions internal/bundler/appender.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,65 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote"
)

type appendOpts struct {
ropts []remote.Option
layers []Layerer
envs []string // A list of extra environment variables to set on the image
}

type AppendOpt func(*appendOpts) error

func AppendWithRemoteOptions(opts ...remote.Option) AppendOpt {
return func(a *appendOpts) error {
a.ropts = append(a.ropts, opts...)
return nil
}
}

func AppendWithLayers(layers ...Layerer) AppendOpt {
return func(a *appendOpts) error {
a.layers = append(a.layers, layers...)
return nil
}
}

func AppendWithEnvs(envs map[string]string) AppendOpt {
return func(a *appendOpts) error {
if envs == nil {
a.envs = make([]string, 0)
}

for k, v := range envs {
a.envs = append(a.envs, fmt.Sprintf("%s=%s", k, v))
}
return nil
}
}

func Append(ctx context.Context, base name.Reference, target name.Repository, options ...AppendOpt) (name.Reference, error) {
opts := &appendOpts{}

for _, opt := range options {
if err := opt(opts); err != nil {
return nil, err
}
}

bundler := &appender{
base: base,
ropts: opts.ropts,
envs: opts.envs,
}

return bundler.Bundle(ctx, target, opts.layers...)
}

// appender is a bundler that appends layers to existing images,
// copying the base to the target repo.
type appender struct {
base name.Reference
ropts []remote.Option
envs []string // A list of extra environment variables to set on the image
}

type AppenderOpt func(*appender) error
Expand Down Expand Up @@ -72,6 +126,18 @@ func (a *appender) Bundle(ctx context.Context, repo name.Repository, layers ...L
}
}

cf, err := mutated.ConfigFile()
if err != nil {
return nil, fmt.Errorf("failed to get config file: %w", err)
}

cf.Config.Env = append(cf.Config.Env, a.envs...)

mutated, err = mutate.ConfigFile(mutated, cf)
if err != nil {
return nil, fmt.Errorf("failed to set config file: %w", err)
}

mdig, err := mutated.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
Expand Down Expand Up @@ -148,3 +214,13 @@ func AppenderWithRemoteOptions(opts ...remote.Option) AppenderOpt {
return nil
}
}

func AppenderWithEnvs(envs ...string) AppenderOpt {
return func(a *appender) error {
if envs == nil {
a.envs = make([]string, 0)
}
a.envs = append(a.envs, envs...)
return nil
}
}
36 changes: 32 additions & 4 deletions internal/bundler/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"archive/tar"
"io"
"io/fs"
"path/filepath"
"os"
"path"

"chainguard.dev/apko/pkg/tarfs"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/tarball"
Expand All @@ -29,6 +31,32 @@ func NewFSLayer(source fs.FS, target string) Layerer {
}
}

// NewFSLayerFromPath is a helper function that creates a FS from a local path.
func NewFSLayerFromPath(source string, target string) Layerer {
pi, err := os.Stat(source)
if err != nil {
return nil
}

if pi.IsDir() {
return NewFSLayer(os.DirFS(source), target)
}

// There are better ways to make an FS from a isngle layer, but we already
// import tfs through apko, so just be a little lazy here
data, err := os.ReadFile(source)
if err != nil {
return nil
}

tfs := tarfs.New()
if err := tfs.WriteFile(pi.Name(), data, pi.Mode()); err != nil {
return nil
}

return NewFSLayer(tfs, target)
}

func (l *fsl) Layer() (v1.Layer, error) {
return tarball.LayerFromOpener(func() (io.ReadCloser, error) {
pr, pw := io.Pipe()
Expand All @@ -38,7 +66,7 @@ func (l *fsl) Layer() (v1.Layer, error) {
defer tw.Close()
defer pw.Close()

if err := fs.WalkDir(l.source, ".", func(path string, d fs.DirEntry, err error) error {
if err := fs.WalkDir(l.source, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand All @@ -53,14 +81,14 @@ func (l *fsl) Layer() (v1.Layer, error) {
return err
}

hdr.Name = filepath.Join(l.target, path)
hdr.Name = path.Join(l.target, p)

if err := tw.WriteHeader(hdr); err != nil {
return err
}

if !d.IsDir() {
f, err := l.source.Open(path)
f, err := l.source.Open(p)
if err != nil {
return err
}
Expand Down
17 changes: 17 additions & 0 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type Request struct {
HealthCheck *container.HealthConfig
Contents []*Content
PortBindings nat.PortMap
CgroupnsMode container.CgroupnsMode
PidMode string
ExtraHosts []string
AutoRemove bool
Logger io.Writer
Expand Down Expand Up @@ -479,6 +481,21 @@ func (r *Response) GetFile(ctx context.Context, path string) (io.Reader, error)
return tr, nil
}

// ReadFile is a helper method over GetFile() that returns the raw contents.
func (r *Response) ReadFile(ctx context.Context, path string) ([]byte, error) {
rdr, err := r.GetFile(ctx, path)
if err != nil {
return nil, err
}

data, err := io.ReadAll(rdr)
if err != nil {
return nil, err
}

return data, nil
}

func (d *Client) withDefaultLabels(labels map[string]string) map[string]string {
l := map[string]string{
"dev.chainguard.imagetest": "true",
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 5e7817d

Please sign in to comment.