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