diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2489922..af56161 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,10 +6,10 @@ name: Tests
on:
pull_request:
paths-ignore:
- - 'README.md'
+ - "README.md"
push:
paths-ignore:
- - 'README.md'
+ - "README.md"
# Testing only needs permissions to read the repository contents.
permissions:
@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version-file: 'go.mod'
+ go-version-file: "go.mod"
cache: true
- run: go mod download
- run: go build -v .
@@ -40,7 +40,7 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version-file: 'go.mod'
+ go-version-file: "go.mod"
cache: true
- run: go generate ./...
- name: git diff
@@ -57,18 +57,14 @@ jobs:
strategy:
fail-fast: false
matrix:
- # list whatever Terraform versions here you would like to support
terraform:
- - '1.0.*'
- - '1.1.*'
- - '1.2.*'
- - '1.3.*'
- - '1.4.*'
+ - "1.5.*"
+ - "1.6.*"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version-file: 'go.mod'
+ go-version-file: "go.mod"
cache: true
- uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
with:
diff --git a/docs/index.md b/docs/index.md
index 0017b4c..ca661a6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,3 +14,85 @@ description: |-
## Schema
+
+### Optional
+
+- `harnesses` (Attributes) (see [below for nested schema](#nestedatt--harnesses))
+
+
+### Nested Schema for `harnesses`
+
+Optional:
+
+- `container` (Attributes) (see [below for nested schema](#nestedatt--harnesses--container))
+- `k3s` (Attributes) (see [below for nested schema](#nestedatt--harnesses--k3s))
+
+
+### Nested Schema for `harnesses.container`
+
+Optional:
+
+- `envs` (Map of String) Environment variables to set on the container.
+- `mounts` (Attributes List) The list of mounts to create on the container. (see [below for nested schema](#nestedatt--harnesses--container--mounts))
+- `networks` (Attributes Map) A map of existing networks to attach the container to. (see [below for nested schema](#nestedatt--harnesses--container--networks))
+
+
+### Nested Schema for `harnesses.container.mounts`
+
+Required:
+
+- `destination` (String) The absolute path on the container to mount the source directory to.
+- `source` (String) The relative or absolute path on the host to the source directory to mount.
+
+
+
+### Nested Schema for `harnesses.container.networks`
+
+Required:
+
+- `name` (String) The name of the existing network to attach the container to.
+
+
+
+
+### Nested Schema for `harnesses.k3s`
+
+Optional:
+
+- `registries` (Attributes Map) A map of registries containing configuration for optional auth, tls, and mirror configuration. (see [below for nested schema](#nestedatt--harnesses--k3s--registries))
+
+
+### Nested Schema for `harnesses.k3s.registries`
+
+Optional:
+
+- `auth` (Attributes) (see [below for nested schema](#nestedatt--harnesses--k3s--registries--auth))
+- `mirror` (Attributes) (see [below for nested schema](#nestedatt--harnesses--k3s--registries--mirror))
+- `tls` (Attributes) (see [below for nested schema](#nestedatt--harnesses--k3s--registries--tls))
+
+
+### Nested Schema for `harnesses.k3s.registries.tls`
+
+Optional:
+
+- `auth` (String)
+- `password` (String, Sensitive)
+- `username` (String)
+
+
+
+### Nested Schema for `harnesses.k3s.registries.tls`
+
+Optional:
+
+- `endpoints` (List of String)
+
+
+
+### Nested Schema for `harnesses.k3s.registries.tls`
+
+Optional:
+
+- `ca_file` (String)
+- `cert_file` (String)
+- `key_file` (String)
diff --git a/docs/resources/feature.md b/docs/resources/feature.md
index 1b89e21..e5e1b16 100644
--- a/docs/resources/feature.md
+++ b/docs/resources/feature.md
@@ -17,6 +17,7 @@ Example resource
### Required
+- `harness` (String) The ID of the test harness to use for the feature
- `name` (String) The name of the feature
### Optional
@@ -24,7 +25,6 @@ Example resource
- `after` (Attributes List) Actions to run againast the harness after the core steps have run OR after a step has failed. (see [below for nested schema](#nestedatt--after))
- `before` (Attributes List) Actions to run against the harness before the core feature steps. (see [below for nested schema](#nestedatt--before))
- `description` (String) A descriptor of the feature
-- `harness` (String) The ID of the test harness to use for the feature
- `labels` (Map of String) A set of labels used to optionally filter execution of the feature
- `steps` (Attributes List) Actions to run against the harness. (see [below for nested schema](#nestedatt--steps))
diff --git a/docs/resources/harness_container.md b/docs/resources/harness_container.md
index 05a6fe9..840374e 100644
--- a/docs/resources/harness_container.md
+++ b/docs/resources/harness_container.md
@@ -20,6 +20,7 @@ A harness that runs steps in a sandbox container.
- `envs` (Map of String) Environment variables to set on the container.
- `image` (String) The full image reference to use for the k3s container.
- `mounts` (Attributes List) The list of mounts to create on the container. (see [below for nested schema](#nestedatt--mounts))
+- `networks` (Attributes Map) A map of existing networks to attach the container to. (see [below for nested schema](#nestedatt--networks))
- `privileged` (Boolean)
### Read-Only
@@ -31,5 +32,13 @@ A harness that runs steps in a sandbox container.
Required:
-- `destination` (String) The absolute path on the container to mount the source directory to.
+- `destination` (String) The absolute path on the container to mount the source directory.
- `source` (String) The relative or absolute path on the host to the source directory to mount.
+
+
+
+### Nested Schema for `networks`
+
+Required:
+
+- `name` (String) The name of the existing network to attach the container to.
diff --git a/docs/resources/harness_k3s.md b/docs/resources/harness_k3s.md
index fa047d0..70507a3 100644
--- a/docs/resources/harness_k3s.md
+++ b/docs/resources/harness_k3s.md
@@ -21,7 +21,44 @@ A harness that runs steps in a sandbox container networked to a running k3s clus
- `disable_metrics_server` (Boolean) When true, the builtin metrics server will be disabled.
- `disable_traefik` (Boolean) When true, the builtin traefik ingress controller will be disabled.
- `image` (String) The full image reference to use for the k3s container.
+- `registries` (Attributes Map) A map of registries containing configuration for optional auth, tls, and mirror configuration. (see [below for nested schema](#nestedatt--registries))
### Read-Only
- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `registries`
+
+Optional:
+
+- `auth` (Attributes) (see [below for nested schema](#nestedatt--registries--auth))
+- `mirror` (Attributes) (see [below for nested schema](#nestedatt--registries--mirror))
+- `tls` (Attributes) (see [below for nested schema](#nestedatt--registries--tls))
+
+
+### Nested Schema for `registries.auth`
+
+Optional:
+
+- `auth` (String)
+- `password` (String, Sensitive)
+- `username` (String)
+
+
+
+### Nested Schema for `registries.mirror`
+
+Optional:
+
+- `endpoints` (List of String)
+
+
+
+### Nested Schema for `registries.tls`
+
+Optional:
+
+- `ca_file` (String)
+- `cert_file` (String)
+- `key_file` (String)
diff --git a/examples/resources/main.tf b/examples/resources/main.tf
index dbe7d93..76c7bdb 100644
--- a/examples/resources/main.tf
+++ b/examples/resources/main.tf
@@ -12,12 +12,6 @@ provider "imagetest" {}
# Create a harness that runs features in a container.
resource "imagetest_harness_container" "this" {
image = "cgr.dev/chainguard/wolfi-base:latest"
- mounts = [
- {
- source = path.module
- destination = "/src"
- }
- ]
}
resource "imagetest_harness_teardown" "container" { harness = imagetest_harness_container.this.id }
@@ -34,12 +28,6 @@ resource "imagetest_feature" "container" {
apk add curl
EOF
},
- {
- name = "Access files we mounted from the host"
- cmd = < 0 {
var err error
for _, e := range errs {
- err = fmt.Errorf("%w: %v", err, e)
+ if err != nil {
+ err = fmt.Errorf("%w: %v", err, e)
+ } else {
+ err = e
+ }
}
return err
}
@@ -127,7 +148,7 @@ func (p *DockerProvider) Teardown(ctx context.Context) error {
// Exec implements Provider.
func (p *DockerProvider) Exec(ctx context.Context, command string) (io.Reader, error) {
- resp, err := p.client.ContainerExecCreate(ctx, p.id, types.ExecConfig{
+ resp, err := p.cli.ContainerExecCreate(ctx, p.id, types.ExecConfig{
Cmd: []string{"/bin/sh", "-c", command},
AttachStderr: true,
AttachStdout: true,
@@ -137,13 +158,13 @@ func (p *DockerProvider) Exec(ctx context.Context, command string) (io.Reader, e
}
check := types.ExecStartCheck{}
- attach, err := p.client.ContainerExecAttach(ctx, resp.ID, check)
+ attach, err := p.cli.ContainerExecAttach(ctx, resp.ID, check)
if err != nil {
return nil, err
}
defer attach.Close()
- if err := p.client.ContainerExecStart(ctx, resp.ID, check); err != nil {
+ if err := p.cli.ContainerExecStart(ctx, resp.ID, check); err != nil {
return nil, err
}
@@ -155,7 +176,7 @@ func (p *DockerProvider) Exec(ctx context.Context, command string) (io.Reader, e
// Block until the command is done
var exitCode int
for {
- exec, err := p.client.ContainerExecInspect(ctx, resp.ID)
+ exec, err := p.cli.ContainerExecInspect(ctx, resp.ID)
if err != nil {
return nil, err
}
@@ -174,3 +195,24 @@ func (p *DockerProvider) Exec(ctx context.Context, command string) (io.Reader, e
return out, nil
}
+
+// pull the image if it doesn't exist in the daemon
+// TODO: Do this with ggcr.
+func (p *DockerProvider) pull(ctx context.Context, imageId string) error {
+ // check if the imageId exists in the daemon
+ _, _, err := p.cli.ImageInspectWithRaw(ctx, imageId)
+ if err != nil {
+ if !client.IsErrNotFound(err) {
+ return fmt.Errorf("checking if image exists: %w", err)
+ }
+ }
+
+ // pull the image if it doesn't exist
+ pull, err := p.cli.ImagePull(ctx, imageId, types.ImagePullOptions{})
+ if err != nil {
+ return err
+ }
+
+ _, err = io.ReadAll(pull)
+ return err
+}
diff --git a/internal/harnesses/provider/provider.go b/internal/containers/provider/provider.go
similarity index 88%
rename from internal/harnesses/provider/provider.go
rename to internal/containers/provider/provider.go
index 8bcf4b9..64a5252 100644
--- a/internal/harnesses/provider/provider.go
+++ b/internal/containers/provider/provider.go
@@ -15,15 +15,6 @@ type Provider interface {
Exec(ctx context.Context, command string) (io.Reader, error)
}
-var runtimes map[string]string
-
-func init() {
- runtimes = map[string]string{
- "docker": DockerProviderName,
- // TODO: Other runtimes
- }
-}
-
type ContainerRequest struct {
Image string
Entrypoint []string
@@ -51,7 +42,7 @@ type File struct {
Mode int64
}
-// TODO: Jon pls halp.
+// TODO: Jon pls.
func (f File) tar() (io.Reader, error) {
cbuf := &bytes.Buffer{}
size, err := io.Copy(cbuf, f.Contents)
diff --git a/internal/features/feature.go b/internal/features/feature.go
index a5a7f24..3100a6f 100644
--- a/internal/features/feature.go
+++ b/internal/features/feature.go
@@ -41,7 +41,7 @@ func NewBuilder(name string) *FeatureBuilder {
}
}
-// Build the feature for the given environment
+// Build the feature for the given environment.
func (b *FeatureBuilder) Build() types.Feature {
return b.feat
}
diff --git a/internal/harnesses/base.go b/internal/harnesses/base/base.go
similarity index 78%
rename from internal/harnesses/base.go
rename to internal/harnesses/base/base.go
index 6548b31..2130071 100644
--- a/internal/harnesses/base.go
+++ b/internal/harnesses/base/base.go
@@ -1,4 +1,4 @@
-package harnesses
+package base
import (
"context"
@@ -8,22 +8,23 @@ import (
"github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
)
-// base is a base harness implementation. it can be embedded into other
-type base struct {
- mu sync.Mutex
+// Base is a Base harness implementation. It's often useful to embed this into
+// other harness implementations.
+type Base struct {
triggered chan struct{}
- once sync.Once
using int
+ once sync.Once
+ mu sync.Mutex
}
-func NewBase() *base {
- return &base{
+func New() *Base {
+ return &Base{
mu: sync.Mutex{},
triggered: make(chan struct{}),
}
}
-func (h *base) WithCreate(f types.StepFn) types.StepFn {
+func (h *Base) WithCreate(f types.StepFn) types.StepFn {
return func(ctx context.Context) (context.Context, error) {
h.using++
@@ -51,7 +52,7 @@ func (h *base) WithCreate(f types.StepFn) types.StepFn {
}
}
-func (h *base) Finish() types.StepFn {
+func (h *Base) Finish() types.StepFn {
return func(ctx context.Context) (context.Context, error) {
h.using--
@@ -63,7 +64,7 @@ func (h *base) Finish() types.StepFn {
}
}
-func (h *base) Done() error {
+func (h *Base) Done() error {
<-h.triggered
for h.using > 0 {
time.Sleep(1 * time.Second)
diff --git a/internal/harnesses/container.go b/internal/harnesses/container/container.go
similarity index 80%
rename from internal/harnesses/container.go
rename to internal/harnesses/container/container.go
index 62c8bf4..bc44d42 100644
--- a/internal/harnesses/container.go
+++ b/internal/harnesses/container/container.go
@@ -1,10 +1,11 @@
-package harnesses
+package container
import (
"context"
"io"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/provider"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/containers/provider"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/base"
"github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
"github.com/docker/docker/api/types/mount"
"github.com/hashicorp/terraform-plugin-log/tflog"
@@ -13,9 +14,9 @@ import (
var _ types.Harness = &container{}
// container is a harness that spins up a container and steps within the
-// container environment
+// container environment.
type container struct {
- *base
+ *base.Base
provider provider.Provider
}
@@ -57,21 +58,22 @@ func (h *container) StepFn(command string) types.StepFn {
}
}
-type ContainerConfig struct {
+type Config struct {
Env map[string]string
Image string
- Mounts []ContainerConfigMount
+ Mounts []ConfigMount
+ Networks []string
Privileged bool
}
-// ContainerConfigMount is a simplified wrapper around mount.Mount, intended to
-// only support BindMounts
-type ContainerConfigMount struct {
+// ConfigMount is a simplified wrapper around mount.Mount, intended to
+// only support BindMounts.
+type ConfigMount struct {
Source string
Destination string
}
-func NewContainer(ctx context.Context, name string, cfg ContainerConfig) (types.Harness, error) {
+func New(_ context.Context, name string, cfg Config) (types.Harness, error) {
// TODO: Support more providers
mounts := make([]mount.Mount, 0, len(cfg.Mounts))
@@ -89,6 +91,7 @@ func NewContainer(ctx context.Context, name string, cfg ContainerConfig) (types.
Entrypoint: []string{"/bin/sh", "-c"},
Cmd: []string{"tail -f /dev/null"},
Env: cfg.Env,
+ Networks: cfg.Networks,
},
Mounts: mounts,
})
@@ -97,7 +100,7 @@ func NewContainer(ctx context.Context, name string, cfg ContainerConfig) (types.
}
return &container{
- base: NewBase(),
+ Base: base.New(),
provider: p,
}, nil
}
diff --git a/internal/harnesses/doc.go b/internal/harnesses/doc.go
deleted file mode 100644
index 8b9069e..0000000
--- a/internal/harnesses/doc.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Package harness provides various harnesses that features can use to test
-// against. The harnesses act on the StepFn of the test to both build the
-// testing environment (such as kubernetes clusters), and to execute the test
-// itself. For example, the kubernetes harness will use the StepFn to create a
-// kubernetes cluster, and then execute the test against that cluster.
-//
-// TODO: This package is a mess right now with all sorts of os/exec nastiness.
-// Factor this out into proper use of the docker sdk when the api stabilizes.
-package harnesses
diff --git a/internal/harnesses/executors.go b/internal/harnesses/executors.go
deleted file mode 100644
index 94db63a..0000000
--- a/internal/harnesses/executors.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package harnesses
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- "os/exec"
-)
-
-// Executor is an interface for executing commands.
-type Executor interface {
- Exec(ctx context.Context, command []string) (io.Reader, error)
-}
-
-// HostExecutor is an implementation of Executor that runs commands on the host
-type HostExecutor struct {
- // Env is a map of environment variables to set when running commands.
- Env map[string]string
-}
-
-func NewHostExecutor() Executor {
- return &HostExecutor{
- Env: make(map[string]string),
- }
-}
-
-// Exec runs the given command using os/exec.
-func (e *HostExecutor) Exec(ctx context.Context, command []string) (io.Reader, error) {
- cmd := exec.CommandContext(ctx, command[0], command[1:]...)
-
- out := &bytes.Buffer{}
- cmd.Stdout = out
- cmd.Stderr = out
-
- // Intentionally don't inherit the hosts environment variables to prevent
- // leaking things.
- env := make([]string, 0, len(e.Env))
- for k, v := range e.Env {
- env = append(env, fmt.Sprintf("%s=%s", k, v))
- }
- cmd.Env = env
-
- if err := cmd.Run(); err != nil {
- return nil, fmt.Errorf("running command: %w", err)
- }
-
- return out, nil
-}
diff --git a/internal/harnesses/host.go b/internal/harnesses/host.go
deleted file mode 100644
index 13d260d..0000000
--- a/internal/harnesses/host.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package harnesses
-
-import (
- "context"
- "fmt"
-
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
-)
-
-var _ types.Harness = &host{}
-
-// host is a harness type that runs steps on the host machine
-type host struct {
- *base
-
- execer HostExecutor
-}
-
-func NewHost() types.Harness {
- return &host{
- base: NewBase(),
- }
-}
-
-// StepFn implements types.Harn.
-func (h *host) StepFn(command string) types.StepFn {
- return func(ctx context.Context) (context.Context, error) {
- if _, err := h.execer.Exec(ctx, []string{"sh", "-c", command}); err != nil {
- return ctx, fmt.Errorf("running step on host: %w", err)
- }
- return ctx, nil
- }
-}
-
-// Setup implements types.Harn.
-func (h *host) Setup() types.StepFn {
- return func(ctx context.Context) (context.Context, error) {
- return ctx, nil
- }
-}
-
-// Destroy implements types.Harn.
-func (*host) Destroy(context.Context) error {
- return nil
-}
diff --git a/internal/harnesses/host/host.go b/internal/harnesses/host/host.go
new file mode 100644
index 0000000..0fa4f51
--- /dev/null
+++ b/internal/harnesses/host/host.go
@@ -0,0 +1,72 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os/exec"
+
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/base"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
+)
+
+var _ types.Harness = &host{}
+
+// host is a harness type that runs steps on the host machine.
+type host struct {
+ *base.Base
+
+ env map[string]string
+}
+
+func NewHost() types.Harness {
+ return &host{
+ Base: base.New(),
+ env: make(map[string]string),
+ }
+}
+
+// StepFn implements types.Harn.
+func (h *host) StepFn(command string) types.StepFn {
+ return func(ctx context.Context) (context.Context, error) {
+ if _, err := h.exec(ctx, []string{"sh", "-c", command}); err != nil {
+ return ctx, fmt.Errorf("running step on host: %w", err)
+ }
+ return ctx, nil
+ }
+}
+
+// Setup implements types.Harn.
+func (h *host) Setup() types.StepFn {
+ return func(ctx context.Context) (context.Context, error) {
+ return ctx, nil
+ }
+}
+
+// Destroy implements types.Harn.
+func (*host) Destroy(context.Context) error {
+ return nil
+}
+
+func (h *host) exec(ctx context.Context, command []string) (io.Reader, error) {
+ cmd := exec.CommandContext(ctx, command[0], command[1:]...)
+
+ out := &bytes.Buffer{}
+ cmd.Stdout = out
+ cmd.Stderr = out
+
+ // Intentionally don't inherit the hosts environment variables to prevent
+ // leaking things.
+ env := make([]string, 0, len(h.env))
+ for k, v := range h.env {
+ env = append(env, fmt.Sprintf("%s=%s", k, v))
+ }
+ cmd.Env = env
+
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("running command: %w", err)
+ }
+
+ return out, nil
+}
diff --git a/internal/harnesses/k3s.go b/internal/harnesses/k3s/k3s.go
similarity index 64%
rename from internal/harnesses/k3s.go
rename to internal/harnesses/k3s/k3s.go
index f286af8..77b7f7e 100644
--- a/internal/harnesses/k3s.go
+++ b/internal/harnesses/k3s/k3s.go
@@ -1,4 +1,4 @@
-package harnesses
+package k3s
import (
"bytes"
@@ -11,25 +11,23 @@ import (
"github.com/google/go-containerregistry/pkg/name"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/provider"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/containers/provider"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/base"
"github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
"github.com/docker/docker/api/types/mount"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
-type K3sConfig struct {
- Image string
- Traefik bool
- Cni bool
- MetricsServer bool
-}
+const (
+ K3sImage = "cgr.dev/chainguard/k3s:latest"
+)
type k3s struct {
- *base
-
- Config K3sConfig
- id string
-
+ *base.Base
+ // opt are the options for the k3s harness
+ opt *Opt
+ // id is an identifier used to prepend to containers created by this harness
+ id string
// service is the provider that is running the service, which is k3s in a
// container
service provider.Provider
@@ -38,30 +36,57 @@ type k3s struct {
sandbox provider.Provider
}
-func NewK3s(id string, cfg K3sConfig) (types.Harness, error) {
+func New(id string, opts ...Option) (types.Harness, error) {
+ opt := &Opt{
+ Image: K3sImage,
+ Cni: true,
+ MetricsServer: false,
+ Traefik: false,
+ }
+
+ for _, o := range opts {
+ if err := o(opt); err != nil {
+ return nil, err
+ }
+ }
+
k3s := &k3s{
- base: NewBase(),
- Config: cfg,
- id: id,
+ Base: base.New(),
+ id: id,
+ opt: opt,
}
- ref, err := name.ParseReference(cfg.Image)
+ ref, err := name.ParseReference(opt.Image)
if err != nil {
return nil, fmt.Errorf("invalid image reference: %w", err)
}
- svcName := id + "-service"
- service, err := provider.NewDocker(svcName, provider.DockerRequest{
+ kcfg, err := k3s.genConfig()
+ if err != nil {
+ return nil, fmt.Errorf("creating k3s config: %w", err)
+ }
+
+ rcfg, err := k3s.genRegistries()
+ if err != nil {
+ return nil, fmt.Errorf("creating k3s registries config: %w", err)
+ }
+
+ service, err := provider.NewDocker(k3s.serviceName(), provider.DockerRequest{
ContainerRequest: provider.ContainerRequest{
Image: ref.Name(),
Cmd: []string{"server"},
Privileged: true,
Files: []provider.File{
{
- Contents: bytes.NewBufferString(k3s.config(svcName)),
+ Contents: kcfg,
Target: "/etc/rancher/k3s/config.yaml",
Mode: 0644,
},
+ {
+ Contents: rcfg,
+ Target: "/etc/rancher/k3s/registries.yaml",
+ Mode: 0644,
+ },
},
},
Mounts: []mount.Mount{
@@ -76,13 +101,13 @@ func NewK3s(id string, cfg K3sConfig) (types.Harness, error) {
return nil, err
}
- sandbox, err := provider.NewDocker(id+"-sandbox", provider.DockerRequest{
+ sandbox, err := provider.NewDocker(k3s.sandboxName(), provider.DockerRequest{
ContainerRequest: provider.ContainerRequest{
// TODO: Dynamically build this with predetermined apks
Image: "cgr.dev/chainguard/kubectl:latest-dev",
Entrypoint: []string{"/bin/sh", "-c"},
Cmd: []string{"tail -f /dev/null"},
- Networks: []string{svcName},
+ Networks: []string{k3s.serviceName()},
Env: map[string]string{
"KUBECONFIG": "/k3s-config/k3s.yaml",
},
@@ -167,7 +192,11 @@ func (h *k3s) Destroy(ctx context.Context) error {
if len(errs) > 0 {
var err error
for _, e := range errs {
- err = fmt.Errorf("%w: %v", err, e)
+ if err != nil {
+ err = fmt.Errorf("%w: %v", err, e)
+ } else {
+ err = e
+ }
}
return err
}
@@ -197,13 +226,57 @@ func (h *k3s) StepFn(command string) types.StepFn {
}
}
-func (h *k3s) ref() (string, error) {
- return "", nil
+func (h *k3s) serviceName() string {
+ return h.id + "-service"
}
-func (h *k3s) config(hostname string) string {
+func (h *k3s) sandboxName() string {
+ return h.id + "-sandbox"
+}
+
+func (h *k3s) genRegistries() (io.Reader, error) {
+ // who needs an an api when you have yaml and gotemplates!11!
+ cfgtmpl := `
+mirrors:
+ {{- range $k, $v := .Mirrors }}
+ "{{ $k }}":
+ endpoint:
+ {{- range $v.Endpoints }}
+ - "{{ . }}"
+ {{- end }}
+ {{- end}}
+
+configs:
+ {{- range $k, $v := .Registries }}
+ "{{ $k }}":
+ auth:
+ username: "{{ $v.Auth.Username }}"
+ password: "{{ $v.Auth.Password }}"
+ auth: "{{ $v.Auth.Auth }}"
+ {{- if and $v.Tls $v.Tls.CertFile $v.Tls.KeyFile $v.Tls.CaFile }}
+ tls:
+ cert_file: "{{ $v.Tls.CertFile }}"
+ key_file: "{{ $v.Tls.KeyFile }}"
+ ca_file: "{{ $v.Tls.CaFile }}"
+ {{- end }}
+ {{- end }}
+`
+
+ tmpl, err := template.New("registry").Parse(cfgtmpl)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := &bytes.Buffer{}
+ if err := tmpl.Execute(buf, h.opt); err != nil {
+ return nil, err
+ }
+
+ return buf, nil
+}
+
+func (h *k3s) genConfig() (io.Reader, error) {
// who needs an an api when you have yaml and gotemplates!11!
- // TODO: This is where we'd also handle auth and mirroring
cfgtmpl := fmt.Sprintf(`
tls-san: "%[1]s"
disable:
@@ -216,17 +289,17 @@ disable:
{{- if not .Cni }}
flannel-backend: none
{{- end }}
-`, hostname)
+`, h.serviceName())
tmpl, err := template.New("config").Parse(cfgtmpl)
if err != nil {
- return ""
+ return nil, err
}
buf := &bytes.Buffer{}
- if err := tmpl.Execute(buf, h.Config); err != nil {
- return ""
+ if err := tmpl.Execute(buf, h.opt); err != nil {
+ return nil, err
}
- return buf.String()
+ return buf, nil
}
diff --git a/internal/harnesses/k3s/opts.go b/internal/harnesses/k3s/opts.go
new file mode 100644
index 0000000..3fe49db
--- /dev/null
+++ b/internal/harnesses/k3s/opts.go
@@ -0,0 +1,113 @@
+package k3s
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+type Opt struct {
+ Image string
+ Traefik bool
+ Cni bool
+ MetricsServer bool
+
+ Registries map[string]*RegistryOpt
+ Mirrors map[string]*RegistryMirrorOpt
+}
+
+type RegistryOpt struct {
+ Auth *RegistryAuthOpt
+ Tls *RegistryTlsOpt
+}
+
+type RegistryAuthOpt struct {
+ Username string
+ Password string
+ Auth string
+}
+
+type RegistryTlsOpt struct {
+ CertFile string
+ KeyFile string
+ CaFile string
+}
+
+type RegistryMirrorOpt struct {
+ Endpoints []string
+}
+
+type Option func(*Opt) error
+
+func WithImage(image string) Option {
+ return func(opt *Opt) error {
+ opt.Image = image
+ return nil
+ }
+}
+
+func WithAuthFromStatic(registry, username, password, auth string) Option {
+ return func(opt *Opt) error {
+ if opt.Registries == nil {
+ opt.Registries = make(map[string]*RegistryOpt)
+ }
+ if _, ok := opt.Registries[registry]; !ok {
+ opt.Registries[registry] = &RegistryOpt{}
+ }
+
+ opt.Registries[registry].Auth = &RegistryAuthOpt{
+ Username: username,
+ Password: password,
+ Auth: auth,
+ }
+
+ return nil
+ }
+}
+
+func WithAuthFromKeychain(registry string) Option {
+ return func(opt *Opt) error {
+ if opt.Registries == nil {
+ opt.Registries = make(map[string]*RegistryOpt)
+ }
+ if _, ok := opt.Registries[registry]; !ok {
+ opt.Registries[registry] = &RegistryOpt{}
+ }
+
+ r, err := name.NewRegistry(registry)
+ if err != nil {
+ return fmt.Errorf("invalid registry name: %w", err)
+ }
+
+ a, err := authn.DefaultKeychain.Resolve(r)
+ if err != nil {
+ return fmt.Errorf("resolving keychain for registry %s: %w", r.String(), err)
+ }
+
+ acfg, err := a.Authorization()
+ if err != nil {
+ return fmt.Errorf("getting authorization for registry %s: %w", r.String(), err)
+ }
+
+ opt.Registries[registry].Auth = &RegistryAuthOpt{
+ Username: acfg.Username,
+ Password: acfg.Password,
+ Auth: acfg.Auth,
+ }
+
+ return nil
+ }
+}
+
+func WithRegistryMirror(registry string, endpoints ...string) Option {
+ return func(opt *Opt) error {
+ if opt.Mirrors == nil {
+ opt.Mirrors = make(map[string]*RegistryMirrorOpt)
+ }
+ opt.Mirrors[registry] = &RegistryMirrorOpt{
+ Endpoints: endpoints,
+ }
+ return nil
+ }
+}
diff --git a/internal/harnesses/provider/doc.go b/internal/harnesses/provider/doc.go
deleted file mode 100644
index 4f504f6..0000000
--- a/internal/harnesses/provider/doc.go
+++ /dev/null
@@ -1 +0,0 @@
-package provider
diff --git a/internal/provider/feature_resource.go b/internal/provider/feature_resource.go
index b00ac7c..7672e0c 100644
--- a/internal/provider/feature_resource.go
+++ b/internal/provider/feature_resource.go
@@ -7,7 +7,6 @@ import (
"github.com/chainguard-dev/terraform-provider-imagetest/internal/environment"
"github.com/chainguard-dev/terraform-provider-imagetest/internal/features"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses"
itypes "github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -73,7 +72,7 @@ func (r *FeatureResource) Schema(ctx context.Context, req resource.SchemaRequest
},
"harness": schema.StringAttribute{
Description: "The ID of the test harness to use for the feature",
- Optional: true,
+ Required: true,
},
"before": schema.ListNestedAttribute{
Description: "Actions to run against the harness before the core feature steps.",
@@ -161,18 +160,13 @@ func (r *FeatureResource) Create(ctx context.Context, req resource.CreateRequest
}
var harness itypes.Harness
- // Use a host harness if none is specified
- if data.HarnessId.IsUnknown() || data.HarnessId.IsNull() {
- harness = harnesses.NewHost()
- } else {
- // Get the harness from the store
- h, ok := r.store.harnesses.Get(data.HarnessId.ValueString())
- if !ok {
- resp.Diagnostics.AddError("invalid harness id", "...")
- return
- }
- harness = h
+
+ h, ok := r.store.harnesses.Get(data.HarnessId.ValueString())
+ if !ok {
+ resp.Diagnostics.AddError("invalid harness id", "...")
+ return
}
+ harness = h
builder := features.NewBuilder(data.Name.ValueString()).
WithDescription(data.Description.ValueString()).
diff --git a/internal/provider/feature_resource_test.go b/internal/provider/feature_resource_test.go
index 738a6a1..9c70a60 100644
--- a/internal/provider/feature_resource_test.go
+++ b/internal/provider/feature_resource_test.go
@@ -1,7 +1,6 @@
package provider
import (
- "fmt"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -14,19 +13,42 @@ func TestAccFeatureResource(t *testing.T) {
Steps: []resource.TestStep{
// Create and read testing
{
- Config: testAccFeatureResourceConfig(),
- Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckResourceAttr("imagetest_feature.test", "name", "DoATest"),
- ),
+ Config: `
+resource "imagetest_harness_container" "test" {}
+resource "imagetest_harness_teardown" "test" { harness = imagetest_harness_container.test.id }
+resource "imagetest_feature" "test" {
+ name = "Ordering"
+ description = "Test the step ordering"
+ harness = imagetest_harness_container.test.id
+ before = [
+ {
+ name = "1"
+ cmd = "echo 1 >> /tmp/feature_test"
+ },
+ ]
+ after = [
+ {
+ name = "3"
+ cmd = "echo 3 >> /tmp/feature_test"
+ },
+ {
+ name = "assert"
+ cmd = < /dev/null
+ EOF
+ },
+ ]
+ steps = [
+ {
+ name = "2"
+ cmd = "echo 2 >> /tmp/feature_test"
+ },
+ ]
+}
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(),
},
},
})
}
-
-func testAccFeatureResourceConfig() string {
- return fmt.Sprintf(`
-resource "imagetest_feature" "test" {
- name = "DoATest"
-}
-`)
-}
diff --git a/internal/provider/harness_container_resource.go b/internal/provider/harness_container_resource.go
index aefa39f..d8481c3 100644
--- a/internal/provider/harness_container_resource.go
+++ b/internal/provider/harness_container_resource.go
@@ -5,7 +5,7 @@ import (
"fmt"
"path/filepath"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/container"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -33,18 +33,23 @@ type HarnessContainerResource struct {
// HarnessContainerResourceModel describes the resource data model.
type HarnessContainerResourceModel struct {
- Id types.String `tfsdk:"id"`
- Image types.String `tfsdk:"image"`
- Privileged types.Bool `tfsdk:"privileged"`
- Envs types.Map `tfsdk:"envs"`
- Mounts []HarnessContainerResourceMountModel `tfsdk:"mounts"`
+ Id types.String `tfsdk:"id"`
+ Image types.String `tfsdk:"image"`
+ Privileged types.Bool `tfsdk:"privileged"`
+ Envs types.Map `tfsdk:"envs"`
+ Mounts []ContainerResourceMountModel `tfsdk:"mounts"`
+ Networks map[string]ContainerResourceModelNetwork `tfsdk:"networks"`
}
-type HarnessContainerResourceMountModel struct {
+type ContainerResourceMountModel struct {
Source types.String `tfsdk:"source"`
Destination types.String `tfsdk:"destination"`
}
+type ContainerResourceModelNetwork struct {
+ Name types.String `tfsdk:"name"`
+}
+
func (r *HarnessContainerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_harness_container"
}
@@ -73,6 +78,18 @@ func (r *HarnessContainerResource) Schema(ctx context.Context, req resource.Sche
Optional: true,
ElementType: types.StringType,
},
+ "networks": schema.MapNestedAttribute{
+ Description: "A map of existing networks to attach the container to.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "The name of the existing network to attach the container to.",
+ Required: true,
+ },
+ },
+ },
+ },
"mounts": schema.ListNestedAttribute{
Description: "The list of mounts to create on the container.",
Optional: true,
@@ -83,7 +100,7 @@ func (r *HarnessContainerResource) Schema(ctx context.Context, req resource.Sche
Required: true,
},
"destination": schema.StringAttribute{
- Description: "The absolute path on the container to mount the source directory to.",
+ Description: "The absolute path on the container to mount the source directory.",
Required: true,
},
},
@@ -112,59 +129,83 @@ func (r *HarnessContainerResource) Configure(ctx context.Context, req resource.C
func (r *HarnessContainerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data HarnessContainerResourceModel
- // Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
if resp.Diagnostics.HasError() {
return
}
data.Id = types.StringValue(r.id)
- cfg, err := r.containerCfg(ctx, data)
- if err != nil {
- resp.Diagnostics.AddError("invalid resource data", err.Error())
- return
+ cfg := container.Config{
+ Image: data.Image.ValueString(),
+ Privileged: data.Privileged.ValueBool(),
+ Mounts: []container.ConfigMount{},
+ Networks: []string{},
+ Env: map[string]string{},
}
- harness, err := harnesses.NewContainer(ctx, r.id, cfg)
- if err != nil {
- resp.Diagnostics.AddError("invalid provider data", "...")
- return
+ mounts := []ContainerResourceMountModel{}
+ if data.Mounts != nil {
+ mounts = data.Mounts
}
- r.store.harnesses.Set(r.id, harness)
- // Save data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
+ networks := make(map[string]ContainerResourceModelNetwork)
+ if data.Networks != nil {
+ networks = data.Networks
+ }
-func (r *HarnessContainerResource) containerCfg(ctx context.Context, data HarnessContainerResourceModel) (harnesses.ContainerConfig, error) {
- cfg := harnesses.ContainerConfig{
- Image: data.Image.ValueString(),
- Privileged: data.Privileged.ValueBool(),
+ if r.store.providerResourceData.Harnesses != nil {
+ if c := r.store.providerResourceData.Harnesses.Container; c != nil {
+ mounts = append(mounts, c.Mounts...)
+
+ for k, v := range c.Networks {
+ networks[k] = v
+ }
+
+ envs := make(map[string]string)
+ if diags := c.Envs.ElementsAs(ctx, &envs, false); diags.HasError() {
+ resp.Diagnostics.AddError("invalid resource input", fmt.Sprintf("invalid envs input: %s", diags.Errors()))
+ return
+ }
+ cfg.Env = envs
+ }
}
- mounts := []harnesses.ContainerConfigMount{}
- for _, mount := range data.Mounts {
+ for _, mount := range mounts {
src, err := filepath.Abs(mount.Source.ValueString())
if err != nil {
- return harnesses.ContainerConfig{}, err
+ resp.Diagnostics.AddError("invalid resource input", fmt.Sprintf("invalid mount source: %s", err))
+ return
}
- mounts = append(mounts, harnesses.ContainerConfigMount{
+ cfg.Mounts = append(cfg.Mounts, container.ConfigMount{
Source: src,
Destination: mount.Destination.ValueString(),
})
}
- cfg.Mounts = mounts
- envs := map[string]string{}
+ for _, network := range networks {
+ cfg.Networks = append(cfg.Networks, network.Name.ValueString())
+ }
+
+ envs := make(map[string]string)
if diags := data.Envs.ElementsAs(ctx, &envs, false); diags.HasError() {
- return harnesses.ContainerConfig{}, fmt.Errorf("invalid envs input: %w", diags.Errors())
+ resp.Diagnostics.AddError("invalid resource input", fmt.Sprintf("invalid envs input: %s", diags.Errors()))
+ return
+ }
+ for k, v := range envs {
+ cfg.Env[k] = v
+ }
+
+ harness, err := container.New(ctx, r.id, cfg)
+ if err != nil {
+ resp.Diagnostics.AddError("invalid provider data", "...")
+ return
}
- cfg.Env = envs
+ r.store.harnesses.Set(r.id, harness)
- return cfg, nil
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *HarnessContainerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
diff --git a/internal/provider/harness_container_resource_test.go b/internal/provider/harness_container_resource_test.go
new file mode 100644
index 0000000..8e34649
--- /dev/null
+++ b/internal/provider/harness_container_resource_test.go
@@ -0,0 +1,72 @@
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccHarnessContainerResource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create testing
+ {
+ Config: `
+ resource "imagetest_harness_container" "test" {}
+ resource "imagetest_harness_teardown" "test" { harness = imagetest_harness_container.test.id }
+ resource "imagetest_feature" "test" {
+ name = "Simple container based test"
+ description = "Test that we can spin up a container and run some steps"
+ harness = imagetest_harness_container.test.id
+ steps = [
+ {
+ name = "Echo"
+ cmd = "echo hello world"
+ },
+ ]
+ }
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(),
+ },
+ },
+ })
+}
+
+func TestAccHarnessContainerResourceProvider(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: `
+provider "imagetest" {
+ harnesses = {
+ container = {
+ envs = {
+ foo = "foo"
+ baz = "override"
+ }
+ }
+ }
+}
+resource "imagetest_harness_container" "test" { envs = { "bar" = "bar", "baz" = "baz" }}
+resource "imagetest_harness_teardown" "test" { harness = imagetest_harness_container.test.id }
+resource "imagetest_feature" "test" {
+ name = "Simple container based test"
+ description = "Test that we can spin up a container and run some steps"
+ harness = imagetest_harness_container.test.id
+ steps = [
+ {
+ name = "Echo"
+ cmd = "echo $foo $bar $baz | diff - <(echo foo bar baz) > /dev/null"
+ },
+ ]
+}
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(),
+ },
+ },
+ })
+}
diff --git a/internal/provider/harness_k3s_resource.go b/internal/provider/harness_k3s_resource.go
index f4cf1da..5cb2c62 100644
--- a/internal/provider/harness_k3s_resource.go
+++ b/internal/provider/harness_k3s_resource.go
@@ -3,13 +3,14 @@ package provider
import (
"context"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses"
+ "github.com/chainguard-dev/terraform-provider-imagetest/internal/harnesses/k3s"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
// Ensure provider defined types fully satisfy framework interfaces.
@@ -31,11 +32,34 @@ type HarnessK3sResource struct {
// HarnessK3sResourceModel describes the resource data model.
type HarnessK3sResourceModel struct {
- Id types.String `tfsdk:"id"`
- Image types.String `tfsdk:"image"`
- DisableCni types.Bool `tfsdk:"disable_cni"`
- DisableTraefik types.Bool `tfsdk:"disable_traefik"`
- DisableMetricsServer types.Bool `tfsdk:"disable_metrics_server"`
+ Id types.String `tfsdk:"id"`
+ Image types.String `tfsdk:"image"`
+ DisableCni types.Bool `tfsdk:"disable_cni"`
+ DisableTraefik types.Bool `tfsdk:"disable_traefik"`
+ DisableMetricsServer types.Bool `tfsdk:"disable_metrics_server"`
+ Registries map[string]RegistryResourceModel `tfsdk:"registries"`
+}
+
+type RegistryResourceModel struct {
+ Auth *RegistryResourceAuthModel `tfsdk:"auth"`
+ Tls *RegistryResourceTlsModel `tfsdk:"tls"`
+ Mirror *RegistryResourceMirrorModel `tfsdk:"mirror"`
+}
+
+type RegistryResourceAuthModel struct {
+ Username types.String `tfsdk:"username"`
+ Password types.String `tfsdk:"password"`
+ Auth types.String `tfsdk:"auth"`
+}
+
+type RegistryResourceTlsModel struct {
+ CertFile types.String `tfsdk:"cert_file"`
+ KeyFile types.String `tfsdk:"key_file"`
+ CaFile types.String `tfsdk:"ca_file"`
+}
+
+type RegistryResourceMirrorModel struct {
+ Endpoints types.List `tfsdk:"endpoints"`
}
func (r *HarnessK3sResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
@@ -74,6 +98,52 @@ func (r *HarnessK3sResource) Schema(ctx context.Context, req resource.SchemaRequ
Computed: true,
Default: stringdefault.StaticString("cgr.dev/chainguard/k3s:latest"),
},
+ "registries": schema.MapNestedAttribute{
+ Description: "A map of registries containing configuration for optional auth, tls, and mirror configuration.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "auth": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "username": schema.StringAttribute{
+ Optional: true,
+ },
+ "password": schema.StringAttribute{
+ Optional: true,
+ Sensitive: true,
+ },
+ "auth": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "tls": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "cert_file": schema.StringAttribute{
+ Optional: true,
+ },
+ "key_file": schema.StringAttribute{
+ Optional: true,
+ },
+ "ca_file": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "mirror": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "endpoints": schema.ListAttribute{
+ ElementType: basetypes.StringType{},
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
},
}
}
@@ -106,7 +176,43 @@ func (r *HarnessK3sResource) Create(ctx context.Context, req resource.CreateRequ
data.Id = types.StringValue(r.id)
- harness, err := harnesses.NewK3s(r.id, r.k3sConfig(ctx, data))
+ kopts := []k3s.Option{
+ k3s.WithImage(data.Image.ValueString()),
+ }
+
+ registries := make(map[string]RegistryResourceModel)
+ if data.Registries != nil {
+ registries = data.Registries
+ }
+
+ if r.store.providerResourceData.Harnesses != nil {
+ if pc := r.store.providerResourceData.Harnesses.K3s; pc != nil {
+ for k, v := range pc.Registries {
+ registries[k] = v
+ }
+ }
+ }
+
+ for rname, rdata := range registries {
+ if rdata.Auth != nil {
+ if rdata.Auth.Auth.IsNull() && rdata.Auth.Password.IsNull() && rdata.Auth.Username.IsNull() {
+ kopts = append(kopts, k3s.WithAuthFromKeychain(rname))
+ } else {
+ kopts = append(kopts, k3s.WithAuthFromStatic(rname, rdata.Auth.Username.ValueString(), rdata.Auth.Password.ValueString(), rdata.Auth.Auth.ValueString()))
+ }
+ }
+
+ if rdata.Mirror != nil {
+ endpoints := []string{}
+ if diags := rdata.Mirror.Endpoints.ElementsAs(ctx, &endpoints, false); diags.HasError() {
+ resp.Diagnostics.AddError("failed to convert mirror endpoints", "...")
+ return
+ }
+ kopts = append(kopts, k3s.WithRegistryMirror(rname, endpoints...))
+ }
+ }
+
+ harness, err := k3s.New(r.id, kopts...)
if err != nil {
resp.Diagnostics.AddError("failed to initialize k3s harness", err.Error())
return
@@ -118,15 +224,6 @@ func (r *HarnessK3sResource) Create(ctx context.Context, req resource.CreateRequ
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
-func (r *HarnessK3sResource) k3sConfig(ctx context.Context, data HarnessK3sResourceModel) harnesses.K3sConfig {
- return harnesses.K3sConfig{
- Image: data.Image.ValueString(),
- Cni: !data.DisableCni.ValueBool(),
- MetricsServer: !data.DisableMetricsServer.ValueBool(),
- Traefik: !data.DisableTraefik.ValueBool(),
- }
-}
-
func (r *HarnessK3sResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data HarnessK3sResourceModel
diff --git a/internal/provider/harness_k3s_resource_test.go b/internal/provider/harness_k3s_resource_test.go
new file mode 100644
index 0000000..ca3a4a4
--- /dev/null
+++ b/internal/provider/harness_k3s_resource_test.go
@@ -0,0 +1,35 @@
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccHarnessK3sResource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create testing
+ {
+ Config: `
+resource "imagetest_harness_k3s" "test" {}
+resource "imagetest_harness_teardown" "test" { harness = imagetest_harness_k3s.test.id }
+resource "imagetest_feature" "test" {
+ name = "Simple k3s based test"
+ description = "Test that we can spin up a k3s cluster and run some steps"
+ harness = imagetest_harness_k3s.test.id
+ steps = [
+ {
+ name = "Access cluster"
+ cmd = "kubectl get po -A"
+ },
+ ]
+}
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(),
+ },
+ },
+ })
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 4d36c72..4d27875 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -3,11 +3,12 @@ package provider
import (
"context"
- "github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
var _ provider.Provider = &ImageTestProvider{}
@@ -22,16 +23,128 @@ type ImageTestProvider struct {
}
// ImageTestProviderModel describes the provider data model.
-type ImageTestProviderModel struct{}
+type ImageTestProviderModel struct {
+ Harnesses *ImageTestProviderHarnessModel `tfsdk:"harnesses"`
+}
+
+type ImageTestProviderHarnessModel struct {
+ Container *ProviderHarnessContainerModel `tfsdk:"container"`
+ K3s *ProviderHarnessK3sModel `tfsdk:"k3s"`
+}
+
+type ProviderHarnessContainerModel struct {
+ Networks map[string]ContainerResourceModelNetwork `tfsdk:"networks"`
+ Envs types.Map `tfsdk:"envs"`
+ Mounts []ContainerResourceMountModel `tfsdk:"mounts"`
+}
+
+type ProviderHarnessK3sModel struct {
+ Registries map[string]RegistryResourceModel `tfsdk:"registries"`
+}
func (p *ImageTestProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
- resp.TypeName = types.ProviderName
+ resp.TypeName = "imagetest"
resp.Version = p.version
}
func (p *ImageTestProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
- Attributes: map[string]schema.Attribute{},
+ Attributes: map[string]schema.Attribute{
+ "harnesses": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "container": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "envs": schema.MapAttribute{
+ Description: "Environment variables to set on the container.",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "networks": schema.MapNestedAttribute{
+ Description: "A map of existing networks to attach the container to.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "The name of the existing network to attach the container to.",
+ Required: true,
+ },
+ },
+ },
+ },
+ "mounts": schema.ListNestedAttribute{
+ Description: "The list of mounts to create on the container.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "source": schema.StringAttribute{
+ Description: "The relative or absolute path on the host to the source directory to mount.",
+ Required: true,
+ },
+ "destination": schema.StringAttribute{
+ Description: "The absolute path on the container to mount the source directory to.",
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ "k3s": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "registries": schema.MapNestedAttribute{
+ Description: "A map of registries containing configuration for optional auth, tls, and mirror configuration.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "auth": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "username": schema.StringAttribute{
+ Optional: true,
+ },
+ "password": schema.StringAttribute{
+ Optional: true,
+ Sensitive: true,
+ },
+ "auth": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "tls": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "cert_file": schema.StringAttribute{
+ Optional: true,
+ },
+ "key_file": schema.StringAttribute{
+ Optional: true,
+ },
+ "ca_file": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ "mirror": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "endpoints": schema.ListAttribute{
+ ElementType: basetypes.StringType{},
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}
}
@@ -44,6 +157,8 @@ func (p *ImageTestProvider) Configure(ctx context.Context, req provider.Configur
return
}
+ p.store.providerResourceData = data
+
resp.DataSourceData = p.store
resp.ResourceData = p.store
}
diff --git a/internal/provider/store.go b/internal/provider/store.go
index 4a7975d..71a86d3 100644
--- a/internal/provider/store.go
+++ b/internal/provider/store.go
@@ -2,12 +2,11 @@ package provider
import (
"os"
- "strings"
"sync"
"github.com/chainguard-dev/terraform-provider-imagetest/internal/environment"
"github.com/chainguard-dev/terraform-provider-imagetest/internal/types"
- "github.com/google/uuid"
+ petname "github.com/dustinkirkland/golang-petname"
)
const RuntimeLabelEnv = "IMAGETEST_LABELS"
@@ -20,6 +19,10 @@ type ProviderStore struct {
portAllocator *environment.PortAllocator
// harnesses stores a map of the available harnesses, keyed by their ID.
harnesses *smap[string, types.Harness]
+
+ // providerResourceData stores the data for the provider resource.
+ // TODO: This shouldn't need to be like this
+ providerResourceData ImageTestProviderModel
}
func NewProviderStore() *ProviderStore {
@@ -33,7 +36,8 @@ func NewProviderStore() *ProviderStore {
}
func (s *ProviderStore) RandomID() string {
- return uuid.NewString()
+ // h/t dustin
+ return petname.Generate(2, "-")
}
func newSmap[K comparable, V any]() *smap[K, V] {
@@ -43,7 +47,7 @@ func newSmap[K comparable, V any]() *smap[K, V] {
}
}
-// smap is a generic thread-safe map implementation
+// smap is a generic thread-safe map implementation.
type smap[K comparable, V any] struct {
store map[K]V
mu sync.Mutex
@@ -67,28 +71,3 @@ func (m *smap[K, V]) Delete(key K) {
defer m.mu.Unlock()
delete(m.store, key)
}
-
-type Labels map[string]string
-
-func newLabels() Labels {
- ls := make(Labels)
- for _, label := range strings.Split(os.Getenv("IMAGETEST_LABELS"), ",") {
- kv := strings.SplitN(label, "=", 2)
- if len(kv) != 2 {
- continue
- }
- ls[kv[0]] = kv[1]
- }
- return ls
-}
-
-// Match takes a map of labels and returns true if all of the given labels are
-// present in the map
-func (ls Labels) Match(matches map[string]string) bool {
- for k, v := range ls {
- if matches[k] != v {
- return false
- }
- }
- return true
-}
diff --git a/internal/types/types.go b/internal/types/types.go
index c05ada5..0b0cf35 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -4,8 +4,6 @@ import (
"context"
)
-const ProviderName = "imagetest"
-
type Environment interface {
// Test executes a feature(set) against the environment.
Test(context.Context, Feature) error