Skip to content

Commit 63b6377

Browse files
authored
Merge pull request #1769 from gruntwork-io/ci/gcp-github-actions
ci: add GCP tests workflow for GitHub Actions
2 parents b95109a + 1a24b25 commit 63b6377

16 files changed

Lines changed: 753 additions & 1432 deletions
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: GCP Integration Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'modules/gcp/**'
8+
- 'test/gcp/**'
9+
- 'go.mod'
10+
- 'go.sum'
11+
- '.github/workflows/gcp-integration-tests.yml'
12+
pull_request:
13+
paths:
14+
- 'modules/gcp/**'
15+
- 'test/gcp/**'
16+
- 'go.mod'
17+
- 'go.sum'
18+
- '.github/workflows/gcp-integration-tests.yml'
19+
workflow_dispatch:
20+
21+
jobs:
22+
gcp-integration-tests:
23+
name: GCP Integration Tests
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 60
26+
permissions:
27+
contents: read
28+
id-token: write
29+
30+
env:
31+
# GOOGLE_CLOUD_PROJECT is the env var that the GCP provider helper
32+
# (gcp.GetGoogleProjectIDFromEnvVar) actually reads.
33+
GOOGLE_CLOUD_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
34+
# GOOGLE_COMPUTE_ZONE is optional; only used to set the gcloud CLI default zone.
35+
GOOGLE_COMPUTE_ZONE: ${{ vars.GOOGLE_COMPUTE_ZONE }}
36+
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Install mise
41+
uses: jdx/mise-action@v3
42+
with:
43+
version: 2025.12.10
44+
experimental: true
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
48+
- name: Go module cache
49+
uses: actions/cache@v4
50+
with:
51+
path: |
52+
~/go/pkg/mod
53+
~/.cache/go-build
54+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
55+
restore-keys: |
56+
${{ runner.os }}-go-
57+
58+
- name: Download Go modules
59+
run: go mod download
60+
61+
- name: Authenticate to Google Cloud
62+
uses: google-github-actions/auth@v2
63+
with:
64+
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER_ID }}
65+
service_account: ${{ secrets.GCP_SA_EMAIL }}
66+
audience: ${{ secrets.GCP_ALLOWED_AUDIENCES }}
67+
68+
- name: Set up gcloud CLI
69+
uses: google-github-actions/setup-gcloud@v2
70+
with:
71+
project_id: ${{ secrets.GCP_PROJECT_ID }}
72+
73+
- name: Configure gcloud defaults
74+
run: |
75+
gcloud --quiet config set project "$GOOGLE_CLOUD_PROJECT"
76+
if [ -n "$GOOGLE_COMPUTE_ZONE" ]; then
77+
gcloud --quiet config set compute/zone "$GOOGLE_COMPUTE_ZONE"
78+
fi
79+
80+
- name: Run GCP integration tests
81+
run: |
82+
mkdir -p /tmp/logs
83+
go test -v -count=1 -timeout 45m -tags gcp ./test/gcp/... 2>&1 | tee /tmp/logs/gcp-integration-tests.log
84+
85+
- name: Upload test logs
86+
if: always()
87+
uses: actions/upload-artifact@v4
88+
with:
89+
name: gcp-integration-test-logs
90+
path: /tmp/logs/
91+
retention-days: 14

.github/workflows/gcp-tests.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: GCP Unit Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'modules/gcp/**'
8+
- 'test/gcp/**'
9+
- 'go.mod'
10+
- 'go.sum'
11+
- '.github/workflows/gcp-tests.yml'
12+
pull_request:
13+
paths:
14+
- 'modules/gcp/**'
15+
- 'test/gcp/**'
16+
- 'go.mod'
17+
- 'go.sum'
18+
- '.github/workflows/gcp-tests.yml'
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
test:
26+
name: GCP Unit Tests
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 15
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Install mise
34+
uses: jdx/mise-action@v3
35+
with:
36+
version: 2025.12.10
37+
experimental: true
38+
env:
39+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
41+
- name: Go module cache
42+
uses: actions/cache@v4
43+
with:
44+
path: |
45+
~/go/pkg/mod
46+
~/.cache/go-build
47+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
48+
restore-keys: |
49+
${{ runner.os }}-go-
50+
51+
- name: Download Go modules
52+
run: go mod download
53+
54+
- name: Build GCP module
55+
run: go build ./modules/gcp/...
56+
57+
- name: Compile GCP integration tests
58+
run: go test -tags gcp -c -o /dev/null ./test/gcp/...
59+
60+
- name: Run GCP module tests
61+
run: |
62+
mkdir -p /tmp/logs
63+
go test -v -count=1 -timeout 10m ./modules/gcp/... 2>&1 | tee /tmp/logs/gcp-module-tests.log
64+
65+
- name: Upload test logs
66+
if: always()
67+
uses: actions/upload-artifact@v4
68+
with:
69+
name: gcp-unit-test-logs
70+
path: /tmp/logs/
71+
retention-days: 14

modules/gcp/cloudbuild_test.go

Lines changed: 85 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,111 @@
1-
//go:build gcp
2-
// +build gcp
3-
4-
// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.
5-
61
package gcp_test
72

83
import (
9-
"archive/tar"
10-
"bytes"
11-
"compress/gzip"
12-
"fmt"
13-
"strings"
4+
"context"
5+
"net"
146
"testing"
157

8+
cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2"
169
cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
1710
"github.com/gruntwork-io/terratest/modules/gcp"
18-
"github.com/gruntwork-io/terratest/modules/logger"
19-
"github.com/gruntwork-io/terratest/modules/random"
11+
"github.com/stretchr/testify/assert"
2012
"github.com/stretchr/testify/require"
13+
"google.golang.org/api/option"
14+
"google.golang.org/grpc"
15+
"google.golang.org/grpc/credentials/insecure"
16+
"google.golang.org/grpc/test/bufconn"
2117
)
2218

23-
func TestCreateBuild(t *testing.T) {
24-
t.Parallel()
25-
// This test performs the following steps:
26-
//
27-
// 1. Creates a tarball with a single Dockerfile
28-
// 2. Creates a GCS bucket
29-
// 3. Uploads the tarball to the GCS Bucket
30-
// 4. Triggers a build using the Cloud Build API
31-
// 5. Attempts to untag and delete all pushed Build images (best-effort cleanup)
32-
// 6. Deletes the GCS bucket
33-
34-
// Create and add some files to the archive.
35-
tarball := createSampleAppTarball(t)
36-
37-
// Create GCS bucket
38-
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
39-
id := random.UniqueID()
40-
gsBucketName := "cloud-build-terratest-" + strings.ToLower(id)
41-
sampleAppPath := "docker-example.tar.gz"
42-
imagePath := fmt.Sprintf("gcr.io/%s/test-image-%s", projectID, strings.ToLower(id))
43-
44-
logger.Default.Logf(t, "Random values selected Bucket Name = %s\n", gsBucketName)
45-
46-
gcp.CreateStorageBucket(t, projectID, gsBucketName, nil)
47-
defer gcp.DeleteStorageBucket(t, gsBucketName)
48-
49-
// Write the compressed archive to the storage bucket
50-
objectURL := gcp.WriteBucketObject(t, gsBucketName, sampleAppPath, tarball, "application/gzip")
51-
logger.Default.Logf(t, "Got URL: %s", objectURL)
52-
53-
// Create a new build
54-
build := &cloudbuildpb.Build{
55-
Source: &cloudbuildpb.Source{
56-
Source: &cloudbuildpb.Source_StorageSource{
57-
StorageSource: &cloudbuildpb.StorageSource{
58-
Bucket: gsBucketName,
59-
Object: sampleAppPath,
60-
},
61-
},
62-
},
63-
Steps: []*cloudbuildpb.BuildStep{{
64-
Name: "gcr.io/cloud-builders/docker",
65-
Args: []string{"build", "-t", imagePath, "."},
66-
}},
67-
Images: []string{imagePath},
68-
}
69-
70-
// CreateBuild blocks until the build is complete
71-
b := gcp.CreateBuild(t, projectID, build)
72-
73-
// Attempt to delete the pushed build images (best-effort cleanup).
74-
// Note: GCR (gcr.io) has been deprecated in favor of Artifact Registry.
75-
// The cleanup may fail due to permission changes, but this doesn't affect
76-
// the validity of the Cloud Build test itself.
77-
// We could just use the `b` struct above, but we want to explicitly test
78-
// the `GetBuild` method.
79-
b2 := gcp.GetBuild(t, projectID, b.GetId())
80-
for _, image := range b2.GetImages() {
81-
if err := gcp.DeleteGCRRepoE(t, image); err != nil {
82-
logger.Default.Logf(t, "Warning: Failed to delete image %s (this may be expected due to GCR deprecation): %v", image, err)
83-
}
84-
}
85-
86-
// Empty the storage bucket so we can delete it
87-
defer gcp.EmptyStorageBucket(t, gsBucketName)
19+
// fakeCloudBuildServer only implements the methods the terratest *WithClient helpers actually
20+
// call. CreateBuild (long-running op) is left to the build-tagged integration test.
21+
type fakeCloudBuildServer struct {
22+
cloudbuildpb.UnimplementedCloudBuildServer
23+
24+
getBuild func(*cloudbuildpb.GetBuildRequest) *cloudbuildpb.Build
25+
listBuilds func(*cloudbuildpb.ListBuildsRequest) *cloudbuildpb.ListBuildsResponse
26+
}
27+
28+
func (f *fakeCloudBuildServer) GetBuild(_ context.Context, req *cloudbuildpb.GetBuildRequest) (*cloudbuildpb.Build, error) {
29+
return f.getBuild(req), nil
8830
}
8931

90-
func createSampleAppTarball(t *testing.T) *bytes.Reader {
32+
func (f *fakeCloudBuildServer) ListBuilds(_ context.Context, req *cloudbuildpb.ListBuildsRequest) (*cloudbuildpb.ListBuildsResponse, error) {
33+
return f.listBuilds(req), nil
34+
}
35+
36+
// newFakeCloudBuildClient runs `srv` on an in-memory bufconn gRPC server and returns a client
37+
// pointed at it. No credentials, no network.
38+
func newFakeCloudBuildClient(t *testing.T, srv *fakeCloudBuildServer) *cloudbuild.Client {
9139
t.Helper()
9240

93-
var buf bytes.Buffer
41+
lis := bufconn.Listen(1024 * 1024)
42+
grpcServer := grpc.NewServer()
43+
cloudbuildpb.RegisterCloudBuildServer(grpcServer, srv)
9444

95-
tw := tar.NewWriter(&buf)
45+
go func() { _ = grpcServer.Serve(lis) }()
9646

97-
file := `FROM busybox:latest
98-
MAINTAINER Rob Morgan (rob@gruntwork.io)
99-
`
47+
t.Cleanup(func() { grpcServer.Stop(); _ = lis.Close() })
10048

101-
hdr := &tar.Header{
102-
Name: "Dockerfile",
103-
Mode: 0600,
104-
Size: int64(len(file)),
105-
}
49+
conn, err := grpc.NewClient("passthrough:///bufnet",
50+
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }),
51+
grpc.WithTransportCredentials(insecure.NewCredentials()),
52+
)
53+
require.NoError(t, err)
54+
t.Cleanup(func() { _ = conn.Close() })
10655

107-
err := tw.WriteHeader(hdr)
56+
client, err := cloudbuild.NewClient(context.Background(), option.WithGRPCConn(conn))
10857
require.NoError(t, err)
58+
t.Cleanup(func() { _ = client.Close() })
10959

110-
_, werr := tw.Write([]byte(file))
111-
require.NoError(t, werr)
60+
return client
61+
}
11262

113-
cerr := tw.Close()
114-
require.NoError(t, cerr)
63+
func TestGetBuildWithClient(t *testing.T) {
64+
t.Parallel()
11565

116-
// gzip the tar archive
117-
var zbuf bytes.Buffer
66+
client := newFakeCloudBuildClient(t, &fakeCloudBuildServer{
67+
getBuild: func(req *cloudbuildpb.GetBuildRequest) *cloudbuildpb.Build {
68+
return &cloudbuildpb.Build{Id: req.GetId(), ProjectId: req.GetProjectId(), Status: cloudbuildpb.Build_SUCCESS}
69+
},
70+
})
11871

119-
gzw := gzip.NewWriter(&zbuf)
120-
_, gwerr := gzw.Write(buf.Bytes())
121-
require.NoError(t, gwerr)
72+
got, err := gcp.GetBuildWithClient(context.Background(), client, "p", "b1")
73+
require.NoError(t, err)
74+
assert.Equal(t, "b1", got.GetId())
75+
assert.Equal(t, cloudbuildpb.Build_SUCCESS, got.GetStatus())
76+
}
77+
78+
func TestGetBuildsWithClient(t *testing.T) {
79+
t.Parallel()
12280

123-
gcerr := gzw.Close()
124-
require.NoError(t, gcerr)
81+
client := newFakeCloudBuildClient(t, &fakeCloudBuildServer{
82+
listBuilds: func(req *cloudbuildpb.ListBuildsRequest) *cloudbuildpb.ListBuildsResponse {
83+
assert.Equal(t, "p", req.GetProjectId())
12584

126-
// return the compressed buffer
127-
return bytes.NewReader(zbuf.Bytes())
85+
return &cloudbuildpb.ListBuildsResponse{Builds: []*cloudbuildpb.Build{{Id: "a"}, {Id: "b"}}}
86+
},
87+
})
88+
89+
got, err := gcp.GetBuildsWithClient(context.Background(), client, "p")
90+
require.NoError(t, err)
91+
require.Len(t, got, 2)
92+
}
93+
94+
func TestGetBuildsForTriggerWithClient(t *testing.T) {
95+
t.Parallel()
96+
97+
client := newFakeCloudBuildClient(t, &fakeCloudBuildServer{
98+
listBuilds: func(_ *cloudbuildpb.ListBuildsRequest) *cloudbuildpb.ListBuildsResponse {
99+
return &cloudbuildpb.ListBuildsResponse{Builds: []*cloudbuildpb.Build{
100+
{Id: "a", BuildTriggerId: "match"},
101+
{Id: "b", BuildTriggerId: "other"},
102+
{Id: "c", BuildTriggerId: "match"},
103+
}}
104+
},
105+
})
106+
107+
got, err := gcp.GetBuildsForTriggerWithClient(context.Background(), client, "p", "match")
108+
require.NoError(t, err)
109+
require.Len(t, got, 2)
110+
assert.ElementsMatch(t, []string{"a", "c"}, []string{got[0].GetId(), got[1].GetId()})
128111
}

0 commit comments

Comments
 (0)