diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d31441f3..147d86d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,18 +71,17 @@ 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.*" - "1.7.*" - "1.8.*" - "1.9.*" + - "1.10.*" + - "1.11.*" + - "1.12.*" + - "1.13.*" + - "1.14.*" steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -118,7 +117,7 @@ jobs: - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 with: - terraform_version: "1.9.*" + terraform_version: "1.11.*" terraform_wrapper: false - name: Get dependencies diff --git a/README.md b/README.md index e79f8eb9..96a76a7c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The provider currently supports resources and data sources for: ## Requirements -- [Terraform](https://developer.hashicorp.com/terraform/downloads) >= 1.0 +- [Terraform](https://developer.hashicorp.com/terraform/downloads) >= 1.5 - [Go](https://golang.org/doc/install) >= 1.21 - [Coder](https://github.com/coder/coder) >= 2.10.1 diff --git a/integration/integration.go b/integration/integration.go index bb5e0cbe..8130979a 100644 --- a/integration/integration.go +++ b/integration/integration.go @@ -1,6 +1,7 @@ package integration import ( + "bytes" "context" "fmt" "io" @@ -13,7 +14,9 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/require" @@ -67,7 +70,6 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( option(&opts) } - // Env vars override user-selected options. if v, ok := os.LookupEnv("CODER_IMAGE"); ok { opts.image = v } @@ -83,6 +85,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( t.Skip("Skipping tests that require a license.") } + ref := opts.image + ":" + opts.version t.Logf("using coder image %s:%s", opts.image, opts.version) cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -90,21 +93,32 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( p := randomPort(t) t.Logf("random port is %d", p) - // Stand up a temporary Coder instance - puller, err := cli.ImagePull(ctx, opts.image+":"+opts.version, image.PullOptions{}) - require.NoError(t, err, "pull coder image") - defer func() { - if err := puller.Close(); err != nil { - t.Logf("error closing image puller: %v", err) + + // Give Coder an external PostgreSQL on a per-test network instead of its + // embedded one, which downloads a binary from Maven at startup (a flaky, + // rate-limited fetch that reds CI lanes). + netName := "terraform-provider-coderd-" + name + "-net" + net, err := cli.NetworkCreate(ctx, netName, network.CreateOptions{}) + require.NoError(t, err, "create test network") + t.Cleanup(func() { + // t.Context() is canceled before t.Cleanup callbacks run, so use a + // fresh context or the removal is a no-op and the network leaks. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := cli.NetworkRemove(ctx, net.ID); err != nil { + t.Logf("error removing network %s: %v", net.ID, err) } - }() - _, err = io.Copy(os.Stderr, puller) - require.NoError(t, err, "pull coder image") + }) + startPostgres(ctx, t, cli, netName) + + // Stand up a temporary Coder instance. + pullImage(ctx, t, cli, ref) env := []string{ "CODER_HTTP_ADDRESS=0.0.0.0:3000", // Listen on all interfaces inside the container. "CODER_ACCESS_URL=http://localhost:3000", // Avoid creating try.coder.app URLs. "CODER_TELEMETRY_ENABLE=false", // Avoid creating noise. + "CODER_PG_CONNECTION_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable", } if !opts.enableRateLimits { env = append(env, "CODER_DANGEROUS_DISABLE_RATE_LIMITS=true") @@ -114,7 +128,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( } ctr, err := cli.ContainerCreate(ctx, &container.Config{ - Image: opts.image + ":" + opts.version, + Image: ref, Env: env, Labels: map[string]string{}, ExposedPorts: map[nat.Port]struct{}{nat.Port("3000/tcp"): {}}, @@ -122,17 +136,28 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( PortBindings: map[nat.Port][]nat.PortBinding{ nat.Port("3000/tcp"): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", p)}}, }, - }, nil, nil, "terraform-provider-coderd-"+name) + }, &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{netName: {}}, + }, nil, "terraform-provider-coderd-"+name) require.NoError(t, err, "create test deployment") t.Logf("created container %s\n", ctr.ID) t.Cleanup(func() { // Make sure we clean up after ourselves. // TODO: also have this execute if you Ctrl+C! t.Logf("stopping container %s\n", ctr.ID) + // t.Context() is canceled before t.Cleanup callbacks run, so use a + // fresh context or the removal is a no-op and the container leaks. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() _ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{ Force: true, }) }) + t.Cleanup(func() { + if t.Failed() { + dumpContainerLogs(t, cli, ctr.ID) + } + }) err = cli.ContainerStart(ctx, ctr.ID, container.StartOptions{}) require.NoError(t, err, "start container") @@ -149,7 +174,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( coderURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", p)) require.NoError(t, err, "parse coder URL") client := codersdk.New(coderURL) - // Wait for container to come up + // Wait for the container to come up. require.Eventually(t, func() bool { _, err := client.BuildInfo(ctx) if err != nil { @@ -178,6 +203,70 @@ func StartCoder(ctx context.Context, t *testing.T, name string, options ...func( return client } +// pullImage pulls a Docker image, streaming progress to stderr. +func pullImage(ctx context.Context, t *testing.T, cli *client.Client, ref string) { + t.Helper() + reader, err := cli.ImagePull(ctx, ref, image.PullOptions{}) + require.NoError(t, err, "pull image %s", ref) + defer func() { + if err := reader.Close(); err != nil { + t.Logf("error closing image puller: %v", err) + } + }() + _, err = io.Copy(os.Stderr, reader) + require.NoError(t, err, "read image pull output for %s", ref) +} + +// startPostgres runs a throwaway PostgreSQL that Coder connects to instead of +// starting its embedded one. It's reachable at hostname "postgres" on netName. +// Coder retries its database connection on startup, so we don't wait for it. +func startPostgres(ctx context.Context, t *testing.T, cli *client.Client, netName string) { + t.Helper() + const ref = "us-docker.pkg.dev/coder-v2-images-public/public/postgres:17" + pullImage(ctx, t, cli, ref) + ctr, err := cli.ContainerCreate(ctx, &container.Config{ + Image: ref, + Env: []string{"POSTGRES_PASSWORD=postgres"}, + }, &container.HostConfig{}, &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + netName: {Aliases: []string{"postgres"}}, + }, + }, nil, "") + require.NoError(t, err, "create postgres container") + t.Cleanup(func() { + // t.Context() is canceled before t.Cleanup callbacks run, so use a + // fresh context or the removal is a no-op and the container leaks. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + _ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{Force: true}) + }) + require.NoError(t, cli.ContainerStart(ctx, ctr.ID, container.StartOptions{}), "start postgres container") +} + +func dumpContainerLogs(t *testing.T, cli *client.Client, containerID string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + logs, err := cli.ContainerLogs(ctx, containerID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + if err != nil { + t.Logf("failed to fetch logs for container %s: %v", containerID, err) + return + } + defer func() { + if err := logs.Close(); err != nil { + t.Logf("error closing container log reader: %v", err) + } + }() + var buf bytes.Buffer + if _, err := stdcopy.StdCopy(&buf, &buf, logs); err != nil { + t.Logf("failed to read logs for container %s: %v", containerID, err) + return + } + t.Logf("=== coder container %s logs ===\n%s=== end coder container logs ===", containerID, buf.String()) +} + // randomPort is a helper function to find a free random port. // Note that the OS may reallocate the port very quickly, so // this is not _guaranteed_. diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go index 2651ab31..f74d1883 100644 --- a/internal/provider/template_resource_test.go +++ b/internal/provider/template_resource_test.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/tfversion" cp "github.com/otiai10/copy" "github.com/stretchr/testify/require" @@ -1142,14 +1141,8 @@ resource "coderd_template" "test" { cfg2 := fmt.Sprintf(cfg, client.URL.String(), client.SessionToken(), "yes", exTemplateOne) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IsUnitTest: true, - // Terraform 1.0 panics while formatting plans that contain marked nested - // values in this scenario ("value is marked, so must be unmarked first"). - // The provider regression is still covered on Terraform 1.1+. - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_1_0), - }, + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: true, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "random": {